最近在爬坑自定义View,看到狗东支付时有一个支付成功后的动画效果。
遂决定自己也撸一个,加入了自己的一些想法,把实现的思路分享一下。
特点:
- 加载的view元素颜色支持自定义
- 加载成功和加载失败会有一个动画效果
源码已上传GitHub PowerfulLoadingView ,欢迎交流。
先上效果图
自定义属性
新建 res/values/attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="PowerfulLoadingView">
<!--加载视图背景的颜色-->
<attr name="bg_color" format="color"/>
<!--加载条的颜色-->
<attr name="loading_bar_color" format="color"/>
<!--钩和叉的颜色-->
<attr name="tick_cross_color" format="color"/>
</declare-styleable>
</resources>
老规矩,构造方法三连击。然后进行自定义属性值的读取。如果未指定自定义属性,则使用默认值。
这里new了两个画笔,一个用于画线和圆弧,一个用于画实心圆
private static final int DEFAULT_CONTENT_COLOR = Color.WHITE;
private static final int DEFAULT_LOADING_BAR_COLOR = Color.rgb(65, 105, 225);
private static final int DEFAULT_BG_COLOR = Color.argb(55, 0, 0, 0);
private int mLoadingBarColor = DEFAULT_LOADING_BAR_COLOR;
private int mLoadingBgColor = DEFAULT_BG_COLOR;
private int mTickOrCrossColor = DEFAULT_CONTENT_COLOR;
public PowerfulLoadingView(Context context) {
super(context);
init(context, null);
}
public PowerfulLoadingView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public PowerfulLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
private void init(Context context, @Nullable AttributeSet attrs) {
mContext = context;
//用于画实心圆
mPaintFill = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintFill.setStyle(Paint.Style.FILL);
//用于画线和圆弧
mPaintStroke = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintStroke.setStyle(Paint.Style.STROKE);
mPaintStroke.setStrokeWidth(STROKE_WIDTH);
if (attrs != null) {
TypedArray array = mContext.obtainStyledAttributes(attrs, R.styleable.PowerfulLoadingView);
for (int i = 0; i < array.getIndexCount(); i++) {
int attr = array.getIndex(i);
switch (attr) {
case R.styleable.PowerfulLoadingView_bg_color:
mLoadingBgColor = array.getColor(attr, DEFAULT_BG_COLOR);
break;
case R.styleable.PowerfulLoadingView_loading_bar_color:
mLoadingBarColor = array.getColor(attr, DEFAULT_LOADING_BAR_COLOR);
break;
case R.styleable.PowerfulLoadingView_tick_cross_color:
mTickOrCrossColor = array.getColor(attr, DEFAULT_CONTENT_COLOR);
break;
}
}
array.recycle();
}
}
加载中效果实现
看下加载效果,是一条圆弧在围绕中心转动。这里采用 ValueAnimator 来实现。
定义一个0~360的范围,表示角度360度变化。在 setDuration 设置的时间内,AnimatedValue 会从0递增到360,值每变化一次,都会调用 onDraw 方法进行重绘。每次进入onDraw方法,都会通过 drawCircle 先绘制一个圆形的背景,然后通过 getAnimatedValue 方法获取当前的值(表示角度值),然后通过 drawArc 方法绘制圆弧。只需要改变绘制圆弧时的传入的角度参数,就可以实现旋转的效果。
private ValueAnimator mCircleAngleAnimator;
public void startLoading() {
clearAllAnimator();
//初始化加载条动画,并循环播放
mCircleAngleAnimator = ValueAnimator.ofFloat(0, 360);
mCircleAngleAnimator.setDuration(ANIMATOR_TIME);
mCircleAngleAnimator.setRepeatMode(ValueAnimator.RESTART);
mCircleAngleAnimator.setRepeatCount(ValueAnimator.INFINITE);
mCircleAngleAnimator.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制转动的加载条
if (mCircleAngleAnimator != null && mCircleAngleAnimator.isRunning()) {
drawBackground(canvas, mLoadingBgColor);
mPaintStroke.setColor(mLoadingBarColor);
mRectF.set(STROKE_WIDTH * 2, STROKE_WIDTH * 2,
getWidth() - STROKE_WIDTH * 2, getHeight() - STROKE_WIDTH * 2);
canvas.drawArc(mRectF, (float) mCircleAngleAnimator.getAnimatedValue(), 270, false, mPaintStroke);
invalidate();
}
......
}
//绘制背景
private void drawBackground(Canvas canvas, int color) {
mPaintFill.setColor(color);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f, getWidth() / 2f, mPaintFill);
}
加载成功和加载失败效果实现
确定钩和叉的大小和位置
绘制加载完成的动画之前,需要先确定钩和叉的大小和位置。这一步在 onMeasure 中完成。
分析一下,可以知道,确定一个钩的大小和位置,需要确定三个点的坐标。确定一个叉的大小和位置,需要确定四个点的坐标。所以这里定义两个 Point 对象的数组来存储这些坐标信息。
这里采用view整个画布的中心点,来做为参考点,来确定这个7个点的坐标。具体可以自行调整。
private Point[] mTickPoint = new Point[3];
private Point[] mCrossPoint = new Point[4];
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
int height = getMeasuredHeight();
measureTickPosition(width / 2, height / 2);
measureCrossPosition(width / 2, height / 2);
}
//测量钩的大小和位置
private void measureTickPosition(int centerX, int centerY) {
Point position = new Point();
position.x = centerX / 2;
position.y = centerY;
mTickPoint[0] = position;
position = new Point();
position.x = centerX / 10 * 9;
position.y = centerY + centerY / 3;
mTickPoint[1] = position;
position = new Point();
position.x = centerX + centerX / 2;
position.y = centerY / 3 * 2;
mTickPoint[2] = position;
}
//测量叉的大小和位置
private void measureCrossPosition(int centerX, int centerY) {
Point position = new Point();
position.x = centerX / 3 * 2;
position.y = centerY / 3 * 2;
mCrossPoint[0] = position;
position = new Point();
position.x = centerX / 3 * 2;
position.y = centerY + centerY / 3;
mCrossPoint[1] = position;
position = new Point();
position.x = centerX + centerX / 3;
position.y = centerY + centerY / 3;
mCrossPoint[2] = position;
position = new Point();
position.x = centerX + centerX / 3;
position.y = centerY / 3 * 2;
mCrossPoint[3] = position;
}
加载成功的效果实现
向外暴露方法 loadSucceed(),来实现加载成功的入口。加载完成后,先有个以画布中心为圆心的实心圆不断缩小的过渡动画,然后同时播放钩出现的动画和放大再回弹的动画。同样采用属性动画来实现。动画的实现,跟上面的加载条基本一致,也是利用 ValueAnimator 的值不断变化,然后不断重绘,来实现动画效果。
- 绘制向圆心缩的动画,先绘制一个固定的背景实心圆,然后通过 ValueAnimator 值的变化,在背景上绘制半径不断减小的实心圆。
- 绘制一个钩,只需要根据之前保存在 mTickPoint 数组中的三个点的坐标,通过 drawLine 绘制两条线即可。然后通过ValueAnimator 值变化,改变绘制的透明度,从全透明到不透明,免得钩出现的很突兀,有一个过渡效果。
- 放大再回弹的效果。通过 ObjectAnimator 的缩放动画来完成。先放大,再缩小到圆尺寸即可。
动画完成后,如果需要进行进一步的逻辑操作,可以传入一个监听接口,当动画完成后,会回调 onAnimationEnd 方法,在该方法中进行相应操作即可。
private ValueAnimator mCircleRadiusAnimator;
private ValueAnimator mTickAnim;
private AnimatorSet mAnimatorSet = new AnimatorSet();
private ObjectAnimator mScaleAnimator;
public void loadSucceed(@Nullable Animator.AnimatorListener listener) {
clearAllAnimator();
//初始化向圆心缩小的圆的动画
mCircleRadiusAnimator = ValueAnimator.ofFloat(0, getWidth() / 2f);
mCircleRadiusAnimator.setDuration(ANIMATOR_TIME / 2);
//初始化打钩的动画,并注册监听
mTickAnim = ValueAnimator.ofInt(0, 255);
mTickAnim.setDuration(ANIMATOR_TIME / 2);
if (listener != null) {
mTickAnim.addListener(listener);
}
//放大再回弹的动画
mScaleAnimator = getScaleAnimator();
mAnimatorSet.play(mTickAnim).after(mCircleRadiusAnimator).with(mScaleAnimator);
mAnimatorSet.start();
}
//获取放大再回弹的动画
private ObjectAnimator getScaleAnimator() {
PropertyValuesHolder scaleX = PropertyValuesHolder.ofFloat(SCALE_X, 1f, 1.2f, 1f);
PropertyValuesHolder scaleY = PropertyValuesHolder.ofFloat(SCALE_Y, 1f, 1.2f, 1f);
return ObjectAnimator
.ofPropertyValuesHolder(this, scaleX, scaleY)
.setDuration(ANIMATOR_TIME / 2);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
......
//绘制向圆心缩小的圆
if (mCircleRadiusAnimator != null && mCircleRadiusAnimator.isRunning()) {
drawBackground(canvas, mLoadingBarColor);
mPaintFill.setColor(mTickOrCrossColor);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f,
getWidth() / 2f - (float) mCircleRadiusAnimator.getAnimatedValue(), mPaintFill);
invalidate();
}
//绘制钩
if (mTickAnim != null && mTickAnim.isRunning()) {
drawBackground(canvas, mLoadingBarColor);
mPaintStroke.setAlpha((int) mTickAnim.getAnimatedValue());
mPaintStroke.setColor(mTickOrCrossColor);
mPaintStroke.setStrokeCap(Paint.Cap.ROUND); //画线时,线头为圆形
canvas.drawLine(mTickPoint[0].x, mTickPoint[0].y, mTickPoint[1].x, mTickPoint[1].y, mPaintStroke);
canvas.drawLine(mTickPoint[1].x, mTickPoint[1].y, mTickPoint[2].x, mTickPoint[2].y, mPaintStroke);
invalidate();
}
......
}
加载失败的效果实现
向外暴露方法 loadFailed(),来实现加载失败的入口。大部分的实现和上面的一致。唯一的区别就是绘制叉。绘制叉也很简单,根据保存在数组 mCrossPoint 中的四个点的坐标,通过 drawLine 绘制出两条交叉的线即可完成。
private ValueAnimator mCircleRadiusAnimator;
private ValueAnimator mCrossAnim;
private AnimatorSet mAnimatorSet = new AnimatorSet();
private ObjectAnimator mScaleAnimator;
public void loadFailed(@Nullable Animator.AnimatorListener listener) {
clearAllAnimator();
//初始化向圆心缩小的圆的动画
mCircleRadiusAnimator = ValueAnimator.ofFloat(0, getWidth() / 2f);
mCircleRadiusAnimator.setDuration(ANIMATOR_TIME / 2);
//初始化打叉的动画,并注册监听
mCrossAnim = ValueAnimator.ofInt(0, 255);
mCrossAnim.setDuration(ANIMATOR_TIME / 2);
if (listener != null) {
mCrossAnim.addListener(listener);
}
//放大再回弹的动画
mScaleAnimator = getScaleAnimator();
mAnimatorSet.play(mCrossAnim).after(mCircleRadiusAnimator).with(mScaleAnimator);
mAnimatorSet.start();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制向圆心缩小的圆
if (mCircleRadiusAnimator != null && mCircleRadiusAnimator.isRunning()) {
drawBackground(canvas, mLoadingBarColor);
mPaintFill.setColor(mTickOrCrossColor);
canvas.drawCircle(getWidth() / 2f, getHeight() / 2f,
getWidth() / 2f - (float) mCircleRadiusAnimator.getAnimatedValue(), mPaintFill);
invalidate();
}
//绘制叉
if (mCrossAnim != null && mCrossAnim.isRunning()) {
drawBackground(canvas, mLoadingBarColor);
mPaintStroke.setAlpha((int) mCrossAnim.getAnimatedValue());
mPaintStroke.setColor(mTickOrCrossColor);
canvas.drawLine(mCrossPoint[0].x, mCrossPoint[0].y, mCrossPoint[2].x, mCrossPoint[2].y, mPaintStroke);
canvas.drawLine(mCrossPoint[1].x, mCrossPoint[1].y, mCrossPoint[3].x, mCrossPoint[3].y, mPaintStroke);
invalidate();
}
}
啰嗦完毕,欢迎交流指教哦。