接着上篇Android进阶之自定义控件(2)高仿钉钉运动步数实现可动的进度圆环(上)的基础,我们来实现钉钉运动的效果:
《一》View效果分析:
对钉钉运动的效果进行分析:
1、圆弧应该是从135°起,绘制了270°。
2、步数小于10000步时,背景圆弧为灰色,进度圆弧为蓝色渐变色;步数大于10000步时,进度圆弧为渐变的黄色
3、需要绘制中间的步数及步数上面的名次
4、实现步数及进度的动态变化
有了上篇的基础,实现钉钉运动步数的效果,就很简单了。还是先看一下我最终实现的高仿版效果,给大家点信心。变化的速度可以配置:
《二》实现步骤分解:
(1)绘制灰色背景圆弧,135°起,绘制了270°
canvas.drawArc(oval, 135, 270, false, mPaint);
(2)绘制进度圆弧
canvas.drawArc(oval, 0 + 135, mCurrentStep/ mMaxStep * 270, false, mProgressPaint);
(3)绘制文字很简单,只需要以正中间的文字为标准,往上移动一个textRect.height()和文字之间的间距textPadding即可。上篇这些基础的知识及需要注意的小细节都有详细的讲解,不多做解释。到此步骤,实现的效果如下:
//绘制中间的步数的文字
Rect textRect = new Rect();
String mShowText = (int) mCurrentStep + "";
mTextPaint.setTextSize(sp2px(mTextSize));
mTextPaint.getTextBounds(mShowText, 0, mShowText.length(), textRect);
canvas.drawText(mShowText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2, mTextPaint);
//绘制排名的文字,在步数文字的上方
String mRandText = "第4名";//使用时,名次动态传入即可,此处写死为了测试
mTextPaint.setTextSize(sp2px(mRandTextSize));
mTextPaint.getTextBounds(mRandText, 0, mRandText.length(), textRect);
canvas.drawText(mRandText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2 - (textRect.height() + textPadding), mTextPaint);
(4)可以看到,上图还是静态的效果,接下来我们通过两种方式来让进度圆弧和文字动起来。
1、方法一:还是上篇讲解过的方法,通过开分线程的方式,不断更新进度值,不断重绘。
new Thread(new Runnable() {
@Override
public void run() {
setCurrentStep(0);
float changeProgress = currentStep;
for (float i = 0; i < changeProgress; i++) {
setCurrentStep(getCurrentStep() + rate);
SystemClock.sleep(20);
// invalidate();//invalidate()必须在主线程中执行,此处不能使用
// postInvalidate();//强制重绘,postInvalidate()可以在主线程也可以在分线程中执行
changeProgress = changeProgress - rate;
}
//由于上面的循环结束时,可能计算后最终无法到达mCurrentProgress的值,所以在循环结束后,将mCurrentProgress重新设置
setCurrentStep(currentStep);
}
}).start();
2、方法二:使用属性动画和差值器实现
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, currentStep);
valueAnimator.setDuration(1000);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentStep = (float) animation.getAnimatedValue();
setCurrentStep((int) currentStep);
}
});
valueAnimator.start();
(5)最后一步了,来看看渐变色的进度圆弧如何处理。
处理渐变有两个类:
1、线性渐变:LinearGradient
2、扫描式渐变:SweepGradient ——在中心点画一个扫描梯度的着色器
很明显,在这里我们需要用到SweepGradient来处理,如下,给画笔设置Shader,简单的设置如下。
int[] mGradientColorArray = new int[]{ContextCompat.getColor(mContext, R.color.color_5e9fff), ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_5e9fff)};
SweepGradient shader = new SweepGradient(getWidth() / 2 - mRoundWidth / 2, getWidth() / 2 - mRoundWidth / 2, mGradientColorArray, null);
mProgressPaint.setShader(shader);
还是和钉钉的效果有点区别,钉钉的圆弧两边是浅色的,上面顶部的部分是深色的,我们稍作处理一下,代码如下:
/**
* 处理渐变色
*/
//渐变颜色数组
int[] mGradientColorArray = new int[]{ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_5e9fff), ContextCompat.getColor(mContext, R.color.color_4fd8f5)};
int count = mGradientColorArray.length;
int[] colors = new int[count];
System.arraycopy(mGradientColorArray, 0, colors, 0, count);
float[] positions = new float[count];
//由于此处绘制的是不完整的圆弧,故需要额外处理一下变化的比例值
float v = (360f / 270);
positions[0] = 0.0f;
positions[1] = 0.33f * v;
positions[2] = 0.67f * v;
positions[3] = 1.0f;
SweepGradient shader = new SweepGradient(getWidth() / 2 - mRoundWidth / 2, getWidth() / 2 - mRoundWidth / 2, mGradientColorArray, positions);
mProgressPaint.setShader(shader);
渐变的效果搞定。
测试代码,在使用的地方调用startCountStep()即可,是不是敲极简单:
//当前实际的步数
float mCurrentStep = 9709;
sportStepView.startCountStep(mCurrentStep);
View的完整代码:
package com.example.jojo.learn.customview;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.SweepGradient;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.animation.DecelerateInterpolator;
import com.example.jojo.learn.R;
/**
* Created by JoJo on 2018/8/1.
* wechat:18510829974
* description: 仿钉钉运动步数效果
*/
public class SportStepView extends View {
private Context mContext;
//内圆环颜色
private int innerRoundColor;
//外圆环颜色
private int outerRoundColor;
//绘制背景圆环的画笔
private Paint mPaint;
//绘制外面进度的圆环的画笔
private Paint mProgressPaint;
//绘制外面进度的圆环的画笔
private Paint mTextPaint;
//背景圆弧的绘制的宽度
private int mRoundWidth = 10;
//进度圆环的宽度
private float mProgressRoundWidth = 15;
//中间步数文字的大小
private int mTextSize = 40;//单位 sp
//名次文字大小
private int mRandTextSize = 15;
//圆环最大进度
private int mMaxStep = 10000;
//圆环当前进度
private float mCurrentStep = 0;
//排名与步数文字直接的间隔
private float textPadding = 80;
//速度,值越大,变化速度越快
private float rate = 128;
public SportStepView(Context context) {
this(context, null);
}
public SportStepView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public SportStepView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
this.mContext = context;
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.SportStepView);
innerRoundColor = typedArray.getColor(R.styleable.SportStepView_innerRoundColor, ContextCompat.getColor(mContext, R.color.color_e4e4e4));
outerRoundColor = typedArray.getColor(R.styleable.SportStepView_outerRoundColor, ContextCompat.getColor(mContext, R.color.color_4fd8f5));
init();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取宽的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
//获取宽的尺寸
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
//对wrap_content这种模式进行处理
if (heightMode == MeasureSpec.AT_MOST) {
heightSize = widthSize;
}
//以宽度为标准保存丈量结果
setMeasuredDimension(widthSize, heightSize);
}
private void init() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(innerRoundColor);
mPaint.setStrokeCap(Paint.Cap.ROUND);// 圆形笔头
mPaint.setStrokeWidth(mRoundWidth);
mProgressPaint = new Paint();
mProgressPaint.setAntiAlias(true);// 抗锯齿效果
mProgressPaint.setStyle(Paint.Style.STROKE);
mProgressPaint.setColor(outerRoundColor);
mProgressPaint.setStrokeCap(Paint.Cap.ROUND);// 圆形笔头
mProgressPaint.setStrokeWidth(mProgressRoundWidth);
mTextPaint = new Paint();
mTextPaint.setAntiAlias(true);// 抗锯齿效果
mTextPaint.setStyle(Paint.Style.STROKE);
mTextPaint.setColor(Color.BLACK);
mTextPaint.setColor(ContextCompat.getColor(mContext, R.color.color_56a9ff));
mTextPaint.setTextSize(sp2px(mTextSize));
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
/**
* 处理渐变色
*/
//默认的渐变颜色数组:
// int[] mGradientColorArray = new int[]{ContextCompat.getColor(mContext, R.color.color_5e9fff), ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_5e9fff)};
int[] mGradientColorArray = new int[]{ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_4fd8f5), ContextCompat.getColor(mContext, R.color.color_5e9fff), ContextCompat.getColor(mContext, R.color.color_4fd8f5)};
int count = mGradientColorArray.length;
int[] colors = new int[count];
System.arraycopy(mGradientColorArray, 0, colors, 0, count);
float[] positions = new float[count];
float v = (360f / 270);
positions[0] = 0.0f;
positions[1] = 0.33f * v;
positions[2] = 0.67f * v;
positions[3] = 1.0f;
SweepGradient shader = new SweepGradient(getWidth() / 2 - mRoundWidth / 2, getWidth() / 2 - mRoundWidth / 2, mGradientColorArray, positions);
mProgressPaint.setShader(shader);
if (mRoundWidth < mProgressRoundWidth) {
RectF oval = new RectF(0 + mProgressRoundWidth / 2, 0 + mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2, getWidth() - mProgressRoundWidth / 2);
//绘制背景圆环
canvas.drawArc(oval, 135, 270, false, mPaint);
//绘制进度圆环,绘制的角度最大不超过270°
if (mCurrentStep * 1f / mMaxStep <= 1) {
canvas.drawArc(oval, 0 + 135, mCurrentStep / mMaxStep * 270, false, mProgressPaint);
} else {
canvas.drawArc(oval, 0 + 135, 270, false, mProgressPaint);
}
} else {
RectF oval = new RectF(0 + mRoundWidth / 2, 0 + mRoundWidth / 2, getWidth() - mRoundWidth / 2, getWidth() - mRoundWidth / 2);
//绘制背景圆环
canvas.drawArc(oval, 135, 270, false, mPaint);
//绘制进度圆环
if (mCurrentStep * 1f / mMaxStep <= 1) {
canvas.drawArc(oval, 0 + 135, mCurrentStep / mMaxStep * 270, false, mProgressPaint);
} else {
canvas.drawArc(oval, 0 + 135, 270, false, mProgressPaint);
}
}
//绘制中间的步数的文字
Rect textRect = new Rect();
String mShowText = (int) mCurrentStep + "";
mTextPaint.setTextSize(
sp2px(mTextSize));
mTextPaint.getTextBounds(mShowText, 0, mShowText.length(), textRect);
canvas.drawText(mShowText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2, mTextPaint);
//绘制排名的文字,在步数文字的上方
String mRandText = "第4名";
mTextPaint.setTextSize(
sp2px(mRandTextSize));
mTextPaint.getTextBounds(mRandText, 0, mRandText.length(), textRect);
canvas.drawText(mRandText, getWidth() / 2 - textRect.width() / 2, getHeight() / 2 + textRect.height() / 2 - (textRect.height() + textPadding), mTextPaint);
}
public void setCurrentStep(float currentStep) {
this.mCurrentStep = currentStep;
//强制重绘,postInvalidate()可以在主线程也可以在分线程中执行
postInvalidate();
}
public void setMaxProgress(int maxStep) {
this.mMaxStep = maxStep;
}
public int getMaxtep() {
return mMaxStep;
}
public float getCurrentStep() {
return mCurrentStep;
}
/**
* 将sp转换成px
*
* @param sp
* @return
*/
private int sp2px(int sp) {
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp,
getResources().getDisplayMetrics());
}
/**
* 开始动态计步
*
* @param currentStep
*/
public void startCountStep(final float currentStep) {
//方法一:开一个分线程,动态改变进度的值,不断绘制达到进度变化的效果
// new Thread(new Runnable() {
// @Override
// public void run() {
// setCurrentStep(0);
// float changeProgress = currentStep;
// for (float i = 0; i < changeProgress; i++) {
// setCurrentStep(getCurrentStep() + rate);
// SystemClock.sleep(20);
//// invalidate();//invalidate()必须在主线程中执行,此处不能使用
//// postInvalidate();//强制重绘,postInvalidate()可以在主线程也可以在分线程中执行
// changeProgress = changeProgress - rate;
// }
// //由于上面的循环结束时,可能计算后最终无法到达mCurrentProgress的值,所以在循环结束后,将mCurrentProgress重新设置
// setCurrentStep(currentStep);
// }
// }).start();
/**
* 方法二
*/
ValueAnimator valueAnimator = ObjectAnimator.ofFloat(0, currentStep);
valueAnimator.setDuration(1000);
valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float currentStep = (float) animation.getAnimatedValue();
setCurrentStep((int) currentStep);
}
});
valueAnimator.start();
}
}
涉及到的自定义属性及color.xml
#4fd8f5
#5e9fff
#e4e4e4
最后,附上我的一个Kotlin编写+组件化开发的开源项目Designer
Kotlin+组件化开发实践—开源项目Designer-App
Designer项目算是倾注了我蛮多心血了,每个页面和功能都当成是上线的App来做,App的logo还特地做了UI设计力求做到精致和完善,其中还包括了很多自己项目开发中的经验汇总和对新技术的探索和整合,希望对各位读者有所帮助,欢迎点个star,follow,或者给个小心心,嘻嘻也可以分享给你更多的朋友一起学习,您的支持是我不断前进的动力。如果有任何问题,欢迎在GitHub上给我提issue或者留言。