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

相关推荐
游戏开发爱好者823 分钟前
完整教程:App上架苹果App Store全流程指南
android·ios·小程序·https·uni-app·iphone·webview
YIN_尹1 小时前
【MySQL】SQL里的“连连看”:从笛卡尔积到自连接
android·sql·mysql
bisal(Chen Liu)1 小时前
0.5 hour还是0.5 hours?
android
特立独行的猫a2 小时前
Kuikly多端框架(KMP)实战:现代Android/KMP状态管理指南:基于StateFlow与UDF架构的实践
android·架构·harmonyos·状态管理·kmp·stateflow·kuikly
范桂飓3 小时前
Google 提示词工程最佳实践白皮书解读
android·人工智能
贤泽3 小时前
Android 15 Service 源码解析
android
吴声子夜歌4 小时前
RxJava——并行编程
android·echarts·rxjava
小飞学编程...5 小时前
【Java相关八股文(二)】
android·java·开发语言
FunW1n5 小时前
Android Studio与Hook模块开发相关问题及实现方案梳理
android·ide·android studio
技术传感器6 小时前
解剖“数字孪生“:语义层定义世界,动力层驱动世界
android·运维·服务器