從源碼角度深入探尋Scroller的奧祕

前言

給未使用過scroller的人說的話
Scroller是一個跟滑動有關的類(大家都這麼說(大家:我不承認!😱)),很多滑動的操作可以藉助Scroller來完成,而且很多有滑動效果的框架啊什麼的,都是藉助他來完成的,所以如果你們掌握了Scroller類,也能製作那些有意思的滑動效果。

給使用過Scroller的人說的話
你們可能會覺得Scroller有啥講的,不就是調用startScroll方法,然後重寫computeScroll方法不就完了嗎,有啥好講的。對於你們的疑問,我就提一個問題,你們用過Scroller的fling方法嗎,啊?用過啊?呵呵,這樣啊😅;什麼!沒用過?!那就好辦了。

概念

給未使用過scroller的人說的話
關於Scroller這個類,單單隻看名字,我們可以覺得這個類隱隱約約跟滾動有點關係,沒錯,我們經常使用這個類實現一些視圖滾動的行爲。像是大名鼎鼎的ViewPager,內部也用到了Scroller。所以可見Scroller這個類有多厲害。

給使用過Scroller的人說的話
雖說我們經常使用Scroller實現一些滑動效果,但是老實說,經過我對Scroller的研究,這個類確實跟滑動沒有絲毫關係,如果要我定義這個類,這個類應該算是一個由算法合集的工具類,你們其實也知道,在使用Scroller的時候,確實也不能直接那Scroller來做滑動效果,而是利用他計算出來的數據進行單方面的滑動。

用法

在說用法之前,我要大概說明一下,這篇博客的結構,主要是先講用法,然後我會在源碼的基礎上探索整個Scroller,如果你的目的只是爲了知道要如何使用Scroller,那麼你看這個章節就夠了。好了,我開始了。

scrollTo和scrollBy

講用法之前,我們要了解一個知識點,scrollTo(int x, int y)scrollBy(int x, int y)方法,首先要告訴大家的是,這兩個方法都可以讓View滾動起來。我們來快速的舉個例子。

public class MyView extends View {

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
		// 給該view設置點擊事件,每點擊一次,都會執行一次scrollTo方法
        setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.scrollTo(20, 20);
            }
        });

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        // 將這個view的大小固定爲 500x500
        setMeasuredDimension(500, 500);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        // 將該view的背景設置爲紅色
        canvas.drawColor(Color.RED);

        // 在view中間繪製一個半徑爲50的圓,默認paint,所以這個圓是個黑色的
        canvas.drawCircle(250, 250, 50, new Paint());

    }

}

執行效果:

大家可以看到,我在第一次點擊這個view的時候,中間的小黑點確實動了一下,但是之後的點擊就沒用了,大家也能看到我努力嘗試了許久後,最終只能無奈的按下結束錄屏= =

緊接着,我們將這個view裏面的點擊事件裏面的scrollTo,改成scrollBy,也就是這樣:

setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                v.scrollBy(20, 20);
            }
        });

好的,接下來我們再試試:

這次就不一樣了,我們每點擊一次,這個小球都會移動一次,要不我們先來總結下?

當我們使用多次scrollTo的時候,小球只會平移一次,使用多次scrollBy的時候,小球會平移多次
(某讀者:滾蛋!這算哪門子總結!)

好了好了,我們先思考下,爲什麼我們使用的scrollTo(20, 20),明明用的正數,小球爲什麼在向左上角移動,算了,先不多想,記住這個特點就行了。不過我們可以這樣記憶。大家都知道手機的座標系是在手機屏幕的左上角爲原點吧,像這樣:

所以我們暫且將scrollTo(20, 20)理解爲,將手機屏幕上面的原點設爲(20, 20),然後既然(20,20)已經是原點了,不如吧這個點放到手機的左上角。那有人可能會提出第二個問題,這個紅色的框框爲什麼沒有移動,大家細想,我們使用scrollTo(20, 20)只移動了這個黑色小球,是不是意味着scrollTo只能移動當前view的內容,而不能移動view本身。所以如果我們要移動這個view本身,就應該讓這個view的父容器來使用scrollTo方法。

我們還是來看看scrollTo(int x, int y)scrollBy(int x, int y)方法,根據上面的運行結果,我們可以這樣思考,scrollTo像是擁有記憶功能,他能記住自己已經移動了(20,20),但是scrollBy就像是一個沒有記憶功能的方法,他不知道他曾經移動過,每次調用的時候都會移動一遍。

就像這樣:

我們:給我移動(20,20)
scrollTo:移動完了
scrollBy:移動完了
我們:給我移動(20,20)
scrollTo:我已經移動到這裏來了
scrollBy:移動完了
我們:給我移動(20,20)
scrollTo:勞資不移動,我特麼已經移動到這裏來了
scrollBy:移動完了
。。。

所以其實我們可以這樣認爲,scrollTo永遠記得他最開始的位置跟座標系的位置關係,而scrollBy只記得他現在跟座標系的位置關係。
假設,我們調用scrollTo(10, 0),再調用scrollTo(20,0),那麼內容最終會停留在(20,0)
如果我們調用scrollBy(10, 0),再調用scrollBy(20,0),那麼內容最終就會停留在(30,0)

吹逼結束,我們還是來正經看看源碼,看源碼之前,我先提供一個知識點,view這個類有兩個變量:

protected int mScrollX;
protected int mScrollY;

這兩個變量記錄了這個view移動了多少位置,也就是我們最終移動了多少位置。

好了,知識提供結束,我們來看看scrollBy的源碼:

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

沒想到吧,scrollBy還是在scrollTo的基礎上進行的操作,只是加了mScrollX和mScrollY,也就是在原來已經移動過的基礎上再次進行移動。
所以我們來看看scrollTo的源碼:

    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 scrollTo(int x, int y) {
        	...
            mScrollX = x;
            mScrollY = y;
            // 滾動到mScrollX和mScrollY的位置
            ...
    }

哈!是不是簡單多了,上面不是說了嗎,mScrollX和mScrollY記錄了最終移動的位置,來看看這裏,再結合這個scrollTo,是不是有種scrollTo方法的目的,就是滾動到最終位置。我們把scrollBy的源碼scrollTo的源碼結合一下:

    public void scrollBy(int x, int y) {
        	...
            mScrollX = mScrollX + x;
            mScrollY = mScrollY + y;
            // 滾動到mScrollX和mScrollY的位置
            ...
    }

scrollBy就是在已經滾動了的基礎上再滾動一次。

給大家十秒鐘體會一下這兩個方法的區別。

好了,我們繼續講scrollToscrollBy,我們來利用他們實現個小功能,讓我們自定義的view隨着我們的手指運動,既然是隨着我們的手指進行運動,那麼就不得不重寫onTouchEvent方法了,所以最後我們得到以下代碼:

public class MyView extends View {

    private Bitmap bitmap;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                scrollTo(0 - x, 0 - y);
                break;
        }

        return true;
    }

}

運行看看:

也許我們希望按住圖片中間任意一點拖動,於是有了以下代碼:

public class MyView extends View {

    private Bitmap bitmap;

    // 記錄手指剛按下時的座標
    private float firstX;
    private float firstY;
    
    // 記錄總偏移量
    private int sumX;
    private int sumY;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }
    

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                firstY = event.getY();
                break;

            case MotionEvent.ACTION_UP:
                // 記錄總偏移量
                sumX = getScrollX();
                sumY = getScrollY();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                // 在上次移動的基礎上再次移動
                scrollTo(sumX + (int) firstX - x, sumY + (int) firstY - y);

                break;

        }

        return true;
    }

}

效果:

可能這是你們比較喜歡的效果。

關於scrollerTo和scrollBy大概就講到這裏,我們還是來看看Scroller。

Scroller用法

關於scroller的日常用法,主要有以下三個步驟:

  1. 聲明scroller對象
  2. 設置scroller的滾動距離
  3. 重寫computeScroll方法

第一點:我想大家都明白,那麼其他兩點具體怎麼做呢,我具體先描述以下,再結合代碼進行使用。
第二點:設置scroller的滾動距離的意思是什麼意思,其實就是設置我們要把view從哪個位置,滾動到哪個位置,調用scroller的startScroll方法。

startScroll(int startX, int startY, int dx, int dy)

解釋下參數:
startX和startY:代表我們選中的參考點的位置,
dx和dy:代表相對於參考點要移動的位置

這樣說不知道你們懵不懵,用實際的點舉例子好了,假設我們選中(10,10)作爲參考點A,現在A點的座標就是(startX,startY) = (10,10),現在我們相對於A點的偏移(dx,dy) = (20,20),那麼實際上我們偏移了多少,如果是以A點爲參考系,我們就只偏移了(20,20),但是A點相對於原點已經偏移了(10,10),所以我們實際上相對於原點(手機右上角)偏移了(30,30)。

舉個通俗易懂的例子就是,假設有10個椅子排成一排,分別用1到10號表示,讓你坐第2個椅子,你肯定就坐2號椅子了,因爲你默認1號椅子是第一個,如果我這樣說,你坐從2號椅子開始數的第2個椅子,你會坐哪個椅子上,那麼我們就會從第二個椅子開始數,答案自然是3號椅子,其實就是這個道理,只是參考物不一樣而已。

不過爲了讓我們的參考系不要那麼多變化,我們還是將startX和startY設置爲0吧,相對於(0,0)偏移就好了= =

第三點:看到第二點你可能會有疑問,startScroll不是都開始滾動了嗎,爲啥還要重寫computeScroll,話說這個方法是幹啥的啊。
的確,startScroll直接翻譯過來就是開始滾動的意思,但是這個方法完全沒有滾動的功能,不信你看源碼(哎呀,看看源碼怎麼了嘛):


    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }
    
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

看到了吧?就是賦值,其他啥都沒有了。
既然使用startScroll並沒有什麼卵用,所以我們要重寫viewcomputeScroll方法,所以這個方法到底是幹啥用到,我沒打算帶大家走一遍view的源碼,這樣就太長了,所以還是直接告訴你們答案吧,當view發生滾動的時候,會調用一次這個方法,所以連續滾動的時候就會連續調用這個方法,還記得滾動會發生啥事吧,如果你還記得上面我提到的:

protected int mScrollX;
protected int mScrollY;

這兩個變量,你現在就可以這樣理解,view進行滾動,也就是mScrollXmScrollY發生變化的時候,就會調用computeScroll方法。
那麼我們怎麼重寫這個方法呢,老實說,還有最後一步就能使用scroller滾動了,好開森,你特麼倒是快點講啊,磨磨唧唧的。

講這個方法之前,我要告訴大家一個噩耗。唉~,Scroller不能實現滑動功能!

WTF?!不能實現滑動還搞這麼多幺蛾子?

老實說,即便使用了Scroller,但若要view滑動,還得靠scrollByscrollTo,不然你以爲我爲什麼要花大量篇幅來講解這兩個方法😏

在講解怎麼重寫computeScroll方法之前,我先講Scroller的一個很重要的方法,computeScrollOffset,這個方法有一個boolean返回值,表示滾動行爲是否結束,並且每調用一次,我們都可以再通過scroller得到一個位置信息,這個位置我稱爲:當前view應該滾動到的位置。

所以當你調用了Scroller的computeScrollOffset方法後,你就能夠得到當前view應該滾動到什麼位置,然後調用scrollByscrollTo進行實際的滾動了,所以到最後還是得靠scrollByscrollTo

我們來看看具體怎麼重寫,直接上完整代碼,我相信你們看碼的壓力應該也不會那麼大了:

public class MyView extends View {

    private Bitmap bitmap;
    private Scroller scroller;

    public MyView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
        scroller = new Scroller(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }


    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:
                scroller.startScroll(100, 100, 300, 300);
                invalidate();
                break;
        }
        return true;
    }

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

    }
}

再講一下這個computeScroll方法,已經說了,當view有滾動行爲的時候,纔會調用這個方法,這裏面的流程大概是這樣:

if判斷:滾動行爲未完成,進入if內部
得到應該滾動到哪個位置
開始滾動(出現滾動!調用computeScroll
if判斷:滾動行爲未完成,進入if內部
得到應該滾動到哪個位置
開始滾動(出現滾動!調用computeScroll

if判斷:滾動行爲已完成,結束!

就不畫流程圖了,萬一我畫的流程圖,你們看不懂還要思索一會兒才能看懂= =

實踐:實現一個劣質的ViewPager

什麼叫實現一個劣質的viewpager,就是隻實現viewpager的滑動效果,也沒有setAdapter之類的方法,來看看效果

雖然名義上說的是一個劣質的viewpager,不過效果看着還不錯,不是嗎。
本來打算貼上所有源碼就跑路,但是覺得這樣不負責任,所以我們還是來細細的說一下實現思路。

首先這肯定是一個viewgroup,所以我們要自定義一個ViewGroup咯!

看看在哪裏會用到Scroller,首先我們在這個自定義的ViewGroup中添加了3個子view,手指在屏幕上滑動的時候,我們肯定用的是scrollTo或者scrollBy,具體使用哪個方法就要看個人喜好了。

當我們手指離開屏幕的時候,要作判斷,最終需要定位到哪個item,然後自動滑到合適的item,這裏的自動滑動,自然就要派scroller登場了。

首先我們按照正常的自定義一個viewgroup的流程開始,重寫onMeasure方法計算每個view的大小,再重寫onLayout方法,規定每個view在這個viewgroup的位置,所以順其自然的,出現了以下代碼。

public class BadViewPager extends ViewGroup {

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @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);
            // 爲每個子view測量大小
            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);
                // 讓每個子控件都是屏幕寬度,並且水平佈局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }

}

現在我們來使用一下這個容器:

    <com.example.BadViewPager
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#f0f" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#ff0" />

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="#0ff" />

    </com.example.BadViewPager>

目前的效果是這樣的:

但是現在還不能滑動,我們根據上面講的內容,做一下手指觸摸滑動的處理,當然,只需要水平滑動就可以了,所以在使用scrollTo或者scrollBy的時候,就不需要傳遞在Y軸上的變化了,所以就變成了這個樣子:

public class BadViewPager extends ViewGroup {

    private float firstX;
    private int sumX = 0;

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @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);
            // 爲每一個子控件測量大小
            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);
                // 讓每個子控件都是屏幕寬度,並且水平佈局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                break;

            case MotionEvent.ACTION_UP:
                sumX = getScrollX();
                invalidate();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int scrollX = sumX + (int) firstX - x;
                scrollTo(scrollX, 0);
                break;
        }

        return true;
    }

}

效果圖:

嗯,還算有模有樣,接着我們加上scroller,並且當手指離開屏幕的時候,判斷一下應該滾動到哪個item,然後藉助scroller自動滾到對應的位置,所以最終的代碼就是這樣的:

public class BadViewPager extends ViewGroup {

    private float firstX;

    private Scroller scroller;
    
	// 移動多少,就認爲是發生了滑動行爲
    private int slop;

    int sumX = 0;

    // 當前item
    float currItem = 0;

    public BadViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        scroller = new Scroller(context);
        ViewConfiguration viewConfiguration = ViewConfiguration.get(context);
        slop = viewConfiguration.getScaledPagingTouchSlop();
    }

    @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);
            // 爲每一個子控件測量大小
            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);
                // 讓每個子控件都是屏幕寬度,並且水平佈局
                childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
            }
        }

    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        switch (event.getAction()) {

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                break;

            case MotionEvent.ACTION_UP:

                currItem = Math.round((float) getScrollX() / getWidth());
                int dx = (int) (currItem * getWidth() - getScrollX());
                scroller.startScroll(getScrollX(), 0, dx, 0);

                // 記錄總偏移量
                sumX = (int) (currItem * getWidth());

                invalidate();

                break;

            case MotionEvent.ACTION_MOVE:

                int x = (int) event.getX();
                int scrollX = sumX + (int) firstX - x;
                scrollTo(scrollX, 0);

                // 限制每個頁面的邊界
                if (scrollX < 0) {
                    scrollTo(0, 0);
                } else if (scrollX > (getChildCount() - 1) * getWidth()) {
                    scrollTo((getChildCount() - 1) * getWidth(), 0);
                } else {
                    scrollTo(scrollX, 0);
                }

                break;
        }

        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        if (scroller.computeScrollOffset()) {
            int currX = scroller.getCurrX();
            scrollTo(currX, 0);
            invalidate();
        }

    }
}

效果圖跟最開始給大家展示的效果一樣,就不再給大家展示一遍了。

如果你的目的只是爲了知道scroller的用法,我相信你看了以上的篇章,應該就知道了,也許部分東西還不是很清楚,那麼我建議你再閱讀一遍用法篇,在閱讀的過程中同時進行手動操作。因爲在我的思想中,要學會一個知識,光看一遍知識講解,是無法掌握這個知識的,需要你手動操作,在手動操作的時候,你就會發現哪些東西還不夠清楚,然後針對不清楚的地方,再反覆鑽研,相信你很快就能掌握這個知識點。

探索scroller原理

在我研究scroller的時候,發現scroller是一個很特別的類,它不依賴其他類,它就像是一個獨立存在的類。你可以做這樣一個操作:新建一個類,然後將scroller的源碼全部複製進來,你會發現這個類都不會報錯。以這種形式存在的類,在Android源碼裏確實算很少見的了,所以它極大的增加了我研究它的興趣,再加上scroller的源碼不算太多,所以我們來研究它也不會顯得壓力太大。

不過本篇幅講的過於細節(我可能會一行一行的講解源碼),會很長,如果你決定看了,希望你還是靜下心來細細研讀,這裏的研讀,是希望在看我的文章的同時,也要仔細看我提供的scroller源碼,我會盡量讓你在一個舒適的環境閱讀源碼。

我這裏的建議就是,希望你看完本篇章之後,你也去看看scroller的全部源碼,沒錯,就是全部,如果遇到難以理解的,再回來看看這篇博客,希望能夠對你有所幫助。

那我們就開始吧!

構造方法

既然是研究這個類,那麼我們就從這個類的構造方法開始研究,進入源碼,我們會發現這個類有3個構造方法,作爲剛開始研究,自然從參數最少的構造方法開始看,這樣我們的壓力不會太大:

    public Scroller(Context context) {
        this(context, null);
    }

我們發現最簡單的構造方法調用了另一個構造方法,我們進去:

    public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

Context參數我們就不講了,直接來看看第二個參數Interpolator,這是一個差值器,一般差值器都是用來控制數據變化的趨勢的,比如我們可以讓數據勻速變化,也可以讓數據開始慢,後來快,這些都需要使用到差值器來實現,既然scroller裏面用到了這個東西,我們可以認爲scroller在做滑動行爲的時候,我們可以通過這個Interpolator來決定滑動的趨勢,先快後慢,或者勻速運動之類的。

這個構造方法裏面又調用了一個構造方法,所以我們來看看第三個構造方法:

    // 差值器
    private final Interpolator mInterpolator;
    
    // 結束
    private boolean mFinished;
    // 飛輪?
    private boolean mFlywheel;

    // 減速
    private float mDeceleration;
    // PPI是Pixels Per Inch縮寫,pixels per inch所表示的是每英寸所擁有的像素(pixel)數目。(參考百科)
    private final float mPpi;
    
    // A context-specific coefficient adjusted to physical values.
    // 根據物理值調整的特定於上下文的係數。
    private float mPhysicalCoeff;

    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

看到這麼多源碼,是不是有點想打退堂鼓了?沒關係,慢慢來嘛,我故意將這個構造方法裏面的變量也原封不動的複製了過來,就是爲了讓你能在一個良好的環境下閱讀,裏面的變量我做了翻譯,但是這種翻譯並不是一定就是正確的翻譯,只是站在一個剛剛看源碼的人的角度做的翻譯,所以即便有了翻譯,這可能也不是這個變量本身的意義。

跟我一起來一行一行的閱讀這些源碼,我們發現這些源碼都是給變量賦值,

 mFinished = true;

默認爲true,知道就行了,繼續:

       if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }

差值器賦值,如果我們沒有給scroller傳遞一個差值器,那麼scroller就會自己使用一個默認的差值器,其中這個默認的差值器ViscousFluidInterpolator其實是scroller的一個內部類,是scroller內部實現的一個差值器,我就不復制代碼了,大家知道scroller內部有一個ViscousFluidInterpolator類就行了。

mPpi = context.getResources().getDisplayMetrics().density * 160.0f;

屏幕PPI,關於dp的官方敘述爲當屏幕每英寸有160個像素時(也就是160dpi),dp與px等價的。所以這樣是通過屏幕密度轉化成了以像素爲單位的長度,也就是px。

mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());

這裏出現了變化,調用了一個方法才計算出這個值,我們先來看看ViewConfiguration.getScrollFriction()這個能得到什麼東西,這個方法得到的是滾動摩擦係數,也就是摩擦係數,不知道你們是否還記得高中學的物理力學,裏面有這麼一個東西。

一個物體在粗糙的表面上受一個拉力F,其中這個表面的摩擦係數爲u,這時,這個物體會在這個表面進行勻加速直線運動,根據牛頓第二定律,這個物體的合力就是F拉 - f摩 = ma,然後這裏面的f摩 = mgu
如果這個物理的拉力突然消失,但是物體已經擁有一個速度了,不會突然停止。因爲這個物體擁有動能,將動能轉化爲內能後,這個物體纔會停止運動,假設拉力消失後,物體的速度爲v,此時根據動能公式,該物體目前的動能爲E動 = 1/2mv^2,物體要運行多久纔會停止呢,由於當前已經沒有拉力存在了,所以目前摩擦力做功,所以1/2mv^2 = mguL,這個L就是物體失去拉力運動後的距離。

以上物理知識只是我們看到ViewConfiguration.getScrollFriction()莫名其妙想到的,跟源碼無關啊,我們繼續來看源碼:

mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());

還是這行源碼,裏面有個computeDeceleration方法,翻譯過來計算減速,然後計算減速的時候需要傳遞一個摩擦因數,我們進這個方法一探究竟:

    private float computeDeceleration(float friction) {
        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
                      * 39.37f               // inch/meter
                      * mPpi                 // pixels per inch
                      * friction;
    }

老實說,我對這個方法百思不得其解,查了很多資料不知道這個公式怎麼來的,不過最後我還是有一個想法,首先我看到了39.37f這個常量,我發現一米就不偏不倚的剛剛好等於39.37英寸,39.37英寸*ppi,也就是一米有多少個像素。這個方法還用到了重力加速度g,到底要怎麼理解呢,我強行理解成了如下:質量爲1的物體,移動1米摩擦力做的功。然後套上這個剛剛好,E摩擦力 = mguL,其中m = 1,g = 9.8,u = ViewConfiguration.getScrollFriction(),L = 1米。剛剛好!所以mDeceleration就是質量爲1的物體移動1米摩擦力做的功。
接着看源碼:

 mFlywheel = flywheel;

這個賦值就不說了,繼續:

mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning

這裏也調用了computeDeceleration方法,不過後面居然有個註釋,翻譯過來好像是google的工程師經過測試發現摩擦係數爲0.84的時候,給人的感覺是最佳的。

現在總算是把構造方法給看完了,那麼我們接下來應該看什麼呢。
既然不知道應該看什麼方法,那麼我們就來看startScroll方法吧。

startScroll

先看看源碼:

    private static final int DEFAULT_DURATION = 250;
    
    public void startScroll(int startX, int startY, int dx, int dy) {
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
    }

內部又調用了startScroll方法,我們進這個方法看看:

    // 模式
    private int mMode;
    // 結束
    private boolean mFinished;
    // 時間
    private int mDuration;
    // 開始時間
    private long mStartTime;
    // 開始X位置
    private int mStartX;
    // 開始Y位置
    private int mStartY;
    // 結束X位置
    private int mFinalX;
    // 結束Y位置
    private int mFinalY;
    
    private float mDeltaX;
    private float mDeltaY;
    
    private float mDurationReciprocal;

    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

這個方法,我在上面已經提到過了,複製一下上面的講解,免得你們已經不記得了:

startX和startY:代表我們選中的參考點的位置,
dx和dy:代表相對於參考點要移動的位置
這樣說不知道你們懵不懵,用實際的點舉例子好了,假設我們選中(10,10)作爲參考點A,現在A點的座標就是(startX,startY) = (10,10),現在我們相對於A點的偏移(dx,dy) =
(20,20),那麼實際上我們偏移了多少,如果是以A點爲參考系,我們就只偏移了(20,20),但是A點相對於原點已經偏移了(10,10),所以我們實際上相對於手機偏移了(30,30)。

舉個通俗易懂的例子就是,假設有10個椅子排成一排,分別用1到10號表示,讓你坐第2個椅子,你肯定就坐2號椅子了,因爲你默認1號椅子是第一個,如果我這樣說,你坐從2號椅子開始數的第2個椅子,你會坐哪個椅子上,那麼我們就會從第二個椅子開始數,答案自然是3號椅子,其實就是這個道理,只是參考物不一樣而已。

這裏多了一個參數duration,代表從A點移動到B點所消耗的時間。

所以你告訴了scroller要移動的點的位置,並且也告訴了scroller要移動多少距離,在個方法裏面,scroller已經定好了移動需要花費的所有時間,並且這個方法裏面,scroller已經計算好了移動的最終位置finalX和finalY。

這個方法我們就不看了,不過要注意的是,當前的滾動模式mModeSCROLL_MODE

接下來我們來看看相對比較複雜的computeScrollOffset方法,我先吧源碼放出來,你們先不着急看。

    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;
                    // 100個裏面應該選擇哪一個
                    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;
    }

注意這裏面有一個switch分支,剛剛在看startScroll方法的時候,我已經強調startScroll用的是SCROLL_MODE,所以我們簡化一下上面的代碼:

    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;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

一點一點的來看,第一個if

        if (mFinished) {
            return false;
        }

看來mFinished這個變量是作爲一個標識存在的,如果finished了,這個方法也就沒有進去的必要了。

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

這個變量,用當前時間減去開始時間,也就得到了已經過去了多少時間,也就是調用startScroll之後已經過去了多少時間了。

        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;
            }
        } else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }

if判斷,是否到了規定時間。

進入SCROLL_MODE分支,我們發現,在這裏,mCurrXmCurrY被賦值了。

注意!當調用了computeScrollOffset方法後,mCurrXmCurrY才正式被賦值。

然後我們就可以使用scrollergetCurrXgetCurrY方法,得到當前應該被移動到的位置。

最後我們使用viewscrollToscrollBy移動。

所以scroller給我們一種這樣的感覺,我們調用startScroll指定了開始位置和結束位置,並規定了從開始位置到結束位置需要花費多少時間,當我們在這段時間使用computeScrollOffset後,computeScrollOffset內部才根據已經過去了的時間計算應該走到哪個位置,然後我們通過getCurrXgetCurrY方法得到這個位置。

所以scroller也不過如此是嗎,這樣的話,我用ValueAnimator也能實現相同的功能,而且至少ValueAnimator是在時時的變化。你這樣說也沒錯,確實只用ValueAnimator也能實現相同的效果,不過scroller使用起來更簡單不是嗎。何況scroller還有複雜的fling滾動,並且我們可以說scroller是爲滾動而生的一個類,google的工程師考慮到了很多物理上的因素,讓我們在滾動的時候,看着很舒服很自然。

接下來我們要說說scrollerfling了,在說這個之前,我們需要先看一個東西,scroller類裏面有一個靜態代碼塊。

靜態代碼塊

scroller的靜態代碼塊,這意味着什麼,在還沒有使用到startScroll的時候,scroller內部已經有代碼開始運作了,我們來看看這段代碼長什麼樣子:


    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
    private static final float START_TENSION = 0.5f;
    private static final float END_TENSION = 1.0f;
    private static final float P1 = START_TENSION * INFLEXION;
    private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);

    private static final int NB_SAMPLES = 100;
    private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
    private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];

    static {
        float x_min = 0.0f;
        float y_min = 0.0f;
        for (int i = 0; i < NB_SAMPLES; i++) { // NB_SAMPLES = 100
            // 百分之幾
            final float alpha = (float) i / NB_SAMPLES; // 0~1

            float x_max = 1.0f;

            float x, tx, coef;

            while (true) {
                x = x_min + (x_max - x_min) / 2.0f;
                coef = 3.0f * x * (1.0f - x);
                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
                if (Math.abs(tx - alpha) < 1E-5) break;
                if (tx > alpha) x_max = x;
                else x_min = x;
            }
            // SPLINE_POSITION = 100 + 1
            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;

            float y_max = 1.0f;
            float y, dy;
            while (true) {
                y = y_min + (y_max - y_min) / 2.0f;
                coef = 3.0f * y * (1.0f - y);
                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
                if (Math.abs(dy - alpha) < 1E-5) break;
                if (dy > alpha) y_max = y;
                else y_min = y;
            }
            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
        }
        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
    }

這段代碼看着恐懼嗎。
老實說這段代碼將我折磨的死去活來的,因爲很難理解裏面的式子,我最開始嘗試在表面張力的角度去思考這段代碼,因爲TENSION有張力的意思,然後我發現這條路走不通,後來我覺得TENSION可能是拉力的意思,然後我重溫了高中力學,感覺還稍微靠了點邊,不過還是沒有完全吃透這堆代碼。

本來打算這這篇文章中寫下我思考和探索這堆靜態代碼塊的過程以及進度,但是由於我自己也不能確定是否正確,所以不敢誤人子弟。

也就不帶大家細細品讀這堆代碼,我就直接說結果好了。

這個代碼塊的主要目的是爲了給SPLINE_POSITIONSPLINE_TIME這兩個數組賦值,一個位置一個時間,將時間和位置分成精度爲0.00001的樣子,然後以百分比的形式投放到數組中。

我這樣說,你可能也不一定能夠特別明白,所以我做了一件這樣到事情,將這兩個數組都打印了出來,我就得到了兩列數據,然後我將這兩列數據利用Excel做成了折線圖:

這裏的時間是以百分比的形式出現的,這裏的位置是以小數的形式出現的,其實都差不多。
我們通過這張圖可以清楚的知道時間與位置的關係。
當時間走了百分之多少了,我們可以拿到對應的位置走了百分之多少。

這個靜態代碼塊我們就算過去了,接下來我們來看看fling方法

fling

源碼:

    public void fling(int startX, int startY, int velocityX, int velocityY,
                      int minX, int maxX, int minY, int maxY) {
        // Continue a scroll or fling in progress
        if (mFlywheel && !mFinished) {
            float oldVel = getCurrVelocity();

            float dx = (float) (mFinalX - mStartX);
            float dy = (float) (mFinalY - mStartY);
            float hyp = (float) Math.hypot(dx, dy); // 假設dx和dy爲直角三角形的兩條直角邊,hyp則爲斜邊長

            float ndx = dx / hyp;
            float ndy = dy / hyp;

            float oldVelocityX = ndx * oldVel;// 計算X邊的速度
            float oldVelocityY = ndy * oldVel;// 計算Y邊的速度

            // Math.signum 判斷正負數,參數爲正,返回1.0,參數爲負返回-1.0,參數爲0返回0
            if (Math.signum(velocityX) == Math.signum(oldVelocityX)
                    && Math.signum(velocityY) == Math.signum(oldVelocityY)) {
                velocityX += oldVelocityX;
                velocityY += oldVelocityY;
            }
        }

        mMode = FLING_MODE;
        mFinished = false;

        float velocity = (float) Math.hypot(velocityX, velocityY);

        mVelocity = velocity;
        mDuration = getSplineFlingDuration(velocity);
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;

        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;

        double totalDistance = getSplineFlingDistance(velocity);
        mDistance = (int) (totalDistance * Math.signum(velocity));

        mMinX = minX;
        mMaxX = maxX;
        mMinY = minY;
        mMaxY = maxY;

        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
        // Pin to mMinX <= mFinalX <= mMaxX
        mFinalX = Math.min(mFinalX, mMaxX);
        mFinalX = Math.max(mFinalX, mMinX);

        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
        // Pin to mMinY <= mFinalY <= mMaxY
        mFinalY = Math.min(mFinalY, mMaxY);
        mFinalY = Math.max(mFinalY, mMinY);
    }

看源碼之前,我先說說這個方法的各個參數是啥:

int startX:起始位置X
int startY:起始位置Y
int velocityX:X軸上的速度
int velocityY:Y軸上的速度
int minX:X軸上最小移動的距離
int maxX:X軸上最大移動的距離
int minY:Y軸上最小移動的距離
int maxY:Y軸上最大移動的距離

雖然我已經描述了每個參數是幹啥的,但是你們也不一定知道是怎麼用的。

所以我還是講一下這裏面的參數是幹啥用的,講解之前,我們要知道fling是一個怎樣的操作,可能有很多人已經知道了,不過我還是說一下,fling操作其實就是,比如這裏有一個列表,我們手指觸摸在屏幕上滑動,然後手指鬆開,這個列表依然在繼續滑動,彷彿列表有慣性似的。就像我們小時候玩紙飛機,飛機還在我們手上的時候,我們手怎麼動,飛機就怎麼動,當我們放開手,飛機不會馬上停止,而會在手放開的那個方向上繼續飛行,就是有慣性的意思。這種放開手物體還在運動的行爲,我們稱爲fling。

好了,接下來我們來講解下fling的這幾個參數,前4個參數應該不用講了吧,不過還是講一下好了。
startX和startY:代表飛機離開手指時候的位置
velocityX和velocityY:飛機離開手指的時候,飛機在X軸和Y軸上的速度,速度帶有方向,可能是正的可能是負的,這個別忘了。
minX和maxX:飛機在X軸上運行的範圍
minY和maxY:飛機在Y軸上運行的範圍

這樣解釋是不是要好理解一些。

說到fling方法,好像我上面沒有說fling應該怎麼用,就在這裏補充一下吧。

fling用法

其實fling和startScroll用法一樣,只是參數不一樣。
還記得之前講的startScroll怎麼用嗎,三步:

  1. 聲明Scroller
  2. 調用startScroll
  3. 重寫view的computeScroll方法

這裏fling也是三步:

  1. 聲明Scroller
  2. 調用fling
  3. 重寫view的computeScroll方法

不過fling方法要傳遞兩個速度值,這速度值應該怎麼計算呢,google提供了一個VelocityTracker類,專門用來計算速度,這個類怎麼用呢,就不讓你們還專門去查一下了,我這裏快速說一下。
首先聲明一下這個類

VelocityTracker velocityTracker;

然後在構造方法裏實例化這個變量(也不一定非要在構造方法裏面實例)

velocityTracker = VelocityTracker.obtain();

然後在onTouchEvent裏面添加相應事件

velocityTracker.addMovement(event);

手指擡起的時候調用這個方法:

velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);

裏面的參數我說明一下,第一個參數表示獲取多少毫秒內的速度,第二個參數表示允許的最大速度,
我這裏設置的是500毫秒,最大速度我設置的是Float的最大值,你速度想多快就多快。

所以整體大概長這樣。

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        velocityTracker.addMovement(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:

                velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);
                int xVelocity = (int) velocityTracker.getXVelocity();
                int yVelocity = (int) velocityTracker.getYVelocity();

                break;

        }

        return true;
    }

然後我們就可以得到X和Y軸上的速度了。

如果要知道VelocityTracker的詳細使用方法,還是還是去查一下相關資料吧,我這裏重點不是講這個,就粗略的說一下好了。

好了,現在我們知道怎麼得到速度了,現在我們就開始使用fling方法吧。
我是這樣使用的:

scroller.fling(sumX, sumY,  -xVelocity, -yVelocity, 
	-Integer.MAX_VALUE, Integer.MAX_VALUE, -Integer.MAX_VALUE, Integer.MAX_VALUE);

不限制他的範圍,直接給最大範圍。
整個自定義view的代碼就長這樣:

public class ScrollerView extends View {

    private final Scroller scroller;
    private float firstX;
    private float firstY;
    private Bitmap bitmap;

    private VelocityTracker velocityTracker;

    private boolean isfling = false;

    private int sumX = 0;
    private int sumY = 0;

    public ScrollerView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.shader1);
        scroller = new Scroller(context);
        velocityTracker = VelocityTracker.obtain();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        canvas.drawBitmap(bitmap, 0, 0, null);
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {

        velocityTracker.addMovement(event);

        switch (event.getAction()) {
            case MotionEvent.ACTION_UP:

                velocityTracker.computeCurrentVelocity(500, Float.MAX_VALUE);
                int xVelocity = (int) velocityTracker.getXVelocity();
                int yVelocity = (int) velocityTracker.getYVelocity();

                sumX += firstX - event.getX();
                sumY += firstY - event.getY();

                scroller.fling(sumX, sumY, -xVelocity, -yVelocity,
                        -Integer.MAX_VALUE, Integer.MAX_VALUE, -Integer.MAX_VALUE, Integer.MAX_VALUE);

                invalidate();

                break;

            case MotionEvent.ACTION_DOWN:
                firstX = event.getX();
                firstY = event.getY();
                break;

            case MotionEvent.ACTION_MOVE:
                int x = (int) event.getX();
                int y = (int) event.getY();
                scrollTo(sumX + (int) firstX - x, sumY + (int) firstY - y);
                break;
        }

        return true;
    }

    @Override
    protected void onDetachedFromWindow() {
        velocityTracker.recycle();
        super.onDetachedFromWindow();
    }

    @Override
    public void computeScroll() {
        super.computeScroll();

        if (scroller.computeScrollOffset()) {

            isfling = true;

            int currX = scroller.getCurrX();
            int currY = scroller.getCurrY();

            scrollTo(currX, currY);
            invalidate();
            
        } else {

            if (isfling) {
                sumX = getScrollX();
                sumY = getScrollY();
            }
            isfling = false;

        }

    }
}

一行註釋都沒有,不知道你們看着會不會吃力,不過既然能看到這裏來,耐心也是非比尋常,相信你們!
來看看效果圖:

大概就是這樣,手指離開屏幕後,圖片還是會滑動一段距離。用法就講到這裏吧,跟startScroll用法差不多就不贅述了。

fling源碼

我們接着再來講fling的源碼。
其實fling方法的源碼跟startScroll方法的本質是一樣的,都是各種賦值,只不過fling要稍微麻煩一點,並且將模式設置成了

mMode = FLING_MODE;

所以我們繼續看computeScrollOffset方法,不過這次就只看FLING_MODE下的方法了

    public boolean computeScrollOffset() {

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

        if (timePassed < mDuration) {
            switch (mMode) {
                case FLING_MODE:
                    // 時間過去了百分之多少
                    final float t = (float) timePassed / mDuration;
                    // 100個裏面應該選擇哪一個
                    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的思路一樣,只有在調用了computeScrollOffset方法,scroller內部纔會進行計算,計算應該滾動到哪個位置。不過這裏的計算並不是依賴差值器,而是依賴靜態代碼塊中計算出來的SPLINE_POSITIONSPLINE_TIME這兩個數組的值,還記得這兩個數組中保存這什麼東西嗎,時間和位置的關係。

也就是這個。

到現在爲止,scroller的主要部分的源碼就算被我們探索完了。

總結

scroller主要提供了兩種方式做滑動,正經的滑動startScroll和有拋擲行爲的fling,這兩種方法的使用步驟都一樣:

  1. 聲明Scroller
  2. 調用startScroll或者fling方法
  3. 重寫view的computeScroll方法

他們的原理都是一樣的,使用startScroll或者fling的時候,記錄當前狀態,位置速度什麼的,當你調用scroller的computeScrollOffset方法之後,scroller內部會根據時間差飛快的計算當前時間點應該移動到哪個位置,然後使用scroller的getCurrX和getCurrY就可以得到這個位置了。

現在聽原理倒是挺簡單的,但是scroller內部用了很多複雜的算法來計算當前應該移動到哪些位置,相信google的那些工程師在思考這些算法的時候下了不少功夫,不僅要考慮到滑動的物理效果,還要考慮到視覺上的美觀,雖然這只是一個單獨的類,不過在整個探索的過程中,真是下了不少功夫才摸個七七八八。

那麼關於Scroller的探索就到這咯!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章