
2.4 折叠布局实战(一)——核心原理
经过前面的学习,我们大概了解了Matrix所具有的功能。在第1章中,我们已经简单了解过如何通过Matrix来迁移旋转中心。下面就利用Matrix的setPolyToPoly函数来实现折叠布局效果,如图2-26所示。
可以看到,在这个效果中,菜单是可以折叠、展开和关闭的,而且在点击菜单上的菜单项以后,会有对应的Toast弹出。下面就一步步来实现这个折叠菜单吧。

图2-26

扫码查看动态效果图
2.4.1 折叠原理概述
2.4.1.1 折叠原理
从图2-26可以看出,我们主要做的工作就是将原来平铺的菜单View画成折叠的样子,即如图2-27所示的样子。

图2-27
其实就是将菜单分为8份,每份按照不同的方式折叠,分成的8份如图2-28所示。
仔细分析可以发现,完整的折叠效果其实是第1份和第2份折叠效果的重复,我们只要弄明白第1份和第2份折叠效果的实现方式,整个菜单就能实现出来,第1份和第2份折叠效果如图2-29所示。

图2-28

图2-29
所以,这里主要解决如下3个问题。
●折叠问题:很明显,折叠效果的实现是通过将菜单分为8份,每份调用setPolyToPoly函数来实现错切效果。
●阴影问题:仔细看会发现,在折叠过程中,每份上都有阴影,第1份的阴影是半透明全灰的,而第2份的阴影是渐变的,从暗到亮,在《Android自定义控件开发入门与实战》一书中已经讲过,线性渐变是通过LinearGradient来实现的。
●截断显示的问题:上面虽然说到我们将菜单分为8份,每份调用setPolyToPoly函数来实现错切效果,但怎么在显示时只显示一份的内容呢?在Canvas中,可以通过clipRect来裁剪画布,只画出裁剪后的画布区域。
下面,先不考虑整个菜单的效果,就用一张图来尝试一步步实现折叠效果,看看针对单份是如何做的。
2.4.1.2 实现第1份的倾斜效果
我们先用如图2-30所示的左图中带有横线的图来表示菜单,其折叠效果如图2-30所示的右图的样子。

图2-30

扫码查看彩色图
尝试做出第1份的倾斜效果,如图2-31所示。

图2-31

扫码查看彩色图
图2-31左图表示原图,右图表示第1份的倾斜效果。下面看看要实现这种倾斜效果的代码。首先看看所要实现的自定义控件的源码:


这段源码比较简单,整体逻辑就是根据图2-32所标注的图形,计算出变换后的4个点的目标坐标,然后利用setPloyToPoly函数计算出Matrix,最后在画Bitmap时,对Canvas应用这个Matrix的过程。src数组与dst数组所对应的坐标如图2-32所示。

图2-32

扫码查看彩色图
为了方便起见,这里直接指定图最右侧向下倾斜100 px。我们先了解大概原理,后面会具体地根据公式讲解计算倾斜距离的方法。其中会用到资源R.mipmap.sample,如图2-33所示。
在调用setPolyToPoly时,用到了src.length >>1,这其实是一个二进制位移操作,>>是右移操作符,>>1表示在二进制状态下右移1位,作用与除以2所得的整数结果相同。这里src.length的值是4,对应的二进制数是100,在右移1位后,得到的二进制数是10,对应的十进制数是2。同样地,假设src.length的值是9,对应的二进制数是1001,右移1位后的二进制数是100,即4。所以右移1位后获得的值与除以2取整的值相同。

图2-33

扫码查看彩色图
使用这个自定义控件比较简单,直接全屏展示即可(activity_set_rect_to_rect.xml):

2.4.1.3 只显示第1份
上面初步实现了第1份的倾斜效果,下面再来看看如何利用Canvas.clipRect实现只显示第1份,效果如图2-34所示。

图2-34

扫码查看彩色图
这里主要是利用canvas.clipRect函数来截取一部分图像进行显示,改动部分如下:

在初始化时,定义了两个变量,其中sFoldsNum表示有几个折叠部分,mFoldWidth表示每个折叠部分的宽度。mFoldWidth的计算方法也比较简单,使用Bitmap的width/sFoldNum即可。
最重要的是绘图时的操作:


这段代码是核心内容,理解起来可能比较费劲。下面一句句代码来进行讲解。
首先,构造第1份所在矩形的位置:

图2-35表示在坐标系没有被Matrix操作改变前,第1份的位置信息。

图2-35
在执行了Canvas.setMatrix(mMatrix)操作后,坐标系及第1份的位置关系如图2-36所示。

图2-36

扫码查看彩色图
在图2-36中,绿色部分表示在Canvas经过Matrix变换以后,第1份在整个坐标系中的位置。需要注意的是,现在只是用绿色表示坐标系变换以后第1份的位置,其实到目前为止,还没有执行绘图操作。
然后,当调用canvas.clipRect(rect)来裁剪画布时,需要注意,这里裁剪的画布就是第1份所占据的位置,也就是图2-36中的绿色区域。
注意,在利用clipRect裁剪画布后,画布就只有裁剪后的大小了。在后面会看到,虽然利用canvas.drawBitmap所画的是完整的图像,但由于画布的限制,因此只能显示画布所在区域和大小的那部分内容,不会展示其他部分。画布与所绘图像的合成过程如图2-37所示。

图2-37

扫码查看彩色图
绿色区域表示画布所在位置及大小,而红色区域表示在调用canvas.drawBitmap(mBitmap,mMatrix,null);后所画的完整图像。但正是因为画布区域被裁剪了,只有绿色区域大小,所以画出的图像也只有绿色区域那么大,所以此时画出来的效果如图2-38所示。

图2-38

扫码查看彩色图
2.4.1.4 实现第2份的倾斜效果
在2.4.1.3节中,我们已经学习了如何实现第1份的倾斜和裁剪效果,下面再来学习一下如何实现第2份的倾斜和裁剪效果。在理解了第1份和第2份以后,我们就可以找出规律,进而利用公式来自动实现倾斜和裁剪效果。
图2-39展示了裁剪部分与完整部分的关系。

图2-39

扫码查看彩色图
左图展示了在应用Matrix且裁剪画布后只显示第2份区域的情况。中间的图表示应用了Matrix后的完整图。而右图的绿色框区域,表示在该完整图中第2份所在的区域。
可以看到,在第2份的错切效果中,最难的地方是如何计算出Matrix。
很显然,我们没办法知道错切后完整图4个顶点的坐标,但我们可以知道右图中绿色框框出的第2份的各个顶点的坐标,只要我们将正常情况下的第2份的区域映射到绿色框部分,即可求出对应的Matrix。
下面讲解一下如何计算出Matrix的过程。图2-40展示了图原始状态时第2份的宽度mFoldWidth与错切后第2份宽度的关系。

图2-40

扫码查看彩色图
有一点大家必须弄清楚,Matrix改变的是坐标系,所以图倾斜的根本原因是坐标系倾斜了,这样通过同样的坐标位置画出来的图才表现的是倾斜的。从canvas.drawBitmap(mBitmap,0,0,null)函数可以看出,我们每次都是从(0,0)位置开始画的,而且图的位置也没有变,因为Matrix改变了坐标系,它使坐标系倾斜了,所以画同样的东西却看起来是倾斜的。
在图2-40中,右图中每份的宽度为mFoldWidth,在上面的例子中,我们已经用过它。而在左图中,由于坐标系倾斜了,所以画出来的绿色矩形框也是倾斜的,此时的mFoldWidth已经是倾斜的了。
我们在计算Matrix时,需要输入src数组和dst数组,分别代表正常坐标系下图2-40左图和右图中矩形的顶点坐标。这一点一定要注意,dst数组是在未应用Matrix的情况下,矩形4个顶点的坐标组合。即主要计算下面两张图中矩形顶点的坐标位置,以计算出Matrix,如图2-41和图2-42所示。

图2-41

扫码查看彩色图

图2-42

扫码查看彩色图
首先看src数组的计算过程,在这个数组中,每份宽度是mFoldWidth,图的高度是bmpHeight,所以从左上角以顺时针顺序取值时,很容易得出src数组如下:


在图2-42中,图(1)展示了错切后的图在原始矩阵中计算dst数组的情况,将其分解为图(2)和图(3)。在图(2)中,假设图倾斜后在原始坐标系下,当前每份宽w高h,那么左上角点的坐标就是(w,h),顺时针第2个点的坐标是(w×2,0),同样地,第3个点的坐标是(w×2,bmpHeight),第4个点的坐标是(w,bmpHeight+h)。
计算出的dst数组如下:

在这里,foldedItemWidth表示折叠后每份的宽度,即图2-42中的w,而depth即为图2-42中的h。那么问题来了,如何计算foldedItemWidth和depth呢?
foldedItemWidth其实比较容易计算,假如折叠后的总宽度是原宽度的0.8倍,那么foldedItemWidth=bmpWidth×0.8/sFoldsNum,其中sFoldsNum表示折叠了几次。
从最终的效果图可以看出,折叠后的宽度会跟随手指变动,所以,为了更新折叠后的宽度,我们使用一个系数mFactor参与计算,以便后期变动:

图2-43截取了错切后第2份的上半部分,在原始坐标系下,depth的计算方法如图中所示。

图2-43

扫码查看彩色图
很明显,在图2-43所构成的直角三角形中,3条边的长度满足勾股定理,所以计算公式如下:

以上公式可转化为如下代码:

到这里,计算第2份相关数据的代码就结束了,此时的代码如下:


此时运行代码后的效果如图2-44所示。

图2-44

扫码查看彩色图
2.4.1.5 只显示第2份
在实现了第2份的错切效果以后,再来看看如何实现只显示第2份。其实这里的原理与只显示第1份的原理是相同的,只需要找到第2份的Rect,让它显示出来即可,代码如下:

完整处理及合成过程如图2-45所示。

图2-45

扫码查看彩色图
在图2-45中,图(1)表示,通过Rect rect=new Rect(mFoldWidth,0,mFoldWidth*2,getHeight());定位的第2份所在的位置区域。图(2)表示,通过canvas.clipRect(rect);裁剪后的画布区域。图(3)表示,在调用canvas.drawBitmap(mBitmap,0,0,null);绘图时,虽然绘制了完整Bitmap,但由于画布的限制,所以最终将显示裁剪后的画布部分,此时的效果如图2-46所示。

图2-46

扫码查看彩色图
2.4.2 实现完整折叠效果
2.4.2.1 核心原理
上面已经基本讲解了如何实现第1份和第2份的折叠和局部显示效果,但如何实现完整折叠效果呢?很明显,要实现完整折叠效果,需要动态地计算src、dst与Matrix。
下面的代码展示了src数组的计算过程:

在上面的代码中,sLeft表示每份矩形区域左上角点的X轴坐标值,sRight表示每份矩形区域右上角点的X轴坐标值,很容易计算出来。
接下来计算折叠后的各个点的位置,先来计算顶部的9个点的位置,如图2-47所示。

图2-47

扫码查看彩色图
首先,看图2-47中顶部的9个点,对于0、2、4、6、8这些偶数索引点,它们的y坐标值都是0,x坐标值是foldedItemWidth×i;对于1、3、5、7这些奇数索引点,它们的y坐标值都是depth,x坐标值是foldedItemWidth×(i+1)。
然后,看底部的9个点,同样地,对于0、2、4、6、8这些偶数索引点,它们的y坐标值都是bmpHeight,它们的x坐标值是foldedItemWidth×i;对于1、3、5、7这些奇数索引点,它们的y坐标值都是depth+bmpHeight,它们的x坐标值是foldedItemWidth×(i+1)。
在图2-48中,我们对折叠部分进行拆分,拆分为8份,这与最开始讲解原理时分为8份一致。

图2-48

扫码查看彩色图
所以,对于图2-48中标记出的第1、3、5、7份矩形,它们的位置代码如下:


同样地,对于图2-48中标记出的第2、4、6、8份矩形,它们的位置代码如下:

然后将它们整合一下,假设变量isEven用于标记当前计算的是否是奇数索引点,比如当前计算的是第1、3、5、6份时,isEven的值为true,当计算其他份时,isEven的值为false。整合后的代码如下:

在理解了最难的部分以后,接着会把完整的实现代码列出来讲解一下。
2.4.2.2 代码讲解
首先来看init函数中的改造代码:


除了上面已讲解的src数组与dst数组的计算过程外,还有如下几点需要注意。
首先,因为需要存储多份的Matrix实例,所以,需要利用数组进行保存,这里使用的是mMatrices数组。在创建数组时,需要注意,不是调用Matrix[] mMatrices=new Matrix [sFoldsNum];创建数组就行了,还需要为数组中的每个Matrix对象创建实例,否则值都是null。因此,在初始化时,就会轮询mMatrices,并逐个实例化其中的每个元素。
然后,在for循环中,i表示第几份的索引。第1份的索引是0,第2份的索引是1,……,第8份的索引是7。可见,当计算奇数份时,索引是偶数。而isEven表示当前计算的是第几份,当i % 2==0时,表示当前计算的是奇数份,此时的isEven是true。
对于其他代码,前面都已经讲过了,这里就不再赘述了。
下面看看onDraw函数的具体实现:

这里在绘图时,需要轮询每份并将它们画出来。每份所对应的矩形位置是Rect rect=new Rect(mFoldWidth * i,0,mFoldWidth *(i+1),getHeight());,然后每份对应的Matrix是mMatrices[i]。整个裁剪及逻辑代码都没有变化,只是需要轮询每份并分别给它们应用Matrix。
此时的效果如图2-49所示。

图2-49

扫码查看彩色图
图2-49中的折叠效果看起来很奇怪,好像做出了折叠效果,但看起来不真实。下面将其与前面的折叠效果对比一下,如图2-50所示

图2-50

扫码查看彩色图
通过对比可以看出,已经实现了折叠效果,但因为没有添加阴影效果,所以看起来不真实。
2.4.3 添加阴影效果
2.4.3.1 基本实现
通过仔细观察图2-50中的右图效果,可以发现,所要添加的阴影其实包含两部分。
●第1、3、5、7奇数份的阴影,需要完全覆盖这些份。
●第2、4、6、8偶数份的阴影,是渐变阴影,即实现从折痕处的半透明黑到完全透明的渐变。
了解了上面的原理,下面就来看看具体的代码吧。首先,定义相关的Paint变量并初始化:


其中,mSolidPaint是用来画完全覆盖奇数份的阴影的,mShadowPaint是用来画偶数份的渐变阴影的。通过《Android自定义控件开发入门与实战》一书的7.5节,我们知道线性渐变是通过LinearGradient实现的,所以还定义了一个mShadowGradientShader变量来实现渐变效果。
从动态效果可以看出,随着菜单的展开,阴影越来越淡,这种变淡的效果是通过调整alpha值来实现的,所以菜单越展开alpha值越小。转化为代码表述,就是mFactor值越大,alpha值越小。所以,alpha的计算公式为int alpha=(int)(255 *(1-mFactor));。
同样地,mSolidPaint的颜色值是半透明的黑色,同时也随着mFactor值改变而改变,但很明显,mSolidPaint的颜色值不能是全黑的,所以我们在alpha值的基础上乘以0.8,以防mSolidPaint的值被设置为全黑。
最后,由于后面我们会构造从全黑到完全透明的渐变颜色,所以为了方便起见,直接给mShadowPaint设置alpha值即可。
下面再来看看onDraw中的操作代码:


从代码可以看出,在drawBitmap之后开始绘制阴影。
对于奇数份,比较简单,直接利用mSolidPaint对整个区域进行填充即可:

对于偶数份,会稍微麻烦一些,因为需要构造渐变效果。LinearGradient中总共有7个参数,前4个参数表示从哪一点到哪一点来构造线性渐变效果,我们从区域的左上角点到右上角点进行线性渐变填充;后面的两个参数是Color.BLACK和Color.TRANSPARENT,表示从哪个颜色渐变到哪个颜色,这里是从全黑到全透明地填充渐变颜色。最后需要设置平铺模式(TileMode),有几种平铺模式,效果各不相同。利用平铺模式也能实现一些有意思的效果,在《Android自定义控件开发入门与实战》一书的7.6节有详细介绍。
在构造了mShadowGradientShader(也就是在偶数份)之后,进行渐变填充:

效果如图2-51所示。

图2-51

扫码查看彩色图
从图2-51可以看到,在添加了阴影之后,折叠效果就看起来正常了。
虽然以上代码逻辑比较容易理解,但需要在onDraw中创建LinearGradient对象,这是自定义控件时的“大忌”,因为onDraw函数很容易被触发,其不光在调用invalidate函数时会被触发,而且在将应用退到后台后再切换到前台时也可能被触发并进行重新绘制。因此,在很多种情况下都可能会触发重绘,而且随着代码逻辑的复杂度增加,onDraw函数会执行多少次很难控制。正因为如此,一定不要在onDraw函数中创建对象,这样做的话,将增大OOM风险。下面就来看一下,在不创建对象的情况下,如何实现各份的渐变效果。
2.4.3.2 代码改进
若要实现不在onDraw函数中动态创建LinearGradient对象,那么我们只能在初始化时创建一次,然后在每次绘制时移动绘制矩形的位置。
因此,在初始化代码中做如下修改:

在初始化时,先创建mShadowGradientShader,其中的渐变区间使用第1份的区间大小。
在绘制代码中做如下修改:

首先,在绘制Bitmap后,利用canvas.translate(mFoldWidth * i,0);将坐标系原点移到当前要绘制矩形的左上角。
然后,根据不同的画笔,固定地画出当前要绘制的区域,此时的区域坐标为(0,0,mFoldWidth,mBitmap.getHeight())。
绘制效果与2.4.3.1节的图2-51相同,这里就不再列出了。
到这里,介绍了有关折叠效果的核心实现方法。在2.5节中,我们将看看如何将折叠效果与手势结合起来,以实现动态折叠菜单的效果。