mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-22 18:55:12 +00:00
Better insets propagation.
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user