淘寶評分ratingbar及invilidate方法原始碼簡單分析

weixin_34234823發表於2017-06-10

一、概述

現在Android系統自帶的有ratingbar,一般用於評分的功能,今天我們自己自定義一個ratingbar,一來熟悉自定義view的套路和繪製流程,二來可以去優化,找出不足,加深對原始碼的理解。我們先看下效果圖:

1967153-49a5e059682b3fa0.gif
ratingbar.gif

二、寫程式碼的思路

看gif動態圖,首先是5個(評分數量)灰色的五角星評分圖示(正常時的圖示狀態),代表評分等級,然後,點選後變色(選中),移動後也變色(選中),星星之間有padding,控制元件的上下也有padding。

我們看上面的這段話,就可以吧相關自定義屬性抽取出來,一個是評分等級,一個是正常狀態下的圖片資源,還有一個是選中狀態下的圖片資源,還有左右上下間距,至於選中後變色,就是測量和繪製工作了。

好了,我們看下具體的程式碼

2.1定義的屬性
    private Bitmap mSelectedStar; //選中時的圖片資源
    private Bitmap mNormalStar;   //正常時的圖片資源
    private int mGrade = 5;           //總的分數 預設為5
    private int mCurrentPosition;
    private int mStarPaddingleft = 4;
    private int mStarPaddingRight = 4;
    private int mWidth; //一個星星的繪製寬度 包含左右的padding距離

這裡解釋下mStarPaddingleft和mStarPaddingRight,一個是五角星左邊的padding值,一個是右邊的padding值,這裡我寫死了,你們也可以寫成自定義屬性,通過XML傳進來,也可以寫成getter,setter方法暴露給使用者。其他的不做解釋,看註釋。
  構造方法裡面我就不多說了,都是簡單的獲取屬性賦值的一個過程,不過這裡要注意的是要把圖片資源轉為bitmap:

 //把圖片資源轉化為bitmap
 mNormalStar = BitmapFactory.decodeResource(getResources(), normalId);
 mSelectedStar = BitmapFactory.decodeResource(getResources(), selectedId);

2.2其他的方法

2.2.1 onmeasure測繪寬高
 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int height = mNormalStar.getHeight()
                + getPaddingTop() + getPaddingBottom(); //高度等於上下間距 + 星星圖片的高度
        mWidth = mNormalStar.getWidth() + mStarPaddingleft + mStarPaddingRight;

        int width = mWidth * mGrade;
        setMeasuredDimension(width, height); //設定寬高
    }
2.2.2 onTouchEnven處理使用者手指觸控
@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
            case MotionEvent.ACTION_UP:
                float x = event.getX(); //獲得觸控控制元件手指的位置
                int curPosition = (int) ((x / mWidth) + 1);
                mCurrentPosition = curPosition;
                invalidate(); //呼叫invalidate方法 不斷的去重繪(呼叫ondraw方法)
                break;
        }
        return true;
    }

這裡,在使用者手指按下,移動和抬起的時候進行監聽,獲取使用者在控制元件內的x座標,然後計算出是哪個星星(星星的位置),然後呼叫invalidate方法,不斷重繪。

2.2.3呼叫onDraw方法繪製五角星
 @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //1.首先 把正常狀態下的五角星畫出來 通過for迴圈畫五角星
        /**
         *  @param bitmap The bitmap to be drawn
         * @param left   The position of the left side of the bitmap being drawn
         * @param top    The position of the top side of the bitmap being drawn
         * @param paint  The paint used to draw the bitmap (may be null)
         */
        for (int i = 0; i < mGrade; i++) {

            if (mCurrentPosition > i) {
                //畫選中的星星
                canvas.drawBitmap(mSelectedStar, i * mWidth + mStarPaddingleft, getPaddingTop(), null);
            } else {
                //畫未選中的星星
                canvas.drawBitmap(mNormalStar, i * mWidth + mStarPaddingleft, getPaddingTop(), null);
            }
        }
    }

for迴圈裡面,通過位置判斷,進行選中和非選中的五角星的圖片的繪製。drawBitmap方法可以自己看原始碼去理解,多試一試。

三、後續的優化

通過上述的分析,基本上完成了自定義ratingbar的程式碼書寫,功能完成後,我們還要考慮優化問題,這裡應該怎麼優化呢?
  主要是在onTouchEvent裡面操作,首先,手指抬起不需要監聽,就不需要呼叫繪製方法,然後,在同一個位置的時候我們不需要呼叫invilidate方法,invilidate方法呼叫後,會一層一層往上呼叫。這裡做下invilidate原始碼的簡單分析。

3.1Android原始碼:
  final ViewParent p = mParent;
            if (p != null && ai != null && l < r && t < b) {
                final Rect damage = ai.mTmpInvalRect;
                damage.set(l, t, r, b);
                p.invalidateChild(this, damage);
            }

呼叫invilidate方法後,如果父容器不為空,就會呼叫 p.invalidateChild(this, damage)這句程式碼,這句程式碼由父佈局實現,
在父佈局中,我們看到:

      parent = parent.invalidateChildInParent(location, dirty);
                if (view != null) {
                    // Account for transform on current parent
                    Matrix m = view.getMatrix();
                    if (!m.isIdentity()) {
                        RectF boundingRect = attachInfo.mTmpTransformRect;
                        boundingRect.set(dirty);
                        m.mapRect(boundingRect);
                        dirty.set((int) Math.floor(boundingRect.left),
                                (int) Math.floor(boundingRect.top),
                                (int) Math.ceil(boundingRect.right),
                                (int) Math.ceil(boundingRect.bottom));
                    }
                }
            } while (parent != null);

通過while迴圈,一直找到最頂層的佈局ViewRootImpl,呼叫其invalidateChildInParent方法,接著我們看ViewRootImpl的invalidateChildInParent方法做了啥,看關鍵程式碼程式碼:

    checkThread();
        if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
        if (dirty == null) {
            invalidate();
            return null;
        } else if (dirty.isEmpty() && !mIsAnimating) {
            return null;
        }
        if (mCurScrollY != 0 || mTranslator != null) {
            mTempRect.set(dirty);
            dirty = mTempRect;
            if (mCurScrollY != 0) {
                dirty.offset(0, -mCurScrollY);
            }
            if (mTranslator != null) {
                mTranslator.translateRectInAppWindowToScreen(dirty);
            }
            if (mAttachInfo.mScalingRequired) {
                dirty.inset(-1, -1);
            }
        }
        invalidateRectOnScreen(dirty);

第一句程式碼主要檢測執行緒,這就是為啥不能在主執行緒更新UI的原因,接著我們看invalidateRectOnScreen(dirty)這個方法,關鍵程式碼:

   if (!mWillDrawSoon && (intersected || mIsAnimating)) {
            scheduleTraversals();
        }

scheduleTraversals();這個方法往下執行,會呼叫TraversalRunnable子執行緒裡的doTraversal();方法, performTraversals()方法,performDraw()方法,draw()方法,一層一層呼叫,最終會呼叫 mAttachInfo.mTreeObserver.dispatchOnDraw();方法,一層一層觸發子控制元件重新繪製。
 說了這麼多,也就是想說明一個道理,就是呼叫invalidate方法後,先是呼叫invalidateChildInParent一層一層往上傳遞,然後一層一層往下呼叫draw方法重新繪製,這個過程就比較耗效能。所以我們應該要減少其呼叫。

3.2具體優化方案

去掉onTouchEvent方法中up的監聽,判斷位置,沒有變化就不往下執行,程式碼如下:

@Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
            case MotionEvent.ACTION_MOVE:
//            case MotionEvent.ACTION_UP:
                float x = event.getX(); //獲得觸控控制元件手指的位置
                Log.e("RatingBar",x+"");
                int curPosition = (int) ((x / mWidth) + 1);
                if (curPosition == mCurrentPosition) { //觸控在同一個控制元件的範圍內,不進行重新繪製
                    return true;
                }
                mCurrentPosition = curPosition;
                invalidate(); //呼叫invalidata方法 不斷的去重繪(呼叫ondraw方法)

                break;
        }
        return true;
    }

可以通過列印日誌來個前後對比,這裡我就不截圖了。

四、結語

分析完畢,程式碼地址:https://github.com/lcty1201/AndroidLearn.git

相關文章