手动实现布局过渡效果(Layout transition)- 第三部分

英文原文:Manual Layout Transitions – Part 3 

前面的文章中我们做到了创建两个不同的布局状态并能够在两者之间切换,本文,我们将让它们“动”起来。

我们已经有了所要创建动画的基本元素(AnimatorBuilder),我们也有了可以提供开始和结束状态的两个静态的布局,现在我们要做的只是让两个状态切换的时候呈现动画效果而已,雕虫小技嘛!

这两个布局具有相同的view,与view相关的id也是相同的,唯一不同的是两种状态下view的可见状态和位置,因此我们只需检测出这种改变的规则,然后把相应的translation或者alpha动画应用到每个view。要记住的是,由于我们inflate了新的布局,虽然两个布局中view的id和类型都是一致的,但是它们代表的是两个不同的view对象。同时,在我们过渡到新的布局之后,旧的布局已经不在范围之内,我们在此时无法判断旧布局中各控件的状态。因此我们需要一个保存指定view状态属性的机制:

public final class ViewState {
    private final int top;
    private final int visibility;
    public static ViewState ofView(View view) {
        int top = view.getTop();
        int visibility = view.getVisibility();
        return new ViewState(top, visibility);
    }
    private ViewState(int top, int visibility) {
        this.top = top;
        this.visibility = visibility;
    }
    public boolean hasMovedVertically(View view) {
        return view.getTop() != top;
    }
    public boolean hasAppeared(View view) {
        int newVisibility = view.getVisibility();
        return visibility != newVisibility && newVisibility == View.VISIBLE;
    }
    public boolean hasDisappeared(View view) {
        int newVisibility = view.getVisibility();
        return visibility != newVisibility && newVisibility != View.VISIBLE;
    }
    public int getY() {
        return top;
    }
}

这点很简单,因为我们只关心每个view的垂直偏移量以及可见状态。我们还有几个工具方法,用于计算新元素的差值。ps:这里应该说的是hasAppeared,hasDisappeared和hasMovedVertically三个方法吧,根据差值判断状态变化。

好了,现在我们有了可以保存离开场景的view对象状态的机制,让我们看看我们是如何使用它的。前面我们已经完成了实现布局切换的TransitionController,它调用activity的setContentView来实现布局的切换。因此我们要做的,就是在切换布局以前,捕获布局中将要被替换的view的状态。我们将使用TransitionAnimator类来计算动画需要的状态数据,并执行动画。Part3TransitionController就是这样的:

public class Part3TransitionController extends TransitionController {
    Part3TransitionController(WeakReference<Activity> activityWeakReference, AnimatorBuilder animatorBuilder) {
        super(activityWeakReference, animatorBuilder);
    }
    public static TransitionController newInstance(Activity activity) {
        WeakReference<Activity> activityWeakReference = new WeakReference<>(activity);
        AnimatorBuilder animatorBuilder = AnimatorBuilder.newInstance(activity);
        return new Part3TransitionController(activityWeakReference, animatorBuilder);
    }
    @Override
    protected void enterInputMode(Activity activity) {
        createTransitionAnimator(activity);
        activity.setContentView(R.layout.activity_part2_input);
    }
    @Override
    protected void exitInputMode(Activity activity) {
        createTransitionAnimator(activity);
        activity.setContentView(R.layout.activity_part2);
    }
    private void createTransitionAnimator(Activity activity) {
        ViewGroup parent = (ViewGroup) activity.findViewById(android.R.id.content);
        View inputView = parent.findViewById(R.id.input_view);
        View inputDone = parent.findViewById(R.id.input_done);
        View translation = parent.findViewById(R.id.translation);
        TransitionAnimator.begin(parent, inputView, inputDone, translation);
    }
}

这里与第二章不同之处在于多了个createTransitionAnimator()方法,它将调用findviewbyid找到当前布局中合适的view对象 - 其实具体找谁取决于你自己,总之是你想看到过渡效果的元素 - ,然后实例化一个TransitionAnimator。不管是在enterInputMode()还是在exitInputMode()方法中,createTransitionAnimator()都是在activity.setContentView()之前调用。

让我们来看看TransitionAnimator:

public final class TransitionAnimator implements ViewTreeObserver.OnPreDrawListener {
    private final ViewGroup parent;
    private final SparseArray<ViewState> startStates;
    private final AnimatorBuilder animatorBuilder;
    public static void begin(ViewGroup parent, View... views) {
        SparseArray<ViewState> startStates = buildViewStates(views);
        AnimatorBuilder animatorBuilder = AnimatorBuilder.newInstance(parent.getContext());
        final TransitionAnimator transitionAnimator = new TransitionAnimator(animatorBuilder, parent, startStates);
        ViewTreeObserver viewTreeObserver = parent.getViewTreeObserver();
        viewTreeObserver.addOnPreDrawListener(transitionAnimator);
    }
    private TransitionAnimator(AnimatorBuilder animatorBuilder, ViewGroup parent, SparseArray<ViewState> startStates) {
        this.animatorBuilder = animatorBuilder;
        this.parent = parent;
        this.startStates = startStates;
    }
    private static SparseArray<ViewState> buildViewStates(View... views) {
        SparseArray<ViewState> viewStates = new SparseArray<>();
        for (View view : views) {
            viewStates.put(view.getId(), ViewState.ofView(view));
        }
        return viewStates;
    }
    .
    .
    .
}

静态方法begin() 被TransitionController调用,所有的一切都是从这里开始的。

首先,在begin() 中调用buildViewStates(),它遍历传递进来的view数组,并通过ViewState的构造函数把它们一一放入ViewState对象中,这些ViewState对象保存在以View的id为索引的SparseArray中。接着实例化一个AnimatorBuilder对象(AnimatorBuilder的作用在前面已经讨论过,用于构造基本的属性动画),同时使用它,ViewStates,以及父布局的容器去构造一个TransitionAnimator的实例。

现在到了需要发挥点聪明才智的部分。我们已经捕获了即将消失的布局的view状态,但是现在需要的是在新布局创建的时候做点事情。如何做呢?不能直接在inflate的时候就去做,因为在测量过程完成之前我们无法得到子view的正确位置。但是我们可以为parent 容器注册OnPreDrawListener。它可以在下一次绘制的时候触发一个回调方法。当TransitionController要调用Activity的setContentView()时,在测量和布局传递完成之后,绘制开始之前,这个回调将被触发。

TransitionAnimator实现了ViewTreeObserver.OnPreDrawListener ,并且在begin()方法中被注册为OnPreDrawLister,这样onPreDraw() 方法就会在新布局绘制的时候被调用:

@Override
public boolean onPreDraw() {
    ViewTreeObserver viewTreeObserver = parent.getViewTreeObserver();
    viewTreeObserver.removeOnPreDrawListener(this);
    SparseArray<View> views = new SparseArray<>();
    for (int i = 0; i < startStates.size(); i++) {
        int resId = startStates.keyAt(i);
        View view = parent.findViewById(resId);
        views.put(view.getId(), view);
    }
    Animator animator = buildAnimator(views);
    animator.start();
    return false;
}

首先做的事情就是取消自身的注册 - 我们不想这个操作在每次绘制的时候都发生 - 我们只想在替换布局,开始布局过渡动画的时候才触发。

下一件事情就是遍历代表初始ViewStates的SparseArray。并且找到当前布局中和每个被保存状态相匹配的view,然后将这个view数组传递给buildAnimator():

private Animator buildAnimator(SparseArray<View> views) {
    AnimatorSet animatorSet = new AnimatorSet();
    List<Animator> animators = new ArrayList<>();
    for (int i = 0; i < views.size(); i++) {
        int resId = views.keyAt(i);
        ViewState startState = startStates.get(resId);
        View view = views.get(resId);
        animators.add(buildViewAnimator(view, startState));
    }
    animatorSet.playTogether(animators);
    return animatorSet;
}

注:“并且找到当前布局中和每个被保存状态相匹配的view”这里的意思其实就是根据以前的id找到新布局中的view对象,此时的view是代表新布局中的view了,它将被用来和ViewStates中的对象相比较。

这里将创建一个包含了所有单个view属性动画的AnimatorSet。这些动画将同时播放。每个view都使用buildViewAnimator() 来构造一个相应的Animator:

private Animator buildViewAnimator(final View view, ViewState startState) {
    Animator animator = null;
    if (startState.hasAppeared(view)) {
        animator = animatorBuilder.buildShowAnimator(view);
    } else if (startState.hasDisappeared(view)) {
        final int visibility = view.getVisibility();
        view.setVisibility(View.VISIBLE);
        animator = animatorBuilder.buildHideAnimator(view);
        animator.addListener(
                new AnimatorListenerAdapter() {
                    @Override
                    public void onAnimationEnd(@NonNull Animator animation) {
                        super.onAnimationEnd(animation);
                        view.setVisibility(visibility);
                    }
                });
    } else if (startState.hasMovedVertically(view)) {
        int startY = startState.getY();
        int endY = view.getTop();
        animator = animatorBuilder.buildTranslationYAnimator(view, startY - endY, 0);
    }
    return animator;
}

这里通过ViewState对象的工具类计算每个view的过渡类型。又三种可能的过渡类型:不可见的变成可见的,可见的变成不可见的,垂直移动的。每种类型我们都构造一个相应的Animator。

ok,运行之后可以看到过渡效果还不错:

Manual Layout Transitions - Part 3 (1).mp4_1435538933.gif

现在一切都工作的很顺利,不过存在一个非常重要的问题:开始布局中的所有view在结束布局中都必须要有相应的view与之对应,反之亦然。不过这个问题总会解决,在下篇文章中,我们将继续深入的探讨。

本文的源代码在这里

作者 Mark Allison。保留所有权利。本文最先发表在 Styling Android