Android 如何实现气泡选择动画(1)

我需要圆自然地运动,就像碳酸饮料中的气泡那样。对 Android 来说有许多可用的物理引擎,同时我又有一些特定需要,使得选择变得更加困难。我的需求是:引擎要轻量级并且方便嵌入 Android 库。多数的引擎是为游戏开发的,并且它们需要调整工程结构来适应它们。功夫不负有心人,我最终找到了 JBox2DC++ 引擎 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 变量包含由圆初始化位置的x 和 y 构成的变化矩阵,显然它的值对图形的所有顶点拉说都是相同的,类型为 uniform,然而顶点的位置是不同的,所以 a_Position 变量是 attribute 类型。a_UV 变量有两个用途:

  • 确定当前片段和正方形中心位置的距离。根据这个距离,我可以调整片段的颜色而实现画圆。

  • 正确地将 texture(照片和国家的名字)置于图形的中心位置。

image

圆的中心

a_UV 包含 x 和 y,它们的值每个顶点都不同,取值范围是 0 ~ 1。我只给顶点着色器 a_UV 和 v_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 与背景的变换起始点的距离平滑的从01变化。因此距离 0 到 0.49 时 texture 的透明度为 1,大于等于 0.5 时为 00.49 和 0.5 之间时平滑变化,如此圆的边就平滑了。

image

无锯齿圆

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)

}

}

如你所见创建实体很简单:需要指定实体的类型(例如:动态、静态、运动学)、位置、半径、形状、密度以及运动。

每次画面绘制,都需要调用 World 的 step() 方法移动所有的实体。之后你可以在图形的新位置进行绘制。

我遇到的问题是 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()

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

image

y

private fun move(body: CircleBody) {

body.physicalBody.apply {

val direction = gravityCenter.sub(position)

val distance = direction.length()

最后

针对于上面的问题,我总结出了互联网公司Android程序员面试涉及到的绝大部分面试题及答案,并整理做成了文档,以及系统的进阶学习视频资料。
(包括Java在Android开发中应用、APP框架知识体系、高级UI、全方位性能调优,NDK开发,音视频技术,人工智能技术,跨平台技术等技术资料),希望能帮助到你面试前的复习,且找到一个好的工作,也节省大家在网上搜索资料的时间来学习。

[外链图片转存中…(img-Udqa2L1S-1719496558508)]

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值