Osheep

时光不回头,当下最重要。

Android直播点赞动画和Yahoo摘要动画

前段时间看红橙Darren写的关于自定义View的相关文章,决定自己撸一撸,让记忆更深刻

先看下这两个动画的效果

《Android直播点赞动画和Yahoo摘要动画》

效果.gif

点赞动画

1.将可爱的点赞图标(ImageView)添加到自定义View中
2.利用贝塞尔估值器为每一个添加的ImageView设置路径,并设置插值器
3.动画完成之后将ImageView从View中移除

来分析下源码,100多行

//自定义View继承RelativeLayout
public class LoveIconView extends RelativeLayout {
//View自身的宽度和高度
private int width, height;
//ImageView的宽高
private int iconWidth, iconHeight;

//插值器列表
private Interpolator[] interpolators;
//随机数
private Random random = new Random();

public LoveIconView(Context context) {
    this(context, null);
}

public LoveIconView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public LoveIconView(Context context, AttributeSet attrs, int defStyleAttr) {
    this(context, attrs, defStyleAttr, 0);
}

public LoveIconView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);

    init();
}

private void init() {
    initInterpolator();
}

//初始化了一些插值器,这样每一个ImageView就可以有不同的移动速率了
private void initInterpolator() {
    interpolators = new Interpolator[]{
            new LinearInterpolator(),
            new AccelerateDecelerateInterpolator(),
            new AccelerateInterpolator(),
            new DecelerateInterpolator(),
    };
}

//获取View的宽高
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    width = getMeasuredWidth();
    height = getMeasuredHeight();
}

//在离开窗口后移除View
@Override
protected void onDetachedFromWindow() {
    removeAllViews();
    super.onDetachedFromWindow();
}

private void startAnimator(ImageView view) {
    //曲线的两个顶点
    PointF pointF1 = new PointF(
            random.nextInt(width),
            random.nextInt(height / 2) + height / 2);
    PointF pointF2 = new PointF(
            random.nextInt(width),
            random.nextInt(height / 2));
    //曲线的开始和结束点
    PointF pointStart = new PointF((width - iconWidth) / 2,
            height - iconHeight);
    PointF pointEnd = new PointF(random.nextInt(width), random.nextInt(height / 2));

    //贝塞尔估值器
    BezierEvaluator evaluator = new BezierEvaluator(pointF1, pointF2);
    ValueAnimator animator = ValueAnimator.ofObject(evaluator, pointStart, pointEnd);
    animator.setTarget(view);
    animator.setDuration(3000);
    animator.addUpdateListener(new UpdateListener(view));
    animator.addListener(new AnimatorListener(view, this));
    animator.setInterpolator(interpolators[random.nextInt(4)]);

    animator.start();
}

//添加ImageView并开始动画
public void addLoveIcon(int resId) {
    ImageView view = new ImageView(getContext());
    view.setImageResource(resId);
    iconWidth = view.getDrawable().getIntrinsicWidth();
    iconHeight = view.getDrawable().getIntrinsicHeight();

    addView(view);
    startAnimator(view);
}

public static class UpdateListener implements ValueAnimator.AnimatorUpdateListener {

    private WeakReference<ImageView> iv;

    public UpdateListener(ImageView iv) {
        this.iv = new WeakReference<>(iv);
    }

    @Override
    public void onAnimationUpdate(ValueAnimator animation) {
        //更新ImageView的透明度
        PointF pointF = (PointF) animation.getAnimatedValue();
        ImageView view = iv.get();
        if (null != view) {
            view.setX(pointF.x);
            view.setY(pointF.y);
            view.setAlpha(1 - animation.getAnimatedFraction() + 0.1f);
        }
    }
}

public static class AnimatorListener extends AnimatorListenerAdapter {

    private WeakReference<ImageView> iv;
    private WeakReference<LoveIconView> parent;

    public AnimatorListener(ImageView iv, LoveIconView parent) {
        this.iv = new WeakReference<>(iv);
        this.parent = new WeakReference<>(parent);
    }

    @Override
    public void onAnimationEnd(Animator animation) {
        //动画结束时移除View
        ImageView view = iv.get();
        LoveIconView parent = this.parent.get();
        if (null != view
                && null != parent) {
            parent.removeView(view);
        }
    }
}
}

这里用到了贝塞尔三次方公式

《Android直播点赞动画和Yahoo摘要动画》

贝塞尔公式.jpg

下面是基于这个公式来实现的贝塞尔估值器实现

public class BezierEvaluator implements TypeEvaluator<PointF> {

private PointF point1, point2;
private PointF point;

public BezierEvaluator(PointF point1, PointF point2) {
    this.point1 = point1;
    this.point2 = point2;
    point = new PointF();
}

@Override
public PointF evaluate(float t, PointF startValue, PointF endValue) {
    point.x = startValue.x * (1 - t) * (1 - t) * (1 - t)
            + 3 * point1.x * t * (1 - t) * (1 - t)
            + 3 * point2.x * t * t * (1 - t)
            + endValue.x * t * t * t;
    point.y = startValue.y * (1 - t) * (1 - t) * (1 - t)
            + 3 * point1.y * t * (1 - t) * (1 - t)
            + 3 * point2.y * t * t * (1 - t)
            + endValue.y * t * t * t;
    return point;
}
}

这个动画主要涉及到的知识点
1.贝塞尔曲线公式
2.ValueAnimator的使用(自定义属性时的使用)

简单说下ValueAnimator,属性动画ObjectAnimator就是继承自这个类,这个类就是一个数值动画类,用来计算具体的动画数据值,除了可以使用ofInt,ofFloat等基本属性外,还可以实现自定义属性,实现数据的变化(ofObject)需要传入一个参数(实现TypeEvaluator),这里BezierEvaluator实现了TypeEvaluator,利用贝塞尔三次方公式来计算两个PointF的估值.


Yahoo新闻摘要动画

1.绘制小圆,并让一直旋转
2.小圆聚合动画
3.圆圈扩展动画,让下面的视图展现出来

OK来分析源码,200行左右,比较简单
首先看下类成员定义

//因为View继承自SurfaceView,所以需要这个
private SurfaceHolder surfaceHolder;

//View width/2 height/2 center point(这里的宽度和高度就是中心点坐标位置,不是View的宽高)
private int width, height;

//Draw small circle(绘制小圆)
private Paint paint;

//Draw expanded circle(绘制最后扩展圆来展现下层控件)
private Paint expandPaint;
//Small color list(可以定义一些小圆的颜色列表)
private int[] colors;
//小圆转圈动画
private ValueAnimator circleAnimator;
//小圆向外扩展动画
private ValueAnimator expandAnimator;
//小圆向内聚合动画
private ValueAnimator collapsingAnimator;
//最后的扩展显示下层控件的动画
private ValueAnimator filterAnimator;

//Current rotate angle(当前的旋转角度)
private float rotateAngle;
//分别为小圆半径,大圆半径,扩展动画的圆的半径(变化的)
private int innerRadius, outerRadius, expandRadius = -1;
//The angle between each circle(每个小圆之间的间隔角度)
private float gapAngle;
//The factor for collapsing and expand(小圆聚合的乘数因子)
private float factor = 1.0f;
//Status (Loading状态)
private Status status = Status.IDLE;
//Expand animator set(小圆集合以及最后铺开显示下层控件的动画集合)
private AnimatorSet animatorSet;
//Circle animator set(小圆转圈的动画集合)
private AnimatorSet circleAnimatorSet;
//绘图的模式
private PorterDuffXfermode mode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);

定义了一个状态枚举类

public enum Status {
    IDLE,//初始状态
    LOADING,//正在加载
    COMPLETE,//加载完成
}

初始化

public YahooLoadingView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    //这里定义了两个自定义属性,可以设置小圆的大圆的半径
    TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.YahooLoading);
    innerRadius = a.getDimensionPixelSize(R.styleable.YahooLoading_innerRadius, 15);
    outerRadius = a.getDimensionPixelSize(R.styleable.YahooLoading_outerRadius, 160);
    a.recycle();

    surfaceHolder = getHolder();

    initData();
    initAnimator();
    
    //这句话需要,否则图层最后会显示黑色
    setLayerType(LAYER_TYPE_HARDWARE, null);
    //需要设置背景色,否则View无效果
    setBackgroundColor(Color.WHITE);

    //SurfaceView需要设置这句话,能让图层在最上层
    setZOrderOnTop(true);
    surfaceHolder.addCallback(this);
}  

初始化一些数据

private void initData() {
    //默认给了小圆6个颜色,并计算了小圆的间隔角度
    colors = new int[]{Color.RED, Color.BLUE, Color.GREEN, Color.CYAN, Color.DKGRAY, Color.GRAY};
    gapAngle = (float) Math.PI * 2 / colors.length;

    paint = new Paint();
    paint.setAntiAlias(true);

    expandPaint = new Paint();
    expandPaint.setAntiAlias(true);
}

核心的动画效果定义

    private void initAnimator() {
    circleAnimator = ValueAnimator.ofFloat(0, (float) Math.PI * 2);
    circleAnimator.setDuration(1600);
    circleAnimator.setRepeatCount(ValueAnimator.INFINITE);
    circleAnimator.setInterpolator(new LinearInterpolator());
    circleAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            expand();
        }
    });
    circleAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            if (Status.COMPLETE == status) {
                animation.cancel();
                return;
            }
            rotateAngle = (float) animation.getAnimatedValue();
            invalidate();
        }
    });

    expandAnimator = ValueAnimator.ofFloat(1, 1.5f);
    expandAnimator.setDuration(200);
    expandAnimator.setRepeatCount(0);
    expandAnimator.setInterpolator(new DecelerateInterpolator());
    expandAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            factor = (float) animation.getAnimatedValue();
            invalidate();
        }
    });

    collapsingAnimator = ValueAnimator.ofFloat(1.5f, 0f);
    collapsingAnimator.setDuration(300);
    collapsingAnimator.setRepeatCount(0);
    collapsingAnimator.setInterpolator(new AccelerateInterpolator());
    collapsingAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            factor = (float) animation.getAnimatedValue();
            invalidate();
        }
    });


    WindowManager windowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
    Point point = new Point();
    windowManager.getDefaultDisplay().getSize(point);
    int width = point.x / 2;
    int height = point.y / 2;
    filterAnimator = ValueAnimator.ofInt(0, (int) Math.sqrt(width * width + height * height));
    filterAnimator.setDuration(2000);
    filterAnimator.setRepeatCount(0);
    filterAnimator.setInterpolator(new LinearInterpolator());
    filterAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            expandRadius = (int) animation.getAnimatedValue();
            invalidate();
        }
    });
    filterAnimator.addListener(new AnimatorListenerAdapter() {
        @Override
        public void onAnimationEnd(Animator animation) {
            resetData();
            setVisibility(GONE);
        }
    });

    animatorSet = new AnimatorSet();
    animatorSet.playSequentially(expandAnimator, collapsingAnimator, filterAnimator);

    circleAnimatorSet = new AnimatorSet();
    circleAnimatorSet.playTogether(circleAnimator);
}

核心的绘制方法

    @Override
protected void onDraw(Canvas canvas) {
    if (expandRadius > -1) {
        expandPaint.setColor(Color.WHITE);
        expandPaint.setXfermode(mode);
        canvas.drawCircle(width, height, expandRadius, expandPaint);
    }

    for (int i = 0; i < colors.length; i++) {
        paint.setColor(colors[i]);
        float radius = (outerRadius - innerRadius) * factor;
        float cx = (float) (radius * Math.sin((double) (rotateAngle + i * gapAngle)) + width);
        float cy = (float) (height - radius * Math.cos((double) (rotateAngle + i * gapAngle)));
        if (radius > innerRadius) {
            canvas.drawCircle(cx, cy, innerRadius, paint);
        } else {
            canvas.drawCircle(cx, cy, radius, paint);
        }
    }
}

这里遇到的一个坑就是PorterDuffXfermode

这个模式的官方的那张图有些误导,需要自己亲自实践一下,理解每种模式的含义。关于PorterDuffXfermode的具体解释网上有很多

最后贴上GitHub源码地址
https://github.com/ly85206559/AndroidCustomizeView/tree/master
欢迎Fork和Star,这个项目有新的动画就会往里面添加,也可以在评论区留言看那些动画比较不错,我来实现并往里面添加

点赞