![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=1739570382-38aITpZtFF0QMg6186W3MWRZcmHoOgMF-0-8cfc84bd354e8287441292eccc7d878b)
然后,把其他原来与Bitmap宽度和高度相关的变量全部都放在另一个函数中待用:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_277.jpg?sign=1739570382-4XrUhYTTyWMsYwkLhbeNCZFpJU3sRE5Q-0-072f3717d2076366edda4ab7ebb5b301)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_278.jpg?sign=1739570382-brcYGdDxHrqHyrCJTIYw32wsWlEwAzT0-0-35de7e8f7743d88423654dd986d7dc94)
可以看到,在这个函数的开头有使用:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_279.jpg?sign=1739570382-ygTAfFhWclGVyprKsGsLwCQGrPOlPrZo-0-3e5abbce27565e775df336b66b9c9c68)
也就是使用整个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=1739570382-gunWVGJ0fYsvxHrjukTBo4LRMsVvEank-0-c8bddbe97118cf180011da51470d529f)
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=1739570382-RkT6jYdG28Emv6vKsmxuNmjIrmXqb29B-0-61e63f18722a456a8933a9b89a835392)
我们在使用这个自定义控件时,如果单纯地包裹一个显示图的ImageView:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_282.jpg?sign=1739570382-vw3EL6PiyYWXKI5oysj4ucOt1kOOHMD3-0-9eec4e422d63d6f9d5f07d544e34bc04)
此时的效果如图2-52所示。
从图2-52可以看到,图顶部显示了折叠效果,但底部是怎么回事呢?怎么还这么平整?假如我们拿图2-52与前面的效果图(见图2-51)进行对比,如图2-53所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_283.jpg?sign=1739570382-TQoM3DpbEDK5V3plgajdlk5tg0SFSvnG-0-7fe2b876128b26f96782375bd5543e4d)
图2-52
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_284.jpg?sign=1739570382-EW4ZCpKUEvj5SZgR6NS5EvnRpePoMSmJ-0-39eb7be166bf4d3221a3aefba1d12216)
扫码查看彩色图
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_285.jpg?sign=1739570382-jq4Hh0h0yVKxwIuok15aWpxJ9kZfZ6h3-0-1486ac72bc02c83d0179673351e8e664)
图2-53
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_286.jpg?sign=1739570382-UzgtrouGQa8t91NBQt3AvutsmtWQx2w5-0-da22ea6c0cf4853c01afb65e13e340de)
扫码查看彩色图
很明显可以看到,底部平齐是因为布局的高度问题,底部的折叠效果被截掉了。这是为什么呢?
仔细分析上面的布局代码,可以看出,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=1739570382-LJ11FZzynessELpFcTsN2QyPKuyQ6Lcy-0-df52f67cce634c739931e0bd1901b82c)
图2-54
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_288.jpg?sign=1739570382-SlzTsGd9ZEjaKhQtMcZEgNvaShothUAU-0-f3e968c12841c7c8d6457cb275dedbbe)
扫码查看彩色图
因此,我们需要修改dst数组:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_289.jpg?sign=1739570382-sASLNI7w4lwFMDYQP31GrYhkQ8EIAYBQ-0-ce17ec48aa3071e43f7f6682b79c6964)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_290.jpg?sign=1739570382-VUM50zelwfALQ6eIThPf3JEWDRJGboGE-0-c79220b46d9c3e2cf0d5c086422b4433)
用//注释掉原来的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=1739570382-PUtBsRQPFo9kO4R24t8zJ5l57KDVoENF-0-32f3aeb4331d821c8771abca12c71c32)
图2-55
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_292.jpg?sign=1739570382-jGDMFoKjZs0KkuDvONk3BkinYMzUhz3T-0-5b75099dc953f4f32f3074448cad63c1)
扫码查看彩色图
2.5.1.5 测试成果
我们将包裹的ImageView改为其他布局,再来看看效果:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_293.jpg?sign=1739570382-Pp6RsRrnebxwq5V6JVHlHGEWhoUelnux-0-0fb38a6e279a1979c265244ee99f3419)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_294.jpg?sign=1739570382-ib6Nh2hXwGfvUV25WQWMnRGfUrk4YHDl-0-579bb15462e7d3438956acc6105f81af)
效果如图2-56所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_295.jpg?sign=1739570382-ZjkfpnFFh8uqWmSxOGiAA35eRy1WCeKR-0-79eca3b6e6d6f030b465623e18a86e3b)
图2-56
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_296.jpg?sign=1739570382-E19lKkQl2vgtYn4UFnxwgNVemk7FCDjm-0-14c992d18c1040a5038fdd1e1bc84605)
扫码查看动态图
从图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=1739570382-wmmLNyF2xYR3UaketIr26mtTvxmCouh9-0-7a4de66127aef112bd2be48cc307750f)
这里新增了一个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=1739570382-6LyfFoPxg8BOz3VlhEAgnITLts9kfmyq-0-ee97fbd2ab266a50a8a5231cacc5f21b)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_299.jpg?sign=1739570382-6IynRKiG0t4lrj0N5adUhB6CMIXvQmvk-0-20d2d257cafac145bc2695402ac7d3da)
我们需要将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=1739570382-3c5fZqzlge50Vb6FCQgohQrSo9uwjPrH-0-91daacd7ceddc7700a8da8a40d9c2b8d)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_301.jpg?sign=1739570382-f6YZlftJIhOPS8Qpbhrl0jtKxpSIELen-0-e20af6fd82c65813f409bd8bb71137dd)
然后在MainActivity中使用这个布局即可:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_302.jpg?sign=1739570382-ORPNJmS24DAnisOk7lrv0zRcdJMZ7feO-0-c875d6d6a4c484deb81acd34e5549869)
效果如图2-57所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_303.jpg?sign=1739570382-p06j2QImctGYtXd8VAmj7Px0KIcwDbKX-0-c42c8f8966740275024d33d20570ff97)
图2-57
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_304.jpg?sign=1739570382-w1ayZVQ9uQ2NmytlBoyLA1qOjxhJzNbE-0-4b5d7ab68c55ef28bcdbd908ff223f53)
扫码查看动态效果图
2.5.2.4 完整实现折叠菜单效果
在前面的效果图中,大概实现了折叠菜单效果,但很明显有一个问题,这就是当手指拖动的时候,折叠菜单并不紧跟手指变化,而是出现了延后现象,比如图2-58中的白点是手指位置,而此时的折叠菜单右侧边在手指距离屏幕左边一半的位置,这是怎么回事呢?
我们知道,一般而言,滑动菜单展开的右侧边位置应该就是手指的位置,这里之所以会出现两个位置不一致的情况,是因为我们在显示折叠菜单时,根据菜单的原始宽度进行了缩放,缩放系数就是mFactor。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_305.jpg?sign=1739570382-0ZOIG93XoYdnlw4w5g6NWtMcYs7iVU7u-0-e98e1bd03fb3f5be96c1e66d02d70c1b)
图2-58
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_306.jpg?sign=1739570382-PNIiCrmJvCIVyAgsrNIOwonrFRTTCiXQ-0-affb266d5bcf01bb66fce93586b6fb4a)
扫码查看彩色图
但缩放后布局时,仍是以(0,0)点为坐标系原点进行布局的,这就导致看起来菜单右侧边与手指有一定的距离,原理如图2-59所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_307.jpg?sign=1739570382-Tbvrm0toH2t2mGpwUkwcMUFmUO6xSJ9v-0-692a283a620a42ad3ff55f084d2b043e)
图2-59
解决这个问题的办法也比较简单,只需要让缩放后的菜单靠右布局即可,原理如图2-60所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_308.jpg?sign=1739570382-lu4WmgXMiQMohJbV5JT2vUrbTz87Y8Yn-0-3d9ffab43ea9749f4a0b09ea41c5ba46)
图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=1739570382-kyV1eVY4gXnwiFXBPj1OuFiWpDL9FvVh-0-a4d6ee8d24dfaa914d5987c3fcfb05aa)
修改后的代码效果如图2-61所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_310.jpg?sign=1739570382-7eRGsymvLSEdma0ZRuw7omJA0ZxGzl6T-0-27864cc05f1d6da0e499cb70afca5ac3)
图2-61
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt002_311.jpg?sign=1739570382-Wp3hNEfEpO5Cp7kuWNREMWgmdUfAhOz7-0-f94e752db83a61a68b469a43f9fe614e)
扫码查看彩色图
到这里,有关位置矩阵的所有知识就讲解完成了。单纯理解位置矩阵有一定的难度,使用起来更困难,但位置矩阵的应用范围比较广,在自定义控件中经常会用到,所以如果不懂这个知识点的话,可能会读不明白一些代码,因此大家还是应该尽量学会和掌握它。