![Android自定义控件高级进阶与精彩实例](https://wfqqreader-1252317822.image.myqcloud.com/cover/747/36511747/b_36511747.jpg)
2.5 折叠布局实战(二)——折叠菜单
在2.4节中,我们已经初步了解了实现折叠菜单的原理,而在本节中,我们将实现两方面内容。首先,根据实现原理生成继承自ViewGroup的控件,让用户可以自定义布局;然后,为该控件添加手势交互,以实现响应手势的折叠菜单。
2.5.1 使用ViewGroup实现折叠效果
2.5.1.1 技术选型
一般而言,对于需要展示自身的控件,会继承自View类的控件,比如ImageView、TextView等。但若我们需要用户自定义布局内部控件,则需要继承自ViewGroup类的控件,比如LinearLayout、FrameLayout等。
另外,对于继承自ViewGroup类的控件,除非一些需要自定义布局的需求外(比如实现FlowLayout等),一般都不直接继承自ViewGroup,而是继承自它的子控件,比如LinearLayout等,因为ViewGroup中没有onLayout,所以如果继承自ViewGroup的话,我们需要自己实现onLayout,这有点麻烦。而当继承自类似LinearLayout这类ViewGroup的子控件时,onLayout已经实现好了,只关注我们自己要实现的功能即可,不必关注布局问题。
很显然,在这里我们关注的不是如何布局,而且如何在绘制子控制时实现折叠效果。因此,我们可以直接继承自LinearLayout等子控件。
如果将原本继承自View的效果迁移到继承自ViewGroup,则需要改动的位置如下。
●extends View需要改为extends LinearLayout。
●不存在Bitmap,绘制高度需要使用整个控件的高度。
●在ViewGroup及其子类中,绘制时调用的是dispatchDraw,而不是onDraw。
下面根据这几点变化,重新梳理一下代码。
2.5.1.2 整改init函数
在继承自View时,我们所有的初始化操作都放在init函数中,但在继承自ViewGroup时,由于没有Bitmap,则在初始化时无法获取相关的高度和宽度,这时我们必须延后处理,所以我们将其他不依赖宽度和高度的变量还放在init函数中,仅将依赖的变量先移出来。
此时的init函数代码如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_276.jpg?sign=1738992945-7ehawgIqKNZOknWIR47uqk5lARNfV89D-0-1111e4764a2ecfa77eace1c3e4dc68e3)
然后,把其他原来与Bitmap宽度和高度相关的变量全部都放在另一个函数中待用:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_277.jpg?sign=1738992945-2gDR6zC8zQKbfIt6oE4u7exeHpEcX9OP-0-504f70007d1b0ebb3b7f0334200009e7)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_278.jpg?sign=1738992945-DIm9QPqvw0Oh9HQTDAgxIQJuNdWPhUG5-0-d2f30dd6d39dc5789fed560741658cb6)
可以看到,在这个函数的开头有使用:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_279.jpg?sign=1738992945-pu31Ax6Uq4kjAHXm6AhD5qOvJ5rCJ1En-0-452ea9b78c034e5ce0797505ffa43056)
也就是使用整个ViewGroup的宽度和高度来代替原来mBitmap的宽度和高度,在代码中将原来所有的mBitmap.getWidth都替换为mWidth,所有的mBitmap.getHeight都替换为mHeight。其他代码逻辑没有变化。
那么问题就来了,新建的updateFold函数放在哪里呢?因为我们需要利用getMeasuredWidth和getMeasuredHeight,所以必须将其放在onMeasure之后的生命周期函数内,一般放在onLayout函数中:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_280.jpg?sign=1738992945-WmuXvkZyxQIcpFF2NFKX1Vm03oQ1eSBE-0-7ccc9d6b6c9f37f16a8d8861026f57d4)
2.5.1.3 整改dispatchDraw函数
在ViewGroup的绘制过程中,肯定会调用的绘图函数是dispatchDraw,此时不一定会调用onDraw函数。在dispatchDraw函数的整改中,只是将原来的canvas.drawBitmap函数改为super.dispatchDraw(canvas);,这样就实现了在操作完Canvas后绘制子控件的视图,代码如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_281.jpg?sign=1738992945-4JmtjqkSLXv7ON3bPWXkftQOejn3xa7N-0-0f2f5431e61a0219a0671b60500ada3a)
我们在使用这个自定义控件时,如果单纯地包裹一个显示图的ImageView:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_282.jpg?sign=1738992945-UR73h60uXflDo8CQsGJxcvRq79Ci8Ih4-0-5631236ee4e7f64e494db8b35c3bc490)
此时的效果如图2-52所示。
从图2-52可以看到,图顶部显示了折叠效果,但底部是怎么回事呢?怎么还这么平整?假如我们拿图2-52与前面的效果图(见图2-51)进行对比,如图2-53所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_283.jpg?sign=1738992945-YHELU7jjSBrbXURLw7dnFMUp2bzyAig7-0-1924d1df283afa22275b079b3b327444)
图2-52
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_284.jpg?sign=1738992945-7PY34h0FkgVARV4TnNQeI46j2ZQlVd5N-0-6f7c7f71857f131fef9a50357496e186)
扫码查看彩色图
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_285.jpg?sign=1738992945-vPiIQQdeKAZEWCIsA9HmFTABLqCU3JgX-0-c5106ef17a28be3acd031196c2251ac0)
图2-53
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_286.jpg?sign=1738992945-mkJ31cP1t7NJxCSq4qCjxh2sBxdFolPy-0-aa8bd9b3b8c4efbb8c003d8ca1ec8fae)
扫码查看彩色图
很明显可以看到,底部平齐是因为布局的高度问题,底部的折叠效果被截掉了。这是为什么呢?
仔细分析上面的布局代码,可以看出,PolyToPolySample4View的layout_height的值是wrap_content,而它的content是ImageView,其高度就是图2-53右图中绿框部分的高度。很显然,底部的折叠效果会被截掉。
2.5.1.4 截掉问题修复
那么怎么解决底部折叠效果被截掉的问题呢?有两种方法可以解决这个问题。
第一种方法:增加PolyToPolySample4View的测量高度。即在测量结果的基础上,增加depth的高度,这种方法需要重新执行onMeasure,相对比较麻烦。第二种方法:只需要我们将底部往上缩一缩,在PolyToPolySample4View测量高度不变的情况下,通过变形改变底部最低点的位置,使最低点位置处于测量范围内,也就是说底部整体向上缩了depth高度,如图2-54所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_287.jpg?sign=1738992945-yAQilqpcG1L2z7kF7ZrsKltvVDblWM3R-0-1ed997a52f9e096b95684ce5495f9474)
图2-54
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_288.jpg?sign=1738992945-2HFurWlChgNLOQID8HNMmjDpvYx7E8vQ-0-afa46a29498ecfb465c41316442eea42)
扫码查看彩色图
因此,我们需要修改dst数组:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_289.jpg?sign=1738992945-bcAP90HhZBW3iGK3kKWUiYLipISG8NQU-0-a69aa10934efdca4ba78ed5f9234355e)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_290.jpg?sign=1738992945-PNevb0laW6c1fKigThesTWHDWb7w8B4b-0-6d2b017896def9d24272590d0af8dffd)
用//注释掉原来的dst数组内容,可以看到,改变前后的区别在于原来的mHeight+depth被替换为mHeight,表示最大高度是mHeight,原来的mHeight被替换为mHeight-depth,以显示折叠效果。这样修改了以后,整个控件的最低点位置就保持在了mHeight处,也就不会出现底部折叠效果被截掉的问题了。此时的效果如图2-55所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_291.jpg?sign=1738992945-szHTfZ4Z1N2VRz8vi9X7x4LhBK92diEJ-0-4a70e323d18e3082fd4fd4cb1b5d5398)
图2-55
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_292.jpg?sign=1738992945-wgnwZF4iKH0crpvUjQ0OeZ2CFNB6b3GV-0-ead58756e9b4a33a70aecd85f6c2143a)
扫码查看彩色图
2.5.1.5 测试成果
我们将包裹的ImageView改为其他布局,再来看看效果:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_293.jpg?sign=1738992945-alSL4OYuqIVQW0fSFp2PQtsYkR74bocB-0-5f5457295727eba9088635d935c16acd)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_294.jpg?sign=1738992945-l88ipqPZfQxXVYLHt4hAtR0EQA5RMynq-0-2e6309cee3b6bf4b00b592080349e19a)
效果如图2-56所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_295.jpg?sign=1738992945-zqVyHm2TAxy2Cp1k15xpZErv4TvEaeb6-0-c046c5beab7e3f17834d7d3795b8e72d)
图2-56
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_296.jpg?sign=1738992945-a9xRzYrdQaTS0LO5Pi0R3REF7Hwl4Tim-0-b2fc737025da35c10b822b55b609f260)
扫码查看动态图
从图2-56可以看到,在更改了子控件之后,整个布局自然变更了折叠效果,而且其中的子控件本身的功能依然可用。这就是继承自ViewGroup的好处。
2.5.2 实现折叠菜单
在理解了原理之后,下面就开始着手实现折叠菜单的效果。
2.5.2.1 使用PolyToPolySample6View动态改变宽度
首先,因为在前面的例子中我们都将整个菜单的宽度设定为原宽度的0.8倍,所以在我们要实现动态更新菜单的宽度时,需要增加一个接口,以动态设置菜单的宽度:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_297.jpg?sign=1738992945-E9cwrdtqjh5bOTLrTMXrDUevRa0Qrphc-0-eefd70745bd10a0efe532a9d3fe9f3a4)
这里新增了一个setFactor函数,可以动态设置缩放变量mFactor的值。设置以后,调用updateFold函数更新各种变量,然后调用invalidate函数重绘整个ViewGroup。
2.5.2.2 实现抽屉菜单控件
那么问题来了,怎么实现抽屉效果呢?在Android Support包中,Google为我们提供了一个官方的抽屉组件:DrawerLayout。这里先大概讲解一下,如果有不理解它的用法的读者,可以先学习此控件的使用方法后再回来学习本节内容。
因此,继承自DrawerLayout来自定义一个抽屉容器,将原来DrawerLayout的菜单布局转移到PolyToPolySample6View中,这样就可以将DrawerLayout的菜单折叠起来了。
相关代码如下,先列出完整代码,然后逐步讲解:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_298.jpg?sign=1738992945-O5P1WnHkSRGFeXw7fXElVj6WfJvTpAQ9-0-a90cb0e9c3d6090f5e65d056f72e90ec)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_299.jpg?sign=1738992945-4swnhBfHalrSaQCZhUXs2vOdzZz1JJb5-0-f11b3cd2050b825fd326a85b801e9980)
我们需要将DrawerLayout的菜单布局转移到PolyToPolySample6View中,需要在View已经生成但还没有显示出来的这个阶段实现。在ViewGroup的生命周期函数中,onFinishInflate和onAttachedToWindow都符合条件,这里将处理代码写在onAttachedToWindow中。
这里主要分为3个步骤。
(1)在onAttachedToWindow中轮询所有的子View,并找到菜单View。我们知道,在使用DrawerLayout时,如果layout_gravity的值是left、right的View,那么这个View肯定是菜单View。函数isDrawerView就是利用Gravity是不是left、right来判断是否是菜单的。
(2)如果是菜单View,则将它加入PolyToPolySample6View中。在将该子View加入PolyToPolySample6View中时,需要注意两点。
●先调用remove函数再调用add函数。
●新增PolyToPolySample6View时,需要使用该子View的布局参数。因为我们已经在子View的布局参数中提前定义了layout_gravity的值,所以DrawerLayout只需要识别它来确定它是否是菜单即可,如果是才会有菜单效果。
(3)设置抽屉滑动监听,当抽屉滑动时,实时地在onDrawerSlide中设置菜单的缩放比例。
2.5.2.3 使用自定义的抽屉组件FoldDrawerLayout
在使用抽屉组件时,因为它本质上是DrawerLayout,所以只需要遵循DrawerLayout的使用方法即可,只需要在菜单View上明确标注它的layout_gravity属性。这里为了方便,将TextView作为菜单项。代码如下(activity_fold_principle6.xml):
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_300.jpg?sign=1738992945-Hkp7aDJP7tdMimAeBPMSNXijtcCwT6oR-0-4d584ac8943642560ab27890e6fe8bf6)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_301.jpg?sign=1738992945-sKc2vF4YdVrCsDg3AXdbrT9gXKrSkFRJ-0-f159585ea114c5dff7ae205932c096bc)
然后在MainActivity中使用这个布局即可:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_302.jpg?sign=1738992945-amRArTuaYZgkcUplzjKWYSkyz25SBguQ-0-597ff38a9d50b2b78c36e48ee5ac635b)
效果如图2-57所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_303.jpg?sign=1738992945-ocOOU8gXId7ZYuNJxMUwqQAPz02gXUSM-0-87fbdfe3c02e9e67fd1bf4a1da099d50)
图2-57
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_304.jpg?sign=1738992945-mc8XIZjLr4RM1LgBdXQp1RAsYiU4S5sf-0-195badc87b75c48bd2f719e18951cc2e)
扫码查看动态效果图
2.5.2.4 完整实现折叠菜单效果
在前面的效果图中,大概实现了折叠菜单效果,但很明显有一个问题,这就是当手指拖动的时候,折叠菜单并不紧跟手指变化,而是出现了延后现象,比如图2-58中的白点是手指位置,而此时的折叠菜单右侧边在手指距离屏幕左边一半的位置,这是怎么回事呢?
我们知道,一般而言,滑动菜单展开的右侧边位置应该就是手指的位置,这里之所以会出现两个位置不一致的情况,是因为我们在显示折叠菜单时,根据菜单的原始宽度进行了缩放,缩放系数就是mFactor。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_305.jpg?sign=1738992945-7upMKPyPNTkRSnPZgOFVEyaaY1xFXBxS-0-27a02dfc8d896e58caba0764c67e7b32)
图2-58
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_306.jpg?sign=1738992945-i7wo62le3FtUC94DmABeViisBwP7LYgS-0-15479a0e8095d5fd68ec556d72b2939b)
扫码查看彩色图
但缩放后布局时,仍是以(0,0)点为坐标系原点进行布局的,这就导致看起来菜单右侧边与手指有一定的距离,原理如图2-59所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_307.jpg?sign=1738992945-NUvyvzG0T5KOnhtKO4ZNy7ZSCxfgNtmE-0-571d4a72110ee2eb9f187fb6119627f2)
图2-59
解决这个问题的办法也比较简单,只需要让缩放后的菜单靠右布局即可,原理如图2-60所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_308.jpg?sign=1738992945-bC9YDWaDSiN6DIKxd8FvLKKtv3qnu42c-0-95b644ed1bee0adc3b7d2e98b964785a)
图2-60
因为折叠菜单跟随手指移动的最大距离就是整个菜单宽度,所以右侧菜单缩小后的大小是mFactor×mWidth(mWidth是整个菜单的宽度),左侧空出来的距离是(1-mFacotr)×mWidth。
这样我们只需要对dst数组进行修改,整个折叠菜单向右移(1-mFacotr)×mWidth即可:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_309.jpg?sign=1738992945-3hhpJTFqvTuRBxDdF9N5uTh4MxRC7qD0-0-2cf155747f4b4f46b24170bfce59c4da)
修改后的代码效果如图2-61所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_310.jpg?sign=1738992945-7QsoyXtQRbQDxEBvQt2CrQxIhCfdGUnG-0-a401a4841ba4c12e5978f2f722259ae0)
图2-61
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_311.jpg?sign=1738992945-nBJe5w9j5VvSZtMmEJcRfEj7znwJ7dFu-0-71c0f1cc71ea7b3c886ab8d1a98a89db)
扫码查看彩色图
到这里,有关位置矩阵的所有知识就讲解完成了。单纯理解位置矩阵有一定的难度,使用起来更困难,但位置矩阵的应用范围比较广,在自定义控件中经常会用到,所以如果不懂这个知识点的话,可能会读不明白一些代码,因此大家还是应该尽量学会和掌握它。