【Android View事件(三)】Scroll類源碼分析與應(yīng)用


【本文出自大圣代的技術(shù)專欄 http://blog.csdn.net/qq_23191031
【禁止任何商業(yè)活動。轉(zhuǎn)載煩請注明出處】

學(xué)前準(zhǔn)備

詳解Android控件體系與常用坐標(biāo)系
Android常用觸控類分析:MotionEvent 、 ViewConfiguration、VelocityTracker

前言

在前面的幾篇文章,我向大家介紹的都是單一View事件,從這篇文章開始,我將向大家介紹連續(xù)的事件 —— 滑動?;瑒邮且苿佣嗽O(shè)備提供的重要功能,正是由于強(qiáng)大的滑動事件讓我們小巧的屏幕可以展現(xiàn)無限的數(shù)據(jù)。而滑動事件沖突卻常常困擾著廣大開發(fā)者。孫子云:知己知彼,百戰(zhàn)不殆。想更好的協(xié)調(diào)滑動事件,不知道其中原理的確困難重重。當(dāng)你學(xué)習(xí)本篇文章之后你會發(fā)現(xiàn)其實(shí)Scroll很簡單,你只是被各種文章與圖書弄糊涂了。

在真正講解之前,我們需要掌握Android坐標(biāo)系與觸控事件相關(guān)知識,對此不太明確的同學(xué)請參見上文的 學(xué)前準(zhǔn)備

View滑動產(chǎn)生的原理

從原理上講View滑動的本質(zhì)就是隨著手指的運(yùn)動不斷地改變坐標(biāo)。當(dāng)觸摸事件傳到View時,系統(tǒng)記下觸摸點(diǎn)的坐標(biāo),手指移動時系統(tǒng)記下移動后的觸摸的坐標(biāo)并算出偏移量,并通過偏移量來修改View的坐標(biāo),不斷的重復(fù)這樣的過程,從而實(shí)現(xiàn)滑動過程。

1 scrollTo 與 scrollBy

說到Scroll就不得不提到scrollTo()與scrollBy()這兩個方法。

1.1 scrollTo

首先我們要知道Android每一個控件都有滾動條,只不過系統(tǒng)對我們隱藏了,所以我們看不見。
對于控件來說它的大小是有限的,(例如我們指定了大小、屏幕尺寸的束縛等),系統(tǒng)在繪制圖像的時候只會在這個有限的控件內(nèi)繪制,但是內(nèi)容(content)的載體Canvas在本質(zhì)上是無限的,例如我們的開篇圖片,控件仿佛就是一個窗口我們只能通過它看到這塊畫布。

    /**
     * Set the scrolled position of your view. This will cause a call to
     * {@link #onScrollChanged(int, int, int, int)} and the view will be
     * invalidated.
     * @param x the x position to scroll to
     * @param y the y position to scroll to
     */
    public void scrollTo(int x, int y) {//滾動到目標(biāo)位置
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX; // 已經(jīng)滾動到的X
            int oldY = mScrollY; //已經(jīng)滾動到的Y
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);//回調(diào)方法,通知狀態(tài)改變
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation(); //重新繪制
            }
        }
    }

通過注釋Set the scrolled position of your view我們可以清楚的得知 scrollTo(x,y)的作用就是將View滾動到(x,y)這個點(diǎn),注意是滾動(scroll本意滾動,滑動是translate)。

在初始時 mScrollX 與mScrollY均為0,表示著View中展示的是從畫布左上角開始的內(nèi)容(如圖 1),當(dāng)調(diào)用scrollTo(100,100)時相當(dāng)于將View的坐標(biāo)原點(diǎn)滾動到(100,100)這個位置,展示畫布上從(100,100)開始的內(nèi)容(如圖2),但是事實(shí)上View是靜止不動的,所以最終的效果是View的內(nèi)容平移了(-100,-100)的偏移量(如圖3)

image.png

1.2 scrollBy

/** 
    * Move the scrolled position of your view. This will cause a call to 
    * {@link #onScrollChanged(int, int, int, int)} and the view will be 
    * invalidated. 
    * @param x the amount of pixels to scroll by horizontally 
    * @param y the amount of pixels to scroll by vertically 
    */  
   public void scrollBy(int x, int y) {  
       scrollTo(mScrollX + x, mScrollY + y);  
   }  

學(xué)習(xí)scrollTo在學(xué)習(xí)scrollBy就簡單了,通過源碼可以看到它里面調(diào)用了ScrollTo(),傳入的參數(shù)是mScrollX+x,也就是說這次x是一個增量,所以scrollBy實(shí)現(xiàn)的效果就是,在當(dāng)前位置上,再偏移x距離
這是ScrollTo()和ScrollBy()的重要區(qū)別。

1.3 小結(jié):

  1. scrollTo與scrollBy都會另View立即重繪,所以移動是瞬間發(fā)生的
  2. scrollTo(x,y):指哪打哪,效果為View的左上角滾動到(x,y)位置,但由于View相對與父View是靜止的所以最終轉(zhuǎn)換為相對的View的內(nèi)容滑動到(-x,-y)的位置。
  3. scrollBy(x,y): 此時的x,y為偏移量,既在原有的基礎(chǔ)上再次滾動
  4. scrollTo與scrollBy的最用效果會作用到View的內(nèi)容,所以要是想滑動當(dāng)前View,就需要對其父View調(diào)用二者。也可以在當(dāng)前View中使用((View)getParent).scrollXX(x,y)達(dá)到同樣目的。

2 Scroller

OK,通過上面的學(xué)習(xí)我們知道scrollTo與scrollBy可以實(shí)現(xiàn)滑動的效果,但是滑動的效果都是瞬間完成的,在事件執(zhí)行的時候平移就已經(jīng)完成了,這樣的效果會讓人感覺突兀,Google建議使用自然過渡的動畫來實(shí)現(xiàn)移動效果。因此,Scroller類這樣應(yīng)運(yùn)而生了。

2.1 簡單實(shí)例

舉一個簡單的實(shí)例方便大家的理解與學(xué)習(xí) Scroller

主要代碼

public class CustomScrollerView extends LinearLayout {
    private Scroller mScroller;

    private View mLeftView;
    private View mRightView;

    private float mInitX, mInitY;
    private float mOffsetX, mOffsetY;

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

    public CustomScrollerView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CustomScrollerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init() {
        this.setOrientation(LinearLayout.HORIZONTAL);

        mScroller = new Scroller(getContext(), null, true);
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();

        if (getChildCount() != 2) {
            throw new RuntimeException("Only need two child view! Please check you xml file!");
        }

        mLeftView = getChildAt(0);
        mRightView = getChildAt(1);
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        switch (ev.getActionMasked()) {
            case MotionEvent.ACTION_DOWN:
                mInitX = ev.getX();
                mInitY = ev.getY();
                super.dispatchTouchEvent(ev);
                return true;
            case MotionEvent.ACTION_MOVE:
                //>0為手勢向右下
                mOffsetX = ev.getX() - mInitX;
                mOffsetY = ev.getY() - mInitY;
                //橫向手勢跟隨移動
                if (Math.abs(mOffsetX) - Math.abs(mOffsetY) > ViewConfiguration.getTouchSlop()) {
                    int offset = (int) -mOffsetX;
                    if (getScrollX() + offset > mRightView.getWidth() || getScrollX() + offset < 0) {
                        return true;
                    }
                    this.scrollBy(offset, 0);
                    mInitX = ev.getX();
                    mInitY = ev.getY();
                    return true;
                }
                break;
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP:
                //松手時刻滑動
                int offset = ((getScrollX() / (float) mRightView.getWidth()) > 0.5) ? mRightView.getWidth() : 0;
//                this.scrollTo(offset, 0);
                mScroller.startScroll(this.getScrollX(), this.getScrollY(), offset - this.getScrollX(), 0);
                invalidate();
                mInitX = 0;
                mInitY = 0;
                mOffsetX = 0;
                mOffsetY = 0;
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡化我們在非UI線程更新view的步驟
        }
    }
}

主要布局

    <com.im_dsd.blogdemo.CustomScrollerView
        android:layout_width="200sp"
        android:layout_height="200sp"
        android:layout_centerInParent="true"
        android:orientation="horizontal"
        >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@android:color/holo_blue_light"/>

        <TextView
            android:layout_width="100sp"
            android:layout_height="match_parent"
            android:background="@android:color/holo_green_light"/>
    </com.im_dsd.blogdemo.CustomScrollerView>
項(xiàng)目效果

通過上面實(shí)例我們可以發(fā)現(xiàn)在自定義View的過程中使用Scroller的流程如下圖所示:

下面我們就按照這個流程進(jìn)行源碼分析吧

2.2 源碼分析

對于Scroller類 Google給出的如下解釋:

This class encapsulates scrolling. You can use scrollers ( Scroller or OverScroller) to collect the data you need to produce a scrolling animation
for example, in response to a fling gesture. Scrollers track scroll offsets for you over time, but they don't automatically apply those positions to your view. It's your responsibility to get and apply new coordinates at a rate that will make the scrolling animation look smooth.

我們中可以看出:Scroller 是一個工具類,它只是產(chǎn)生一些坐標(biāo)數(shù)據(jù),而真正讓View平滑的滾動起來還需要我們自行處理。我們使用的處理工具就是—— scrollTo與scrollBy

2.2.1 構(gòu)造方法分析

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

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

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
}

Scroller的構(gòu)造方法沒啥特殊的地方只不過第二個參數(shù)interpolator是插值器,不同的插值器實(shí)現(xiàn)不同的動畫算法(這里不是重點(diǎn)不做展開,以后重點(diǎn)講解),如果我們不傳,則默認(rèn)使用ViscousFluidInterpolator()插值器。

2.2.2 startScroll與fling

/** 
     * 使用默認(rèn)滑動時間完成滑動 
     */  
    public void startScroll(int startX, int startY, int dx, int dy) {  
        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);  
    }  
  
    /** 
     * 在我們想要滾動的地方調(diào)運(yùn),準(zhǔn)備開始滾動,手動設(shè)置滾動時間
     *  
     * @param startX  滑動起始X坐標(biāo) 
     * @param startY     滑動起始Y坐標(biāo) 
     * @param dx    X方向滑動距離 
     * @param dy   Y方向滑動距離 
     * @param duration  完成滑動所需的時間      
     */  
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {  
        mMode = SCROLL_MODE;  
        mFinished = false;  
        mDuration = duration;  
        mStartTime = AnimationUtils.currentAnimationTimeMillis();//獲取當(dāng)前時間作為滑動的起始時間  
        mStartX = startX;  
        mStartY = startY;  
        mFinalX = startX + dx;  
        mFinalY = startY + dy;  
        mDeltaX = dx;  
        mDeltaY = dy;  
        mDurationReciprocal = 1.0f / (float) mDuration;  
    }  

    /** 
     * 開始基于滑動手勢的滑動。根據(jù)初始的滑動手勢速度,決定滑動的距離(滑動的距離,不能大于設(shè)定的最大值,不能小于設(shè)定的最小值)  
     */
public void fling(int startX, int startY, int velocityX, int velocityY,
    int minX, int maxX, int minY, int maxY) {
    ......
    mMode = FLING_MODE;
    mFinished = false;
    ......
    mStartX = startX;
    mStartY = startY;
    ......
    mDistance = (int) (totalDistance * Math.signum(velocity));

    mMinX = minX;
    mMaxX = maxX;
    mMinY = minY;
    mMaxY = maxY;
    ......
    mFinalY = Math.min(mFinalY, mMaxY);
    mFinalY = Math.max(mFinalY, mMinY);
}

在這兩個方法中,都是一些全局變量的賦值,果真沒有實(shí)現(xiàn)滾動的方法,也佐證了Scroller是一個工具的解讀。而要實(shí)現(xiàn)滑動還是要依靠我們手動調(diào)用View的invalidated()方法觸發(fā)computeScroll()方法。

   @Override
    public void computeScroll() {
        if (mScroller.computeScrollOffset()) {
            this.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
             postInvalidate(); //允許在非主線程中出發(fā)重繪,它的出現(xiàn)就是簡化我們在非UI線程更新view的步驟
        }
    }

一旦觸發(fā)成功就會調(diào)用Scroller.computeScrollOffset()方法,返回結(jié)果如果為true表示當(dāng)前的滑動尚未結(jié)束,如果返回false表示滑動完成。
在Scroller類中,最最重要的就是這個computeScrollOffset方法,看上去只是返回了一個boolean類型,但他卻是Scroller的核心,所有的坐標(biāo)與滑動時間都由它計算完成。他將原本瞬間的滑動拆分成連續(xù)平滑的過程。

/** 
     * Call this when you want to know the new location.  If it returns true, 
     * the animation is not yet finished.  loc will be altered to provide the 
     * new location. 
     * 調(diào)用這個函數(shù)獲得新的位置坐標(biāo)(滑動過程中)。如果它返回true,說明滑動沒有結(jié)束。 
     * getCurX(),getCurY()方法就可以獲得計算后的值。 
     */   
    public boolean computeScrollOffset() {  
        if (mFinished) {//是否結(jié)束  
            return false;  
        }  
  
        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);//滑動開始,經(jīng)過了多長時間  
      
        if (timePassed < mDuration) {//如果經(jīng)過的時間小于動畫完成所需時間  
            switch (mMode) {  
            case SCROLL_MODE:  
                float x = timePassed * mDurationReciprocal;  
      
                if (mInterpolator == null)//如果沒有設(shè)置插值器,利用默認(rèn)算法  
                    x = viscousFluid(x);   
                else//否則利用插值器定義的算法  
                    x = mInterpolator.getInterpolation(x);  
      
                mCurrX = mStartX + Math.round(x * mDeltaX);//計算當(dāng)前X坐標(biāo)  
                mCurrY = mStartY + Math.round(x * mDeltaY);//計算當(dāng)前Y坐標(biāo)  
                break;  
            case FLING_MODE:  
                final float t = (float) timePassed / mDuration;  
                final int index = (int) (NB_SAMPLES * t);  
                final float t_inf = (float) index / NB_SAMPLES;  
                final float t_sup = (float) (index + 1) / NB_SAMPLES;  
                final float d_inf = SPLINE[index];  
                final float d_sup = SPLINE[index + 1];  
                final float distanceCoef = d_inf + (t - t_inf) / (t_sup - t_inf) * (d_sup - d_inf);  
                  
                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;  
    }  

從代碼可以看到,如果我們沒有設(shè)置插值器,就會調(diào)用內(nèi)部默認(rèn)算法。

/** 
     * 函數(shù)翻譯是粘性流體 
     * 估計是一種算法 
     */  
    static float viscousFluid(float x)  
    {  
        x *= sViscousFluidScale;  
        if (x < 1.0f) {  
            x -= (1.0f - (float)Math.exp(-x));  
        } else {  
            float start = 0.36787944117f;   // 1/e == exp(-1)  
            x = 1.0f - (float)Math.exp(1.0f - x);  
            x = start + x * (1.0f - start);  
        }  
        x *= sViscousFluidNormalize;  
        return x;  
    }  

接著是兩個重要的get方法

/** 
     * Returns the current X offset in the scroll.  
     *  
     * @return The new X offset as an absolute distance from the origin. 
     * 獲得當(dāng)前X方向偏移 
     */  
    public final int getCurrX() {  
        return mCurrX;  
    }  
      
    /** 
     * Returns the current Y offset in the scroll.  
     *  
     * @return The new Y offset as an absolute distance from the origin. 
     * 獲得當(dāng)前Y方向偏移 
     */  
    public final int getCurrY() {  
        return mCurrY;  
    }  

2.2.3 其他方法

public class Scroller  {
    ......
    public Scroller(Context context) {}
    public Scroller(Context context, Interpolator interpolator) {}
    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {}
    //設(shè)置滾動持續(xù)時間
    public final void setFriction(float friction) {}
    //返回滾動是否結(jié)束
    public final boolean isFinished() {}
    //強(qiáng)制終止?jié)L動
    public final void forceFinished(boolean finished) {}
        //返回滾動持續(xù)時間
    public final int getDuration() {}
    //返回當(dāng)前滾動的偏移量
    public final int getCurrX() {}
    public final int getCurrY() {}
    //返回當(dāng)前的速度
    public float getCurrVelocity() {}
    //返回滾動起始點(diǎn)偏移量
    public final int getStartX() {}
    public final int getStartY() {}
        //返回滾動結(jié)束偏移量
    public final int getFinalX() {}
    public final int getFinalY() {}
    //實(shí)時調(diào)用該方法獲取坐標(biāo)及判斷滑動是否結(jié)束,返回true動畫沒結(jié)束
    public boolean computeScrollOffset() {}
    //滑動到指定位置
    public void startScroll(int startX, int startY, int dx, int dy) {}
    public void startScroll(int startX, int startY, int dx, int dy, int duration) {}
    //快速滑動松開手勢慣性滑動
    public void fling(int startX, int startY, int velocityX, int velocityY,
            int minX, int maxX, int minY, int maxY) {}
    //終止動畫,滾到最終的x、y位置
    public void abortAnimation() {}
    //延長滾動的時間
    public void extendDuration(int extend) {}
    //返回滾動開始經(jīng)過的時間
    public int timePassed() {}
    //設(shè)置終止時偏移量
    public void setFinalX(int newX) {}
    public void setFinalY(int newY) {}
}

3 總結(jié):

  1. 滑動的本質(zhì)就是View隨著手指的運(yùn)動不斷地改變坐標(biāo)
  2. scrollTo(x,y)指的就是View滾動到(x,y)這個位置,但是View 要相當(dāng)于父控件靜止不懂,所以相對的View的內(nèi)容就會滑動到(-x, -y)的位置
  3. scrollTo、scrollBy移動是瞬間的
  4. 滑動效果作用的對象是View內(nèi)容
  5. Scroller類其實(shí)是一個工具類,生產(chǎn)滑動過程的平滑坐標(biāo),但最終的滑動動作還是需要我們自行處理
  6. Scroller類的使用流程:

參考

《Android群英傳》
http://blog.csdn.net/crazy__chen/article/details/45896961
http://blog.csdn.net/yanbober/article/details/49904715

版權(quán)聲明:
禁止一切商業(yè)行為,轉(zhuǎn)載請著名出處 http://blog.csdn.net/qq_23191031。作者: 大圣代
Copyright (c) 2017 代圣達(dá). All rights reserved.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容