源碼解析---Scroller徹底解析

                                               Scroller徹底解析

1.概述

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

這兩個方法都是用於對View進行滾動的,那麼它們之間有什麼區別呢?簡單點講,scrollBy()方法是讓View相對於當前的位置滾動某段距離,而scrollTo()方法則是讓View相對於初始的位置滾動某段距離。這樣講你們理解起來可能有點費勁,咱們來經過例子實驗一下就知道了。 
修改activity_main.xml中的佈局文件,代碼以下所示:ide

<?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>
<?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中的代碼,以下所示:函數

 

沒錯,代碼就是這麼簡單。當點擊了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佈局,代碼以下所示:學習

package qu.com.handlerthread;

import android.content.Context;
import android.support.v4.view.ViewConfigurationCompat;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.Scroller;

/**
 * Created by quguangle on 2016/11/23.
 */

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佈局中的內容,以下所示:spa

<?xml version="1.0" encoding="utf-8"?>
<qu.com.handlerthread.ScrollerLayout 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">

    <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"/>
</qu.com.handlerthread.ScrollerLayout>

能夠看到,這裏咱們在ScrollerLayout中放置了三個按鈕用來進行測試,其實這裏不只能夠放置按鈕,放置任何控件都是沒問題的。 
最後MainActivity當中刪除掉以前測試的代碼:.net

public class MainActivity extends AppCompatActivity {

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

好的,全部代碼都在這裏了,如今咱們能夠運行一下程序來看一看效果了,以下圖所示: code

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

相關文章
相關標籤/搜索