自定义 View 之仿 QQ 步数变动动画效果

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主威威喵  |  博客主页https://blog.csdn.net/smile_running

    今天在打开 QQ 的时候,我本人一般都是不怎么去看消息的,除了各大群里面的消息刷屏,也很少聊天。其实,我个人不太喜欢在网络上聊天,除了有什么事情要说,那倒是情有可原。如果就闲聊、扯淡的话,那等到有时间,几个人坐下来喝喝茶,聊聊天不是更好嘛。虽然我高中也喜欢聊天,到大学了,就真的很少和别人在网络上聊天了。也许是年龄越来越大了,融入不了小年轻的世界啦,哈哈,我 TM 才 21 岁啊,老了老了。。。

    好了,瞎扯淡的话就到这里。也许是我今天走了挺多路程的,不知咋地了,QQ 运动忽然对我说 “今天状态爆表”,我赶紧点进去看了一下,不知不觉今天都走了 2W 多步了,不过就是脚有点酸。我又点进去看了一下,咦,这个步数的动画效果很不错啊。之前虽然也看过了,但都没在意,今天突然来兴趣了,总想试试如何实现这个效果。那么,说干就干吧。

    首先呢,QQ 步数动画是这样一个效果,它的外圈是一条弧线,中间是步数的值,随着这个值的增加,那么弧线的弧度也会随之递增,直接来看我实现的效果图吧,更加形象:

《自定义 View 之仿 QQ 步数变动动画效果》

    好了,就是这样的一个效果,在很多 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 一样,看看效果

《自定义 View 之仿 QQ 步数变动动画效果》

    好了,这样就可以完成一个效果很不错的动画了,所以你在学会了这种值变化动画后,对于那些支付宝金额、QQ步数等一些数值变化的动画还不是手到擒来。

    原文作者:威威貓
    原文地址: https://blog.csdn.net/smile_Running/article/details/81223260
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞