ribot 致力于打造美好且充满意义的用户体验,在这一过程中,动画不可或缺

探索安卓中有意义的动画! 技术分享 第1张

在 Droidcon London 听完一场 激励人心的演讲之后, 笔者决定深入研究安卓动画。本文集中展示了其研究结果,希望使开发者和设计者们意识到,为 Android 应用添加漂亮的动画并不复杂。

探索安卓中有意义的动画! 技术分享 第2张

动画!

如果你想尝试这些动画效果,本文所有实例都能在 Github 上的这款 Android 应用 中找到。

笔者非常喜欢动画效果,因为它不仅提高用户参与度,还能迅速夺人眼球。想想那些以动画设计著称的应用,它们使用起来是多么可心、流畅、自然

探索安卓中有意义的动画! 技术分享 第3张

Falcon Pro:即使细微的动画效果也可以对用户体验产生巨大影响。

现在,与那些你很喜欢但没有动画的应用做一番比较。

探索安卓中有意义的动画! 技术分享 第4张

Medium: 尽管笔者很喜爱 medium APP,但它的确缺少恰当的动画。

小动作也能造就大不同

我们可以从多个方面利用动画,从而: * 通过导航上下文传输用户; * 强化元素的层级结构; * 展示屏幕显示的组件变化。

本文旨在说明,在应用中实现有意义的动画十分简单可行——那么,即刻开始吧。

触觉反馈

在用户触摸屏幕时提供反馈,有助于视觉交流,形成互动。这些动画不应分散用户的注意力,但又使他们享受其中,获得清晰的视感,从而鼓励进一步操作。

安卓框架为此类反馈提供了波纹效果,通过设定视图背景,即可使用:

?android:attr/selectableItemBackground-在视图范围内展示波纹效果;

探索安卓中有意义的动画! 技术分享 第5张

波纹在接触点开始,之后填充整个视图背景。

?android:attr/selectableItemBackgroundBorderless –将波纹效果延伸至视图之外。

探索安卓中有意义的动画! 技术分享 第6张

圆形波纹效果在接触点开始,并沿半径延伸至视图之外。

View Property Animator

ViewPropertyAnimator 在 API 12 首次引入,允许我们只使用一个Animator实例,就可以简单高效地使多个视图属性(并行地)执行动画操作。

探索安卓中有意义的动画! 技术分享 第7张

此处将绘制下文提到的所有动画属性。

注意: 如果已在视图中设置了侦听器,并打算在相同视图下,实现其他动画且不使用回调函数,则需要将侦听器设为 null

用程序实现时,简单又整洁:

 mButton.animate()
        .alpha(1f)
        .scaleX(1f)
        .scaleY(1f)
        .translationZ(10f)
        .setInterpolator(new FastOutSlowInInterpolator())
        .setStartDelay(200)
        .setListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) { }

            @Override
            public void onAnimationEnd(Animator animation) { }

            @Override
            public void onAnimationCancel(Animator animation) { }

            @Override
            public void onAnimationRepeat(Animator animation) { }
        })
        .start();

注意: 其实我们不需要在动画生成器中调用 start( ) 方法,因为在停止声明的同时,动画就会自动启动。在这种情况下,只有在 UI toolkit 事件队列开始下一次更新时,动画才会再开始。

探索安卓中有意义的动画! 技术分享 第8张

制作 FAB 的 alpha 动画值

探索安卓中有意义的动画! 技术分享 第9张

FAB 的(X 和 Y 轴)坐标动画

探索安卓中有意义的动画! 技术分享 第10张

FAB 的 Z 坐标动画

注意: 考虑到向后兼容性,你可以使用ViewCompat 类,来实现在安卓 API 4 以及以上版本的ViewPropertyAnimator 类。

Object Animator

ViewPropertyAnimator 类似,ObjectAnimator 允许我们在目标视图(代码和 XML 源文件中)的不同属性中执行动画。然而,它们还是有些差异的:

  • 在每个实例中,ObjectAnimator 只允许对单一属性执行动画。例如,坐标 Y坐标 X 变化;
  • 但是,它允许自定义属性的动画,例如视图的前景色

使用自定义属性给视图做缩放动画,并改变其前景色,可以达成下图的效果:

探索安卓中有意义的动画! 技术分享 第11张

使用自定义属性时,可以通过调用ObjectAnimator.ofInt(),创建一个ObjectAnimator实例,此处我们声明:

  • view – 应用动画的视图;
  • property – 设定动画的属性;
  • start color – 动画视图的初始颜色;
  • target color – 动画视图的目标颜色。

接下来,设置评估器(此处使用ArgbEvaluator 设置颜色动画),设置延迟并执行 start( )

private void animateForegroundColor(@ColorInt final int targetColor) {
    ObjectAnimator animator = 
        ObjectAnimator.ofInt(YOUR_VIEW, FOREGROUND_COLOR, Color.TRANSPARENT, targetColor);
    animator.setEvaluator(new ArgbEvaluator());
    animator.setStartDelay(DELAY_COLOR_CHANGE);
    animator.start();
}

接下来,使用相似的方法做视图缩放的动画,主要区别在于:

  • 使用ObjectAnimator.ofFloat() 创建 ObjectAnimator 实例,因为在调整视图大小时,并没有改动整型值;
  • 使用 View.SCALE_X 和 View.SCALE_Y 视图属性,而非自定义属性。
    private void resizeView() {
        final float widthHeightRatio = (float)     getHeight() / (float) getWidth();
        resizeViewProperty(View.SCALE_X, .5f, 200);
        resizeViewProperty(View.SCALE_Y, .5f / widthHeightRatio, 250);
    }

    private void resizeViewProperty(Property<View, Float> property,
                                    float targetScale, 
                                    int durationOffset) {
        ObjectAnimator animator = ObjectAnimator.ofFloat(this, property, 1f, targetScale);
        animator.setInterpolator(new LinearOutSlowInInterpolator());
        animator.setStartDelay(DELAY_COLOR_CHANGE + durationOffset);
        animator.start();
    }

最后,将调整完大小的视图移开屏幕。在这种情况下,使用AdapterViewFlipper 容纳离屏视图,可以对 ViewFlipper 实例调用showNext()方法,后者会使用(定义好的动画处理该过程。接着,下一个视图也会使用定义好的入场动画,自动出现在屏幕上。

Interpolators

Interpolator 可用于定义动画的变化率,意味着动画的速度、加速度、行为都可以改变。可用的 interpolator 有数种,且相互之间的差别微乎其微,建议读者在设备上一探究竟。

探索安卓中有意义的动画! 技术分享 第12张

该视图以线型动作开始和结束动画。

探索安卓中有意义的动画! 技术分享 第13张

该视图开始动作很快,逐渐降速直至结束。

探索安卓中有意义的动画! 技术分享 第14张

该视图以线型动作开始,逐渐降速直至结束。

探索安卓中有意义的动画! 技术分享 第15张

该视图在动画开始时加速,并在接近结束时逐渐减速。

Circular Reveal

CircularReveal 使用剪切的圆形显示或隐藏一组 UI 元素。该动画除了带来视觉上的连续性,还十分赏心悦目,有助于提高用户参与度。

探索安卓中有意义的动画! 技术分享 第16张

如上图所示,在视图的动画效果显示之前,使用 ViewPropertyAnimator 隐藏浮动操作图标。只需定义如下属性就可以配置 circular reveal:

  • startView – CircularReveal 的开始视图(即压缩视图);
  • centerX –点击视图的 X轴中心;
  • centerY -点击视图的 Y轴中心;
  • targetView –要显示的视图;
  • finalRadius –剪切圆的半径,大小等于以 X 中心和 Y 中心为直角边的三角形的斜边的值。
int centerX = (startView.getLeft() + startView.getRight()) / 2;  
int centerY = (startView.getTop() + startView.getBottom()) / 2;  
float finalRadius = (float) Math.hypot((double) centerX, (double) centerY);  
Animator mCircularReveal = ViewAnimationUtils.createCircularReveal(  
  targetView, centerX, centerY, 0, finalRadius);

窗口转换

定制用于活动间导航的转换,可使用户对应用状态产生更为强烈的视觉联系。默认情况可定制如下转换:

  • enter –决定活动视图如何进入场景;
  • exit -决定活动视图如何退出场景;
  • reenter –决定活动视图退出后如何再度进入;
  • shared elements –决定活动间如何共享视图转换。

API 21起,还有如下几种新的转换方式:

爆炸

Explode 转换允许视图从屏幕各个方位退出,会使压缩视图产生爆炸效果。

探索安卓中有意义的动画! 技术分享 第17张

在网格布局中爆炸效果尤其好。

这种效果易于实现——首先,需要在 res/transition 目录中创建如下转换:

<explode xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="300"/>

具体做法如下:

  • 声明 explode 转换;
  • 设置持续时间为300毫秒。

接下来,需要将此设置为活动的转换。既可以将其添加到活动主题:

<style name="AppTheme.Explode" parent="AppTheme.NoActionBar">
  <item name="android:windowExitTransition">@transition/slide_explode</item>
  <item name="android:windowReenterTransition">@android:transition/slide_top</item>
</style>

也可以编程的方式解决:

Transition explode = TransitionInflater.from(this).inflateTransition(R.transition.explode);
getWindow().setEnterTransition(explode);

滑动

滑动切换可以使活动从屏幕右侧或底部滑入/出。可能你以前有过类似的效果,但是这个新切换更加灵活。

探索安卓中有意义的动画! 技术分享 第18张

滑动切换使我们依次滑动子视图

这种转换在切换活动时尤为常见,笔者对向右侧滑的流畅感觉情有独钟,当然这也很容易创建:

<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/decelerate_cubic"
    android:slideEdge="end"/>

在这里: - 声明了 slide 转换; - 设置切换的slideEdgeend(右侧),从而实现从右侧开始滑动——若想要底部滑动将设置为 bottom

渐变

渐变切换使活动转换出现淡入或淡出的效果。

探索安卓中有意义的动画! 技术分享 第19张

在视图中使用渐变动画操作简单,且效果宜人。

创建此切换的操作比之前的切换更加简单:

<fade xmlns:android="http://schemas.android.com/apk/res/android"  
        android:duration="300"/>

在这里: - 声明了 fade 转换; - 设置持续时间为300毫秒。

优化转换

实验的同时,笔者发现了一些可以改善上述转换效果的方法。

允许窗口页面转换——需要在主题中启用下列属性,主题都来源于一个资料主题:

<item name="android:windowContentTransitions">true</item>  

启用/禁用转换重叠——上一转换过程结束,新的页面动画才会开始,这样就会形成时延。在不同的案例中,若启用如下属性,转换过程都会更加流畅自然:

<item name="android:windowAllowEnterTransitionOverlap">true</item>  
<item name="android:windowAllowReturnTransitionOverlap">true</item>  

排除特定视图转换—有时我们并不想让活动中的所有视图参与动画,而且大多数情况下,工具栏和状态栏是造成转换故障主因。所幸,可以排除特定的视图,使之无法转换:

     <explode xmlns:android="http://schemas.android.com/apk/res/android"
     android:duration="200">
        <targets>
            <target android:excludeId="@android:id/navigationBarBackground"/>
            <target android:excludeId="@android:id/statusBarBackground"/>
        </targets>
      </explode>

工具栏和操作栏——当使用操作栏的活动向使用工具栏的活动转换时(反之亦然),转换过程总是磕磕绊绊。为此,应当确保转换中的两个活动都使用相同的组件。

转换持续时间——既不能让用户等太久,也不能让动画转换过快。这取决于转换持续时间,最好通过试验敲定恰当的时间。笔者发现,多数情况下200-500微秒最为合适。

共享元素转换

共享元素转换方便我们为页面间的共享视图制作动画,使动画更为人性化,并给用户带来更好的视觉感受。

探索安卓中有意义的动画! 技术分享 第20张

这里,第一个页面中的视图缩小并平移至第二个页面的标题图片位置。

在布局中,必须使用 transitionName 属性将所有共享视图联系起来——这表明了视图间的转换关系。下图展示了之前动画中的共享视图:

探索安卓中有意义的动画! 技术分享 第21张

这些都是共享视图,意味着它们会在每次页面转换过程中形成动画。

为了完成如上转换,我们首先要声明共享转换名称,可以通过使用 XML 布局中的 transitionName 属性来完成。

屏幕 1)

<RelativeLayout>
    <LinearLayout>

        <View 
            android:id="@+id/view_shared_transition"
            android:transitionName="@string/transition_view"/>

        <!-- Your other views -->

    </LinearLayout>
</RelativeLayout>

屏幕2)

<LinearLayout>

    <View
        android:id="@+id/view_shared_transition"
        android:transitionName="@string/transition_view"/>

    <View
        android:id="@+id/view_separator"/>

    <TextView
        android:id="@+id/text_detail"/>

    <TextView
        android:id="@+id/text_close"/>

</LinearLayout>

之后,在页面1中创建 Pair 对象,使之包含转换视图与其 transitionName。然后将其传给页面选择实例(ActivityOptionsCompat),由此两个页面都得知了共享组件,就可以开始动画了。

Pair participants = new Pair<>(mSquareView, ViewCompat.getTransitionName(mSquareView));

ActivityOptionsCompat transitionActivityOptions = 
        ActivityOptionsCompat.makeSceneTransitionAnimation(
                SharedTransitionsActivity.this, participants);

ActivityCompat.startActivity(SharedTransitionsActivity.this, 
                      intent, transitionActivityOptions.toBundle());

探索安卓中有意义的动画! 技术分享 第22张

转换的同时滑动这些视图,有助于完成转换。

以上就是两个视图间的转换,那么在第二个页面中从底部滑入的视图怎么办呢?

(它们就是左边的那些视图)

其实这个实现过程也很简单,如下:

Slide slide = new Slide(Gravity.BOTTOM);
slide.addTarget(R.id.view_separator);
slide.addTarget(R.id.text_detail);
slide.addTarget(R.id.text_close);
getWindow().setEnterTransition(slide);

如你所见,创建一个新的Slide 转换:将目标视图添加到转换中,并将滑动动作设为入场动画。

自定义转换

我们也可以使用之前介绍过的 API 动画创建自己的自定义转换。例如,将共享元素转换衍伸,成为转换视图变体——当我们需要显示对话框(或者类似的弹框视图)时,自定义转换就会非常有用。具体如下所示:

探索安卓中有意义的动画! 技术分享 第23张

该动画可以在组件状态间引导用户的注意力。

先来简单了解一下上图发生了什么:

  • 首先创建一个SharedTransition,传入压缩视图与转换名称以引用共享组件。
  • 然后创建ArcMotion 实例,使两个视图转换时形成曲线动画效果。
  • 接下来扩展 ChangeBounds 以创建自定义转换,改变(morph)两个形状(对于button 和 FAB ,有两个不同的类)。此处重写了类中的多个方法,以便为所需属性做动画。最后,使用 ViewPropertyAnimator 调整对话框的透明度,使用 ObjectAnimator 调整两个视图间的色彩,使用 AnimatorSet 实例将两种动画效果整合在一起。

动态矢量图片 ##

API 21中(Lollipop),AnimatedVectorDrawable 可用于制定VectorDrawable 属性的动画,生成动态图片。

探索安卓中有意义的动画! 技术分享 第24张

在图片上做几种不同的动画并不容易。

那么如何完成呢,请看下图:

探索安卓中有意义的动画! 技术分享 第25张

该图由几个不同文件组成,首先创建两个独立的矢量文件,每个都包含如下属性:

  • Height & Width –矢量图像的实际大小;
  • Viewport Height & Width –声明描述矢量路径的虚拟画布的大小;
  • Group name –声明路径所属的组名;
  • Pivot X & Y –声明群组规模和旋转所使用的中心点;
  • Path Fill Color –描述矢量路径的填充色;
  • Path Data –声明用于绘制矢量的矢量路径数据。

注意: 所有被引用的属性都存储在 general strings file 中,这样可以保持程序整洁美观。

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="56dp" 
        android:width="56dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <group
        android:name="@string/groupAddRemove"
        android:pivotX="12"
        android:pivotY="12">
        <path 
            android:fillColor="@color/stroke_color"
            android:pathData="@string/path_add"/>
    </group>
</vector>

探索安卓中有意义的动画! 技术分享 第26张

该矢量由 ic_add.xml 文件(如下所示)生成

<vector xmlns:android="http://schemas.android.com/apk/res/android"
        android:height="56dp"
        android:width="56dp"
        android:viewportHeight="24.0"
        android:viewportWidth="24.0">
    <group
        android:name="@string/groupAddRemove"
        android:pivotX="12"
        android:pivotY="12">
        <path 
            android:fillColor="@color/stroke_color" 
            android:pathData="@string/path_remove"/>
    </group>
</vector>

探索安卓中有意义的动画! 技术分享 第27张

该矢量由ic_remove.xml 文件(如下所示)生成

接下来声明 Animated Vector Drawable 文件,其中包含 Vector Drawable 和每个图片状态动画(AddRemove)的声明。检查从 AddRemove 的矢量动画,声明一个目标文件(target)以完成:

  • 状态转换的动画;
  • 图片旋转的动画。

    <animated-vector android:drawable="@drawable/ic_add">
        <target
            android:name="@string/add"
            android:animation="@animator/add_to_remove" />
         <target
            android:name="@string/groupAddRemove"
            android:animation="@animator/rotate_add_to_remove" />
    </animated-vector>
    

然后创建目标文件中引用的每个文件。

改变图片状态

addtoremove.xml 文件中,使用ObjectAnimator 改变图形形状,其中会用到如下属性:

  • propertyName –执行动画的属性;
  • valueFrom –矢量路径的初始值;
  • valueTo –矢量路径的目标值;
  • duration –动画持续时间;
  • interpolator –动画插值器;
  • valueType –动画值类型。
      <objectAnimator
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:propertyName="pathData"
        android:valueFrom="@string/path_add"
        android:valueTo="@string/path_remove"
        android:duration="@integer/duration"
        android:interpolator="@android:interpolator/fast_out_slow_in"
        android:valueType="pathType" />

形状旋转

可使用相似的方法旋转图像,只是会用到旋转属性和旋转值:

<objectAnimator
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:propertyName="rotation"
    android:valueFrom="-180"
    android:valueTo="0"
    android:duration="@integer/duration"
    android:interpolator="@android:interpolator/fast_out_slow_in" />

执行相反的动画(从 RemoveAdd)所需的操作方法相同,只不过将动画值反置。

探索安卓中有意义的动画! 技术分享 第28张

完成后的动态矢量图,效果很不错吧?

使用OneAPM分析UI卡顿

使用 OneAPM 可以快速定位分析UI性能,Mobile Insight的卡顿可以直观地展示这些信息。 探索安卓中有意义的动画! 技术分享 第29张 可以分析绘制APP卡顿趋势图,精确定位每1秒内的绘图刷新信号中断的次数,从多维度分析卡顿现象,如APP版本、操作系统版本的分布情况等。 探索安卓中有意义的动画! 技术分享 第30张 卡顿详情列表展示:访问时间,发生卡顿时的流畅度,耗时,发生卡顿时的设备信息,APP版本,操作系统及版本,CPU信息 通过分析该页面信息可以清楚了解到卡顿来源,以便针对性快速优化。 探索安卓中有意义的动画! 技术分享 第31张

动画卡顿原因

动画卡顿的原因大概有这样三种,这些因素将直接影响动画的性能,导致卡顿。即:

  1. 手势滑动速度
  2. 帧率
  3. 触摸事件响应的速度

手势滑动、帧率是跟各种手机设备有直接的关系,各种各样的硬件设备会表现出不一样的性能,如果从这个方面入手考虑优化,就十分需要 OneAPM Mobile Insight 这样的从多维度来分析性能的一款工具。

结语

虽然只是浅谈,文本旨在围绕创建有意义的动画提供有益的视角,使读者受益。今后,笔者会继续努力,以求进一步改善应用的外观与用户体验。

原文地址:https://medium.com/ribot-labs/exploring-meaningful-motion-on-android-1cd95a4bc61d#.vqazussmj

OneAPM Mobile Insight 真实用户体验为度量标准进行 Crash 分析,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客