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;当你不折…

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

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

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

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

看完这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…

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

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

ISA Server服务器故障恢复一例系统盘符更换之后的应对方法

周四下午的时候&#xff0c;某政府信息中心领导打电话告诉我&#xff0c;ISA Server服务器不能开机了。随后公司的技术员到达现场&#xff0c;经过检查&#xff0c;发现服务器显卡损坏。在更换显卡后&#xff0c;服务器可以开机&#xff0c;但却不能进入系统—-服务器在经过BIO…

扩展Windows Mobile模拟器存储空间的方法

在Windows Mobile应用程序开发的初期&#xff0c;可以使用SDK自带的模拟器来进行调试&#xff0c;这给我们开发人员提供了一种方便的途径。一般的应用程序&#xff0c;占用空间的大小也就几 百K&#xff0c;或者几M&#xff0c;这在模拟器上调试起来一点问题也没有。但是有的时…

UOS LoongArch 上成功安装.NET Core 3.1

龙芯.NET团队正式发布了.NET Core 3.1 For LoongArch, 具体参见龙芯开源网站 http://www.loongnix.cn/index.php/Dotnet 。进入安装包下载地址LoongArch64-.NET Core 3.1&#xff0c;可以看到龙芯.NET团队做了很多工作&#xff0c;为Debian和Redhat两大Linux体系平台都做好了基…

c++ vector拷贝构造_JDK源码分析-Vector

1. 概述上文「JDK源码分析-ArrayList」主要分析了 ArrayList 的实现原理。本文分析 List 接口的另一个实现类&#xff1a;Vector。Vector 的内部实现与 ArrayList 类似&#xff0c;也可以理解为一个「可变数组」。其继承结构如下&#xff08;省略部分接口&#xff09;&#xff…

除了PS,原来这个也可以轻松实现图像处理!

全世界只有3.14 % 的人关注了爆炸吧知识在我们生活中&#xff0c;常见的图像处理软件有Adobe Photoshop、Adobe Illustrator等。然而&#xff0c;并非只有软件才能实现图像处理&#xff0c;通过编程手段也是能实现的&#xff01;今天&#xff0c;小天将要带着大家走进计算机视觉…

下雨天纵使少了什么也是少不了一把伞的&#xff0c;即使是几千年前&#xff0c;也不管细雨霏霏&#xff0c;大雨倾盆。愿意沐浴风雨中&#xff0c;享受这样惠泽的人总是少的。从“孤舟蓑笠翁&#xff0c;独钓寒江雪”&#xff0c;“十里一长亭&#xff0c;五里一短亭”&#xf…

oracle用户名密码过期引起的网站后台无法登录

本来今天休息&#xff0c;但是接到同事反映&#xff1a;客户的WEB无法登录了&#xff0c;网站能打开&#xff0c;但是后台登录不了。我就联系了客户&#xff0c;客户说是WEB用户的密码过期导致的&#xff0c;默认是180天到期。接着就是我的操作流程了&#xff1a;1.先从WEB服务…