前言

在网上看到一张图,花了些时间自己尝试着写了一个自定义View,里面涉及到了自定义属性、自定义View
padding属性的处理、画笔(Paint)和画布(Canvas)的使用、分辨率适配问题、性能问题、属性动画等,觉得还是有些东西值的记录一下的,效果图如下:



自定义属性

基础属性定义说明:

属性类型 属性定义方式 属性值说明
color FF565656
string “字符串”
integer 123
boolean true
fraction 0.9
reference @drawable/text_color
dimension 23dp
enum
自定义属性的步骤如下:

1、在values目录下创建一个attrs.xml文件。



2、在属性文件attrs.xml中声明属性。
<declare-styleable name="CircleProgress"> <attr name="foreground_color" format=
"color"/> <attr name="background_color" format="color"/> <attr name="text_color"
format="color"/> <attr name="stroke_width" format="dimension"/> </
declare-styleable>
3、在布局文件中使用自定义属性。

布局中使用自定义属性(最后一行):
<com.android.peter.animationdemo.CircleProgress android:id="@+id/cp_progress"
android:layout_width="wrap_content" android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/message"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf=
"parent" app:stroke_width="8dp"/>
在自定义View中使用这些属性:
private void initTypedArray(Context context,AttributeSet attrs) { TypedArray
ta = context.obtainStyledAttributes(attrs,R.styleable.CircleProgress); try {
mStrokeWidth = ta.getDimension(R.styleable.CircleProgress
_stroke_width,getResources().getDimension(R.dimen.circle
_progress_default_stroke_width)); mBackgroundColor = ta.getColor(R.styleable
.CircleProgress_background_color,getResources().getColor(R.color.white,null));
mForegroundColor = ta.getColor(R.styleable.CircleProgress
_foreground_color,getResources().getColor(R.color.colorPrimary,null));
mTitleTextColor = ta.getColor(R.styleable.CircleProgress
_text_color,getResources().getColor(R.color.colorPrimary,null)); } catch
(Exception ex) { ex.printStackTrace(); } finally { if(ta != null) { ta.recycle()
; } } }
通过上述方式就可以在布局文件中定义这些属性的值,在Class中读取这些属性值。如果,自定义属性没有被赋值就会使用预设的缺省值作为属性值。这里有两点需要注意:
1、TypedArray使用完一定要回收,否则会造成内存泄漏。
2、属性缺省值应该在demens.xml文件中定义,不能只是传递一个数值。否则会导致在不同分辨率的手机上显示大小不一致。

自定义View

自定义View需要重写onMeasure、onLayout和onDraw三个方法,分别用来测量、定位和绘制。

1、在onMeasure方法中根据测量模式设置View默认的宽高。
@Override protected void onMeasure(int widthMeasureSpec, int
heightMeasureSpec) { Log.i(TAG,"onMeasure"); super.onMeasure(widthMeasureSpec,
heightMeasureSpec); int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); int widthSpecSize
= MeasureSpec.getSize(widthMeasureSpec); int heightSpecSize = MeasureSpec
.getSize(heightMeasureSpec); if(widthSpecMode == MeasureSpec.AT_MOST &&
heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int)getResources().getDimension(R.dimen.circle
_progress_default_width), (int)getResources().getDimension(R.dimen.circle
_progress_default_height)); } else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension((int)getResources().getDimension(R.dimen.circle
_progress_default_width), heightSpecSize); } else if (heightSpecMode ==
MeasureSpec.AT_MOST) { setMeasuredDimension(widthSpecSize,(int)getResources()
.getDimension(R.dimen.circle_progress_default_height)); } }
2、在onLayout方法中对一些数据进行处理。
@Override protected void onLayout(boolean changed, int left, int top, int
right,int bottom) { Log.i(TAG,"onLayout"); super.onLayout(changed, left, top,
right, bottom); mPaddingStart = getPaddingStart() + (int)(mStrokeWidth/4);
mPaddingEnd = getPaddingEnd() + (int)(mStrokeWidth/4); mPaddingTop =
getPaddingTop() + (int)(mStrokeWidth/4); mPaddingBottom = getPaddingBottom() + (
int)(mStrokeWidth/4); mViewWidth = getWidth() - mPaddingStart - mPaddingEnd;
mViewHeight = getHeight() - mPaddingTop - mPaddingBottom; mCenterX =
mPaddingStart + mViewWidth/2; mCenterY = mPaddingTop + mViewHeight/2;
mCircleRadius = mViewWidth < mViewHeight ? mViewWidth/2 : mViewHeight/2;
mFrameRectF.set(mCenterX - mCircleRadius, mCenterY - mCircleRadius, mCenterX +
mCircleRadius, mCenterY + mCircleRadius); }
3、在onDraw中进行View的绘制。
@Override protected void onDraw(Canvas canvas) { Log.i(TAG,"onDraw"); super
.onDraw(canvas); // background canvas.drawCircle
(mCenterX,mCenterY,mCircleRadius - mStrokeWidth/2, mBackgroundPaint); //
foreground canvas.drawArc(mFrameRectF.left + mStrokeWidth/2,mFrameRectF.top +
mStrokeWidth/2,mFrameRectF.right - mStrokeWidth/2, mFrameRectF.bottom -
mStrokeWidth/2,270,(float) (3.6* mPercentage),false,mForegroundPaint); // end
circle if(mPercentage !=0 && mPercentage != 100) { // end out circle canvas
.drawCircle((float) (mCenterX + (mCircleRadius-mStrokeWidth/2)*Math.cos((3.6*
mPercentage -90)*Math.PI/180)), (float)(mCenterY + (mCircleRadius-mStrokeWidth/2
)*Math.sin((3.6* mPercentage -90)*Math.PI/180)), mStrokeWidth*3/4,
mEndOutCirclePaint); // end inner circle canvas.drawCircle((float) (mCenterX +
(mCircleRadius-mStrokeWidth/2)*Math.cos((3.6* mPercentage -90)*Math.PI/180)),
(float)(mCenterY + (mCircleRadius-mStrokeWidth/2)*Math.sin((3.6* mPercentage -90
)*Math.PI/180)), mStrokeWidth/4, mEndInnerCirclePaint); } // text
mTitleTextPaint.getTextBounds(mTitleText,0,mTitleText.length(), mTextRect);
canvas.drawText(mTitleText,mCenterX,mCenterY/2 + mTextRect.height()/2
,mTitleTextPaint); mValueTextPaint.getTextBounds(mValueText,0,mValueText.length
(),mTextRect); canvas.drawText(mValueText,mCenterX,mCenterY + mTextRect.height
()/2,mValueTextPaint); mUnitTextPaint.getTextBounds(mUnitText,0,mUnitText.length
(),mTextRect); canvas.drawText(mUnitText,mCenterX,mCenterY*4/3,mUnitTextPaint);
// coordinate/*canvas.drawLine(0,mCenterY,2*mCenterX,mCenterY,mTestPaint);
canvas.drawLine(mCenterX,0,mCenterX,2*mCenterY,mTestPaint);*/ }

注意,在onDraw方法里面尽量不要新建对象和做耗时操作,因为onDraw经常会被频繁调用,新建对象会触发垃圾回收导致内存抖动影响性能。耗时操作会导致在一个绘制周期内无法完成所有的绘制工作从而出现丢帧问题。

添加动画效果


首先借助ValueAnimator类定义一个动画,然后为这个动画添加一个监听器监听动画更新事件。每当有事件更新就获取animation的值并记录下来,然后触发view刷新,view刷新的时候就会重新onDraw从而根据记录下来的值进行重绘,这样连续起来就实现了想要的动画效果。
对之前的代码重新封装了一下,核心代码如下。

在第一次触发onDraw的时候启动动画。
@Override protected void onDraw(Canvas canvas) { Log.i(TAG,"onDraw"); super
.onDraw(canvas);if(mIsFirstTime) { startCircleProgressAnim(); startValueAnim();
mIsFirstTime =false; } // background circle drawBackgroundCircle(canvas); //
foreground circle drawForegroundCircle(canvas); // content drawContent(canvas);
// coordinate // drawCoordinate(canvas); }
实现动画及监听动画更新的逻辑。
public void startCircleProgressAnim() { ValueAnimator anim =
ValueAnimator.ofInt(0, mPercent); anim.setDuration(500); anim.setInterpolator(
new LinearInterpolator()); anim.addUpdateListener(new
ValueAnimator.AnimatorUpdateListener() {@Override public void onAnimationUpdate
(ValueAnimator animation) { mTempPercent = (int) animation.getAnimatedValue();
invalidate(); } }); anim.start(); }public void startValueAnim() { ValueAnimator
anim = ValueAnimator.ofInt(0, mValue); anim.setDuration(500);
anim.setInterpolator(new LinearInterpolator()); anim.addUpdateListener(new
ValueAnimator.AnimatorUpdateListener() {@Override public void onAnimationUpdate
(ValueAnimator animation) { mTempValueText = Integer.toString((Integer)
animation.getAnimatedValue()); invalidate(); } }); anim.start(); }
小结


通过上述简单步骤就可以实现一个带动画的自定义View,重要地方都做了加粗处理。在实现自定义View的时候主要耗时在绘制位置的计算上了。另外,Math的sin和cos方法使用的时候需要注意传递参数要用度数乘以Math.PI/180。文字居中绘制只需对画笔进行如下设置无需测量计算。
Paint.setTextAlign(Paint.Align.CENTER);
Demo <https://gitee.com/peter_RD_nj/DemoAllInOne/tree/master/AnimationDemo>

参考文献

关于Android自定义属性你可能不知道的细节 <https://www.jianshu.com/p/d29ecf4c68f3>
Android Canvas之Path操作 <https://www.jianshu.com/p/9ad3aaae0c63>
Property Animation(属性动画)使用详解
<https://github.com/OCNYang/Android-Animation-Set/tree/master/property-animation>