从概念设计到安卓实现, 第二部分(译)

原文:From design to android, part 2 

自从上一篇文章发布之后已经有一段时日了,虽然期间经历了很多事情,但是最终还是来了,希望你们依旧喜欢!

这是我的“从设计到android”系列的新篇,如果你记得这个系列的第一部分,就应该知道当时我选了一个有趣的概念设计,并尝试在 Android 上实现它,根据我作为一个android开发者的看法,重点讲一些有趣的话题。

本文涉及到的所有代码都可以在  Github的repository  ? 找到。

这里是我为这部分所选择的设计图:

1*-2PD1PyuxLrB9mjoVzP2rQ.gif

我再一次用到了来自 Johny Vino 的概念设计图。

先停下来思考下,作为像你这样的android开发者,如何实现这个设计。

如果你想到了下面的两点,那么我们的想法是一样的,我首先想到的是分为两个部分:

  • 如何实现建筑的动画。

  • seekbar可以不必那么复杂

建筑的动画

我们先假设一个完美的条件,我们可以叫设计师创建动画,利用After Effects +  Bodymovin的插件就可以得到想要的animated vector drawable.。   

或者...,我们根本没有设计师,你是一个大胆独行的开发者,需要在没有多少设计知识的情况下完成这个工作,让我们作一次这样的尝试。

矢量图的制作

使用一些矢量做图工具比如 Sketch 或者 BoxySvg 可以轻松的制作建筑的图片。我们把这些部分进行分组,这样对我们下一阶段有帮助。

1*sK6g7Il9jVSnyode1nsjqw.png

Creating the desired image on Sketch

接下来要介绍一个神奇的工具了,它就是Alex Lockwood 的s Shape Shifter。这个工具可以帮助你为SVG图片制作动画,并把动画导出为AVD以及其他格式。

1*6vPUlrUQ1Q4IiqwIt9_o-Q.gif

经过几步尝试之后得到了想要的效果,我们可以导出新鲜出炉的h animated vector drawable了,把它拷贝到 Android Studio ,绑定到一个 ImageView 然后就完成了。

(img_building.drawable as Animatable).start()

1*DgQkbwVvYhZyH0Y2A8SzlA.gif

Exported ShapeFilter’s avd running on an android device

很神奇,是吧?我们毫不费力的创建了一个很棒的动画,但是如果我们回头看看 Johny Vino的设计 就可以知道我们漏掉了一点细节。设计图中动画是由滑杆控制的,(在Android中叫 Seekbar ),它根据bar的进度调整动画的状态。

所以我们现在需要的是设置一个AVD中的动画进度。好吧...据我所知目前是不支持的,(Lottie 可以)。我们只有start()stop() 以及 reset()这样的方法。

这种情况下我们可以直接:

1*aAbK06sdpSU9VSj039PgUg.gif

或者继续,再前进一点点。

Animated Selector Drawables

目前的情况是,在SeekBar到达特定位置的时候,我们需要让动画也到达特定的帧。也就是说我们的动画是有状态的。让我们为这些状态或者说是帧定义一些属性。

<declare-styleable name="BuildingState">
    <!-- Idle state -->
    <attr name="state_zero" format="boolean"/>
    <!-- One flat and a simple roof -->
    <attr name="state_one" format="boolean"/>
    <!-- Two flats and expanded roof -->
    <attr name="state_two" format="boolean"/>
</declare-styleable>

此时此刻请让我介绍一个不太知名的drawable: AnimatedStateListDrawable ,它拯救了我们。

摘自android文档:

Drawable包含了一套Drawable keyframes,当前显示的keyframe取决于当前的状态设置。keyframes之间的动画可以使用transition elements来定义,也可与不定义。

这个drawable可以在XML文件中使用  元素来定义。每个keyframe Drawable定义在一个嵌套的  元素中。Transitions定义在一个嵌套的元素中。

看起来是我们想要的东西,是吧?我们可以定义一些可以代表我们动画中不同帧的 ,以及代表如何从一个item到另一个item的transition。

我们将定义三个item,每个都是由一个vector drawable组成的。

drawable/vd_building1.xml
drawable/vd_building2.xml
drawable/vd_building3.xml

1*f3eIyZbq766fa_D6AKenYw.png

每个 代表 之间的过渡,我们需要4个才能让这些帧能来回循环。

在Shape Shifter的帮助下,我们可以轻松的把所需的AVD导出到Android Studio,他们将在AnimationStateListDrawable中发挥transitions的作用。

drawable/avd_building_1_2
drawable/avd_building_2_3
drawable/avd_building_3_2
drawable/avd_building_2_1

avd_building_1_2.xml

avd_building_2_3.xml

最终我们得到了整个AnimatedStateDrawable:

<?xml version="1.0" encoding="utf-8"?>
<animated-selector
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    >
    <item
        android:id="@+id/frame_0"
        android:drawable="@drawable/vd_building_1"
        app:state_one="false"
        app:state_two="false"
        />
    <item
        android:id="@+id/frame_1"
        android:drawable="@drawable/vd_building_2"
        app:state_one="true"
        />
    <item
        android:id="@+id/frame_2"
        android:drawable="@drawable/vd_building_3"
        app:state_two="true"
        />
    <transition
        android:fromId="@id/frame_0"
        android:toId="@id/frame_1"
        android:drawable="@drawable/avd_building_1_2"
        />
    <transition
        android:fromId="@id/frame_1"
        android:toId="@id/frame_2"
        android:drawable="@drawable/avd_building_2_3"
        />
    <transition
        android:fromId="@id/frame_2"
        android:toId="@id/frame_1"
        android:drawable="@drawable/avd_building_3_2"
        />
    <transition
        android:fromId="@id/frame_1"
        android:toId="@id/frame_0"
        android:drawable="@drawable/avd_building_2_1"
        />
</animated-selector>

稍微调整一下activity和layout,把这个AnimatedSelectorDrawable作为 source resource设置给ImageView,再使用 setImageState 方法设置想要的状态,然后动画就如预期的那样工作了。

private val STATE_ZERO = intArrayOf(
        R.attr.state_zero, -R.attr.state_one, -R.attr.state_two
)
private val STATE_ONE = intArrayOf(
        -R.attr.state_zero, R.attr.state_one, -R.attr.state_two
)
private val STATE_TWO = intArrayOf(
        -R.attr.state_zero, -R.attr.state_one, R.attr.state_two
)
private fun onSeekProgressChanged(position: Int) {
    val max = seekbar.max
    val businessType = when(position) {
        in 0..max/3 -> STATE_ZERO
        in 10..max/2 -> STATE_ONE
        in 20..max/1 -> STATE_TWO
        else -> throw IllegalStateException()
    }
    imageView.setImageState(businessType, true)
}

结果:

1*A79i7YzBIlJCtzZVlON5KQ.gif

Seekbar thumb的动画

哦也!现在我们完成了第一个部分,第二部分是如何在改变进度的时候实现 Seekbar thumb 大小的动画,就如 Johny Vino 的概念图中那样:

1*P-q4HkBVBMT_KtL3yfFqNw.gif

Size of the Seekbar’s thumb on dragging

这里我们可以提取出两种行为:

  • 根据进度不同 thumb size 在以某种方式增大。

  • 当松开的时候,执行了某种overshooting动画(也可以说是弹簧动画吧)。

A visit to the ScaleDrawable

这里介绍另一个不知名的(至少对我而言)drawable:ScaleDrawable ,让我们再一次引用 android 文档:

一个可以根据自己当前level值改变另一个Drawable大小的Drawable。你可以根据这个level控制子Drawable改变多少宽和高,以及改变gravity控制它在所处容器的位置。通常用于实现类似 progress bar这样的控件。

默认的level可以在XML中使用android:level属性来指定。如果没有指定,默认的level 0,对应的高(或者宽)为0,这取决于android.R.styleable#ScaleDrawable_scaleWidth scaleWidth和 android.R.styleable#ScaleDrawable_scaleHeight scaleHeight的值。要在运行时设置level的话,可以调用setLevel(int)

译者注:可以参考 https://liuzhichao.com/2016/android-scaledrawable.html 

看起来我们可以定义一种根据另一个drawable,level以及缩放属性缩放的drawable。如果我们把bar的进度跟那个level关联,是不是就可以了呢?

让我们定义一个drawable的layer list ,我们只想要蓝色的部分可缩放,于是把它包裹在一个ScaleDrawable中。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:tools="http://schemas.android.com/tools"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:aapt="http://schemas.android.com/aapt">
    <item>
        <scale
            android:scaleWidth="30%"
            android:scaleHeight="30%"
            android:scaleGravity="center"
            android:level="1"
            tools:ignore="UnusedAttribute">
            <aapt:attr name="android:drawable">
              <shape android:shape="oval">
                  <size
                      android:width="70dp"
                      android:height="70dp" />
                  <solid android:color="@color/blue_accent_200" />
              </shape>
            </aapt:attr>
        </scale>
    </item>
    <item
        android:left="25dp"
        android:top="25dp"
        android:right="25dp"
        android:bottom="25dp">
        <shape android:shape="oval">
            <solid android:color="#FFF" />
        </shape>
    </item>
</layer-list>

你可能看到了,使用  可以在中内联所需的属性。

1*RJ5CGWWAptO1QMVp9iGpUw.png

thumb containing a ScaleDrawable

现在我们可以再次调整activity,当SeekBar的progress改变的时候设置ScaleDrawable的level。

private fun onSeekProgressChanged(position: Int) {
    // ...
    
    ((seekbar.thumb as LayerDrawable)).level = 
            (position * (SCALE_MAX / seekbar.max))
}

下面是结果:

1*sCaG8_x3C9zcW2v-j2pTBQ.gif

现在我们只需要在松开的时候对thumb做动画了,我们可以使用ScaleDrawable以及一个ValueAnimator来完成。

private fun animateThumbRelease() {
    val thumb = seekbar.thumb
    val initLevel = thumb.level
    val maxLevel = thumb.level * THUMB_RELEASE_SCALE_FACTOR
    val animator = ValueAnimator.ofInt(
            initLevel, maxLevel, initLevel)
    with(animator) {
        interpolator = OVERSHOOT
        duration = THUMB_SCALE_DURATION
        addUpdateListener {
            thumb.level = it.animatedValue as Int
        }
        start()
    }
}

结果:

1*rA76TXFkXAeslnspZOUEkA.gif

总结

到现在为止,我们解决了动画的问题,接着完成了Seekbar,至于移动的云彩可以使用更复杂的AVD,文字使用new font as resources ,这样就可以很接近Johny Vino的概念设计了。

最终结果:

1*WfNBTsNo_9ipck02sUoU_w.gif

Github 代码

here ?

参考