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:

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:

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:

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:

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:

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:

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:

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:

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 具有的属性:










  <!-- 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使用及工作原理详解:
https://blog.youkuaiyun.com/iispring/article/details/50472485

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值