[譯] Android 如何實現氣泡選擇動畫

Android 如何實現氣泡選擇動畫

做者:Irina Galata Android 開發者;Yulia Serbenenko UI/UX 設計師javascript

跨平臺用戶體驗統一正處於增加趨勢:早些時候 iOS 和安卓有着不一樣的體驗,可是最近在應用設計以及交互方面變得愈來愈接近。html

從安卓 Nougat 的底部導航到分屏特性,兩個平臺間有了許多相同之處。對設計師而言,咱們能夠將主流功能設計成兩個平臺一致(過去須要單獨設計)。對開發者而言,這是一個提升、改進開發技巧的好機會。java

因此咱們決定開發一個安卓氣泡選擇的組件庫 —— 靈感來自於蘋果音樂的氣泡選擇。android

先說設計

咱們的氣泡選擇動畫是一個好的範例,它對不一樣的用戶羣體有着一樣的吸引力。氣泡以方便的 UI 元素彙總信息,通俗易懂而且視覺一致。它讓界面對新手足夠簡單的同時還能吸引老司機的興趣。git

這種動畫類型對豐富應用的內容由很大幫助,主要使用場景是:用戶要從一系列選項中進行選擇時的頁面。例如,咱們使用氣泡來選擇旅遊應用中潛在目的地名字。氣泡自由的浮動,當用戶點擊一個氣泡時,選中的氣泡會變大。這給用戶很深入的反饋並加強操做的直觀感覺。github

組件使用白色主題,明亮的顏色和圖片貫穿始終。此外,我決定試驗漸變來增長深度和體積。漸變多是主要的顯示特徵,會吸引新用戶的注意。canvas

氣泡選擇的漸變app

咱們容許開發者自定義全部的 UI 元素,因此咱們的組件適合任意的應用。ide

再看開發者的挑戰

當我決定實現這個動畫時,我面臨的第一個問題就是使用什麼工具開發。我清楚知道繪製如此快速的動畫在 Canvas 上繪製的效率是不夠的,因此決定使用 OpenGL (Open Graphics Library)。OpenGL 是一個跨平臺的 2D 和 3D 圖形繪製應用開發接口。幸運地是,Android 支持部分版本的 OpenGL。工具

我須要圓天然地運動,就像碳酸飲料中的氣泡那樣。對 Android 來講有許多可用的物理引擎,同時我又有一些特定須要,使得選擇變得更加困難。個人需求是:引擎要輕量級而且方便嵌入 Android 庫。多數的引擎是爲遊戲開發的,而且它們須要調整工程結構來適應它們。功夫不負有心人,我最終找到了 JBox2D(C++ 引擎 Box2D 的 Java 版),由於咱們的動畫不須要支持大量的物理實體(例如 200+),使用非原版的 Java 版引擎已經足夠了。

此外,本文後面我會解釋我爲何選擇 Kotlin 語言開發,以及這樣作的好處。須要瞭解 Java 和 Kotlin 更多不一樣之處能夠閱讀我以前的文章

如何建立着色器?

首先,咱們須要理解 OpenGL 中的基礎構件三角形,由於它是和其它形狀相似且最簡單的形狀。因此你繪製的任意圖形都是由一個或多個三角形組成。在動畫實現中,我使用兩個關聯的三角形表明一個實體,因此我畫圓的地方像一個正方形。

繪製一個形狀至少須要兩個着色器 —— 頂點着色器和片斷着色器。經過名字就能夠區分他們的用途。頂點着色器負責繪製每一個三角形的頂點,片斷着色器負責繪製三角形中每一個像素。

三角形的片斷和頂點

頂點着色器負責控制圖形的變化(例如:大小、位置、旋轉),片斷着色器負責形狀的顏色。

// language=GLSL
val vertexShader = """
    uniform mat4 u_Matrix;
    attribute vec4 a_Position;
    attribute vec2 a_UV;
    varying vec2 v_UV;
    void main()
    {
        gl_Position = u_Matrix * a_Position;
        v_UV = a_UV;
    }
"""複製代碼

頂點着色器

// language=GLSL
val fragmentShader = """
    precision mediump float;
    uniform vec4 u_Background;
    uniform sampler2D u_Texture;
    varying vec2 v_UV;
    void main()
    {
        float distance = distance(vec2(0.5, 0.5), v_UV);
        gl_FragColor = mix(texture2D(u_Texture, v_UV), u_Background, smoothstep(0.49, 0.5, distance));
    }
"""複製代碼

片斷着色器

着色器使用 GLSL(OpenGL 着色語言) 編寫,須要運行時編譯。若是項目使用的是 Java,那麼最方便的方式是在另外一個文件編寫你的着色器,而後使用輸入流讀取。如上述示例代碼所示,Kotlin 能夠簡單地在類中建立着色器。你能夠在 """ 中間添加任意的 GLSL 代碼。

GLSL 中有許多類型的變量:

  • 頂點和片斷的 uniform 變量的值是相同的
  • 每一個頂點的 attribute 變量是不一樣的
  • varying 變量負責從頂點着色器向片斷着色器傳遞數據,它的值由片斷線性地插入。

u_Matrix 變量包含由圓初始化位置的 xy 構成的變化矩陣,顯然它的值對圖形的全部頂點拉說都是相同的,類型爲 uniform,然而頂點的位置是不一樣的,因此 a_Position 變量是 attribute 類型。a_UV 變量有兩個用途:

  1. 肯定當前片斷和正方形中心位置的距離。根據這個距離,我能夠調整片斷的顏色而實現畫圓。
  2. 正確地將 texture(照片和國家的名字)置於圖形的中心位置。


圓的中心

a_UV 包含 xy,它們的值每一個頂點都不一樣,取值範圍是 0 ~ 1。我只給頂點着色器 a_UVv_UV 兩個入參,所以每一個片斷均可以插入 v_UV。而且對於片斷中心點的 v_UV 值爲 [0.5, 0.5]。我使用 distance() 方法計算兩個點的距離。

使用 smoothstep 繪製平滑的圓

起初片斷着色器看上去不太同樣:

gl_FragColor = distance < 0.5 ? texture2D(u_Text, v_UV) : u_BgColor;

我根據點到中心的距離調整片斷的顏色,沒有采起抗鋸齒手段。固然結果差強人意 —— 圓的邊是凹凸不平的。


有鋸齒的圓

解決方案是 smoothstep。它根據到 texture 與背景的變換起始點的距離平滑的從0到1變化。所以距離 0 到 0.49 時 texture 的透明度爲 1,大於等於 0.5 時爲 0,0.49 和 0.5 之間時平滑變化,如此圓的邊就平滑了。


無鋸齒圓

OpenGL 中如何使用 texture 顯示圖像和文本?

在動畫中圓有兩種狀態 —— 普通和選中。在普通狀態下圓的 texture 包含文字和顏色,在選中狀態下同時包含圖像。所以我須要爲每一個圓建立兩個不一樣的 texture。

我使用 Bitmap 實例來建立 texture,繪製全部元素。

fun bindTextures(textureIds: IntArray, index: Int) {
        texture = bindTexture(textureIds, index * 2, false)
        imageTexture = bindTexture(textureIds, index * 2 + 1, true)
    }

    private fun bindTexture(textureIds: IntArray, index: Int, withImage: Boolean): Int {
        glGenTextures(1, textureIds, index)
        createBitmap(withImage).toTexture(textureIds[index])
        return textureIds[index]
    }

    private fun createBitmap(withImage: Boolean): Bitmap {
        var bitmap = Bitmap.createBitmap(bitmapSize.toInt(), bitmapSize.toInt(), Bitmap.Config.ARGB_4444)
        val bitmapConfig: Bitmap.Config = bitmap.config ?: Bitmap.Config.ARGB_8888
        bitmap = bitmap.copy(bitmapConfig, true)

        val canvas = Canvas(bitmap)

        if (withImage) drawImage(canvas)
        drawBackground(canvas, withImage)
        drawText(canvas)

        return bitmap
    }

    private fun drawBackground(canvas: Canvas, withImage: Boolean) {
        ...
    }

    private fun drawText(canvas: Canvas) {
        ...
    }


    private fun drawImage(canvas: Canvas) {
        ...
    }複製代碼

以後我將 texture 單元賦值給 u_Text 變量。我使用 texture2() 方法獲取片斷的真實顏色,texture2() 接收 texture 單元和片斷頂點的位置兩個參數。

使用 JBox2D 讓氣泡動起來

關於動畫的物理特性十分的簡單。主要的對象是 World 實例,全部的實體建立都須要它。

class CircleBody(world: World, var position: Vec2, var radius: Float, var increasedRadius: Float) {

    val decreasedRadius: Float = radius
    val increasedDensity = 0.035f
    val decreasedDensity = 0.045f
    var isIncreasing = false
    var isDecreasing = false
    var physicalBody: Body
    var increased = false

    private val shape: CircleShape
        get() = CircleShape().apply {
            m_radius = radius + 0.01f
            m_p.set(Vec2(0f, 0f))
        }

    private val fixture: FixtureDef
        get() = FixtureDef().apply {
            this.shape = this@CircleBody.shape
            density = if (radius > decreasedRadius) decreasedDensity else increasedDensity
        }

    private val bodyDef: BodyDef
        get() = BodyDef().apply {
            type = BodyType.DYNAMIC
            this.position = this@CircleBody.position
        }

    init {
        physicalBody = world.createBody(bodyDef)
        physicalBody.createFixture(fixture)
    }

}複製代碼

如你所見建立實體很簡單:須要指定實體的類型(例如:動態、靜態、運動學)、位置、半徑、形狀、密度以及運動。

每次畫面繪製,都須要調用 Worldstep() 方法移動全部的實體。以後你能夠在圖形的新位置進行繪製。

我遇到的問題是 World 的重力只能是一個方向,而不能是一個點。JBox2D 不支持軌道重力。所以將圓移動到屏幕中心是沒法實現的,因此我只能本身來實現引力。

private val currentGravity: Float
        get() = if (touch) increasedGravity else gravity

private fun move(body: CircleBody) {
        body.physicalBody.apply {
            val direction = gravityCenter.sub(position)
            val distance = direction.length()
            val gravity = if (body.increased) 1.3f * currentGravity else currentGravity
            if (distance > step * 200) {
                applyForce(direction.mul(gravity / distance.sqr()), position)
            }
        }
}複製代碼

引擎


引力挑戰

每次發生移動時,我計算出力的大小並做用於每一個實體,看上去就像圓受引力做用在移動。

GlSurfaceView 中檢測用戶觸摸事件

GLSurfaceView 和其它的 Android view 同樣能夠響應用戶的點擊事件。

override fun onTouchEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                startX = event.x
                startY = event.y
                previousX = event.x
                previousY = event.y
            }
            MotionEvent.ACTION_UP -> {
                if (isClick(event)) renderer.resize(event.x, event.y)
                renderer.release()
            }
            MotionEvent.ACTION_MOVE -> {
                if (isSwipe(event)) {
                    renderer.swipe(event.x, event.y)
                    previousX = event.x
                    previousY = event.y
                } else {
                    release()
                }
            }
            else -> release()
        }

        return true
}

private fun release() = postDelayed({ renderer.release() }, 1000)

private fun isClick(event: MotionEvent) = Math.abs(event.x - startX) < 20 && Math.abs(event.y - startY) < 20

private fun isSwipe(event: MotionEvent) = Math.abs(event.x - previousX) > 20 && Math.abs(event.y - previousY) > 20複製代碼

GLSurfaceView 攔截全部的點擊,並用渲染器進行處理。

fun swipe(x: Float, y: Float) = Engine.swipe(x.convert(glView.width, scaleX),
            y.convert(glView.height, scaleY))

fun release() = Engine.release()

fun Float.convert(size: Int, scale: Float) = (2f * (this / size.toFloat()) - 1f) / scale複製代碼

渲染器

fun swipe(x: Float, y: Float) {
        gravityCenter.set(x * 2, -y * 2)
        touch = true
}

fun release() {
        gravityCenter.setZero()
        touch = false
}複製代碼

引擎

用戶點擊屏幕時,我將重力中心設爲用戶點擊點,這樣看起來就像用戶在控制氣泡的移動。用戶中止移動後我會將氣泡恢復到初始位置。

根據用戶點擊座標查找氣泡

當用戶點擊圓時,我從 onTouchEvent() 方法獲取屏幕點擊點。可是我也須要找到 OpenGL 座標系中點擊的圓。GLSurfaceView 的默認中心位置座標爲 [0, 0],x y 取值範圍爲 -1 到 1。因此我須要考慮屏幕的比例。

private fun getItem(position: Vec2) = position.let {
        val x = it.x.convert(glView.width, scaleX)
        val y = it.y.convert(glView.height, scaleY)
        circles.find { Math.sqrt(((x - it.x).sqr() + (y - it.y).sqr()).toDouble()) <= it.radius }
}複製代碼

渲染器

當找到選擇的圓後,我會修改它的半徑和 texture。

你能夠隨機的使用本組件!

咱們的組件可讓應用更聚焦內容、原始以及充滿樂趣。

如下途徑能夠獲取 Bubble Picker : GitHub , Google Play 以及 Dribbble

這只是組件的第一個版本,但咱們確定會有後續的迭代。咱們將支持自定義氣泡的物理特性和經過 url 添加動畫的圖像。此外,咱們還計劃添加一些新特性(例如:移除氣泡)。

不要猶豫把您的實驗發給咱們,咱們很是想知道您是怎樣使用 Bublle Picker 的。若是您有任何問題或者建議,歡迎隨時聯繫咱們。

咱們將會繼續發佈一些炫酷的東西。敬請期待!

相關文章
相關標籤/搜索