从概念设计到安卓实现, 第二部分(译)
原文:From design to android, part 2
自从上一篇文章发布之后已经有一段时日了,虽然期间经历了很多事情,但是最终还是来了,希望你们依旧喜欢!
这是我的“从设计到android”系列的新篇,如果你记得这个系列的第一部分,就应该知道当时我选了一个有趣的概念设计,并尝试在 Android 上实现它,根据我作为一个android开发者的看法,重点讲一些有趣的话题。
本文涉及到的所有代码都可以在 Github的repository ? 找到。
这里是我为这部分所选择的设计图:
我再一次用到了来自 Johny Vino 的概念设计图。
先停下来思考下,作为像你这样的android开发者,如何实现这个设计。
如果你想到了下面的两点,那么我们的想法是一样的,我首先想到的是分为两个部分:
-
如何实现建筑的动画。
-
seekbar可以不必那么复杂
建筑的动画
我们先假设一个完美的条件,我们可以叫设计师创建动画,利用After Effects + Bodymovin的插件就可以得到想要的animated vector drawable.。
或者...,我们根本没有设计师,你是一个大胆独行的开发者,需要在没有多少设计知识的情况下完成这个工作,让我们作一次这样的尝试。
矢量图的制作
使用一些矢量做图工具比如 Sketch 或者 BoxySvg 可以轻松的制作建筑的图片。我们把这些部分进行分组,这样对我们下一阶段有帮助。
Creating the desired image on Sketch
接下来要介绍一个神奇的工具了,它就是Alex Lockwood 的s Shape Shifter。这个工具可以帮助你为SVG图片制作动画,并把动画导出为AVD以及其他格式。
经过几步尝试之后得到了想要的效果,我们可以导出新鲜出炉的h animated vector drawable了,把它拷贝到 Android Studio ,绑定到一个 ImageView 然后就完成了。
(img_building.drawable as Animatable).start()
Exported ShapeFilter’s avd running on an android device
很神奇,是吧?我们毫不费力的创建了一个很棒的动画,但是如果我们回头看看 Johny Vino的设计 就可以知道我们漏掉了一点细节。设计图中动画是由滑杆控制的,(在Android中叫 Seekbar ),它根据bar的进度调整动画的状态。
所以我们现在需要的是设置一个AVD中的动画进度。好吧...据我所知目前是不支持的,(Lottie 可以)。我们只有start(), stop() 以及 reset()这样的方法。
这种情况下我们可以直接:
或者继续,再前进一点点。
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
每个 代表 之间的过渡,我们需要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)
}
结果:
Seekbar thumb的动画
哦也!现在我们完成了第一个部分,第二部分是如何在改变进度的时候实现 Seekbar thumb 大小的动画,就如 Johny Vino 的概念图中那样:
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>
你可能看到了,使用 可以在中内联所需的属性。
thumb containing a ScaleDrawable
现在我们可以再次调整activity,当SeekBar的progress改变的时候设置ScaleDrawable的level。
private fun onSeekProgressChanged(position: Int) {
// ...
((seekbar.thumb as LayerDrawable)).level =
(position * (SCALE_MAX / seekbar.max))
}
下面是结果:
现在我们只需要在松开的时候对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()
}
}
结果:
总结
到现在为止,我们解决了动画的问题,接着完成了Seekbar,至于移动的云彩可以使用更复杂的AVD,文字使用new font as resources ,这样就可以很接近Johny Vino的概念设计了。
最终结果:
Github 代码
here ?
参考
-
VectorDrawable Adaptive Icons, Ian Lake
-
Designing Adaptive Icons, Nick Butcher
-
Shape Shifter, Alex Lockwood
-
An Introduction to Icon Animation Techniques, Alex Lockwood
-
adp-delightful-details, Alex Lockwood
-
How we design a beautiful animation, Jeremie Martinez