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

之前看了一款有点黄的17app底角的爱心各种乱飞,好奇这种效果的实现方式,恰巧看到这篇文章:程序亦非猿:一步一步教你实现Periscope点赞效果,遂按照其思路实现了一个落叶飘零的效果,如下动图: 【Android|属性动画+贝塞尔曲线实现落叶效果~~~(@_@; )】Android|属性动画+贝塞尔曲线实现落叶效果~~~(@_@;)
文章图片

实现的要点如下:

  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);

Android|属性动画+贝塞尔曲线实现落叶效果~~~(@_@;)
文章图片

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 {/** * 三次方贝塞尔曲线 * 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:飘零落叶控件

    推荐阅读