自定义 View 之仿 QQ 步数变动动画效果
博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
今天在打开 QQ 的时候,我本人一般都是不怎么去看消息的,除了各大群里面的消息刷屏,也很少聊天。其实,我个人不太喜欢在网络上聊天,除了有什么事情要说,那倒是情有可原。如果就闲聊、扯淡的话,那等到有时间,几个人坐下来喝喝茶,聊聊天不是更好嘛。虽然我高中也喜欢聊天,到大学了,就真的很少和别人在网络上聊天了。也许是年龄越来越大了,融入不了小年轻的世界啦,哈哈,我 TM 才 21 岁啊,老了老了。。。
好了,瞎扯淡的话就到这里。也许是我今天走了挺多路程的,不知咋地了,QQ 运动忽然对我说 “今天状态爆表”,我赶紧点进去看了一下,不知不觉今天都走了 2W 多步了,不过就是脚有点酸。我又点进去看了一下,咦,这个步数的动画效果很不错啊。之前虽然也看过了,但都没在意,今天突然来兴趣了,总想试试如何实现这个效果。那么,说干就干吧。
首先呢,QQ 步数动画是这样一个效果,它的外圈是一条弧线,中间是步数的值,随着这个值的增加,那么弧线的弧度也会随之递增,直接来看我实现的效果图吧,更加形象:
image 效果图好了,就是这样的一个效果,在很多 app 里面都很常见的,好像支付宝的钱钱动画也是类似的效果,一个值会在变化,直到值为最终结果为止。那么我们接下来看看如何实现这个效果吧。
首先呢,这肯定是一个自定义 View,我们需要获取有它的一个步数值、最大步数值、弧线、弧线的宽度、不同的颜色值等这几个属性,那我们就可以将它定义为一个 attrs.xml 文件,在 View 标签中用属性来设置值。第一步,在 res — value 下新建一个 attrs.xml 文件,其中代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="QQStepsView">
<!-- 步数文本的颜色 -->
<attr name="step_text_color" format="color" />
<!-- 步数文本字体的大小 -->
<attr name="step_text_size" format="dimension" />
<!-- 步数文本字体的大小 -->
<attr name="step_text_spacing" format="float" />
<!-- 步数的最大值 -->
<attr name="step_max_value" format="float" />
<!-- 弧的颜色 -->
<attr name="step_arc_color" format="color" />
<!-- 进度的颜色 -->
<attr name="step_progress_color" format="color" />
<!-- 弧的宽度 -->
<attr name="step_arc_width" format="dimension" />
<!-- 弧开始的角度 -->
<attr name="arc_start_angle" format="float" />
<!-- 弧开始的角度 -->
<attr name="arc_sweep_angle" format="float" />
</declare-styleable>
</resources>
上面代码我都标了属性作用,也就不要我再解释了吧。然后要在哪里获取这些自定义属性呢?肯定是在我们的自定义 View 里面,我们继承 View 时都会默认添加三个构造函数,就是在构造函数里面去获取这些值的。
public QQStepsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
init();
}
private void initAttrs(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepsView);
mArcColor = array.getColor(R.styleable.QQStepsView_step_arc_color, Color.LTGRAY);
mStepsColor = array.getColor(R.styleable.QQStepsView_step_progress_color, Color.RED);
mTextColor = array.getColor(R.styleable.QQStepsView_step_text_color, Color.RED);
mStartAngle = array.getFloat(R.styleable.QQStepsView_arc_start_angle, -225f);
mSweepAngle = array.getFloat(R.styleable.QQStepsView_arc_start_angle, 270f);
mStepsWidth = array.getFloat(R.styleable.QQStepsView_step_arc_width, dp2px(10f));
mTextSize = array.getDimension(R.styleable.QQStepsView_step_text_size, dp2px(28f));
mTextSpacing = array.getFloat(R.styleable.QQStepsView_step_text_spacing, dp2px(0.02f));
mMaxSteps = array.getFloat(R.styleable.QQStepsView_step_max_value, 20000f);
array.recycle();
}
}
下面的话,就要去测量 View 的宽和高了。根据这种情况,我们 View 的宽和高要一致才行,不然的话圆弧会变成椭圆的弧,就不好看了,代码如下:
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST || MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
mWidth = mHeight = 200;
} else {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
if (mWidth != mHeight) {
mHeight = mWidth = Math.min(mWidth, mHeight);
}
}
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
float paintSize = mArcPaint.getStrokeWidth() / 2;
mRect = new RectF(paintSize, paintSize, mWidth - paintSize, mHeight - paintSize);
}
最后,就是绘制图像了。一个是弧线的绘制,一个是步数文本的绘制,这部分没啥好解释的,就是计算一下坐标而已,代码如下:
private void drawSteps(Canvas canvas) {
// 绘制空的弧
canvas.drawArc(mRect, mStartAngle, mSweepAngle, false, mArcPaint);
//绘制步数的弧
mStepScale = mSteps / mMaxSteps;
canvas.drawArc(mRect, mStartAngle, mStepScale * mSweepAngle, false, mStepsPaint);
}
private void drawStepsText(Canvas canvas) {
String steps = String.valueOf(mSteps);
mBounds = new Rect();
mStepsTextPaint.getTextBounds(steps, 0, steps.length(), mBounds);
Paint.FontMetrics metrics = mStepsTextPaint.getFontMetrics();
canvas.drawText(steps, mCenterX - mBounds.width() / 2, mCenterY - (metrics.top + metrics.bottom) / 2, mStepsTextPaint);
}
这样的话,这 QQ 步数效果所需的关键性代码都写完了,那我直接放出所以代码:
package nd.no.xww.qqmessagedragview;
import android.annotation.SuppressLint;
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.Typeface;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
/**
* @author xww
* @desciption : QQ 步数动画效果
* @date 2018/7/26
* @time 16:48
* @威威喵
*/
public class QQStepsView extends View {
private Paint mArcPaint;
private Paint mStepsPaint;
private Paint mStepsTextPaint;
private float mStepsWidth;
private float mStartAngle;
private float mSweepAngle;
private int mTextColor;
private int mArcColor;
private int mStepsColor;
private float mTextSize;
private float mTextSpacing;
private int mSteps;
private float mMaxSteps;
private float mStepScale;
private RectF mRect;
private Rect mBounds;
private float mWidth;
private float mHeight;
private float mCenterX;
private float mCenterY;
private void init() {
initPaint();
}
private void initAttrs(Context context, AttributeSet attrs) {
if (attrs != null) {
TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.QQStepsView);
mArcColor = array.getColor(R.styleable.QQStepsView_step_arc_color, Color.LTGRAY);
mStepsColor = array.getColor(R.styleable.QQStepsView_step_progress_color, Color.RED);
mTextColor = array.getColor(R.styleable.QQStepsView_step_text_color, Color.RED);
mStartAngle = array.getFloat(R.styleable.QQStepsView_arc_start_angle, -225f);
mSweepAngle = array.getFloat(R.styleable.QQStepsView_arc_start_angle, 270f);
mStepsWidth = array.getDimension(R.styleable.QQStepsView_step_arc_width, dp2px(10f));
mTextSize = array.getDimension(R.styleable.QQStepsView_step_text_size, dp2px(28f));
mTextSpacing = array.getFloat(R.styleable.QQStepsView_step_text_spacing, dp2px(0.02f));
mMaxSteps = array.getFloat(R.styleable.QQStepsView_step_max_value, 20000f);
array.recycle();
}
}
private void initPaint() {
mArcPaint = getPaint(mArcColor);
mStepsPaint = getPaint(mStepsColor);
mStepsTextPaint = getPaint(mTextColor);
mStepsTextPaint.setStyle(Paint.Style.FILL);
mStepsTextPaint.setTextSize(mTextSize);
mStepsTextPaint.setLetterSpacing(mTextSpacing);
mStepsTextPaint.setTypeface(Typeface.MONOSPACE);
}
private Paint getPaint(int color) {
Paint paint = new Paint();
paint.setDither(true);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(mStepsWidth);
paint.setAntiAlias(true);
paint.setColor(color);
//设置弧度两端为圆滑
paint.setStrokeCap(Paint.Cap.ROUND);
return paint;
}
public QQStepsView(Context context) {
this(context, null);
}
public QQStepsView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public QQStepsView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initAttrs(context, attrs);
init();
}
@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST || MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
mWidth = mHeight = 200;
} else {
mWidth = MeasureSpec.getSize(widthMeasureSpec);
mHeight = MeasureSpec.getSize(heightMeasureSpec);
if (mWidth != mHeight) {
mHeight = mWidth = Math.min(mWidth, mHeight);
}
}
mCenterX = mWidth / 2;
mCenterY = mHeight / 2;
float paintSize = mArcPaint.getStrokeWidth() / 2;
mRect = new RectF(paintSize, paintSize, mWidth - paintSize, mHeight - paintSize);
}
@Override
protected void onDraw(Canvas canvas) {
drawSteps(canvas);
drawStepsText(canvas);
}
private void drawSteps(Canvas canvas) {
// 绘制空的弧
canvas.drawArc(mRect, mStartAngle, mSweepAngle, false, mArcPaint);
//绘制步数的弧
mStepScale = mSteps / mMaxSteps;
canvas.drawArc(mRect, mStartAngle, mStepScale * mSweepAngle, false, mStepsPaint);
}
private void drawStepsText(Canvas canvas) {
String steps = String.valueOf(mSteps);
mBounds = new Rect();
mStepsTextPaint.getTextBounds(steps, 0, steps.length(), mBounds);
Paint.FontMetrics metrics = mStepsTextPaint.getFontMetrics();
canvas.drawText(steps, mCenterX - mBounds.width() / 2, mCenterY - (metrics.top + metrics.bottom) / 2, mStepsTextPaint);
}
public void setSteps(int steps) {
this.mSteps = steps;
invalidate();
}
public void setMaxSteps(float maxSteps) {
this.mMaxSteps = maxSteps;
}
private float dp2px(float dp) {
return dp * getResources().getDisplayMetrics().density;
}
}
上面就是完整代码,那使用起来也非常简单,比如你可以这样:
<nd.no.xww.qqmessagedragview.QQStepsView
android:id="@+id/qqstepsview"
android:layout_width="200dp"
app:step_arc_color="#D15FEE"
app:step_max_value="40000"
app:step_progress_color="#FF83FA"
app:step_text_color="#B23AEE"
app:step_text_size="25sp"
android:layout_height="200dp"
android:layout_gravity="center" />
这里要记得添加一下 app 的一个命名空间,否则是出现不了我们自定义的属性的。这个命名空间的话,在上一层 View 下打入 app 就会跑出来啦。最后,也是至关重要的一步,动画效果的设定,上面仅仅是一个静态的 View,要想让它动起来,还需要加入以下代码方可呈现动画:
qqStepsView = findViewById(R.id.qqstepsview);
ValueAnimator valueAnimator = ObjectAnimator.ofInt(0, 26666);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
qqStepsView.setSteps((int) animation.getAnimatedValue());
}
});
valueAnimator.setDuration(2000);
valueAnimator.setInterpolator(new AccelerateInterpolator());
valueAnimator.start();
解释一下上面的代码,在 MainActivity 中绑定一下 id 拿到实例,然后给它设置一个值动画,给这个动画添加一个值的监听,它会在上面设置的 Duration(2S)内从 0 变化到 26666 的值,然后我们设置一个步数就好了,那么 View 就会一直跟随着它的变化直到动画结束。
切换一下颜色和字体大小等,都非常的简单,像使用 TextView 一样,看看效果
image好了,这样就可以完成一个效果很不错的动画了,所以你在学会了这种值变化动画后,对于那些支付宝金额、QQ步数等一些数值变化的动画还不是手到擒来。