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

K3661158 9年前發布 | 74K 次閱讀 安卓開發 Android開發 移動開發

轉載請注明出處: 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的源碼感興趣也可以嘗試去讀一下,不過一定需要非常扎實的基本功才行。

關注我的微信公眾號,第一時間獲得博客的更新提醒,更有很多其它的技術信息分享給大家

掃一掃下方二維碼或搜索微信號guolin_blog即可關注:

來自: http://blog.csdn.net/guolin_blog/article/details/48719871

 本文由用戶 K3661158 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!