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

相关推荐
MiyamuraMiyako26 分钟前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel1 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥3 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走4 小时前
创建自定义语音录制View
android·前端
用户2018792831674 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831674 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker6 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong6 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil7 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌14 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端