本文地址,轉(zhuǎn)載請注明
代碼下載地址 :https://github.com/ImmortalZ/StereoView
嗯,2個月沒有寫博客店诗,是要好好反省下,趁著放暑假把這兩個月看的東西好好沉淀下音榜。嗯庞瘸,就立下這個Flag,希望不要自己再打自己臉囊咏。
1.概述
回到正題恕洲,這次帶來的效果塔橡,是一個Android 的3D立體旋轉(zhuǎn)的效果。
當然靈感的來源霜第,來自早些時間微博上看到的效果圖葛家。
非常酷有木有泌类!作為程序猿我當然要把它加入我的下一個項目中啦癞谒!
原效果
我們實現(xiàn)的效果:
(為了更加可定制化,我在原圖基礎(chǔ)上新增了新的效果)
可以快速滾動刃榨,并且無限循環(huán)
這個是對一些參數(shù)的進行設(shè)定
對圖片的包裹效果
因為本身繼承自ViewGroup弹砚,所以基本控件都是可以包裹的
2.分析
因為代碼量有點大,感覺把代碼全部粘貼上來也不現(xiàn)實枢希。所以想了解我的思路的盆友可以先來這里下載代碼桌吃。然后邊看代碼邊看我的分析
下載地址 :https://github.com/ImmortalZ/StereoView
通過我們實現(xiàn)的效果圖可以發(fā)現(xiàn):
1.切換的時候是一個3D立體的效果
2.布局中的每一個Item可以自由切換,且無限循環(huán)滾動
要解決上面的效果苞轿,我們需要什么技術(shù)點呢茅诱?
1.要想實現(xiàn)一個3D效果,我們可以借助Android中的Camera搬卒、Matrix
2.要想實現(xiàn)滾動瑟俭,毫無疑問,我們需要借助Scroller
當然一切看起來很簡單契邀,其實不然摆寄,除此之外,你還需要對于滑動沖突進行處理等等坯门,下面我開始介紹啦微饥。
這就是我們這次項目的大致
3.實現(xiàn)
因為我們是要打造一個容器類,所以肯定得繼承自 ViewGroup
按照一般的思路古戴,我們肯定是先要進行一些變量的申明畜号,onMeasure,onLayout操作
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();
//滑動到設(shè)置的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 完成無限循環(huán)滑動滾動
我們的item數(shù)量是有限的,如何實現(xiàn)無限循環(huán)滾動呢蛮拔?很簡單述暂,以3個item為例子(分別為1,2,3),我們讓屏幕顯示的是2
如此反復建炫,屏幕所在的位置始終是第2個item所在的位置畦韭,這樣就實現(xiàn)了我們的無限循環(huán)滾動,向下滾動也是如此
@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()) {
//當上一次滑動沒有結(jié)束時肛跌,再次點擊艺配,強制滑動在點擊位置結(jié)束
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()) {
//因為要循環(huán)滾動
recycleMove(realDelta);
}
break;
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000);
float yVelocity = mVelocityTracker.getYVelocity();
//滑動的速度大于規(guī)定的速度察郁,或者向上滑動時,上一頁頁面展現(xiàn)出的高度超過1/2转唉。則設(shè)定狀態(tài)為State.ToPre
if (yVelocity > standerSpeed || ((getScrollY() + mHeight / 2) / mHeight < mStartScreen)) {
mState = State.ToPre;
} else if (yVelocity < -standerSpeed || ((getScrollY() + mHeight / 2) / mHeight > mStartScreen)) {
//滑動的速度大于規(guī)定的速度皮钠,或者向下滑動時,下一頁頁面展現(xiàn)出的高度超過1/2赠法。則設(shè)定狀態(tài)為State.ToNext
mState = State.ToNext;
} else {
mState = State.Normal;
}
//根據(jù)mState進行相應的變化
changeByState(yVelocity);
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
break;
}
//返回true,消耗點擊事件
return true;
}
當手從屏幕上移開時麦轰,我們來看下這個方法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個數(shù)
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); 開始執(zhí)行卢鹦。
執(zhí)行的過程中會回調(diào)這個函數(shù)方法computeScroll
完成到這一步端蛆,我們的無限滑動滾動就算是完成了
3.2 實現(xiàn)3D切換效果毁习。
正常情況下厘托,我們自定義ViewGroup并不需要重寫dispatchDraw 方法灌诅。
而這里我們則需要重寫
@Override
protected void dispatchDraw(Canvas canvas) {
if (!isAdding && isCan3D) {
//當開啟3D效果并且當前狀態(tài)不屬于 computeScroll中 addPre() 或者addNext()
//如果不做這個判斷骚揍,addPre() 或者addNext()時頁面會進行閃動一下
//我當時寫的時候就被這個坑了治力,后來通過log判斷眶熬,原來是computeScroll中的onlayout,和子Child的draw觸發(fā)的順序?qū)е碌摹? //知道原理的朋友希望可以告知下
for (int i = 0; i < getChildCount(); i++) {
drawScreen(canvas, i, getDrawingTime());
}
} else {
isAdding = false;
super.dispatchDraw(canvas);
}
}
好,我們來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();
}
這里面的關(guān)鍵就在于
mCamera.rotateX(degree);
mMatrix.preTranslate(-centerX, -centerY);
mMatrix.postTranslate(centerX, centerY);
對于Camera我們知道我們整個布局都是平鋪的晾浴,為什么會產(chǎn)生3D的效果呢?原因就是這個Camera類牍白,人如其名,它就相當于一個相機茂腥,它對物體進行拍照。我們把相機正對物體拍攝最岗,拍攝出的效果就是平面的帕胆,當我們把相機旋轉(zhuǎn)了90度再來拍攝原來物體,物體就相當于旋轉(zhuǎn)了90度般渡。
Camera拍攝完畢后,然后把拍攝的參數(shù)值傳到Matrix中驯用,Matrix再和Canvas綁定,由Canvas進行繪制蝴乔。最終顯示在屏幕中。
那么preTranslate片酝,postTranslate又是怎么一回事呢?
很簡單雕沿,我們知道坐標系是以(0,0)作為參照點的。現(xiàn)在我們對拍攝的對象進行的縮放變形操作是在物體的中心晦炊。我們需要把物體的中心先移動到(0,0)位置,最后再移動到物體原來中心位置即可贤姆。
具體的大家可以參考下這篇文章
http://blog.csdn.net/rav009/article/details/7763223 ( Android postTranslate和preTranslate的理解)
不過對于Camera的坐標系我還有一點點疑問,我準備有機會寫一篇關(guān)于Camera和Matrix文章霞捡。
3.3 滑動事件沖突的處理(先看后面的更新說明)
完成上面兩個步驟薄疚,那么我們就算Over了嗎?
不街夭!還有很重要的一點,就是事件沖突的處理板丽。 舉個例子:我們把手放到我們的容器上,系統(tǒng)怎么知道我們這個滑動事件是給容器還是要給容器的子類的呢?
(給容器自己猖辫,則進行滑動的操作砚殿,給容器的子類啃憎,則容器的子類可以進行點擊事件的判斷處理)
對于這種情況似炎,我就很大度啦,全部交給容器子類處理羡藐!子類不要,OK,那容器你自己拿來玩吧岸晦。
————之所以不走尋常路:交給容器處理睛藻,容器不需要再交給子類
原因在于:容器拿到滑動事件只需要做滑動操作,而子類則不同邢隧,它有點擊事件需要判斷店印,一個容器有很多子類,而很多子類只有一個共同的容器倒慧,如果把控制權(quán)交給容器按摘,那么容器怎么可能能夠判斷得出不同的子類到底需不需要這個滑動事件呢?所以炫贤,既然這么麻煩付秕,那么統(tǒng)統(tǒng)交給子類處理。
交給子類處理询吴,則容器中onInterceptTouchEvent需要做如下操作
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
}
return true;
}
而子類(用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);
}
在isContain中唠摹,我做的是點擊的坐標是否在Edittext中奉瘤,在則攔截,子類處理望艺,不在肌访,則交給父類容器
private boolean isContain(MotionEvent event) {
region.set(rect);
if (region.contains((int) event.getX(), (int) event.getY())) {
return true;
}
return false;
}
當然交給子類這樣也導致了一個問題,就是我如果需要給容器中的子類進行點擊事件吼驶,則都需要自定義一個View(例如上面的CustomEdittext 繼承自Edittext)。
例如我就自定義了三個View风钻,不過還是很簡單的酒请,幾分鐘的事就搞定了(在自定義View中dispatchTouchEvent進行判斷)。
具體的可以參考代碼。
更新說明 2016/8/5
滑動沖突之前我是把控制權(quán)交給了子類囤萤,這里https://github.com/Y-bao 這位作者提交的pull
request中將事件沖突交給了父類(StereoView)
是趴,我這邊通過了pull,我覺得寫得挺好的富雅,把點擊事件的控制權(quán)轉(zhuǎn)移給父類肛搬,就不需要自定義View。
如果你還想查看控制權(quán)轉(zhuǎn)移給子類的代碼(我之前的)图筹,可以點擊這里
3.4 點擊水紋波效果
細心的人會發(fā)現(xiàn)让腹,我這里還有個RippleView。
沒錯這就是點擊后有水紋波的效果骇窍。
Android本身可以在XML中用ripple實現(xiàn),不過是Android 5.0以上痢掠,個人覺得兼容性不太好嘲恍,就自己隨便寫了一個簡易的,哈哈佃牛,效率不能保證,各位看客看看就好啦象缀。
4.應用
4.1 定義的方法
使用方法也和其他的沒有什么區(qū)別爷速,我這里自定義了幾個方法,我這里說明下惫东。
自定義的方法
setStartScreen(int startScreen) :設(shè)置第一頁展示的頁面 @param startScreen (0,getChildCount-1)
setResistance(float resistance) : 設(shè)置滑動阻力 @param resistance (0,...)
setInterpolator(Interpolator mInterpolator) : 設(shè)置滾動時interpolator插補器
setAngle(float mAngle):設(shè)置滾動時兩個item的夾角度數(shù) [0f,180f]
setCan3D(boolean can3D) : 是否開啟3D效果
setItem(int itemId) : 跳轉(zhuǎn)到指定的item @param itemId [0,getChildCount-1]
toPre() : 上一頁
toNext() : 下一頁
定義的回調(diào)接口
4.2 使用方法
直接在布局中
在代碼中
4.3 缺陷說明
目前容器的item數(shù)量需要大于等于3禁谦,小于3個滑動時會些問題。設(shè)置的最開始展示的item位置不能是第一個或者最后一個州泊,這么做是為了保證第1個或者最后一個被隱藏漂洋,從而保證最開始向上滑動或者向下滑動時的正常。
5.下載
如果覺得對你有幫助演训,歡迎 star贝咙,fork,如果對于我感興趣庭猩,歡迎follow 我
下載地址 :https://github.com/ImmortalZ/StereoView
參考文章: