1. 前言
Android提供的官方裁切图片的库ShapeableImageView,内部封装好了一些方法和属性,可以很方便的实现对图片的裁切,为什么要介绍这个控件,因为我觉得它实现功能的思路很有意思,很贴切现实。注意这里指的是图片,不是所有控件,因为ShapeableImageView是继承ImageView,而它裁切的操作其实是OutlineProvider

这些都是它已经封装好的,至于这个控件的基础用法可以参考juejin.cn/post/686937...
2. 自定义裁切图形
上面老哥的文章说是玩转ShapeableImageView,里面确实对ShapeableImageView的基础用法介绍得很清楚,而现实中确实使用这些方法也能实现大部分的需求,比如说裁切成圆形啊、圆角啊之类的。
但是如果仅仅是这样,如果仅仅会这样使用这个控件,你只会觉得这个控件很方便,而体现不出它的魅力所在。只有当你去自定义实现裁切的图形的时候,你才能感受到这个控件的真正魅力。
首先它的自定义可以用ShapeAppearanceModel来实现。ShapeAppearanceModel提供了4个方法,TopEdge、LeftEdge、BottomEdge和RightEdge。大致的代码是这样

(1)原理
OK,要搞清楚这个东西的用法之前,先要思考一件事情,如果让你实现一个复杂的图形,你会怎么做?根据路径是吧,就行自定义view的绘制一样,我们要绘制一个复杂的形状,就需要用Path去绘制路径
那么这里也是一样的,这4个方法都会提供一个shapePath对象,这个就是表示路径。
但是不同的是,注意,这是重点,如果你不先理解我的这个结论,你就很难搞懂这个东西是怎么用的。shapePath和Path不同,Path是绘制路径,shapePath是裁剪路径,这是什么意思呢?就是字面意思,你把这两个东西搬到现实生活中,path就是你拿一只笔在一张白纸上画东西,shapePath就是你拿一把剪刀去剪这张纸。
而TopEdge、LeftEdge、BottomEdge和RightEdge就是表示从上下左右4条边开始剪,一个是画,一个是剪,这很重要,接下来的操作就会体现出这个思路。
(2)位置
可以先介绍下位置点,这4个方法都要重写getEdgePath()方法,而getEdgePath中有4个参数,

其中shapePath就是上面介绍的裁剪路径,而length和center就是两个坐标点,终点和中心点,而再加上0是起点,所以我们一开始能得到3个点位。
按照我们正常的思路,你会觉得这个坐标系是这样的

所以这就是ShapeAppearanceModel的其中一个误区,我们可以写一个Demo
kotlin
val shapePathModel = ShapeAppearanceModel
.Builder()
.setTopEdge(object : EdgeTreatment() {
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
shapePath.lineTo(center, dip2px(10f).toFloat())
shapePath.lineTo(length, 0f)
}
}).setLeftEdge(object : EdgeTreatment() {
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
shapePath.lineTo(center, dip2px(10f).toFloat())
shapePath.lineTo(length, 0f)
}
}).setBottomEdge(object : EdgeTreatment() {
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
shapePath.lineTo(center, dip2px(10f).toFloat())
shapePath.lineTo(length, 0f)
}
}).setRightEdge(object : EdgeTreatment() {
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
shapePath.lineTo(center, dip2px(10f).toFloat())
shapePath.lineTo(length, 0f)
}
})
shapeView?.shapeAppearanceModel = shapePathModel.build()
最终得到的效果

是不是很不科学,因为我这4个方法的shapePath都写的是

但是按我们正常的思考方式不应该是这样,比如顶部裁切是用这个代码没错,从(0,0)剪到(x/2,10),再剪到(x,0),就是顶部剪掉的那个凹槽。
但是左边,我们正常思维应该是从(0,y)剪到(10, y/2),再剪到(0,0),而实际中的代码却是和顶部的一样。
所以可以简单理解成,你剪的每个方向,都要先旋转到top再开始剪,剪完之后再旋转回来。这里就是我觉得很贴切生活的一个地方,像我们剪纸,你想想你用你右手剪左边是怎么剪,是不是先把纸转个方向在剪,而不是拧巴你的右手直接去反手剪
所以上面的Demo可以看成,每个方向都要旋转到top这个方向去剪

有的人设置路径觉得最终的效果不是他心里预期的,可能就是没有考虑过这个旋转的操作。
(3)裁剪特殊图片
上面也说了,主要是通过shapePath去实现一个路径,但是对于比较复杂的路径,其实不好去从每个方向开工,比如说我要剪一个圆角五边形,你就不好从4个方向去设计怎么剪,如果我剪上面的图形,或者菱形这种,从4个方向去剪还是比较方便的,但是5边行你怎么从4个方向去规划怎么剪,当然也能做,就是麻烦,所以我自己做的话就是只从top一个方向去剪。
最终的效果就是这样

然后看看我的代码
scss
val shapePathModel = ShapeAppearanceModel
.Builder()
.setTopEdge(object : EdgeTreatment() {
override fun getEdgePath(
length: Float,
center: Float,
interpolation: Float,
shapePath: ShapePath
) {
shapePath.lineTo(paX1, 0f)
shapePath.lineTo(paX1, paY1)
shapePath.quadToPoint(
rdX1,
rdY1,
paX2,
paY2
)
shapePath.lineTo(pbX1, pbY1)
shapePath.quadToPoint(
rdX2,
rdY2,
pbX2,
pbY2
)
shapePath.lineTo(pcX1, pcY1)
shapePath.quadToPoint(
rdX3,
rdY3,
pcX2,
pcY2
)
shapePath.lineTo(pdX1, pdY1)
shapePath.quadToPoint(
rdX4,
rdY4,
pdX2,
pdY2
)
shapePath.lineTo(peX1, peY1)
shapePath.quadToPoint(
rdX5,
rdY5,
peX2,
peY2
)
shapePath.lineTo(paX1, paY1)
shapePath.lineTo(paX1, 0f)
shapePath.lineTo(0f, 0f)
shapePath.lineTo(0f, length)
shapePath.lineTo(length, length)
shapePath.lineTo(length, 0f)
}
})
应该也比较好理解吧,paX1、rdY2、rdX1...... 这些就是一些坐标,shapePath.lineTo就是剪直线,shapePath.quadToPoint就是剪弧线。
但是你们会好奇最后几行是什么意思
scss
shapePath.lineTo(paX1.toFloat(), paY1.toFloat())
shapePath.lineTo(paX1.toFloat(), 0f)
shapePath.lineTo(0f, 0f)
shapePath.lineTo(0f, length)
shapePath.lineTo(length, length)
shapePath.lineTo(length, 0f)
可以看看屏蔽掉最后几行代码的效果


发现剪掉的是中间,但是为什么加上这几行代码就是正常的了
其实这就是上面说的,一定要带入到这个裁剪的思路,你可以理解成除了top这条边,其他的边都是向外延伸的,所以我们要再剪外面一圈,用图来表示就是这样(数字就是裁剪的步骤)

因为对于top边来说,右上角就是终点,所以剪到shapePath.lineTo(length, 0f)就行了。
其实还有个技巧,ShapeableImageView有个描边功能,可以通过这个描边,看出整个裁切的路径

3. 总结
这个控件其实不难,很多人用自定义实现不了,是因为去开发这个功能的时候是用一个开发者的思路,而不是用一个现实的思路。我们都知道Material Design的提测就是为了让我们做的东西更贴近现实,比如阴影、动画的插值器(加减速)等等,而ShapeableImageView也是属于material包中的控件。所以我觉得开发这个功能的过程很有意思,它会让我结合现实中去思考怎么实现这个功能。