[译] 如何创建 BubblePicker – Android 多彩菜单动画-阿里云开发者社区

开发者社区> 玄学酱> 正文

[译] 如何创建 BubblePicker – Android 多彩菜单动画

简介: 本文讲的是[译] 如何创建 BubblePicker – Android 多彩菜单动画,我们已经习惯了移动应用丰富的交互方式,如滑动手势去选择、拖拽。但是我们没有察觉到,统一用户的跨平台体验是一个正在发生的趋势。
+关注继续查看
本文讲的是[译] 如何创建 BubblePicker – Android 多彩菜单动画,

如何创建 BubblePicker – Android 多彩菜单动画

我们已经习惯了移动应用丰富的交互方式,如滑动手势去选择、拖拽。但是我们没有察觉到,统一用户的跨平台体验是一个正在发生的趋势。

早期时候,iOS 和 Android 都有其独特的体验,但是在近期,这两个平台上的应用体验和交互在逐渐的靠拢。底部导航和分屏的特性已经成为Android Nougat版本的特性,Android 和 iOS 已经有了很多相同的地方了。

对于设计者而言,设计语言的融合意味着在一个平台上流行的特性可以适配到另一个平台。

最近,为了跟上跨平台风格的步伐,我们受 Apple music 上气泡动画的启发,用 Android 动画实现了一份。我们设计了一个接口,使得初学者也可以方便的使用,而且也让有经验的开发者觉得有趣。

使用 BubblePicker 能让一个应用更加的聚焦内容、原汁原味和有趣。尽管 Google 已经对它所有的产品推出了材料设计语言,但是我们依然决定在此时尝试大胆的颜色和渐变的效果,使得图像增加更多的深度和体积。渐变可能是界面显示最主要的视觉效果,也可能会吸引到更多的人使用。

1

我们的组件是白色背景,上面包含了很多明亮的颜色和图形。

这种高反差对丰富应用的内容很有帮助,在这里用户不得不从一系列选项列表中做出选择。比如,在我们的概念中,我们在旅行应用中使用气泡来持有潜在的目的地名称。气泡在自由的漂浮,当用户点击其中一个时,那个气泡就会变大。

1

此外,开发者可以通过自定义屏幕中的元素使得动画适配任何应用。

当我们在制作这个动画的同时,我们要面对下面五个挑战:

1. 选择最佳开发工具

很明显,在 Canvas 上渲染这样一个快速的动画效果不够高效,所以我们决定使用OpenGL (Open Graphics Library)。 OpenGL 是一个提供 2D 或 3D 图形渲染的、跨平台的应用程序接口。幸运的是,Android 支持一些 OpenGL 的版本。

我们需要让圆更加的自然,就像是汽水中的气泡。有很多物理引擎可用于 Android,但我们的特殊需求使得做出选择格外困难:这个引擎必须轻量而且方便嵌入 Android 库中。大多数引擎都是为游戏开发的,你必须使项目结构适应它们。经过一些研究,我们发现了 JBox2D (一个使用 C++ 开发的、 Java 端口的 Box2D 引擎);因为我们的动画并不支持很多数量的 body(换句话说,它不是为了200个或更多的对象设计的),我们可以使用 Java 端口而不是原生引擎。

另外,在本文的后面我们会解释为何选择了 Kotlin 语言编写,并且谈到这种新语言的优点。想要了解 Java 与 Kotlin 更多的区别,请访问之前的文章

2. 创建着色器

在开始的时候,我们需要先理解 OpenGL 中的构建块是三角形,因为三角形是能够模拟成其他形状中最简单的形状。你在 OpenGL 中创建出的任何形状,都包含了一个或多个三角形。为了实现动画,我们为每个 body 使用了两个组合三角形,所以看起来像个正方形,我们可以在里面画圆。

渲染一个形状至少需要写两个着色器 - 一个顶点着色器和一个片段着色器。它们的名称已经体现了各自的不同。对每个三角形的每个顶点执行一个顶点着色器,而对三角形中的每个像素大小的部分则执行片段着色器。

1

顶点着色器通常被用于控制形状(如缩放、位置、旋转),而片段着色器负责控制其颜色。

    // 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 Shading Language) 编写的,必须在运行时编译。如果你用的是 Java 代码,最方便的方法是将你的着色器写到一个单独的文件中,然后使用输入流取回。如你所见,Kotlin 开发人员通过将任何多行代码放到三重引号(""")中,更方便的在类中创建着色器。

GLSL 有几种不同类型的变量:

  • 统一变量对所有顶点和片段持有相同的值

  • 属性变量对每个顶点都不同

  • 变化中变量将数据从顶点着色器传递到片段着色器,对于每个片段都是用线性内插法赋值

u_Move 变量包含了 x 和 y 两个值,用于表示顶点当前位置的移动增量。很明显,他们的值应该与一个形状中的所有顶点的该变量的值相同,类型也应该是相同的,虽然这些顶点各自的位置不同。a_Position 变量是属性变量,a_UV 变量用于以下两个目的:

  1. 得到当前片段与正方形中心的距离;根据这个距离,我们能够改变片段的颜色来画圆。

  2. 将纹理(照片和国家名称)放在图形的中心。

1

a_UV 变量包含了 x 和 y 两个变量,这两个值对每个顶点都不同但都在 0 和 1 之间。在顶点着色器中,我们将值从 a_UV 变量传递给 v_UV 变量,这样每个片段都会被插入 v_UV 变量。结果,形状中心片段的 v_UV 变量的值就是 [0.5, 0.5]。我们使用 distance() 方法来计算一个选中的片段到中心的距离。这个方法使用两点作为参数。

3. 使用 smoothstep 方法画抗锯齿圆

起初,我的片段着色器看起来有些不一样:

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

我根据到中心的距离改变了片段颜色,没有使用抗锯齿。结果并不理想,圆的边缘被切开了。

1

smoothstep 方法可以解决这个问题。在纹理和背景间平滑插入由起点和终点决定的值,取值范围在 0 到 1 之间。。纹理的透明度在 0 到 0.49 之间值设为1,0.5 以上的为0,并且0.49 到 0.5 之间会被插入,所以圆的边缘会被抗锯齿。

1

4. 使用纹理在 OpenGL 中显示图片和文本

动画中的每个圆都有两个状态 - 正常状态和选中状态。在正常状态中,圆中的纹理包含了文字和颜色;在选中的状态,纹理则还会包含了一个图片。所以,对每个圆我们都应该创建两个不同的纹理。

为了创建纹理,我们使用一个 Bitmap 的实例,在实例里我们画出所有的元素并绑定纹理:

    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){
            ...
        }

做完这些之后,我们将这个纹理传递给 u_Text 变量。我们通过 texture2D() 方法来获取一个片段的真实颜色,我们还能获得纹理单元和片段相对于其顶点的位置。

5. 使用 JBox2D 让气泡移动

从物理的角度,这个动画非常简单。主对象是一个 World 实例,所有的 body 都需要在这个 World 里创建:

    classCircleBody(world: World, varposition: Vec2, varradius: Float, varincreasedRadius: Float) {

        val decreasedRadius: Float = radius
        val increasedDensity = 0.035f
        val decreasedDensity = 0.045f
        var isIncreasing = false
        var isDecreasing = false
        var physicalBody: Body
        var increased = falseprivate 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)
        }

    }

正如我们所见,body 容易创建:我们需要简单的制定 body 类型(如:dynamic, static, kinematic),position,radius,shape,density 和 fixture 属性。

当这个面被画出来,我们需要调用 World 的 step() 方法来移动所有的 body。然后,我们就可以在新的位置画出所有的形状了。

我们遇到一个问题,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)
                }
            }
    }

1

每当 World 移动时,我们计算一个合适的力度作用于每个 body,使得看起来像是受到了重力的影响。

6. 在 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()
            }

            returntrue
    }

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

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

GLSurfaceView 拦截所有的触摸事件,渲染器处理它们:

    //Rendererfun 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

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

    fun release(){
            gravityCenter.setZero()
            touch = false
    }

当用户滑动屏幕,我们增加重力并改变中心,在用户看来就像是控制了气泡的移动。当用户停止了滑动,我们将气泡恢复到初始状态。

7. 通过用户触碰的坐标找到气泡

当用户点击了一个圆,我们通过 onTouchEvent() 方法接收到了触碰点在屏幕上的坐标。但是,我们还需要找到被点击的圆在 OpenGL 坐标体系中的位置。默认情况下,GLSerfaceView 中心的坐标是 [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 }
    }

当我们找到了选中的圆就改变它的半径、密度和纹理。

这是我们第一版 Bubble Picker,而且还将进一步完善。其他开发者可以自定义泡泡的物理行为,并指定 url 将图片添加到动画中。而且我们还将添加一些新的特性,比如移除泡泡。

请将你们的实验发给我们,让我们看到你是如何使用 Bubble Picker 的。如果对动画有任何问题或建议,请告诉我们。

我们会尽快发布更多干货。 敬请关注!






原文发布时间为:2017年5月19日

本文来自云栖社区合作伙伴掘金,了解相关信息可以关注掘金网站。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Android官方开发文档Training系列课程中文版:动画视图之创建自定义转场动画
原文地址:http://android.xsoftlab.net/training/transitions/custom-transitions.html 自定义转场可以创建自定义动画。
943 0
以百度天气预报查询API 服务为例,创建Jmeter JavaSampler请求范例
最近在整理性能测试的一些入门文章,给同事们分享,介绍API 接口自动化和性能测试入门。 下面将以百度天气预报查询API 服务为例,创建Java API 请求范例。 1. API 服务信息 参考文档:https://blog.
2290 0
使用OpenApi弹性创建云服务器ECS
除了可以在ECS控制台或者售卖页创建ECS之外,您可以使用OpenApi代码来弹性的创建和管理ECS。这里使用Python来作例子。 开通按量付费产品,您的账户余额不得少于100元,更多的需求参见ECS 使用须知,您需要在阿里云的费用中心确保自己的余额充足。
5906 0
阿里云服务器端口号设置
阿里云服务器初级使用者可能面临的问题之一. 使用tomcat或者其他服务器软件设置端口号后,比如 一些不是默认的, mysql的 3306, mssql的1433,有时候打不开网页, 原因是没有在ecs安全组去设置这个端口号. 解决: 点击ecs下网络和安全下的安全组 在弹出的安全组中,如果没有就新建安全组,然后点击配置规则 最后如上图点击添加...或快速创建.   have fun!  将编程看作是一门艺术,而不单单是个技术。
4504 0
Frame - 快速创建高品质的 Web 应用原型
  Frame 是一个让你够能够快速创建高品质的网站或应用程序产品原型的工具。你上传的图片将被包裹在真实的设备环境中。它是一个用于创建宣传资料的专业工具。Frame 完全免费供给商业和个人使用。他们也正探索一种可能性,增加额外的功能给那些正在寻找特色功能的用户,但没有计划立即开始收费。
576 0
【百度地图API】建立全国银行位置查询系统(一)——如何创建地图
原文:【百度地图API】建立全国银行位置查询系统(一)——如何创建地图 你将在第一章中学会以下知识: 如何创建一个网页文件 怎样利用百度地图API建立一张2D地图,以及3D地图 如何添加对地图进行鼠标和键盘操作的功能 ------------------------------------------------------------------------------------------------------------------- 一、创建网页文件 粘贴以下代码至记事本中,保存文件为bank1-1.htm文件。
1130 0
+关注
玄学酱
这个时候,玄酱是不是应该说点什么...
17436
文章
438
问答
文章排行榜
最热
最新
相关电子书
更多
文娱运维技术
立即下载
《SaaS模式云原生数据仓库应用场景实践》
立即下载
《看见新力量:二》电子书
立即下载