1. 程式人生 > >Android Scroller完全解析,關於Scroller你所需知道的一切

Android Scroller完全解析,關於Scroller你所需知道的一切

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/48719871
2016大家新年好!這是今年的第一篇文章,那麼應CSDN工作人員的建議,為了能給大家帶來更好的閱讀體驗,我也是將部落格換成了寬屏版。另外,作為一個對新鮮事物從來後知後覺的人,我終於也在新的一年裡改用MarkDown編輯器來寫部落格了,希望大家在我的部落格裡也能體驗到新年新的氣象。

我寫部落格的題材很多時候取決於平時大家問的問題,最近一段時間有不少朋友都問到ViewPager是怎麼實現的。那ViewPager相信每個人都再熟悉不過了,因此它實在是太常用了,我們可以藉助ViewPager來輕鬆完成頁面之間的滑動切換效果,但是如果問到它是如何實現的話,我感覺大部分人還是比較陌生的, 為此我也是做了一番功課。其實說到ViewPager最基本的實現原理主要就是兩部分內容,一個是事件分發,一個是Scroller,那麼對於事件分發,其實我在很早之前就已經寫過了相關的內容,感興趣的朋友可以去閱讀

Android事件分發機制完全解析,帶你從原始碼的角度徹底理解,但是對於Scroller我還從來沒有講過,因此本篇文章我們就先來學習一下Scroller的用法,並結合事件分發和Scroller來實現一個簡易版的ViewPager。

Scroller是一個專門用於處理滾動效果的工具類,可能在大多數情況下,我們直接使用Scroller的場景並不多,但是很多大家所熟知的控制元件在內部都是使用Scroller來實現的,如ViewPager、ListView等。而如果能夠把Scroller的用法熟練掌握的話,我們自己也可以輕鬆實現出類似於ViewPager這樣的功能。那麼首先新建一個ScrollerTest專案,今天就讓我們通過例子來學習一下吧。
先撇開Scroller類不談,其實任何一個控制元件都是可以滾動的,因為在View類當中有scrollTo()和scrollBy()這兩個方法,如下圖所示:


這兩個方法都是用於對View進行滾動的,那麼它們之間有什麼區別呢?簡單點講,scrollBy()方法是讓View相對於當前的位置滾動某段距離,而scrollTo()方法則是讓View相對於初始的位置滾動某段距離。這樣講大家理解起來可能有點費勁,我們來通過例子實驗一下就知道了。
修改activity_main.xml中的佈局檔案,程式碼如下所示:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools
="http://schemas.android.com/tools" android:id="@+id/layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.example.guolin.scrollertest.MainActivity">
<Button android:id="@+id/scroll_to_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="scrollTo"/> <Button android:id="@+id/scroll_by_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="scrollBy"/> </LinearLayout>

外層我們使用了一個LinearLayout,然後在裡面包含了兩個按鈕,一個用於觸發scrollTo邏輯,一個用於觸發scrollBy邏輯。
接著修改MainActivity中的程式碼,如下所示:

public class MainActivity extends AppCompatActivity {

    private LinearLayout layout;

    private Button scrollToBtn;

    private Button scrollByBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        layout = (LinearLayout) findViewById(R.id.layout);
        scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
        scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
        scrollToBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollTo(-60, -100);
            }
        });
        scrollByBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                layout.scrollBy(-60, -100);
            }
        });
    }
}

沒錯,程式碼就是這麼簡單。當點選了scrollTo按鈕時,我們呼叫了LinearLayout的scrollTo()方法,當點選了scrollBy按鈕時,呼叫了LinearLayout的scrollBy()方法。那有的朋友可能會問了,為什麼都是呼叫的LinearLayout中的scroll方法?這裡一定要注意,不管是scrollTo()還是scrollBy()方法,滾動的都是該View內部的內容,而LinearLayout中的內容就是我們的兩個Button,如果你直接呼叫button的scroll方法的話,那結果一定不是你想看到的。
另外還有一點需要注意,就是兩個scroll方法中傳入的引數,第一個引數x表示相對於當前位置橫向移動的距離,正值向左移動,負值向右移動,單位是畫素。第二個引數y表示相對於當前位置縱向移動的距離,正值向上移動,負值向下移動,單位是畫素。
那說了這麼多,scrollTo()和scrollBy()這兩個方法到底有什麼區別呢?其實執行一下程式碼我們就能立刻知道了:


可以看到,當我們點選scrollTo按鈕時,兩個按鈕會一起向右下方滾動,因為我們傳入的引數是-60和-100,因此向右下方移動是正確的。但是你會發現,之後再點選scrollTo按鈕就沒有任何作用了,介面不會再繼續滾動,只有點選scrollBy按鈕介面才會繼續滾動,並且不停點選scrollBy按鈕介面會一起滾動下去。
現在我們再來回頭看一下這兩個方法的區別,scrollTo()方法是讓View相對於初始的位置滾動某段距離,由於View的初始位置是不變的,因此不管我們點選多少次scrollTo按鈕滾動到的都將是同一個位置。而scrollBy()方法則是讓View相對於當前的位置滾動某段距離,那每當我們點選一次scrollBy按鈕,View的當前位置都進行了變動,因此不停點選會一直向右下方移動。
通過這個例子來理解,相信大家已經把scrollTo()和scrollBy()這兩個方法的區別搞清楚了,但是現在還有一個問題,從上圖中大家也能看得出來,目前使用這兩個方法完成的滾動效果是跳躍式的,沒有任何平滑滾動的效果。沒錯,只靠scrollTo()和scrollBy()這兩個方法是很難完成ViewPager這樣的效果的,因此我們還需要藉助另外一個關鍵性的工具,也就我們今天的主角Scroller。
Scroller的基本用法其實還是比較簡單的,主要可以分為以下幾個步驟:
1. 建立Scroller的例項
2. 呼叫startScroll()方法來初始化滾動資料並重新整理介面
3. 重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
那麼下面我們就按照上述的步驟,通過一個模仿ViewPager的簡易例子來學習和理解一下Scroller的用法。
新建一個ScrollerLayout並讓它繼承自ViewGroup來作為我們的簡易ViewPager佈局,程式碼如下所示:
/**
 * Created by guolin on 16/1/12.
 */
public class ScrollerLayout extends ViewGroup {

    /**
     * 用於完成滾動操作的例項
     */
    private Scroller mScroller;

    /**
     * 判定為拖動的最小移動畫素數
     */
    private int mTouchSlop;

    /**
     * 手機按下時的螢幕座標
     */
    private float mXDown;

    /**
     * 手機當時所處的螢幕座標
     */
    private float mXMove;

    /**
     * 上次觸發ACTION_MOVE事件時的螢幕座標
     */
    private float mXLastMove;

    /**
     * 介面可滾動的左邊界
     */
    private int leftBorder;

    /**
     * 介面可滾動的右邊界
     */
    private int rightBorder;

    public ScrollerLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 第一步,建立Scroller的例項
        mScroller = new Scroller(context);
        ViewConfiguration configuration = ViewConfiguration.get(context);
        // 獲取TouchSlop值
        mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            // 為ScrollerLayout中的每一個子控制元件測量大小
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (changed) {
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 為ScrollerLayout中的每一個子控制元件在水平方向上進行佈局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
            // 初始化左右邊界值
            leftBorder = getChildAt(0).getLeft();
            rightBorder = getChildAt(getChildCount() - 1).getRight();
        }
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                mXDown = ev.getRawX();
                mXLastMove = mXDown;
                break;
            case MotionEvent.ACTION_MOVE:
                mXMove = ev.getRawX();
                float diff = Math.abs(mXMove - mXDown);
                mXLastMove = mXMove;
                // 當手指拖動值大於TouchSlop值時,認為應該進行滾動,攔截子控制元件的事件
                if (diff > mTouchSlop) {
                    return true;
                }
                break;
        }
        return super.onInterceptTouchEvent(ev);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                mXMove = event.getRawX();
                int scrolledX = (int) (mXLastMove - mXMove);
                if (getScrollX() + scrolledX < leftBorder) {
                    scrollTo(leftBorder, 0);
                    return true;
                } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                    scrollTo(rightBorder - getWidth(), 0);
                    return true;
                }
                scrollBy(scrolledX, 0);
                mXLastMove = mXMove;
                break;
            case MotionEvent.ACTION_UP:
                // 當手指擡起時,根據當前的滾動值來判定應該滾動到哪個子控制元件的介面
                int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                int dx = targetIndex * getWidth() - getScrollX();
                // 第二步,呼叫startScroll()方法來初始化滾動資料並重新整理介面
                mScroller.startScroll(getScrollX(), 0, dx, 0);
                invalidate();
                break;
        }
        return super.onTouchEvent(event);
    }

    @Override
    public void computeScroll() {
        // 第三步,重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯
        if (mScroller.computeScrollOffset()) {
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            invalidate();
        }
    }
}

整個Scroller用法的程式碼都在這裡了,程式碼並不長,一共才100多行,我們一點點來看。
首先在ScrollerLayout的建構函式裡面我們進行了上述步驟中的第一步操作,即建立Scroller的例項,由於Scroller的例項只需建立一次,因此我們把它放到建構函式裡面執行。另外在構建函式中我們還初始化的TouchSlop的值,這個值在後面將用於判斷當前使用者的操作是否是拖動。
接著重寫onMeasure()方法和onLayout()方法,在onMeasure()方法中測量ScrollerLayout裡的每一個子控制元件的大小,在onLayout()方法中為ScrollerLayout裡的每一個子控制元件在水平方向上進行佈局。如果有朋友對這兩個方法的作用還不理解,可以參照我之前寫的一篇文章 Android檢視繪製流程完全解析,帶你一步步深入瞭解View(二)
接著重寫onInterceptTouchEvent()方法, 在這個方法中我們記錄了使用者手指按下時的X座標位置,以及使用者手指在螢幕上拖動時的X座標位置,當兩者之間的距離大於TouchSlop值時,就認為使用者正在拖動佈局,然後我們就將事件在這裡攔截掉,阻止事件傳遞到子控制元件當中。
那麼當我們把事件攔截掉之後,就會將事件交給ScrollerLayout的onTouchEvent()方法來處理。如果當前事件是ACTION_MOVE,說明使用者正在拖動佈局,那麼我們就應該對佈局內容進行滾動從而影響拖動事件,實現的方式就是使用我們剛剛所學的scrollBy()方法,使用者拖動了多少這裡就scrollBy多少。另外為了防止使用者拖出邊界這裡還專門做了邊界保護,當拖出邊界時就呼叫scrollTo()方法來回到邊界位置。
如果當前事件是ACTION_UP時,說明使用者手指擡起來了,但是目前很有可能使用者只是將佈局拖動到了中間,我們不可能讓佈局就這麼停留在中間的位置,因此接下來就需要藉助Scroller來完成後續的滾動操作。首先這裡我們先根據當前的滾動位置來計算佈局應該繼續滾動到哪一個子控制元件的頁面,然後計算出距離該頁面還需滾動多少距離。接下來我們就該進行上述步驟中的第二步操作,呼叫startScroll()方法來初始化滾動資料並重新整理介面。startScroll()方法接收四個引數,第一個引數是滾動開始時X的座標,第二個引數是滾動開始時Y的座標,第三個引數是橫向滾動的距離,正值表示向左滾動,第四個引數是縱向滾動的距離,正值表示向上滾動。緊接著呼叫invalidate()方法來重新整理介面。
現在前兩步都已經完成了,最後我們還需要進行第三步操作,即重寫computeScroll()方法,並在其內部完成平滑滾動的邏輯 。在整個後續的平滑滾動過程中,computeScroll()方法是會一直被呼叫的,因此我們需要不斷呼叫Scroller的computeScrollOffset()方法來進行判斷滾動操作是否已經完成了,如果還沒完成的話,那就繼續呼叫scrollTo()方法,並把Scroller的curX和curY座標傳入,然後重新整理介面從而完成平滑滾動的操作。
現在ScrollerLayout已經準備好了,接下來我們修改activity_main.xml佈局中的內容,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    >

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is first child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is second child view"/>

    <Button
        android:layout_width="match_parent"
        android:layout_height="100dp"
        android:text="This is third child view"/>

</com.example.guolin.scrollertest.ScrollerLayout>

可以看到,這裡我們在ScrollerLayout中放置了三個按鈕用來進行測試,其實這裡不僅可以放置按鈕,放置任何控制元件都是沒問題的。
最後MainActivity當中刪除掉之前測試的程式碼:

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}

好的,所有程式碼都在這裡了,現在我們可以執行一下程式來看一看效果了,如下圖所示:


怎麼樣,是不是感覺有點像一個簡易的ViewPager了?其實藉助Scroller,很多漂亮的滾動效果都可以輕鬆完成,比如實現圖片輪播之類的特效。當然就目前這一個例子來講,我們只是藉助它來學習了一下Scroller的基本用法,例子本身有很多的功能點都沒有去實現,比如說ViewPager會根據使用者手指滑動速度的快慢來決定是否要翻頁,這個功能在我們的例子中並沒有體現出來,不過大家也可以當成自我訓練來嘗試實現一下。

好的,那麼本篇文章就到這裡,相信通過這篇文章的學習,大家已經能夠熟練掌握Scroller的使用方法了,當然ViewPager的內部實現要比這複雜得多,如果有朋友對ViewPager的原始碼感興趣也可以嘗試去讀一下,不過一定需要非常紮實的基本功才行。

關注我的技術公眾號,每天都有優質技術文章推送。關注我的娛樂公眾號,工作、學習累了的時候放鬆一下自己。

微信掃一掃下方二維碼即可關注: