Android带动画进度条简单实现
一、前言
最近在使用一个打卡软件时,发现它使用的打卡记录的进度条效果挺不错的,进度条会从0走到当前的完成进度,这中间有一个平缓的动画效果。然后,试着自己也简单的实现了一个,先上效果图。
实现主要分为三部分:
- 绘制边框,该部分是固定
- 绘制边框内的进度值,从0增加到指定值
- 为进度值的改变添加动画效果
二、代码实现
1、自定义属性
在resouces\values路径下新建attrs.xml,在这里我们只是简单的添加了控件宽度、控件高度、边框距内部填充距离这三个属性。
attrs.xml:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="MyProgressbar">
<attr name="progressbar_width" format="dimension"/>
<attr name="progressbar_height" format="dimension"/>
<attr name="progressbar_ProgressToFrameWidth" format="dimension"/>
</declare-styleable>
</resources>
2、实现MyProgressbar类
(1)新建MyProgressbar类继承View。在构造函数中获取自定义控件属性值。
public MyProgressbar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.MyProgressbar);
mProgressbarWidth = (int)array.getDimension(R.styleable.MyProgressbar_progressbar_width, 100);
mProgressbarHeight = (int)array.getDimension(R.styleable.MyProgressbar_progressbar_height, 10);
mProgressToFrameWidth = array.getDimension(R.styleable.MyProgressbar_progressbar_ProgressToFrameWidth, 6f);
}
(2)重写onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mProgressbarWidth, mProgressbarHeight);
}
(3)重写onDraw方法
对控件的绘制逻辑都是在该方法中完成的。首先初始化画笔,将Style设置为非填充,并设置画笔宽度,用来实现边框效果。通过RectF设置边框范围,调用Canvas的drawRoundRect()方法进行绘制。该方法中间的两个参数代表四个圆角的x轴和y轴半径长度。这里需要注意,当画笔宽度大于1时,如果绘制的矩形是紧贴控件边框的,会发现边框外的一半不见了。需要将边框的绘制范围设置为控件范围往里缩半个画笔宽度,用来正好显示整个边框。
RectF frameRectF = new RectF(mPaintWidth / 2, mPaintWidth / 2, mProgressbarWidth - mPaintWidth / 2, mProgressbarHeight - mPaintWidth / 2);
canvas.drawRoundRect(frameRectF, 15, 15, mPaint);
接着来实现中间的进度填充部分,该部分为一个实心的圆角矩形。和边框的区别为绘制该部分时,画笔Style应为FILL填充。因为填充部分不需要体现出边框宽度的效果,重新设置画笔宽度为1即可。这样做的好处是减少计算量,不然还需要像上面计算边框范围一样,考虑画笔带来的影响。绘制这个实心的圆角矩形,其中left、top、bottom这三个属性值是固定的。随着动画效果,进度条在增长,也就是right属性值在变化。在初始时,进度从0开始,圆角矩形的长度为0,这时right值等于left值。随着传入进度值的改变,计算所占百分比,right值开始变化,通过不断地重绘,完成进度条改变的动画效果。
float percent = (float) mPercent / 100f;
RectF progressRectF = new RectF(mPaintWidth + mProgressToFrameWidth, mPaintWidth + mProgressToFrameWidth, (mPaintWidth + mProgressToFrameWidth) + percent * (mProgressbarWidth - 2 * mPaintWidth - 2 * mProgressToFrameWidth), mProgressbarHeight - mPaintWidth - mProgressToFrameWidth);
canvas.drawRoundRect(progressRectF, 15, 15, mPaint);
下图是对该控件各部分范围的一个直观图,为了可以更清晰的表现出来,图有些夸张,不太像进度条。其中图片边界为控件边界。红色边框外部紧贴控件边框,画笔位置为图中黑线部分。内部为填充为蓝色部分,对应画笔为图中粉色部分。
(4)定义setProgress()方法
该方法用来完成对传入进度值的动画添加及监听,并发出重绘界面的请求。这里用到了ValueAnimator类,该类用来对值做动画。在ofInt()方法中,设置初始值和结束值。为该动画添加addUpdateListener,完成对值变化的监听,getAnimatedValue()方法可以获取到实时的变化值,该值也正是我们所需要的。得到变化值后发出重绘界面的请求。
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPercent = (int)valueAnimator.getAnimatedValue();
invalidate();
}
});
注意,外部传入的进度值可能并不是以100作为最大值的(比如一个星期有7天,过了3天,相当于过了3/7天,不是3/100天),需要在这里做一个转换。
int percent = progress * 100 / maxProgress;//得出当前progress占最大进度值百分比(0-100)
if (percent < 0) {
percent = 0;
}
if (percent > 100) {
percent = 100;
}
3、测试
最后,进行简单的测试。在MainActivity中添加该控件和一个Button。单击Button调用setProgress()方法即可。
progressbar = findViewById(R.id.my_progress);
Button start_Button = findViewById(R.id.start_button);
start_Button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
progressbar.setProgress(76, 100);
}
});
三、源码
宇宙惯例,贴上源码,代码中有不妥之处欢迎大家指出。稍后也会将工程上传GitHub。
MyProgressbar.java:
package com.example.progressbar;
import android.animation.ValueAnimator;
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.RectF;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
public class MyProgressbar extends View {
private Paint mPaint;//画笔
private float mPaintWidth = 6f;//初始画笔宽度
private int mProgressbarWidth;//控件外边框宽度
private int mProgressbarHeight;//控件外边框高度
private float mProgressToFrameWidth;//内部填充进度距内边框距离
private int mPercent = 0;//已转化为0至100范围的当前进度,随动画时间改变而改变
public MyProgressbar(Context context) {
super(context);
}
@SuppressLint("Recycle")
public MyProgressbar(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray array = getContext().obtainStyledAttributes(attrs, R.styleable.MyProgressbar);
mProgressbarWidth = (int)array.getDimension(R.styleable.MyProgressbar_progressbar_width, 100);
mProgressbarHeight = (int)array.getDimension(R.styleable.MyProgressbar_progressbar_height, 10);
mProgressToFrameWidth = array.getDimension(R.styleable.MyProgressbar_progressbar_ProgressToFrameWidth, 6f);
}
public MyProgressbar(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(mProgressbarWidth, mProgressbarHeight);
}
@Override
@SuppressLint("DrawAllocation")
protected void onDraw(Canvas canvas) {
//绘制进度条外边框
initPaint();
RectF frameRectF = new RectF(mPaintWidth / 2, mPaintWidth / 2, mProgressbarWidth - mPaintWidth / 2, mProgressbarHeight - mPaintWidth / 2);
canvas.drawRoundRect(frameRectF, 15, 15, mPaint);
//填充内部进度
mPaint.setPathEffect(null);
mPaint.setColor(Color.BLUE);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setAntiAlias(true);
//内部进度填充长度,随动画时间改变而改变
float percent = (float) mPercent / 100f;
RectF progressRectF = new RectF(mPaintWidth + mProgressToFrameWidth, mPaintWidth + mProgressToFrameWidth, (mPaintWidth + mProgressToFrameWidth) + percent * (mProgressbarWidth - 2 * mPaintWidth - 2 * mProgressToFrameWidth), mProgressbarHeight - mPaintWidth - mProgressToFrameWidth);
canvas.drawRoundRect(progressRectF, 15, 15, mPaint);
}
private void initPaint(){
mPaint = new Paint();
mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(mPaintWidth);
}
public void setProgress(int progress, int maxProgress) {
int percent = progress * 100 / maxProgress;//得出当前progress占最大进度值百分比(0-100)
if (percent < 0) {
percent = 0;
}
if (percent > 100) {
percent = 100;
}
ValueAnimator animator = ValueAnimator.ofInt(0, percent);
animator.setDuration(1000);
animator.setInterpolator(new AccelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
mPercent = (int)valueAnimator.getAnimatedValue();
invalidate();
}
});
animator.start();
}
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">
<com.example.progressbar.MyProgressbar
android:id="@+id/my_progress"
android:layout_marginTop="50dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:progressbar_width="240dp"
app:progressbar_height="45dp"/>
<Button
android:id="@+id/start_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="50dp"
android:layout_gravity="center"
android:text="开始"/>
</LinearLayout>
MainActivity.java:
package com.example.progressbar;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
public class MainActivity extends AppCompatActivity {
private MyProgressbar progressbar;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
progressbar = findViewById(R.id.my_progress);
Button start_Button = findViewById(R.id.start_button);
start_Button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
progressbar.setProgress(76, 100);
}
});
}
}