Android clipChildren 使用与疑难点解析

前言

ClipXX 系列:

Android clipChildren 使用与疑难点解析
Android clipToPadding 使用与疑难点解析

我们知道,通常来说当子布局的边界处在父布局之外的时候,此时子布局超出的部分是无法显示的。想要显示超出的部分,通过设置clipChildren 属性可以解决此问题,本篇将会探究clipChildren 属性的使用及其原理。
通过本篇文章,你将了解到:

1、clipChildren 使用场景
2、clipChildren 如何使用
3、clipChildren 设置在父布局为什么无效
4、子布局超出部分如何响应点击事件
5、总结

1、clipChildren 使用场景

先来看图:

image.png

通过布局文件并结合上图可知:

1、三个Button是放在一个横向的LinearLayout里的。
2、LinearLayout(父布局)背景色为红色。
3、Button高度与父布局高度一致。

现在想要一个效果:

点击对应的Button,使其往上移动,凸显点击效果。

效果如下:

tt0.top-114249.gif

这正是开头想要的效果。当然,借助于clipChildren 特性,我们还可以对Button做动画效果,比如点击Button后,让其移动到ViewGroup之外。

3、clipChildren 设置在父布局为什么无效

网上大部分的文章在分析clipChildren 时只会提到之前的两点:使用场景与如何使用。
思考一个问题:

既然是限制子布局的展示,而Button的父布局是LinearLayout,为啥不在LinearLayout 节点下设置android:clipChildren=“false”,而要在爷爷布局FrameLayout节点下设置呢/p>

当然一开始按照正常的逻辑是设置在父布局节点下的,然而却没什么效果,接下来分析一下为啥没效果。
想要知道为什么不生效,就需要找到clipChildren属性值在哪被使用了。我们知道自定义View的三个过程:测量、摆放、绘制。因为涉及到展示,因此猜测是在绘制过程被裁剪了,而裁剪展示区域我们就想到了Canvas的裁剪。
通过前面的文章分析的绘制过程,直接定位到如下代码(软件绘制为例):

由此可知:

1、若是clipChildrentrue,那么将会裁剪子布局,方式是通过裁剪Canvas。
2、若是clipChildren
false,那么将不会裁剪Canvas。

在父布局节点设置

爷爷布局:FrameLayout
父布局:LinearLayout
子布局:Button

image.png

当在爷爷布局(FrameLayout)节点里设置clipChildren==false时,爷爷布局不会限制其子布局(红色部分父布局LinearLayout),因此父布局(LinearLayout)绘制范围为:canvas=[0,0,800,1280]。
而当父布局(LinearLayout)限制子布局(Button)的展示范围时,Canvas进行clip操作,取交集,得出子布局(Button)绘制范围为:canvas=[100,980,300,1280],超出的部分(980-800)即为多出的展示区域。
最后呈现的效果即是子布局能够超出父布局展示。

一言蔽之:

想要超出父布局展示,只需要子布局canvas绘制范围超出父布局边界即可。

注:上述以软件绘制为例阐述的,爷爷布局,父布局,子布局都是同一个Canvas对象,而开启硬件加速后Canvas不是同一对象。具体的差别请查看之前的文章。

4、子布局超出部分如何响应点击事件

在第三步已经解决了如何超出父布局展示,现在又引入了新的问题:

子布局超出的部分如何响应点击事件/p>

老样子,既然点击无法响应,那么先看看影响点击响应的因素是啥。
还是要从事件分发开始说起,如果点击的坐标落在目标View之内(此处是子布局Button),那么它是能够响应的。
现在问题就转为了:

点击事件分发到哪一层了/p>

虽然父布局(LinearLayout)的Canvas改变了,但是其顶点(left、top、right、bottom)坐标也没变,因此父布局也无法收到点击事件。可以确认的是,点击事件肯定是分发给了爷爷布局的。
问题又转为了:

爷爷布局的事件如何传递给父布局br> 换句话说,父布局如何扩大点击区域/p>

这让我们想到了TouchDelegate—一个专注扩大目标View点击区域的类。
找到解决方案了,看代码:

以上代码目的是:

扩大父布局响应的点击区域,在爷爷布局里将事件分发给父布局。

然而运行这段代码,子布局(Button)依然无法响应点击,于是到TouchDelegate 寻找答案。
当爷爷布局发现之前设置了TouchDelegate,于是就会调用TouchDelegate.onTouchEvent(xx)检测:

找到问题根源了:虽然父布局(FrameLayout)收到了点击事件,但是这个坐标是它的中心点,而中心点不一定落在其子布局(Button)里,因此Button是无法收到点击事件的。
还好,TouchDelegate是public类型的,于是我们可以重写TouchDelegate

此时父布局(LinearLayout)可以收到点击事件了,但问题又来了:

父布局如何将事件传递给子布局,并且还要区分三个不同的Button。

父布局收到点击事件后调用会流转到onTouchEvent(xx)里,因此需要在该方法内做文章。试想,现在父布局的onTouchEvent(xx)方法可以拿到点击的坐标,那么只需要判断该点是否落在各个子布局(Button)内即可。当然不能单纯依赖Button的四个顶点坐标,还需要配合View.getLocationOnScreen(xx)使用。
因此需要重写onTouchEvent(xx):

使用ClipViewGroup 替代父布局(LinearLayout)。
最后看看效果:

tt0.top-473084.gif

注:为了更显眼地表示点击区域,此处是将子布局往上全部移动超出父布局

5、总结

虽然 clipChildren属性比较简单,使用范围也比较局限,但是想要真正弄明白它需要结合测量、摆放、绘制流程源码分析,若是还想要对点击区域做文章,那么还需要对事件分发有一定的了解。
当然,这些基础知识在前面的文章中已有系统的分析过,若是看过之前的文章,那么理解clipChildren 更简单了。

本文基于Android 10。
完整代码演示 若是有帮助,给github 点个赞呗~

您若喜欢,请点赞、关注,您的鼓励是我前进的动力

持续更新中,和我一起步步为营系统、深入学习Android/Java

来源:小鱼人爱编程

声明:本站部分文章及图片转载于互联网,内容版权归原作者所有,如本站任何资料有侵权请您尽早请联系jinwei@zod.com.cn进行处理,非常感谢!

上一篇 2023年1月2日
下一篇 2023年1月3日

相关推荐