
1.4 实现小米时钟的触摸倾斜效果
前面已经详细讲解了Camera类的使用函数,在本节的例子中,我们将学习Camera是如何与手势一起使用的。可扫码查看本节例子的效果图。

扫码查看动态效果图
从效果图可以看出,这个效果主要有以下几个特性。
(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。很明显,在这个例子中,主要是对它进行处理。下面搭建框架,先列出派生函数,不进行具体的实现:

在使用时,只需要在它内部包裹要操作的控件即可(activity_main.xml):


在使用时,在Activity中正常使用XML即可:

可以看到,整体框架非常简单。接下来,对于所有手指触摸操作图像的代码,将在自定义的ClockViewGroup中实现。
1.4.2 实现ClockViewGroup
1.绘制旋转后的控件
从效果图可以看出,在手指按下和滑动时,图片绕X轴和Y轴旋转了不同的角度,需要确定角度是多少,dispatchDraw中旋转操作的代码如下。下面列出了完整代码,后面再分解讲述:


从前面几节对Camera的处理可以知道,旋转需要有旋转中心,所以我们需要找到图像的中心点,这里的mCenterX、mCenterY就表示图像中心点的坐标值。
计算函数是,在onSizeChanged生命周期中,通过w/2和h/2来获取最新的控件宽度和高度,以计算出控件中心点位置。
另外,对于ViewGroup而言,肯定会调用dispatchDraw函数,而onDraw函数则不一定会被调用(只有ViewGroup定义了背景色时才会调用),所以一般会在dispatchDraw函数中处理绘图事件。
这里主要将ViewGroup中的控件旋转mCanvasRotateX和mCanvasRotateY角度,这里的代码与前面两节中的都一样,故不再赘述。
2.捕捉按下、移动手势
在ViewGroup中,捕捉手势的方法很简单,只需要重写onTouchEvent函数即可。在这里,我们需要在onTouchEvent函数中根据用户的手指坐标计算出旋转角度(mCanvasRotateX、mCanvasRotateY):

可以看到,在onTouchEvent函数中只获取了手指的坐标位置,根据坐标位置计算出旋转角度是在rotateCanvasWhenMove(x,y)中实现的,具体实现如下:

首先定义了一个常量MAX_ROTATE_DEGREE=20,用于表示最大旋转角度,然后利用float percentX=dx/(getWidth()/2);计算出X轴的旋转百分比,其计算原理如图1-34所示。

图1-34

扫码查看彩色图
在图1-34中,红线表示中心点到屏幕边缘的距离,即半屏宽度,它的长度是getWidth()/2,手指位置在红线上的绿点处。很明显,手指位置到中心点的距离是x-mCenterX,所以percentX表示的就是手指位置到中心点的距离占半屏宽度的多少。为了保险起见,这里也设置了最大旋转角度,当percentX大于1时,按1算,以此来控制最大旋转角度:

最后,根据percentX计算出mCanvasRotateY和mCanvasRotateX,并利用postInvalidate来更新界面即可。
因为我们在dispatchDraw函数中是用mCanvasRotateY和mCanvasRotateX来实现旋转的,所以改变它们之后再重绘,就可以实时更新旋转角度了。
3.捕捉抬起手势
最后,当抬起手指的时候,需要让图像复位,但这里需要注意,旋转时会同时改变rotateX和rotateY,所以在复位时,也需要同时将这两个变量复位:

在抬起手指后,调用startNewSteadyAnim来实现复位动画,具体实现如下:


这里先声明了一个变量ValueAnimator mSteadyAnim,用来做数值动画。因为我们需要同时对rotateX和rotateY两个变量进行复位,所以需要使用能够同时操作多个数值的ValueAnimator构造方式,也就是使用PropertyValuesHolder来构造ValueAnimator实例。
在《Android自定义控件开发入门与实战》一书中,我们详细讲解过ValueAnimator和ObjectAnimator,其中PropertyValuesHolder的使用方法是放在ObjectAnimator中讲解的,具体在该书的4.1.1节。
对于ValueAnimator使用PropertyValuesHolder的方法,与ObjectAnimator类似,也是先构造PropertyValuesHolder实例:

需要注意的是,onFloat的函数声明如下:

其中的参数说明如下。
●propertyName:属性名,在动画过程中,我们将使用属性名来获取对应的值。
●float...values:代表数据的变换过程,其中3个点表示可变长度参数列表,即可以传入用逗号间隔的多个值。在这里,我们只传入两个值(mCanvasRotateX,0),表示数值的变化过程是从mCanvasRotateX变为0。
然后,通过ofPropertyValuesHolder函数构造ValueAnimator:

在这里,同时传入两个PropertyValuesHolder对象,表示ValueAnimator同时对这两个PropertyValuesHolder实例做动画。
然后,像其他ValueAnimator一样,通过监听AnimatorUpdateListener来实时获取动画过程中的Value值:


这里需要注意获取动画过程中值的方式,以mCanvasRotateX为例:

很明显,在构造holderRotateX时,我们指定的propertyName是propertyNameRotateX,所以在动画过程中对应的值是通过指定这个属性名来获取的:

就这样,动画完成了,此时可扫码查看效果图。

扫码查看动态效果图
这个效果图不太明显,但仔细看可以看出,在手指抬起后,图像在做动画的过程中,手指再按下和移动都是无效的。这是为什么呢?
4.实时响应手势信息
很显然,因为动画还没有结束,所以还在持续地执行动画的界面刷新操作。其实,在手指按下的时候,界面也改变了,只不过被后来的动画界面刷新操作给覆盖了,看不出来而已。所以,要做到实时响应手势信息,就需要在响应手势信息前先停掉动画。下面我们对onTouchEvent进行改造:

很明显,我们在处理ACTION_DOWN消息时,如果有动画,就先取消动画,取消动画的具体实现如下:

到这里,完整的根据手势来变换控件的ViewGroup就完成了,实现效果如1.4节开始时的效果。从本例可见,Camera与手势相结合并不困难,只需要在捕捉到手势信息之后,利用postInvalidate重绘界面,并在绘制界面时根据最新的值操作Camera。
1.4.3 ClockViewGroup应用
在前面的例子中,我们实现了ClockViewGroup,它继承自LinearLayout。很明显,它能包裹任何控件,并实现其中控件的手势操作。比如,我们对布局进行如下修改:

其中包裹了3个控件,可扫码查看效果图。

扫码查看动态效果图
到这里,有关Camera的基本使用方法就介绍完了。在第2章中,我们将具体讲解位置矩阵的使用方法。