![Android自定义控件高级进阶与精彩实例](https://wfqqreader-1252317822.image.myqcloud.com/cover/747/36511747/b_36511747.jpg)
1.1 3D特效概述
在Android中,总能看到一些非常酷炫的3D特效,很多人觉得它们很难,在本章中我们就来“啃啃这块硬骨头”。本章将主要介绍位置坐标系变换的相关操作,其中关于Matrix操作的部分可能有些难以理解,但不用太担心,在本章中不会过多涉及Matrix操作,而是主要通过其辅助类Camera来完成3D变换的。在后续的章节中,我们才会具体讲解Matrix操作的用法。下面就带大家领略两个非常酷炫的3D特效。
注意:以下两个项目均非我原创,书中留有如何查找它们的方法,有需要的读者可以自行查找并加以了解,在此感谢项目作者做出的贡献。
小米时钟效果:当手指触碰时钟时,该时钟会出现3D偏转效果,而且会随着手指的移动转向不同的角度,如图1-1所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_1.jpg?sign=1739290381-BeqTVieONsD95rA98XIy2rpvMakMY4cl-0-4a1d58b4953482c5e6e00de94bc8d38d)
图1-1
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_2.jpg?sign=1739290381-1Flpg7NjdvTJrFQ1qJiL9K9FoZ0sKdwg-0-ee86fa64e7a5221b012611c6b49f169c)
扫码查看动态效果图
项目地址:请移步GitHub并搜索MiClockView。
3D翻转效果:这里有一个Container组件,其可以包裹任何控件,以实现被包裹控件的3D翻转效果,比如实现翻转输入账号信息的效果,如图1-2所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_3.jpg?sign=1739290381-dF8M3zAWjHOkfFWcCZULwUJmBfWEo3T8-0-b267fbe78466f4ecc54de8ca61071c61)
图1-2
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_4.jpg?sign=1739290381-vKjXfHZjiihImnBBKmumeGtHVITqnOQE-0-35745551083766438251aabc6b02c343)
扫码查看动态效果图
这个Container组件也可以包裹图片,以实现翻转、浏览图片的效果,如图1-3所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_5.jpg?sign=1739290381-bCFHjqmGm6jKV5B6GibO5FNvThMo3qzn-0-475d5b9abeab35b8a6777439394d0092)
图1-3
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_6.jpg?sign=1739290381-jxSNzOn71tGoOb3SLe4ly0jShlA4yryq-0-ff7ced0b46bcc8e789afc21a63fcdf00)
扫码查看动态效果图
项目地址:请移步GitHub并搜索StereoView。
GitHub上还有很多优秀的3D翻转效果控件,这里就不再一一列举了,大家感兴趣的话,可以自己去找找。在掌握了本章的内容以后,大家就可以读懂这些控件的源码了。
1.1.1 2D坐标系与3D坐标系
1.2D坐标系
在2D和3D的概念中,D是单词Dimension的缩写,所以2D坐标系也叫二维坐标系,3D坐标系也叫三维坐标系。
我们经常会使用2D坐标系,在手机屏幕上使用的坐标系就是2D坐标系,如图1-4所示。
图1-4中的紫色区域表示手机屏幕,绿色区域表示屏幕上的View控件。以屏幕左上角为原点的坐标系,是2D坐标系中的绝对坐标系。同时,还有一种以View控件的左上角为原点的坐标系,其被称为相对坐标系。有关绝对坐标系和相对坐标系的概念将在4.3节中进行详细的讲解。无论是绝对坐标系还是相对坐标系,都可以定位屏幕上的位置,都只有X轴和Y轴两个维度,所以它们都是2D(二维)坐标系。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_7.jpg?sign=1739290381-qgWwDSCSwbYn5F9Z9QkYklmvl6HbTD03-0-445b1b732b5fe5bad71ef21e15610bf6)
图1-4
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_8.jpg?sign=1739290381-12oITCWsq7uHqMGHbnKQ6kY0hdRoLE5j-0-d3ac80a4eff13650314b787e91c911ef)
扫码查看彩色图
2.3D坐标系
(1)左手坐标系与右手坐标系
3D坐标系分为左手坐标系和右手坐标系,如图1-5所示。
●左手坐标系:伸出左手,让拇指和食指成“L”形,拇指向上,食指向前,其余的手指向右,这样就建立了一个左手坐标系。拇指、食指和其余手指分别代表X、Y、Z轴的正方向。
●右手坐标系:伸出右手,让拇指和食指成“L”形,拇指向上,食指向前,其余的手指向左,这样就建立了一个右手坐标系。拇指、食指和其余手指同样分别代表X、Y、Z轴的正方向。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_9.jpg?sign=1739290381-1H6CtnJ3lSlRNdgdAPaTB2PCsetGqTdI-0-5a837df895c37437b0e83ed96053b9ab)
图1-5
可以看出,左手坐标系与右手坐标系的区别就在于Z轴的正方向刚好相反。在数学中,右手坐标系用得多;在计算机中,左手坐标系用得多。
注意:以下讲到3D坐标系时,不再说明是左手坐标系还是右手坐标系,默认使用的都是左手坐标系。
(2)3D坐标系的方向
屏幕上的3D坐标系与2D坐标系没有一点儿关系,下面是2D坐标系和3D坐标系的示意图,如图1-6所示。
同样是屏幕和控件View,但在3D坐标系中,X轴是向右的,Y轴并不是沿着屏幕向下的,而是沿着屏幕向上的,Z轴是垂直屏幕向屏幕里的。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_10.jpg?sign=1739290381-m1AVNkFBaEaPSUKE5ialjCD4s2KxxCXB-0-db590e23a8d912617dd9fd4f1ed693d8)
图1-6
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_11.jpg?sign=1739290381-b4SYYWwyP6tQNyk49QS1rpVNo3BOUpwW-0-5c52cf1431d315f9828af970458bd094)
扫码查看彩色图
3.3D坐标系与屏幕的关系
我们知道,屏幕本身是二维的,那么3D坐标系与屏幕有什么关系呢?怎么在一个二维的屏幕上显示三维物体呢?
这个过程其实非常像用手机拍照。用手机拍照就是将一个三维物体投影到一个二维平面上,形成一个二维的像。这个拍照与成像的过程恰好就是通过屏幕显示三维物体的过程。
下面就以拍照为例来讲解如何在照片上成像。
手机成像原理相对复杂,本节将略去翻转成像、相机坐标系等概念,若感兴趣的读者想要了解手机成像的完整过程,请自行查阅相关资料。
手机的成像原理可以简化为图1-7(以下描述为了方便理解而进行了简化和抽象,真实的手机成像原理请读者查阅相关资料)。人眼通过屏幕看到的物体在屏幕上所成的像,就是我们最终拍照留下来的二维图像。以人眼为原点,与物体上各点的连线,都会从屏幕穿过,可以将连线从屏幕穿过时所形成的图像视为我们最终拍照留下的二维图像。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_12.jpg?sign=1739290381-S2JRwymawJQzLkmgmXmoJK0oYYvTnzab-0-fb3d48d7ad4d2474fac52931210934d1)
图1-7
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_13.jpg?sign=1739290381-3ekfsGpCD95aVQL242ZRloNvefs7ExjQ-0-e7da48e2dd14e2cc248edc10042a1033)
扫码查看彩色图
其实对于手机,有一个Camera的概念,Camera可直译为摄像机,这个Camera相当于观察虚拟3D世界的眼睛。根据上面所述,Camera与物体各点的视觉连线在屏幕上的交点所成的图像可视为三维物体在屏幕上的二维成像。当我们让三维物体动起来(或者移动Camera)的时候,屏幕上所成的图像就会发生变化,看起来就像屏幕上显示的是三维物体。
1.1.2 Android中的Camera类
前面提到过,在Android中有一个Camera的概念,这个Camera相当于观察虚拟3D世界的眼睛。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_14.jpg?sign=1739290381-W7f10Nqxva5SxthXgZC7d7kQANLbcl2i-0-4173ac4b12c9bd9104b528730d4bd0e3)
扫码查看动态效果图
在Android中观察View的Camera位于屏幕外,默认情况下Android中Camera的位置与屏幕的关系可以扫码查看右侧效果图,其中灰色部分是手机屏幕,白色部分是上面的View。
从效果图可以看出,Android中拍摄物体的虚拟摄像机是位于屏幕外的,它的位置如图1-8所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_15.jpg?sign=1739290381-OObJ9aOMycQWXsr0BqFx3HUrcHgCgkKS-0-a7bc0c684e096e3771033f31da35d7ec)
图1-8
Camera位于Android 3D坐标系的(0,0,-576)位置。与2D坐标系一样,3D坐标系也分屏幕坐标系和View坐标系(也叫视图坐标系),区别在于坐标系原点的位置。屏幕坐标系的原点在屏幕的左上角,而View坐标系的原点在View的左上角。
如图1-8所示的坐标系的原点在屏幕的左上角,所以是3D屏幕坐标系。有关屏幕坐标系和View坐标系的区别、联系、何时使用的具体讲解,请参考4.3节“坐标系”。
在本章之后的实例讲解中,全部使用的是View坐标系。也就是说,坐标系原点在View的左上角,Camera的位置位于3D View坐标系的(0,0,-576)处。
Camera在Android中由一个单独的类来表示,即android.graphics.Camera类。需要注意,这个类专门针对Android中默认的Camera,用于移动、旋转Camera等动作。另外,还有一个同名的Camera类,用于手机拍照,类名是android.hardware.Camera。
下面就来看看Camera类中提供的一些基本函数,然后尝试初步使用它们。
1.基本函数
基本函数就是save和restore函数,其主要作用是保存当前状态和恢复到上一次保存的状态,这两个函数通常是成对使用的,常用格式如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_16.jpg?sign=1739290381-UrNcMeL24fayIRnBiOnKwHu8LX5okH41-0-a9d0246a3d4c3429f7078af37495a29c)
Camera类的用法与Canvas类的用法完全相同,都使用save和restore函数来执行保存和恢复操作。save和restore函数的具体用法此处不会细讲,感兴趣的读者可参考《Android自定义控件开发入门与实战》一书的1.4节。
2.应用于Canvas
我们来思考一个问题:Camera表示摄像机,在改变Camera位置以后,要怎么将Camera表现在Canvas上呢?要知道屏幕上的所有图像都是通过Canvas画出来的。
答案就是使用Matrix操作。Camera在完成了坐标变换以后,可以计算出Canvas具体应该如何变换的矩阵,然后将这个矩阵应用于Canvas。这样,Canvas画出来的内容就是Camera变换以后的样子了。
将Camera应用于Canvas有如下两个函数,分别进行介绍。
函数一:通过Camera.getMatrix(matrix)
完整的函数声明如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_17.jpg?sign=1739290381-jacY9ZYSOs386JD1SbmqKGHMvTyvat97-0-c0c1e608cca390888ae0d9cca59aac4f)
该函数的具体用法如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_18.jpg?sign=1739290381-clgb1pLSBkoAOysNgxdI5lJNtJ4KbqyU-0-1d5b2b2c96ffafaf06d1840994b67e4c)
可以看到,上面的代码中先利用camera.getMatrix(matrix)函数获得了Camera变换坐标,然后系统计算出Canvas应该如何变换的矩阵,以应对Camera的变换。
然后在Canvas绘图前,调用canvas.setMatrix(Matrix matrix)函数。
函数二:通过Camera.applyToCanvas(canvas)
完整的函数声明如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_19.jpg?sign=1739290381-2YjytJoFdqPxwYT99Dy6J1c9Etl9d7gC-0-52f92c650cf4ebd6eef41b1348e9e7ed)
这个函数省去了上面函数那些让人觉得麻烦的步骤,而是直接将计算出的矩阵应用于Canvas。
该函数的具体用法如下:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_20.jpg?sign=1739290381-W3zrBiZKC4I21wyl6MjvdFVrza65NFAy-0-d220d269c8ff1f7dbc637fd41f7ebf5e)
这里的代码相对比较简单,但是需要注意,为了防止原来的Canvas改变,需要在调用camera.applyToCanvas(canvas)函数前,使用save函数保存原始的Canvas,而在使用完Canvas以后,再使用restore函数来将Canvas还原至初始状态。
1.1.3 构造Camera类使用实例
在具体了解Camera类有哪些函数以前,我们先来写一个demo,为讲解Camera类的具体函数做准备。
demo的效果如图1-9所示。
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_21.jpg?sign=1739290381-FOLev8inF3vcaEdi6giM6n1X528rnwFj-0-163b7f09eed3fc50d914d3975ccc2cfb)
图1-9
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_22.jpg?sign=1739290381-IYRcNQSQmKQ0MK5Y9CjhYSwkxgxIEB2o-0-0e73cd881bc57ffe088780b885fb4694)
扫码查看动态效果图
1.布局架构
从图1-9可以看出,这里的布局非常简单,自上而下分别是3个控件(activity_translate.xml):
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_23.jpg?sign=1739290381-EdsNEMMdjkQS47I0iT3JBKSIpPaFsZOl-0-f439ab01eff3361d05a0cd3a582c5aa2)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_24.jpg?sign=1739290381-CBaXIREu4qzPYWtLXNNBqSdVRzN649Uu-0-0745411c9de7161eab105f202ad9a409)
自上而下的第1个控件是拖动条,值的范围是1~360,用于为自定义控件CameraImageView设置值。第2个控件是TextView,用于实时显示当前拖动条的数值。第3个控件是CameraImageView,这是一个自定义控件,能够显示图片且根据拖动条当前的值来自动更新状态。
然后,看看在Activity中是如何进行处理的:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_25.jpg?sign=1739290381-HS5hBO5yhVh7knXhOEjTpgRjo2Ssu5bt-0-52e5e626d5acc212038f4c270f346bb9)
上面的代码处理逻辑也非常简单,只是实时监听SeekBar的变化。当拖动条的位置发生变化时,将变化的值同时赋给控件TextView和CameraImageView。
在了解了大概的流程之后,下面来看看CameraImageView是如何实现的。
2.CameraImageView的实现
首先,根据效果来观察CameraImageView继承自什么控件。
因为CameraImageView的功能是显示ImageView,通过更改绘制时的Canvas来实现图像的变化,所以其最简单的实现方式是通过继承自ImageView来实现。
当然,也可以通过继承自View来实现,但若通过View实现的话,需要自己画Bitmap,也需要自己编写onMeasure、onLayout等函数来实现测量和布局功能。
这里的代码比较简单,下面直接列出,并对重要部分进行讲解:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_26.jpg?sign=1739290381-PpVe138ea9VRjTQKAPOT3EDObLhRCIbw-0-e47712e49f53e4d388707d7c3c4f3426)
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_27.jpg?sign=1739290381-iSoGaEkMoPbzpEwNJdpRLMwGF5kqScRq-0-2f592d015e865f884c556e28b7b24a57)
首先,开始时主要做一些初始化工作,比如加载图片等。
然后,对外暴露设置progress的函数:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_28.jpg?sign=1739290381-0VTiSZQ9Z3083oA3j03zW7WN5k8VVWK7-0-b623a65fe0febfeec087dbde78078181)
其中会先把progress保存起来,然后调用postInvalidate函数来刷新界面。至于postInvalidate与invalidate的区别,我已经在《Android自定义控件开发入门与实战》一书中多次提到,这里就不再赘述了。
最后,也是最复杂的部分,即onDraw函数。在onDraw函数中,首先将Camera与Canvas的状态保存下来,以便后面需要时进行恢复:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_29.jpg?sign=1739290381-t2MBvJ062h0gKmmN9ldkm054Ncg2UFrt-0-6837bc7187e026ddd13b72a223984c4d)
从图1-9可以看出,在自定义控件中,其实有两张图片,下层是一张半透明的图片,用于显示图片的原始位置,而上层的图片才会随着Camera的变化而旋转。
所以,这里分3步进行处理。第1步,在下层画一张半透明的图片:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_30.jpg?sign=1739290381-YpnugejVfAutStIBRTQ1MfQr31NoX6ra-0-d49d6e80ebac1cc6b24f9c42a58a035e)
第2步,旋转画布,并应用于Canvas:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_31.jpg?sign=1739290381-tNgHBMI917THmi703s8KozpCmD3yX1jg-0-b9d55dff885017c155344b90e38b96c9)
第3步,利用ImageView自带的显示图片的函数super.onDraw(canvas),在已经旋转过的Canvas上绘图,此时绘制出来的图就是显示在上层的旋转过的图片了:
![img](https://epubservercos.yuewen.com/0CBE40/19391577408683706/epubprivate/OEBPS/Images/txt001_32.jpg?sign=1739290381-xUUhjLxQcl68oA509mYEhU7p8gShfwUj-0-3246ae306d135236437939df7120b170)
需要注意,在使用Canvas和Camera后,都需要调用各自的restore函数来恢复到原始位置,避免这次的变更影响到下次。
这样,本节开篇时描述的效果就实现了。在1.2节中,我们将基于这个例子来讲解Camera类中的各个函数。