
可以设置数字颜色,字体颜色,运动步数,运动排名,运动平均步数,虚线下方的蓝色指示条的长度会随着平均步数改变而进行变化。整体效果还是和QQ运动健康界面很像的。
自定义View四部曲,一起来看看怎么实现的。
1.自定义view的属性:
<?xml version="1.0" encoding="utf-8"?><resources> //自定义属性名,定义公共属性 <attr name="titleSize" format="dimension"></attr> <attr name="titleText" format="string"></attr> <attr name="titleColor" format="color"></attr> <attr name="outCircleColor" format="color"></attr> <attr name="inCircleColor" format="color"></attr> <attr name="lineColor" format="color"></attr> //自定义View的属性 <declare-styleable name="MyQQHealthView"> <attr name="titleColor"></attr> <attr name="lineColor"></attr> </declare-styleable></resources>依次定义了字体颜色,线的颜色2个属性,format是该属性的取值类型。
<com.example.tangyangkai.myview.MyQQHealthViewandroid:id="@+id/myQQView"android:layout_width="match_parent"android:layout_height="530dp"android:layout_margin="15dp"myQQ:lineColor="@color/font_tips"myQQ:titleColor="@color/textcolor"myQQ:titleSize="50dp" />自定义view的属性我们可以自己进行设置,记得最后要引入我们的命名空间,
public MyQQHealthView(Context context) { this(context, null); } public MyQQHealthView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public MyQQHealthView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); //获取我们自定义的样式属性 TypedArray array = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MyQQHealthView, defStyleAttr, 0); int n = array.getIndexCount(); for (int i = 0; i < n; i++) {int attr = array.getIndex(i);switch (attr) {case R.styleable.MyQQHealthView_titleColor: // 默认颜色设置为黑色 textColor = array.getColor(attr, Color.BLACK); break;case R.styleable.MyQQHealthView_lineColor: lineColor = array.getColor(attr, Color.BLACK); break;} } array.recycle(); init(); }自定义View一般需要实现一下三个构造方法,这三个构造方法是一层调用一层的,属于递进关系。因此,我们只需要在最后一个构造方法中来获得View的属性了。@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); }这个方法的作用是:测量控件的大小。其实Android系统在加载布局的时候是由系统测量各子View的大小来告诉父View我需要占多大空间,然后父View会根据自己的大小来决定分配多大空间给子View。MeasureSpec的specMode模式一共有三种: @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int widthMode = MeasureSpec.getMode(widthMeasureSpec); int widthSize = MeasureSpec.getSize(widthMeasureSpec); int heightMode = MeasureSpec.getMode(heightMeasureSpec); int heightSize = MeasureSpec.getSize(heightMeasureSpec); int width; int height; //如果布局里面设置的是固定值,这里取布局里面的固定值;如果设置的是match_parent,则取父布局的大小 if (widthMode == MeasureSpec.EXACTLY) {width = widthSize; } else {//如果布局里面没有设置固定值,这里取布局的宽度的1/2width = widthSize * 1 / 2; } if (heightMode == MeasureSpec.EXACTLY) {height = heightSize; } else {//如果布局里面没有设置固定值,这里取布局的高度的3/4height = heightSize * 3 / 4; } widthBg = width; heightBg = height; setMeasuredDimension(width, height); startAnim(); }我这里为了不让布局显得过小,所以WARP_CONTENT分别取宽的1/2,高的3/4,具体情况因人而异。//绘制最底层的背景 radiusBg = widthBg / 20; pathBg.moveTo(0, heightBg); pathBg.lineTo(0, radiusBg); pathBg.quadTo(0, 0, radiusBg, 0); pathBg.lineTo(widthBg - radiusBg, 0); pathBg.quadTo(widthBg, 0, widthBg, radiusBg); pathBg.lineTo(widthBg, heightBg); pathBg.lineTo(0, heightBg); backgroundPaint.setColor(Color.WHITE); canvas.drawPath(pathBg, backgroundPaint);整个自定义View的最底层是一个左上,右上有弧度,左下,右下为直角的白色背景。使用canvas.drawRoundRect实现的矩形是四个角都有弧度,达不到预期。但是一阶贝塞尔曲线加上二阶贝塞尔曲线就能很好的实现这种特殊的情况。
//绘制圆弧 arcPaint.setStrokeWidth(widthBg / 20); //设置空心 arcPaint.setStyle(Paint.Style.STROKE); //防抖动 arcPaint.setDither(true); //连接处为圆弧 arcPaint.setStrokeJoin(Paint.Join.ROUND); //画笔的笔触为圆角 arcPaint.setStrokeCap(Paint.Cap.ROUND); arcPaint.setColor(lineColor); //圆弧范围 arcRect = new RectF(widthBg * 1 / 4, widthBg * 1 / 4, widthBg * 3 / 4, widthBg * 3 / 4); //绘制背景大圆弧 canvas.drawArc(arcRect, 120, 300, false, arcPaint); arcPaint.setColor(textColor); //绘制分数小圆弧 canvas.drawArc(arcRect, 120, arcNum, false, arcPaint);绘制圆弧先确定圆弧的范围,传入的四个参数就是圆弧所在圆的外接矩形的坐标。canvas.drawArc的五个参数依次是圆弧范围;开始的角度;圆弧的角度;第四个为True时,在绘制圆弧时会将圆心包括在内,通常用来绘制扇形,我们这里选false;圆弧的画笔
//绘制圆圈内的数字 textPaint.setColor(textColor); textPaint.setTextSize(widthBg / 10); canvas.drawText(String.valueOf(walkNum), widthBg * 3 / 8, widthBg * 1 / 2 + 20, textPaint); //绘制名次 textPaint.setTextSize(widthBg / 15); canvas.drawText(String.valueOf(rankNum), widthBg * 1 / 2 - 15, widthBg * 3 / 4 + 10, textPaint); //绘制其他文字 textPaint.setColor(lineColor); textPaint.setTextSize(widthBg / 25); canvas.drawText("截止13:45已走", widthBg * 3 / 8 - 10, widthBg * 5 / 12 - 10, textPaint); canvas.drawText("好友平均2781步", widthBg * 3 / 8 - 10, widthBg * 2 / 3 - 20, textPaint); canvas.drawText("第", widthBg * 1 / 2 - 50, widthBg * 3 / 4 + 10, textPaint); canvas.drawText("名", widthBg * 1 / 2 + 30, widthBg * 3 / 4 + 10, textPaint); //绘制圆圈外的文字 canvas.drawText("最近7天", widthBg * 1 / 15, widthBg, textPaint); myaverageTxt = String.valueOf(averageSize); canvas.drawText("平均", widthBg * 10 / 15 - 15, widthBg, textPaint); canvas.drawText(myaverageTxt, widthBg * 11 / 15, widthBg, textPaint); canvas.drawText("步/天", widthBg * 12 / 15 + 20, widthBg, textPaint);绘制文字就稍微简单点,这里计算好各自的位置即可
//绘制虚线 linePaint.setStyle(Paint.Style.STROKE); linePaint.setStrokeWidth(2); linePaint.setColor(lineColor); linePath.moveTo(widthBg * 1 / 15, widthBg + 80); linePath.lineTo(widthBg * 14 / 15, widthBg + 80); linePaint.setPathEffect(effects); canvas.drawPath(linePath, linePaint); rectSize = widthBg / 12; rectAgHeight = widthBg / 10; //绘制虚线上的圆角竖线 for (int i = 0; i < FourActivity.sizes.size(); i++) {rectPaint.setStrokeWidth(widthBg / 25);rectPaint.setStyle(Paint.Style.STROKE);rectPaint.setStrokeJoin(Paint.Join.ROUND);rectPaint.setStrokeCap(Paint.Cap.ROUND);float startHeight = widthBg + 90 + rectAgHeight;rectPath.moveTo(rectSize, startHeight);double percentage = Double.valueOf(FourActivity.sizes.get(i)) / Double.valueOf(averageSize);double height = percentage * rectAgHeight;rectPath.lineTo(rectSize, (float) (startHeight - height));rectPaint.setColor(textColor);canvas.drawPath(rectPath, rectPaint);//绘制下方的文字textPaint.setColor(lineColor);canvas.drawText("0" + (i + 1) + "日", rectSize - 25, startHeight + 50, textPaint);rectSize += widthBg / 7; }DashPathEffect的作用就是将Path的线段虚线化。构造函数为DashPathEffect(float[] intervals, float offset),其中intervals为虚线的ON和OFF数组,该数组的length必须大于等于2,float[0] ,float[1] 依次代表第一条实线与第一条虚线的长度,如果数组后面不再有数据则重复第一个数以此往复循环。offset为绘制时的偏移量。//绘制底部波纹 weavPaint.setColor(textColor); weavPath.moveTo(0, heightBg); weavPath.lineTo(0, heightBg * 10 / 12); weavPath.cubicTo(widthBg * 1 / 10, heightBg * 10 / 12, widthBg * 3 / 10, heightBg * 11 / 12, widthBg, heightBg * 10 / 12); weavPath.lineTo(widthBg, heightBg); weavPath.lineTo(0, heightBg); canvas.drawPath(weavPath, weavPaint); //绘制底部文字 weavPaint.setColor(Color.WHITE); weavPaint.setTextSize(widthBg / 20); canvas.drawText("成绩不错,继续努力哟!", widthBg * 1 / 10 - 20, heightBg * 11 / 12 + 50, weavPaint);底部水波纹的实现使用的是三阶贝塞尔曲线:
private void startAnim() { //步数动画的实现 ValueAnimator walkAnimator = ValueAnimator.ofInt(0, mySize); walkAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {walkNum = (int) animation.getAnimatedValue();postInvalidate();} }); //排名动画的实现 ValueAnimator rankAnimator = ValueAnimator.ofInt(0, rank); rankAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {rankNum = (int) animation.getAnimatedValue();postInvalidate();} }); double size = mySize; double avgSize = averageSize; if (size > avgSize) {size = avgSize; } //圆弧动画的实现 ValueAnimator arcAnimator = ValueAnimator.ofFloat(0, (float) (size / avgSize * 300)); arcAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {@Overridepublic void onAnimationUpdate(ValueAnimator animation) {arcNum = (float) animation.getAnimatedValue();postInvalidate();} }); animSet.setDuration(3000); animSet.playTogether(walkAnimator, rankAnimator, arcAnimator); animSet.start(); }这里就不得不提到属性动画的优越性了,不仅可以作用在view上,也可以作用于某个对象上。只需要设置好开始值与结束值,添加一个动画的监听,就能够得到变化的值,再使用postInvalidate()方法,从而调用onDraw方法来进行数值的改变。最后设置一个组合动画—-AnimatorSet,使三个动画达到同步一致的效果。 public void reSet(int mysize, int myrank, int myaverageSize) { walkNum = 0; arcNum = 0; rankNum = 0; mySize = mysize; rank = myrank; averageSize = myaverageSize; startAnim(); }将设置的值通过构造方法传递过来,最后调用开启动画的方法即可。对应的Activity的代码:public class FourActivity extends AppCompatActivity { private MyQQHealthView view; public static List<Integer> sizes = new ArrayList<>(); private Button btn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_four); initview(); } private void initview() { view = (MyQQHealthView) findViewById(R.id.myQQView); view.setMySize(2345); view.setRank(11); view.setAverageSize(5436); sizes.add(1234); sizes.add(2234); sizes.add(4234); sizes.add(6234); sizes.add(3834); sizes.add(7234); sizes.add(5436); btn = (Button) findViewById(R.id.set_btn); btn.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {view.reSet(6534, 8, 4567);} }); }}自己根据情况设置想要的值就可以了。