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

泡在网上的日子 / 文 发表于2017-09-25 10:13 次阅读 概念设计

原文: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文件中使用 <animated-selector> 元素来定义。每个keyframe Drawable定义在一个嵌套的 <item> 元素中。Transitions定义在一个嵌套的<transition>元素中。

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

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

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

1*f3eIyZbq766fa_D6AKenYw.png

每个<transition> 代表<item> 之间的过渡,我们需要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>

你可能看到了,使用 <aapt> 可以在<layer-list>中内联所需的属性。

1*RJ5CGWWAptO1QMVp9iGpUw.png

thumb <layer-list> 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 👈

参考

收藏 赞 (6) 踩 (0)
上一篇:Android Paging Library按页获取网络数据实例
新的 Paging Library 成为了 Architecture Components 的一部分。虽然现在还是alpha阶段,但是无疑你已经开始准备尝试了!我不准备全去讲它的用法,因为本文只是对 Chris Craik 这篇文章 的补充。 因为官方的示例第一眼看上去好像它只能跟 Room 一起使用,如
下一篇:短视频图像处理 OpenGL ES 实践
2017年,短视频正以其丰富的内容表现力和时间碎片化的特点,快速崛起,而短视频最具可玩性之处就在支持人脸识别的动态贴图和各种不同效果的美颜、滤镜等。那短视频动态贴纸、滤镜、美颜等功能究竟是如何实现的呢? 为什么选择 OpenGL ES Android 手机在处理