Scroller的調(diào)用過(guò)程以及View的重繪

背景

這是一個(gè)滑動(dòng)幫助類台谍,并不可以使View真正的滑動(dòng),而是根據(jù)時(shí)間的流逝吁断,獲取插值器中的數(shù)據(jù)趁蕊,傳遞給我們,讓我們?nèi)ヅ浜蟬crollTo/scrollBy去讓view產(chǎn)生緩慢滑動(dòng)仔役,產(chǎn)生動(dòng)畫(huà)的效果掷伙,其實(shí)是和屬性動(dòng)畫(huà)同一個(gè)原理。下面是官方文檔對(duì)于這個(gè)類所給的解釋:

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的繪制過(guò)程:

  1. 調(diào)用public void startScroll(int startX, int startY, int dx, int dy)
    該方法為scroll做一些準(zhǔn)備工作.
    比如設(shè)置了移動(dòng)的起始坐標(biāo),滑動(dòng)的距離和方向以及持續(xù)時(shí)間等.
    該方法并不是真正的滑動(dòng)scroll的開(kāi)始,感覺(jué)叫prepareScroll()更貼切些.

  2. 調(diào)用invalidate()或者postInvalidate()使View(ViewGroup)樹(shù)重繪
    重繪會(huì)調(diào)用View的draw()方法
    draw()一共有六步:

  3. 繪制背景

  4. 保存畫(huà)布

  5. 調(diào)用onDraw()繪制內(nèi)容

  6. 去調(diào)用dispatchDraw()繪制子View

  7. If necessary, draw the fading edges and restore layers

  8. Draw decorations (scrollbars for instance)

其中最重要的是第三步和第四步骂因,重繪分兩種情況:
2.1 . ViewGroup的重繪
在完成第三步onDraw()以后,進(jìn)入第四步ViewGroup重寫(xiě)了
父類View的dispatchDraw()繪制子View,于是這樣繼續(xù)調(diào)用:
dispatchDraw()-->drawChild()-->child.computeScroll();

2.2 .View的重繪

當(dāng)View調(diào)用invalidate()方法時(shí),會(huì)導(dǎo)致整個(gè)View樹(shù)進(jìn)行從上至下的一次重繪.比如從最外層的Layout到里層的Layout,直到每個(gè)子View.在重繪View樹(shù)時(shí)ViewGroup和View時(shí)按理都會(huì)經(jīng)過(guò)onMeasure()和onLayout()以及onDraw()方法。
當(dāng)然系統(tǒng)會(huì)判斷這三個(gè)方法是否都必須執(zhí)行,如果沒(méi)有必要就不會(huì)調(diào)用.看到這里就明白了:當(dāng)這個(gè)子View的父容器重繪時(shí),也會(huì)調(diào)用上面提到的線路:onDraw()-->dispatchDraw()-->drawChild()-->child.computeScroll();
于是子View(比如此處舉例的ButtonSubClass類)中重寫(xiě)的computeScroll()方法就會(huì)被調(diào)用到.

3.** View樹(shù)的重繪會(huì)調(diào)用到View中的computeScroll()方法**

4.** 在computeScroll()方法中赃泡,在View的源碼中可以看到public void computeScroll(){}是一個(gè)空方法. 具體的實(shí)現(xiàn)需要自己來(lái)寫(xiě).在該方法中我們可調(diào)用scrollTo()或scrollBy()來(lái)實(shí)現(xiàn)移動(dòng).該方法才是實(shí)現(xiàn)移動(dòng)的核心.**
4.1 利用Scroller的mScroller.computeScrollOffset()判斷移動(dòng)過(guò)程是否完成
注意:該方法是Scroller中的方法而不是View中的

       public boolean computeScrollOffset(){
         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.
     }
   

返回true時(shí)表示還移動(dòng)還沒(méi)有完成.
4.2 若動(dòng)畫(huà)沒(méi)有結(jié)束,則調(diào)用:scrollTo(By)();使其滑動(dòng)scrolling

5.再次調(diào)用invalidate()
調(diào)用invalidate()方法那么又會(huì)重繪View樹(shù).
從而跳轉(zhuǎn)到第3步,如此循環(huán),直到computeScrollOffset返回false

二.onMeasure寒波、onLayout、draw 關(guān)系

onMeasure()方法
onMeasure(int widthMeasureSpec,int heightMeasureSpec)
1升熊、調(diào)用時(shí)間:當(dāng)控件的父元素放置該控件時(shí)俄烁,用于告訴父元素該控件需要的大小。
2级野、傳入?yún)?shù):widthMeasureSpec页屠,heightMeasureSpec。這兩個(gè)傳入?yún)?shù)由高32位和低16位組成,高32位保存的值叫specMode辰企,可以通過(guò)MeasureSpec.getMode()獲确缇馈;低16位為specSize可以由MeasureSpec.getSize()獲取牢贸。這兩個(gè)值是由ViewGroup中的layout_width竹观,layout_height和padding以及View自身的layout_margin共同決定。權(quán)值weight也是尤其需要考慮的因素潜索,有它的存在情況可能會(huì)稍微復(fù)雜點(diǎn)臭增。

specMode可以取三個(gè)值:MeasureSpec.EXACTLY ,MeasureSpec.AT_MOST竹习,MeasureSpec.UNSPECIFIED誊抛;specMode與layout_的對(duì)應(yīng)關(guān)系如下:

match_parent - MeasureSpec.EXACTLY:當(dāng)layout_為match_parent或者為某一具體值的時(shí)候specMode為EXACTLY代表精確的值;

wrap_content - MeasureSpec.AT_MOST:表示能獲得的最大尺寸整陌;
當(dāng)無(wú)法確定尺寸的時(shí)候則是 MeasureSpec.UNSPECIFIED拗窃,這時(shí)候specSize會(huì)為最小值(即0);

3蔓榄、可以在onMeasure()中來(lái)計(jì)算控件的尺寸并炮,然后根據(jù)setMeasuredDimension(mWidth,mHeight);方法來(lái)告訴父控件此控件需要的尺寸,onMeasure()方法中必須調(diào)用此方法甥郑。

4逃魄、值得注意的是:
1)specSize和傳入setMeasuredDimension()方法中的值的單位都是px(dp*density就是px)。2)match_parent并不是填充整個(gè)父容器澜搅,而是在不覆蓋已經(jīng)加入父容器的控件的情況下填充父容器伍俘。

onLayout()方法
onLayout(boolean changed, int left, int top,int right,int bottom);
父容器的onLayout()調(diào)用子類的onLayout()來(lái)確定子view在viewGroup中的位置,如:onLayout(10勉躺,10癌瘾,100,100)表示子容器在父容器中(10饵溅,10)位置顯示妨退,長(zhǎng)、寬都是90蜕企。結(jié)合onMeasure()方法使用可以確定子view的布局咬荷。

onDraw()方法
onDraw(Canvas canvas)
自定義view的關(guān)鍵方法,用于繪制界面轻掩,可以重寫(xiě)此方法以繪制自定義View幸乒。

onMeasure 屬于View的方法,用來(lái)測(cè)量自己和內(nèi)容的來(lái)確定寬度和高度 唇牧,view的measure方法體中會(huì)調(diào)用onMeasure罕扎。
onLayout屬于ViewGroup的方法聚唐,用來(lái)為當(dāng)前ViewGroup的子元素分配位置和大小 View的layout方法體中會(huì)調(diào)用onLayout。
onMeasure和onLayout腔召, onMeasure在onLayout之前調(diào)用杆查。
設(shè)置background后,會(huì)重新調(diào)用onMeasure和onLayout宴咧,onMeasure測(cè)量子VIEW大小后調(diào)用LAYOUT布局 所以初始化的時(shí)候會(huì)多次調(diào)用onlayout方法

實(shí)例:

ublic class MultiViewGroup extends ViewGroup {  
  
     private VelocityTracker mVelocityTracker; // 用于判斷甩動(dòng)手勢(shì)  
    private static final int SNAP_VELOCITY = 600; // X軸速度基值根灯,大于該值時(shí)進(jìn)行切換  
    private Scroller mScroller;// 滑動(dòng)控制  
    private int mCurScreen; // 當(dāng)前頁(yè)面為第幾屏  
    private int mDefaultScreen = 0;  
    private float mLastMotionX;// 記住上次觸摸屏的位置  
    private int deltaX;  
  
    private OnViewChangeListener mOnViewChangeListener;  
  
    public MultiViewGroup(Context context) {  
        this(context, null);  
    }  
  
    public MultiViewGroup(Context context, AttributeSet attrs) {  
        super(context, attrs);  
        init(getContext());  
    }  
  
    private void init(Context context) {  
        mScroller = new Scroller(context);  
        mCurScreen = mDefaultScreen;  
    }  
  
    @Override  
    public void computeScroll() {  
        if (mScroller.computeScrollOffset()) {// 會(huì)更新Scroller中的當(dāng)前x,y位置  
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
            postInvalidate();  
        }  
    }  
  
    @Override  
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);  
        int width = MeasureSpec.getSize(widthMeasureSpec);  
        int count = getChildCount();  
        for (int i = 0; i < count; i++) {  
            measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);  
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);  
        }  
        scrollTo(mCurScreen * width, 0);// 移動(dòng)到第一頁(yè)位置  
    }  
  
    @Override  
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        int margeLeft = 0;  
        int size = getChildCount();  
        for (int i = 0; i < size; i++) {  
            View view = getChildAt(i);  
            if (view.getVisibility() != View.GONE) {  
                int childWidth = view.getMeasuredWidth();  
                // 將內(nèi)部子孩子橫排排列  
                view.layout(margeLeft, 0, margeLeft + childWidth,  
                        view.getMeasuredHeight());  
                margeLeft += childWidth;  
            }  
        }  
    }  
  
    @Override  
    public boolean onTouchEvent(MotionEvent event) {  
        int action = event.getAction();  
        float x = event.getX();  
        switch (action) {  
        case MotionEvent.ACTION_DOWN:  
            obtainVelocityTracker(event);  
            if (!mScroller.isFinished()) {  
                mScroller.abortAnimation();  
            }  
            mLastMotionX = x;  
            break;  
        case MotionEvent.ACTION_MOVE:  
            deltaX = (int) (mLastMotionX - x);  
            if (canMoveDis(deltaX)) {  
                obtainVelocityTracker(event);  
                mLastMotionX = x;  
                // 正向或者負(fù)向移動(dòng),屏幕跟隨手指移動(dòng)  
                scrollBy(deltaX, 0);  
            }  
            break;  
        case MotionEvent.ACTION_UP:  
        case MotionEvent.ACTION_CANCEL:  
            // 當(dāng)手指離開(kāi)屏幕時(shí)掺栅,記錄下mVelocityTracker的記錄烙肺,并取得X軸滑動(dòng)速度  
            obtainVelocityTracker(event);  
            mVelocityTracker.computeCurrentVelocity(1000);  
            float velocityX = mVelocityTracker.getXVelocity();  
            // 當(dāng)X軸滑動(dòng)速度大于SNAP_VELOCITY  
            // velocityX為正值說(shuō)明手指向右滑動(dòng),為負(fù)值說(shuō)明手指向左滑動(dòng)  
            if (velocityX > SNAP_VELOCITY && mCurScreen > 0) {  
                // Fling enough to move left  
                snapToScreen(mCurScreen - 1);  
            } else if (velocityX < -SNAP_VELOCITY  
                    && mCurScreen < getChildCount() - 1) {  
                // Fling enough to move right  
                snapToScreen(mCurScreen + 1);  
            } else {  
                snapToDestination();  
            }  
            releaseVelocityTracker();  
            break;  
        }  
        // super.onTouchEvent(event);  
        return true;// 這里一定要返回true,不然只接受down  
    }  
  
    /** 
     * 邊界檢測(cè) 
     *  
     * @param deltaX 
     * @return 
     */  
    private boolean canMoveDis(int deltaX) {  
        int scrollX = getScrollX();  
        // deltaX<0說(shuō)明手指向右劃  
        if (deltaX < 0) {  
            if (scrollX <= 0) {  
                return false;  
            } else if (deltaX + scrollX < 0) {  
                scrollTo(0, 0);  
                return false;  
            }  
        }  
        // deltaX>0說(shuō)明手指向左劃  
        int leftX = (getChildCount() - 1) * getWidth();  
        if (deltaX > 0) {  
            if (scrollX >= leftX) {  
                return false;  
            } else if (scrollX + deltaX > leftX) {  
                scrollTo(leftX, 0);  
                return false;  
            }  
        }  
        return true;  
    }  
  
    /** 
     * 使屏幕移動(dòng)到第whichScreen+1屏 
     *  
     * @param whichScreen 
     */  
    public void snapToScreen(int whichScreen) {  
        int scrollX = getScrollX();  
        if (scrollX != (whichScreen * getWidth())) {  
            int delta = whichScreen * getWidth() - scrollX;  
            mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta) * 2);  
            mCurScreen = whichScreen;  
            invalidate();  
            if (mOnViewChangeListener != null) {  
                mOnViewChangeListener.OnViewChange(mCurScreen);  
            }  
        }  
    }  
  
    /** 
     * 當(dāng)不需要滑動(dòng)時(shí)氧卧,會(huì)調(diào)用該方法 
     */  
    private void snapToDestination() {  
        int screenWidth = getWidth();  
        int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;  
        snapToScreen(whichScreen);  
    }  
  
    private void obtainVelocityTracker(MotionEvent event) {  
        if (mVelocityTracker == null) {  
            mVelocityTracker = VelocityTracker.obtain();  
        }  
        mVelocityTracker.addMovement(event);  
    }  
  
    private void releaseVelocityTracker() {  
        if (mVelocityTracker != null) {  
            mVelocityTracker.recycle();  
            mVelocityTracker = null;  
        }  
    }  
  
    public void SetOnViewChangeListener(OnViewChangeListener listener) {  
        mOnViewChangeListener = listener;  
    }  
  
    public interface OnViewChangeListener {  
        public void OnViewChange(int page);  
    }  
}  
總結(jié):

Scroller執(zhí)行流程里面的三個(gè)核心方法

  mScroller.startScroll()
  mScroller.computeScrollOffset()
  view.computeScroll()
  1. mScroller.startScroll()中為滑動(dòng)做了一些初始化準(zhǔn)備.
    比如:起始坐標(biāo),滑動(dòng)的距離和方向以及持續(xù)時(shí)間(有默認(rèn)值)等.
    其實(shí)除了這些,在該方法內(nèi)還做了些其他事情:
    比較重要的一點(diǎn)是設(shè)置了動(dòng)畫(huà)開(kāi)始時(shí)間.

  2. computeScrollOffset()方法主要是根據(jù)當(dāng)前已經(jīng)消逝的時(shí)間
    來(lái)計(jì)算當(dāng)前的坐標(biāo)點(diǎn)并且保存在mCurrX和mCurrY值中桃笙。
    因?yàn)樵趍Scroller.startScroll()中設(shè)置了動(dòng)畫(huà)時(shí)間,那么在computeScrollOffset()方法中依據(jù)已經(jīng)消逝的時(shí)間就很容易得到當(dāng)前時(shí)刻應(yīng)該所處的位置并將其保存在變量mCurrX和mCurrY中。除此之外該方法還可判斷動(dòng)畫(huà)是否已經(jīng)結(jié)束沙绝。

@Override
    public void computeScroll() {
       super.computeScroll();
       if (mScroller.computeScrollOffset()) {
           scrollTo(mScroller.getCurrX(), 0);
           invalidate();
       }
    }

先執(zhí)行mScroller.computeScrollOffset()判斷了滑動(dòng)是否結(jié)束
2.1 返回false,滑動(dòng)已經(jīng)結(jié)束.
2.2 返回true,滑動(dòng)還沒(méi)有結(jié)束.
并且在該方法內(nèi)部也計(jì)算了最新的坐標(biāo)值mCurrX和mCurrY.
就是說(shuō)在當(dāng)前時(shí)刻應(yīng)該滑動(dòng)到哪里了.
既然computeScrollOffset()如此貼心,盛情難卻啊!
于是我們就覆寫(xiě)View的computeScroll()方法,
調(diào)用scrollTo(By)滑動(dòng)到那里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末搏明,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子闪檬,更是在濱河造成了極大的恐慌星著,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,826評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粗悯,死亡現(xiàn)場(chǎng)離奇詭異虚循,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)样傍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門横缔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人衫哥,你說(shuō)我怎么就攤上這事茎刚。” “怎么了撤逢?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,234評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵膛锭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我蚊荣,道長(zhǎng)初狰,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,562評(píng)論 1 293
  • 正文 為了忘掉前任妇押,我火速辦了婚禮跷究,結(jié)果婚禮上姓迅,老公的妹妹穿的比我還像新娘敲霍。我一直安慰自己俊马,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,611評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布肩杈。 她就那樣靜靜地躺著柴我,像睡著了一般。 火紅的嫁衣襯著肌膚如雪扩然。 梳的紋絲不亂的頭發(fā)上艘儒,一...
    開(kāi)封第一講書(shū)人閱讀 51,482評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音夫偶,去河邊找鬼界睁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛兵拢,可吹牛的內(nèi)容都是我干的翻斟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,271評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼说铃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼访惜!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起腻扇,我...
    開(kāi)封第一講書(shū)人閱讀 39,166評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤债热,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后幼苛,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體窒篱,經(jīng)...
    沈念sama閱讀 45,608評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,814評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蚓峦,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了舌剂。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,926評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瞪慧,到底是詐尸還是另有隱情易稠,我是刑警寧澤,帶...
    沈念sama閱讀 35,644評(píng)論 5 346
  • 正文 年R本政府宣布给梅,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏岩喷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,249評(píng)論 3 329
  • 文/蒙蒙 一监憎、第九天 我趴在偏房一處隱蔽的房頂上張望纱意。 院中可真熱鬧,春花似錦鲸阔、人聲如沸偷霉。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,866評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)类少。三九已至叙身,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間硫狞,已是汗流浹背信轿。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,991評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留残吩,地道東北人财忽。 一個(gè)月前我還...
    沈念sama閱讀 48,063評(píng)論 3 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像泣侮,于是被迫代替她去往敵國(guó)和親定罢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,871評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容