android自定義佈局中的平滑移動

在android應用程序的開發過程中,相信我們很多人都想把應用的交互做的比較絢麗,比如讓界面切換平滑的滾動,還有熱度灰常高的僞3D等界面效果,通常情況下,系統提供的應用在特效這方面只能爲我們提供簡單的動畫接口,所以要想實現比較酷炫的效果還是要自己去開發佈局控件(即所謂的自定義View、ViewGroup)。小弟也經常做一些自定義的控件,最近工作比較清閒,所以便將自己對自定義佈局控件的一些心得寫出來,權當是自己的學習筆記了,各位高手看到了可以忽略。下面就我最近工作中遇到的一個自定義控件開發做一些簡單的介紹,其實那個地方原本可以用ScrollView解決很大一部分問題的,但有一些效果確實需要對控件進行重新定義,在繼承ScrollView開發中仍然會遇到一些ScrollView自身的限制,所以就仿照ScrollView自己做了一個控件。在其中遇到了一些問題自然就是像ScrollView中拖動的效果(比如快速拖動在手指離開屏幕時控件依舊會由於慣性繼續滑動一段距離後纔會停止運動),所以就對這個東東做了一下仔細的研究,雖然以前也做過類似的開發,這次由於時間比較充裕,所以將開發中遇到的一些問題都一一記錄了下來。下面開始正題:

自定義佈局控件自然是要繼承某個View或ViewGroup

由於是根據項目的開發來寫的這篇博客,所以我就以自定義佈局控件(ViewGroup)來做介紹了。

開發一個自定義的ViewGroup自然是要繼承ViewGroup類了,在繼承這個類之後必須要重寫的方法就是

onLayout(boolean changed, int l, int t, int r, int b)

另外至少要有一個構造方法,我個人習慣重寫那個有兩個參數的構造方法(XXX(Context context, AttributeSet attrs)),因爲有了這個構造方法就可以在xml佈局文件裏使用這個類了。

如果想要對這個佈局控件以及其子控件的尺寸進行精確的控制那就要重寫下面這個方法了

onMeasure(int widthMeasureSpec, int heightMeasureSpec)

這個方法從字面理解就是估算控件的尺寸大小了,關於這個方法的詳細說明引用一下另一位童鞋的文章http://www.eoeandroid.com/thread-102385-1-1.html,這裏就不詳細介紹了

 

下面開始介紹關於如何讓自定義的控件進行平滑的移動,並能夠根據手勢的情況產生慣性滑動的效果

先介紹一下開發這種滑動效果需要用到的各種工具類:

android.view.VelocityTracker

android.view.Scroller

android.view.ViewConfiguration

VelocityTracker從字面意思理解那就是速度追蹤器了,在滑動效果的開發中通常都是要使用該類計算出當前手勢的初始速度(不知道我這麼理解是否正確,對應的方法是velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity))並通過getXVelocity或getYVelocity方法得到對應的速度值initialVelocity,並將獲得的速度值傳遞給Scroller類的fling(int startX, int startY, int velocityX, int velocityY, int minX, int maxX, int minY, int maxY) 方法進行控件滾動時各種位置座標數值的計算,API中對fling 方法的解釋是基於一個fling手勢開始滑動動作,滑動的距離將由所獲得的初始速度initialVelocity來決定。關於ViewConfiguration 的使用主要使用了該類的下面三個方法:

configuration.getScaledTouchSlop() //獲得能夠進行手勢滑動的距離
configuration.getScaledMinimumFlingVelocity()//獲得允許執行一個fling手勢動作的最小速度值
configuration.getScaledMaximumFlingVelocity()//獲得允許執行一個fling手勢動作的最大速度值

需要重寫的方法至少要包含下面幾個方法:

onTouchEvent(MotionEvent event)//有手勢操作必然少不了這個方法了

computeScroll()//必要時由父控件調用請求或通知其一個子節點需要更新它的mScrollX和mScrollY的值。典型的例子就是在一個子節點正在使用Scroller進行滑動動畫時將會被執行。所以,從該方法的註釋來看,繼承這個方法的話一般都會有Scroller對象出現。

 

在往下就是介紹比較具體的開發思路

首先我們要初始化一些變量,其中的多數代碼已經在上面做出介紹了

複製代碼
void init(Context context) {
                mScroller = new Scroller(getContext());
                setFocusable(true);
                setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
                setWillNotDraw(false);
                final ViewConfiguration configuration = ViewConfiguration.get(context);
                mTouchSlop = configuration.getScaledTouchSlop();
                mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
                mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();

        }
複製代碼
然後我們申明一個用來處理滑動操作的方法fling(int velocityY),代碼如下:
複製代碼
public void fling(int velocityY) {
        if (getChildCount() > 0) {
                mScroller.fling(getScrollX(), getScrollY(), 0, velocityY, 0, 0, 0,
                                maxScrollEdge);
                final boolean movingDown = velocityY > 0;
                awakenScrollBars(mScroller.getDuration());
                invalidate();
        }
}
複製代碼
在這個方法裏只是使用Scroller的fling方法開始執行fling手勢動作了,關於其中的各種參數就不一一解釋了。
awakenScrollBars(int startDelay)方法根據我對註釋的理解就是在這裏給出動畫開始的延時,當參數startDelay爲0時動畫將立刻開始,其實就是一個延遲的作用


下面是對VelocityTracker的初始化以及資源釋放的方法
複製代碼
private void obtainVelocityTracker(MotionEvent event) {
        if (mVelocityTracker == null) {
                mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
}

private void releaseVelocityTracker() {
        if (mVelocityTracker != null) {
                mVelocityTracker.recycle();
                mVelocityTracker = null;
        }
}
複製代碼
onTouchEvent(MotionEvent event)方法的重寫
複製代碼
public boolean onTouchEvent(MotionEvent event) {
                if (event.getAction() == MotionEvent.ACTION_DOWN
                                && event.getEdgeFlags() != 0) {
                        return false;
                }

                obtainVelocityTracker(event);

                final int action = event.getAction();
                final float x = event.getX();
                final float y = event.getY();

                switch (action) {
                case MotionEvent.ACTION_DOWN:
                        LogUtil.log(TAG, "ACTION_DOWN#currentScrollY:" + getScrollY()
                                        + ", mLastMotionY:" + mLastMotionY,
                                        LogUtil.LOG_E);
                        if (!mScroller.isFinished()) {
                                mScroller.abortAnimation();
                        }
                        mLastMotionY = y;
                        break;

                case MotionEvent.ACTION_MOVE:
                        final int deltaY = (int) (mLastMotionY - y);
                        mLastMotionY = y;
                        if (deltaY < 0) {
                                if (getScrollY() > 0) {
                                        scrollBy(0, deltaY);
                                } 
                        } else if (deltaY > 0) {
                                mIsInEdge = getScrollY() <= childTotalHeight - height;
                                if (mIsInEdge) {
                                        scrollBy(0, deltaY);
                                }
                        }
                        break;

                case MotionEvent.ACTION_UP:
                        final VelocityTracker velocityTracker = mVelocityTracker;
                        velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                        int initialVelocity = (int) velocityTracker.getYVelocity();

                        if ((Math.abs(initialVelocity) > mMinimumVelocity)
                                        && getChildCount() > 0) {
                                fling(-initialVelocity);
                        }

                        releaseVelocityTracker();
                        break;
                }

                return true;
        }
複製代碼
在onTouchEvent方法中,當手勢執行到ACTION_UP時獲得當時手勢的速度值然後判斷這個速度值是否大於可滑動的最小速度,如果符合條件那麼就執行fling(int velocityY)方法,通過fling方法中的日誌發現,在執行了invalidate()方法之後,程序便會執行computeScroll()方法,在computeScroll()方法中執行scrollTo方法主要是因爲mScrollX、mScrollY這兩個變量的修飾符爲portected,無法在擴展類裏面無法對這兩個變量直接進行操作,那麼就需要使用scrollTo方法對這兩個變量進行操作,以刷新當前的UI控件,下面附上computeScroll()方法的代碼
複製代碼
public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
                int scrollX = getScrollX();
                int scrollY = getScrollY();
                int oldX = scrollX;
                int oldY = scrollY;
                int x = mScroller.getCurrX();
                int y = mScroller.getCurrY();
                scrollX = x;
                scrollY = y;
                scrollY = scrollY + 10;
                scrollTo(scrollX, scrollY);
                postInvalidate();
        }
}
複製代碼
其中的mScroller.computeScrollOffset()是用來判斷動畫是否完成,如果沒有完成返回true繼續執行界面刷新的操作,各種位置信息將被重新計算用以重新繪製最新狀態的界面。關於scrollTo方法,我們需要看一下該方法的代碼(來自View中):
複製代碼
public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                invalidate();
            }
        }
    }
複製代碼
我們可以看到,當傳遞進來的x、y的值與控件當前的mScrollX、mScrollY的值不相同時對界面進行重新計算,根據日誌打印的情況來看似乎awakenScrollBars()返回的總是true, 這樣的話每執行一次computeScroll()方法,就需要執行一次postInvalidate()方法來刷新界面,而postInvalidate()方法會通過內部線程重新調用invalidate()已達到界面刷新的效果,產生手勢離開屏幕之後的慣性滑動效果。


可能上面說的比較凌亂,在這裏總結一下,大概的思路如下:
首先我們通過VelocityTrackerViewConfiguration類得到一些慣性滑動所必須的變量,比如手勢離開屏幕時的初始速度,允許進行手勢操作的最小距離以及允許手勢操作的速度邊界值;
第二,創建Scroller的對象,使用它的fling方法供我們控制界面滑動使用;
第三,重寫onTouchEvent方法,當我們用手指在屏幕上來回滑動時此時執行的是scrollBy方法來刷新界面,當手指離開屏幕,此時就要開始執行ACTION_UP後面的操作了;
通過對手指離開屏幕時的速度進行判斷是否能夠進行慣性滑動操作,
如果能夠執行那麼就使用Scroller類的fling方法啓動滑動動畫,
這時需要調用一下invalidate()方法來間接的調用computeScroll方法,
在computeScroll方法中對Scroller的動畫是否執行完成做了判斷,
如果動畫沒有完成(mScroller.computeScrollOffset() == true)那麼就使用scrollTo方法對mScrollX、mScrollY的值進行重新計算刷新界面,
調用postInvalidate()方法重新繪製界面,
postInvalidate()方法會調用invalidate()方法,
invalidate()方法又會調用computeScroll方法,
就這樣周而復始的相互調用,直到mScroller.computeScrollOffset() 返回false纔會停止界面的重繪動作


總結,滑動效果來看,它依然是在不停的計算控件的位置刷新屏幕,不停的繪製新的圖片替換舊的圖片,當然每次刷新的速度很快,從而給人一種是在快速滑動的感覺,寫到這裏我發現,現在所謂的動畫總是逃脫不了電影的那種模式,每秒播放多少幀的圖片來達到連續播放的效果欺騙人的眼睛。
而且,關於android一些酷炫效果的開發,還是要自己多動手,熟悉View、ViewGroup中每個繪製方法、位置計算方法的調用方式以及順序,那麼至少是在2D動畫開發中,也就是一種方式,逃脫不了不停重新繪製的這個圈。
關於熟悉View、ViewGroup中每個繪製方法、位置計算方法的調用方式以及順序的問題,我建議最好自己寫一個簡單的自定義View或ViewGroup的擴展類,重載那些繪製、位置計算的方法打個日誌出來一看自然就明白了,雖然這個方法很笨,但是很容易出效果的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章