手动实现布局 Transitions 动画 第三部分

发布于 2024-10-25 16:25:21 字数 8890 浏览 8 评论 0

布局切换动画在 Material design 中是一个重要的方面,因为它们能够指明应用的工作流程,并且能够将 UI 上的可视化元素绑定在一起作为用户的导航。两个重要的工具可以实现这种效果,分别为 Activity 转场动画和布局动画(Layout Transitions)。然后布局动画需要在 API 19 及其之后才支持。

上一篇文章中我们创建了两个布局代表两个视图状态,我们通过 setContentView 来切换它们。这篇文章我们在它们切换时添加动画效果。

我们已经找掌握了关于动画的相关基础知识,现在我们就要在两个布局状态切换时加入动画。

我们定义的两个布局都有相同的 View 以及 id,两个状态的切换只是会修改这些视图的可见性以及位置。因此我们仅仅需要检测这些自然变化,然后应用合适的位置变换或者 alpha 动画到每个视图上。值得注意的是,由于我们加载了一个新的布局,但是两个布局中的视图类型和 id 都是一样的,它们代表的是两个不同的视图对象。此时,我们需要切换到一个新的布局视图中,旧的布局视图就不会出现在我们的视野中,所以我们不能确定旧布局视图的控件状态。因此我们需要一种机制来存储旧布局中特定 View 的状态属性。

part3/ViewState.java

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;
    }
}

这段代码非常简单,因为我们只关心各视图的竖直偏移量和可见性。此外也就是写辅助方法以便于我们能够确定视图对象是否发生了改变。

此时我们已经有了一套机制来存储不在可见范围的视图的状态,下面我们来看看我们如何运用的。当我们调用 setContentView 时,通过 TransitionController 我们已经有了切换布局的机制。下一步我们需要做的就是在我们切换布局之前捕获这些视图的状态,并且替换掉。我们会通过 TransitionAnimator 类来实现这些功能,它会计算并且执行动画。Part3TransitionController 类的代码如下 :

part3/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 函数来查找视图,并且调用了 TransitionAnimator 的 begin 函数。这个函数在 enterInputMode 和 exitInputMode 函数中调用 Activity 的 setContentView 之前被调用。你需要注意的是 TransitionAnimator 只是在两个视图状态之间进行切换,因此除了那些我们感兴趣的视图之外我们不需要对这两个布局有额外的了解。

我们看看 TransitionAnimator 类 :

So let’s take a look at 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;
    }
    .
    .
    .
}

在 TransitionController 类中调用 TransitionAnimator 的 begin 函数来做一些准备。

在 begin 函数中首先会调用 buildViewStates 函数来遍历所有传递进来的视图,并且将这些视图的状态以视图 id 为 key 存储到 SparseArray 对象中。然后通过 AnimatorBuilder 对象和 parent 视图和存储了视图状态的 SparseArray 对象来创建一个 TransitionAnimator 实例。

现在的代码看起来聪明一点了。在旧布局还没有从我们的视野中消失时我们捕获了它的视图状态,但是需要在新的布局创建之前我们现在需要做些其他的事情。我们不能简单的加载一个布局并且运用它,因为布局中的子视图可能还在错误的位置,直到我们经过了测量和布局两个过程之后它们才会在正确的位置。但是现在我们做的只是在 parent 容器中注册了一个 OnPreDrawListener.这使得我们在 parent 下次绘制之前能够触发一个 OnPreDrawListener 回调。当 TransitionController 类中调用 setContentView 函数时,这个回调会在新布局被加载、测量和布局过程完成之后被调用一次,但是这个调用会执行在视图绘制之前。

TransitionAnimator 类实现了 ViewTreeObserver.OnPreDrawListener,并且被注册为 OnPreDrawLister。它的 onPreDraw 函数会在新布局会绘制前调用。onPreDraw 函数如下 :

part3/TransitionAnimator.java

@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;
}

在 onPreDraw 函数中首先将 TransitionAnimator 自身从 ViewTreeObserver 中注销,因为我们不需要在每次绘制之前都回调 onPreDraw 函数。如果我们忘记了注销那么它会变得相对重量级,以至于会影响动画的流畅度。我们只需要在替换布局时回调一次 onPreDraw 函数,然后我们会在次函数中开始执行切换动画。

下一步我们要做的是迭代前面构建的 SparseArray 中的 ViewStates,从 ViewStates 中取出视图 id,然后根据这个 id 到 parent 中找到对应的视图,最后将视图存储到另一个 SparseArray 对象中。最后将这个 SparseArray 对象传递给 buildAnimator 函数。

part3/TransitionAnimator.java

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;
}

这会构建一个包含了所有独立视图 Animator 的集合,这些 Animator 会并行的执行。在构建合适的 Animator 时会又调用 buildViewAnimator 函数。

part3/TransitionAnimator.java

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 中的辅助方法确定每个视图的转换的类型。这些转换类型有三种,分别为一个 invisible 的视图变为 visible、一个 visible 的视图变为 invisible、在 y 轴上移动视图。每个转换动画我们都会构建一个对应的 Animator 对象。

如果此时我们运行这个示例,我们会看到很好的效果。 视频地址

这些代码能够很好的工作,但是有一个明显的问题它需要起始布局中的所有的视图在结束布局都有对应的视图,也就是两个布局中都含有类型和 id 的子 view。但是这不是并不是所有的情况下都会这样。在下一篇文章中我们看看如何适配这个特定的场景。

源代码在 这里

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

笙痞

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

謌踐踏愛綪

文章 0 评论 0

开始看清了

文章 0 评论 0

高速公鹿

文章 0 评论 0

alipaysp_PLnULTzf66

文章 0 评论 0

热情消退

文章 0 评论 0

白色月光

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文