Better insets propagation.

This commit is contained in:
Alex Hart
2025-11-12 15:46:58 -04:00
parent dd8104bf61
commit baf3309a04
6 changed files with 27 additions and 139 deletions

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components
import android.content.Context
import android.content.res.Configuration
import android.util.AttributeSet
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.Guideline
import androidx.core.content.withStyledAttributes
@@ -14,9 +13,7 @@ import androidx.core.view.WindowInsetsCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.main.VerticalInsets
import org.thoughtcrime.securesms.util.ViewUtil
import kotlin.math.roundToInt
/**
* A specialized [ConstraintLayout] that sets guidelines based on the window insets provided
@@ -62,11 +59,10 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
private val keyboardAnimator = KeyboardInsetAnimator()
private var overridingKeyboard: Boolean = false
private var previousKeyboardHeight: Int = 0
private var applyRootInsets: Boolean = false
private var previousStatusBarInset: Int = 0
private var insets: WindowInsetsCompat? = null
private var windowTypes: Int = InsetAwareConstraintLayout.windowTypes
private var verticalInsetOverride: VerticalInsets = VerticalInsets.Zero
private val windowInsetsListener = androidx.core.view.OnApplyWindowInsetsListener { _, insets ->
this.insets = insets
@@ -80,39 +76,25 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
override fun onAttachedToWindow() {
super.onAttachedToWindow()
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), windowInsetsListener)
ViewCompat.setOnApplyWindowInsetsListener(this, windowInsetsListener)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), null)
ViewCompat.setOnApplyWindowInsetsListener(this, null)
}
init {
if (attrs != null) {
context.withStyledAttributes(attrs, R.styleable.InsetAwareConstraintLayout) {
applyRootInsets = getBoolean(R.styleable.InsetAwareConstraintLayout_applyRootInsets, false)
if (getBoolean(R.styleable.InsetAwareConstraintLayout_animateKeyboardChanges, false)) {
ViewCompat.setWindowInsetsAnimationCallback(insetTarget(), keyboardAnimator)
ViewCompat.setWindowInsetsAnimationCallback(this@InsetAwareConstraintLayout, keyboardAnimator)
}
}
}
}
private fun insetTarget(): View = if (applyRootInsets) rootView else this
fun setApplyRootInsets(useRootInsets: Boolean) {
if (applyRootInsets == useRootInsets) {
return
}
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), null)
applyRootInsets = useRootInsets
ViewCompat.setOnApplyWindowInsetsListener(insetTarget(), windowInsetsListener)
}
/**
* Specifies whether or not window insets should be accounted for when applying
* insets. This is useful when choosing whether to display the content in this
@@ -130,14 +112,6 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
}
}
fun applyInsets(insets: VerticalInsets) {
verticalInsetOverride = insets
if (this.insets != null) {
applyInsets(this.insets!!.getInsets(windowTypes), this.insets!!.getInsets(keyboardType))
}
}
fun addKeyboardStateListener(listener: KeyboardStateListener) {
keyboardStateListeners += listener
}
@@ -157,18 +131,24 @@ open class InsetAwareConstraintLayout @JvmOverloads constructor(
private fun applyInsets(windowInsets: Insets, keyboardInsets: Insets) {
val isLtr = ViewUtil.isLtr(this)
val statusBar = if (verticalInsetOverride == VerticalInsets.Zero) windowInsets.top else verticalInsetOverride.statusBar.roundToInt()
val navigationBar = if (verticalInsetOverride == VerticalInsets.Zero) windowInsets.bottom else verticalInsetOverride.navBar.roundToInt()
val statusBar = windowInsets.top
val navigationBar = windowInsets.bottom
val parentStart = if (isLtr) windowInsets.left else windowInsets.right
val parentEnd = if (isLtr) windowInsets.right else windowInsets.left
statusBarGuideline?.setGuidelineBegin(statusBar)
navigationBarGuideline?.setGuidelineEnd(navigationBar)
parentStartGuideline?.setGuidelineBegin(parentStart)
parentEndGuideline?.setGuidelineEnd(parentEnd)
val statusBarShrinking = previousStatusBarInset > 0 && statusBar < previousStatusBarInset
windowInsetsListeners.forEach {
it.onApplyWindowInsets(statusBar, navigationBar, parentStart, parentEnd)
if (!statusBarShrinking) {
statusBarGuideline?.setGuidelineBegin(statusBar)
navigationBarGuideline?.setGuidelineEnd(navigationBar)
parentStartGuideline?.setGuidelineBegin(parentStart)
parentEndGuideline?.setGuidelineEnd(parentEnd)
windowInsetsListeners.forEach {
it.onApplyWindowInsets(statusBar, navigationBar, parentStart, parentEnd)
}
previousStatusBarInset = statusBar
}
if (keyboardInsets.bottom > 0) {

View File

@@ -273,7 +273,6 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModelV2
import org.thoughtcrime.securesms.longmessage.LongMessageFragment
import org.thoughtcrime.securesms.main.MainNavigationListLocation
import org.thoughtcrime.securesms.main.MainNavigationViewModel
import org.thoughtcrime.securesms.main.VerticalInsets
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity
@@ -623,15 +622,10 @@ class ConversationFragment :
SignalLocalMetrics.ConversationOpen.start()
}
fun applyRootInsets(insets: VerticalInsets) {
binding.root.applyInsets(insets)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
binding.toolbar.isBackInvokedCallbackEnabled = false
binding.root.setApplyRootInsets(!isLargeScreenSupportEnabled())
binding.root.setUseWindowTypes(!isLargeScreenSupportEnabled())
binding.root.setUseWindowTypes(args.conversationScreenType == ConversationScreenType.NORMAL && !resources.getWindowSizeClass().isSplitPane())
disposables.bindTo(viewLifecycleOwner)
@@ -2821,6 +2815,11 @@ class ConversationFragment :
invalidateOptionsMenu()
}
private fun scrollToBottom() {
layoutManager.scrollToPositionWithOffset(0, 0)
scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0)
}
/**
* Controls animation and visibility of the scrollDateHeader.
*/
@@ -2920,8 +2919,7 @@ class ConversationFragment :
private inner class DataObserver : RecyclerView.AdapterDataObserver() {
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
if (positionStart == 0 && shouldScrollToBottom()) {
layoutManager.scrollToPositionWithOffset(0, 0)
scrollListener?.onScrolled(binding.conversationItemRecycler, 0, 0)
scrollToBottom()
}
}
@@ -4538,6 +4536,7 @@ class ConversationFragment :
toast(R.string.AttachmentManager_cant_open_media_selection, Toast.LENGTH_LONG)
}
}
AttachmentKeyboardButton.POLL -> {
CreatePollFragment.show(childFragmentManager)
childFragmentManager.setFragmentResultListener(CreatePollFragment.REQUEST_KEY, requireActivity()) { _, bundle ->

View File

@@ -21,7 +21,6 @@ import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.ImageBitmap
@@ -72,8 +71,6 @@ fun NavGraphBuilder.chatNavGraphBuilder(
val route = navBackStackEntry.toRoute<MainNavigationDetailLocation.Chats.Conversation>()
val fragmentState = key(route) { rememberFragmentState() }
val context = LocalContext.current
val insets by rememberVerticalInsets()
val insetFlow = remember { snapshotFlow { insets } }
// Because it can take a long time to load content, we use a "fake" chat list image to delay displaying
// the fragment and prevent pop-in
@@ -124,14 +121,6 @@ fun NavGraphBuilder.chatNavGraphBuilder(
.background(MaterialTheme.colorScheme.background)
.fillMaxSize()
) { fragment ->
fragment.viewLifecycleOwner.lifecycleScope.launch {
fragment.repeatOnLifecycle(Lifecycle.State.STARTED) {
insetFlow.collect {
fragment.applyRootInsets(insets)
}
}
}
backPressedState.attach(fragment)
fragment.viewLifecycleOwner.lifecycleScope.launch {

View File

@@ -1,78 +0,0 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.main
import android.os.Parcelable
import androidx.annotation.Px
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.statusBars
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import kotlinx.parcelize.Parcelize
import org.thoughtcrime.securesms.window.isSplitPane
@Parcelize
data class VerticalInsets(
@param:Px val statusBar: Float,
@param:Px val navBar: Float
) : Parcelable {
companion object {
val Zero = VerticalInsets(0f, 0f)
}
}
@Composable
fun rememberVerticalInsets(): State<VerticalInsets> {
val statusBarInsets = WindowInsets.statusBars
val navigationBarInsets = WindowInsets.navigationBars
val statusBarPadding = statusBarInsets.asPaddingValues()
val navigationBarPadding = navigationBarInsets.asPaddingValues()
val density = LocalDensity.current
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val insets = rememberSaveable { mutableStateOf(VerticalInsets.Zero) }
val updated = remember(statusBarInsets, navigationBarInsets, windowSizeClass) {
insets.value = if (windowSizeClass.isSplitPane()) {
VerticalInsets.Zero
} else {
calculateAndUpdateInsets(density, statusBarPadding, navigationBarPadding)
}
0
}
return insets
}
private fun calculateAndUpdateInsets(
density: Density,
statusBarPadding: PaddingValues,
navigationBarPadding: PaddingValues
): VerticalInsets {
val statusBarPx = with(density) {
(statusBarPadding.calculateTopPadding() + statusBarPadding.calculateBottomPadding()).toPx()
}
val navBarPx = with(density) {
(navigationBarPadding.calculateTopPadding() + navigationBarPadding.calculateBottomPadding()).toPx()
}
return VerticalInsets(
statusBar = statusBarPx,
navBar = navBarPx
)
}