從零開始打造一個Android 3D立體旋轉(zhuǎn)容器

本文地址,轉(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)滾動,向下滾動也是如此

QQ截圖20160715190642.png
    @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

參考文章:

http://blog.csdn.net/dawanganban/article/details/38421221

http://blog.csdn.net/rav009/article/details/7763223

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末蔼水,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子趴腋,更是在濱河造成了極大的恐慌,老刑警劉巖颁井,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蠢护,死亡現(xiàn)場離奇詭異,居然都是意外死亡秀又,警方通過查閱死者的電腦和手機贬芥,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來昏苏,“玉大人,你說我怎么就攤上這事贤惯。” “怎么了屁商?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵颈墅,是天一觀的道長。 經(jīng)常有香客問我恤筛,道長,這世上最難降的妖魔是什么望伦? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任煎殷,我火速辦了婚禮,結(jié)果婚禮上愕掏,老公的妹妹穿的比我還像新娘顶伞。我一直安慰自己,他們只是感情好唆貌,可當我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布锨咙。 她就那樣靜靜地躺著语卤,像睡著了一般酪刀。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上眼滤,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天历涝,我揣著相機與錄音漾唉,去河邊找鬼堰塌。 笑死,一個胖子當著我的面吹牛般此,可吹牛的內(nèi)容都是我干的牵现。 我是一名探鬼主播恤煞,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼概漱!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起竿裂,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤照弥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后悔常,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體给赞,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年残邀,在試婚紗的時候發(fā)現(xiàn)自己被綠了柑蛇。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡耻台,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出鼓蜒,到底是詐尸還是另有隱情,我是刑警寧澤都弹,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布畅厢,位于F島的核電站,受9級特大地震影響框杜,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜咪辱,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一油狂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧专筷,春花似錦、人聲如沸磷蛹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽莺葫。三九已至,卻和暖如春再层,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背聂受。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工烤镐, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人碗旅。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像祟辟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子醇份,可洞房花燭夜當晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容