乐趣区

关于android:在-View-上使用挂起函数

Kotlin 协程 让咱们能够用同步代码来建设异步问题的模型。这是十分好的个性,然而目前大部分用例都专一于 I/O 工作或是并发操作。其实协程不仅在解决跨线程的问题有劣势,还能够用来解决同一线程中的异步问题。

我认为有一个中央能够真正从中受害,那就是在 Android 视图零碎中应用协程。

Android 视图  ???? 回调

Android 视图零碎中尤其热衷于应用回调: 目前在 Android Framework 中,view 和 widgets 类中的回调有 80+ 个,在 Jetpack 中回调的数目更是超过了 200 个 (这里也蕴含了没有界面的依赖库)。

最常见的用法有以下几项:

  • AnimatorListener 获取动画完结相干的事件
  • RecyclerView.OnScrollListener 获取滑动状态变更事件
  • View.OnLayoutChangeListener 获取 View 布局扭转的事件

而后还有一些通过承受 Runnable 来执行异步操作的 API,比方 View.post()、View.postDelayed() 等等。

正是因为 Android 上的 UI 编程从根本上就是异步的,所以造成了如此之多的回调。从测量、布局、绘制,到调度插入,整个过程都是异步的。通常状况下,一个类 (通常是 View) 调用零碎办法,一段时间之后零碎来调度执行,而后通过回调触发监听。

KTX 扩大办法

上述提及的 API,在 Jetpack 中都减少了扩大办法来进步开发效率。其中 View.doOnPreDraw()办法是我最喜爱的一个,该办法对期待下一次绘制被执行进行了极大的精简。其实还有很多我罕用的办法,比方 View.doOnLayout()、Animator.doOnEnd().doOnEnd(kotlin.Function1))。

然而这些扩大办法也是仅止步于此,他们只是将旧格调的回调 API 改成了 Kotlin 中比拟敌对的基于 lambda 格调的 API。尽管用起来很优雅,但咱们只是在用另一种形式解决回调,这还是没有解决简单的 UI 的回调嵌套问题。既然咱们在探讨异步操作,那在这种状况下,咱们能够应用协程优化这些问题么?

应用协程解决问题

这里假设您曾经对协程有肯定的了解,如果接下来的内容对您来说会有些生疏,能够通过咱们往年晚期的系列文章进行回顾: 在 Android 开发中应用协程 | 背景介绍。

挂起函数 (Suspending functions) 是协程的根底组成部分,它容许咱们以非阻塞的形式编写代码。这种个性十分实用于咱们解决 Android UI,因为咱们不想阻塞主线程,阻塞主线程会带来性能上的问题,比方: jank。

suspendCancellableCoroutine

在 Kotlin 协程库中,有很多协程的结构器办法,这些结构器办法外部能够应用挂起函数来封装回调的 API。最次要的 API 是 [suspendCoroutine()](https://kotlinlang.org/api/la…
) 和 suspendCancellableCoroutine(),后者是能够被勾销的。

咱们举荐始终应用 suspendCancellableCoroutine(),因为这个办法能够从两个维度解决协程的勾销操作:

#1: 能够在异步操作实现之前勾销协程。如果某个 view 从它所在的层级中被移除,那么依据协程所处的作用域 (scope),它有可能会被勾销。举个例子: Fragment 返回出栈,通过解决勾销事件,咱们能够勾销异步操作,并革除相干援用的资源。

#2: 在协程被挂起的时候,异步 UI 操作被勾销或者抛出异样。并不是所有的操作都有已勾销或出错的状态,然而这些操作有。就像前面 Animator 的示例中那样,咱们必须把这些状态传递到协程中,让调用者能够处理错误的状态。

期待 View 被布局实现

让咱们看一个例子,它封装了一个期待 View 传递下一次布局事件的工作 (比如说,咱们扭转了一个 TextView 中的内容,须要期待布局事件实现后能力获取该控件的新尺寸):

suspend fun View.awaitNextLayout() = suspendCancellableCoroutine<Unit> { cont ->

    // 这里的 lambda 表达式会被立刻调用,容许咱们创立一个监听器
    val listener = object : View.OnLayoutChangeListener {override fun onLayoutChange(...) {
            // 视图的下一次布局工作被调用
            // 先移除监听,避免协程透露
            view.removeOnLayoutChangeListener(this)
            // 最终,唤醒协程,复原执行
            cont.resume(Unit)
        }
    }
    // 如果协程被勾销,移除该监听
    cont.invokeOnCancellation {removeOnLayoutChangeListener(listener) }
    // 最终,将监听增加到 view 上
    addOnLayoutChangeListener(listener)

    // 这样协程就被挂起了,除非监听器中的 cont.resume() 办法被调用}

此办法仅反对协程中一个维度的勾销 (#1 操作),因为布局操作没有谬误状态供咱们监听。

接下来咱们就能够这样应用了:

viewLifecycleOwner.lifecycleScope.launch {
    // 将该视图设置为不可见,再设置一些文字
    titleView.isInvisible = true
    titleView.text = "Hi everyone!"

    // 期待下一次布局事件的工作,而后才能够获取该视图的高度
    titleView.awaitNextLayout()

    // 布局工作被执行
    // 当初,咱们能够将视图设置为可见,并其向上平移,而后执行向下的动画
    titleView.isVisible = true
    titleView.translationY = -titleView.height.toFloat()
    titleView.animate().translationY(0f)
}

咱们为 View 的布局创立了一个 await 函数。用同样的办法能够代替很多常见的回调,比方 doOnPreDraw(),它是在 View 失去绘制时调用的办法;再比方 postOnAnimation(),在动画的下一帧开始时调用的办法,等等。

作用域

不晓得您有没有发现这样一个问题,在下面的例子中,咱们应用了 lifecycleScope 来启动协程,为什么要这样做呢?

为了防止产生内存透露,在咱们操作 UI 的时候,抉择适合的作用域来运行协程是极其重要的。侥幸的是,咱们的 View 有一些范畴适合的 Lifecycle。咱们能够应用扩大属性 lifecycleScope 来取得一个绑定生命周期的 CoroutineScope。

LifecycleScope 被蕴含在 AndroidX 的   lifecycle-runtime-ktx  依赖库中,能够在 这里 找到更多信息

咱们最罕用的生命周期的持有者 (lifecycle owner) 就是 Fragment 中的 viewLifecycleOwner),只有加载了 Fragment 的视图,它就会处于沉闷状态。一旦 Fragment 的视图被移除,与之关联的 lifecycleScope 就会主动被勾销。又因为咱们曾经为挂起函数中增加了对勾销操作的反对,所以 lifecycleScope 被勾销时,所有与之关联的协程都会被革除。

期待 Animator 执行实现

咱们再来看一个例子来加深了解,这次是期待 Animator 执行完结:

suspend fun Animator.awaitEnd() = suspendCancellableCoroutine<Unit> { cont ->

    // 减少一个解决协程勾销的监听器,如果协程被勾销,// 同时执行动画监听器的 onAnimationCancel() 办法,勾销动画
    cont.invokeOnCancellation {cancel() }

    addListener(object : AnimatorListenerAdapter() {
        private var endedSuccessfully = true

        override fun onAnimationCancel(animation: Animator) {
            // 动画曾经被勾销,批改是否胜利完结的标记
            endedSuccessfully = false
        }

        override fun onAnimationEnd(animation: Animator) {

            // 为了在协程复原后的不产生透露,须要确保移除监听
            animation.removeListener(this)
            if (cont.isActive) {

                // 如果协程仍处于沉闷状态
                if (endedSuccessfully) {
                    // 并且动画失常完结,复原协程
                    cont.resume(Unit)
                } else {
                    // 否则动画被勾销,同时勾销协程
                    cont.cancel()}
            }
        }
    })
}

这个办法反对两个维度的勾销,咱们能够别离勾销动画或者协程:

#1: 在 Animator 运行的时候,协程被勾销。咱们能够通过 invokeOnCancellation 回调办法来监听协程何时被勾销,这能让咱们同时勾销动画。

#2: 在协程被挂起的时候,Animator 被勾销。咱们通过 onAnimationCancel()) 回调来监听动画被勾销的事件,通过调用协程的 cancel() 办法来勾销挂起的协程。

这就是应用挂起函数期待办法执行来封装回调的根本应用了。????

组合应用

到这里,您可能有这样的疑难,” 看起来不错,然而我能从中播种什么呢?” 独自应用其中某个办法,并不会产生多大的作用,然而如果把它们组合起来,便能施展微小的威力。

上面是一个应用 Animator.awaitEnd() 来顺次运行 3 个动画的示例:

viewLifecycleOwner.lifecycleScope.launch {ObjectAnimator.ofFloat(imageView, View.ALPHA, 0f, 1f).run {start()
        awaitEnd()}

    ObjectAnimator.ofFloat(imageView, View.TRANSLATION_Y, 0f, 100f).run {start()
        awaitEnd()}

    ObjectAnimator.ofFloat(imageView, View.TRANSLATION_X, -100f, 0f).run {start()
        awaitEnd()}
}

这是一个很常见的应用案例,您能够把这些动画放进 AnimatorSet 中来实现同样的成果。

然而这里应用的办法实用于不同类型的异步操作: 咱们应用一个 ValueAnimator,一个 RecyclerView 的平滑滚动,以及一个 Animator 来举例:

viewLifecycleOwner.lifecycleScope.launch {
    // #1: ValueAnimator
    imageView.animate().run {alpha(0f)
        start()
        awaitEnd()}

    // #2: RecyclerView smooth scroll
    recyclerView.run {smoothScrollToPosition(10)
        // 该办法和其余办法相似,期待以后的滑动实现,咱们不须要刻意关注实现
        // 代码能够在文末的援用中找到
        awaitScrollEnd()}

    // #3: ObjectAnimator
    ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -100f, 0f).run {start()
        awaitEnd()}
}

试着用 AnimatorSet 实现一下吧????!如果不必协程,那就意味着咱们要监听每一个操作,在回调中执行下一个操作,这回调层级想想都可怕。

通过把不同的异步操作转换为协程的挂起函数,咱们取得了简洁明了地编排它们的能力。

咱们还能够更进一步 …

如果咱们心愿 ValueAnimator 和平滑滚动同时开始,而后在两者都实现之后启动 ObjectAnimator,该怎么做呢?那么在应用了协程之后,咱们能够应用 async() 来并发地执行咱们的代码:

viewLifecycleOwner.lifecycleScope.launch {
    val anim1 = async {imageView.animate().run {alpha(0f)
            start()
            awaitEnd()}
    }

    val scroll = async {
        recyclerView.run {smoothScrollToPosition(10)
            awaitScrollEnd()}
    }

    // 期待以上两个操作全副实现
    anim1.await()
    scroll.await()

    // 此时,anim1 和滑动都实现了,咱们开始执行 ObjectAnimator
    ObjectAnimator.ofFloat(textView, View.TRANSLATION_X, -100f, 0f).run {start()
        awaitEnd()}
}

然而如果您还想让滚动提早执行怎么办呢? (相似 Animator.startDelay) 办法) 那么应用协程也有很好的实现,咱们能够用 delay() 办法:

viewLifecycleOwner.lifecycleScope.launch {
    val anim1 = async {// ...}

    val scroll = async {
        // 咱们心愿在 anim1 实现后,提早 200ms 执行滚动
        delay(200)

        recyclerView.run {smoothScrollToPosition(10)
            awaitScrollEnd()}
    }

    // …
}

如果咱们想反复动画,那么咱们能够应用 repeat() 办法,或者应用 for 循环实现。上面是一个 view 淡入淡出 3 次的例子:

viewLifecycleOwner.lifecycleScope.launch {repeat(3) {ObjectAnimator.ofFloat(textView, View.ALPHA, 0f, 1f, 0f).run {start()
            awaitEnd()}
    }
}

您甚至能够通过反复计数来实现更精妙的性能。假如您心愿淡入淡出在每次反复中逐步变慢:

viewLifecycleOwner.lifecycleScope.launch {repeat(3) { repetition ->
        ObjectAnimator.ofFloat(textView, View.ALPHA, 0f, 1f, 0f).run {
            // 第一次执行继续 150ms,第二次:300ms,第三次:450ms
            duration = (repetition + 1) * 150L
            start()
            awaitEnd()}
    }
}

在我看来,这就是在 Android 视图零碎中应用协程能真正发挥作用的中央。咱们就算不去组合不同类型的回调,也能创立简单的异步变换,或是将不同类型的动画组合起来。

通过应用与咱们利用中数据层雷同的协程开发原语,还能使 UI 编程更便捷。对于刚接触代码的人来说,await 办法要比看似会断开的回调更具可读性。

最初

心愿通过本文,您能够进一步思考协程还能够在哪些其余的 API 中发挥作用。

接下来的文章中,咱们将探讨如何应用协程来组织一个简单的变换动画,其中也包含了一些常见 View 的实现,感兴趣的读者请持续关注咱们的更新。

退出移动版