從零開始打造一個Android 3D立體旋轉容器

來源:互聯網
上載者:User

標籤:

本文地址,轉載請註明 http://blog.csdn.net/mr_immortalz/article/details/51918560

嗯,2個月沒有寫部落格,是要好好反省下,趁著放暑假把這兩個月看的東西好好沉澱下。嗯,就立下這個Flag,希望不要自己再打自己臉。

1.概述

回到正題,這次帶來的效果,是一個Android 的3D立體旋轉的效果。
當然靈感的來源,來自早些時間微博上看到的。
非常酷有木有!作為程式猿我當然要把它加入我的下一個項目中啦!
原效果

我們實現的效果:

(為了更加可定製化,我在原圖基礎上新增了新的效果)

可以快速滾動,並且無限迴圈

這個是對一些參數的進行設定

對圖片的包裹效果

因為本身繼承自ViewGroup,所以基本控制項都是可以包裹的

2.分析
因為代碼量有點大,感覺把代碼全部粘貼上來也不現實。所以想瞭解我的思路的盆友可以先來這裡下載代碼。然後邊看代碼邊看我的分析

:https://github.com/ImmortalZ/StereoView

通過我們實現的可以發現:

1.切換的時候是一個3D立體的效果

2.布局中的每一個Item可以自由切換,且無限迴圈滾動

要解決上面的效果,我們需要什麼技術點呢?

1.要想實現一個3D效果,我們可以藉助Android中的Camera、Matrix

2.要想實現滾動,毫無疑問,我們需要藉助Scroller

當然一切看起來很簡單,其實不然,除此之外,你還需要對於滑動衝突進行處理等等,下面我開始介紹啦。

這就是我們這次項目的大致

3.實現
因為我們是要打造一個容器類,所以肯定得繼承自 ViewGroup按照一般的思路,我們肯定是先要進行一些變數的申明,onMeasure,onLayout操作
private void init(Context context) {    mCamera = new Camera();    mMatrix = new Matrix();    if (mScroller == null) {        mScroller = new Scroller(context);    }}@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {    super.onMeasure(widthMeasureSpec, heightMeasureSpec);    measureChildren(widthMeasureSpec, heightMeasureSpec);    mWidth = getMeasuredWidth();    mHeight = getMeasuredHeight();    //滑動到設定的StartScreen位置    scrollTo(0, mStartScreen * mHeight);}@Overrideprotected 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;    }

當手從螢幕上移開時,我們來看下這個方法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 flingSpeed = (yVelocity - standerSpeed) > 0 ? (int) (yVelocity - standerSpeed) : 0;    addCount = flingSpeed / 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);        }    }

好,我們來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();    }

這裡面的關鍵就在於
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)位置,最後再移動到物體原來中心位置即可。

具體的大家可以參考下這篇文章
http://blog.csdn.net/rav009/article/details/7763223 ( Android postTranslate和preTranslate的理解)

不過對於Camera的座標系我還有一點點疑問,我準備有機會寫一篇關於Camera和Matrix文章。

3.3 滑動事件衝突的處理

完成上面兩個步驟,那麼我們就算Over了嗎?

不!還有很重要的一點,就是事件衝突的處理。 舉個例子:我們把手放到我們的容器上,系統怎麼知道我們這個滑動事件是給容器還是要給容器的子類的呢?

(給容器自己,則進行滑動的操作,給容器的子類,則容器的子類可以進行點擊事件的判斷處理)

對於這種情況,我就很大度啦,全部交給容器子類處理!子類不要,OK,那容器你自己拿來玩吧。

————之所以不走尋常路:交給容器處理,容器不需要再交給子類

原因在於:容器拿到滑動事件只需要做滑動操作,而子類則不同,它有點擊事件需要判斷,一個容器有很多子類,而很多子類只有一個共同的容器,如果把控制權交給容器,那麼容器怎麼可能能夠判斷得出不同的子類到底需不需要這個滑動事件呢?所以,既然這麼麻煩,那麼統統交給子類處理。

交給子類處理,則容器中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進行判斷)。

具體的可以參考代碼。

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 我

:https://github.com/ImmortalZ/StereoView

從零開始打造一個Android 3D立體旋轉容器

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.