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

shamine 8年前發布 | 55K 次閱讀 Android Android開發 移動開發

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操作 </code></pre>

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(); } } } </code></pre>

完成這些操作后,我們需要在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 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--; } </code></pre>

    然后會進入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);
    

    } } </code></pre>

    最后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

     

    來自:http://blog.csdn.net/Mr_immortalZ/article/details/51918560

     

 本文由用戶 shamine 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!