博主声明:
转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。
本文首发于此 博主:威威喵 | 博客主页:https://blog.csdn.net/smile_running
Material Design 给出了一套标准的设计方案和动画效果,在 Android 开发方面,越来越多的 App 几乎都向 Material Design 风格靠拢,当然也是因为 Google 给出的设计效果确实好。我个人也非常喜欢这种简约而不失大方的风格,接下来我将通过一个案例来学习一下 Material Design 的一个跳转动画效果吧。
首先呢,大家肯定好奇是怎样的效果,先看看我实现的效果:
可以看到,点击 Fab 就会跳转到另一个 Activity,这其中有一段过渡动画,它是一种水波纹扩散效果,下面就是我将动画持续时间加到了 2S,方便大家仔细看看
其实,说是水波纹效果也没错,但它实际上是一个圆,通过不断地修改圆的半径,以达到一种波纹扩散的视觉效果。那么,我们来看看它是如何实现的吧。
相信学过动画和自定义 View 的人来说,这个效果其实并不难,无非是 draw 一个圆,然后通过属性动画不断的增大它的半径即可。下面我们直接看代码吧。
package nd.no.xww.mdanimationdemo;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
/**
* @author xww
* @desciption :
* @date 2020/1/17
* @time 14:13
*/
public class RippleView extends View {
private static final String TAG = "RippleView";
private Paint paint;
private float startX;
private float startY;
private float radius;
private onRippleListener onRippleListener;
private void init() {
paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(Paint.Style.FILL);
paint.setDither(true);
paint.setColor(getResources().getColor(R.color.colorAccent));
}
// 通过 ObjectAnimator 来开启动画,需要反射方式去设置 radius,因此要 setter() 方法
public void setRadius(float radius) {
this.radius = radius;
invalidate();
}
public RippleView(Context context) {
this(context, null);
}
public RippleView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public RippleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
public void startRippleAnimation(RipplePosition position) {
onRippleListener.rippleState(RippleState.RIPPLE_START);
this.startX = position.getX();
this.startY = position.getY();
float side = (float) Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2));
@SuppressLint("ObjectAnimatorBinding")
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", 0, side);
animator.setDuration(300);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
onRippleListener.rippleState(RippleState.RIPPLE_END);
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawCircle(startX, startY, radius, paint);
}
public interface onRippleListener {
void rippleState(int state);
}
public void addOnRippleListener(RippleView.onRippleListener onRippleListener) {
this.onRippleListener = onRippleListener;
}
}
如上代码就是我们的波纹效果,首先我们绘制一个圆,肯定需要知道它的圆心坐标以及半径。可以看到图中的动画起始处,其实是 Fab 空间的中间坐标,如下图:
那么,如何获取这个 x,y 坐标呢,其实很简单,通过 Fab 的 getWidth 和 getHeight 以及 getX 和 getY 方法配合即可。代码如下:
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.fab:
Intent intent = new Intent(this, PersonalActivity.class);
int startX = (int) (v.getX() + v.getWidth() / 2);
int startY = (int) (v.getY() + v.getHeight() / 2);
RipplePosition position = new RipplePosition(startX, startY);
intent.putExtra("position", position);
startActivity(intent);
// 取消系统默认的 Activity 跳转动画
overridePendingTransition(0, 0);
break;
}
}
获取的 position 后,然后传到另一个 Activity 即可。我们在另一个 Activity 中取得这个 position 值,然后调用
ripple_view.startRippleAnimation(position);
便可拿到圆心的坐标,然后开启动画,如下关键代码:
public void startRippleAnimation(RipplePosition position) {
onRippleListener.rippleState(RippleState.RIPPLE_START);
this.startX = position.getX();
this.startY = position.getY();
float side = (float) Math.sqrt(Math.pow(getWidth(), 2) + Math.pow(getHeight(), 2));
@SuppressLint("ObjectAnimatorBinding")
ObjectAnimator animator = ObjectAnimator.ofFloat(this, "radius", 0, side);
animator.setDuration(300);
animator.setInterpolator(new AccelerateInterpolator());
animator.start();
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
onRippleListener.rippleState(RippleState.RIPPLE_END);
}
});
}
这里的 ObjectAnimator 通过反射的方式调用自身的 radius 方法,不断的改变 radius 的值,所以通过反射的方式,就必须实现如下的 setter 方法,否则将反射失败。
// 通过 ObjectAnimator 来开启动画,需要反射方式去设置 radius,因此要 setter() 方法
public void setRadius(float radius) {
this.radius = radius;
invalidate();
}
动画执行过程中,每一次都需要重新绘制 View,所以要刷新一下,调用 invalidate() 方法。
好了,如上就是水波纹扩散动画的实现方式,这个效果我们以及初步完成了一半,接下来就是水波纹扩散过后的效果了,我把动画放慢了,再看看如何
首先呢,是头部的图片以及文字往下移动,然后中间那部分文字是从左往右平移的,最后是 RecyclerView 的一个 Item 加载时的动画效果,通过这几种动画组合在一起,以达到一种视觉上的效果,给用户良好的体验。
这几部分动画就比较简单了,通过平移、缩放和插值器结合一起完成。首先呢,需要注意的一点,这些动画都是在水波纹扩散动画之后才进行的,也就是我们需要监听水波纹动画结束,这里我们通过一个接口来监听,代码如下:
public interface onRippleListener {
void rippleState(int state);
}
public void addOnRippleListener(RippleView.onRippleListener onRippleListener) {
this.onRippleListener = onRippleListener;
}
这里的水波纹状态,初步设置为两种
package nd.no.xww.mdanimationdemo;
/**
* @author xww
* @desciption :
* @date 2020/1/17
* @time 14:36
*/
public class RippleState {
public static final int RIPPLE_START = 1;
public static final int RIPPLE_END = 2;
}
由于再水波纹扩散动画执行完成之前的时间段内,我们需要将那些图片、文字之类的 View 先进行隐藏,并且将这些 View 设置到平移之前的位置,这个很关键,因为它需要从起始处平移进来。
接着,在水波纹动画结束后,将 View 进行显示,并且此时开启平移动画,代码如下:
@Override
public void rippleState(int state) {
switch (state) {
case RippleState.RIPPLE_START:
rl_author.setVisibility(View.INVISIBLE);
iv_photo.setVisibility(View.INVISIBLE);
iv_photo.setTranslationY(-iv_photo.getHeight());
tv_name.setVisibility(View.INVISIBLE);
tv_name.setTranslationY(-tv_name.getHeight());
tv_blog.setVisibility(View.INVISIBLE);
tv_blog.setTranslationY(-tv_blog.getHeight());
ll_articles.setVisibility(View.INVISIBLE);
ll_articles.setTranslationX(-ll_articles.getWidth());
rv_favor.setVisibility(View.INVISIBLE);
ripple_view.setVisibility(View.VISIBLE);
break;
case RippleState.RIPPLE_END:
rl_author.setVisibility(View.VISIBLE);
iv_photo.setVisibility(View.VISIBLE);
iv_photo.animate().translationY(0).setDuration(300).setInterpolator(new AccelerateInterpolator()).start();
tv_name.setVisibility(View.VISIBLE);
tv_name.animate().translationY(0).setDuration(300).setInterpolator(new AccelerateInterpolator()).start();
tv_blog.setVisibility(View.VISIBLE);
tv_blog.animate().translationY(0).setDuration(300).setInterpolator(new AccelerateInterpolator()).start();
ll_articles.setVisibility(View.VISIBLE);
ll_articles.animate().translationX(0).setDuration(300).setInterpolator(new AccelerateInterpolator()).start();
rv_favor.setVisibility(View.VISIBLE);
ripple_view.setVisibility(View.GONE);
break;
}
}
这样的话,我们就基本完成了这个组合动画的实现了,还有就是在适配器中的动画,通过滑动 RecyclerView 时,会产生一种抖动的效果,关键代码如下:
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int i) {
holder.imageView.setImageResource(mData.get(i));
AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator scaleX = ObjectAnimator.ofFloat(holder.imageView, "scaleX", 0.95f, 1f);
ObjectAnimator scaleY = ObjectAnimator.ofFloat(holder.imageView, "scaleY", 0.95f, 1f);
animatorSet.playTogether(scaleX, scaleY);
animatorSet.setDuration(300);
animatorSet.setInterpolator(new AnticipateOvershootInterpolator());
animatorSet.start();
}
通过对 X 和 Y 的缩放,配合插值器,就可以达到这样的效果。
不过,也许你会发现你已经掉入了一个坑中,你会发现水波纹扩散动画,其实它是无法显示出来的。这个是什么原因呢,我们来分析一下。
我们在 onCreate() 方法中,直接开启水波纹动画,代码如下
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_personal);
initView();
position = (RipplePosition) getIntent().getSerializableExtra("position");
data = new ArrayList<>();
random = new Random();
for (int i = 0; i < 30; i++) {
data.add(pics[random.nextInt(9)]);
}
rv_favor.setLayoutManager(new GridLayoutManager(this, 3));
PictureAdapter adapter = new PictureAdapter(data);
rv_favor.setAdapter(adapter);
ripple_view.addOnRippleListener(this);
ripple_view.startRippleAnimation(position);
}
我们直接在 onCreate 中调用 ripple_view.startRippleAnimation(position) 是不行的,因为 View Tree 的绘制过程中,我们无法保证这个 View 在 onDraw 方法执行之前开启动画效果,所以它就无法显示出来。
那么,如何保证在 onDraw 方法执行之前将动画开启呢,这个 android 给我们提供了一个 api,我们可以通过如下代码,进行监听
ripple_view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
ripple_view.getViewTreeObserver().removeOnPreDrawListener(this);
ripple_view.startRippleAnimation(position);
return true;
}
});
此方法,就是当 onDraw 准备开始绘制的时候回调这个监听,所以我们要将 ripple_view.startRippleAnimation(position) 放在 onDraw 之前开启动画,并且这个监听需要进行移除,否则将一直处于监听中,阻塞主线程。好了,到此这个动画效果算是实现完成了,不过太多的动画效果势必会占用较大的性能。