轉(zhuǎn)載請(qǐng)注明出處
1、前言
在我們的項(xiàng)目中涮俄,很多場(chǎng)景需要使用加載進(jìn)度動(dòng)畫蛉拙,如網(wǎng)絡(luò)請(qǐng)求,數(shù)據(jù)加載等彻亲。
現(xiàn)在市面大多數(shù)app都有擁有自己獨(dú)特風(fēng)格的加載動(dòng)畫孕锄,而不是谷歌為我們提供的菊花圈。一個(gè)絢麗美觀的加載動(dòng)畫可以消除用戶的等待焦慮苞尝。本文主要介紹利用自定義view打造一個(gè)絢麗的加載動(dòng)畫畸肆。
先看效果圖:
2、動(dòng)畫分析
從效果圖中宙址,我們可以把整個(gè)加載動(dòng)畫拆分成以下4個(gè)功能點(diǎn):
- 畫指定數(shù)目的環(huán)繞圓環(huán)
- 圓環(huán)旋轉(zhuǎn)動(dòng)畫
- 旋轉(zhuǎn)過(guò)程圓環(huán)聚攏
- 旋轉(zhuǎn)過(guò)程圓環(huán)收縮
3轴脐、代碼實(shí)現(xiàn)
3.1 自定義屬性
我們需要自定義倆個(gè)屬性:圓點(diǎn)個(gè)數(shù)dot_count
、圓點(diǎn)顏色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 獲取布局文件中設(shè)置好的自定義屬性
我們需要在java代碼中獲取在xml布局文件中設(shè)置的自定義屬性:
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();
}
我們的布局屬性全部?jī)?chǔ)存在構(gòu)造器的attrs
中抡砂,通過(guò)context.obtainStyledAttributes(attrs, R.styleable.ProgressView)
方法即可獲取到設(shè)置的自定義屬性大咱,記得獲取完成后調(diào)用recycle()
回收資源.
3、 初始化
在我們的自定義view ProgressView
的構(gòu)造器中進(jìn)行初始化工作注益。
mPaint = new Paint();
mPaint.setAntiAlias(true);
mPaint.setStyle(Style.FILL_AND_STROKE);
mPaint.setColor(mDotColor);
// 屏幕適配碴巾,轉(zhuǎn)化圓環(huán)半徑,小點(diǎn)半徑
mRingRadius = DensityUtils.dp2px(getContext(), mRingRadius);
mDotRadius = DensityUtils.dp2px(getContext(), mDotRadius);
mOriginalDotRadius = mDotRadius;
初始化畫筆丑搔,設(shè)置顏色厦瓢,抗鋸齒,通過(guò)setStyle(Style.FILL_AND_STROKE)
設(shè)置畫筆實(shí)心啤月。
設(shè)置小圓離中心的距離mRingRadius
煮仇,小圓半徑mDotRadius
,因?yàn)閯?dòng)畫工程中小圓半徑會(huì)變化顽冶,所以用一個(gè)變量mOriginalDotRadius
來(lái)保存小圓半徑的初始值(用來(lái)計(jì)算變化中的小圓半徑)欺抗。
其中DensityUtils.dp2px()
方法是根據(jù)屏幕像素密度將像dp值轉(zhuǎn)化為像素。
具體代碼:
public static int dp2px(Context context, float dp)
{
return (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp,
context.getResources().getDisplayMetrics());
}
初始化動(dòng)畫
private void initAnimatior()
{
mAnimator = ValueAnimator.ofInt(0, 359);
mAnimator.setDuration(4000);
mAnimator.setRepeatCount(-1);
mAnimator.setRepeatMode(ValueAnimator.INFINITE);
mAnimator.setInterpolator(new LinearInterpolator());
}
我們用ValueAnimator
來(lái)動(dòng)態(tài)計(jì)算當(dāng)前旋轉(zhuǎn)角度mCurrentAngle
强重,變化值從0到359更換竖共,其他設(shè)置都很簡(jiǎn)單,比如設(shè)置動(dòng)畫時(shí)間跌穗,重復(fù)次數(shù)-1(表示無(wú)限循環(huán))偷霉,注意這行代碼mAnimator.setInterpolator(new LinearInterpolator())
,設(shè)置動(dòng)畫差值器為線性勻速倘要,這個(gè)值改變后會(huì)改變動(dòng)畫效果圾亏。
mAnimator.addUpdateListener(new AnimatorUpdateListener()
{
@Override
public void mAnimator(ValueAnimator animation)
{
mCurrentAngle = (int) animation.getAnimatedValue();
invalidate();
}
});
給我們的mAnimator設(shè)置監(jiān)聽(tīng)十拣,在mAnimator()
方法中將當(dāng)前計(jì)算出來(lái)的值賦值給mCurrentAngle,再調(diào)用invalidate()
重繪頁(yè)面志鹃,此時(shí)view會(huì)執(zhí)行ondraw方法夭问,我們這個(gè)動(dòng)畫的原理就是,動(dòng)態(tài)更改mCurrentAngle
的值曹铃,不斷重繪缰趋,稍后講解怎么根據(jù)mCurrentAngle
繪圖。
4陕见、 重新調(diào)整小球到中心點(diǎn)得距離
一個(gè)好的自定義view必須提供完美的兼容性秘血,有時(shí)候,我們可能在布局文件了設(shè)置了view的大小评甜,如果view的長(zhǎng)寬小于我們代碼設(shè)值得小圓點(diǎn)離中心點(diǎn)得距離mRingRadius
的倆倍灰粮,小球?qū)?huì)繪制在視圖之外,導(dǎo)致看不到忍坷。所以我們?cè)?code>onLayout方法中調(diào)整mRingRadius
粘舟,這里計(jì)算寬高需要扣除內(nèi)邊距。
因?yàn)樵趘iew的繪制流程onLayout()
中佩研,可以獲取到view的實(shí)際寬高蓖乘,所以我們把調(diào)整代碼放在這里,以下是具體代碼:
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom)
{
super.onLayout(changed, left, top, right, bottom);
// 重設(shè)圓環(huán)半徑韧骗,防止超出視圖大小
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 小球坐標(biāo)分析
假設(shè)坐標(biāo)軸y軸向上方向?yàn)?度,小球的當(dāng)前角度angle
可以計(jì)算出小球所在的坐標(biāo)袍暴,計(jì)算方法如下圖:
在我們這些侍,圓點(diǎn)坐標(biāo)是view的中心點(diǎn),即寬高的一半政模。
private void drawDot(Canvas canvas, double angle)
{
//根據(jù)當(dāng)前角度獲取x岗宣、y坐標(biāo)點(diǎn)
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 小球縮小聚攏實(shí)現(xiàn)
將小球到中心的距離mRingRadius
縮小就達(dá)到了聚攏的效果,同理縮小小球半徑mDotRadius
就可以改變小球大小淋样。我們根據(jù)當(dāng)前旋轉(zhuǎn)角度mCurrentAngle
進(jìn)行變化耗式。
為了方便計(jì)算,我們封裝一個(gè)估值器方法:
private Integer evaluate(float fraction, Integer startValue, Integer endValue)
{
int startInt = startValue;
return (int) (startInt + fraction * (endValue - startInt));
}
這個(gè)方法在安卓動(dòng)畫計(jì)算中很常用趁猴,實(shí)現(xiàn)還是很簡(jiǎn)單的刊咳,傳入三個(gè)參數(shù),含義如下:
- fraction:估值器的值儡司,大小從0-1變化娱挨,控制我們最終值變化的變量
- startValue:起始值,當(dāng)fraction為0時(shí)計(jì)算得出的值
- endValue:最終值捕犬,當(dāng)fraction為1時(shí)計(jì)算得出的值
下面講解如何通過(guò)該方法mCurrentAngle
計(jì)算mRingRadius
:
我們需要一個(gè)fraction變量來(lái)控制mRingRadius
的最終值跷坝,前面說(shuō)了酵镜,變量是當(dāng)前旋轉(zhuǎn)的角度mCurrentAngle
。那么如何一個(gè)0-360的mCurrentAngle
將轉(zhuǎn)為為一個(gè)0-1的值呢柴钻?
倆行代碼搞定:
float fraction = 1.0f * mCurrentAngle / 180 - 1;
fraction = Math.abs(fraction);
這樣淮韭,當(dāng)mCurrentAngle
從0到180度變化時(shí),fraction
從1到0贴届,隨著mCurrentAngle
從180變化到360時(shí)fraction
繼續(xù)從1變化到0缸濒,如此循環(huán)往復(fù)。得到了我們估值器的估值變量fraction
粱腻。
我們mRingRadius
的變化是從原始值減小到一半,mDotRadius
的變化是從原始值減小到4/5斩跌。
我們打印下mRingRadius
變化情況绍些。
6、完整代碼
public class ProgressView extends View
{
private int mDotCount = 5; // 圓點(diǎn)個(gè)數(shù)
private int mDotColor = 0xFFFF9966;// 圓點(diǎn)顏色
private Paint mPaint;
private int mRingRadius = 50;// 圓環(huán)半徑耀鸦,單位dp
private int mOriginalRingRadius;// 保存的原始圓環(huán)半徑柬批,單位dp
private int mDotRadius = 7; // 小點(diǎn)半徑,單位dp
private int mOriginalDotRadius; // 保存的原始小點(diǎn)半徑袖订,單位dp
private int mCurrentAngle = 0; // 當(dāng)前旋轉(zhuǎn)的角度
private ValueAnimator mAnimator;// 旋轉(zhuǎn)動(dòng)畫
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);
// 屏幕適配氮帐,轉(zhuǎn)化圓環(huán)半徑,小點(diǎn)半徑
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);
// 重設(shè)圓環(huán)半徑洛姑,防止超出視圖大小
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)
{
// 根據(jù)小球總數(shù)平均分配整個(gè)圓上沐,得到每個(gè)小球的間隔角度
double cellAngle = 360 / mDotCount;
for (int i = 0; i < mDotCount; i++)
{
double ange = i * cellAngle + mCurrentAngle;
// 根據(jù)當(dāng)前角度計(jì)算小球到圓心的距離
calculateRadiusFromProgress();
// 根據(jù)角度繪制單個(gè)小球
drawDot(canvas, ange * 2 * Math.PI / 360);
}
}
/**
* 根據(jù)當(dāng)前旋轉(zhuǎn)角度計(jì)算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:當(dāng)前的估值器計(jì)算值,startValue:起始值,endValue:終點(diǎn)值
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)
{
// 根據(jù)當(dāng)前角度獲取x楞艾、y坐標(biāo)點(diǎn)
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();
}
//銷毀頁(yè)面時(shí)停止動(dòng)畫
@Override
protected void onDetachedFromWindow()
{
super.onDetachedFromWindow();
stopAnimation();
}
}
最后参咙,奉上demo下載地址:http://download.csdn.net/detail/q649381130/9552643