Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> scrollTo + Scroller + ViewDragHelper

scrollTo + Scroller + ViewDragHelper

編輯:關於Android編程

看標題就知道這篇文章講的主要是view滑動的相關內容。

ScrollTo && ScrollBy

先看下源碼:

public void scrollTo(int x, int y) {
    if (mScrollX != x || mScrollY != y) {
        int oldX = mScrollX;
        int oldY = mScrollY;
        mScrollX = x;
        mScrollY = y;
        invalidateParentCaches();
        onScrollChanged(mScrollX, mScrollY, oldX, oldY);
        if (!awakenScrollBars()) {
            postInvalidateOnAnimation();
        }
    }
}

public void scrollBy(int x, int y) {
    scrollTo(mScrollX + x, mScrollY + y);
}

可以看到scrollBy其實也是調用了scrollTo,區別就是scrollBy是根據相對位置移動,而scrollTo是移動到指定的位置,與原來位置沒什麼關系。
不過這兩個方法移動的是view中的內容,而不是view。舉個例子,如果是textview的話,那麼滑動的是控件中的文字,而textview本身並不會移動。scrollTo很簡單,就不多說了。那來解釋下scrollBy中的變量mScrollX和mScrollY。
mScrollX和mScrollY記錄的是當前view內容所處的位置(移動後所處的位置,一開始為0),這兩個值分別有正值和負值。
mScrollX = view.left - viewcontent.left;
mScrollY = view.top - viewcontent.top;
這裡的view.left指的是該view的左邊框。viewcontent就指的是view內容的左邊框。
這裡寫圖片描述
看個例子就是移動TextView中的內容:
這裡寫圖片描述這裡寫圖片描述
看到這裡應該明白scrollTo和scrollBy的用法了,不過這種方法有2個缺陷:<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4KCgrI57n7v9i8/srHyrnTw3dyYXBfY29udGVudLXEu7CjrMjnufvSxravxNrI3bvhtbzWwsTayN2xu9Xa16GjrL+0sru8+6GjCta00NBzY3JvbGxUb7rNc2Nyb2xsQnm6r8r9tcS7sKOsu+HLsrzk0sa2r6Os08O7p8zl0emyu7rDo6zI57n7u6y2r7XEvuDA67HIvc+087XEu7CjrNPDu6e/ycTcuPzPo837t9a24Lj20KGyvbustq/N6qOstviyu8rH0ruyvb7Nu6zN6qGjxMfV4rj2yrG68lNjcm9sbGVyvs3FycnP08OzocHLo6zL+77NysfAtL3ivvbI57rO0ruyvbK9u6y2r83qxOPP69KqtcS+4MDroaMKCgoKCjxoMiBpZD0="scroller">Scroller

首先需要解釋的是,Scroller並沒有直接操作View的移動,看源代碼就知道Scroller中並沒有一個記錄view的變量。Scroller中許多變量都是int變量,Scroller的工作就是根據你提供的數據(從哪個坐標開始移動,橫向縱向各走多少停下,移動時間總共要花多久)來計算出每一步你應該走在哪個坐標,computeScrollOffset方法來計算你是否需要繼續走,如果返回true,則view還需要繼續滑動,你可以通過getCurrX和getCurrY方法來得到下一步應該移動到哪一步,然後你可以通過scrollTo和scrollBy來移動view;如果返回false,則說明已經走到終點。
現在結合Scroller和scrollTo來展示一個例子:
主要是利用TextView展示如何滑動文字,想要的效果是讓Textview中的文字向右下方滑動50個像素。
這裡寫圖片描述
首先需要自定義一個TextView。

public class ScrollTextView extends TextView {
    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private Scroller mScroller = new Scroller(getContext());

    public void startScroll() {
        mScroller.startScroll(0, 0, -50, -50, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

上面代碼中startScroll函數中,告訴scroller在1秒內開始從(0,0)開始向右向下移動50個像素。首先解釋下為什麼是-50,因為在前面介紹scrollTo中講到過,mScrollX是等於view.left - viewcontent.left,所以如果你向右平移的話,mScrollX的值為負數(view的左邊框在viewcontent的左邊,減一減當然為負數啦)。
剛才講到scroller其實是把一步ScrollTo分成好多步來走,對於每一步的話都是調用onDraw來重新繪制,所以看起來是在滑動的。每次Draw的時候都會調用computeScroll函數,可以在該函數中,去獲取每一步的坐標,然後調用scrollTo函數,然後去刷新(invalidate函數)。
接下來就是在xml中定義一個ScrollTextView了,需要注意的是,要把大小弄得大一點:



public class MainActivity extends AppCompatActivity {

    private ScrollTextView tview;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tview = (ScrollTextView) findViewById(R.id.stv);
    }

    public void onClick(View view) {
        tview.startScroll();
    }
}

為什麼要把ScrollTextView的大小寫的大點?如果你layout_width和layout_height是wrap_content的話,那你滑動content的話,就看不見了,這就是scrollto的缺點,滑動到view的區域外就看不見了!!!ViewDragHelper就是解決這個的。
Scroller還有一個要說的就是fling,fling的意思是急沖的意思。在Scroller中有兩種模式,一種是SCROLL_MODE,另一個是FLING_MODE。

scroll用於已知目標位置的情況(例如:Viewpager中向左滑動,就是要展示右邊的一頁,那麼我們就可以准確計算出滑動的目標位置,此時就可以使用Scroller.startScroll()方法) fling用於不能准確得知目標位置的情況(例如:ListView,每一次的滑動,我們事先都不知道滑動距離,而是根據手指抬起是的速度來判斷是滑遠一點還是近一點,這時就可以使用Scroller.fling()方法)
fling的效果就是那種我們在浏覽網頁,然後點擊一鍵返回頂部,然後頁面快速滾動回到頂部的效果。fling執行的時間往往比較短,比較快。用的比較少,這裡暫不介紹,具體看API就好了。

ViewDragHelper

這是一個工具類,主要是方便我們自定義ViewGroup,他提供一些有用的操作並且跟蹤狀態,允許用戶在父viewgroup中去drag(拖)和reposition(重定位)一些child views。
Android ViewDragHelper完全解析 自定義ViewGroup神器這篇文章講解了ViewDragHelper中是怎麼用的以及API,這裡就不詳述了。
那ViewDragHelper實現的原理是怎麼樣的呢?
在剛剛提供的那篇文章中,有一個demo,我運行了下,給大家看下效果:
這裡寫圖片描述
我觸碰第一個TextView並將其拖拽至其他地方。但是在上述效果中看到,移動的只有文字,TextView的邊界並沒有移動,還是在原來的左上方位置。
ViewDragHelper是用在ViewGroup中,而不是用在View中的,他的作用是移動ViewGroup中的child view。這裡你需要知道ViewGroup事件傳遞的相關知識,可以看該篇文章View的事件分發。ViewDragHelper主要是攔截ViewGroup的觸摸事件,根據手勢滑動的軌跡來滑動View。ViewDragHelper中也用到了Scroller,但是其繪制的時候,卻不是通過scrollTo方法的,是通過ViewCompat.offsetLeftAndRight和ViewCompat.offsetTopAndBottom方法。ViewDragHelper中類似Scroller的computeScrollOffset的方法是continueSettling,可以通過這個方法來繪制View,看下源碼:

    public boolean continueSettling(boolean deferCallbacks) {
        if (mDragState == STATE_SETTLING) {
            boolean keepGoing = mScroller.computeScrollOffset();
            final int x = mScroller.getCurrX();
            final int y = mScroller.getCurrY();
            final int dx = x - mCapturedView.getLeft();
            final int dy = y - mCapturedView.getTop();

            if (dx != 0) {
                ViewCompat.offsetLeftAndRight(mCapturedView, dx);
            }
            if (dy != 0) {
                ViewCompat.offsetTopAndBottom(mCapturedView, dy);
            }

            if (dx != 0 || dy != 0) {
                mCallback.onViewPositionChanged(mCapturedView, x, y, dx, dy);
            }

            if (keepGoing && x == mScroller.getFinalX() && y == mScroller.getFinalY()) {
                // Close enough. The interpolator/scroller might think we're still moving
                // but the user sure doesn't.
                mScroller.abortAnimation();
                keepGoing = false;
            }

            if (!keepGoing) {
                if (deferCallbacks) {
                    mParentView.post(mSetIdleRunnable);
                } else {
                    setDragState(STATE_IDLE);
                }
            }
        }

        return mDragState == STATE_SETTLING;
    }

使用ViewCompat中的兩個方法就可以重新繪制View的content,而對於childView來說,他們什麼代碼都不用變。這種方法替代了ScrollTo,即使content超過了view的邊界,也依舊可以顯示。
這裡需要注意的是ViewCompat中的兩個函數傳入的值,是針對全頁面的絕對位置,而不是針對View邊界的相對位置。
例如傳進scrollTo(10,10)的話,是將content以view邊界為中心,向左上方偏移10,10。如果傳進ViewCompat.offsetLeftAndRight(view, 100)的話,則是將content往右偏移100像素,這與scrollTo是不一樣的(主要是方向不一樣!!!!)。
另外,如果你執行scrollTo(-10,0);這句代碼兩次,content只會向右平移10像素,但是你如果執行ViewCompat.offsetLeftAndRight(view, 10)兩次,那麼content會向右平移20像素,這個是非常重要的區別。因為scrollTo是將傳入的值10賦值給mScrollX,所以無論你執行多少次,它的content都只會向右平移10,但是ViewCompat不同,他不管你當前位置在哪裡,只要你傳入的值不為0,他就會直接向右移動,所以你執行那個函數n次,那麼就會向右移動10n像素。

那根據這個用法,我們修改剛剛scroller + scrollTo的例子,看下代碼:

public class ScrollTextView extends TextView {
    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private Scroller mScroller = new Scroller(getContext());

    public void startScroll() {
        mScroller.startScroll(getLeft(), getTop(), 50, 50, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            Log.d("tag", "computeScroll : " + mScroller.getCurrX() + "," + mScroller.getCurrY() + "," + mScroller.getFinalX() + "," + mScroller.getFinalY());
            ViewCompat.offsetLeftAndRight(this, mScroller.getCurrX() - getLeft());
            ViewCompat.offsetTopAndBottom(this, mScroller.getCurrY() - getTop());
            invalidate();
        }
    }
}

看下修改的地方,第一個主要是調用startScroll傳入的參數不同,這裡傳入的是getLeft、getTop、50,50。先講下後面兩個參數,因為剛剛說過ViewCompat若傳入正值為向右,傳入負值為向左,則按照需求來,這裡應該傳入整數。
在下面computeScroll函數中,調用ViewCompat的兩個方法來進行偏移,接下來先看下打印出的tag日志。
D: computeScroll : 0,237,50,287
D: computeScroll : 1,238,50,287
D: computeScroll : 4,241,50,287
D: computeScroll : 7,244,50,287
D: computeScroll : 10,247,50,287
D: computeScroll : 14,251,50,287
D: computeScroll : 17,254,50,287
D: computeScroll : 22,259,50,287
D: computeScroll : 25,262,50,287
D: computeScroll : 28,265,50,287
D: computeScroll : 31,268,50,287
D: computeScroll : 33,270,50,287
D: computeScroll : 35,272,50,287
D: computeScroll : 37,274,50,287
D: computeScroll : 40,277,50,287
D: computeScroll : 43,280,50,287
D: computeScroll : 44,281,50,287
D: computeScroll : 45,282,50,287
D: computeScroll : 46,283,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 47,284,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 48,285,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 49,286,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
D: computeScroll : 50,287,50,287
從日志中我們可以看出有的時候scroller算出的每一步其實與上一步有重復,一開始getCurrX還是遞增,但是到後面,就出現一些重復的getCurrX,getCurrY也是一樣的規律,這是為什麼呢,讓我們看看代碼computeOff的代碼:

    public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);

                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

因為調用的是startScroll,所以處於SCROLL_MODE模式下。在我們調用startScroll的時候,傳入的時間是1000ms,也就是一秒。看代碼可以知道,在SCROLL_MODE模式下,此次滑動結束唯一的條件就是時間到,即使currX和currY已經到達終點也還得繼續。

case SCROLL_MODE:
    final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
    mCurrX = mStartX + Math.round(x * mDeltaX);
    mCurrY = mStartY + Math.round(x * mDeltaY);
    break;

看下這段代碼,首先會根據當前已經流逝的時間算出占整個時間的百分比,然後乘上deltaX(就是我們傳進去的50),來計算出我們當前在x軸方向上應該處於哪一個位置。然後使用math.round四捨五入,所以有的時候每走一步的時候,會發現四捨五入之後,和上一步的結果是一樣,相當於這一步沒動過。所以這解釋了我們剛剛貼出來的日志中存在重復的坐標。但是對於FLING_MODE不同,他結束的條件有兩種,第一種和SCROLL_MODE一樣,時間到了也就會停止,第二種就是如果currX和currY已經到達終點,則也會停止,即使時間沒到也會停止,這和SCROLL_MODE是不一樣的。
好,現在為止應該能理解剛剛的代碼了,那我現在再給出一份代碼,看看大家能不能看出什麼問題來!

public class ScrollTextView extends TextView {
    public ScrollTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    private Scroller mScroller = new Scroller(getContext());

    public void startScroll() {
        mScroller.startScroll(0, 0, 50, 50, 1000);
        invalidate();
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            Log.d("tag", "computeScroll : " + mScroller.getCurrX() + "," + mScroller.getCurrY() + "," + mScroller.getFinalX() + "," + mScroller.getFinalY());
            ViewCompat.offsetLeftAndRight(this, mScroller.getCurrX());
            ViewCompat.offsetTopAndBottom(this, mScroller.getCurrY());
            invalidate();
        }
    }
}

讀者可以思考下下面這段代碼能不能達到我們想要的效果:將content向右下方移動50像素。
事實上這是達不到我們的效果的,實際上的效果是content向右下方不斷移動,在我的手機上content一直平移至滑出屏幕了,那為什麼會這樣呢?
看下日志就知道了
D: computeScroll : 0,0,50,50
D: computeScroll : 1,1,50,50
D: computeScroll : 2,2,50,50
D: computeScroll : 4,4,50,50
D: computeScroll : 7,7,50,50
D: computeScroll : 10,10,50,50
D: computeScroll : 13,13,50,50
D: computeScroll : 17,17,50,50
D: computeScroll : 21,21,50,50
D: computeScroll : 25,25,50,50
D: computeScroll : 28,28,50,50
D: computeScroll : 31,31,50,50
D: computeScroll : 33,33,50,50
D: computeScroll : 35,35,50,50
D: computeScroll : 37,37,50,50
D: computeScroll : 39,39,50,50
D: computeScroll : 40,40,50,50
D: computeScroll : 41,41,50,50
D: computeScroll : 42,42,50,50
D: computeScroll : 43,43,50,50
D: computeScroll : 44,44,50,50
D: computeScroll : 45,45,50,50
D: computeScroll : 46,46,50,50
D: computeScroll : 46,46,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 47,47,50,50
D: computeScroll : 48,48,50,50
D: computeScroll : 48,48,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 49,49,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50
D: computeScroll : 50,50,50,50

看前面幾行就知道了,在第二行執行時,content向右平移1像素,向下平移1像素,在執行第三行時,content向右平移2,向下平移2像素,到現在為止,content已經向下平移1+2 = 3像素了,向右平移1+2=3像素了,但是content在執行完第三行時,按照預想應該只向右平移2像素,向下平移2像素,但是現實卻是多平移了,所以按照日志來看,當全部運行完,content應該是向右平移了1+2+4+7+10……是遠遠大於50像素的,所以這種方法失敗。那為什麼第一種方法成功了呢?
因為在每次content移動的時候,getLeft其實也是相應的改變了,隨著content在移動,所以當getCurrX改變的時候,getLeft也在改變,所以兩者一相減就是正確需要移動的值。

而且下面那種方法在調試的時候又是正確的,這是因為調試的話,每次執行到computeScrollOffset的話,時間不一樣,打斷點的時候,其實時間已經流逝了,所以在調試狀態下,打印出來的日志就兩行,第一行就是0,第二行就是正確移動的地點,很難調試出來發現其中的問題。
下次如果調試是正確的,但是正常運行是錯誤的話,很可能是跟時間有關,例如並發或者類似今天這種問題。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved