1分析小紅書(shū)的效果
首先看一下這個(gè)效果
可能看著有一些卡頓,這是由于上傳gif大小有限制遂鹊,壓縮過(guò)度造成的卡頓桥胞。實(shí)際上是很流暢的野舶。
要實(shí)現(xiàn)一些效果槐雾,我們首先要分析這個(gè)效果的組成部分。就像我們平時(shí)寫(xiě)程序是一樣的若河,一個(gè)模塊的整體功能是由若干個(gè)小功能構(gòu)成的能岩。只要分析出了這個(gè)動(dòng)畫(huà)的組成部分接下來(lái)就好做了。
1)首先我們可以看見(jiàn)這個(gè)動(dòng)畫(huà)展開(kāi)以后萧福,中間有一個(gè)小圓的大小是不變的拉鹃,這個(gè)比較簡(jiǎn)單直接畫(huà)出來(lái)就行了。
2)展開(kāi)以后會(huì)有一個(gè)漸變的圓鲫忍,圓起始半徑和中間的小圓一樣膏燕,這個(gè)圓半徑變大的同時(shí)透明度也在不斷的變化。并且這個(gè)動(dòng)畫(huà)是不斷的循環(huán)的悟民。這個(gè)動(dòng)畫(huà)可以用屬性動(dòng)畫(huà)坝辫,使圓的半徑不斷的變化,然后不斷的重繪射亏。
3)還有就是路徑的動(dòng)畫(huà)近忙,路徑是不斷變化的竭业,如果有什么方法可以實(shí)時(shí)改變路徑長(zhǎng)度就好了?這時(shí)候就要用到我們上一篇博客中提到的PathMeasure银锻。
4)在動(dòng)畫(huà)結(jié)束的時(shí)候?qū)⑽淖诛@示出來(lái)永品。
分析了整個(gè)效果的組成,那么接下來(lái)就開(kāi)始實(shí)現(xiàn)吧击纬!
2.實(shí)現(xiàn)
- 畫(huà)筆路徑的初始化
/**
* 中心圓的畫(huà)筆
*/
private Paint mPaint;
/**
* 漸變圓的畫(huà)筆
*/
private Paint mPaint1;
/**
* 路徑的畫(huà)筆
*/
private Paint mPaint2;
/**
* 文字的畫(huà)筆
*/
private Paint mPaintText;
/**
* 初始化路徑,這個(gè)路徑?jīng)]有真正的繪制
*/
private Path mInitPath;
/**
* 用getSegment獲取的路徑片段
*/
private Path mPath;
public ReadBook1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint1.setStyle(Paint.Style.FILL);
mPaint1.setColor(Color.WHITE);
mPaint3 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint3.setStyle(Paint.Style.STROKE);
mPaint3.setColor(Color.WHITE);
mPaint3.setStrokeWidth(4);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStyle(Paint.Style.STROKE);
mPaintText.setColor(Color.WHITE);
mPaintText.setAlpha(0);
mPaintText.setTextAlign(Paint.Align.CENTER);
mInitPath = new Path();
mPath = new Path();
}
畫(huà)筆的初始化千萬(wàn)不要放到onDraw()方法中進(jìn)行钾麸,因?yàn)閛nDraw方法會(huì)被調(diào)用很多次更振,會(huì)new出大量不必要的對(duì)象。
接下來(lái)是繪制中間的圓
canvas.drawCircle(mWidth / 2, mHeight / 2, 10, mPaint1);
- 繪制漸變的圓
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mPaint);
public void startCircleAnim() {
mAnimatorCircle = ValueAnimator.ofFloat(10, 50);
final float per = 255f / 40f;
mAnimatorCircle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
mRadius = (int) animatedValue;
int a = (int) ((40 - (animatedValue - 10)) * per);
mPaint.setAlpha(a);
invalidate();
}
});
mAnimatorCircle.setRepeatMode(ValueAnimator.RESTART);
mAnimatorCircle.setRepeatCount(ValueAnimator.INFINITE);
mAnimatorCircle.setDuration(3000);
mAnimatorCircle.start();
}
我這里講漸變半徑設(shè)置為10-50饭尝, final float per = 255f / 40f;同時(shí)根據(jù)半徑的大小設(shè)置透明度肯腕。
- 繪制路徑
首先要初始化一個(gè)路徑,來(lái)給PathMeasure測(cè)量钥平,就好像指定一條路一樣实撒,
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
mInitPath.moveTo(mWidth / 2, mHeight / 2);
mInitPath.lineTo(mWidth / 2 + 100, mHeight / 2 - 100);
mInitPath.lineTo(mWidth / 2 + 300, mHeight / 2 - 100);
pathAnimStartOrStop();
}
然后用PathMeasure中的方法進(jìn)行測(cè)量路徑的長(zhǎng)短,
mPathMeasure = new PathMeasure(mInitPath, false);
float length = mPathMeasure.getLength();
將測(cè)量得到的值交給屬性動(dòng)畫(huà)涉瘾,需要判斷展開(kāi)還是知态,收縮
ValueAnimator animator;
if (flag) {
animator = ValueAnimator.ofFloat(length, 0);
} else
animator = ValueAnimator.ofFloat(0, length);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedValue = (Float) animation.getAnimatedValue();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
//動(dòng)畫(huà)展開(kāi)式時(shí)候
if (!flag) {
mPaint.setAlpha(255);
mPaint1.setAlpha(255);
startCircleAnim();
System.out.println("動(dòng)畫(huà)展開(kāi)的時(shí)候");
}
ReadBook1.this.setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
if (flag) {
mPaintText.setAlpha(0);
mPaint.setAlpha(0);
mPaint1.setAlpha(0);
System.out.println("動(dòng)畫(huà)關(guān)閉的時(shí)候");
mAnimatorCircle.cancel();
} else {
System.out.println("動(dòng)畫(huà)展開(kāi)的時(shí)候");
mPaintText.setAlpha(255);
}
flag = !flag;
ReadBook1.this.setEnabled(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.setDuration(3000);
animator.start();
最后在onDraw方法中調(diào)用getSegment方法,得到實(shí)時(shí)路徑的長(zhǎng)短立叛,然后繪制
mPath.reset();
// 硬件加速的BUG负敏,需要設(shè)置到0,0
mPath.lineTo(0, 0);
float stop = animatedValue;
float start = 0;
mPathMeasure.getSegment(start, stop, mPath, true);
canvas.drawPath(mPath, mPaint2);
這里面比較關(guān)鍵的地方就是float length = mPathMeasure.getLength();測(cè)量得到路徑的長(zhǎng)短,然后根據(jù)屬性動(dòng)畫(huà)動(dòng)態(tài)改變這個(gè)值秘蛇,最后調(diào)用 mPathMeasure.getSegment(start, stop, mPath, true);這個(gè)方法其做,得到實(shí)時(shí)的路徑長(zhǎng)短。
總結(jié):
1)需要判斷動(dòng)畫(huà)是展開(kāi)還是收縮赁还,根據(jù)這個(gè)設(shè)置屬性動(dòng)畫(huà)的值妖泄,
2)監(jiān)聽(tīng)動(dòng)畫(huà)的開(kāi)始和結(jié)束,根據(jù)這個(gè)顯示和隱藏中間的圓
3)動(dòng)畫(huà)展開(kāi)結(jié)束時(shí)顯示文字艘策。
最后不斷循環(huán)的動(dòng)畫(huà)會(huì)造成內(nèi)存泄露蹈胡,作為一個(gè)嚴(yán)謹(jǐn)?shù)某绦騿T千萬(wàn)不能忘記。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAnimatorCircle != null)
mAnimatorCircle.cancel();
}
全部代碼如下
public class ReadBook1 extends View {
/**
* 中心圓的畫(huà)筆
*/
private Paint mPaint;
/**
* 漸變圓的畫(huà)筆
*/
private Paint mPaint1;
/**
* 路徑的畫(huà)筆
*/
private Paint mPaint2;
/**
* 文字的畫(huà)筆
*/
private Paint mPaintText;
/**
* 初始化路徑柬焕,這個(gè)路徑?jīng)]有真正的繪制
*/
private Path mInitPath;
/**
* 用getSegment獲取的路徑片段
*/
private Path mPath;
/**
* 中心圓的半徑
*/
private int mRadius = 10;
/**
* 圓漸變的動(dòng)畫(huà)
*/
private ValueAnimator mAnimatorCircle;
/**
* 測(cè)量路徑的類
*/
private PathMeasure mPathMeasure;
private Float animatedValue = 0f;
private int mWidth;
private int mHeight;
//判斷是展開(kāi)是回收
private boolean flag;
public void setFlag(boolean flag) {
this.flag = flag;
}
public ReadBook1(Context context) {
this(context, null);
}
public ReadBook1(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ReadBook1(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.FILL);
mPaint.setColor(Color.WHITE);
mPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint1.setStyle(Paint.Style.FILL);
mPaint1.setColor(Color.WHITE);
mPaint2 = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint2.setStyle(Paint.Style.STROKE);
mPaint2.setColor(Color.WHITE);
mPaint2.setStrokeWidth(4);
mPaintText = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaintText.setStyle(Paint.Style.STROKE);
mPaintText.setColor(Color.WHITE);
mPaintText.setAlpha(0);
mPaintText.setTextAlign(Paint.Align.CENTER);
mInitPath = new Path();
mPath = new Path();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
mWidth = getMeasuredWidth();
mHeight = getMeasuredHeight();
mInitPath.moveTo(mWidth / 2, mHeight / 2);
mInitPath.lineTo(mWidth / 2 + 100, mHeight / 2 - 100);
mInitPath.lineTo(mWidth / 2 + 300, mHeight / 2 - 100);
pathAnimStartOrStop();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(mWidth / 2, mHeight / 2, 10, mPaint1);
canvas.drawCircle(mWidth / 2, mHeight / 2, mRadius, mPaint);
mPath.reset();
// 硬件加速的BUG审残,需要設(shè)置到0,0
mPath.lineTo(0, 0);
float stop = animatedValue;
float start = 0;
mPathMeasure.getSegment(start, stop, mPath, true);
canvas.drawPath(mPath, mPaint2);
canvas.drawText("Hello World", mWidth / 2 + 200, mHeight / 2 - 104, mPaintText);
}
public void startCircleAnim() {
mAnimatorCircle = ValueAnimator.ofFloat(10, 50);
final float per = 255f / 40;
mAnimatorCircle.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
mRadius = (int) animatedValue;
int a = (int) ((40 - (animatedValue - 10)) * per);
mPaint.setAlpha(a);
invalidate();
}
});
mAnimatorCircle.setRepeatMode(ValueAnimator.RESTART);
mAnimatorCircle.setRepeatCount(ValueAnimator.INFINITE);
mAnimatorCircle.setDuration(3000);
mAnimatorCircle.start();
}
public void pathAnimStartOrStop() {
mPathMeasure = new PathMeasure(mInitPath, false);
float length = mPathMeasure.getLength();
ValueAnimator animator;
if (flag) {
animator = ValueAnimator.ofFloat(length, 0);
} else
animator = ValueAnimator.ofFloat(0, length);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
animatedValue = (Float) animation.getAnimatedValue();
}
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
//動(dòng)畫(huà)展開(kāi)式時(shí)候
if (!flag) {
mPaint.setAlpha(255);
mPaint1.setAlpha(255);
startCircleAnim();
System.out.println("動(dòng)畫(huà)展開(kāi)的時(shí)候");
}
ReadBook1.this.setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
if (flag) {
mPaintText.setAlpha(0);
mPaint.setAlpha(0);
mPaint1.setAlpha(0);
System.out.println("動(dòng)畫(huà)關(guān)閉的時(shí)候");
mAnimatorCircle.cancel();
} else {
System.out.println("動(dòng)畫(huà)展開(kāi)的時(shí)候");
mPaintText.setAlpha(255);
}
flag = !flag;
ReadBook1.this.setEnabled(true);
}
@Override
public void onAnimationCancel(Animator animation) {
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.setDuration(3000);
animator.start();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAnimatorCircle != null)
mAnimatorCircle.cancel();
}
}