要实现什么效果?
我们就是要实现如图所示的动画效果,在开始之前我们先了解一下实现这个动画的相关知识。
属性动画相关知识
动画执行的逻辑
逻辑大概流程如下:
- 为 ValueAnimator 设置动画的时长,以及对应属性的始 & 末值
- 设置属性在 始 & 末值 间的变化逻辑
- TimeInterpolator实现类:插值器-描述动画的变化速率
- TypeEvaluator实现类:估值器-描述 属性值 变化的具体数值
- 根据2中的逻辑更新当前值
- 获取3中更新的值 ,修改目标属性值
- 刷新视图
- 重复4-5,直到 属性值 == 末值
动画工作的关键类
Java类 | 说明 |
ValueAnimator | 动画执行类,核心。负责动画的整体协调 |
ObjectAnimator | 动画执行类 |
TimeInterpolator | 时间插值(插值器接口),控制动画变化率 |
TypeEvaluator | 类型估值(估值器接口),设置属性值计算方式,根据属性的始末值和插值计算出当前时间的属性值 |
AnimatorSet | 动画集 |
AnimatorInflater | 加载属性动画的XML文件 |
思路分析与实现代码
首先我们实现这样一个TextView用于展示,实现代码如下:
<TextView
android:id="@+id/tv_left"
android:layout_width="wrap_content"
android:layout_height="60dp"
android:layout_gravity="end"
android:layout_marginTop="30dp"
android:background="@drawable/circleside"
android:gravity="center_vertical"
android:maxLines="1"
android:paddingLeft="16dp"
android:paddingRight="16dp"
android:text="青柠天气提醒您,天冷注意添加衣"
android:textSize="17sp" />
其中的@drawable/circleside 代码如下:
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<!-- 填充颜色 -->
<solid android:color="#40FF552E"></solid>
<!-- 矩形的圆角半径 -->
<corners android:bottomLeftRadius="999dp" android:topLeftRadius="999dp"></corners>
</shape>
其实思路很简单,就是先将View的宽度设置为0,随着时间的增多逐渐把View的宽度设置为它应该有的宽度即可。
private ValueAnimator createDropAnimator(final View v, int start, int end) {
ValueAnimator animator = ValueAnimator.ofInt(start, end);
animator.addUpdateListener(arg0 -> {
int value = (int) arg0.getAnimatedValue();
ViewGroup.LayoutParams layoutParams = v.getLayoutParams();
layoutParams.width = value;
v.setLayoutParams(layoutParams);
});
return animator;
}
首先我们构建一个ValueAnimator,添加了一个UpdateLisenter的方法,这个方法是在动画开始后返回值来让我们进行处理的,这个方法我们传入了三个参数分别是View,start,end,分别代表了我们创建的那个TextView,动画开始的宽度,最终的宽度。我们在updateListener里对view的宽度进行的修改。
private void show(View view, int tvWidth, long delay) {
view.setVisibility(View.VISIBLE);
ValueAnimator valueAnimator = createDropAnimator(view, 0, tvWidth);
valueAnimator.setDuration(500);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
Timer timer = new Timer();
TimerTask timerTask = new TimerTask() {
@Override
public void run() {
runOnUiThread(() -> disMiss(view, tvWidth));
}
};
timer.schedule(timerTask, delay);
}
});
valueAnimator.start();
}
这个是触发动画的方法,第三行调用了上述的ValueAnimator的构建方法,从0到width的一个变化过程。在打开动画结束后我们又延时delay ms调用了dismiss方法来把view收起来。
private void disMiss(View view, int tvWidth) {
ValueAnimator valueAnimator = createDropAnimator(view, tvWidth, 0);
valueAnimator.setDuration(500);
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
}
@Override
public void onAnimationEnd(Animator animation) {
view.setVisibility(View.GONE);
}
});
valueAnimator.start();
}
dismiss方法的思路和show的差不多,只不过是从width到0的一个过程。
进过上述的3个方法我们的动画基本上已经完成了
要踩的坑
1. show()方法何时调用(动画)何时开始?
如果你在onCreate里调用show方法,动画播放的过程是这样的。等你看见View的时候动画已经播放到一半了,为什么会这样?我们要了解Activity的生命周期。
onCreate和onStart调用之后我们还不能看见Activity的视图,所以调用之后动画已经绘制了一半了我们还没有看见。那我们放在onResume里再调用不就可以了吗?这样做可以,但是还是有问题,因为我们进入Activity时是有动画的,这个动画是和我们的动画一起执行的,所以还是会有可能看不全的(概率较小)。这里我们就要引入一个监听了,代码如下:
ViewTreeObserver viewTreeObserver = getWindow().getDecorView().getViewTreeObserver();
viewTreeObserver.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
show(mTextView,tvWidth,1000);
getWindow().getDecorView().getViewTreeObserver().removeOnPreDrawListener(this::onPreDraw);
return true;
}
});
这个方法监听了onDraw方法,在onDraw执行之前就会调用这个show方法保证我们的动画播放完全。
2.为什么横幅(View)有时候会闪一下?
有时候View会闪,就是因为View本来就是VISIBLE的,动画开始的时候回突然把view的宽度置为0,所以会闪一下。如何解决这个问题呢?答案就一句话:
在XML里把View的可见性设置为INVISIBLE或者GONE
3.这个TVWidth(View的宽度)如何获得呢?
众所周知,在OnCreate里是无法获取View的真实宽高的,那我们要如何解决这个问题呢?有人要说了,我百度了,用第一个问题里的监听方法就可以获取宽高。可是这个动画里你能获取到吗?答案是不能,第二个问题里我们已经把View的可见性设置为INVISIBLE或者GONE了,这两个方法View是不会绘制的,所以你根本无法获得View的宽高,那怎么解决呢?答案就是两个字:
计算
tvWidth = ScreenUtils.dip2px(this, (float) (paddingLeft + paddingRight)) +
ScreenUtils.sp2px(this, (float) ( 17))*s.length();
这里我提供了一个简单的思路,View的宽度就是paddingLeft + paddingRight + 字的宽度;看这个计算方法应该非常简单了。下面是ScreenUtils的三个方法:
public static int dp2px(Context context, float dpValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (dpValue * scale + 0.5F);
}
public static int px2dp(Context context, float pxValue) {
float scale = context.getResources().getDisplayMetrics().density;
return (int) (pxValue / scale + 0.5F);
}
public static int sp2px(Context context, float spValue) {
final float fontScale = context.getResources().getDisplayMetrics().scaledDensity;
return (int) (spValue * fontScale + 0.5f);
}
总结
至此,我们已经实现了这个简单的展开收回动画,简单易懂,如果可以的话可以帮我
点个赞吗?
如果有不懂的也可以在评论里问我,下次见拜拜!