圆环步数显示器的实现
目录
ViewGroup 默认不走 onDraw() 方法(源码)及解决方案
先看一下图片
先说一下绘制的基本步骤:
1、分析效果
2、在 attrs.xml 文件中 自定义属性
3、导入命名空间,在布局中使用自定义属性
4、在代码中获取自定义属性
5、onMeasure()
6、onDraw 绘制 外环、内环、文字
7、其他伴随行为
一、先分析效果组成部分
由外圆环、内圆环、以及中显示的步数,三部分组成
二、自定义view 的属性
包括外圆环的 Color、borderWidth,内圆环的 Color,还有步数字体的Size、Color
<declare-styleable name="StepView">
<attr name="stepTextSize" format="dimension"/>
<attr name="stepTxtColor" format="color"/>
<attr name="outerColor" format="color"/>
<attr name="innerColor" format="color"/>
<attr name="borderWidth" format="dimension"/>
</declare-styleable>
三、在代码中获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
mOuterColor = array.getColor(R.styleable.StepView_outerColor, mOuterColor);
mInnerColor = array.getColor(R.styleable.StepView_innerColor, mInnerColor);
mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, mBorderWidth);
mTextColor = array.getColor(R.styleable.StepView_stepTxtColor, mTextColor);
mTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, mTextSize);
// 记得先回收
array.recycle();
四、在 onMeasure() 中 测量大小
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 5、onMeasure()
// 获取大小
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// 获取测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
throw new IllegalArgumentException("宽、高请给固定值,这样圆环才不会那么丑");
}
// 设定值 取宽高的最小值,保证 View 是正方形
setMeasuredDimension(width > height ? height : width, width > height ? height : width);
}
五、在 onDraw() 中绘制图形
我们需要绘制的有三个 外圆环、内圆环、文字
先放两张图
基线 baseLine的计算:
// baseLine = 中心位 + dy
// dy = 高度的一半 - bottom
// FontMetricsInt 中 top 是 baseLine 到最上面的距离 负值
// bottom 是baseLine 到最下面的距离 正值
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
int baseLine = getHeight() / 2 + dy;
还有画圆环的角度问题:
圆弧的角度是在 canvas.drawArc() 使用的, 135 是圆弧开始的角度,270 是圆弧顺时针扫描的角度
具体代码:
canvas.drawArc(rectF, 135, 270, false, mOutPaint);
如果我们想绘制一个完整的圆弧,就可以这么写:
canvas.drawArc(rectF, 0, 360, false, mOutPaint);
具体代码如下:
StepView.class,里面有注释的:
public class StepView extends View {
private int mOuterColor = Color.BLUE;
private int mInnerColor = Color.RED;
private int mBorderWidth = 10;
private int mTextColor = Color.RED;
private int mTextSize = 10;
// 画笔
private Paint mOutPaint, mInnerPaint, mTextPaint;
// 内环总进度
private int mMaxProgress = 0;
// 内环当前进度
private int mCurrentProgress = 0;
public StepView(Context context) {
this(context, null);
}
public StepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public StepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// 1、分析效果
// 2、在 attrs.xml 文件中 自定义属性
// 3、导入命名空间,在布局中使用自定义属性
// 4、在代码中获取自定义属性
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.StepView);
mOuterColor = array.getColor(R.styleable.StepView_outerColor, mOuterColor);
mInnerColor = array.getColor(R.styleable.StepView_innerColor, mInnerColor);
mBorderWidth = (int) array.getDimension(R.styleable.StepView_borderWidth, mBorderWidth);
mTextColor = array.getColor(R.styleable.StepView_stepTxtColor, mTextColor);
mTextSize = array.getDimensionPixelSize(R.styleable.StepView_stepTextSize, mTextSize);
array.recycle();
// 5、onMeasure()
// 6、onDraw 绘制 外环、内环、文字
// 7、其他伴随行为
// 外环画笔
mOutPaint = new Paint();
mOutPaint.setAntiAlias(true);
mOutPaint.setColor(mOuterColor);
mOutPaint.setStrokeWidth(mBorderWidth);
mOutPaint.setStyle(Paint.Style.STROKE); // 圆环空心
mOutPaint.setStrokeCap(Paint.Cap.ROUND); // 进出口为圆形
// 内环画笔
mInnerPaint = new Paint();
mInnerPaint.setAntiAlias(true);
mInnerPaint.setColor(mInnerColor);
mInnerPaint.setStrokeWidth(mBorderWidth);
mInnerPaint.setStyle(Paint.Style.STROKE); // 圆环空心
mInnerPaint.setStrokeCap(Paint.Cap.ROUND); // 进出口为圆形
// 文字画笔
mTextPaint = new Paint();
mTextPaint.setTextSize(mTextSize);
mTextPaint.setColor(mTextColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 5、onMeasure()
// 获取大小
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
// 获取测量模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.AT_MOST){
throw new IllegalArgumentException("宽、高请给固定值,这样圆环才不会那么丑");
}
// 设定值 取宽高的最小值
setMeasuredDimension(width > height ? height : width, width > height ? height : width);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 6、onDraw 绘制
// 6.1 画外环
int center = getWidth() / 2;
int radius = getWidth() / 2 - mBorderWidth / 2;
RectF rectF = new RectF(center - radius, center - radius, center + radius, center + radius);
canvas.drawArc(rectF, 135, 270, false, mOutPaint);
// 6.2 画内环
if (mMaxProgress == 0) return;
// 获取百分比
float sweepAngle = (float) mCurrentProgress / mMaxProgress;
canvas.drawArc(rectF, 135, sweepAngle * 270, false, mInnerPaint);
// 6.3 画文字
String text = mCurrentProgress + "";
// 获取 x baseLine 的位置
Rect textBounds = new Rect();
mTextPaint.getTextBounds(text, 0, text.length(), textBounds);
int x = getWidth() / 2 - textBounds.width() / 2;
Paint.FontMetricsInt fontMetrics = mTextPaint.getFontMetricsInt();
int dy = (fontMetrics.bottom - fontMetrics.top) / 2 - fontMetrics.bottom;
int baseLine = getHeight() / 2 + dy;
canvas.drawText(text,x, baseLine, mTextPaint);
}
// 7、其他,让使用者设置最大值、当前进度
public synchronized void setMaxProgress(int maxProgress){
mMaxProgress = maxProgress;
}
public synchronized void setCurrentProgress(int currentProgress){
mCurrentProgress = currentProgress;
// 记得重绘 更新时,不断调用 onDraw
invalidate();
}
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="StepView">
<attr name="stepTextSize" format="dimension"/>
<attr name="stepTxtColor" format="color"/>
<attr name="outerColor" format="color"/>
<attr name="innerColor" format="color"/>
<attr name="borderWidth" format="dimension"/>
</declare-styleable>
</resources>
activity_mian.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">
<com.xy.view.StepView
android:id="@+id/custom_sv"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:outerColor="#2ae0c8"
app:innerColor="#fe6673"
app:borderWidth="20dp"
app:stepTextSize="30sp"
app:stepTxtColor="#fe6673"/>
</LinearLayout>
MainActivity.java 中:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 测试代码
final StepView stepView = findViewById(R.id.custom_sv);
// 设置当前圆弧最大值
stepView.setMaxProgress(5000);
// 属性动画
ValueAnimator animator = ObjectAnimator.ofFloat(0, 3586);
animator.setDuration(1500);
// 设置差值器 当前进度变化速度 由快变慢
animator.setInterpolator(new DecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentProgress = (float) animation.getAnimatedValue();
// 不断更新当前圆弧值
stepView.setCurrentProgress((int) currentProgress);
}
});
// 记得先写开启,不然容易忘
animator.start();
}
}
ViewGroup 默认不走 onDraw() 方法(源码)及解决方案
测试的时候,尝试继承 LinearLayout,发现没有绘制图案。LinearLayout 继承于ViewGroup 是不走 onDraw 方法的,在此记录一下原因:
LinearLayout 继承于 ViewGroup, ViewGroup 继承于 View,View 中绘制调用的是 draw(Canvas canvas),我们看一下这个方法
public void draw(Canvas canvas) {
final int privateFlags = mPrivateFlags;
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
(mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
........
// Step 3, draw the content
if (!dirtyOpaque) onDraw(canvas);
// Step 4, draw the children
dispatchDraw(canvas);
.......
}
我们可以看到 dirtyOpaque 为 false 时,才会调用 onDraw。是否调用 onDraw() 与 dirtyOpaque 有关,而 dirtyOpaque 又与 mPrivateFlags 有关。
mPrivateFlags 的赋值,是在 View 的构造函数 中。
public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
this(context);
final TypedArray a = context.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
......
if (!setScrollContainer && (viewFlagValues&SCROLLBARS_VERTICAL) != 0) {
setScrollContainer(true);
}
// 在这个方法中,为 mPrivateFlags 赋值
computeOpaqueFlags();
}
看一下 computeOpaqueFlags,计算 Flag 的值。
protected void computeOpaqueFlags() {
// Opaque if:
// - Has a background
// - Background is opaque
// - Doesn't have scrollbars or scrollbars overlay
if (mBackground != null && mBackground.getOpacity() == PixelFormat.OPAQUE) {
mPrivateFlags |= PFLAG_OPAQUE_BACKGROUND;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_BACKGROUND;
}
final int flags = mViewFlags;
if (((flags & SCROLLBARS_VERTICAL) == 0 && (flags & SCROLLBARS_HORIZONTAL) == 0) ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_INSIDE_OVERLAY ||
(flags & SCROLLBARS_STYLE_MASK) == SCROLLBARS_OUTSIDE_OVERLAY) {
mPrivateFlags |= PFLAG_OPAQUE_SCROLLBARS;
} else {
mPrivateFlags &= ~PFLAG_OPAQUE_SCROLLBARS;
}
}
现在我们再看一下 ViewGroup 的构造函数:
public ViewGroup(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
// ViewGroup 构造函数中调用了 这个方法
initViewGroup();
initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
}
private void initViewGroup() {
// 这里可以看到 initViewGroup 方法会重新赋值,默认不绘制
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
.......
}
可以看到最后因为 setFlags 的原因导致 viewGrop 中的 OnDraw 不可见,默认不绘制。
为什么设置背景时,又调用了 onDraw 方法?
因为 设置背景时,调用了 setBackgroundDrawable,而在 这个方法中,又重新为 flag 赋值,进入了之前的那个 if,调用了 onDraw 方法。
public void setBackgroundDrawable(Drawable background) {
// 又重新调用了 计算 flags 的方法
computeOpaqueFlags();
if (background == mBackground) {
return;
}
.......
}
解决方法有三:
1、将 onDraw 改为 dispatchDraw,在 View 中的 draw(Canvas canvas) 方法中可以看到 调用 dispatchDraw 方法没有 if 判断
2、在构造方法中,给自定义 View设置一个透明背景
3、在 View 源码中 搜索 setFlags ,发现在另一个方法中调用了 setFlags,我们可以在构造方法中 调用这个方法,重写为 flags 赋值。setWillNotDraw(false)
public void setWillNotDraw(boolean willNotDraw) {
setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
}