转载请注明出处
1、前言
在我们的项目中,很多场景需要使用加载进度动画,如网络请求,数据加载等。
现在市面大多数app都有拥有自己独特风格的加载动画,而不是谷歌为我们提供的菊花圈。一个绚丽美观的加载动画可以消除用户的等待焦虑。本文主要介绍利用自定义view打造一个绚丽的加载动画。
先看效果图:
2、动画分析
从效果图中,我们可以把整个加载动画拆分成以下4个功能点:
- 画指定数目的环绕圆环
- 圆环旋转动画
- 旋转过程圆环聚拢
- 旋转过程圆环收缩
3、代码实现
3.1 自定义属性
我们需要自定义俩个属性:圆点个数dot_count
、圆点颜色dot_color
代码如下:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="ProgressView">
<attr name="dot_color" format="color"/>
<attr name="dot_count" format="integer"/>
</declare-styleable>
</resources>
3.2 获取布局文件中设置好的自定义属性
我们需要在java代码中获取在xml布局文件中设置的自定义属性:
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
ta.recycle();
}
我们的布局属性全部储存在构造器的attrs
中,通过context.obtainStyledAttributes(attrs, R.styleable.ProgressView)
方法即可获取到设置的自定义属性,记得获取完成后调用recycle()
回收资源.
3、 初始化
在我们的自定义view ProgressView
的构造器中进行初始化工作。
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.FILL_AND_STROKE);
mPaint.setColor(mDotColor);
// 屏幕适配,转化圆环半径,小点半径
mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
mOriginalDotRadius = mDotRadius;
初始化画笔,设置颜色,抗锯齿,通过setStyle(Style.FILL_AND_STROKE)
设置画笔实心。
设置小圆离中心的距离mRingRadius
,小圆半径mDotRadius
,因为动画工程中小圆半径会变化,所以用一个变量mOriginalDotRadius
来保存小圆半径的初始值(用来计算变化中的小圆半径)。
其中DensityUtils.dp2px()
方法是根据屏幕像素密度将像dp值转化为像素。
具体代码:
public static int dp2px(Context context, float dp)
{
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.getResources().getDisplayMetrics());
}
初始化动画
private void initAnimatior()
{
mAnimator = ValueAnimator.ofInt(0, 359);
mAnimator.setDuration(4000);
mAnimator.setRepeatCount(-1);
mAnimator.setRepeatMode(ValueAnimator.INFINITE);
mAnimator.setInterpolator(new LinearInterpolator());
}
我们用ValueAnimator
来动态计算当前旋转角度mCurrentAngle
,变化值从0到359更换,其他设置都很简单,比如设置动画时间,重复次数-1(表示无限循环),注意这行代码mAnimator.setInterpolator(new LinearInterpolator())
,设置动画差值器为线性匀速,这个值改变后会改变动画效果。
mAnimator.addUpdateListener(new AnimatorUpdateListener()
{
@Override
public void mAnimator(ValueAnimator animation)
{
mCurrentAngle = (int) animation.getAnimatedValue();
invalidate();
}
});
给我们的mAnimator设置监听,在mAnimator()
方法中将当前计算出来的值赋值给mCurrentAngle,再调用invalidate()
重绘页面,此时view会执行ondraw方法,我们这个动画的原理就是,动态更改mCurrentAngle
的值,不断重绘,稍后讲解怎么根据mCurrentAngle
绘图。
4、 重新调整小球到中心点得距离
一个好的自定义view必须提供完美的兼容性,有时候,我们可能在布局文件了设置了view的大小,如果view的长宽小于我们代码设值得小圆点离中心点得距离mRingRadius
的俩倍,小球将会绘制在视图之外,导致看不到。所以我们在onLayout
方法中调整mRingRadius
,这里计算宽高需要扣除内边距。
因为在view的绘制流程onLayout()
中,可以获取到view的实际宽高,所以我们把调整代码放在这里,以下是具体代码:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
super.onLayout(changed, left, top, right, bottom);
// 重设圆环半径,防止超出视图大小
int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;
mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
mOriginalRingRadius = mRingRadius;
}
5、绘制小球
5.1 小球坐标分析
假设坐标轴y轴向上方向为0度,小球的当前角度angle
可以计算出小球所在的坐标,计算方法如下图:
在我们这,圆点坐标是view的中心点,即宽高的一半。
private void drawDot(Canvas canvas, double angle)
{
//根据当前角度获取x、y坐标点
float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));
//绘制圆
canvas.drawCircle(x, y, mDotRadius, mPaint);
}
5.2 小球缩小聚拢实现
将小球到中心的距离mRingRadius
缩小就达到了聚拢的效果,同理缩小小球半径mDotRadius
就可以改变小球大小。我们根据当前旋转角度mCurrentAngle
进行变化。
为了方便计算,我们封装一个估值器方法:
private Integer evaluate(float fraction, Integer startValue, Integer endValue)
{
int startInt = startValue;
return (int) (startInt + fraction * (endValue - startInt));
}
这个方法在安卓动画计算中很常用,实现还是很简单的,传入三个参数,含义如下:
- fraction:估值器的值,大小从0-1变化,控制我们最终值变化的变量
- startValue:起始值,当fraction为0时计算得出的值
- endValue:最终值,当fraction为1时计算得出的值
下面讲解如何通过该方法mCurrentAngle
计算mRingRadius
:
我们需要一个fraction变量来控制mRingRadius
的最终值,前面说了,变量是当前旋转的角度mCurrentAngle
。那么如何一个0-360的mCurrentAngle
将转为为一个0-1的值呢?
俩行代码搞定:
float fraction = 1.0f * mCurrentAngle / 180 - 1;
fraction = Math.abs(fraction);
这样,当mCurrentAngle
从0到180度变化时,fraction
从1到0,随着mCurrentAngle
从180变化到360时fraction
继续从1变化到0,如此循环往复。得到了我们估值器的估值变量fraction
。
我们mRingRadius
的变化是从原始值减小到一半,mDotRadius
的变化是从原始值减小到4/5。
我们打印下mRingRadius
变化情况。
6、完整代码
public class ProgressView extends View
{
private int mDotCount = 5; // 圆点个数
private int mDotColor = 0xFFFF9966;// 圆点颜色
private Paint mPaint;
private int mRingRadius = 50;// 圆环半径,单位dp
private int mOriginalRingRadius;// 保存的原始圆环半径,单位dp
private int mDotRadius = 7; // 小点半径,单位dp
private int mOriginalDotRadius; // 保存的原始小点半径,单位dp
private int mCurrentAngle = 0; // 当前旋转的角度
private ValueAnimator mAnimator;// 旋转动画
public ProgressView(Context context, AttributeSet attrs, int defStyleAttr)
{
super(context, attrs, defStyleAttr);
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ProgressView);
mDotColor = ta.getColor(R.styleable.ProgressView_dot_color, mDotColor);
mDotCount = ta.getInt(R.styleable.ProgressView_dot_count, mDotCount);
ta.recycle();
init();
}
public ProgressView(Context context, AttributeSet attrs)
{
this(context, attrs, 0);
}
public ProgressView(Context context)
{
this(context, null);
}
private void init()
{
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.FILL_AND_STROKE);
mPaint.setColor(mDotColor);
// 屏幕适配,转化圆环半径,小点半径
mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
mOriginalDotRadius = mDotRadius;
initAnimatior();
}
private void initAnimatior()
{
mAnimator = ValueAnimator.ofInt(0, 359);
mAnimator.setDuration(4000);
mAnimator.setRepeatCount(-1);
mAnimator.setRepeatMode(ValueAnimator.INFINITE);
mAnimator.setInterpolator(new LinearInterpolator());
mAnimator.addUpdateListener(new AnimatorUpdateListener()
{
@Override
public void onAnimationUpdate(ValueAnimator animation)
{
mCurrentAngle = (int) animation.getAnimatedValue();
invalidate();
}
});
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
super.onLayout(changed, left, top, right, bottom);
// 重设圆环半径,防止超出视图大小
int effectiveWidth = getWidth() - getPaddingLeft() - getPaddingRight();
int effectiveHeight = getHeight() - getPaddingBottom() - getPaddingTop();
int maxRadius = Math.min(effectiveWidth / 2, effectiveHeight / 2) - mDotRadius;
mRingRadius = mRingRadius > maxRadius ? maxRadius : mRingRadius;
mOriginalRingRadius = mRingRadius;
}
@Override
protected void onDraw(Canvas canvas)
{
// 根据小球总数平均分配整个圆,得到每个小球的间隔角度
double cellAngle = 360 / mDotCount;
for (int i = 0; i < mDotCount; i++)
{
double ange = i * cellAngle + mCurrentAngle;
// 根据当前角度计算小球到圆心的距离
calculateRadiusFromProgress();
// 根据角度绘制单个小球
drawDot(canvas, ange * 2 * Math.PI / 360);
}
}
/**
* 根据当前旋转角度计算mRingRadius、mDotRadius的值
* mCurrentAngle: 0 - 180 - 360
* mRingRadius: 最小 - 最大 - 最小
* @author 漆可
* @date 2016-6-17 下午3:04:35
*/
private void calculateRadiusFromProgress()
{
float fraction = 1.0f * mCurrentAngle / 180 - 1;
fraction = Math.abs(fraction);
mRingRadius = evaluate(fraction, mOriginalRingRadius, mOriginalRingRadius * 2 / 4);
mDotRadius = evaluate(fraction, mOriginalDotRadius, mOriginalDotRadius * 4 / 5);
}
// fraction:当前的估值器计算值,startValue:起始值,endValue:终点值
private Integer evaluate(float fraction, Integer startValue, Integer endValue)
{
return (int) (startValue + fraction * (endValue - startValue));
}
@Override
protected void onAttachedToWindow()
{
super.onAttachedToWindow();
startAnimation();
}
private void drawDot(Canvas canvas, double angle)
{
// 根据当前角度获取x、y坐标点
float x = (float) (getWidth() / 2 + mRingRadius * Math.sin(angle));
float y = (float) (getHeight() / 2 - mRingRadius * Math.cos(angle));
// 绘制圆
canvas.drawCircle(x, y, mDotRadius, mPaint);
}
public void startAnimation()
{
mAnimator.start();
}
public void stopAnimation()
{
mAnimator.end();
}
//销毁页面时停止动画
@Override
protected void onDetachedFromWindow()
{
super.onDetachedFromWindow();
stopAnimation();
}
}
最后,奉上demo下载地址:http://download.csdn.net/detail/q649381130/9552643
网友评论