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 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹3 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空5 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭5 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日6 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安6 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑7 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟11 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡12 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0012 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体