前言
前段時間讀到一篇文章,作者通過自定義View實現(xiàn)了一個高仿小米時鐘猜嘱,其中的3D效果很是吸引我,于是抽時間學(xué)習(xí)了一下纫普,現(xiàn)在總結(jié)出來阅悍,和大家分享。
正文
- 想要在Android上實現(xiàn)3D效果昨稼,其實并沒有想象中那么復(fù)雜节视,我們需要運用兩樣?xùn)|西:Camera和Matrix,這里的Camera可不是我們平常拍照用的Camera假栓,這里的Camera是位于android.graphics包下的Camera:
Camera的坐標(biāo)系是左手坐標(biāo)系杆烁。當(dāng)手機(jī)平整的放在桌面上,X軸是手機(jī)的水平方向简卧,Y軸是手機(jī)的豎直方向兔魂,Z軸是垂直于手機(jī)向里的那個方向。
Camera位于坐標(biāo)點(0,0)举娩,也就是視圖的左上角析校。
我們再來了解一下Matrix,Android中的Matrix是一個3 x 3的矩陣铜涉,其內(nèi)容如下:
從字面上來看智玻, MSCALE用于處理縮放變換,MTRANS用于處理平移變換芙代,MSKEW用于處理錯切變換吊奢。最后一行的MPERSP用于處理透視變換,關(guān)于透視變換链蕊,官方文檔中并沒有具體的說明事甜,這里也就不再贅述。另外滔韵,矩陣是支持旋轉(zhuǎn)變換的逻谦,旋轉(zhuǎn)變換是通過同時設(shè)置MSCALE和MSKEW來實現(xiàn)的(這里邊就是一些數(shù)學(xué)原理了,筆者也是半壺水陪蜻,就不在這丟人了邦马,感興趣的同學(xué)可以自己研究一下)。另外有同學(xué)可能對錯切變換也不是特別理解宴卖,筆者當(dāng)時也是自己查了下才明白滋将,這里簡單說明一下,就免得大家再去百度了:
錯切變換(skew)在數(shù)學(xué)上又稱為Shear mapping(可譯為“剪切變換”)或者Transvection(縮并)症昏,它是一種比較特殊的線性變換随闽。錯切變換的效果就是讓所有點的x坐標(biāo)(或者y坐標(biāo))保持不變,而對應(yīng)的y坐標(biāo)(或者x坐標(biāo))則按比例發(fā)生平移肝谭,且平移的大小和該點到x軸(或y軸)的垂直距離成正比掘宪。錯切變換,屬于等面積變換攘烛,即一個形狀在錯切變換的前后魏滚,其面積是相等的。
上圖中坟漱,各點的y坐標(biāo)保持不變鼠次,但其x坐標(biāo)則按比例發(fā)生了平移。
上圖中,各點的x坐標(biāo)保持不變腥寇,但其y坐標(biāo)則按比例發(fā)生了平移成翩。
還有一個不容易理解的地方,Matrix針對每種變換花颗,都提供了set捕传、pre和post三種操作方式。
pre方法表示矩陣前乘扩劝,如果變換矩陣為A庸论,原始矩陣為M,pre方法即是 M x A
post方法表示矩陣后乘棒呛,如果變換矩陣為A聂示,原始矩陣為M,post方法即是 A x M
之所以需要區(qū)分前乘和后乘簇秒,是因為矩陣的乘法不滿足交換率鱼喉,即 A x M != M x A
另外還有比較重要的一點, 在圖像處理中趋观,越靠近右邊的矩陣越先執(zhí)行
調(diào)用一系列set扛禽、pre、post方法時皱坛,可以理解為將這些操作插入一個隊列:set是清空隊列再添加编曼,pre是在隊首插入,post是在隊尾插入剩辟。
舉個栗子:
Matrix m = new Matrix();
m.postTranslate(20, 20);
m.preScale(0.2f, 0.5f);
m.setScale(0.8f, 0.8f);
m.postScale(3f, 3f);
m.preTranslate(0.5f, 0.5f);
執(zhí)行順序為:preTranslate(0.5f, 0.5f) → setScale(0.8f, 0.8f) → postScale(3f, 3f)
因為setScale(0.8f, 0.8f)會將前面的postTranslate(20, 20)和preScale(0.2f, 0.5f)清除掉掐场,然后再將postScale(3f, 3f)插入隊尾,preTranslate(0.5f, 0.5f)插入隊首贩猎。
2018.4.25補(bǔ)充---Canvas的幾何變換:
Canvas的幾何變換方法包括 translate熊户、rotate、scale吭服、skew 幾種嚷堡,其原理也是運用matrix做幾何變換。
我們以 canvas.rotate(float degrees) 方法舉例:
以下兩段代碼其實是等價的:
canvas.rotate(-degreeZ); // 1
Matrix matrix = new Matrix(); // 2
matrix.reset();
matrix.preRotate(-degreeZ);
canvas.concat(matrix);
由于是前乘欠肾,所以canvas的幾何變換方法是倒序的,需要把變換的代碼倒著寫拟赊,舉個栗子:
// 如果想要讓canvas先移動 (-centerX, -centerY) 距離刺桃,再移動 (centerX, centerY) 距離進(jìn)行恢復(fù)
// 代碼需要倒著寫
canvas.translate(centerX, centerY);
canvas.translate(-centerX, -centerY);
這里再補(bǔ)充一下 canvas.concat(matrix) 方法:
用 Canvas 當(dāng)前的變換矩陣和 Matrix 相乘,即基于 Canvas 當(dāng)前的變換吸祟,疊加上 Matrix 中的變換瑟慈。
-
熟悉了基本的工具桃移,我們就可以開工了,我們先來看一下最終的效果:
圓盤跟隨手指的移動而變換角度葛碧,呈現(xiàn)出3D的效果借杰,看起來還是很不錯的,我們看看如何來實現(xiàn)這個效果吧:
首先我們需要做一些初始化的工作:
private Paint mWhitePaint;
private Paint mCirclePaint;
private float mCircleStrokeWidth = 2;
private float mMaxRadius = 300;
/* Camera旋轉(zhuǎn)的最大角度 */
private float mMaxCameraRotate = 15;
/* 我們今天的主角 */
private Matrix mMatrix;
private Camera mCamera;
/* Camera繞X軸旋轉(zhuǎn)的角度 */
private float mCameraRotateX;
/* Camera繞Y軸旋轉(zhuǎn)的角度 */
private float mCameraRotateY;
private void init(){
mMatrix = new Matrix();
mCamera = new Camera();
//白色大圓的畫筆
mWhitePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mWhitePaint.setStyle(Paint.Style.FILL_AND_STROKE);
mWhitePaint.setStrokeWidth(mCircleStrokeWidth);
mWhitePaint.setColor(Color.WHITE);
//內(nèi)部藍(lán)色圓環(huán)的畫筆
mCirclePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setStrokeWidth(mCircleStrokeWidth);
mCirclePaint.setColor(Color.WHITE);
mCirclePaint.setColor(0xff237EAD);
}
這是重寫的onDraw方法进泼,做了兩件事情:
1.將canvas傳入setCameraRotate方法中
2.再畫幾個圈圈
@Override
protected void onDraw(Canvas canvas) {
setCameraRotate(canvas);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mMaxRadius, mWhitePaint);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mMaxRadius / 6 * 5, mCirclePaint);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mMaxRadius / 6 * 4, mCirclePaint);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mMaxRadius / 6 * 3, mCirclePaint);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mMaxRadius / 6 * 2, mCirclePaint);
}
接下來我們看看setCameraRotate方法里面做了什么
private void setCameraRotate(Canvas mCanvas) {
mMatrix.reset();
mCamera.save();
mCamera.rotateX(mCameraRotateX);//繞x軸旋轉(zhuǎn)
mCamera.rotateY(mCameraRotateY);//繞y軸旋轉(zhuǎn)
mCamera.getMatrix(mMatrix);//計算對于當(dāng)前變換的矩陣蔗衡,并將其復(fù)制到傳入的mMatrix中
mCamera.restore();
/**
* Camera默認(rèn)位于視圖的左上角,故生成的矩陣默認(rèn)也是以其左上角為旋轉(zhuǎn)中心乳绕,
* 所以在動作之前調(diào)用preTranslate將mMatrix向左移動getWidth()/2個長度绞惦,
* 向上移動getHeight()/2個長度,
* 使旋轉(zhuǎn)中心位于矩陣的中心位置洋措,動作之后再post回到原位
*/
mMatrix.preTranslate(-getWidth() / 2, -getHeight() / 2);
mMatrix.postTranslate(getWidth() / 2, getHeight() / 2);
mCanvas.concat(mMatrix);//將mMatrix與canvas中當(dāng)前的Matrix相關(guān)聯(lián)
}
以上這段代碼济蝉,除了旋轉(zhuǎn)操作以外,其余的基本屬于固定寫法菠发,這樣寫的原因都在注釋里寫清楚了王滤,可能有點不太好理解,多看幾遍或者自己試著寫一下就明白了滓鸠。
上面這段代碼中的mCameraRotateX和mCameraRotateY這兩個全局變量的值應(yīng)該與此時手指觸摸坐標(biāo)相關(guān)聯(lián)雁乡,所以我們在onTouchEvent方法中動態(tài)設(shè)置:
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//根據(jù)手指坐標(biāo)計算Camera應(yīng)該旋轉(zhuǎn)的角度
getCameraRotate(event);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
getCameraRotate(event);
invalidate();
break;
}
return true;
}
我們最后來看看getCameraRotate方法中是如何處理的:
private void getCameraRotate(MotionEvent event) {
float rotateX = -(event.getY() - getHeight() / 2);
float rotateY = (event.getX() - getWidth() / 2);
/**
*為什么旋轉(zhuǎn)角度要這樣計算:
* 當(dāng)Camera.rotateX(x)的x為正時,圖像圍繞X軸哥力,上半部分向里下半部分向外蔗怠,進(jìn)行旋轉(zhuǎn),
* 也就是手指觸摸點要往上移吩跋。這個x就會與event.getY()的值有關(guān)寞射,x越大,繞X軸旋轉(zhuǎn)角度越大锌钮,
* 以圓心為基準(zhǔn)桥温,手指往上移動,event.getY() - getHeight() / 2的值為負(fù)梁丘,
* 故 float rotateX = -(event.getY() - getHeight() / 2)
* 同理侵浸,
* 當(dāng)Camera.rotateY(y)的y為正時,圖像圍繞Y軸氛谜,右半部分向里左半部分向外掏觉,進(jìn)行旋轉(zhuǎn),
* 也就是手指觸摸點要往右移值漫。這個y就會與event.getX()的值有關(guān)澳腹,y越大,繞Y軸旋轉(zhuǎn)角度越大,
* 以圓心為基準(zhǔn)酱塔,手指往右移動沥邻,event.getX() - getWidth() / 2的值為正,
* 故 float rotateY = event.getX() - getWidth() / 2
*/
/**
* 此時得到的rotateX羊娃、rotateY 其實是以圓心為基準(zhǔn)唐全,手指移動的距離,
* 這個值很大蕊玷,不能用來作為旋轉(zhuǎn)的角度邮利,
* 所以還需要繼續(xù)處理
*/
//求出移動距離與半徑之比。mMaxRadius為白色大圓的半徑
float percentX = rotateX / mMaxRadius;
float percentY = rotateY / mMaxRadius;
if (percentX > 1) {
percentX = 1;
} else if (percentX < -1) {
percentX = -1;
}
if (percentY > 1) {
percentY = 1;
} else if (percentY < -1) {
percentY = -1;
}
//將最終的旋轉(zhuǎn)角度控制在一定的范圍內(nèi)集畅,這里mMaxCameraRotate的值為15近弟,效果比較好
mCameraRotateX = percentX * mMaxCameraRotate;
mCameraRotateY = percentY * mMaxCameraRotate;
}
到這里,我們要的3D效果就已經(jīng)實現(xiàn)了挺智。也是費了一番功夫祷愉。
結(jié)語
寫這篇文章的起因是讀到了猴菇先生的博客高仿小米時鐘 - 使用Camera和Matrix實現(xiàn)3D效果對文中實現(xiàn)的3D效果產(chǎn)生了興趣,但是文中主要的篇幅還是介紹如何自定義View赦颇,關(guān)于3D效果的實現(xiàn)只有主要代碼和簡單的注釋二鳄,所以我又自己從Camera和Matrix的定義開始,將3D效果作為主體重新學(xué)習(xí)了一遍媒怯,便是有了這篇文章订讼。文中的部分代碼也是從猴菇先生的代碼中借鑒的,將代碼進(jìn)行了簡化扇苞,只保留了3D效果的部分欺殿,將注釋和說明進(jìn)行了豐富,從頭開始講解鳖敷,更加易于學(xué)習(xí)脖苏。
從開始研究到寫完這篇文章,斷斷續(xù)續(xù)加起來差不多花了2天時間定踱,發(fā)現(xiàn)寫文章確實很鍛煉人棍潘,以前自己遇到問題,上網(wǎng)隨便搜搜崖媚,看個大概亦歉,就完事兒了。現(xiàn)在想要寫出來畅哑,必須要弄明白肴楷、透徹,才敢動手寫荠呐,也算是對自己的一種監(jiān)督吧阶祭,我可不愿意誤人子弟绷杜,所以經(jīng)常寫到一半,發(fā)現(xiàn)某些地方不是特別清楚濒募,又回過頭去弄明白了再繼續(xù)寫,寫完之后圾结,收獲也是大大的瑰剃。而且之前寫的文章還收到了點贊、關(guān)注還有打賞筝野,真的特別開心晌姚。
最后,如有錯誤歇竟,歡迎指正挥唠。