淘宝评分ratingbar及invilidate方法源码简单分析

一、概述

现在Android系统自带的有ratingbar,一般用于评分的功能,今天我们自己自定义一个ratingbar,一来熟悉自定义view的套路和绘制流程,二来可以去优化,找出不足,加深对源码的理解。我们先看下效果图:

淘宝评分ratingbar及invilidate方法源码简单分析_第1张图片
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

你可能感兴趣的:(淘宝评分ratingbar及invilidate方法源码简单分析)