属性动画+贝塞尔曲线实现落叶效果~~~(@_@;)

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/oushangfeng123/article/details/50545453

之前看了一款有点黄的17app底角的爱心各种乱飞,好奇这种效果的实现方式,恰巧看到这篇文章:程序亦非猿:一步一步教你实现Periscope点赞效果,遂按照其思路实现了一个落叶飘零的效果,如下动图:

看起来还蛮带感的( ˘͈ ᵕ ˘͈ )

实现的要点如下:

  1. 值动画的使用
  2. 贝塞尔公式估值器的设置
  3. 落叶的起点、途径点、终点处理
  4. Activity退出时动画和子线程的处理,防止内存泄露

实现步骤:

① 控件初始化添加叶子集合和补间器集合

    public FloatLeafLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {

        // 四张不同形状的叶子
        mLeafs = new Drawable[]{getResources().getDrawable(R.mipmap.leaf_1),
                getResources().getDrawable(R.mipmap.leaf_2),
                getResources().getDrawable(R.mipmap.leaf_3),
                getResources().getDrawable(R.mipmap.leaf_4)};

        // 四个不同的补间器
        mInterpolator = new Interpolator[]{new AccelerateDecelerateInterpolator(),
                new AccelerateInterpolator(),
                new DecelerateInterpolator(),
                new LinearInterpolator()};

    }

② onMeasure()测出宽高,并且添加树,树的图片做了缩放处理

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        setMeasuredDimension(mWidthSize = measure(widthMeasureSpec), mHeightSize = measure(heightMeasureSpec));
        if (getChildCount() == 0) {
            addTree(mWidthSize, mHeightSize);
        }
    }

    private int measure(int measureSpec) {
        int result = 0;
        int mode = MeasureSpec.getMode(measureSpec);
        int size = MeasureSpec.getSize(measureSpec);
        if (mode == MeasureSpec.EXACTLY) {
            result = size;
        } else {
            result = dip2px(getContext(), 300);
            if (mode == MeasureSpec.AT_MOST) {
                result = Math.min(result, size);
            }
        }
        return result;
    }

    // 添加树的图片
    private void addTree(int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(getResources(), R.mipmap.tree, options);
        final int outWidth = options.outWidth;
        final int outHeight = options.outHeight;
        int inSampleSize = 1;
        if (outWidth > reqWidth || outHeight > reqHeight) {
            final int widthRatio = outWidth / reqWidth;
            final int heightRatio = outHeight / reqHeight;
            inSampleSize = Math.min(widthRatio, heightRatio);
        }
        options.inSampleSize = inSampleSize == 0 ? 1 : inSampleSize;
        options.inJustDecodeBounds = false;
        ImageView mTree = new ImageView(getContext());
        final Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.tree, options);
        mTree.setBackgroundDrawable(new BitmapDrawable(bitmap));
        addView(mTree, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

③ 接下来就是暴露添加树叶:addLeaf()播放树叶:playLeaf()两个方法

首先addLeaf()开始随机添加一片树叶,起点X坐标随机取,然后算出Y坐标

    public void addLeaf() {

        ImageView mLeaf = new ImageView(getContext());
        Random random = new Random();
        // 设置随机一片落叶
        mLeaf.setImageDrawable(mLeafs[random.nextInt(4)]);

        // 随机设置落叶的起点x坐标
        float leafX = random.nextInt(mWidthSize);

        float leafY;
        // 根据x坐标算出y坐标,因为树叶的范围呈三角形,并且约占高度一半,所以要控制y坐标
        if (leafX > mWidthSize / 2) {
            leafY = mHeightSize * 1.0f / mWidthSize * leafX - mHeightSize / 2;
        } else {
            leafY = -mHeightSize * 1.0f / mWidthSize * leafX + mHeightSize / 2;
        }

        // 设置落叶起点,添加到布局
        ViewCompat.setX(mLeaf, leafX);
        ViewCompat.setY(mLeaf, leafY);
        addView(mLeaf);

坐标图
Y坐标按照一次方程解出即可,很简单不再阐述。

重点来了,看下动画设置代码

// 设置树叶刚开始出现的动画
        ObjectAnimator alpha = ObjectAnimator.ofFloat(mLeaf, "alpha", 0.1f, 1);
        ObjectAnimator scaleX = ObjectAnimator.ofFloat(mLeaf, "scaleX", 0.1f, 1);
        ObjectAnimator scaleY = ObjectAnimator.ofFloat(mLeaf, "scaleY", 0.1f, 1);
        AnimatorSet set = new AnimatorSet();
        set.playTogether(alpha, scaleX, scaleY);
        set.setDuration(300);

        // 树叶落下经过的第二个点
        final PointF pointF1 = new PointF(leafX + random.nextInt((int) (mWidthSize - leafX)), leafY + random.nextInt((int) (mHeightSize - leafY)));
        // 树叶落下经过的第三个点
        final PointF pointF2 = new PointF(leafX + random.nextInt((int) (mWidthSize - leafX)), leafY + random.nextInt((int) (mHeightSize - leafY)));
        // 树叶落下的起点
        final PointF pointF0 = new PointF(ViewCompat.getX(mLeaf), ViewCompat.getY(mLeaf));
        // 树叶落下的终点
        final PointF pointF3 = new PointF(random.nextInt(mWidthSize), mHeightSize);

        // 通过自定义的贝塞尔估值器算出途经的点的想x,y坐标
        final BazierTypeEvaluator bazierTypeEvaluator = new BazierTypeEvaluator(pointF1, pointF2);
        // 设置值动画
        ValueAnimator bazierAnimator = ValueAnimator.ofObject(bazierTypeEvaluator, pointF0, pointF3);
        bazierAnimator.setTarget(mLeaf);
        bazierAnimator.addUpdateListener(new BazierUpdateListener(mLeaf));
        bazierAnimator.setDuration(2000);

        // 将以上动画添加到动画集合
        AnimatorSet allSet = new AnimatorSet();
        allSet.play(set).before(bazierAnimator);
        // 随机设置一个补间器
        allSet.setInterpolator(mInterpolator[random.nextInt(4)]);
        allSet.addListener(new AnimatorEndListener(mLeaf));
        allSet.start();

属性动画用到了两个集合,开始是一个树叶生成时缩放透明度的动画,接下来就是值动画的使用,使用到了一个自定义的估值器BazierTypeEvaluator,此货运用了三次方贝塞尔公式算出落叶途经的坐标。贝塞尔是啥呢?我反正不想知道 凸(⊙▂⊙✖ ) ,想简单了解的可以看下爱哥的自定义控件其实很简单5/12,这里直接拿公式套上去就OK了,通过evaluate()的t值变化,算出途经的坐标值。

public class BazierTypeEvaluator implements TypeEvaluator<PointF> {

    /**
     * 三次方贝塞尔曲线
     * B(t)=P0*(1-t)^3+3*P1*t*(1-t)^2+3*P2*t^2*(1-t)+P3*t^3,t∈[0,1]
     * P0,是我们的起点,
     * P3是终点,
     * P1,P2是途径的两个点
     * 而t则是我们的一个因子,取值范围是0-1
     */
    private PointF pointF1;
    private PointF pointF2;

    public BazierTypeEvaluator(PointF pointF1, PointF pointF2) {
        this.pointF1 = pointF1;
        this.pointF2 = pointF2;
    }

    @Override
    public PointF evaluate(float t, PointF startValue, PointF endValue) {
        PointF pointF = new PointF();
        pointF.x = (float) (startValue.x * Math.pow(1 - t, 3) + 3 * pointF1.x * t * Math.pow(1 - t, 2) + 3 * pointF2.x * Math.pow(t, 2) * (1 - t) + endValue.x * Math.pow(t, 3));
        pointF.y = (float) (startValue.y * Math.pow(1 - t, 3) + 3 * pointF1.y * t * Math.pow(1 - t, 2) + 3 * pointF2.y * Math.pow(t, 2) * (1 - t) + endValue.y * Math.pow(t, 3));
        return pointF;
    }
}

上面bazierAnimator.addUpdateListener(new BazierUpdateListener(mLeaf)),继承ValueAnimator.AnimatorUpdateListener后不断去取算出的坐标值设置给落叶即可,还做了个透明度的变化

    // 值动画更新监听
    private class BazierUpdateListener implements ValueAnimator.AnimatorUpdateListener {

        View target;

        public BazierUpdateListener(View target) {
            BazierUpdateListener.this.target = target;
        }

        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            // 获取坐标,设置落叶的位置
            final PointF pointF = (PointF) animation.getAnimatedValue();
            ViewCompat.setX(target, pointF.x);
            ViewCompat.setY(target, pointF.y);
            ViewCompat.setAlpha(target, 1 - animation.getAnimatedFraction());
        }
    }

allSet.addListener(new AnimatorEndListener(mLeaf));动画集合添加动画停止的监听,用于移除落叶节约资源

    // 动画更新适配器,用于动画停止的时候移除落叶
    private class AnimatorEndListener extends AnimatorListenerAdapter {

        View target;

        public AnimatorEndListener(View target) {
            this.target = target;
        }

        @Override
        public void onAnimationEnd(Animator animation) {
            super.onAnimationEnd(animation);
            removeView(target);
            Log.e(TAG, "child:" + getChildCount());
        }
    }

播放落叶无非开启子线程不断调用addLeaf()生成落叶

    // 播放落叶,播放15片
    public void playLeaf() {

        new Thread() {
            @Override
            public void run() {
                if (mIsDestoryed)
                    // 页面销毁直接返回
                    return;
                for (int i = 0; i < 15; i++) {
                    if (mIsDestoryed)
                        // 页面销毁直接返回
                        return;
                    ((Activity) getContext()).runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            addLeaf();
                        }
                    });
                    try {
                        Thread.sleep(300);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

    }

④页面消耗时候的处理,因为有可能在所有落叶在执行动画未完成前用户退出页面了,所以这里暴露方法onDestory()做清理工作

    // 销毁的时候做清理工作
    public void onDestroy() {
        Log.e(TAG, "Activity被销毁了");
        mIsDestoryed = true;
        if (mAnimatorSets == null) return;
        for (int i = 0; i < mAnimatorSets.size(); i++) {
            mAnimatorSets.get(i).cancel();
        }
        mAnimatorSets.clear();
    }

总结:

整体思路不难,重要的是掌握一些有趣的公式结合属性动画做出好玩的效果!

ヽ(^o^)ρ┳┻┳°σ(^o^)/

最后附上资源Demo:飘零落叶控件

展开阅读全文

没有更多推荐了,返回首页