
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函数代码如下:

然后,把其他原来与Bitmap宽度和高度相关的变量全部都放在另一个函数中待用:


可以看到,在这个函数的开头有使用:

也就是使用整个ViewGroup的宽度和高度来代替原来mBitmap的宽度和高度,在代码中将原来所有的mBitmap.getWidth都替换为mWidth,所有的mBitmap.getHeight都替换为mHeight。其他代码逻辑没有变化。
那么问题就来了,新建的updateFold函数放在哪里呢?因为我们需要利用getMeasuredWidth和getMeasuredHeight,所以必须将其放在onMeasure之后的生命周期函数内,一般放在onLayout函数中:

2.5.1.3 整改dispatchDraw函数
在ViewGroup的绘制过程中,肯定会调用的绘图函数是dispatchDraw,此时不一定会调用onDraw函数。在dispatchDraw函数的整改中,只是将原来的canvas.drawBitmap函数改为super.dispatchDraw(canvas);,这样就实现了在操作完Canvas后绘制子控件的视图,代码如下:

我们在使用这个自定义控件时,如果单纯地包裹一个显示图的ImageView:

此时的效果如图2-52所示。
从图2-52可以看到,图顶部显示了折叠效果,但底部是怎么回事呢?怎么还这么平整?假如我们拿图2-52与前面的效果图(见图2-51)进行对比,如图2-53所示。

图2-52

扫码查看彩色图

图2-53

扫码查看彩色图
很明显可以看到,底部平齐是因为布局的高度问题,底部的折叠效果被截掉了。这是为什么呢?
仔细分析上面的布局代码,可以看出,PolyToPolySample4View的layout_height的值是wrap_content,而它的content是ImageView,其高度就是图2-53右图中绿框部分的高度。很显然,底部的折叠效果会被截掉。
2.5.1.4 截掉问题修复
那么怎么解决底部折叠效果被截掉的问题呢?有两种方法可以解决这个问题。
第一种方法:增加PolyToPolySample4View的测量高度。即在测量结果的基础上,增加depth的高度,这种方法需要重新执行onMeasure,相对比较麻烦。第二种方法:只需要我们将底部往上缩一缩,在PolyToPolySample4View测量高度不变的情况下,通过变形改变底部最低点的位置,使最低点位置处于测量范围内,也就是说底部整体向上缩了depth高度,如图2-54所示。

图2-54

扫码查看彩色图
因此,我们需要修改dst数组:


用//注释掉原来的dst数组内容,可以看到,改变前后的区别在于原来的mHeight+depth被替换为mHeight,表示最大高度是mHeight,原来的mHeight被替换为mHeight-depth,以显示折叠效果。这样修改了以后,整个控件的最低点位置就保持在了mHeight处,也就不会出现底部折叠效果被截掉的问题了。此时的效果如图2-55所示。

图2-55

扫码查看彩色图
2.5.1.5 测试成果
我们将包裹的ImageView改为其他布局,再来看看效果:


效果如图2-56所示。

图2-56

扫码查看动态图
从图2-56可以看到,在更改了子控件之后,整个布局自然变更了折叠效果,而且其中的子控件本身的功能依然可用。这就是继承自ViewGroup的好处。
2.5.2 实现折叠菜单
在理解了原理之后,下面就开始着手实现折叠菜单的效果。
2.5.2.1 使用PolyToPolySample6View动态改变宽度
首先,因为在前面的例子中我们都将整个菜单的宽度设定为原宽度的0.8倍,所以在我们要实现动态更新菜单的宽度时,需要增加一个接口,以动态设置菜单的宽度:

这里新增了一个setFactor函数,可以动态设置缩放变量mFactor的值。设置以后,调用updateFold函数更新各种变量,然后调用invalidate函数重绘整个ViewGroup。
2.5.2.2 实现抽屉菜单控件
那么问题来了,怎么实现抽屉效果呢?在Android Support包中,Google为我们提供了一个官方的抽屉组件:DrawerLayout。这里先大概讲解一下,如果有不理解它的用法的读者,可以先学习此控件的使用方法后再回来学习本节内容。
因此,继承自DrawerLayout来自定义一个抽屉容器,将原来DrawerLayout的菜单布局转移到PolyToPolySample6View中,这样就可以将DrawerLayout的菜单折叠起来了。
相关代码如下,先列出完整代码,然后逐步讲解:


我们需要将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):


然后在MainActivity中使用这个布局即可:

效果如图2-57所示。

图2-57

扫码查看动态效果图
2.5.2.4 完整实现折叠菜单效果
在前面的效果图中,大概实现了折叠菜单效果,但很明显有一个问题,这就是当手指拖动的时候,折叠菜单并不紧跟手指变化,而是出现了延后现象,比如图2-58中的白点是手指位置,而此时的折叠菜单右侧边在手指距离屏幕左边一半的位置,这是怎么回事呢?
我们知道,一般而言,滑动菜单展开的右侧边位置应该就是手指的位置,这里之所以会出现两个位置不一致的情况,是因为我们在显示折叠菜单时,根据菜单的原始宽度进行了缩放,缩放系数就是mFactor。

图2-58

扫码查看彩色图
但缩放后布局时,仍是以(0,0)点为坐标系原点进行布局的,这就导致看起来菜单右侧边与手指有一定的距离,原理如图2-59所示。

图2-59
解决这个问题的办法也比较简单,只需要让缩放后的菜单靠右布局即可,原理如图2-60所示。

图2-60
因为折叠菜单跟随手指移动的最大距离就是整个菜单宽度,所以右侧菜单缩小后的大小是mFactor×mWidth(mWidth是整个菜单的宽度),左侧空出来的距离是(1-mFacotr)×mWidth。
这样我们只需要对dst数组进行修改,整个折叠菜单向右移(1-mFacotr)×mWidth即可:

修改后的代码效果如图2-61所示。

图2-61

扫码查看彩色图
到这里,有关位置矩阵的所有知识就讲解完成了。单纯理解位置矩阵有一定的难度,使用起来更困难,但位置矩阵的应用范围比较广,在自定义控件中经常会用到,所以如果不懂这个知识点的话,可能会读不明白一些代码,因此大家还是应该尽量学会和掌握它。