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

来源:互联网 发布:淘宝直通车黑车 编辑:程序博客网 时间:2024/04/29 02:13

之前看了一款有点黄的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:飘零落叶控件

2 0
原创粉丝点击