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軸贰拿。
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容器
先看效果
容器中有四個(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):
- 角度計(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;
- 旋轉(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)。