Google發布,玩轉ShapeableImageView,告別第三方庫

前言

做過安卓開發的都知道,安卓的UI開發耗時耗力,實現不規則圖片效果,如老生常談的圓角、圓形圖片,要么引入第三方控件,要么自定義ImageView,第三方控件不一定滿足,而自定義ImageView對開發者有一定的要求且花時間。Google在去年發布的Android Material 組件 (MDC-Android) 1.2.0,提供了豐富的控件,有助于提高UI開發效率,今天的主角ShapeableImageView正式其中一員,類似的還有MaterialButton。

看下效果:


先來看下ShapeableImageView是什么

從類繼承關系看出,ShapeableImageView只不過是ImageView的一個子類,但是可以輕松實現效果圖中的各種樣式。

xml屬性

屬性名 作用
shapeAppearance 形狀外觀樣式,引用 style 樣式
shapeAppearanceOverlay 外觀疊加樣式,引用 style 樣式
strokeWidth 描邊寬度
strokeColor 描邊顏色

使用

引入material包

implementation 'com.google.android.material:material:1.2.1'

常規使用

<com.google.android.material.imageview.ShapeableImageView
     android:id="@+id/image"
     android:layout_width="110dp"
     android:layout_height="110dp"
     android:padding="1dp"
     android:src="@drawable/head"
/>

跟ImageView效果一樣。

各種花俏樣式

1、圓角圖片

        <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image1"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/roundedCornerStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 圓角圖片 -->
    <style name="roundedCornerStyle">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSize">8dp</item>
    </style>

2、圓形圖片

 <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image2"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/circleStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 圓形圖片 -->
    <style name="circleStyle">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSize">50%</item>
    </style>

3、切角圖片

 <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image3"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/cutCornerStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 切角圖片 -->
    <style name="cutCornerStyle">
        <item name="cornerFamily">cut</item>
        <item name="cornerSize">12dp</item>
    </style>

4、菱形圖片

  <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image4"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/diamondStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 菱形圖片 -->
    <style name="diamondStyle">
        <item name="cornerFamily">cut</item>
        <item name="cornerSize">50%</item>
    </style>

5、右上角圓角圖片

 <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image5"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/topRightCornerStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 右上角圓角圖片 -->
    <style name="topRightCornerStyle">
        <item name="cornerFamilyTopRight">rounded</item>
        <item name="cornerSizeTopRight">50dp</item>
    </style>

6、小雞蛋圖片

   <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image6"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/eggStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 小雞蛋圖片 -->
    <style name="eggStyle">
        <item name="cornerFamilyTopRight">rounded</item>
        <item name="cornerSizeTopRight">50dp</item>
        <item name="cornerSizeTopLeft">50dp</item>
        <item name="cornerFamilyTopLeft">rounded</item>
    </style>

7、組合弧度圖片效果

    <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image7"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/comCornerStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 組合弧度圖片效果 -->
    <style name="comCornerStyle">
        <item name="cornerFamily">rounded</item>
        <item name="cornerSizeTopRight">50%</item>
        <item name="cornerSizeBottomLeft">50%</item>
    </style>

8、 小 Tips

 <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image8"
            android:layout_width="110dp"
            android:layout_height="50dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/tipsCornerStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

    <!-- 小 Tips -->
    <style name="tipsCornerStyle">
        <item name="cornerFamilyTopLeft">rounded</item>
        <item name="cornerSizeTopLeft">50%</item>
        <item name="cornerFamilyBottomLeft">rounded</item>
        <item name="cornerSizeBottomLeft">50%</item>
        <item name="cornerFamilyTopRight">cut</item>
        <item name="cornerSizeTopRight">50%</item>
        <item name="cornerFamilyBottomRight">cut</item>
        <item name="cornerSizeBottomRight">50%</item>
    </style>

9、扇形圖片

 <com.google.android.material.imageview.ShapeableImageView
            android:id="@+id/image9"
            android:layout_width="110dp"
            android:layout_height="110dp"
            android:padding="1dp"
            android:src="@drawable/head"
            app:shapeAppearance="@style/fanStyle"
            app:strokeColor="@android:color/holo_blue_bright"
            app:strokeWidth="2dp"/>

對應的style:

  <!-- 扇形 -->
    <style name="fanStyle">
        <item name="cornerFamilyBottomLeft">rounded</item>
        <item name="cornerFamilyBottomRight">rounded</item>
        <item name="cornerFamilyTopLeft">rounded</item>
        <item name="cornerFamilyTopRight">rounded</item>
        <item name="cornerSizeBottomLeft">0dp</item>
        <item name="cornerSizeBottomRight">0dp</item>
        <item name="cornerSizeTopLeft">0%</item>
        <item name="cornerSizeTopRight">100%</item>
    </style>

通過源碼學知識

從前面應用可以發現,通過定義ShapeableImageView的shapeAppearance屬性style值,可以實現各種不同的樣式,而style有哪些的屬性,分別表示什么,定義了這些style實現這些樣式效果的原理是什么,帶著這些疑問閱讀源碼。

 public ShapeableImageView(Context context, @Nullable AttributeSet attrs, int defStyle) {
    super(wrap(context, attrs, defStyle, DEF_STYLE_RES), attrs, defStyle);
    // Ensure we are using the correctly themed context rather than the context that was passed in.
    context = getContext();

    clearPaint = new Paint();
    clearPaint.setAntiAlias(true);
    clearPaint.setColor(Color.WHITE);
    clearPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT));
    destination = new RectF();
    maskRect = new RectF();
    maskPath = new Path();
    TypedArray attributes =
        context.obtainStyledAttributes(
            attrs, R.styleable.ShapeableImageView, defStyle, DEF_STYLE_RES);

    strokeColor =
        MaterialResources.getColorStateList(
            context, attributes, R.styleable.ShapeableImageView_strokeColor);

    strokeWidth = attributes.getDimensionPixelSize(R.styleable.ShapeableImageView_strokeWidth, 0);

    borderPaint = new Paint();
    borderPaint.setStyle(Style.STROKE);
    borderPaint.setAntiAlias(true);
    shapeAppearanceModel =
        ShapeAppearanceModel.builder(context, attrs, defStyle, DEF_STYLE_RES).build();
    shadowDrawable = new MaterialShapeDrawable(shapeAppearanceModel);
    if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
      setOutlineProvider(new OutlineProvider());
    }
  }

在構造方法中有兩行核心代碼:

   shapeAppearanceModel =
        ShapeAppearanceModel.builder(context, attrs, defStyle, DEF_STYLE_RES).build();
    shadowDrawable = new MaterialShapeDrawable(shapeAppearanceModel);

可以看出,style的屬性由ShapeAppearanceModel來管理,得到style屬性后構造MaterialShapeDrawable對象,由MaterialShapeDrawable繪制形狀。

設置邊和角的屬性

通過 R 文件可以查看當前 ShapeAppearanceModel 具有的屬性:

    <declare-styleable name="ShapeAppearance">
      <!-- Corner size to be used in the ShapeAppearance. All corners default to this value -->
      <attr format="dimension|fraction" name="cornerSize"/>
      <!-- Top left corner size to be used in the ShapeAppearance. -->
      <attr format="dimension|fraction" name="cornerSizeTopLeft"/>
      <!-- Top right corner size to be used in the ShapeAppearance. -->
      <attr format="dimension|fraction" name="cornerSizeTopRight"/>
      <!-- Bottom right corner size to be used in the ShapeAppearance. -->
      <attr format="dimension|fraction" name="cornerSizeBottomRight"/>
      <!-- Bottom left corner size to be used in the ShapeAppearance. -->
      <attr format="dimension|fraction" name="cornerSizeBottomLeft"/>

      <!-- Corner family to be used in the ShapeAppearance. All corners default to this value -->
      <attr format="enum" name="cornerFamily">
        <enum name="rounded" value="0"/>
        <enum name="cut" value="1"/>
      </attr>
      <!-- Top left corner family to be used in the ShapeAppearance. -->
      <attr format="enum" name="cornerFamilyTopLeft">
        <enum name="rounded" value="0"/>
        <enum name="cut" value="1"/>
      </attr>
      <!-- Top right corner family to be used in the ShapeAppearance. -->
      <attr format="enum" name="cornerFamilyTopRight">
        <enum name="rounded" value="0"/>
        <enum name="cut" value="1"/>
      </attr>
      <!-- Bottom right corner family to be used in the ShapeAppearance. -->
      <attr format="enum" name="cornerFamilyBottomRight">
        <enum name="rounded" value="0"/>
        <enum name="cut" value="1"/>
      </attr>
      <!-- Bottom left corner family to be used in the ShapeAppearance. -->
      <attr format="enum" name="cornerFamilyBottomLeft">
        <enum name="rounded" value="0"/>
        <enum name="cut" value="1"/>
      </attr>
    </declare-styleable>
      <declare-styleable name="ShapeableImageView">
      <attr name="strokeWidth"/>
      <attr name="strokeColor"/>

      <!-- Shape appearance style reference for ShapeableImageView. Attribute declaration is in the
           shape package. -->
      <attr name="shapeAppearance"/>
      <!-- Shape appearance overlay style reference for ShapeableImageView. To be used to augment
           attributes declared in the shapeAppearance. Attribute declaration is in the shape package.
           -->
      <attr name="shapeAppearanceOverlay"/>
    </declare-styleable>

可以看出,通過ShapeAppearanceModel 可以定義各種邊和角的屬性。

繪制圖形

自定義不規則圖片的一般的做法是重寫ImageView的onDraw方法,處理邊角則使用\color{red}{PorterDuffXfermode}。何為PorterDuffXfermode?

\color{red}{android.graphics.PorterDuffXfermode}繼承自android.graphics.Xfermode。在用Android中的Canvas進行繪圖時,可以通過使用PorterDuffXfermode將所繪制的圖形的像素與Canvas中對應位置的像素按照一定規則進行混合,形成新的像素值,從而更新Canvas中最終的像素顏色值,這樣會創建很多有趣的效果。

使用方法也必將簡單,將其作為參數傳給Paint.setXfermode(Xfermode xfermode)方法,這樣在用該畫筆paint進行繪圖時,Android就會使用傳入的PorterDuffXfermode,如果不想再使用Xfermode,那么可以執行Paint.setXfermode(null)。

@Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //設置背景色
        canvas.drawARGB(255, 139, 197, 186);

        int canvasWidth = canvas.getWidth();
        int r = canvasWidth / 3;
        //正常繪制黃色的圓形
        paint.setColor(0xFFFFCC44);
        canvas.drawCircle(r, r, r, paint);
        //使用CLEAR作為PorterDuffXfermode繪制藍色的矩形
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
        paint.setColor(0xFF66AAFF);
        canvas.drawRect(r, r, r * 2.7f, r * 2.7f, paint);
        //最后將畫筆去除Xfermode
        paint.setXfermode(null);
    }

效果如下:


而可以實現的混合效果非常多,如圖:


在ShapeableImageView的構造方法中可用看到一行:

  clearPaint.setXfermode(new PorterDuffXfermode(Mode.DST_OUT));

可知ShapeableImageView的原理也是使用PorterDuffXfermode將圖片和指定的圖形混合得到想要的不規則圖片。其核心代碼如下:

  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    canvas.drawPath(maskPath, clearPaint);
    drawStroke(canvas);
  }
  @Override
  protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
    super.onSizeChanged(width, height, oldWidth, oldHeight);
    updateShapeMask(width, height);
  }
 private void updateShapeMask(int width, int height) {
    destination.set(
        getPaddingLeft(), getPaddingTop(), width - getPaddingRight(), height - getPaddingBottom());
    pathProvider.calculatePath(shapeAppearanceModel, 1f /*interpolation*/, destination, path);
    // Remove path from rect to draw with clear paint.
    maskPath.rewind();
    maskPath.addPath(path);
    // Do not include padding to clip the background too.
    maskRect.set(0, 0, width, height);
    maskPath.addRect(maskRect, Direction.CCW);
  }

代碼比較容易看懂,從onDraw看到繪制的流程:
1、先調用父類ImageView的onDraw繪制基本圖片;
2、生成不規則的圖片,clearPaint設置了PorterDuffXfermode(Mode.DST_OUT),即去掉src圖片重疊部分,僅保留剩下部分,而maskPath正是由不規則圖形與矩形圖片邊框組成;
3、繪制邊界。

總結

上面只是分析了ShapeableImageView的核心代碼,很多細節沒有展開,ShapeableImageView提供了豐富的屬性,通過改變邊角的值的組合可以實現各種各樣的圖形,使用起來是非常方便的,值得推薦。

參考

ShapeableImageView官方文檔
Android中Canvas繪圖之PorterDuffXfermode使用及工作原理詳解

關注V: “碼農翻身記”,回復888,免費領取Android/Java高頻面試題解析、進階知識整理、圖解網絡、圖解操作系統等資料。關注后,你將不定期收到優質技術及職場干貨分享。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容