Anki-Android
Anki-Android copied to clipboard
Forward-port UI to new edge-to-edge for API35
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)
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
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
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)
+ }
+ }
}
}
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