Camera與Matrix的那些事兒

1、引子

筆者剛開(kāi)始工作時(shí),做的第一個(gè)模塊是手機(jī)中的launcher约素,launcher可自由選擇滑屏效果届良,甚至還有三D效果,酷炫的動(dòng)畫(huà)讓人震驚同時(shí)也感到非常迷惑圣猎,這動(dòng)畫(huà)效果怎么實(shí)現(xiàn)的呢士葫?幀動(dòng)畫(huà)?補(bǔ)間動(dòng)畫(huà)還是屬性動(dòng)畫(huà)送悔,都不能實(shí)現(xiàn)以上效果慢显。它們都與本文中提到的Camera相關(guān)

ps:自定義view是android應(yīng)用開(kāi)發(fā)工程師的必備技能,除了需要了解view原理欠啤、touch事件分發(fā)等等荚藻,還需要了解繪制相關(guān)的東西,例如canvas跪妥、matrix鞋喇、camera等等

2、camera

android底層所有的繪制都是通過(guò)opengl進(jìn)行的眉撵,為了方便用戶使用侦香,android封裝了一些常用接口,用戶可使用camera進(jìn)行旋轉(zhuǎn)纽疟、移動(dòng)等等

A camera instance can be used to compute 3D transformations and generate a matrix that can be applied, for instance, on aCanvas.
一個(gè)照相機(jī)實(shí)例可以被用于計(jì)算3D變換罐韩,生成一個(gè)可以被使用的Matrix矩陣,一個(gè)實(shí)例污朽,用在畫(huà)布上散吵。

camera的常用方法為:

Camera() 創(chuàng)建一個(gè)沒(méi)有任何轉(zhuǎn)換效果的新的Camera實(shí)例
applyToCanvas(Canvas canvas) 根據(jù)當(dāng)前的變換計(jì)算出相應(yīng)的矩陣,然后應(yīng)用到制定的畫(huà)布上
getLocationX() 獲取Camera的x坐標(biāo)
getLocationY() 獲取Camera的y坐標(biāo)
getLocationZ() 獲取Camera的z坐標(biāo)
getMatrix(Matrixmatrix) 獲取轉(zhuǎn)換效果后的Matrix對(duì)象
restore() 恢復(fù)保存的狀態(tài)
rotate(float x, float y, float z) 沿X蟆肆、Y矾睦、Z坐標(biāo)進(jìn)行旋轉(zhuǎn)
rotateX(float deg)
rotateY(float deg)
rotateZ(float deg)
save() 保存狀態(tài)
setLocation(float x, float y, float z)
translate(float x, float y, float z)沿X、Y炎功、Z軸進(jìn)行平移

可將camera想象成一個(gè)處于坐標(biāo)原點(diǎn)的相機(jī)枚冗,而view在空間中的某一點(diǎn),通過(guò)操作相機(jī)蛇损,view在相機(jī)中看到的就不一樣了赁温,可實(shí)現(xiàn)旋轉(zhuǎn)、移動(dòng)等等淤齐,如果沿Z軸移動(dòng)股囊,view還能放大縮小

上文中提到旋轉(zhuǎn),其實(shí)canvas本身也可以旋轉(zhuǎn)更啄,也可以平衡稚疹。但camera比canvas的強(qiáng)大之處在于,camera的坐標(biāo)系為空間坐標(biāo)系锈死,它有Z軸贫堰,它是立體的穆壕。而canvas的坐標(biāo)系是平面的待牵。

camera的坐標(biāo)系為左手坐標(biāo)系其屏,伸出左手,大姆指朝x軸正向缨该,食指朝Y軸方向偎行,中指垂直于view平面,指向Z軸贰拿。

左手坐標(biāo)系.png

3蛤袒、matrix

matrix代表著一個(gè)矩陣,view的平移膨更、旋轉(zhuǎn)等等妙真,系統(tǒng)都會(huì)封裝成矩陣運(yùn)算,將結(jié)果保存在矩陣中荚守,根據(jù)矩陣?yán)L制view

matrix類(lèi)中封裝了一些接口珍德,開(kāi)發(fā)者不需要直接寫(xiě)矩陣的值,就可以實(shí)現(xiàn)平移矗漾、旋轉(zhuǎn)锈候、縮放等。

setTranslate(floatdx,floatdy):控制Matrix進(jìn)行平移
setSkew(floatkx,floatky,floatpx,floatpy):控制Matrix以px,py為軸心進(jìn)行傾斜敞贡,kx,ky為X,Y方向上的傾斜距離
setRotate(floatdegress):控制Matrix進(jìn)行旋轉(zhuǎn)泵琳,degress控制旋轉(zhuǎn)的角度
setRorate(floatdegress,floatpx,floatpy):設(shè)置以px,py為軸心進(jìn)行旋轉(zhuǎn),degress控制旋轉(zhuǎn)角度
setScale(floatsx,floatsy):設(shè)置Matrix進(jìn)行縮放誊役,sx,sy控制X,Y方向上的縮放比例
setScale(floatsx,floatsy,floatpx,floatpy):設(shè)置Matrix以px,py為軸心進(jìn)行縮放获列,sx,sy控制X,Y方向上的縮放比例

matrix還提供了pre和post兩種類(lèi)型操作。pre代表前乘蛔垢,post代表后乘击孩,因?yàn)榫仃囀遣粷M足交換律的,所以pre和post操作完全不一樣

4啦桌、旋轉(zhuǎn)中心

view常用操作有三種溯壶,平移、縮放甫男、旋轉(zhuǎn)且改,其中平移最簡(jiǎn)單,縮放和旋轉(zhuǎn)略復(fù)雜板驳,下面以旋轉(zhuǎn)為例說(shuō)明又跛。
view的旋轉(zhuǎn)中心默認(rèn)是坐標(biāo)原點(diǎn),如果view在旋轉(zhuǎn)前不作任何操作若治,往往達(dá)不到用戶想要的效果慨蓝。往往需要在旋轉(zhuǎn)前將view的中心點(diǎn)移到原點(diǎn)處感混,再旋轉(zhuǎn),再將中心點(diǎn)移回原位礼烈,這樣view將按照用戶標(biāo)明的中心點(diǎn)旋轉(zhuǎn)

    matrix.preTranslate(-centerX, -centerY);
    matrix.postTranslate(centerX, centerY);

上述代碼中也正好解釋了matrix的pre和post操作弧满。

5、使用Camera與Matrix實(shí)現(xiàn)三D容器

先看效果


效果.png

容器中有四個(gè)子view此熬,子view大小和父view一樣庭呜,且是豎直布局,在繪制時(shí)犀忱,根據(jù)容器的滾動(dòng)距離計(jì)算子view的旋轉(zhuǎn)角度募谎。

  • 測(cè)量過(guò)程

    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
      super.onMeasure(widthMeasureSpec, heightMeasureSpec);
      int w = MeasureSpec.getSize(widthMeasureSpec);
      int h = MeasureSpec.getSize(heightMeasureSpec);
      setMeasuredDimension(w, h);
      mWidth = w;
      mHeight = h;
      
      int childW = w - getPaddingLeft() - getPaddingRight();
      int childH = h - getPaddingTop() - getPaddingBottom();
      
      int childWSpec = MeasureSpec.makeMeasureSpec(childW, MeasureSpec.EXACTLY);
      int childHSpec = MeasureSpec.makeMeasureSpec(childH, MeasureSpec.EXACTLY);
      measureChildren(childWSpec, childHSpec);
      //默認(rèn)此容器滾動(dòng)到第二個(gè)view處
      scrollTo(0, mStartScreen*mHeight);
    }
    

測(cè)量過(guò)程較簡(jiǎn)單,但在最后時(shí)阴汇,讓view滾動(dòng)到第二屏数冬。

  • 布局過(guò)程

    protected void onLayout(boolean changed, int l, int t, int r, int b) {
      for (int i = 0; i < getChildCount(); i++) {
          View child = getChildAt(i);
          int left = (getPaddingLeft() + getPaddingRight())/2;
          int top = (getPaddingTop() + getPaddingBottom())/2;
          //子view是豎直排列的,通過(guò)camera方式搀庶,旋轉(zhuǎn)才看到三D效果
          child.layout(left, top + i*mHeight, left + mWidth, top + (i+1)*mHeight);
      }
    }
    

布局過(guò)程也比較簡(jiǎn)單拐纱,豎直排列

  • 繪制過(guò)程

    protected void dispatchDraw(Canvas canvas) {
      for (int i = 0; i < getChildCount(); i++) {
          drawScreen(canvas, i, getDrawingTime());
      }
    }
    
    private void drawScreen(Canvas canvas, int index, long time){
      int scrollHeight = mHeight * index;
      int scrollY = getScrollY();
      //view的位置明顯看不到,則不需要繪制地来。比如滾動(dòng)距離+view高度戳玫,還小于view的起始top值,則此view不繪制
      if (scrollHeight > scrollY + mHeight || scrollHeight < scrollY - mHeight) {
          return;
      }
      View child = getChildAt(index);
      //旋轉(zhuǎn)中心點(diǎn)是旋轉(zhuǎn)中的關(guān)鍵未斑。view旋轉(zhuǎn)的中心點(diǎn)都是0咕宿,0點(diǎn),
      //所以需要先將中心點(diǎn)移到0蜡秽,0點(diǎn)府阀,旋轉(zhuǎn),再移動(dòng)回來(lái)芽突,看起來(lái)像view是在中心點(diǎn)旋轉(zhuǎn)一樣
      //如果是滾動(dòng)距離大于view的top點(diǎn)试浙,那么則y中心點(diǎn)則是view的bottom位置,否則則是top位置
      float centerX = mWidth/2;
      float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight;
      //計(jì)算旋轉(zhuǎn)角度
      float degree = mAngle * (getScrollY() - scrollHeight)/mHeight;
      if (degree > 90 || degree < -90) {
          return;
      }
      canvas.save();
      mCamera.save();
      matrix.reset();
      mCamera.rotateX(degree);
      mCamera.getMatrix(matrix);
      mCamera.restore();
      //移動(dòng)到旋轉(zhuǎn)中心點(diǎn)
      matrix.preTranslate(-centerX, -centerY);
      matrix.postTranslate(centerX, centerY);
      canvas.concat(matrix);
      drawChild(canvas, child, time);
      canvas.restore();
    }
    

繪制過(guò)程有兩個(gè)難點(diǎn):

  1. 角度計(jì)算
    在measure階段寞蚌,容器向下滾動(dòng)mHeight距離(子view高度)田巴,用戶看到的是第二個(gè)子view,第一個(gè)子view應(yīng)該旋轉(zhuǎn)角度為90度挟秤,第二個(gè)子view旋轉(zhuǎn)角度為0度壹哺,第三個(gè)子view旋轉(zhuǎn)角度為-90度。由此得出以下公式

float degree = mAngle * (getScrollY() - scrollHeight)/mHeight;

  1. 旋轉(zhuǎn)中心點(diǎn)計(jì)算
    每個(gè)子view的旋轉(zhuǎn)中心點(diǎn)是不一樣的艘刚,為了達(dá)到協(xié)同的效果管宵,旋轉(zhuǎn)中心y值按如下公式計(jì)算

float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight;

如果是滾動(dòng)距離大于view的top點(diǎn),那么則y中心點(diǎn)則是view的bottom位置,否則則是top位置箩朴。第一個(gè)子view以(w/2,h)為中心點(diǎn)旋轉(zhuǎn)岗喉,而第二個(gè)子view也是以(w/2,h)為中心點(diǎn)旋轉(zhuǎn),符合上述公式炸庞。旋轉(zhuǎn)中心點(diǎn)的問(wèn)題需要認(rèn)真思考钱床,在紙上繪制示意圖會(huì)幫助理解。

找到旋轉(zhuǎn)中心點(diǎn)以及角度計(jì)算燕雁,則非常簡(jiǎn)單了诞丽,使用camera繞X軸旋轉(zhuǎn)一定角度鲸拥,得到對(duì)應(yīng)的matrix拐格,將矩陣應(yīng)用到canvas上,同時(shí)進(jìn)行旋轉(zhuǎn)時(shí)的中心點(diǎn)平移工作刑赶,注意相關(guān)對(duì)象的狀態(tài)保存及恢復(fù)捏浊,整個(gè)繪制程序則完成。

  • Touch事件處理
    需要view容器完成跟手操作撞叨,當(dāng)手松開(kāi)時(shí)金踪,容器需要回到正確的狀態(tài)(或到上一頁(yè)、下一頁(yè)等)牵敷。

子view的旋轉(zhuǎn)角度與容器的滾動(dòng)距離有關(guān)系胡岔,因此處理move事件時(shí),滾動(dòng)容器即可枷餐。當(dāng)手松開(kāi)時(shí)靶瘸,需要計(jì)算容器的下一個(gè)狀態(tài)是什么,容器是顯示上一個(gè)子view還是顯示下一個(gè)子view毛肋,還是停留在本頁(yè)內(nèi)怨咪,根據(jù)下一個(gè)狀態(tài)讓容器滾動(dòng)適當(dāng)?shù)木嚯x即可。

  case MotionEvent.ACTION_MOVE:
        mVelocityTracker.addMovement(event);
        float y = event.getY();
        float detal = y - mDownY;
        mDownY = y;
        //當(dāng)scroller結(jié)束滾動(dòng)時(shí)再響應(yīng)move事件润匙。
        if (mScroller.isFinished()) {
            moveScroll((int)detal);
        }
        break;
  case MotionEvent.ACTION_UP:
        mVelocityTracker.addMovement(event);
        mVelocityTracker.computeCurrentVelocity(1000);
        float vel = mVelocityTracker.getYVelocity();
  //            Log.i(TAG, "vel = " + vel);
        //y速度值為正則是往下滑動(dòng)诗眨,為負(fù)則是往上滑動(dòng),以500為界定
        if (vel >= 500) {
            moveToNext();
            //滑動(dòng)到下一屏
        }else if (vel <= -500) {
            //滑動(dòng)到上一屏
            moveToPre();
        }else {
            //依然在當(dāng)前屏
            moveNormal();
        }
        mVelocityTracker.clear();
        mVelocityTracker.recycle();
        mVelocityTracker = null;
        break;

當(dāng)容器顯示第一個(gè)子view時(shí),用戶還在向上翻動(dòng)容器孕讳,為完成循環(huán)顯示效果匠楚,需要將容器的第四個(gè)子view在原位置刪除,添加到容器的0位置厂财。同理芋簿,另一種情況下需要將第一個(gè)子view刪除,將它添加到容器的3位置蟀苛,這樣用戶將看到正方體旋轉(zhuǎn)效果益咬。

  private void moveToPre(){
    addPreView();
    int scrolly = getScrollY();
    //以從第二個(gè)view回到第一個(gè)view為例,第二個(gè)view的滾動(dòng)距離為scrolly,而第一個(gè)view的正常位置則是滾動(dòng)距離為0
    //所以從第二個(gè)view滾動(dòng)回第一個(gè)view的真正距離就是scrolly幽告,因?yàn)槭窍蛏厦佛校詾樨?fù)值
    int curY = scrolly + mHeight;
    setScrollY(curY);
    int detal = -(curY - mHeight);
    mScroller.startScroll(0, curY, 0, detal,500);
}

private void moveToNext(){
    addNextView();
    int scrolly = getScrollY();
    int curY = scrolly - mHeight;
    setScrollY(curY);
    int detal = mHeight - (curY);
    mScroller.startScroll(0, curY, 0, detal,500);
}

private void moveNormal(){
    int scrolly = getScrollY();
    int curY = scrolly;
    int detal = mHeight - curY;
    //Log.i(TAG, "cury = " + curY + "  detal = " + detal + "  mheight = " + mHeight);
    mScroller.startScroll(0, curY, 0, detal,500);
    //此處必須刷新,否則computeScroll不會(huì)執(zhí)行
    invalidate();
}

6冗锁、后記

關(guān)于camera與matrix相關(guān)的文章齐唆,有不少大神已經(jīng)寫(xiě)文說(shuō)明了,本例也差不多冻河,且多有借鑒箍邮,例如,http://www.reibang.com/p/34e0fe5f9e31 叨叙,非為抄襲锭弊,只為總結(jié)知識(shí),自成知識(shí)體系擂错。

代碼均已上傳到本人的github:https://github.com/okunu/DemoApp 味滞,歡迎訪問(wèn)。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钮呀,一起剝皮案震驚了整個(gè)濱河市剑鞍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌爽醋,老刑警劉巖蚁署,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異蚂四,居然都是意外死亡光戈,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門(mén)证杭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)田度,“玉大人,你說(shuō)我怎么就攤上這事解愤≌蚪龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵送讲,是天一觀的道長(zhǎng)奸笤。 經(jīng)常有香客問(wèn)我,道長(zhǎng)哼鬓,這世上最難降的妖魔是什么监右? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮异希,結(jié)果婚禮上健盒,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好扣癣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布惰帽。 她就那樣靜靜地躺著,像睡著了一般父虑。 火紅的嫁衣襯著肌膚如雪该酗。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,301評(píng)論 1 301
  • 那天士嚎,我揣著相機(jī)與錄音呜魄,去河邊找鬼。 笑死莱衩,一個(gè)胖子當(dāng)著我的面吹牛爵嗅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膳殷,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼操骡,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了赚窃?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤岔激,失蹤者是張志新(化名)和其女友劉穎勒极,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體虑鼎,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辱匿,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了炫彩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匾七。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖江兢,靈堂內(nèi)的尸體忽然破棺而出昨忆,到底是詐尸還是另有隱情,我是刑警寧澤杉允,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布邑贴,位于F島的核電站,受9級(jí)特大地震影響叔磷,放射性物質(zhì)發(fā)生泄漏拢驾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一改基、第九天 我趴在偏房一處隱蔽的房頂上張望繁疤。 院中可真熱鬧,春花似錦、人聲如沸稠腊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)麻养。三九已至褐啡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鳖昌,已是汗流浹背备畦。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留许昨,地道東北人懂盐。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像糕档,于是被迫代替她去往敵國(guó)和親莉恼。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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