Anki-Android icon indicating copy to clipboard operation
Anki-Android copied to clipboard

Forward-port UI to new edge-to-edge for API35

Open mikehardy opened this issue 1 year ago • 1 comments

Related targetSdk 35 bump with deprecation suppression that needs handling:

  • #17333

New upstream edge-to-edge UI thing for apps that either target SDK 35 or specifically opt in:

https://developer.android.com/develop/ui/views/layout/edge-to-edge

Successful resolution of this issue is when:

  • we use insets and colors correctly + well such that the app is beautiful while edge-to-edge
  • and all of the app UI elements are available / not occluded by device hardware items (pinhole cameras, notches etc)

mikehardy avatar Oct 31 '24 16:10 mikehardy

For anyone interested, here is existing work that shows calculation of insets required to have your content not obscured by whatever hardware items (pinhole cameras, notches, rounded corners) are in part of the total display rectangle once edge-to-edge is enabled: https://github.com/th3rdwave/react-native-safe-area-context/blob/abe15139b641ff1c3c8f989df3c24821befe3384/android/src/main/java/com/th3rdwave/safeareacontext/SafeAreaUtils.kt#L62

I believe the general idea is "figure out the rectangle we can use that is not obscured by anything and set insets for that, and listeners for when it changes (like when status bar or android system button bar slide in or out so you may update insets and re-layout" combined with "set colors for everything including areas we are avoiding by setting insets so that the app theme does take over the whole rectangle - even obscured bits - in edge-to-edge"

But of course I could be wrong - not much experience here myself

mikehardy avatar Nov 04 '24 12:11 mikehardy

Looks like we can opt out https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge

https://developer.android.com/reference/android/R.attr#windowOptOutEdgeToEdgeEnforcement

mikehardy avatar Jul 01 '25 20:07 mikehardy

This adds edge to edge support for DeckPicker & CardBrowser on my Pixel 9 Pro (Android 15).

I haven't tested it on older versions of Android

Edit: browser toolbar is a little too tall

Subject: [PATCH] edge to edge
---
Index: AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt	(revision 91c43fbe8524a8d543806e103943a38c457ad36f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt	(date 1752478353975)
@@ -71,6 +71,8 @@
 import androidx.core.util.component2
 import androidx.core.view.MenuItemCompat
 import androidx.core.view.OnReceiveContentListener
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.doOnLayout
 import androidx.core.view.isVisible
 import androidx.draganddrop.DropHelper
@@ -614,7 +616,18 @@
                     }
             }
 
-        reviewSummaryTextView = findViewById(R.id.today_stats_text_view)
+        reviewSummaryTextView = findViewById<TextView>(R.id.today_stats_text_view).apply {
+            ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
+                val navbarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
+                view.setPadding(
+                    navbarInsets.left,
+                    navbarInsets.top,
+                    navbarInsets.right,
+                    navbarInsets.bottom
+                )
+                insets
+            }
+        }
 
         shortAnimDuration = resources.getInteger(android.R.integer.config_shortAnimTime)
 
Index: AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt b/AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt	(revision 91c43fbe8524a8d543806e103943a38c457ad36f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/NavigationDrawerActivity.kt	(date 1752477640711)
@@ -37,6 +37,8 @@
 import androidx.core.content.pm.ShortcutManagerCompat
 import androidx.core.graphics.drawable.IconCompat
 import androidx.core.view.GravityCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.get
 import androidx.core.view.size
 import androidx.drawerlayout.widget.ClosableDrawerLayout
@@ -50,13 +52,18 @@
 import com.ichi2.anki.preferences.sharedPrefs
 import com.ichi2.anki.utils.ext.showDialogFragment
 import com.ichi2.anki.workarounds.FullDraggableContainerFix
+import com.ichi2.compat.CompatHelper
 import com.ichi2.utils.HandlerUtils
 import com.ichi2.utils.IntentUtil
+import com.ichi2.utils.dp
 import timber.log.Timber
 
 abstract class NavigationDrawerActivity :
     AnkiActivity(),
     NavigationView.OnNavigationItemSelectedListener {
+
+    open val edgeToEdge: Boolean = true
+
     /**
      * Navigation Drawer
      */
@@ -118,7 +125,7 @@
         get() = if (fitsSystemWindows()) R.layout.navigation_drawer_layout else R.layout.navigation_drawer_layout_fullscreen
 
     /** Whether android:fitsSystemWindows="true" should be applied to the navigation drawer  */
-    protected open fun fitsSystemWindows(): Boolean = true
+    protected open fun fitsSystemWindows(): Boolean = false
 
     fun navDrawerIsReady(): Boolean = navigationView != null
 
@@ -129,15 +136,18 @@
         drawerLayout = mainView.findViewById(R.id.drawer_layout)
         // set a custom shadow that overlays the main content when the drawer opens
         drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START)
-        // Force transparent status bar with primary dark color underlaid so that the drawer displays under status bar
-        window.statusBarColor = getColor(R.color.transparent)
-        drawerLayout.setStatusBarBackgroundColor(
-            MaterialColors.getColor(
-                this,
-                R.attr.appBarColor,
-                0,
-            ),
-        )
+
+        if (!edgeToEdge) {
+            // Force transparent status bar with primary dark color underlaid so that the drawer displays under status bar
+            window.statusBarColor = getColor(R.color.transparent)
+            drawerLayout.setStatusBarBackgroundColor(
+                MaterialColors.getColor(
+                    this,
+                    R.attr.appBarColor,
+                    0,
+                ),
+            )
+        }
         // Setup toolbar and hamburger
         navigationView = drawerLayout.findViewById(R.id.navdrawer_items_container)
         navigationView!!.setNavigationItemSelectedListener(this)
@@ -151,6 +161,22 @@
             // Decide which action to take when the navigation button is tapped.
             toolbar.setNavigationOnClickListener { onNavigationPressed() }
         }
+
+        if (edgeToEdge) {
+            CompatHelper.enableEdgeToEdge(window)
+            if (toolbar != null) {
+                ViewCompat.setOnApplyWindowInsetsListener(toolbar) { view, insets ->
+                    val statusBarInsets = insets.getInsets(WindowInsetsCompat.Type.statusBars())
+                    view.setPadding(
+                        view.paddingLeft,
+                        statusBarInsets.top,
+                        view.paddingRight,
+                        view.paddingBottom + 6.dp.toPx(this)
+                    )
+                    insets
+                }
+            }
+        }
         setupBackPressedCallbacks()
         // ActionBarDrawerToggle ties together the the proper interactions
         // between the sliding drawer and the action bar app icon
Index: AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt	(revision 91c43fbe8524a8d543806e103943a38c457ad36f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AbstractFlashcardViewer.kt	(date 1752477640705)
@@ -293,6 +293,9 @@
     val onRenderProcessGoneDelegate = OnRenderProcessGoneDelegate(this)
     protected val tts = TTS()
 
+    override val edgeToEdge: Boolean
+        get() = false
+
     // ----------------------------------------------------------------------------
     // LISTENERS
     // ----------------------------------------------------------------------------
Index: AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt
--- a/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt	(revision 91c43fbe8524a8d543806e103943a38c457ad36f)
+++ b/AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt	(date 1752478253849)
@@ -23,6 +23,8 @@
 import android.widget.TextView
 import androidx.annotation.VisibleForTesting
 import androidx.core.content.ContextCompat
+import androidx.core.view.ViewCompat
+import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.isVisible
 import androidx.fragment.app.Fragment
 import androidx.fragment.app.activityViewModels
@@ -77,6 +79,18 @@
         cardsListView =
             view.findViewById<RecyclerView>(R.id.card_browser_list).apply {
                 attachFastScroller(R.id.browser_scroller)
+
+                this.clipToPadding = false
+                ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
+                    val navbarInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars())
+                    view.setPadding(
+                        view.paddingLeft,
+                        view.paddingTop,
+                        view.paddingRight,
+                        navbarInsets.bottom
+                    )
+                    insets
+                }
             }
         DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL).apply {
             setDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.browser_divider)!!)
Index: AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt b/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt
--- a/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt	(revision 91c43fbe8524a8d543806e103943a38c457ad36f)
+++ b/AnkiDroid/src/main/java/com/ichi2/compat/CompatHelper.kt	(date 1752477809136)
@@ -24,14 +24,20 @@
 import android.content.pm.PackageManager
 import android.content.pm.PackageManager.NameNotFoundException
 import android.content.pm.ResolveInfo
+import android.graphics.Color
 import android.os.Build
 import android.os.Bundle
 import android.view.KeyCharacterMap.deviceHasKey
 import android.view.KeyEvent.KEYCODE_PAGE_DOWN
 import android.view.KeyEvent.KEYCODE_PAGE_UP
 import android.view.View
+import android.view.Window
+import android.view.WindowManager
 import androidx.appcompat.widget.TooltipCompat
 import androidx.core.content.ContextCompat
+import androidx.core.view.WindowCompat.setDecorFitsSystemWindows
+import com.ichi2.compat.CompatHelper.Companion.compat
+import com.ichi2.compat.CompatHelper.Companion.resolveActivityCompat
 import java.io.Serializable
 
 /**
@@ -197,6 +203,41 @@
             filter: IntentFilter,
             @ContextCompat.RegisterReceiverFlags flags: Int,
         ) = ContextCompat.registerReceiver(this, receiver, filter, flags)
+
+        /**
+         * Enables the content of the given [window][Window] to reach the edges of the window
+         * without getting inset by system insets, and prevents the framework from placing color views
+         * behind system bars.
+         *
+         * @param window the given window.
+         */
+        @Suppress("deprecation")
+        fun enableEdgeToEdge(window: Window) {
+            // !! replace when androidx.core:core:1.17.0 is stable
+
+            // This triggers the initialization of the decor view here to prevent the attributes set by
+            // this method from getting overwritten by the initialization later.
+            window.getDecorView()
+
+            setDecorFitsSystemWindows(window, false)
+            window.statusBarColor = Color.TRANSPARENT
+            window.navigationBarColor = Color.TRANSPARENT
+            if (Build.VERSION.SDK_INT >= 28) {
+                val newMode = if (Build.VERSION.SDK_INT >= 30)
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+                else
+                    WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
+                val attrs: WindowManager.LayoutParams = window.getAttributes()
+                if (attrs.layoutInDisplayCutoutMode != newMode) {
+                    attrs.layoutInDisplayCutoutMode = newMode
+                    window.setAttributes(attrs)
+                }
+            }
+            if (Build.VERSION.SDK_INT >= 29) {
+                window.setStatusBarContrastEnforced(false)
+                window.setNavigationBarContrastEnforced(false)
+            }
+        }
     }
 }

david-allison avatar Jul 14 '25 07:07 david-allison

Hello 👋, this issue has been opened for more than 3 months with no activity on it. If the issue is still here, please keep in mind that we need community support and help to fix it! Just comment something like still searching for solutions and if you found one, please open a pull request! You have 7 days until this gets closed automatically

github-actions[bot] avatar Oct 28 '25 17:10 github-actions[bot]