1分析小红书的效果
首先看一下这个效果
可能看着有一些卡顿,这是由于上传gif大小有限制,压缩过度造成的卡顿。实际上是很流畅的。
要实现一些效果,我们首先要分析这个效果的组成部分。就像我们平时写程序是一样的,一个模块的整体功能是由若干个小功能构成的。只要分析出了这个动画的组成部分接下来就好做了。
1)首先我们可以看见这个动画展开以后,中间有一个小圆的大小是不变的,这个比较简单直接画出来就行了。
2)展开以后会有一个渐变的圆,圆起始半径和中间的小圆一样,这个圆半径变大的同时透明度也在不断的变化。并且这个动画是不断的循环的。这个动画可以用属性动画,使圆的半径不断的变化,然后不断的重绘。
3)还有就是路径的动画,路径是不断变化的,如果有什么方法可以实时改变路径长度就好了?这时候就要用到我们上一篇博客中提到的PathMeasure。PathMeasure详解
4)在动画结束的时候将文字显示出来。
分析了整个效果的组成,那么接下来就开始实现吧!
2.实现
- 画笔路径的初始化
/**
* 中心圆的画笔
*/
private Paint mPaint;
/**
* 渐变圆的画笔
*/
private Paint mPaint1;
/**
* 路径的画笔
*/
private Paint mPaint2;
/**
* 文字的画笔
*/
private Paint mPaintText;
/**
* 初始化路径,这个路径没有真正的绘制
*/
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();
}
画笔的初始化千万不要放到onDraw()方法中进行,因为onDraw方法会被调用很多次,会new出大量不必要的对象。
接下来是绘制中间的圆
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();
}
我这里讲渐变半径设置为10-50, final float per = 255f / 40f;同时根据半径的大小设置透明度。
- 绘制路径
首先要初始化一个路径,来给PathMeasure测量,就好像指定一条路一样,
@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中的方法进行测量路径的长短,
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) {
//动画展开式时候
if (!flag) {
mPaint.setAlpha(255);
mPaint1.setAlpha(255);
startCircleAnim();
System.out.println("动画展开的时候");
}
ReadBook1.this.setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
if (flag) {
mPaintText.setAlpha(0);
mPaint.setAlpha(0);
mPaint1.setAlpha(0);
System.out.println("动画关闭的时候");
mAnimatorCircle.cancel();
} else {
System.out.println("动画展开的时候");
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方法中调用getSegment方法,得到实时路径的长短,然后绘制
mPath.reset();
// 硬件加速的BUG,需要设置到0,0
mPath.lineTo(0, 0);
float stop = animatedValue;
float start = 0;
mPathMeasure.getSegment(start, stop, mPath, true);
canvas.drawPath(mPath, mPaint2);
这里面比较关键的地方就是float length = mPathMeasure.getLength();测量得到路径的长短,然后根据属性动画动态改变这个值,最后调用 mPathMeasure.getSegment(start, stop, mPath, true);这个方法,得到实时的路径长短。
总结:
1)需要判断动画是展开还是收缩,根据这个设置属性动画的值,
2)监听动画的开始和结束,根据这个显示和隐藏中间的圆
3)动画展开结束时显示文字。
最后不断循环的动画会造成内存泄露,作为一个严谨的程序员千万不能忘记。
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
if (mAnimatorCircle != null)
mAnimatorCircle.cancel();
}
全部代码如下
public class ReadBook1 extends View {
/**
* 中心圆的画笔
*/
private Paint mPaint;
/**
* 渐变圆的画笔
*/
private Paint mPaint1;
/**
* 路径的画笔
*/
private Paint mPaint2;
/**
* 文字的画笔
*/
private Paint mPaintText;
/**
* 初始化路径,这个路径没有真正的绘制
*/
private Path mInitPath;
/**
* 用getSegment获取的路径片段
*/
private Path mPath;
/**
* 中心圆的半径
*/
private int mRadius = 10;
/**
* 圆渐变的动画
*/
private ValueAnimator mAnimatorCircle;
/**
* 测量路径的类
*/
private PathMeasure mPathMeasure;
private Float animatedValue = 0f;
private int mWidth;
private int mHeight;
//判断是展开是回收
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,需要设置到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) {
//动画展开式时候
if (!flag) {
mPaint.setAlpha(255);
mPaint1.setAlpha(255);
startCircleAnim();
System.out.println("动画展开的时候");
}
ReadBook1.this.setEnabled(false);
}
@Override
public void onAnimationEnd(Animator animation) {
if (flag) {
mPaintText.setAlpha(0);
mPaint.setAlpha(0);
mPaint1.setAlpha(0);
System.out.println("动画关闭的时候");
mAnimatorCircle.cancel();
} else {
System.out.println("动画展开的时候");
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();
}
}