如何在 Android 应用中向 CardView 添加滑动动画

23 篇文章 0 订阅
1 篇文章 0 订阅

如果您正在构建一个 Android 应用程序,您应该考虑添加动画。它们可以改善您应用的用户体验并提高留存率。

这些天来,如果你看到一个没有动画的应用程序,你会觉得它很奇怪而且过时了。由于交互式体验是一种新规范,因此您需要想办法让您的应用与众不同。

我们将在这里建造什么

现在,如果你只有一些基本的东西,比如报价共享应用程序(这就是我们要在这里做的),那么让你的应用程序脱颖而出似乎很困难。很难吸引用户并让他们保持兴趣。

当然,您可以只添加两个简单的按钮来加载下一个/上一个报价并收工。但这是非常基本的,任何应用程序都可以做到这一点!即使您只是在构建一个简单的副项目,也没有为良好的 UX 做权衡:)

所以我们在本教程中要做的就是放下按钮,而是有一个用户可以向左滑动卡片的逻辑。当他们刷得足够远时,该应用程序将加载一张带有新报价的新卡片。

在这篇文章的最后,您将学习如何制作一个非常流畅的动画卡片,用户可以通过滑动它来执行您选择的任何操作。这是它如何工作的演示:

https://cdn.hashnode.com/res/hashnode/image/upload/v1636995092206/__FOPUD4R.gif?auto=format,compress&gif-q=60&format=webm

很神奇,对吧?让我们开始吧!

先决条件

在本教程中,我们将使用 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()实际上用给定的属性开始动画。

该动画将消除您对其工作原理的任何疑问:

https://cdn.hashnode.com/res/hashnode/image/upload/v1636995634763/Sk_XJiR5M.gif?auto=format,compress&gif-q=60&format=webm

我没有制作动画的专业知识,这是我能想到的最好的。)

这是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 内容!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值