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