Android|属性动画+贝塞尔曲线实现落叶效果~~~(@_@;)
之前看了一款有点黄的17app底角的爱心各种乱飞,好奇这种效果的实现方式,恰巧看到这篇文章:程序亦非猿:一步一步教你实现Periscope点赞效果,遂按照其思路实现了一个落叶飘零的效果,如下动图: 【Android|属性动画+贝塞尔曲线实现落叶效果~~~(@_@;
)】
文章图片
实现的要点如下:
- 值动画的使用
- 贝塞尔公式估值器的设置
- 落叶的起点、途径点、终点处理
- 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 {/**
* 三次方贝塞尔曲线
* 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:飘零落叶控件
推荐阅读
- 第6.2章(设置属性)
- android第三方框架(五)ButterKnife
- Android中的AES加密-下
- 带有Hilt的Android上的依赖注入
- android|android studio中ndk的使用
- Android事件传递源码分析
- RxJava|RxJava 在Android项目中的使用(一)
- Android7.0|Android7.0 第三方应用无法访问私有库
- 深入理解|深入理解 Android 9.0 Crash 机制(二)
- android防止连续点击的简单实现(kotlin)