转载请标明出处:
//www.greatytc.com/u/88ded7d85d2f
本文出自:【CF凌晨的博客】
引言
这是第一个简书的文章,写的不好的地方,请大家多多提提意见哈。今天写的文章是关于动画的,前几天公司要求我写一个观看直播点击送花与主播收到花朵的效果。
思路
好了看见了效果之后想必大家都应该知道肯定想到要用Animator(4.0增加的动画类),想必很多人都接触到了,但是刚接触android的兄弟可能就没怎么使用这个。网上有很多文章对这个类的介绍,所以这里就不注重解释了。
主要的思路如下:
1.确定起点(主播是随机起点)与终点位置;
2.为其添加动画特效,从起点运动到终点,然后销毁View;
是不是很简单,哈哈。好了,开始撸吧!!!
撸码
我就不拿以前的代码了,我一边敲一边说,首先创建个项目(不多说了哈),然后我们创建一个工具类FlowerAnimation.java。我们就对这个类疯狂撸吧。
固定起点->固定终点特效实现
private static final String TAG = "FlowerAnimation";
//上下文
private Context mContext;
//生成的View添加在的ViewGroup
private ViewGroup rootView;
private ViewGroup.LayoutParams layoutParams;
//资源文件
private Drawable[] drawables;
//插值器
private Interpolator[] interpolators;
上面代码的注释很清楚了,大家一看就明白了 ,TGA这是为了测试打印log使用的(AS快捷键-logt+enter)。
public FlowerAnimation(Context mContext, ViewGroup rootView) {
this.mContext = mContext;
this.rootView = rootView;
init();
}
private void init() {
drawables = new Drawable[8];
drawables[0] = mContext.getResources().getDrawable(R.mipmap.flower_01);
drawables[1] = mContext.getResources().getDrawable(R.mipmap.flower_02);
drawables[2] = mContext.getResources().getDrawable(R.mipmap.flower_03);
drawables[3] = mContext.getResources().getDrawable(R.mipmap.flower_04);
drawables[4] = mContext.getResources().getDrawable(R.mipmap.flower_05);
drawables[5] = mContext.getResources().getDrawable(R.mipmap.flower_06);
drawables[6] = mContext.getResources().getDrawable(R.mipmap.flower_07);
drawables[7] = mContext.getResources().getDrawable(R.mipmap.flower_08);
interpolators = new Interpolator[4];
interpolators[0] = new LinearInterpolator();//线性
interpolators[1] = new AccelerateInterpolator();//加速
interpolators[2] = new DecelerateInterpolator();//减速
interpolators[3] = new AccelerateDecelerateInterpolator();//先加速后减速
layoutParams = new ViewGroup.LayoutParams(DensityUtil.dip2px(mContext, 50), DensityUtil.dip2px(mContext, 50));
}
上面就是一些初始化的东西了,大家看看就好。
准备工作做好了,我们写最主要的方法,那就是生成动画了,上代码
/**
/**
* 开启动画
*
* @param view 执行动画的view
* @param startP 起点 如果传null 默认从view位置开始
* @param stopP 终点
*/
public void startAnim(@NonNull final View view, @Nullable PointF startP, @NonNull PointF stopP) {
if (startP == null) {
startP = new PointF(view.getX(), view.getY());
}
//透明度变化
ObjectAnimator animatorAlpha = ObjectAnimator.ofFloat(view, "alpha", 0, 1);
animatorAlpha.setDuration(200);
//位移动画
ObjectAnimator animatorX = ObjectAnimator.ofFloat(view, "translationX", startP.x, stopP.x);
animatorX.setDuration(1000);
ObjectAnimator animatorY = ObjectAnimator.ofFloat(view, "translationY", startP.y, stopP.y);
animatorY.setDuration(1000);
//生成动画集合
AnimatorSet set = new AnimatorSet();
//开启透明度动画然后执行位移动画
set.play(animatorAlpha).before(animatorX).with(animatorY);
//加入植入器
set.setInterpolator(interpolators[rand.nextInt(interpolators.length)]);
//添加动画监听事件 为了移除view 防止造成oom
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
rootView.removeView(view);
}
});
set.start();
}
/**
* 开启动画
*
* @param view 执行动画的view
* @param stopP 终点
*/
public void startAnim(@NonNull final View view, @NonNull PointF stopP) {
startAnim(view, null, stopP);
}
/**
* 添加花朵
*
* @param startPoint
*/
public void addFlower(@NonNull PointF startPoint, @NonNull PointF stopP) {
ImageView flower = new ImageView(mContext);
flower.setX(startPoint.x);
flower.setY(startPoint.y);
Drawable drawable = drawables[rand.nextInt(drawables.length)];
flower.setBackground(drawable);
rootView.addView(flower, layoutParams);
startAnim(flower, startPoint, stopP);
}
好了,我们看到,生成这个动画需要View(这是必然的)终点也是必然,起点就无所谓了(要么你指定 要么是自身,哈哈,已经重载了)每一步的注释都写的很清楚,ObjectAnimator这个类功能很强大(4.0+),很强大,很强大。下面开始写个界面来看看效果啦。界面代码我直接上代码 不讲解了!对了有关于view位置的问题大家直接去看别的文章吧,很多的哈,我这里贴个我认为不错的
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//终点坐标imageView
private ImageView endFlowerIv;
//开启动画按钮 也是起点坐标
private Button startFlowerBt;
private FlowerAnimation flowerAnimation;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initView();
}
private void initView() {
endFlowerIv = (ImageView) findViewById(R.id.main_end_flower_iv);
startFlowerBt = (Button) findViewById(R.id.main_start_flower_bt);
startFlowerBt.setOnClickListener(this);
flowerAnimation = new FlowerAnimation(this, (ViewGroup) findViewById(R.id.activity_main));
}
@Override
public void onClick(View v) {
flowerAnimation.addFlower(new PointF(v.getX(), v.getY()), new PointF(endFlowerIv.getX(), endFlowerIv.getY()));
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/activity_main"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/main_end_flower_iv"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal"
android:background="@mipmap/flower_01" />
<Button
android:id="@+id/main_start_flower_bt"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_gravity="center_horizontal|bottom"
android:background="@mipmap/flower_01" />
</FrameLayout>
下面是效果图 好啦 完成了
随即起点->固定终点特效实现
这个问题想必大家在上面的基础上就完全可以写出来 直接把代码贴上来与效果
/**
* 添加花朵 随即生成起点(rootView范围)
*
* @param stopP 终点
*/
public void addFlowerByScope(@NonNull PointF stopP) {
float x = rand.nextFloat() * rootView.getWidth();
float y = rand.nextFloat() * rootView.getHeight();
addFlower(new PointF(x, y), stopP);
}
/**
* 添加花朵 随即生成起点
*
* @param stopP 终点
* @param scopeP 范围 随即生成的点将会按照此范围随即取值
*/
public void addFlowerByScope(@NonNull PointF stopP, @NonNull PointF scopeP) {
float x = rand.nextFloat() * scopeP.x;
float y = rand.nextFloat() * scopeP.y;
addFlower(new PointF(x, y), stopP);
}
界面的点击事件换成对应的方法
@Override
public void onClick(View v) {
flowerAnimation.addFlowerByScope(new PointF(endFlowerIv.getX(), endFlowerIv.getY()));
}
到此,你会发现已经完成主播接到花朵的效果(就是随即从各个地方出现花朵飞到花朵出 嘻嘻)以上就是主播界面显示的效果了。代码比较简单,下面实现观众点击送花的效果
实现观众送花效果
思路呢?其实跟上面差不多,观众送花的效果类似固定点到固定点的效果(类似哈哈),为什么说类似呢?因为从图上可以看到,路径是不同的,很明显发现 观众送花的效果的路径是随即的(乱飘)。这里就引出来了ValueAnimator 这个东西你会发现他是ObjectAnimator的父类(其实当你看到他们名字的时候 就知道啦 哈哈)
ValueAnimator 顾名思义哈 就是针对数值的动画,他能帮我完成什么呢?
比如我我想让一个数值从0-10 时间是10s,我们来写写看
我们新建一个ValueAnimActivity.java来实现观众的界面,顺便在里面谢谢测试demo哈哈
public class ValueAnimActivity extends AppCompatActivity implements View.OnClickListener {
private TextView countTv;
private Button startBt;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_value);
initView();
}
private void initView() {
countTv = (TextView) findViewById(R.id.value_count_tv);
startBt = (Button) findViewById(R.id.value_start_bt);
startBt.setOnClickListener(this);
}
@Override
public void onClick(View v) {
startValueAnim();
}
private void startValueAnim() {
//从0-10 时间10s
ValueAnimator countAnim = ValueAnimator.ofInt(0, 10)
.setDuration(10000);
countAnim.setInterpolator(new LinearInterpolator());
countAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
countTv.setText(animation.getAnimatedValue().toString());
}
});
countAnim.start();
}
}
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/value_count_tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textSize="30dp" />
<Button
android:id="@+id/value_start_bt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal|bottom"
android:text="start" />
</FrameLayout>
代码跟布局都很简单 我们简单写了个ValueAnimator的例子 直接看效果
哈哈 就是那么叼,那怎么实现我们的效果呢?这里就用到一个方法
public static ValueAnimator ofObject(TypeEvaluator evaluator, Object... values)
他能让一个对象进行改变,那怎么改变呢?其实随便是什么?都要跟时间挂钩(重要),为什么跟时间挂钩,这还要解释么?onAnimationUpdate回调的方法你打印log你会发现他会调用N多次,10S回调了496次,可想而知我们对象改变也会跟时间有关系,那么我们看看TypeEvaluator(类型评估者)这是一个接口,我们来看看它要我们实现的方法
public T evaluate(float fraction, T startValue, T endValue);
这个方法看到之后,后面2个我肯定知道是什么意思,动画的起始值,那第一个是什么?(英文翻译是分数)咱们说过肯定跟时间有关的,那么这个是不是就是时间呢?看了官方解释之后,这个意思就是当前完成动画的百分比。
还不懂?那好我们还看看官方有没有默认给我们实现的类,一看,有很多,我们直接拿来一个用看看效果,上代码
private void startValueAnim1() {
//从0-10 时间10s
ValueAnimator countAnim = ValueAnimator.ofObject(new IntEvaluator() {
@Override
public Integer evaluate(float fraction, Integer startValue, Integer endValue) {
Log.e(TAG, "evaluate() called with: fraction = [" + fraction + "], startValue = [" + startValue + "], endValue = [" + endValue + "]");
return super.evaluate(fraction, startValue, endValue);
}
}, 0, 10)
.setDuration(10000);
countAnim.setInterpolator(new LinearInterpolator());
countAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
countTv.setText(animation.getAnimatedValue().toString());
}
});
countAnim.start();
}
哈哈 一目了然的。(时间也很好算出来了吧?fraction = t / duration)
下面重点来了,如果要实现这种效果,那就要一个公式(贝塞尔曲线)这个当初我也是一头雾水啊,先不管直接套用公式就行了(先上一个图片)
这个公式只要理解我们给出4个点,他就算出当前的点。这里有4个点,但是我们只有2个点,另外2个点是为了控制曲线的走向,我随即取就可以咯。好了,我们先不管点,先把TypeEvaluator写好
/**
* 自定义的估值器
*/
public static class MyTypeEvaluator implements TypeEvaluator<PointF> {
private PointF pointF1, pointF2;
public MyTypeEvaluator(PointF pointF1, PointF pointF2) {
this.pointF1 = pointF1;
this.pointF2 = pointF2;
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
float timeLeft = 1.0f - fraction;
PointF pointF = new PointF();//结果
pointF.x = timeLeft * timeLeft * timeLeft * (startValue.x)
+ 3 * timeLeft * timeLeft * fraction * (pointF1.x)
+ 3 * timeLeft * fraction * fraction * (pointF2.x)
+ fraction * fraction * fraction * (endValue.x);
pointF.y = timeLeft * timeLeft * timeLeft * (startValue.y)
+ 3 * timeLeft * timeLeft * fraction * (pointF1.y)
+ 3 * timeLeft * fraction * fraction * (pointF2.y)
+ fraction * fraction * fraction * (endValue.y);
return pointF;
}
}
只是简单的套用公式,就不用多讲了直接复制就好,要不然能看的眼花(撸多了)。
下面放上取中间控制点的代码
/**
* 获取中间控制点
* 取rootView范围
*
* @param i =0为上方控制点 !=0 为下方
* @return
*/
public PointF getPointF(int i) {
return getPointF(i, new PointF(rootView.getWidth(), rootView.getHeight()));
}
/**
* 获取中间控制点
*
* @param i =0为上方控制点 !=0 为下方
* @param scopeP 范围
* @return
*/
public PointF getPointF(int i, @NonNull PointF scopeP) {
PointF p = new PointF();
//随即范围[0,scopeP.x]
p.x = rand.nextFloat() * scopeP.x;
float height = scopeP.y / 2;
//随即范围[0,height]
float y = rand.nextFloat() * height;
if (i != 0) {
//随即范围[height,scopeP.y]
y = y + height;
}
p.y = y;
return p;
}
注释也简单,我觉得很好弄懂,下面上主要代码
/**
* 开启贝塞尔曲线动画
* 根据rootView 在上下方向分别随即取1个点 作为中间控制点
* 本身的位置作为起点
*
* @param startP 起点
* @param stopP 终点
*/
public void addFlowerByValueAnim(@NonNull PointF startP, @NonNull PointF stopP) {
ImageView flower = new ImageView(mContext);
flower.setX(startP.x);
flower.setY(startP.y);
Drawable drawable = drawables[rand.nextInt(drawables.length)];
flower.setBackground(drawable);
rootView.addView(flower, layoutParams);
addFlowerByValueAnim(flower, getPointF(0), getPointF(1), startP, stopP);
}
/**
* 开启贝塞尔曲线动画
* 根据rootView 在上下方向分别随即取1个点 作为中间控制点
* 本身的位置作为起点
*
* @param view 动画view
* @param stopP 终点
*/
public void addFlowerByValueAnim(@NonNull final View view, @NonNull PointF stopP) {
addFlowerByValueAnim(view, getPointF(0), getPointF(1), startP, stopP);
}
/**
* 开启贝塞尔曲线动画
*
* @param view 动画view
* @param startP1 中间控制点1
* @param startP2 中间控制点2
* @param startP 起点
* @param stopP 终点
*/
public void addFlowerByValueAnim(@NonNull final View view, @Nullable PointF startP1, @Nullable PointF startP2, @Nullable PointF startP, @NonNull PointF stopP) {
//透明度变化
ObjectAnimator animatorAlpha = ObjectAnimator.ofFloat(view, "alpha", 0, 1);
animatorAlpha.setDuration(200);
//生成动画集合
AnimatorSet set = new AnimatorSet();
//开启透明度动画然后执行位移动画
set.play(animatorAlpha).before(getValueAnimator(view, startP1, startP2, startP, stopP));
//加入植入器
set.setInterpolator(interpolators[rand.nextInt(interpolators.length)]);
//添加动画监听事件 为了移除view 防止造成oom
set.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
rootView.removeView(view);
}
});
set.start();
}
/**
* 获取贝塞尔曲线动画
*
* @param view 动画view
* @param startP1 中间控制点1
* @param startP2 中间控制点2
* @param startP 起点
* @param stopP 终点
*/
private ValueAnimator getValueAnimator(@NonNull final View view, @Nullable PointF startP1, @Nullable PointF startP2, @Nullable PointF startP, @NonNull PointF stopP) {
ValueAnimator valueAnimator = ValueAnimator.ofObject(new MyTypeEvaluator(startP1, startP2), startP, stopP);
valueAnimator.setDuration(2000);
valueAnimator.setInterpolator(interpolators[rand.nextInt(interpolators.length)]);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
PointF pointF = (PointF) animation.getAnimatedValue();
view.setX(pointF.x);
view.setY(pointF.y);
}
});
return valueAnimator;
}
@Override
public void onClick(View v) {
flowerAnimation.addFlowerByValueAnim(new PointF(v.getX(), v.getY()), new PointF(endFlowerIv.getX(), endFlowerIv.getY()));
}
好了下面是最终效果啦
到这里差不多可以结束了。想必大家可能会问,如果动画还没执行完就退出了,那就内存泄漏了啊。嗯,说的很对,所以我再来个方法
/**
* 销毁方法
*/
public void onDestroy() {
//标记已经取消了
isOnDestory = true;
//对所有的动画set进行取消
for (AnimatorSet set : animatorSets) {
if (set != null && set.isRunning()) {
set.cancel();
}
}
animatorSets.clear();
animatorSets = null;
//对Drawable回调设置null
for (Drawable drawable : drawables) {
if (drawable != null) {
drawable.setCallback(null);
drawable = null;
}
}
drawables = null;
//手动调用gc
System.gc();
}
@Override
protected void onDestroy() {
if (flowerAnimation != null)
flowerAnimation.onDestroy();
super.onDestroy();
}
好了 跑起来没有问题了,大家下期再见!!!
忘了,源码地址还没给