Android Material Design 之 Activity 跳转水波纹扩散动画

博主声明:

转载请在开头附加本文链接及作者信息,并标记为转载。本文由博主 威威喵 原创,请多支持与指教。

本文首发于此   博主威威喵  |  博客主页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 之前开启动画,并且这个监听需要进行移除,否则将一直处于监听中,阻塞主线程。好了,到此这个动画效果算是实现完成了,不过太多的动画效果势必会占用较大的性能。

发布了101 篇原创文章 · 获赞 766 · 访问量 87万+

猜你喜欢

转载自blog.csdn.net/smile_Running/article/details/104024391