Android如何實現(xiàn)3D效果

前言

前段時間讀到一篇文章,作者通過自定義View實現(xiàn)了一個高仿小米時鐘猜嘱,其中的3D效果很是吸引我,于是抽時間學(xué)習(xí)了一下纫普,現(xiàn)在總結(jié)出來阅悍,和大家分享。

正文

  1. 想要在Android上實現(xiàn)3D效果昨稼,其實并沒有想象中那么復(fù)雜节视,我們需要運用兩樣?xùn)|西:Camera和Matrix,這里的Camera可不是我們平常拍照用的Camera假栓,這里的Camera是位于android.graphics包下的Camera:

android.graphics.Camera
我們可以看到它的作用是:計算3D變換寻行,并且生成一個Matrix,可以應(yīng)用到Canvas上匾荆,這句話其實就是實現(xiàn)3D效果的核心原理拌蜘。
Camera的坐標(biāo)系是左手坐標(biāo)系杆烁。當(dāng)手機(jī)平整的放在桌面上,X軸是手機(jī)的水平方向简卧,Y軸是手機(jī)的豎直方向兔魂,Z軸是垂直于手機(jī)向里的那個方向。

Camera坐標(biāo)系

Camera位于坐標(biāo)點(0,0)举娩,也就是視圖的左上角析校。

我們再來了解一下Matrix,Android中的Matrix是一個3 x 3的矩陣铜涉,其內(nèi)容如下:


Matrix

從字面上來看智玻, 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軸)的垂直距離成正比掘宪。錯切變換,屬于等面積變換攘烛,即一個形狀在錯切變換的前后魏滚,其面積是相等的。

X軸錯切變換

上圖中坟漱,各點的y坐標(biāo)保持不變鼠次,但其x坐標(biāo)則按比例發(fā)生了平移。


Y軸錯切變換

上圖中,各點的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(float degrees).png
通過官方文檔我們可以了解到艇棕,rotate方法其實就是用matrix進(jìn)行前乘麦到,然后再將matrix應(yīng)用到當(dāng)前畫布上。
以下兩段代碼其實是等價的:

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 中的變換瑟慈。

  1. 熟悉了基本的工具桃移,我們就可以開工了,我們先來看一下最終的效果:


    3D效果.gif

    圓盤跟隨手指的移動而變換角度葛碧,呈現(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)注還有打賞筝野,真的特別開心晌姚。

最后,如有錯誤歇竟,歡迎指正挥唠。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市焕议,隨后出現(xiàn)的幾起案子宝磨,更是在濱河造成了極大的恐慌,老刑警劉巖盅安,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件唤锉,死亡現(xiàn)場離奇詭異,居然都是意外死亡别瞭,警方通過查閱死者的電腦和手機(jī)窿祥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝙寨,“玉大人晒衩,你說我怎么就攤上這事∏酵幔” “怎么了听系?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長箱亿。 經(jīng)常有香客問我跛锌,道長,這世上最難降的妖魔是什么届惋? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任髓帽,我火速辦了婚禮,結(jié)果婚禮上脑豹,老公的妹妹穿的比我還像新娘郑藏。我一直安慰自己,他們只是感情好瘩欺,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布必盖。 她就那樣靜靜地躺著拌牲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪歌粥。 梳的紋絲不亂的頭發(fā)上塌忽,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天,我揣著相機(jī)與錄音失驶,去河邊找鬼土居。 笑死,一個胖子當(dāng)著我的面吹牛嬉探,可吹牛的內(nèi)容都是我干的擦耀。 我是一名探鬼主播,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼涩堤,長吁一口氣:“原來是場噩夢啊……” “哼眷蜓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胎围,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤吁系,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后痊远,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體垮抗,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年碧聪,在試婚紗的時候發(fā)現(xiàn)自己被綠了曲尸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片洞难。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡太防,死狀恐怖蝙云,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情滞造,我是刑警寧澤续室,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站谒养,受9級特大地震影響挺狰,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜买窟,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一丰泊、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧始绍,春花似錦瞳购、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽年堆。三九已至,卻和暖如春盏浇,著一層夾襖步出監(jiān)牢的瞬間变丧,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工绢掰, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留锄贷,地道東北人。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓曼月,卻偏偏與公主長得像,于是被迫代替她去往敵國和親柔昼。 傳聞我的和親對象是個殘疾皇子哑芹,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容