1、引子
筆者剛開始工作時,做的第一個模塊是手機中的launcher,launcher可自由選擇滑屏效果,甚至還有三D效果,酷炫的動畫讓人震驚同時也感到非常迷惑,這動畫效果怎么實現的呢?幀動畫?補間動畫還是屬性動畫,都不能實現以上效果。它們都與本文中提到的Camera相關
ps:自定義view是android應用開發工程師的必備技能,除了需要了解view原理、touch事件分發等等,還需要了解繪制相關的東西,例如canvas、matrix、camera等等
2、camera
android底層所有的繪制都是通過opengl進行的,為了方便用戶使用,android封裝了一些常用接口,用戶可使用camera進行旋轉、移動等等
A camera instance can be used to compute 3D transformations and generate a matrix that can be applied, for instance, on aCanvas.
一個照相機實例可以被用于計算3D變換,生成一個可以被使用的Matrix矩陣,一個實例,用在畫布上。
camera的常用方法為:
Camera() 創建一個沒有任何轉換效果的新的Camera實例
applyToCanvas(Canvas canvas) 根據當前的變換計算出相應的矩陣,然后應用到制定的畫布上
getLocationX() 獲取Camera的x坐標
getLocationY() 獲取Camera的y坐標
getLocationZ() 獲取Camera的z坐標
getMatrix(Matrixmatrix) 獲取轉換效果后的Matrix對象
restore() 恢復保存的狀態
rotate(float x, float y, float z) 沿X、Y、Z坐標進行旋轉
rotateX(float deg)
rotateY(float deg)
rotateZ(float deg)
save() 保存狀態
setLocation(float x, float y, float z)
translate(float x, float y, float z)沿X、Y、Z軸進行平移
可將camera想象成一個處于坐標原點的相機,而view在空間中的某一點,通過操作相機,view在相機中看到的就不一樣了,可實現旋轉、移動等等,如果沿Z軸移動,view還能放大縮小
上文中提到旋轉,其實canvas本身也可以旋轉,也可以平衡。但camera比canvas的強大之處在于,camera的坐標系為空間坐標系,它有Z軸,它是立體的。而canvas的坐標系是平面的。
camera的坐標系為左手坐標系,伸出左手,大姆指朝x軸正向,食指朝Y軸方向,中指垂直于view平面,指向Z軸。
3、matrix
matrix代表著一個矩陣,view的平移、旋轉等等,系統都會封裝成矩陣運算,將結果保存在矩陣中,根據矩陣繪制view
matrix類中封裝了一些接口,開發者不需要直接寫矩陣的值,就可以實現平移、旋轉、縮放等。
setTranslate(floatdx,floatdy):控制Matrix進行平移
setSkew(floatkx,floatky,floatpx,floatpy):控制Matrix以px,py為軸心進行傾斜,kx,ky為X,Y方向上的傾斜距離
setRotate(floatdegress):控制Matrix進行旋轉,degress控制旋轉的角度
setRorate(floatdegress,floatpx,floatpy):設置以px,py為軸心進行旋轉,degress控制旋轉角度
setScale(floatsx,floatsy):設置Matrix進行縮放,sx,sy控制X,Y方向上的縮放比例
setScale(floatsx,floatsy,floatpx,floatpy):設置Matrix以px,py為軸心進行縮放,sx,sy控制X,Y方向上的縮放比例
matrix還提供了pre和post兩種類型操作。pre代表前乘,post代表后乘,因為矩陣是不滿足交換律的,所以pre和post操作完全不一樣
4、旋轉中心
view常用操作有三種,平移、縮放、旋轉,其中平移最簡單,縮放和旋轉略復雜,下面以旋轉為例說明。
view的旋轉中心默認是坐標原點,如果view在旋轉前不作任何操作,往往達不到用戶想要的效果。往往需要在旋轉前將view的中心點移到原點處,再旋轉,再將中心點移回原位,這樣view將按照用戶標明的中心點旋轉
matrix.preTranslate(-centerX, -centerY);
matrix.postTranslate(centerX, centerY);
上述代碼中也正好解釋了matrix的pre和post操作。
5、使用Camera與Matrix實現三D容器
先看效果
容器中有四個子view,子view大小和父view一樣,且是豎直布局,在繪制時,根據容器的滾動距離計算子view的旋轉角度。
-
測量過程
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); //默認此容器滾動到第二個view處 scrollTo(0, mStartScreen*mHeight); }
測量過程較簡單,但在最后時,讓view滾動到第二屏。
-
布局過程
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是豎直排列的,通過camera方式,旋轉才看到三D效果 child.layout(left, top + i*mHeight, left + mWidth, top + (i+1)*mHeight); } }
布局過程也比較簡單,豎直排列
-
繪制過程
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的位置明顯看不到,則不需要繪制。比如滾動距離+view高度,還小于view的起始top值,則此view不繪制 if (scrollHeight > scrollY + mHeight || scrollHeight < scrollY - mHeight) { return; } View child = getChildAt(index); //旋轉中心點是旋轉中的關鍵。view旋轉的中心點都是0,0點, //所以需要先將中心點移到0,0點,旋轉,再移動回來,看起來像view是在中心點旋轉一樣 //如果是滾動距離大于view的top點,那么則y中心點則是view的bottom位置,否則則是top位置 float centerX = mWidth/2; float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight; //計算旋轉角度 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(); //移動到旋轉中心點 matrix.preTranslate(-centerX, -centerY); matrix.postTranslate(centerX, centerY); canvas.concat(matrix); drawChild(canvas, child, time); canvas.restore(); }
繪制過程有兩個難點:
- 角度計算
在measure階段,容器向下滾動mHeight距離(子view高度),用戶看到的是第二個子view,第一個子view應該旋轉角度為90度,第二個子view旋轉角度為0度,第三個子view旋轉角度為-90度。由此得出以下公式
float degree = mAngle * (getScrollY() - scrollHeight)/mHeight;
- 旋轉中心點計算
每個子view的旋轉中心點是不一樣的,為了達到協同的效果,旋轉中心y值按如下公式計算
float centerY = (getScrollY() > scrollHeight) ? scrollHeight + mHeight : scrollHeight;
如果是滾動距離大于view的top點,那么則y中心點則是view的bottom位置,否則則是top位置。第一個子view以(w/2,h)為中心點旋轉,而第二個子view也是以(w/2,h)為中心點旋轉,符合上述公式。旋轉中心點的問題需要認真思考,在紙上繪制示意圖會幫助理解。
找到旋轉中心點以及角度計算,則非常簡單了,使用camera繞X軸旋轉一定角度,得到對應的matrix,將矩陣應用到canvas上,同時進行旋轉時的中心點平移工作,注意相關對象的狀態保存及恢復,整個繪制程序則完成。
- Touch事件處理
需要view容器完成跟手操作,當手松開時,容器需要回到正確的狀態(或到上一頁、下一頁等)。
子view的旋轉角度與容器的滾動距離有關系,因此處理move事件時,滾動容器即可。當手松開時,需要計算容器的下一個狀態是什么,容器是顯示上一個子view還是顯示下一個子view,還是停留在本頁內,根據下一個狀態讓容器滾動適當的距離即可。
case MotionEvent.ACTION_MOVE:
mVelocityTracker.addMovement(event);
float y = event.getY();
float detal = y - mDownY;
mDownY = y;
//當scroller結束滾動時再響應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速度值為正則是往下滑動,為負則是往上滑動,以500為界定
if (vel >= 500) {
moveToNext();
//滑動到下一屏
}else if (vel <= -500) {
//滑動到上一屏
moveToPre();
}else {
//依然在當前屏
moveNormal();
}
mVelocityTracker.clear();
mVelocityTracker.recycle();
mVelocityTracker = null;
break;
當容器顯示第一個子view時,用戶還在向上翻動容器,為完成循環顯示效果,需要將容器的第四個子view在原位置刪除,添加到容器的0位置。同理,另一種情況下需要將第一個子view刪除,將它添加到容器的3位置,這樣用戶將看到正方體旋轉效果。
private void moveToPre(){
addPreView();
int scrolly = getScrollY();
//以從第二個view回到第一個view為例,第二個view的滾動距離為scrolly,而第一個view的正常位置則是滾動距離為0
//所以從第二個view滾動回第一個view的真正距離就是scrolly,因為是向上,所以為負值
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不會執行
invalidate();
}
6、后記
關于camera與matrix相關的文章,有不少大神已經寫文說明了,本例也差不多,且多有借鑒,例如,http://www.lxweimin.com/p/34e0fe5f9e31 ,非為抄襲,只為總結知識,自成知識體系。
代碼均已上傳到本人的github:https://github.com/okunu/DemoApp ,歡迎訪問。