本文简介
使用apt实现页面跳转共享动画,共享动画可以分别设置进入动画时间、退出动画时间、进入动画插值器、退出动画的插值器、进入动画的优先级、退出动画的优先级。多个共享动画,同级别会并行执行,不同级别会串行执行。
TODO
-
实现Activity跳转参数标识,共享元素标识; -
多个共享元素执行动画 -
跳转动画可自定义实现 -
添加动画执行状态回调 -
设置动画串并行逻辑
效果展示
- 上图的共享元素属性
- 上图的共享元素属性
需要掌握的知识
什么是apt
APT(Annotation Processing Tool) 是javac的一个工具,可以在编译期间扫描和处理注解(Annotation),获得注解和被注解的对象的相关信息,这些信息可以用于生成符合业务的代码,不需要手动编写一些业务代码,提升了开发效率。这些事情都是编译器就做的,对比传统反射方式在运行时做操作来说,apt提升了程序的性能。
共享动画
Material Design 应用中的 Activity 过渡通过常见元素之间的动画和转换效果,在不同状态之间建立视觉联系。您可以为进入和退出过渡,以及 Activity 之间共享元素的过渡指定自定义动画。 共享动画是适用于Android5.0及更高版本的Activity的过渡共享元素动画,可以定义两个Activity中不同的View使用一个共同的android:transitionName,在页面跳转是会有视觉联系。Android For Developers
实现步骤
首先我们看一下项目模块:
- annotations:存放注解的java模块
- compiler:我们的注解处理器模块,在编译过程中,会执行我们相应的注解处理,生成符合业务需求的java或者kotlin代码。
- runtime:定义一些工具类处理Android动画相关,页面跳转相关。
- app:测试模块
编写annotations模块
我们第一步要编写我们的注解,定义我们需要的信息,在这里我们需要Activity相关信息,跳转Activity时Intent相关信息,还有就是共享元素的相关信息。
编写注解的时候我们会用到两个注解,一个是@Target,另一个是@Retention。其中@Target标明的是注解的目标,@Retention表明的是注解的保留位置。
@Target参数含义
- ElementType.TYPE:用于类或接口上;
- ElementType.FIELD:用于类的成员变量上;
- ElementType.METHOD:用于方法上;
- ElementType.PARAMETER:用于方法参数上;
- ElementType.CONSTRUCTOR:用于构造方法上;
- ElementType.LOCAL_VARIABLE:用于局部变量上;
- ElementType.ANNOTATION_TYPE:用于注解上;
- ElementType.PACKAGE:用于记录java文件的package信息;
- ElementType.TYPE_PARAMETER:用于标注泛型的类型参数;
- ElementType.TYPE_USE:标注各种类型;
@Retention参数含义
- RetentionPolicy.SOURCE:保留在原文件,不会生成class字节码;
- RetentionPolicy.CLASS:保留在class文件中,运行时无法获得;
- RetentionPolicy.RUNTIME:保留到运行时,运行时可以获得;
构建Annotation
- 我们希望处理器只处理我们标注的Activity,所以定义的Builder.class:
- 我们定义跳转必须需要的参数annotation:
- 定义跳转可选参数annotation(含有默认值):
- 定义共享元素相关信息annotation:
- 定义入场动画执行完毕回调annotation:
编写注解处理器
定义BuilderProcessor
BuilderProcessor.kt继承于AbstractProcessor实现相关方法实现对自定义接口处理。
-
其中getSupportedAnnotationTypes方法用于定义处理器需要处理什么类型的注解,也可以使用@SupportedAnnotationTypes注解来定义。
-
其中process方法用于我们收集注解及其注解目标的信息,以便后续生成代码使用。 在这里我们做的逻辑是:
- 收集所有标注Builder注解的Activity类信息到一个Map。
- 分别收集@Required、@Optional、@SharedElement、@RunEnterAnim注解的信息,加入到Map中相应Builder标注的Activity信息当中。
- 遍历Map,使用Activity的信息生成相应Builder类用于辅助调用。
Entity类
ActivityClass
- 其中fields存放相关成员变量信息(必要参数、可选参数、共享元素信息)。
- startMethodName存放动画开始回调方法名字。
- endMethodName存放动画结束回调方法名字。
Field
这里记录成员变量的基本信息,实现Comparable接口来排序。
OptionalField
OptionalField继承于Field类,重写了前缀名,重新了排序逻辑,只和OptionalField进行排序,其他让行,增加了默认值。
SharedElementField
SharedElementField用于共享元素的信息记录。
编写runtima模块
该模块定义一些工具类处理Android动画相关,页面跳转相关
定义View信息类
要把View的信息通过Intent传递,需要进行序列化,所以我们要实现Parcelable接口,在View信息类当中要记录上一个activity中View的信息和跳转activity中View的id,还有执行动画的一些参数也要在这里传递过去,所以ViewAttrs类如下:
状态保存实现
为了实现状态保存,我们要拿到Application,然后registerActivityLifecycleCallbacks,来监听Activity的生命周期,在onActivitySaveInstanceState回调中保存当前页面数据,在onActivityCreated回调中重新加载数据。在ActivityBuilder类中定义了startActivity方法,判断上下文是否为Activity,如果不是的话,需要给Intent添加Intent.FLAG_ACTIVITY_NEW_TASK。
共享动画优先级执行实现
共享动画优先级执行逻辑的实现是通过优先队列(PriorityQueue)对所以共享元素进行排序,按照同级并行,不同级串行的逻辑执行动画。记录每一级所需要的最大时间,求出最大总时间,延迟发送动画完成回调。
注意:在入场动画进行时,执行退场动画会出现View乱动的问题,这是因为我们动画使用的是View.animation()方法设置的属性动画,如果在一个View上面同时执行两个动画,会出现属性值变化错乱。 在这里我加了一个volatile属性来控制在执行入场动画的时候不可以退出,来防止这个问题出现。
使用javapoet、kotlinpoet生成代码
javapoet是JakeWharton大神开发的一个框架,是一个可以让我们更加方便的生成java源文件的框架,不需要在用StringBuilder一直append()个不停。而kotlinpoet则是javapoet的生成kotlin源文件版本。在apt中我们使用这两个框架生成相关代码。
ActivityClassBuilder
这个类用来生成各个标注@Buidler的ActivityNameBuilder.java类,在这里TypeSpec.classBuilder(name)用来创建java类,addModifiers用来定义类的修饰,使用ConstantBuilder类来生成类的成员变量,使用startMethodBuilder生成start方法,使用SaveStateMethodBuilder生成保存状态方法,InjectMethodBuilder生成注入方法,RunExitAnimMethodBuilder生成退出动画方法,最后调用build()方法构建完成类,然后写入文件。
InjectMethodBuilder
在这里我们只例举一个方法生成来具体说明如何生成方法的。
- MethodSpec.methodBuilder(name)来创建一个名字为name的方法;
- addParameter(type,name)是添加一个参数,type为类型,name为参数名字;
- addModifiers是添加方法修饰
- addStatement是添加具体代码,$定义变量,如果后面跟着的是T,则代表变量是类,是L则是把本身带入,是S则是代表需要带入字符串。
- beginControlFlow条件控制语句需要用此方法添加,不要忘记在条件控制之行结束的时候调用endControlFlow,每一个beginControlFlow都要对应调用endControlFlow。
测试app模块引入使用
- 在app测试模块的build.gradle文件中引入相关模块
- 然后在需要使用共享元素的Activity类中使用我们的注解
- 在build项目之后,会在/app/build/generated/source/kapt/debug(release)中找到我们自动生成的DetailsActivityBuilder
- 在mainActivity中调用生成的代码进行跳转
彩蛋
用户无感知注入框架
在使用leakcanary的时候,发现这个框架只需要引入就可以使用,很是好奇他是怎么做到的。拜读了源码之后发现是使用ContentProvider来实现自动化注入的,具体方法如下:
- 在AndroidManifest.xml中注册provider
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.sniperking.eatradish.runtime">
<application>
<provider
android:authorities="${applicationId}.ese"
android:name="com.sniperking.runtime.InstallRunTime"
android:enabled="true"
android:multiprocess="true"
android:exported="false"/>
</application>
</manifest>
- 编写ContentProvider
class InstallRunTime : ContentProvider() {
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun query(
uri: Uri,
projection: Array<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
sortOrder: String?
): Cursor? {
return null
}
override fun onCreate(): Boolean {
val application = context!!.applicationContext as Application
ActivityBuilder.INSTANCE.init(application)
return true
}
override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?
): Int {
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
}
}
在这里其他方法都不做处理,只处理onCreate()方法,来做框架的初始化,这样就可以做到用户无感知注入框架,很方便。