转载之
张旭童
本文部分内容出自之前的博文(本文提及的上一节即指这篇)《酷炫Path动画》:
http://blog.csdn.net/zxt0601/article/details/54018970
今天给大家带来的是利用 纯自定义View,实现的仿饿了么加入购物车控件,自带闪转腾挪动画的按钮。效果图如下:
**图1 **项目中使用的效果,考虑到了 View 的回收复用,并且可以看到在 RecyclerView 中使用,切换 LayoutManager 也是没有问题的:
图2 Demo效果,测试各种属性值:
注意,本控件 **非继承自ViewGroup,而是纯自定义View **实现。理由如下:
1 减少布局层级,从而提高性能
**2 **文字和图形纯draw,用到什么draw什么,没有其他的额外工作,也间接提高性能。
**3 **纯自定义View难度更高,更有实(装)践(B)的意义
**1. **减少布局层次,很好理解,ViewGroup 内嵌套几个 TextView、ImageView 这里写代码也可以实现这个效果,然而这会使布局层次多了一级,并且内部要嵌套多个控件,层级越多,控件越多,绘制的就越慢,在列表中对性能的影响更大。
**2. **别小看了“小小”的 TextView 和 ImageView,其实它们有很多的属性和特性在本例中是不必要的,举个例子,查看源码,TextView 有一万多行,ondraw() 方法有一百多行, ImageView 有1588行,这么多行代码都是我们需要的吗?直接使用这些现成的控件嵌套实现,其实性能不如我们用到什么draw什么。唯一的好处可能就是比较简单了。(其实 TextView 的性能是不高的)
**3. **纯自定义View,draw 出这些需要的元素,并且还要考虑动画,以及点击各区域的监听,实现起来还是有一些难度的,但我们多写一些有难度的代码才能提高水平。
如何使用
伸手党福利:讲解实现前,先看一下 如何使用 以及 支持的属性 等。
使用
xml:
注意:加减点击后,具体的操作,要根据业务的不同来编写了,设计到实际的购物车可能还有写数据库操作,或者请求接口等,要操作成功后才执行动画、或者修改count,这一块代码每个人写法可能不同。
使用时,可以重写 onDelClick() 和 onAddClick() 方法,并在合适的时机回调 onCountAddSuccess() 和 onCountDelSuccess() 以执行动画。效果图如 图2.
支持的属性
这么多属性够你用了吧。下面看重点的实现吧,Let’s Go!
实现解剖
关于自定义View的基础,这里不再赘述。如果阅读时有不明白的,建议下载源码边看边读,或者学习自定义View基础知识后再阅读本文。
我们捡重点说,无非是绘制。绘制的重点,这里分三块:
静态绘制。(分两块:加减按钮和数量、hint提示文字和背景)
第一层。(加减按钮和数量)以及它的旋转、位移、透明度动画
第二层。(hint区域)以及它的伸展收缩动画
除了绘制以外的重点是:
由于采用了完全的自定义View去实现这么一个“组合控件效果”,则点击事件的监听需要自己处理。
在回收复用的列表中使用时,列表滑动,如何正确显示UI。
静态绘制
静态绘制就是最基本的自定义View知识,绘制 圆圈(Circle)、线段(Line)、数字(Text) 以及 圆角矩形(RoundRect),值得注意的是,要考虑到 避免overDraw 和 动画的需求,我们要绘制的两层应该是互斥关系。
剥离掉动画代码,大致如下(基本都是draw代码,可以快速阅读):
根据 isHintMode 布尔值变量,区分是绘制 第二层(Hint层) 或者 第一层(加减按钮层)。
绘制 第二层 时没啥好说的,就是利用 canvas.drawRoundRect,绘制圆角矩形,然后 canvas.drawText 绘制 hint。(如果圆角的值足够大,矩形的宽度足够小,就变成了圆形。)
绘制 第一层 时,要根据当前的数量选择不同的颜色,注意在绘制加减按钮的圆圈时,我们是用 Path 绘制的,这是因为我们还需要用 Path 构建 Region类,这个类就是我们监听点击区域的重点。
点击事件的监听
在讲解动画之前,我们先说说如何监听点击的区域,因为本控件的动画是和加减数量息息相关的,而数量的加减是由点击相应”+ - 按钮”区域触发的。所以我们的监听按钮的点击事件,其实就是监听相应的”+ - 按钮”区域。
上一节***** 中,我们在绘制”+ - 按钮”区域时,通过 Path,构建了两个 Region类,Region类 有个 contains(int x, int y)方法如下,通过传入对应触摸的x、y坐标,就可知道知否点击了相应区域。
知道了这一点,再写这部分代码就相当简单了:
hint 模式时,我们可以认为控件所有范围都是“+”的有效区域。
而在 非hint 模式时,根据上一节*构建的 mAddRegion 和 mDelRegion 去判断。
判断确认点击后,具体的操作,要根据业务的不同来编写了,设计到实际的购物车可能还有写数据库操作,或者请求接口等,要操作成功后才执行动画、或者修改count,这一块代码每个人写法可能不同。
使用时,可以重写 onDelClick() 和 onAddClick() 方法,并在合适的时机回调 onCountAddSuccess() 和 onCountDelSuccess() 以执行动画。
本文如下编写:
动画实现
这里会用到两个变量:
依次分析有哪些动画:
Hint动画
主要是圆角矩形的 展开、收缩。固定right、bottom,当展开时,不断减少矩形的左起点left坐标值,则整个矩形宽度变大,呈现展开。收缩时相反。代码:
减按钮动画
看起来是 旋转、位移、透明度。那么对于背景的圆圈来说,我们只需要位移、透明度。因为它本身是个圆,就不要旋转了。代码:
对于前景的“-”号来说,旋转、位移、透明度都需要做。这里我们利用 canvas.translate() canvas.rotate 做旋转和位移动画,别忘了 canvas.save()和 canvas.restore() 恢复画布的状态。(透明度在上面已经设置过了。)
数量的动画
看起来也是 旋转、位移、透明度。同样是利用canvas.translate() canvas.rotate 做旋转和位移动画。
动画的定义
动画是在View初始化时就定义好的,执行顺序:
数量增加,0-1时,先收缩Hint(第二层)mAnimReduceHint执行,完毕后执行减按钮(第一层)进入的动画mAnimAdd。
数量减少,1-0时,先执行减按钮退出的动画mAniDel,再伸展Hint动画mAnimExpandHint,完毕后,显示hint文字。
代码如下:
针对复用机制的处理
因为我们的购物车控件肯定会用在列表中,不管你用 ListView 还是 RecyclerView,都会涉及到复用的问题。
复用给我们带来一个麻烦的地方就是,我们要处理好一些属性状态值,否则UI上会有问题。可以从两处下手处理:
onMeasure
列表复用时,依然会回调 onMeasure()方法,所以在这里初始化一些UI显示的参数。这里顺带将适配 wrap_content 的代码也一同贴上:
在改变count时
一般在 onBindViewHolder() 或者 getView() 时,都会对本控件重新设置 count 值,count 改变时,当然也是需要根据 count 进行属性值的调整。且此时如果View正在做动画,应该停止这些动画。
总结
我在实现这个控件时,觉得难度相对大的地方在于做动画时,“-”按钮 和 数量的旋转动画,如何确定正确的坐标值。因为将text绘制的居中本身就有一些注意事项在里面,再涉及到动画,难免蒙圈。需要多计算,多试验。
还有就是观察饿了么的效果,将hint区域的动画利用改变RoundRect的宽度去实现。起初没有想到,也是思考了一会如何去做。这是属于分析、拆解动画遇到的问题。
除了绘制以外的重点是:
利用Region监听区域点击事件。
复用的列表,如何正确显示UI。
动画次序以及考虑到复用时,在合适的地方取消动画。
尽情在项目中使用它吧,有问题随时gayhub给我反馈。
通过sdk工具查看饿了么,它其实是用 TextView 和 ImageView 组合实现的。另外我十分怀疑它没有封装成控件,因为在列表页和详情页的交互,以及动画居然略有不同, 在详情页,仔细看由0-1时,它右边的 + 按钮的动画居然会闪一下,在列表页却没有,很是不解。
代码传送门:
https://github.com/mcxtzhang/AnimShopButton