From bc88887195cbd33fadc75ba624bbee3dd1255a17 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Tue, 23 May 2023 11:48:42 -0400 Subject: [PATCH] Animate CFv2 with keyboard opening or closing. --- .../components/InsetAwareConstraintLayout.kt | 107 +++++++++++++++--- .../res/layout/v2_conversation_fragment.xml | 3 +- app/src/main/res/values/attrs.xml | 4 + 3 files changed, 98 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt index 044db7b961..846cc565ca 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -5,10 +5,12 @@ import android.os.Build import android.util.AttributeSet import android.util.DisplayMetrics import android.view.Surface -import android.view.WindowInsets import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Guideline +import androidx.core.content.withStyledAttributes import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsAnimationCompat import androidx.core.view.WindowInsetsCompat import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.keyvalue.SignalStore @@ -17,7 +19,8 @@ import org.thoughtcrime.securesms.util.ViewUtil /** * A specialized [ConstraintLayout] that sets guidelines based on the window insets provided - * by the system. + * by the system. For improved backwards-compatibility we must use [ViewCompat] for configuring + * the inset change callbacks. * * In portrait mode these are how the guidelines will be configured: * @@ -34,6 +37,7 @@ import org.thoughtcrime.securesms.util.ViewUtil * These guidelines will only be updated if present in your layout, you can use * `` to quickly include them. */ +@Suppress("LeakingThis") open class InsetAwareConstraintLayout @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, @@ -51,21 +55,26 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val parentEndGuideline: Guideline? by lazy { findViewById(R.id.parent_end_guideline) } private val keyboardGuideline: Guideline? by lazy { findViewById(R.id.keyboard_guideline) } + private val keyboardAnimator = KeyboardInsetAnimator() private val displayMetrics = DisplayMetrics() private var overridingKeyboard: Boolean = false - override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets { - val windowInsetsCompat = WindowInsetsCompat.toWindowInsetsCompat(insets) + init { + ViewCompat.setOnApplyWindowInsetsListener(this) { _, windowInsetsCompat -> + applyInsets(windowInsets = windowInsetsCompat.getInsets(windowTypes), keyboardInsets = windowInsetsCompat.getInsets(keyboardType)) + windowInsetsCompat + } - val windowInsets = windowInsetsCompat.getInsets(windowTypes) - val keyboardInset = windowInsetsCompat.getInsets(keyboardType) - - applyInsets(windowInsets, keyboardInset) - - return super.onApplyWindowInsets(insets) + if (attrs != null) { + context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) { + if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) { + ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator) + } + } + } } - private fun applyInsets(windowInsets: Insets, keyboardInset: Insets) { + private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) { val isLtr = ViewUtil.isLtr(this) statusBarGuideline?.setGuidelineBegin(windowInsets.top) @@ -73,11 +82,19 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( parentStartGuideline?.setGuidelineBegin(if (isLtr) windowInsets.left else windowInsets.right) parentEndGuideline?.setGuidelineEnd(if (isLtr) windowInsets.right else windowInsets.left) - if (keyboardInset.bottom > 0) { - setKeyboardHeight(keyboardInset.bottom) - keyboardGuideline?.setGuidelineEnd(keyboardInset.bottom) + if (keyboardInsets.bottom > 0) { + setKeyboardHeight(keyboardInsets.bottom) + if (!keyboardAnimator.animating) { + keyboardGuideline?.setGuidelineEnd(keyboardInsets.bottom) + } else { + keyboardAnimator.endingGuidelineEnd = keyboardInsets.bottom + } } else if (!overridingKeyboard) { - keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) + if (!keyboardAnimator.animating) { + keyboardGuideline?.setGuidelineEnd(windowInsets.bottom) + } else { + keyboardAnimator.endingGuidelineEnd = windowInsets.bottom + } } } @@ -139,4 +156,64 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor( private val Guideline?.guidelineEnd: Int get() = if (this == null) 0 else (layoutParams as LayoutParams).guideEnd + + /** + * Adjusts the [keyboardGuideline] to move with the IME keyboard opening or closing. + */ + private inner class KeyboardInsetAnimator : WindowInsetsAnimationCompat.Callback(DISPATCH_MODE_STOP) { + + var animating = false + private set + + private var startingGuidelineEnd: Int = 0 + var endingGuidelineEnd: Int = 0 + set(value) { + field = value + growing = value > startingGuidelineEnd + } + private var growing: Boolean = false + + override fun onPrepare(animation: WindowInsetsAnimationCompat) { + if (overridingKeyboard) { + return + } + + animating = true + startingGuidelineEnd = keyboardGuideline.guidelineEnd + } + + override fun onProgress(insets: WindowInsetsCompat, runningAnimations: MutableList): WindowInsetsCompat { + if (overridingKeyboard) { + return insets + } + + val imeAnimation = runningAnimations.find { it.typeMask and WindowInsetsCompat.Type.ime() != 0 } + if (imeAnimation == null) { + return insets + } + + val estimatedKeyboardHeight: Int = if (growing) { + endingGuidelineEnd * imeAnimation.interpolatedFraction + } else { + startingGuidelineEnd * (1f - imeAnimation.interpolatedFraction) + }.toInt() + + if (growing) { + keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(startingGuidelineEnd)) + } else { + keyboardGuideline?.setGuidelineEnd(estimatedKeyboardHeight.coerceAtLeast(endingGuidelineEnd)) + } + + return insets + } + + override fun onEnd(animation: WindowInsetsAnimationCompat) { + if (overridingKeyboard) { + return + } + + keyboardGuideline?.setGuidelineEnd(endingGuidelineEnd) + animating = false + } + } } diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index 8810d234cb..ad9ef2f098 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -3,7 +3,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + app:animateKeyboardChanges="true"> + + + +