Android 带你玩爆ShapeableImageView实现图片裁切

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包中的控件。所以我觉得开发这个功能的过程很有意思,它会让我结合现实中去思考怎么实现这个功能。

相关推荐
沐言人生2 小时前
Android10 Framework—Init进程-9.服务端属性值初始化
android·android studio·android jetpack
追光天使3 小时前
【Mac】和【安卓手机】 通过有线方式实现投屏
android·macos·智能手机·投屏·有线
小雨cc5566ru3 小时前
uniapp+Android智慧居家养老服务平台 0fjae微信小程序
android·微信小程序·uni-app
一切皆是定数4 小时前
Android车载——VehicleHal初始化(Android 11)
android·gitee
一切皆是定数4 小时前
Android车载——VehicleHal运行流程(Android 11)
android
problc4 小时前
Android 组件化利器:WMRouter 与 DRouter 的选择与实践
android·java
图王大胜5 小时前
Android SystemUI组件(11)SystemUIVisibility解读
android·framework·systemui·visibility
服装学院的IT男9 小时前
【Android 13源码分析】Activity生命周期之onCreate,onStart,onResume-2
android
Arms2069 小时前
android 全面屏最底部栏沉浸式
android
服装学院的IT男9 小时前
【Android 源码分析】Activity生命周期之onStop-1
android