From baf3309a04cd26721ec97c1056f60a04e6bf3746 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 12 Nov 2025 15:46:58 -0400 Subject: [PATCH] Better insets propagation. --- .../components/InsetAwareConstraintLayout.kt | 56 +++++-------- .../conversation/v2/ConversationFragment.kt | 17 ++-- .../securesms/main/ChatsNavHost.kt | 11 --- .../securesms/main/VerticalInsets.kt | 78 ------------------- .../res/layout/v2_conversation_fragment.xml | 3 +- app/src/main/res/values/attrs.xml | 1 - 6 files changed, 27 insertions(+), 139 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/main/VerticalInsets.kt 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 9c0e0dfe72..e28405d0ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InsetAwareConstraintLayout.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 07c37ad42f..66bead5735 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -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 -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt b/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt index 86ec9e12e2..30dce191f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/main/ChatsNavHost.kt @@ -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() 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/main/VerticalInsets.kt b/app/src/main/java/org/thoughtcrime/securesms/main/VerticalInsets.kt deleted file mode 100644 index 360c5bc411..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/main/VerticalInsets.kt +++ /dev/null @@ -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 { - 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 - ) -} diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index 15816ae624..899746b0bb 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -5,8 +5,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipChildren="false" - app:animateKeyboardChanges="true" - app:applyRootInsets="false"> + app:animateKeyboardChanges="true"> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 086d440a66..aa60e16d66 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -382,6 +382,5 @@ -