- Android自定义控件高级进阶与精彩实例
- 启舰
- 2367字
- 2021-03-04 18:45:20
1.4 实现小米时钟的触摸倾斜效果
前面已经详细讲解了Camera类的使用函数,在本节的例子中,我们将学习Camera是如何与手势一起使用的。可扫码查看本节例子的效果图。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_129.jpg?sign=1739481648-eRkyF8XomG83eyWFr2U5g0TEBQK2VzD1-0-66410c8dba8bdf10e09c454185dd3271)
扫码查看动态效果图
从效果图可以看出,这个效果主要有以下几个特性。
(1)在手指按下时,钟表会倾斜一个角度,这个角度与手指按下的位置和钟表中心的距离相关。距离越大,钟表倾斜的角度越大;距离越小,钟表倾斜的角度越小。当然我们会设置一个最大倾斜角度,以防界面变得不可控。
(2)在手指移动时,图像的倾斜角度会随着手指的移动而改变。
(3)在抬起手指后,钟表会做出一个复位动画。很明显,复位时的动画使用的是BounceInterpolator。
1.4.1 框架搭建
1.如何继承
我们先考虑如何自定义这个控件,主要针对一个图像的Camera操作进行自定义,有如下3种方法。
●方法一:继承自View,每次改变Camera后,将图像重新画出。
●方法二:继承自ImageView,如1.3节中的操作,在调用super.onDraw(canvas)前对Camera进行更改。
●方法三:继承自ViewGroup,在调用dispatchDraw函数中的super.dispatchDraw(canvas)前,对Camera进行操作,这样就可以对ViewGroup中的所有子控件进行Camera变换了。
在这里,我们使用方法三。为了方便起见,可以继承自LinearLayout、RelativeLayout等已有的布局控件,这样就不必重写onMeasure、onLayout函数了,可以只关注需要重写的dispatchDraw函数。
2.搭建框架
首先,自定义一个继承自LinearLayout的控件,取名为ClockViewGroup。很明显,在这个例子中,主要是对它进行处理。下面搭建框架,先列出派生函数,不进行具体的实现:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_130.jpg?sign=1739481648-oDAr0I4lxDjOErLq5QM80RYSe05qtKM7-0-63e61202fd8d762f1bb10aea7dd75854)
在使用时,只需要在它内部包裹要操作的控件即可(activity_main.xml):
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_131.jpg?sign=1739481648-iv0phQCVbHHWBpKB2kXsKAtpVHWvXCiA-0-f5547221ac7ea60db469fed8fdb1a26d)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_132.jpg?sign=1739481648-NDZPhJPwTRUUxHFvlEfR2F8acdlFvAac-0-0677915f68a2b0dba1340bc1daee3cf3)
在使用时,在Activity中正常使用XML即可:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_133.jpg?sign=1739481648-WFr89S4XFnSSkNKzqCw87pbhxIjY3xdA-0-17da17a99f28ecec8e698ca1de3d4f7d)
可以看到,整体框架非常简单。接下来,对于所有手指触摸操作图像的代码,将在自定义的ClockViewGroup中实现。
1.4.2 实现ClockViewGroup
1.绘制旋转后的控件
从效果图可以看出,在手指按下和滑动时,图片绕X轴和Y轴旋转了不同的角度,需要确定角度是多少,dispatchDraw中旋转操作的代码如下。下面列出了完整代码,后面再分解讲述:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_134.jpg?sign=1739481648-rr0H5Tw9O0KI1wGXFwPlCMhUQMB0RgbP-0-a954c7b42060de9fe3f06c22936415d5)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_135.jpg?sign=1739481648-yJpZ5QRF3ds1ZmvxNeMEWATczS1Yeb5D-0-40e811791f0e551bda792f758abfd9b8)
从前面几节对Camera的处理可以知道,旋转需要有旋转中心,所以我们需要找到图像的中心点,这里的mCenterX、mCenterY就表示图像中心点的坐标值。
计算函数是,在onSizeChanged生命周期中,通过w/2和h/2来获取最新的控件宽度和高度,以计算出控件中心点位置。
另外,对于ViewGroup而言,肯定会调用dispatchDraw函数,而onDraw函数则不一定会被调用(只有ViewGroup定义了背景色时才会调用),所以一般会在dispatchDraw函数中处理绘图事件。
这里主要将ViewGroup中的控件旋转mCanvasRotateX和mCanvasRotateY角度,这里的代码与前面两节中的都一样,故不再赘述。
2.捕捉按下、移动手势
在ViewGroup中,捕捉手势的方法很简单,只需要重写onTouchEvent函数即可。在这里,我们需要在onTouchEvent函数中根据用户的手指坐标计算出旋转角度(mCanvasRotateX、mCanvasRotateY):
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_136.jpg?sign=1739481648-wvV7VSQwCxjjaYcWgw6i0Mxbpl9R0Esr-0-010ba8ae97594890d6c307557a19229a)
可以看到,在onTouchEvent函数中只获取了手指的坐标位置,根据坐标位置计算出旋转角度是在rotateCanvasWhenMove(x,y)中实现的,具体实现如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_137.jpg?sign=1739481648-eFZuhmcLTZPCBYYEH6cmd0XAVUjngcKo-0-a5bd0c3774e0f67f6ceef8ce4cea208f)
首先定义了一个常量MAX_ROTATE_DEGREE=20,用于表示最大旋转角度,然后利用float percentX=dx/(getWidth()/2);计算出X轴的旋转百分比,其计算原理如图1-34所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_138.jpg?sign=1739481648-52CqPjaTbUqA6ogyEsB242WFdO4k7fKc-0-32d97ce9f3fb78558f025b7a121bb1c4)
图1-34
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_139.jpg?sign=1739481648-ogmQNquT2V19i7YhaVsetHJVVIUGYeHk-0-77113e56bcd6fec0472a912649b7c019)
扫码查看彩色图
在图1-34中,红线表示中心点到屏幕边缘的距离,即半屏宽度,它的长度是getWidth()/2,手指位置在红线上的绿点处。很明显,手指位置到中心点的距离是x-mCenterX,所以percentX表示的就是手指位置到中心点的距离占半屏宽度的多少。为了保险起见,这里也设置了最大旋转角度,当percentX大于1时,按1算,以此来控制最大旋转角度:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_140.jpg?sign=1739481648-Stp83ao6jNJKrJbETz78S9EaSPWLpSJK-0-e1225275312f8df1885a798f41cf6d93)
最后,根据percentX计算出mCanvasRotateY和mCanvasRotateX,并利用postInvalidate来更新界面即可。
因为我们在dispatchDraw函数中是用mCanvasRotateY和mCanvasRotateX来实现旋转的,所以改变它们之后再重绘,就可以实时更新旋转角度了。
3.捕捉抬起手势
最后,当抬起手指的时候,需要让图像复位,但这里需要注意,旋转时会同时改变rotateX和rotateY,所以在复位时,也需要同时将这两个变量复位:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_141.jpg?sign=1739481648-xJKXmrKndxsI4s2Pw8daAEadcFczB3Fm-0-f8665894446d2530c7f01ab6463e7587)
在抬起手指后,调用startNewSteadyAnim来实现复位动画,具体实现如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_142.jpg?sign=1739481648-8TCWPixsWD2bRPZS9R7bZcR9O0YjIrJD-0-e9003717a9f8367aea34130cd0c952f7)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_143.jpg?sign=1739481648-CS712w2MHhYy2TGm7GdqQwxTPHiGrs3G-0-d6b528fcc88f2145778b96eff7d12e77)
这里先声明了一个变量ValueAnimator mSteadyAnim,用来做数值动画。因为我们需要同时对rotateX和rotateY两个变量进行复位,所以需要使用能够同时操作多个数值的ValueAnimator构造方式,也就是使用PropertyValuesHolder来构造ValueAnimator实例。
在《Android自定义控件开发入门与实战》一书中,我们详细讲解过ValueAnimator和ObjectAnimator,其中PropertyValuesHolder的使用方法是放在ObjectAnimator中讲解的,具体在该书的4.1.1节。
对于ValueAnimator使用PropertyValuesHolder的方法,与ObjectAnimator类似,也是先构造PropertyValuesHolder实例:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_144.jpg?sign=1739481648-32HvuausM4ir47BuStVrDSVIJNAXl6um-0-8b87347c80d6097b178177b1a3bd2c77)
需要注意的是,onFloat的函数声明如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_145.jpg?sign=1739481648-lc2vYUyr5wDNNK8XDToFXfykoSrobF9D-0-de3ecb8cd83b82ecb67ca80b5c5d50e2)
其中的参数说明如下。
●propertyName:属性名,在动画过程中,我们将使用属性名来获取对应的值。
●float...values:代表数据的变换过程,其中3个点表示可变长度参数列表,即可以传入用逗号间隔的多个值。在这里,我们只传入两个值(mCanvasRotateX,0),表示数值的变化过程是从mCanvasRotateX变为0。
然后,通过ofPropertyValuesHolder函数构造ValueAnimator:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_146.jpg?sign=1739481648-hERUlpTeknieGTK3Y9XKHXQJXbc9wBvJ-0-810c8a989779c90814804ede1b35d3bb)
在这里,同时传入两个PropertyValuesHolder对象,表示ValueAnimator同时对这两个PropertyValuesHolder实例做动画。
然后,像其他ValueAnimator一样,通过监听AnimatorUpdateListener来实时获取动画过程中的Value值:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_147.jpg?sign=1739481648-BCUhOrOcnJ7drirhLlZGl5X5AKyrsc8n-0-5c6211069fa0555b91197e9ec19db01b)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_148.jpg?sign=1739481648-xPjjtNo4nDXn09eYqOJ2l7ttaXaQZzag-0-788bc7ffc1058481e53668997b333989)
这里需要注意获取动画过程中值的方式,以mCanvasRotateX为例:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_149.jpg?sign=1739481648-A0pPiuagbMmqPVtviLzCH5v7CAYi1GVO-0-265ccfc918eb742d0333c240c6c21131)
很明显,在构造holderRotateX时,我们指定的propertyName是propertyNameRotateX,所以在动画过程中对应的值是通过指定这个属性名来获取的:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_150.jpg?sign=1739481648-7Gvu1K4Pa3dfXZVgbuGPhJs6HCvr6O39-0-ae71efe6c9d3cad7508cbaeae4a2b415)
就这样,动画完成了,此时可扫码查看效果图。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_151.jpg?sign=1739481648-UdKQtI9cj7m848lG6kfADpAf18Pc4iAK-0-f4e4afceb5596759bc00312651c3389f)
扫码查看动态效果图
这个效果图不太明显,但仔细看可以看出,在手指抬起后,图像在做动画的过程中,手指再按下和移动都是无效的。这是为什么呢?
4.实时响应手势信息
很显然,因为动画还没有结束,所以还在持续地执行动画的界面刷新操作。其实,在手指按下的时候,界面也改变了,只不过被后来的动画界面刷新操作给覆盖了,看不出来而已。所以,要做到实时响应手势信息,就需要在响应手势信息前先停掉动画。下面我们对onTouchEvent进行改造:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_152.jpg?sign=1739481648-yOgasmQT6byyzrZi5ksOG3NxBbrqH0Tx-0-0505e2d402562781850b823af6a74907)
很明显,我们在处理ACTION_DOWN消息时,如果有动画,就先取消动画,取消动画的具体实现如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_153.jpg?sign=1739481648-k7C2Qk9AgnlVlClxCGY4O1GRFAdnsZJp-0-301803b49cddad8a992abdc88b0c0f24)
到这里,完整的根据手势来变换控件的ViewGroup就完成了,实现效果如1.4节开始时的效果。从本例可见,Camera与手势相结合并不困难,只需要在捕捉到手势信息之后,利用postInvalidate重绘界面,并在绘制界面时根据最新的值操作Camera。
1.4.3 ClockViewGroup应用
在前面的例子中,我们实现了ClockViewGroup,它继承自LinearLayout。很明显,它能包裹任何控件,并实现其中控件的手势操作。比如,我们对布局进行如下修改:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_154.jpg?sign=1739481648-lE2wsC0me93Skcjnzk09GiIvYrGr8X4F-0-e5a7e1160ea6965d98fa6077fee6717e)
其中包裹了3个控件,可扫码查看效果图。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_155.jpg?sign=1739481648-pb28gWiK4yuBtkrWlqLKtUsIUiDWBKtM-0-18a76b35190bd4f2e2d6d72117542fc0)
扫码查看动态效果图
到这里,有关Camera的基本使用方法就介绍完了。在第2章中,我们将具体讲解位置矩阵的使用方法。