Android 從零開始打造一個 3D立體旋轉容器
文章轉載自:http://blog.csdn.net/mr_immortalz/article/details/51918560 學習參考
嗯,2個月沒有寫部落格,是要好好反省下,趁著放暑假把這兩個月看的東西好好沉澱下。嗯,就立下這個Flag,希望不要自己再打自己臉。
1.概述
回到正題,這次帶來的效果,是一個Android 的3D立體旋轉的效果。
當然靈感的來源,來自早些時間微博上看到的效果圖。
非常酷有木有!作為程式猿我當然要把它加入我的下一個專案中啦!
原效果
我們實現的效果:
(為了更加可定製化,我在原圖基礎上新增了新的效果)
可以快速滾動,並且無限迴圈
這個是對一些引數的進行設定
對圖片的包裹效果
因為本身繼承自ViewGroup,所以基本控制元件都是可以包裹的
2.分析
因為程式碼量有點大,感覺把程式碼全部貼上上來也不現實。所以想了解我的思路的盆友可以先來這裡下載程式碼。然後邊看程式碼邊看我的分析
- 1
- 2
- 1
- 2
通過我們實現的效果圖可以發現:
1.切換的時候是一個3D立體的效果
2.佈局中的每一個Item可以自由切換,且無限迴圈滾動
要解決上面的效果,我們需要什麼技術點呢?
1.要想實現一個3D效果,我們可以藉助Android中的Camera、Matrix
2.要想實現滾動,毫無疑問,我們需要藉助Scroller
當然一切看起來很簡單,其實不然,除此之外,你還需要對於滑動衝突進行處理等等,下面我開始介紹啦。
這就是我們這次專案的大致
3.實現
因為我們是要打造一個容器類,所以肯定得繼承自 ViewGroup
按照一般的思路,我們肯定是先要進行一些變數的申明,onMeasure,onLayout操作
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
private void init(Context context) { mCamera = new Camera(); mMatrix = new Matrix(); if (mScroller == null) { mScroller = new Scroller(context); } } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); measureChildren(widthMeasureSpec, heightMeasureSpec); mWidth = getMeasuredWidth(); mHeight = getMeasuredHeight(); //滑動到設定的StartScreen位置 scrollTo(0, mStartScreen * mHeight); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int childTop = 0; for (int i = 0; i < getChildCount(); i++) { View child = getChildAt(i); if (child.getVisibility() != GONE) { child.layout(0, childTop, child.getMeasuredWidth(), childTop + child.getMeasuredHeight()); childTop = childTop + child.getMeasuredHeight(); } } }
完成這些操作後,我們需要在onTouchEvent中進行滑動事件的處理
3.1 完成無限迴圈滑動滾動
我們的item數量是有限的,如何實現無限迴圈滾動呢?很簡單,以3個item為例子(分別為1,2,3),我們讓螢幕顯示的是2
如此反覆,螢幕所在的位置始終是第2個item所在的位置,這樣就實現了我們的無限迴圈滾動,向下滾動也是如此
@Override
public boolean onTouchEvent(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
//當上一次滑動沒有結束時,再次點選,強制滑動在點選位置結束
mScroller.setFinalY(mScroller.getCurrY());
mScroller.abortAnimation();
scrollTo(0, getScrollY());
}
mDownY = y;
break;
case MotionEvent.ACTION_MOVE:
int realDelta = (int) (mDownY - y);
mDownY = y;
if (mScroller.isFinished()) {
//因為要迴圈滾動
recycleMove(realDelta);
}
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000);
float yVelocity = mVelocityTracker.getYVelocity();
//滑動的速度大於規定的速度,或者向上滑動時,上一頁頁面展現出的高度超過1/2。則設定狀態為State.ToPre
if (yVelocity > standerSpeed || ((getScrollY() + mHeight / 2) / mHeight < mStartScreen)) {
mState = State.ToPre;
} else if (yVelocity < -standerSpeed || ((getScrollY() + mHeight / 2) / mHeight > mStartScreen)) {
//滑動的速度大於規定的速度,或者向下滑動時,下一頁頁面展現出的高度超過1/2。則設定狀態為State.ToNext
mState = State.ToNext;
} else {
mState = State.Normal;
}
//根據mState進行相應的變化
changeByState(yVelocity);
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
//返回true,消耗點選事件
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
當手從螢幕上移開時,我們來看下這個方法changeByState(yVelocity);
我們以mState = State.ToPre 為例子來說明
/**
* mState = State.ToPre 時進行的動作
* @param yVelocity 豎直方向的速度
*/
private void toPreAction(float yVelocity) {
int startY;
int delta;
int duration;
mState = State.ToPre;
addPre();//增加新的頁面
//計算鬆手後滑動的item個數
int flingSpeedCount= (yVelocity - standerSpeed) > 0 ? (int) (yVelocity - standerSpeed) : 0;
addCount = flingSpeedCount/ flingSpeed + 1;
//mScroller開始的座標
startY = getScrollY() + mHeight;
setScrollY(startY);
//mScroller 移動的距離
delta = -(startY - mStartScreen * mHeight) - (addCount - 1) * mHeight;
duration = (Math.abs(delta)) * 3;
mScroller.startScroll(0, startY, 0, delta, duration);
addCount--;
}
然後會進入addPre方法中
/**
* 把最後一個item移動到第一個item位置
*/
private void addPre() {
mCurScreen = ((mCurScreen - 1) + getChildCount()) % getChildCount();
int childCount = getChildCount();
View view = getChildAt(childCount - 1);
removeViewAt(childCount - 1);
addView(view, 0);
if (iStereoListener != null) {
iStereoListener.toPre(mCurScreen);
}
}
最後mScroller.startScroll(0, startY, 0, delta, duration); 開始執行。
執行的過程中會回撥這個函式方法computeScroll
完成到這一步,我們的無限滑動滾動就算是完成了
3.2 實現3D切換效果。
正常情況下,我們自定義ViewGroup並不需要重寫dispatchDraw 方法。
而這裡我們則需要重寫
@Override
protected void dispatchDraw(Canvas canvas) {
if (!isAdding && isCan3D) {
//當開啟3D效果並且當前狀態不屬於 computeScroll中 addPre() 或者addNext()
//如果不做這個判斷,addPre() 或者addNext()時頁面會進行閃動一下
//我當時寫的時候就被這個坑了,後來通過log判斷,原來是computeScroll中的onlayout,和子Child的draw觸發的順序導致的。
//知道原理的朋友希望可以告知下
for (int i = 0; i < getChildCount(); i++) {
drawScreen(canvas, i, getDrawingTime());
}
} else {
isAdding = false;
super.dispatchDraw(canvas);
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
好,我們來drawScreen這個方法
private void drawScreen(Canvas canvas, int i, long drawingTime) {
int curScreenY = mHeight * i;
//螢幕中不顯示的部分不進行繪製
if (getScrollY() + mHeight < curScreenY) {
return;
}
if (curScreenY < getScrollY() - mHeight) {
return;
}
float centerX = mWidth / 2;
float centerY = (getScrollY() > curScreenY) ? curScreenY + mHeight : curScreenY;
float degree = mAngle * (getScrollY() - curScreenY) / mHeight;
if (degree > 90 || degree < -90) {
return;
}
canvas.save();
mCamera.save();
mCamera.rotateX(degree);
mCamera.getMatrix(mMatrix);
mCamera.restore();
mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postTranslate(centerX, centerY);
canvas.concat(mMatrix);
drawChild(canvas, getChildAt(i), drawingTime);
canvas.restore();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
這裡面的關鍵就在於
mCamera.rotateX(degree);
mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postTranslate(centerX, centerY);
對於Camera我們知道我們整個佈局都是平鋪的,為什麼會產生3D的效果呢?原因就是這個Camera類,人如其名,它就相當於一個相機,它對物體進行拍照。我們把相機正對物體拍攝,拍攝出的效果就是平面的,當我們把相機旋轉了90度再來拍攝原來物體,物體就相當於旋轉了90度。
Camera拍攝完畢後,然後把拍攝的引數值傳到Matrix中,Matrix再和Canvas繫結,由Canvas進行繪製。最終顯示在螢幕中。
那麼preTranslate,postTranslate又是怎麼一回事呢?
很簡單,我們知道座標系是以(0,0)作為參照點的。現在我們對拍攝的物件進行的縮放變形操作是在物體的中心。我們需要把物體的中心先移動到(0,0)位置,最後再移動到物體原來中心位置即可。
不過對於Camera的座標系我還有一點點疑問,我準備有機會寫一篇關於Camera和Matrix文章。
3.3 滑動事件衝突的處理(請先檢視更新說明)
完成上面兩個步驟,那麼我們就算Over了嗎?
不!還有很重要的一點,就是事件衝突的處理。 舉個例子:我們把手放到我們的容器上,系統怎麼知道我們這個滑動事件是給容器還是要給容器的子類的呢?
(給容器自己,則進行滑動的操作,給容器的子類,則容器的子類可以進行點選事件的判斷處理)
對於這種情況,我就很大度啦,全部交給容器子類處理!子類不要,OK,那容器你自己拿來玩吧。
————之所以不走尋常路:交給容器處理,容器不需要再交給子類
原因在於:容器拿到滑動事件只需要做滑動操作,而子類則不同,它有點選事件需要判斷,一個容器有很多子類,而很多子類只有一個共同的容器,如果把控制權交給容器,那麼容器怎麼可能能夠判斷得出不同的子類到底需不需要這個滑動事件呢?所以,既然這麼麻煩,那麼統統交給子類處理。
交給子類處理,則容器中onInterceptTouchEvent需要做如下操作
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
而子類(用CustomEdittext為例)的dispatchTouchEvent需要做如下判斷
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (!isContain(event)) {
//子類不需要,交給容器自己處理
getParent().requestDisallowInterceptTouchEvent(false);
setFocusable(false);
} else {
//子類自己做操作
setFocusableInTouchMode(true);
}
break;
case MotionEvent.ACTION_UP:
break;
}
return super.dispatchTouchEvent(event);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
在isContain中,我做的是點選的座標是否在Edittext中,在則攔截,子類處理,不在,則交給父類容器
private boolean isContain(MotionEvent event) {
region.set(rect);
if (region.contains((int) event.getX(), (int) event.getY())) {
return true;
}
return false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
當然交給子類這樣也導致了一個問題,就是我如果需要給容器中的子類進行點選事件,則都需要自定義一個View(例如上面的CustomEdittext 繼承自Edittext)。
例如我就自定義了三個View,不過還是很簡單的,幾分鐘的事就搞定了(在自定義View中dispatchTouchEvent進行判斷)。
具體的可以參考程式碼。
更新說明 2016-8/5
滑動衝突之前我是把控制權交給了子類,這裡https://github.com/Y-bao 這位作者提交的pull
request中將事件衝突交給了父類(StereoView)
,我這邊通過了pull,我覺得寫得挺好的,把點選事件的控制權轉移給父類,就不需要自定義View。
如果你還想檢視控制權轉移給子類的程式碼(我之前的),可以點選這裡
3.4 點選水紋波效果
細心的人會發現,我這裡還有個RippleView。
沒錯這就是點選後有水紋波的效果。
Android本身可以在XML中用ripple實現,不過是Android 5.0以上,個人覺得相容性不太好,就自己隨便寫了一個簡易的,哈哈,效率不能保證,各位看客看看就好啦。
4.應用
4.1 定義的方法
使用方法也和其他的沒有什麼區別,我這裡自定義了幾個方法,我這裡說明下。
自定義的方法
setStartScreen(int startScreen) :設定第一頁展示的頁面 @param startScreen (0,getChildCount-1)
setResistance(float resistance) : 設定滑動阻力 @param resistance (0,…)
setInterpolator(Interpolator mInterpolator) : 設定滾動時interpolator插補器
setAngle(float mAngle):設定滾動時兩個item的夾角度數 [0f,180f]
setCan3D(boolean can3D) : 是否開啟3D效果
setItem(int itemId) : 跳轉到指定的item @param itemId [0,getChildCount-1]
toPre() : 上一頁
toNext() : 下一頁
定義的回撥介面
4.2 使用方法
直接在佈局中
在程式碼中
4.3 缺陷說明
目前容器的item數量需要大於等於3,小於3個滑動時會些問題。設定的最開始展示的item位置不能是第一個或者最後一個,這麼做是為了保證第1個或者最後一個被隱藏,從而保證最開始向上滑動或者向下滑動時的正常。
5.下載
如果覺得對你有幫助,歡迎 star,fork,如果對於我感興趣,歡迎follow 我
6.申明
本文已授權微信公眾號:鴻洋(hongyangAndroid)在微信公眾號平臺原創首發。
參考文章: