android canvas_Android 如何实现气泡选择动画

07c910fb3b3cfa460900498235edcbd9.png

作者:Irina Galata Android 开发者: Yulia Serbenenko UI/UX 设计师 译者:skyar2009链接:https://juejin.im/post/58e5ec838d6d8100616d82e2/

跨平台用户体验统一正处于增长趋势:早些时候 iOS 和安卓有着不同的体验,但是最近在应用设计以及交互方面变得越来越接近。从安卓 Nougat 的底部导航到分屏特性,两个平台间有了许多相同之处。对设计师而言,我们可以将主流功能设计成两个平台一致(过去需要单独设计)。对开发者而言,这是一个提高、改进开发技巧的好机会。所以我们决定开发一个安卓气泡选择的组件库 —— 灵感来自于苹果音乐的气泡选择。

67c4fa45768f5b91698875eac69518e4.gif

先说设计

我们的气泡选择动画是一个好的范例,它对不同的用户群体有着同样的吸引力。气泡以方便的 UI 元素汇总信息,通俗易懂并且视觉一致。它让界面对新手足够简单的同时还能吸引老司机的兴趣。

这种动画类型对丰富应用的内容由很大帮助,主要使用场景是:用户要从一系列选项中进行选择时的页面。例如,我们使用气泡来选择旅游应用中潜在目的地名字。气泡自由的浮动,当用户点击一个气泡时,选中的气泡会变大。这给用户很深刻的反馈并增强操作的直观感受。

组件使用白色主题,明亮的颜色和图片贯穿始终。此外,我决定试验渐变来增加深度和体积。渐变可能是主要的显示特征,会吸引新用户的注意。

9c5d8905040f7062ce0fd39ee6808121.png

气泡选择的渐变

我们允许开发者自定义所有的 UI 元素,所以我们的组件适合任意的应用。

再来看看开发者的挑战

当我决定实现这个动画时,我面临的第一个问题就是使用什么工具开发。我清楚知道绘制如此快速的动画在 Canvas 上绘制的效率是不够的,所以决定使用 OpenGL (Open Graphics Library)OpenGL 是一个跨平台的 2D3D 图形绘制应用开发接口。幸运地是,Android 支持部分版本的 OpenGL

我需要圆自然地运动,就像碳酸饮料中的气泡那样。对 Android 来说有许多可用的物理引擎,同时我又有一些特定需要,使得选择变得更加困难。我的需求是:引擎要轻量级并且方便嵌入 Android 库。多数的引擎是为游戏开发的,并且它们需要调整工程结构来适应它们。功夫不负有心人,我最终找到了 JBox2D(C++ 引擎 Box2D 的 Java 版),因为我们的动画不需要支持大量的物理实体(例如 200+),使用非原版的 Java 版引擎已经足够了。

此外,本文后面我会解释我为什么选择 Kotlin 语言开发,以及这样做的好处。需要了解 Java 和 Kotlin 更多不同之处可以阅读我之前的文章。

如何创建着色器?

首先,我们需要理解 OpenGL 中的基础构件三角形,因为它是和其它形状类似且最简单的形状。所以你绘制的任意图形都是由一个或多个三角形组成。在动画实现中,我使用两个关联的三角形代表一个实体,所以我画圆的地方像一个正方形。

绘制一个形状至少需要两个着色器 —— 顶点着色器和片段着色器。通过名字就可以区分他们的用途。顶点着色器负责绘制每个三角形的顶点,片段着色器负责绘制三角形中每个像素。

d1d79c004c64e9b7d971ca34858a0f24.png
三角形的片段和顶点

顶点着色器负责控制图形的变化(例如:大小、位置、旋转),片段着色器负责形状的颜色。

// 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 变量有两个用途:

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

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

5ca36bb55f71344ae453e312ecfff0b7.png
圆的中心

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;

我根据点到中心的距离调整片段的颜色,没有采取抗锯齿手段。当然结果差强人意 —— 圆的边是凹凸不平的。

59f5e04b45cdee56b0c7d4125e619b4c.png
有锯齿的圆

解决方案是 smoothstep。它根据到 texture 与背景的变换起始点的距离平滑的从01变化。因此距离 00.49texture 的透明度为 1,大于等于 0.5 时为 00.490.5 之间时平滑变化,如此圆的边就平滑了。

6947fd590d029feb978365ac286546c2.png
无锯齿圆

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)
}
}
}
8cb8e88925c9b8b76fe58ad809c07581.png

引力挑战

每次发生移动时,我计算出力的大小并作用于每个实体,看上去就像圆受引力作用在移动。

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 取值范围为 -11。所以我需要考虑屏幕的比例。

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:https://github.com/igalata/Bubble-Picker

这只是组件的第一个版本,但我们肯定会有后续的迭代。我们将支持自定义气泡的物理特性和通过 url 添加动画的图像。此外,我们还计划添加一些新特性(例如:移除气泡)。

不要犹豫把您的实验发给我们,我们非常想知道您是怎样使用 Bublle Picker 的。如果您有任何问题或者建议,欢迎随时联系我们。

---END---

推荐阅读:

自定义LayoutManager:实现弧形以及滑动放大效果RecyclerView

现象级产品ZAO,为何火不过三天?

来了,Android 10 正式发布,新增黑暗模式、手势导航等功能

88b2b87bd578f800567bc3ffef375c88.png每一个“在看”,我都当成真的喜欢331af6746fcb13328ed7e5b89b6b1904.gif

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/298615.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

MVC中的验证规则

前面的博客中曾经提到过ModelBing机制&#xff0c;也在Demo中体现过&#xff0c;在MVC中更吊的是封装了自定义的验证规则。下面来一个Demo来展现一下&#xff0c;看了后&#xff0c;你一定会爱上它的&#xff0c;能让你少写很多JS语句。 1.View层 [html] view plaincopyprint…

网络——在网络上发送,接收数据

问题 创建并加入一个网络会话是一回事&#xff0c;但如果不能发送或接收任何数据那么网络会话有什么用呢&#xff1f; 解决方案 当玩家连接到会话时&#xff0c;你可以在一个PacketWriter流中存储所有想要发送的数据。完成这个操作后&#xff0c;你可以使用LocalNetworkPlayer.…

微服务之 EShop on dapr概览

????欢迎点赞 &#xff1a;???? 收藏 ⭐留言 ???? 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;????本文作者&#xff1a;由webmote 原创&#xff0c;首发于 【掘金】????作者格言&#xff1a;生活在于折腾&#xff0c;当你不折…

python开发面试问题及答案_前50个Python面试问题(最受欢迎)

热门Python面试问答下面列出的是关于Python编程语言的最常见面试问题和答案。让我们探索&#xff01;&#xff01;&#xff03;1)Python可以用于Web客户端和Web服务器端编程吗&#xff1f;哪一个最适合Python&#xff1f;答案&#xff1a; Python由于具有创建业务逻辑&#xff…

苹果android 对比,苹果安卓旗舰差距有多少?看了这份对比,果粉傻眼了

最近&#xff0c;身边不少小伙伴都在换机&#xff0c;有的换了最新的 iPhone 12 系列&#xff0c;有的则是换成安卓旗舰&#xff0c;毕竟现在的安卓旗舰与 iPhone 之间的体验已经十分接近&#xff0c;甚至在一些方面安卓旗舰还有着不小的优势。下面&#xff0c;我们以最新的 iP…

【暴强】200种好口碑便宜护肤品 - 生活至上,美容至尚!

200种好口碑便宜护肤品 一.洁面类 1.欧泊莱均衡洁面膏40ML 25元&#xff1a;很丰富的泡沫&#xff0c;&#xff0c;最大的好处就是洗完了脸不干&#xff0c;很舒服。 2.珊拉娜除逗防疤洗面奶&#xff1a;泡沫非常细&#xff0c;毛孔清透 3.丹芭碧绿茶洁面啫喱18元:觉得洗后很…

查看和修改Oracle数据库服务器端的字符集

Oracle数据库查看和修改服务器端的字符集的方法是本文主要要介绍的内容&#xff0c;接下来救让我们一起来了解一下这部分内容。 A、oracle server 端字符集查询 select userenv(‘language’) from dual 其中NLS_CHARACTERSET 为server端字符集 NLS_LANGUAGE 为 server端字符显…

python画画用哪库好_Python我要学画画-turtle库

上帝说&#xff1a;“要有光&#xff01;” 于是&#xff0c;就有了光。 ---《圣经》旧约创世纪篇 我要学画画&#xff0c;Python便有了turtle库。 turtle库是一个点线面的简单图像库。画布中心为坐标系原点&#xff0c;小海龟起始位置就在原点方向向右。turtle界面 Python与库…

Xubuntu菜单删改条记

作者: Zhu.CA 出自: http://www.linuxdiyf.com1. 主菜单&#xff1a;主菜单为 ~/.config/xfce4/desktop/menu.xml。删改可用"Xfce Settings Manager"中的"Menu editer"&#xff0c;运用上绝对俭朴&#xff0c;不做过多详述。 2. System子菜单&#xff…

看完这15张动图,秒懂万有引力与航天难点!

全世界只有3.14 % 的人关注了爆炸吧知识椭圆的画法大质量天体使周围天体绕其运转模拟太阳系星球轨迹非常接近圆火箭运载卫星升空卫星飞行过程中可以点火调整姿态同步卫星必须在赤道上空北斗全球卫星导航轨道半径越大&#xff0c;卫星越慢人类发射的卫星越来越多嫦娥沿椭圆轨道奔…

物联网工程专业的迷茫与抉择

大家好&#xff0c;我是阿辉&#xff0c;很高兴在这里和你讲述所思所想。周末了&#xff0c;就聊点比较轻松的话题。正文共1633字&#xff0c;预计阅读时间5分钟。想必很多朋友是通过#毕业四年&#xff0c;我当初是如何走上编程这条路的&#xff01;#这篇文章才熟知我的吧&…

html二级页面内容滑动,jQuery+CSS实现的网页二级下滑菜单效果

本文实例讲述了jQueryCSS实现的网页二级下滑菜单效果。分享给大家供大家参考。具体如下&#xff1a;这是一款简洁型的 jQueryCSS网页二级下滑菜单&#xff0c;练手写的&#xff0c;有需要的自己拿去美化吧&#xff0c;基本的动画效果和菜单下滑效果和渐变效果已经做出来了&…

parentElement,srcElement 使用

代码 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns"http://www.w3.org/1999/xhtml"><head><meta http-equiv"Content-T…

哈哈哈,程序员没有女朋友的原因,我终于找到了!

全世界只有3.14 % 的人关注了爆炸吧知识程序员没有女朋友的原因▼程序员大脑里想的▼每天要学习太多语言&#xff0c;程序员太忙了▼女朋友 VS 编译器▼程序员sao起来&#xff0c;还需要女朋友吗&#xff1f;▼电脑才是程序员的女朋友▼互道晚安后&#xff0c;会不会偶遇在同一…

基于事件驱动架构构建微服务第7部分:在仓储上实现事件溯源

原文链接&#xff1a;https://logcorner.com/building-microservices-through-event-driven-architecture-part7-implementing-eventsourcing-on-repositories/在本文中&#xff0c;我将讨论Repository上的Event Sourcing实现。仓储负责将事件添加到事件存储并从事件存储中检索…

python选择题题目_Python接口测试题(持续更新中)

1、json和字典的区别&#xff1f; Json是轻量级的数据交互格式&#xff0c;以key-value的键值对形式来保存数据&#xff0c;结构清晰&#xff0c;可以说是目前互联网项目开发中最常用的一种数据交互格式。 字典&#xff0c;同样是以key-value的键值对来保存数据&#xff0c;是p…

javascript事件之:jQuery事件中实例对象和拓展对象之间的通信

我们总结过jQery事件中的实例原型对象对外接口和拓展对象&#xff0c;现在我们看看他们是如何进行通信联系的。 先来看便捷方法&#xff1a; 1 //调用的还是实例对象下的on()和trigger()2 jQuery.each( ("blur focus focusin focusout load resize scroll unload click db…

SQL 中的unicode字符

要将字符转换成unicode字符以显示某些符号&#xff0c;可以在字符串前加N,如&#xff1a; select N♧♡♂♀♠♣♥❤☜☞☎☏ 转载于:https://www.cnblogs.com/Laro/archive/2011/03/07/1976483.html

html5 ajax数据显示,html5的ajax学习(二)

一、基础知识点1.ajax可以做事情&#xff1a;局部刷新 浏览器搜索列表记录 加载更多的数据2.登录页面同步网络请求&#xff1a;页面全部刷新&#xff0c;用户量大体验很不好3.ajax的详解&#xff1a;ajax的get和post请求 同步还是异步&#xff0c;true为异步ajax.open("ge…

后羿采集器怎么导出数据_后羿SEO:如何用后羿采集器查收录?(附详细步骤图)...

摘要&#xff1a;不知道大家查收录是用啥查的&#xff0c;或许市面上有工具&#xff0c;可以批量查&#xff0c;但是这些工具不是收费就是查询结果不太准确。今天狠人SEO教大家一个查询结果比较准确的方法。如标题写的&#xff0c;...不知道大家查收录是用啥查的&#xff0c;或…