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

布局切换动画在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。但是这不是并不是所有的情况下都会这样。在下一篇文章中我们看看如何适配这个特定的场景。

源代码在这里

Android安全概述

Android 安全架构的理解不仅帮助我了解 Android 的工作原理,而且为我开启了如何构建移动操作系统和 Linux 的眼界。 本章从安全角度讲解 Android 架构的基础知识。 在第 1....

发布于:1年以前  |  1630次阅读  |  详细内容 »

Android Linux 内核层安全

作为最广为人知的开源项目之一,Linux 已经被证明是一个安全,可信和稳定的软件,全世界数千人对它进行研究,攻击和打补丁。 不出所料,Linux 内核是 Android 操作系统的基...

发布于:1年以前  |  1378次阅读  |  详细内容 »

Android 本地用户空间层安全

本地用户空间层在 Android 操作系统的安全配置中起到重要作用。 不理解在该层上发生了什么,就不可能理解在系统中如何实施安全架构决策。 在本章中,我们的主题是 Android ...

发布于:1年以前  |  1109次阅读  |  详细内容 »

Android 框架层安全

如我们在第1.2节中所描述的那样,应用程序框架级别上的安全性由 IPC 引用监视器实现。 在 4.1 节中,我们以 Android 中使用的进程间通信系统的描述开始,讲解这个级别上的...

发布于:1年以前  |  1312次阅读  |  详细内容 »

Android 应用层安全

虽然在这一节中我们描述了应用层的安全性,但是实际的安全实施通常出现在到目前为止描述的底层。 但是,在介绍应用层之后,我们更容易解释 Android 的一些安全功能。 5.1 ...

发布于:1年以前  |  1957次阅读  |  详细内容 »

Android 安全的其它话题

在本章中,我们会涉及到与 Android 安全相关的其他主题,这些主题不直接属于已经涉及的任何主题。 6.1 Android 签名过程 Android 应用程序以 Android 应用包文件(.apk文件...

发布于:1年以前  |  1511次阅读  |  详细内容 »

数据绑定(Data Binding)-Part5

原文链接 : Data Binding - Part 5 直到现在,我们已经见识到Data Binding的很多功能了。但是还有一个很强大的特点没有介绍,那就是观察者模式的应用。 观察者模式在数据会...

发布于:1年以前  |  1020次阅读  |  详细内容 »

在Android 5.0中使用JobScheduler

原文链接 : using-the-jobscheduler-api-on-android-lollipop 在这篇文章中,你会学习到在Android 5.0中如何使用JobScheduler API。JobScheduler API允许开发者在符合某些...

发布于:1年以前  |  907次阅读  |  详细内容 »

Code Review最佳实践

原文链接 : Code Review Best Practices 在Wiredrive上,我们做了很多的Code Review。在此之前我从来没有做过,这对于我来说是一个全新的体验,下面来总结一下在Code Revie...

发布于:1年以前  |  1118次阅读  |  详细内容 »

从Dex到源代码(伪代码)

这个系列的头两篇文章中,我写了两篇关于APK format和aapt tool的文章. 在这篇文章中我将重点讲述dex2jar,它是一个作用于Android .dex文件和Java .class文件的工具。已经...

发布于:1年以前  |  1297次阅读  |  详细内容 »

第五章 基于空间的架构

第五章 基于空间的架构 大多数基于网站的商务应用都遵循相同的请求流程:一个请求从浏览器发到web服务器,然后到应用服务器,然后到数据库服务器。虽然这个模式在用户数不...

发布于:1年以前  |  1115次阅读  |  详细内容 »

Retrofit指南

这是一篇关于如何使用Retrofit写一个Android的REST客户端的小教程。 我为什么选择Retrofit? 在使用square的Retrofit之前,我尝试过Volley和AsyncTask。但在使用过Retrofi...

发布于:1年以前  |  1322次阅读  |  详细内容 »

使用Mockito对异步方法进行单元测试

原文链接 : Unit testing asynchronous methods with Mockito 之前我拍着胸脯承诺要维护的我博客,因此才有了这篇文章。但是请忘记我的那些承诺,我今天要写的是关于Mockit...

发布于:1年以前  |  1095次阅读  |  详细内容 »

Android中调试RxJava

原文链接 : Debugging RxJava on Android 调试是查找和分析bug的过程或者预防软件的正确操作出现问题Wikipedia。 当前调试不是一件容易的事情,我们在处理Android的异步操...

发布于:1年以前  |  709次阅读  |  详细内容 »

国内优秀Android学习资源

技术博客 应用开发 博主 博客 备注 任玉刚 CSDN博客 深入Android应用开发,深度与广度兼顾 郭霖 CSDN博客 内容实用,行文流畅,高人气博主 夏安明 CSDN博客 ...

发布于:1年以前  |  1150次阅读  |  详细内容 »

最多阅读

简化Android的UI开发 6月以前  |  166466次阅读
Google Enjarify:可代替dex2jar的dex反编译 1年以前  |  2148次阅读
Android设计与开发工作流 6月以前  |  2107次阅读
Android多渠道打包工具:apptools 1年以前  |  1847次阅读
Android权限 - 第一篇 1年以前  |  1820次阅读
Google Java编程风格规范(中文版) 1年以前  |  1787次阅读
Stetho 1年以前  |  1730次阅读
Android UI基本技术点 1年以前  |  1716次阅读
30分钟搭建一个android的私有Maven仓库 1年以前  |  1631次阅读
2015 Google IO带来的新 Android 开发工具 1年以前  |  1624次阅读
你应该知道的布局和属性 1年以前  |  1575次阅读
Gradle小知识#3:任务的顺序 1年以前  |  1564次阅读
听FackBook工程师讲*Custom ViewGroups* 1年以前  |  1564次阅读
MVP在Android平台上的应用 1年以前  |  1557次阅读