有点粗糙,颜色选择上有点儿不太美观辽,不过自己第一次写自定义view,纪念一下。
首先我们需要有一个外圆弧用来做外层框架,一个内圆弧用来看占比情况,一个具体的中心文本记录准确的步数。我们把这三者整合在一个View里面,类似于平常安卓写的ImageView这种一样,可以直接拿来用,封装成一个整体结构。
首先我们分析下需要哪些自定义属性:
在定义完attrs.xml之后,既然我们把它创造出来了,我们就需要使用它。
这时候我们就来到了activity_main.xml中使用它了,和我们平常使用Button等差不多,只不过这里是写的我们自己定义的view的名字,接着就是给它属性了,与普通的不同,当我们使用Button时,我们可能使用的是Android:textsize=“20sp”,而在这我们得利用我们自己的,不然你用的还是人家的呢。
这里的名字得改成你自己定义的名字哦!
接下来就是重头戏了,刚刚你所做的不过是把水壶拿过来,而水壶需要注水呀插电啊什么的,这些实际功能并没有去做。所以你得给这个View注入它应该做的事。首先就要激活我们刚刚定义的一系列属性,刚刚还只是一堆纸片人呢,还得你给他涂色,变魔术呢!
TypedArray array=context.obtainStyledAttributes(attrs,R.styleable.QQStepView);
mOuterColor=array.getColor(R.styleable.QQStepView_outerColor,mOuterColor);
mInnerColor=array.getColor(R.styleable.QQStepView_innerColor,mInnerColor);
mBoderWidth=(int)array.getDimension(R.styleable.QQStepView_borderWidth,mBoderWidth);
mStepTextColor=array.getColor(R.styleable.QQStepView_stepTextColor,mStepTextColor);
mStepTextSize=array.getDimensionPixelSize(R.styleable.QQStepView_stepTextSize,mStepTextSize);
array.recycle();
这里的意思是我们要得到这一整个view 的框架大小,特别要注意的是为了美观,我们把它设置为正方形,在宽高不一致的情况下取最小值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//由于宽高是开发者设置的,当设置的长宽不一致的时候,为了美观要保证是个正方形
//宽高不一致取最小值,确保是个正方形
int width=MeasureSpec.getSize(widthMeasureSpec);
int height=MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width>height?height:width,width>height?height:width);
}
解释下这段代码的意思:首先第一句代码是通过宽的一半求的这个控件的中心位置应该不难理解,第二点可能挺让人懵的,为啥半径要用宽的一半减圆弧宽的一半,难道不应该是宽的一半减圆弧的宽度吗?这就要提到画笔的属性了,mOutPaint.setStrokeWidth(mBoderWidth); //原因就在于setStrokeWidth这个方法,并不是往圆内侧增加圆环宽度的,而是往外侧增加一半,往内侧增加一半,详情参考这篇博客https://blog.csdn.net/u014704469/article/details/41277751
也就是说我们的画笔是从管道的中心点开始画的,而不是我们以为的从内圆弧画到外圆弧,所以我们要求的这个半径也不能是真正意义上圆的半径,而应该是提笔开始画的位置,也就是管道的中心点,所以就是减圆弧宽的一半。
接下来分析下为什么是135,270,135是因为我们建立以中心点的坐标轴,x轴在右边,则要开始画的点与x轴形成135°。270则代表着这个圆弧是270°。
//6.1画外圆弧
int center=getWidth()/2;//得到中心点位置
int radius=getWidth()/2-mBoderWidth/2;//得到半径
RectF rectF=new RectF(center-radius,center-radius,center+radius,center+radius);
//开始画,需要指定从多少度开始画,画多少度
canvas.drawArc(rectF,135,270,false,mOutPaint);
从图片可以看出,内圆弧与外圆弧其实是差不多的,如果你走到了最大步数就是完全覆盖外圆弧了,所以我们只要改变内圆弧要画多少度这个问题就可以了,内圆弧画多少度可以理解为占比问题,内圆弧在外圆弧上占多少的百分比,也就是当前步数比上最大步数,所以只要用这个百分比*整个圆弧的范围,就是你要画的圆弧范围。
//6.2画内圆弧 百分比是该view的使用者从外面传的
float sweepAngle=(float)mCurrentStep/mStepMax;
canvas.drawArc(rectF,135,sweepAngle*270,false,mInnerPaint);
接下来就是画中心的文本显示步数了,这里最复杂的应该是找到文本要画在哪,也就是所谓的基线。
首先找到起始位置这个是很好理解的。
//6.3画文本 重点是找到文本的基线
String stepText=mCurrentStep+"";
Rect textBounds=new Rect();
mTextPaint.getTextBounds(stepText,0,stepText.length(),textBounds);
//找到起始位置
int dx=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(stepText,dx,baseLine,mTextPaint);里插入代码片
接下来,找基线,由于bottom是正数,top是负数,所以要用fontMetrics.bottom-fontMetrics.top,当除以2的时候得到的就是图中的1,减去fontMetrics.bottom(即图中的2)得到dy,这个时候加上getHeight()/2(即图中所指的3),得到的也就是屏幕中线的位置。
以上我们已经把圆弧的大致样子都完成了,但是这样子是没有动画效果的,没有灵魂的,这个时候我们就需要实现动画效果,当然我们的动画效果不能写在view里面,我们得在调用这个view的时候在MainActivity中写。
//7.其他 让他动起来 动画效果不能写在这里面 主要是定义 具体实现要在MainActivity
public synchronized void setStepMax(int stepMax){
this.mStepMax = stepMax;
}
public synchronized void setCurrentStep(int currentStep){
this.mCurrentStep = currentStep;
// 不断绘制 onDraw()
invalidate();
}
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final QQStepView qqStepView=findViewById(R.id.step_view);
qqStepView.setStepMax(40000);
//属性动画
ValueAnimator valueAnimator= ObjectAnimator.ofFloat(0,10000);
valueAnimator.setDuration(1000);//设置动画加载时长
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentStep=(float)animation.getAnimatedValue();
qqStepView.setCurrentStep((int)currentStep);
}
});
valueAnimator.start();
}
}
为了以防自己忘记,特意把这次用到的paint属性记录下来
mOutPaint=new Paint();
mOutPaint.setAntiAlias(true);//设置是否抗锯齿
mOutPaint.setStrokeWidth(mBoderWidth); //原因就在于setStrokeWidth这个方法,并不是往圆内侧增加圆环宽度的,而是往外侧增加一半,往内侧增加一半。https://blog.csdn.net/u014704469/article/details/41277751
mOutPaint.setColor(mOuterColor);
mOutPaint.setStrokeCap(Paint.Cap.ROUND);//设置其是圆形样式刷子开始刷的,如果不设置这个,圆环两端就可能是平切样式
mOutPaint.setStyle(Paint.Style.STROKE);//Paint.Style.FILL :填充内部Paint.Style.FILL_AND_STROKE :填充内部和描边Paint.Style.STROKE :仅描边
1.为什么ScrollView + ListView 会显示不全?
(1)首先我们看下scollView是继承FrameLayout,FrameLayout中的onMeasure()是不断循环查找自己的子View如果子View不是GONE的话就调用ViewGroup的measureChildWithMargins方法。
(2)scrollView重新measureChildWithMargins方法并且对他的childHeightMeasureSpec进行设置值,设置他的高度的测量模式heightMode为MeasureSpec.UNSPECIFIED设置完成以后调用了child.measure(childWidthMeasureSpec, childHeightMeasureSpec);方法,紧接着也就是进入了ListView的onMeasure方法。
(3)ListView中的onMeasure()方法第一个判断意思是如果他的Item的总数大于0并且它的高或者宽有一个模式为MeasureSpec.UNSPECIFIED就会执行这个方法,那么正常情况下系统这个判断就是为true,也就是拿到它的一个item的高度childHeight = child.getMeasuredHeight();紧接着它判断它的高度的模式是否为MeasureSpec.UNSPECIFIED显然也是true所以他就是让它的整个的高度等于childHeight 最后完成setMeasuredDimension(widthSize, heightSize);所以始终只拿到了listView里面一个item的高度也就是造成了我们的ListView无法正常显示
2.为什么要extends View而不能extends LinearLayout?
当继承LinearLayout的时候,他不能出来界面,LinearLayout又继承ViewGroup,而默认的ViewGroup 不会调用onDraw方法
原因是因为画的其实是 draw(Canvas canvas)
if (!dirtyOpaque)
onDraw(canvas);
dispatchDraw(canvas);
onDrawForeground(canvas);
dirtyOpaque是false 才行 其实就是由 privateFlags -> mPrivateFlags决定的
final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE && (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
得是这个值它才会给你画呀
接下来看看View和ViewGroup的区别
protected void computeOpaqueFlags() {
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;
}
}
这是view中的computeOpaqueFlags()函数,而在viewGroup中很明显ViewGroup中不是他想要的值
private void initViewGroup() {
// ViewGroup doesn't draw by default
if (!debugDraw()) {
setFlags(WILL_NOT_DRAW, DRAW_MASK);
}
}
导致 mPrivateFlags 会重新赋值