如果您正在构建一个 Android 应用程序,您应该考虑添加动画。它们可以改善您应用的用户体验并提高留存率。
这些天来,如果你看到一个没有动画的应用程序,你会觉得它很奇怪而且过时了。由于交互式体验是一种新规范,因此您需要想办法让您的应用与众不同。
我们将在这里建造什么
现在,如果你只有一些基本的东西,比如报价共享应用程序(这就是我们要在这里做的),那么让你的应用程序脱颖而出似乎很困难。很难吸引用户并让他们保持兴趣。
当然,您可以只添加两个简单的按钮来加载下一个/上一个报价并收工。但这是非常基本的,任何应用程序都可以做到这一点!即使您只是在构建一个简单的副项目,也没有为良好的 UX 做权衡:)
所以我们在本教程中要做的就是放下按钮,而是有一个用户可以向左滑动卡片的逻辑。当他们刷得足够远时,该应用程序将加载一张带有新报价的新卡片。
在这篇文章的最后,您将学习如何制作一个非常流畅的动画卡片,用户可以通过滑动它来执行您选择的任何操作。这是它如何工作的演示:
很神奇,对吧?让我们开始吧!
先决条件
在本教程中,我们将使用 Kotlin 作为我们应用程序的编程语言——但您可以轻松地将代码翻译成 Java,并且它的工作方式相同。
作为参考,这是我们希望启用滑动功能的报价卡。
它是一个CardView带有一堆TextViews 和一个ImageView. ProgressBar加载新报价时还会显示一个。
我们不会为用户界面制作 XML 代码。
如何在我们的应用程序中处理滑动
为了处理滑动,我们首先需要在卡片上设置一个触摸监听器。每次在卡片上执行操作时,都会调用触摸侦听器。在侦听器中,我们将添加逻辑来进行数学运算并执行动画。
这是我们将使用的触摸监听器的蓝图:
quoteCard.setOnTouchListener(
View.OnTouchListener { view, event ->
when (event.action) {
MotionEvent.ACTION_MOVE -> {
// TODO: Handle ACTION_MOVE
}
MotionEvent.ACTION_UP -> {
// TODO: Handle ACTION_UP
}
}
// required to by-pass lint warning
view.performClick()
return@OnTouchListener true
}
)
在这里,我们专门监听卡片上的 2 个动作——theACTION_MOVE和ACTION_UP.
ACTION_MOVE当用户开始刷卡,即移动它时,会调用该事件。
当ACTION_UP用户从卡上抬起手指时调用,基本上,当他们释放它时。
我们可以覆盖许多其他动作事件,例如ACTION_DOWN当一个人获得视图时调用,但我们不需要它们来实现此功能。
刷卡的基本设置已经完成,接下来我们来搞清楚刷卡的逻辑。
滑动动作背后的数学
首先,让我们重新思考我们想要实现的目标。当您确切地知道自己想要什么时,实现功能会更容易。当您的要求明确时,您的代码也会更有意义。
在这里,我们有一张报价卡。我们希望用户只能向左滑动它,如果达到加载新报价的最小阈值,它应该移回其原始位置并加载新报价。
现在,为了实现这一点,让我们从卡的角度来考虑它。让我们将平均位置定义为卡片的中心。
且仅当用户将卡片刷到平均位置的左侧时,我们希望卡片能够刷卡。
那么我们怎样才能做到这一点呢?
你猜对了——我们将计算平均位置,在ACTION_MOVE事件中,我们将检查用户是否向左滑动并相应地移动卡片。
如何实现滑动逻辑
为了实现这个逻辑,我们首先需要知道卡片的起始位置,这很容易计算。我们将确保它是根据全屏宽度计算的,而不仅仅是卡片的宽度。
将这些代码行放在when(event.action)语句之前:
quoteCard.setOnTouchListener(
View.OnTouchListener { view, event ->
// variables to store current configuration of quote card.
val displayMetrics = resources.displayMetrics
val cardWidth = quoteCard.width
val cardStart = (displayMetrics.widthPixels.toFloat() / 2) - (cardWidth / 2)
when (event.action) {
...
}
...
}
)
首先我们得到displayMetricsfrom resources,它会给我们使用屏幕的宽度displayMetrics.widthPixels.toFloat()。
然后我们得到cardWidthusing 的width属性quoteCard。
最后,我们使用公式计算卡片的起始位置(width of screen/2) - (cardWidth/2)。本质上,这给了我们卡片这个位置的 x 坐标:
现在,让我们实现ACTION_MOVE事件的代码。
如何处理ACTION_MOVE
事件
在ACTION_MOVE块中,我们首先初始化newX保存卡片已刷到的新 x 坐标的变量。
val newX = event.rawX
event.rawX给我们新坐标相对于屏幕宽度的绝对值。
newX将包含在任何给定时刻用户手指所在的 x 坐标。的值0.0表示newX用户滑动到屏幕的最左侧。对于我的模拟器,1080.0代表屏幕的最右边。
newX因为,我们希望卡片只有在小于卡片的平均位置时才刷卡,所以我们将在这里放置一个 if 条件来验证是否是这种情况。
用简单的值来考虑这一点。假设卡片的平均位置在 x 坐标540.0(小 x 坐标)并且用户滑动到710.0(更大的 x 坐标)。但我们不希望他们能够向右滑动。如果用户刷卡到320.0(较小的 x 坐标),那么我们需要执行刷卡并将卡片移动到新位置。
下面是实现上述逻辑的代码:
if (newX - cardWidth < cardStart) { // or newX < cardStart + cardWidth
quoteCard.animate().x(
min(cardStart, newX - (cardWidth / 2))
)
.setDuration(0)
.start()
}
我们减去cardWidth因为newX是newX一个与卡片无关的绝对值。它具有更高的值,因为cardStart它位于屏幕的开头,并且newX最初位于中间的某个位置(用户通常会从中间滑动)。
我们想将 x 坐标和中位数的 shift 值与 的值而cardStart不是 的值进行比较newX,因此我们通过减去 来考虑这一点cardWidth。
然后,我们使用该函数执行动画,quoteCard.animate()并使用该函数更改其 x 坐标x()。
现在,我们为什么要这样做min(cardStart, newX - (cardWidth/2))?
这是非常有趣和直观的理解。从一开始,我们就强调卡片应该只向左移动,而不是向右移动。
newX - (cardWidth/2))只不过是向左滑动的距离(因此涉及减法 - 对于右侧,应该添加)。
此处的min()函数返回所提供的两个值中的最小值。如果滑动距离小于cardStart,则返回,否则cardStart使用。这是我们想要满足的条件,并且min()非常容易处理。
setDuration(0)确保动画即时进行(使滑动不会感觉迟钝)。start()实际上用给定的属性开始动画。
该动画将消除您对其工作原理的任何疑问:
我没有制作动画的专业知识,这是我能想到的最好的。)
这是ACTION_MOVE事件的最终代码:
MotionEvent.ACTION_MOVE -> {
// get the new coordinate of the event on X-axis
val newX = event.rawX
// carry out swipe only if newX - cardWidth < cardStart, that is
// the card is swiped to the left side, not to the right
if (newX - cardWidth < cardStart) {
quoteCard.animate()
.x(
min(cardStart, newX - (cardWidth / 2))
)
.setDuration(0)
.start()
}
}
您还可以TextView在 UI 中包含一个反映用户应该何时释放卡片的 UI。将此代码也放入上述if语句中:
if (quoteCard.x < MIN_SWIPE_DISTANCE) textView.text = getString(R.string.releaseCard)
else textView.text = getString(R.string.infoText)
哪里MIN_SWIPE_DISTANCE是-250:
// -250 produces best result, feel free to change to your liking
const val MIN_SWIPE_DISTANCE = -250 // User should move alteast -250 from mean position to load new quote
现在,该ACTION_MOVE事件已得到妥善处理。让我们编写代码来处理ACTION_UP事件,即卡片释放时。
如何处理ACTION_UP
事件
对于ACTION_UP事件,我们希望卡片回到原来的位置,等待大约100几毫秒,然后加载新的报价。
动画卡片的逻辑是类似的,但这次我们将使其动画持续时间约为150毫秒,以使其看起来更流畅。
首先,创建一个变量currentX来保存报价卡的 x 坐标的当前值。稍后我们将使用此变量。
var currentX = quoteCard.x
然后,启动卡上的动画。将cardStart变量传递给x()函数以使其返回其原始位置并将持续时间设置为150.
quoteCard.animate()
.x(cardStart)
.setDuration(150)
// continued below
这一次,我们在动画上设置了一个监听器。听众是密切关注动画的东西。通过使用它,我们可以对各种动画事件执行操作,例如开始、结束、恢复等。
// continuation
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
delay(100)
// check if the swipe distance was more than
// minimum swipe required to load a new quote
if (currentX < MIN_SWIPE_DISTANCE) {
// Add logic to load a new quote if swiped adequately
viewModel.getRandomQuote()
currentX = 0f
}
}
}
}
)
.start()
onAnimationEnd()我们设置了一个监听器,通过覆盖该函数来寻找动画的结束。
动画一结束,我们就会启动一个延迟为100毫秒的协程(类似于 Java 中的线程,但效率更高)。然后,它会检查用户是否已超出MIN_SWIPE_DISTANCE加载新报价所需的范围。该变量currentX用于此处的比较。
如果用户实际滑动通过了最小距离,则协程会延迟100几毫秒。然后视图模型从 API 加载一个新的随机引用,同时将currentX变量重置为0f.
事件的最终代码ACTION_UP如下所示:
MotionEvent.ACTION_UP -> {
var currentX = quoteCard.x
quoteCard.animate()
.x(cardStart)
.setDuration(150)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
delay(100)
// check if the swipe distance was more than
// minimum swipe required to load a new quote
if (currentX < MIN_SWIPE_DISTANCE) {
// Add logic to load a new quote if swiped adequately
viewModel.getRandomQuote()
currentX = 0f
}
}
}
})
.start()
textView.text = getString(R.string.infoText)
}
最终代码
这是完整的最终代码onTouchListener():
quoteCard.setOnTouchListener(
View.OnTouchListener { v, event ->
// variables to store current configuration of quote card.
val displayMetrics = resources.displayMetrics
val cardWidth = quoteCard.width
val cardStart = (displayMetrics.widthPixels.toFloat() / 2) - (cardWidth / 2)
when (event.action) {
MotionEvent.ACTION_UP -> {
var currentX = quoteCard.x
quoteCard.animate()
.x(cardStart)
.setDuration(150)
.setListener(
object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
viewLifecycleOwner.lifecycleScope.launch(Dispatchers.Default) {
delay(100)
// check if the swipe distance was more than
// minimum swipe required to load a new quote
if (currentX < MIN_SWIPE_DISTANCE) {
// Add logic to load a new quote if swiped adequately
viewModel.getRandomQuote()
currentX = 0f
}
}
}
}
)
.start()
textView.text = getString(R.string.infoText)
}
MotionEvent.ACTION_MOVE -> {
// get the new co-ordinate of X-axis
val newX = event.rawX
// carry out swipe only if newX < cardStart, that is,
// the card is swiped to the left side, not to the right
if (newX - cardWidth < cardStart) {
quoteCard.animate()
.x(
min(cardStart, newX - (cardWidth / 2))
)
.setDuration(0)
.start()
if (quoteCard.x < MIN_SWIPE_DISTANCE)
textView.text = getString(R.string.releaseCard)
else textView.text = getString(R.string.infoText)
}
}
}
// required to by-pass lint warning
v.performClick()
return@OnTouchListener true
}
}
恭喜!在本文中,我们实现了让用户刷卡包含报价以获得新报价的动画。
结论
现在您已经学习了如何为卡片设置动画并处理其上的动画侦听器。这有助于创建更好的用户体验,让您的应用脱颖而出。
使用您在本文中获得的知识,您现在可以为 Android 中的视图创建以下大部分动画:
以编程方式为 Android 视图创建滑动动画。
就像我们在本文章中所做的那样。
从左到右的动画
这相当简单,只需将变量中的减法转换为加法,<并将if语句中的符号转换为>符号即可。通过这里和那里的一些调整,卡片视图中的从右到左的动画可以变成从左到右的动画!
您还可以使用动画显示和隐藏视图。
为此,您必须跟踪开始位置和结束位置,然后使用alpha()from 0to 为它们设置动画1。例如,您可以参考我的库Accolib来创建动画 FAQ 手风琴。
基本的动画布局更改可以通过视图动画来实现。
非常感谢到目前为止的阅读,我希望这篇文章能增加一些价值。订阅我的时事通讯(在文章的顶部)以了解最新的 Android 内容!