Skip to content

Commit 10474bc

Browse files
committed
feat(card-browser): enable edge to edge
A wrapper was required for the toolbar: adding padding increased the height from minHeight, so apply the padding to a parent element. Roborazzi test added to catch future regressions Issue 17334 Assisted-by: Claude Opus 4.7 - unit tests + frame fix, rest was my own
1 parent ed752d5 commit 10474bc

6 files changed

Lines changed: 157 additions & 41 deletions

File tree

AnkiDroid/src/main/java/com/ichi2/anki/CardBrowser.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package com.ichi2.anki
2020

2121
import android.content.Context
2222
import android.content.Intent
23+
import android.graphics.Color
2324
import android.os.Bundle
2425
import android.view.KeyEvent
2526
import android.view.Menu
@@ -30,6 +31,8 @@ import android.view.WindowManager
3031
import android.widget.LinearLayout
3132
import android.widget.TextView
3233
import androidx.activity.OnBackPressedCallback
34+
import androidx.activity.SystemBarStyle
35+
import androidx.activity.enableEdgeToEdge
3336
import androidx.activity.result.ActivityResult
3437
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
3538
import androidx.annotation.CheckResult
@@ -38,7 +41,10 @@ import androidx.annotation.MainThread
3841
import androidx.annotation.VisibleForTesting
3942
import androidx.core.view.MenuHost
4043
import androidx.core.view.MenuProvider
44+
import androidx.core.view.ViewCompat
45+
import androidx.core.view.WindowInsetsCompat
4146
import androidx.core.view.isVisible
47+
import androidx.core.view.updatePadding
4248
import androidx.fragment.app.commit
4349
import androidx.lifecycle.Lifecycle
4450
import androidx.lifecycle.LifecycleOwner
@@ -298,6 +304,8 @@ open class CardBrowser :
298304
return
299305
}
300306
tagsDialogFactory = TagsDialogFactory(this).attachToActivity<TagsDialogFactory>(this)
307+
// match the status bar theme of the rest of the app
308+
enableEdgeToEdge(statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT))
301309
super.onCreate(savedInstanceState)
302310
binding = ActivityCardBrowserBinding.inflate(layoutInflater)
303311
if (!ensureStoragePermissions()) {
@@ -312,6 +320,7 @@ open class CardBrowser :
312320

313321
setViewBinding(binding)
314322
initNavigationDrawer(findViewById(android.R.id.content))
323+
applyToolbarInsets()
315324

316325
/**
317326
* Check if noteEditorFrame is not null and if its visibility is set to VISIBLE.
@@ -477,6 +486,20 @@ open class CardBrowser :
477486
super.setupBackPressedCallbacks()
478487
}
479488

489+
override fun fitsSystemWindows(): Boolean = false
490+
491+
private fun applyToolbarInsets() {
492+
val container = findViewById<View>(R.id.toolbar_container) ?: return
493+
ViewCompat.setOnApplyWindowInsetsListener(container) { view, insets ->
494+
val bars =
495+
insets.getInsets(
496+
WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.displayCutout(),
497+
)
498+
view.updatePadding(left = bars.left, top = bars.top, right = bars.right)
499+
insets
500+
}
501+
}
502+
480503
private fun showSaveChangesDialog(launcher: NoteEditorLauncher) {
481504
DiscardChangesDialog.showDialog(
482505
context = this,

AnkiDroid/src/main/java/com/ichi2/anki/browser/CardBrowserFragment.kt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import androidx.core.view.MenuProvider
4848
import androidx.core.view.ViewCompat
4949
import androidx.core.view.WindowInsetsCompat
5050
import androidx.core.view.isVisible
51+
import androidx.core.view.updatePadding
5152
import androidx.core.widget.doAfterTextChanged
5253
import androidx.fragment.app.Fragment
5354
import androidx.fragment.app.FragmentTransaction
@@ -252,7 +253,9 @@ class CardBrowserFragment :
252253
cardsListView =
253254
view.findViewById<RecyclerView>(R.id.card_browser_list).apply {
254255
attachFastScroller(R.id.browser_scroller)
256+
clipToPadding = false
255257
}
258+
applyContentInsets(view)
256259
DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL).apply {
257260
setDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.browser_divider)!!)
258261
cardsListView.addItemDecoration(this)
@@ -372,6 +375,19 @@ class CardBrowserFragment :
372375
setupMenu()
373376
}
374377

378+
private fun applyContentInsets(root: View) {
379+
ViewCompat.setOnApplyWindowInsetsListener(root) { v, insets ->
380+
val bars =
381+
insets.getInsets(
382+
WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.displayCutout(),
383+
)
384+
v.updatePadding(left = bars.left, right = bars.right)
385+
// RecyclerView uses clipToPadding=false so list scrolls under the navigation bar
386+
cardsListView.updatePadding(bottom = bars.bottom)
387+
insets
388+
}
389+
}
390+
375391
private fun setupMenu() {
376392
val menuHost: MenuHost = requireCardBrowserActivity()
377393

Lines changed: 42 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,56 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<androidx.appcompat.widget.Toolbar xmlns:android="http://schemas.android.com/apk/res/android"
2+
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
33
xmlns:app="http://schemas.android.com/apk/res-auto"
44
xmlns:tools="http://schemas.android.com/tools"
5-
android:id="@+id/toolbar"
5+
android:id="@+id/toolbar_container"
66
android:layout_width="match_parent"
77
android:layout_height="wrap_content"
8-
android:background="?attr/appBarColor"
9-
android:minHeight="?attr/actionBarSize"
10-
android:theme="@style/ActionBarStyle"
11-
app:navigationContentDescription="@string/abc_action_bar_up_description"
12-
app:navigationIcon="?attr/homeAsUpIndicator">
8+
android:background="?attr/appBarColor">
139

14-
<com.ichi2.ui.FixedTextView
15-
android:id="@+id/toolbar_title"
16-
android:layout_width="wrap_content"
17-
android:layout_height="match_parent"
18-
android:textColor="@color/white"
19-
android:textSize="20sp"
20-
android:visibility="gone" />
21-
22-
<LinearLayout
23-
android:id="@+id/toolbar_content"
10+
<androidx.appcompat.widget.Toolbar
11+
android:id="@+id/toolbar"
2412
android:layout_width="match_parent"
2513
android:layout_height="wrap_content"
26-
android:background="?attr/selectableItemBackground"
27-
android:orientation="vertical">
14+
android:minHeight="?attr/actionBarSize"
15+
android:theme="@style/ActionBarStyle"
16+
app:navigationContentDescription="@string/abc_action_bar_up_description"
17+
app:navigationIcon="?attr/homeAsUpIndicator">
2818

2919
<com.ichi2.ui.FixedTextView
30-
android:id="@+id/deck_name"
31-
android:layout_width="match_parent"
32-
android:layout_height="wrap_content"
33-
android:gravity="center_vertical"
34-
android:drawableEnd="@drawable/id_arrow_drop_down"
35-
android:textAppearance="?attr/textAppearanceListItem"
20+
android:id="@+id/toolbar_title"
21+
android:layout_width="wrap_content"
22+
android:layout_height="match_parent"
3623
android:textColor="@color/white"
37-
android:maxLines="1"
38-
android:ellipsize="end"
39-
tools:text="Deck name here"
40-
/>
24+
android:textSize="20sp"
25+
android:visibility="gone" />
4126

42-
<TextView
43-
android:id="@+id/subtitle"
27+
<LinearLayout
28+
android:id="@+id/toolbar_content"
4429
android:layout_width="match_parent"
4530
android:layout_height="wrap_content"
46-
android:textColor="@color/white"
47-
android:textAppearance="?attr/textAppearanceListItemSecondary"
48-
tools:text="2 cards shown"/>
49-
</LinearLayout>
50-
</androidx.appcompat.widget.Toolbar>
31+
android:background="?attr/selectableItemBackground"
32+
android:orientation="vertical">
33+
34+
<com.ichi2.ui.FixedTextView
35+
android:id="@+id/deck_name"
36+
android:layout_width="match_parent"
37+
android:layout_height="wrap_content"
38+
android:gravity="center_vertical"
39+
android:drawableEnd="@drawable/id_arrow_drop_down"
40+
android:textAppearance="?attr/textAppearanceListItem"
41+
android:textColor="@color/white"
42+
android:maxLines="1"
43+
android:ellipsize="end"
44+
tools:text="Deck name here"
45+
/>
46+
47+
<TextView
48+
android:id="@+id/subtitle"
49+
android:layout_width="match_parent"
50+
android:layout_height="wrap_content"
51+
android:textColor="@color/white"
52+
android:textAppearance="?attr/textAppearanceListItemSecondary"
53+
tools:text="2 cards shown"/>
54+
</LinearLayout>
55+
</androidx.appcompat.widget.Toolbar>
56+
</FrameLayout>
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
3+
*
4+
* This program is free software; you can redistribute it and/or modify it under
5+
* the terms of the GNU General Public License as published by the Free Software
6+
* Foundation; either version 3 of the License, or (at your option) any later
7+
* version.
8+
*
9+
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
10+
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
11+
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
12+
*
13+
* You should have received a copy of the GNU General Public License along with
14+
* this program. If not, see <http://www.gnu.org/licenses/>.
15+
*/
16+
package com.ichi2.anki
17+
18+
import android.content.Intent
19+
import androidx.core.graphics.Insets
20+
import androidx.core.view.ViewCompat
21+
import androidx.core.view.WindowInsetsCompat
22+
import androidx.test.core.app.ActivityScenario
23+
import androidx.test.ext.junit.runners.AndroidJUnit4
24+
import org.junit.Test
25+
import org.junit.runner.RunWith
26+
27+
/**
28+
* Screenshot tests for [CardBrowser]
29+
*
30+
* `./gradlew :AnkiDroid:verifyRoborazziPlayDebug -Pscreenshot --tests "com.ichi2.anki.CardBrowserScreenshotTest"`
31+
*/
32+
@RunWith(AndroidJUnit4::class)
33+
class CardBrowserScreenshotTest : ScreenshotTest() {
34+
init {
35+
setPhoneQualifiers()
36+
}
37+
38+
@Test
39+
fun cardBrowserWith30Notes() {
40+
ensureCollectionLoadIsSynchronous()
41+
repeat(30) { i -> addBasicNote((i + 1).toString(), "back") }
42+
ActivityScenario
43+
.launch<CardBrowser>(Intent(targetContext, CardBrowser::class.java))
44+
.use { scenario ->
45+
scenario.onActivity { activity ->
46+
// Robolectric reports zero system-bar insets by default, so the
47+
// edge-to-edge inset application has no visible effect. Dispatch
48+
// a synthetic 24dp status-bar inset to exercise the real path.
49+
val statusBarPx = (24 * activity.resources.displayMetrics.density).toInt()
50+
val insets =
51+
WindowInsetsCompat
52+
.Builder()
53+
.setInsets(WindowInsetsCompat.Type.statusBars(), Insets.of(0, statusBarPx, 0, 0))
54+
.build()
55+
ViewCompat.dispatchApplyWindowInsets(activity.window.decorView, insets)
56+
captureScreen("30_notes")
57+
}
58+
}
59+
}
60+
}

AnkiDroid/src/test/java/com/ichi2/anki/ScreenshotTest.kt

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@
1515
*/
1616
package com.ichi2.anki
1717

18+
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
19+
import com.github.takahirom.roborazzi.captureScreenRoboImage
1820
import org.junit.experimental.categories.Category
21+
import org.robolectric.RuntimeEnvironment
1922
import org.robolectric.annotation.GraphicsMode
2023

2124
interface ScreenshotTestCategory
@@ -25,4 +28,15 @@ interface ScreenshotTestCategory
2528
*/
2629
@Category(ScreenshotTestCategory::class)
2730
@GraphicsMode(GraphicsMode.Mode.NATIVE)
28-
abstract class ScreenshotTest : RobolectricTest()
31+
abstract class ScreenshotTest : RobolectricTest() {
32+
/** Pixel-class phone in portrait, light theme. */
33+
protected fun setPhoneQualifiers() {
34+
RuntimeEnvironment.setQualifiers("w411dp-h914dp-notnight-420dpi")
35+
}
36+
37+
/** Captures a screenshot to `build/outputs/roborazzi/<TestClass>/<name>.png`. */
38+
@OptIn(ExperimentalRoborazziApi::class)
39+
protected fun captureScreen(name: String) {
40+
captureScreenRoboImage(filePath = "build/outputs/roborazzi/${this.javaClass.simpleName}/$name.png")
41+
}
42+
}

AnkiDroid/src/test/java/com/ichi2/anki/ui/windows/reviewer/StudyScreenScreenshotTest.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
package com.ichi2.anki.ui.windows.reviewer
1717

1818
import androidx.test.core.app.ActivityScenario
19-
import com.github.takahirom.roborazzi.ExperimentalRoborazziApi
20-
import com.github.takahirom.roborazzi.captureScreenRoboImage
2119
import com.ichi2.anki.ScreenshotTest
2220
import com.ichi2.anki.previewer.CardViewerActivity
2321
import com.ichi2.anki.settings.Prefs
@@ -40,15 +38,14 @@ class StudyScreenScreenshotTest(
4038
RuntimeEnvironment.setQualifiers(config.qualifier.toString())
4139
}
4240

43-
@OptIn(ExperimentalRoborazziApi::class)
4441
@Test
4542
fun captureScreenshot() {
4643
ActivityScenario
4744
.launch<CardViewerActivity>(
4845
ReviewerFragment.getIntent(targetContext),
4946
).use { scenario ->
5047
scenario.onActivity {
51-
captureScreenRoboImage(filePath = "build/outputs/roborazzi/${this.javaClass.simpleName}/$config.png")
48+
captureScreen(config.toString())
5249
}
5350
}
5451
}

0 commit comments

Comments
 (0)