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

作者: Conan_Lee | 来源:发表于2017-08-21 15:46 被阅读732次

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

    先看下这两个动画的效果

    效果.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);
            }
        }
    }
    }
    

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

    贝塞尔公式.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,这个项目有新的动画就会往里面添加,也可以在评论区留言看那些动画比较不错,我来实现并往里面添加

    相关文章

      网友评论

        本文标题:Android直播点赞动画和Yahoo摘要动画

        本文链接:https://www.haomeiwen.com/subject/ktcorxtx.html