mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-08-04 20:34:14 +01:00
Fix various edge to edge bugs.
This commit is contained in:
+9
-1
@@ -41,6 +41,12 @@ abstract class DSLSettingsFragment(
|
||||
|
||||
private var toolbar: Toolbar? = null
|
||||
|
||||
/**
|
||||
* Set by layouts that anchor the list to the top of the toolbar rather than below it. Those lists scroll
|
||||
* behind the toolbar, so they have to carry the status bar inset themselves.
|
||||
*/
|
||||
protected open val listScrollsBehindToolbar: Boolean = false
|
||||
|
||||
@CallSuper
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
toolbar = view.findViewById(R.id.toolbar)
|
||||
@@ -106,8 +112,10 @@ abstract class DSLSettingsFragment(
|
||||
}
|
||||
|
||||
recyclerView?.let { recycler ->
|
||||
val insetTypes = WindowInsetsCompat.Type.navigationBars() or if (listScrollsBehindToolbar) WindowInsetsCompat.Type.statusBars() else 0
|
||||
|
||||
recycler.clipToPadding = false
|
||||
SystemWindowInsetsSetter.attach(recycler, viewLifecycleOwner, WindowInsetsCompat.Type.navigationBars())
|
||||
SystemWindowInsetsSetter.attach(recycler, viewLifecycleOwner, insetTypes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
@@ -140,6 +140,8 @@ class ConversationSettingsFragment :
|
||||
menuId = R.menu.conversation_settings
|
||||
) {
|
||||
|
||||
override val listScrollsBehindToolbar: Boolean = true
|
||||
|
||||
private val args: ConversationSettingsFragmentArgs by navArgs()
|
||||
private val alertTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary) }
|
||||
private val alertDisabledTint by lazy { ContextCompat.getColor(requireContext(), R.color.signal_alert_primary_50) }
|
||||
|
||||
+6
@@ -16,6 +16,7 @@ import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
@@ -37,6 +38,7 @@ import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageCategoriesAdap
|
||||
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsRepository;
|
||||
import org.thoughtcrime.securesms.reactions.edit.EditReactionsActivity;
|
||||
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
|
||||
@@ -214,6 +216,10 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends FixedRound
|
||||
|
||||
container.addView(tabBar);
|
||||
|
||||
// The tab bar is pinned to the bottom of the dialog window rather than the sheet, so it is not covered by
|
||||
// the sheet's own inset padding.
|
||||
SystemWindowInsetsSetter.attach(tabBar.findViewById(R.id.emoji_categories_row), getViewLifecycleOwner(), WindowInsetsCompat.Type.navigationBars());
|
||||
|
||||
emojiPageView.addOnScrollListener(new TopAndBottomShadowHelper(requireView().findViewById(R.id.react_with_any_emoji_top_shadow),
|
||||
tabBar.findViewById(R.id.react_with_any_emoji_bottom_shadow)));
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.DisplayCutoutCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
@@ -92,7 +93,7 @@ public final class FullscreenHelper {
|
||||
|
||||
public void showAndHideWithSystemUI(@NonNull Window window, @NonNull View... views) {
|
||||
ViewCompat.setOnApplyWindowInsetsListener(window.getDecorView(), (view, insets) -> {
|
||||
boolean hide = !insets.isVisible(WindowInsetsCompat.Type.systemBars());
|
||||
boolean hide = !areBarsVisible(insets);
|
||||
|
||||
for (View target : views) {
|
||||
if (target == null) {
|
||||
@@ -114,7 +115,7 @@ public final class FullscreenHelper {
|
||||
.start();
|
||||
}
|
||||
|
||||
return insets;
|
||||
return ViewCompat.onApplyWindowInsets(view, insets);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -127,8 +128,22 @@ public final class FullscreenHelper {
|
||||
}
|
||||
|
||||
public boolean isSystemUiVisible() {
|
||||
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(activity.getWindow().getDecorView());
|
||||
return insets == null || insets.isVisible(WindowInsetsCompat.Type.systemBars());
|
||||
return areBarsVisible(ViewCompat.getRootWindowInsets(activity.getWindow().getDecorView()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the bars that {@link #showSystemUI()} / {@link #hideSystemUI()} control are currently on screen.
|
||||
* <p>
|
||||
* Checks the two bars individually rather than {@link WindowInsetsCompat.Type#systemBars()}, which also
|
||||
* covers the caption bar: {@code isVisible} requires every requested type to be visible, and a phone window
|
||||
* has no caption bar source, so the aggregate answer is always "hidden".
|
||||
*/
|
||||
private static boolean areBarsVisible(@Nullable WindowInsetsCompat insets) {
|
||||
if (insets == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return insets.isVisible(WindowInsetsCompat.Type.statusBars()) || insets.isVisible(WindowInsetsCompat.Type.navigationBars());
|
||||
}
|
||||
|
||||
public void hideSystemUI() {
|
||||
|
||||
@@ -22,10 +22,14 @@ object SystemWindowInsetsSetter {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the view whenever a layout occurs to properly account for the system bar insets, added
|
||||
* on top of the view's original padding ([ApplyMode.PADDING]) or margin ([ApplyMode.MARGIN]).
|
||||
* This is safe to call repeatedly because it only triggers an extra layout pass IF the applied
|
||||
* values actually changed.
|
||||
* Accounts for the system bar insets by adding them on top of the view's original padding
|
||||
* ([ApplyMode.PADDING]) or margin ([ApplyMode.MARGIN]).
|
||||
*
|
||||
* Applied from two places. Primarily from the inset dispatch, which runs before measure and layout, so the
|
||||
* first frame is already inset instead of visibly shifting a frame later. That dispatch doesn't reach every
|
||||
* view though (an ancestor may consume the insets first), so each layout re-applies as a fallback, posted
|
||||
* because a layout-time `requestLayout()` is dropped by the framework. Both paths are safe to run
|
||||
* repeatedly: they only trigger another layout if the values actually changed.
|
||||
*/
|
||||
@JvmStatic
|
||||
@JvmOverloads
|
||||
@@ -42,30 +46,41 @@ object SystemWindowInsetsSetter {
|
||||
Insets.of(view.paddingLeft, view.paddingTop, view.paddingRight, view.paddingBottom)
|
||||
}
|
||||
|
||||
val listener = view.doOnEachLayout {
|
||||
val applyInsets = {
|
||||
val insets = resolveInsets(view, insetType)
|
||||
val left = base.left + insets.left
|
||||
val top = base.top + insets.top
|
||||
val right = base.right + insets.right
|
||||
val bottom = base.bottom + insets.bottom
|
||||
|
||||
view.post {
|
||||
when (applyMode) {
|
||||
ApplyMode.PADDING -> view.setPadding(left, top, right, bottom)
|
||||
ApplyMode.MARGIN -> {
|
||||
val params = view.layoutParams as? ViewGroup.MarginLayoutParams ?: return@post
|
||||
if (params.leftMargin != left || params.topMargin != top || params.rightMargin != right || params.bottomMargin != bottom) {
|
||||
params.setMargins(left, top, right, bottom)
|
||||
view.layoutParams = params
|
||||
}
|
||||
when (applyMode) {
|
||||
ApplyMode.PADDING -> view.setPadding(left, top, right, bottom)
|
||||
|
||||
ApplyMode.MARGIN -> {
|
||||
val params = view.layoutParams as? ViewGroup.MarginLayoutParams
|
||||
if (params != null && (params.leftMargin != left || params.topMargin != top || params.rightMargin != right || params.bottomMargin != bottom)) {
|
||||
params.setMargins(left, top, right, bottom)
|
||||
view.layoutParams = params
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view) { target, windowInsets ->
|
||||
// Let the view dispatch on down to its children first, then apply ours on top.
|
||||
val result = ViewCompat.onApplyWindowInsets(target, windowInsets)
|
||||
applyInsets()
|
||||
result
|
||||
}
|
||||
|
||||
val listener = view.doOnEachLayout {
|
||||
view.post { applyInsets() }
|
||||
}
|
||||
|
||||
val lifecycleObserver = object : DefaultLifecycleObserver {
|
||||
override fun onDestroy(owner: LifecycleOwner) {
|
||||
view.removeOnLayoutChangeListener(listener)
|
||||
ViewCompat.setOnApplyWindowInsetsListener(view, null)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,13 +16,15 @@
|
||||
android:background="@drawable/bottom_toolbar_shadow" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/emoji_categories_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?actionBarSize"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@color/react_with_any_background"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="?actionBarSize"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
|
||||
@@ -139,6 +139,13 @@
|
||||
<item name="fixedRoundedCornerBottomSheetStyle">@style/Widget.Signal.FixedRoundedCorners</item>
|
||||
<item name="permissionsRationaleDialogTheme">@style/Theme.Signal.AlertDialog.Light.Cornered</item>
|
||||
<item name="android:forceDarkAllowed" tools:targetApi="29">false</item>
|
||||
<!--
|
||||
Off so the framework does not scrim a transparent system bar for legibility, hiding the content we
|
||||
draw behind it. Set here rather than alongside enableEdgeToEdge() because dialog and bottom sheet
|
||||
windows inherit this theme, and they are not covered by those calls.
|
||||
-->
|
||||
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">false</item>
|
||||
<item name="android:enforceStatusBarContrast" tools:targetApi="29">false</item>
|
||||
|
||||
<!-- Material 3 -->
|
||||
<item name="colorPrimary">@color/signal_colorPrimary</item>
|
||||
@@ -235,6 +242,9 @@
|
||||
<item name="fixedRoundedCornerBottomSheetStyle">@style/Widget.Signal.FixedRoundedCorners</item>
|
||||
<item name="permissionsRationaleDialogTheme">@style/Theme.Signal.AlertDialog.Dark.Cornered</item>
|
||||
<item name="android:forceDarkAllowed" tools:targetApi="29">false</item>
|
||||
<!-- As above: no framework contrast scrim over the transparent system bars. -->
|
||||
<item name="android:enforceNavigationBarContrast" tools:targetApi="29">false</item>
|
||||
<item name="android:enforceStatusBarContrast" tools:targetApi="29">false</item>
|
||||
|
||||
<!-- Material 3 -->
|
||||
<item name="colorPrimary">@color/signal_colorPrimary</item>
|
||||
|
||||
+11
-1
@@ -261,13 +261,23 @@ public class StickyHeaderGridLayoutManager extends RecyclerView.LayoutManager im
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Rows are laid out, and recycled, against {@code getPaddingTop()} and
|
||||
* {@code getHeight() - getPaddingBottom()}. That is the right boundary while the padding clips, but when it
|
||||
* does not -- the usual edge-to-edge setup, where the list is padded by the navigation bar inset and draws
|
||||
* behind it -- the padded strips are visible, so a row treated as off screen there pops into existence in
|
||||
* plain sight. Extending the boundaries by the padding makes rows scroll through those strips instead.
|
||||
*/
|
||||
private int getExtraLayoutSpace(RecyclerView.State state) {
|
||||
if (state.hasTargetScrollPosition()) {
|
||||
return getHeight();
|
||||
}
|
||||
else {
|
||||
else if (getClipToPadding()) {
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return Math.max(getPaddingTop(), getPaddingBottom());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Reference in New Issue
Block a user