diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt index 823542920d72..75631b697bd4 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt @@ -30,6 +30,7 @@ import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.database.SQLException +import android.graphics.Color import android.graphics.PixelFormat import android.os.Bundle import android.text.util.Linkify @@ -37,9 +38,11 @@ import android.view.KeyEvent import android.view.Menu import android.view.MenuItem import android.view.View -import android.view.ViewGroup +import android.view.ViewGroup.MarginLayoutParams import android.widget.TextView import androidx.activity.OnBackPressedCallback +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.activity.result.ActivityResult import androidx.activity.result.ActivityResultCallback import androidx.activity.result.ActivityResultLauncher @@ -60,8 +63,13 @@ import androidx.core.util.component1 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.Type.displayCutout +import androidx.core.view.WindowInsetsCompat.Type.statusBars +import androidx.core.view.WindowInsetsCompat.Type.systemBars import androidx.core.view.doOnLayout import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.draganddrop.DropHelper import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.commit @@ -92,9 +100,11 @@ import com.ichi2.anki.analytics.UsageAnalytics import com.ichi2.anki.android.back.exitViaDoubleTapBackCallback import com.ichi2.anki.android.input.ShortcutGroup import com.ichi2.anki.android.input.shortcut +import com.ichi2.anki.android.view.locationInWindow import com.ichi2.anki.common.annotations.NeedsTest import com.ichi2.anki.common.crashreporting.CrashReportService import com.ichi2.anki.common.time.TimeManager +import com.ichi2.anki.common.utils.android.isRobolectric import com.ichi2.anki.common.utils.annotation.KotlinCleanup import com.ichi2.anki.compat.CompatHelper.Companion.getSerializableCompat import com.ichi2.anki.contextmenu.DeckPickerMenuContentProvider @@ -160,6 +170,7 @@ import com.ichi2.anki.snackbar.SnackbarBuilder import com.ichi2.anki.snackbar.showSnackbar import com.ichi2.anki.sync.MeteredSyncPolicy import com.ichi2.anki.sync.launchCatchingRequiringOneWaySyncDiscardUndo +import com.ichi2.anki.ui.BottomFadeFrameLayout import com.ichi2.anki.ui.ResizablePaneManager import com.ichi2.anki.ui.animations.fadeIn import com.ichi2.anki.ui.animations.fadeOut @@ -252,7 +263,9 @@ open class DeckPicker : @VisibleForTesting internal val deckPickerBinding: IncludeDeckPickerBinding get() = binding.deckPickerPane - private val floatingActionButtonBinding: IncludeFloatingAddButtonBinding + + @VisibleForTesting + val floatingActionButtonBinding: IncludeFloatingAddButtonBinding get() = deckPickerBinding.floatingActionButton override var fragmented: Boolean @@ -472,6 +485,11 @@ open class DeckPicker : return } + // match the status bar theme of the rest of the app + enableEdgeToEdge( + statusBarStyle = SystemBarStyle.dark(Color.TRANSPARENT), + navigationBarStyle = BottomFadeFrameLayout.navigationBarStyle(), + ) // Then set theme and content view super.onCreate(savedInstanceState) @@ -505,6 +523,7 @@ open class DeckPicker : // create inherited navigation drawer layout here so that it can be used by parent class initNavigationDrawer() + applyEdgeToEdgeInsets() title = resources.getString(R.string.app_name) deckPickerBinding.deckPickerContent.visibility = View.GONE @@ -607,6 +626,54 @@ open class DeckPicker : super.setupBackPressedCallbacks() } + override fun fitsSystemWindows(): Boolean = false + + private fun applyEdgeToEdgeInsets() { + fun setRecyclerViewBottomPaddingAbove(target: View) { + val recyclerView = deckPickerBinding.decks + if (recyclerView.height == 0 || target.height == 0) return + val bottom = recyclerView.locationInWindow().y + recyclerView.height + val topOfTarget = (target.layoutParams as? MarginLayoutParams)?.topMargin ?: 0 + recyclerView.updatePadding( + bottom = (bottom - target.locationInWindow().y - topOfTarget).coerceAtLeast(0), + ) + } + + deckPickerBinding.decksFadeWrapper.setup(window) + deckPickerBinding.decksFadeWrapper.anchorView = deckPickerBinding.reviewSummaryTextView + ViewCompat.setOnApplyWindowInsetsListener(binding.toolbarContainer) { toolbar, insets -> + val bars = insets.getInsets(statusBars() or displayCutout()) + toolbar.updatePadding(left = bars.left, top = bars.top, right = bars.right) + insets + } + // Bottom padding is used. wrap_content meant margin wasn't viable + ViewCompat.setOnApplyWindowInsetsListener(deckPickerBinding.root) { deckPickerInclude, insets -> + val bars = insets.getInsets(systemBars() or displayCutout()) + deckPickerInclude.updatePadding( + left = bars.left, + right = if (fragmented) 0 else bars.right, + ) + deckPickerBinding.reviewSummaryTextView.updatePadding(bottom = bars.bottom) + + val fabBottomOffset = if (isRobolectric) 12.dp.toPx(this) else -12.dp.toPx(this) + floatingActionButtonBinding.root.updatePadding(bottom = bars.bottom + fabBottomOffset) + + setRecyclerViewBottomPaddingAbove(floatingActionButtonBinding.fabMain) + insets + } + floatingActionButtonBinding.fabMain.addOnLayoutChangeListener { v, _, _, _, _, _, _, _, _ -> + setRecyclerViewBottomPaddingAbove(v) + } + if (fragmented) { + val studyoptionsView = binding.studyoptionsFragment ?: return + ViewCompat.setOnApplyWindowInsetsListener(studyoptionsView) { studyOptions, insets -> + val bars = insets.getInsets(systemBars() or displayCutout()) + studyOptions.updatePadding(right = bars.right, bottom = bars.bottom) + insets + } + } + } + @Suppress("UNUSED_PARAMETER") private fun setupFlows() { fun onDeckDeleted(result: DeckDeletionResult) { @@ -656,7 +723,7 @@ open class DeckPicker : deckPickerBinding.reviewSummaryTextView.text = studiedToday // Adjust bottom margin of fabLinearLayout based on reviewSummaryTextView height deckPickerBinding.reviewSummaryTextView.doOnLayout { view -> - val layoutParams = floatingActionButtonBinding.fabLinearLayout.layoutParams as ViewGroup.MarginLayoutParams + val layoutParams = floatingActionButtonBinding.fabLinearLayout.layoutParams as MarginLayoutParams layoutParams.setMargins(0, 0, 0, view.height / 2) floatingActionButtonBinding.fabLinearLayout.layoutParams = layoutParams } diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/android/view/ViewLocation.kt b/AnkiDroid/src/main/java/com/ichi2/anki/android/view/ViewLocation.kt new file mode 100644 index 000000000000..3522b514b808 --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/android/view/ViewLocation.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki.android.view + +import android.view.View + +/** Wrapper around the `IntArray` returned by [View.getLocationOnScreen]. */ +@JvmInline +value class ViewLocation( + val data: IntArray, +) { + val x: Int get() = data[0] + val y: Int get() = data[1] +} + +/** The receiver's position on the device screen. */ +fun View.locationOnScreen(scratch: IntArray = IntArray(2)): ViewLocation { + getLocationOnScreen(scratch) + return ViewLocation(scratch) +} + +/** The receiver's position relative to its window. */ +fun View.locationInWindow(scratch: IntArray = IntArray(2)): ViewLocation { + getLocationInWindow(scratch) + return ViewLocation(scratch) +} diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/ui/BottomFadeFrameLayout.kt b/AnkiDroid/src/main/java/com/ichi2/anki/ui/BottomFadeFrameLayout.kt new file mode 100644 index 000000000000..5c295a810f4f --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/anki/ui/BottomFadeFrameLayout.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.anki.ui + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.os.Build +import android.util.AttributeSet +import android.view.View +import android.view.Window +import android.widget.FrameLayout +import androidx.activity.SystemBarStyle +import com.ichi2.anki.android.view.locationOnScreen +import com.ichi2.anki.compat.setDstOutBlendCompat + +/** + * A container which emulates the 'fade' effect which is applied to content when underneath the + * button navigation. + * + * Extends the button nav bar's visual effect to our components. + * + * @see anchorView + * + * BUG: This applies transparency twice on API <= 25 - not a huge deal + */ +class BottomFadeFrameLayout + @JvmOverloads + constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + ) : FrameLayout(context, attrs, defStyleAttr) { + /** buffer used by [View.screenY] */ + private val locBuf = IntArray(2) + + /** The fade applies to this view, and all views below it vertically */ + var anchorView: View? = null + set(value) { + if (field !== value) { + field = value + invalidate() + } + } + + private val paint = + Paint().apply { + color = FADE_ALPHA shl 24 + setDstOutBlendCompat() + } + + override fun dispatchDraw(canvas: Canvas) { + val target = anchorView + if (target == null || target.height == 0 || width == 0 || height == 0) { + super.dispatchDraw(canvas) + return + } + val targetTopInSelf = target.screenY - screenY + val bandTop = (targetTopInSelf + target.paddingTop).toFloat() + val bandBottom = height.toFloat() + if (bandBottom <= bandTop) { + super.dispatchDraw(canvas) + return + } + // Draw children into a scratch buffer so the fade only affects them + val saveCount = canvas.saveLayer(0f, 0f, width.toFloat(), height.toFloat(), null) + super.dispatchDraw(canvas) + canvas.drawRect(0f, bandTop, width.toFloat(), bandBottom, paint) + canvas.restoreToCount(saveCount) + } + + private inline val View.screenY: Int + get() = locationOnScreen(locBuf).y + + /** Configures [window] for use with this view */ + fun setup(window: Window) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + } + + companion object { + /** Alpha removed from each child pixel inside the fade. */ + // 90% - standard Android, so "Studied in" isn't obscured when overlapping a deck name + const val FADE_ALPHA: Int = 0xE6 + + /** `navigationBarStyle` to pass to [androidx.activity.enableEdgeToEdge] */ + fun navigationBarStyle(): SystemBarStyle = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + SystemBarStyle.auto( + lightScrim = Color.TRANSPARENT, + darkScrim = Color.TRANSPARENT, + ) + } else { + // Dark nav bar icons are not supported by the platform + // Maintain a dark nav, rather than just using fade + // androidx.activity.EdgeToEdge.DefaultDarkScrim + SystemBarStyle.dark(Color.argb(0x80, 0x1b, 0x1b, 0x1b)) + } + } + } diff --git a/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt b/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt index e326a9bafaba..1662053ce029 100644 --- a/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt +++ b/AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt @@ -18,11 +18,14 @@ package com.ichi2.themes +import android.app.Activity import android.content.Context import android.content.res.Configuration import android.graphics.Color +import android.util.TypedValue import androidx.annotation.ColorInt import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.withStyledAttributes import androidx.core.graphics.drawable.toDrawable import androidx.core.view.WindowInsetsControllerCompat @@ -34,6 +37,7 @@ import com.ichi2.anki.settings.enums.AppTheme import com.ichi2.anki.settings.enums.DayTheme import com.ichi2.anki.settings.enums.NightTheme import com.ichi2.anki.settings.enums.Theme +import com.ichi2.themes.Themes.currentTheme /** * Helper methods to configure things related to AnkiDroid's themes @@ -50,6 +54,25 @@ object Themes { context.setTheme(currentTheme.styleResId) } + fun setTheme(activity: Activity) { + val tv = TypedValue() + activity.theme.resolveAttribute(android.R.attr.windowBackground, tv, true) + val hadLauncherSplash = tv.resourceId == R.drawable.launch_screen + + setTheme(activity as Context) + + if (hadLauncherSplash) { + activity.theme.resolveAttribute(android.R.attr.windowBackground, tv, true) + val replacement = + if (tv.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) { + tv.data.toDrawable() + } else { + AppCompatResources.getDrawable(activity, tv.resourceId) + } + activity.window.setBackgroundDrawable(replacement) + } + } + fun setLegacyActionBar(context: Context) { context.setTheme(R.style.ThemeOverlay_LegacyActionBar) } diff --git a/AnkiDroid/src/main/res/layout-sw600dp/activity_card_browser.xml b/AnkiDroid/src/main/res/layout-sw600dp/activity_card_browser.xml index d461daaa92ce..4a4525f0f83e 100644 --- a/AnkiDroid/src/main/res/layout-sw600dp/activity_card_browser.xml +++ b/AnkiDroid/src/main/res/layout-sw600dp/activity_card_browser.xml @@ -11,7 +11,6 @@ android:id="@+id/card_browser_xl_view" android:layout_width="match_parent" android:layout_height="match_parent" - android:background="?android:attr/colorBackground" android:orientation="horizontal"> - + + + + - + + + + - + android:layout_height="match_parent"> + + + diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerScreenshotTest.kt new file mode 100644 index 000000000000..aea51157104e --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerScreenshotTest.kt @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki + +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.content.edit +import androidx.core.graphics.Insets +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.anki.preferences.sharedPrefs +import com.ichi2.testutils.BackupManagerTestUtilities +import com.ichi2.utils.dp +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Screenshot tests for [DeckPicker] + * + * `./gradlew :AnkiDroid:verifyRoborazziPlayDebug -Pscreenshot --tests "com.ichi2.anki.DeckPickerScreenshotTest"` + */ +@RunWith(AndroidJUnit4::class) +class DeckPickerScreenshotTest : ScreenshotTest() { + init { + setPhoneQualifiers() + } + + @Before + override fun setUp() { + super.setUp() + } + + @After + fun tearDownBackup() { + BackupManagerTestUtilities.reset() + } + + @Test + fun deckPickerWith30Decks() = + withDeckPicker(deckCount = 30) { deckPicker -> + deckPicker.simulateEdgeToEdge() + captureScreen("30_decks") + } +} + +/** + * Inject realistic system bars for edge to edge + * + * WARN: Does not match reality. There are issues with FAB placement and scrolling the list + */ +fun DeckPicker.simulateEdgeToEdge() { + val insets = + WindowInsetsCompat + .Builder() + .setInsets(WindowInsetsCompat.Type.statusBars(), Insets.of(0, 24.dp.toPx(this), 0, 0)) + .setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.of(0, 0, 0, 48.dp.toPx(this))) + .build() + ViewCompat.dispatchApplyWindowInsets(window.decorView, insets) + + val decor = window.decorView as ViewGroup + val navBarOverlay = + View(this).apply { + setBackgroundColor(0x80000000.toInt()) + } + decor.addView( + navBarOverlay, + FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 48.dp.toPx(this), Gravity.BOTTOM), + ) +} + +context(test: RobolectricTest) +fun withDeckPicker( + deckCount: Int, + withCards: Boolean = false, + block: (DeckPicker) -> Unit, +) { + // startup code occurs here so all users of this method are correctly setup + test.ensureCollectionLoadIsSynchronous() + test.setIntroductionSlidesShown(true) + BackupManagerTestUtilities.setupSpaceForBackup(test.targetContext) + // suppress the periodic 'backup your collection' prompt so the screenshot is just the deck list + test.targetContext.sharedPrefs().edit { putBoolean("backupPromptDisabled", true) } + if (withCards) test.ensureNonEmptyCollection() + for (i in 0 until deckCount) { + // 'Deck' is before 'Default' alphabetically + test.addDeck("Test Deck $i") + } + block(test.startRegularActivity()) +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTabletScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTabletScreenshotTest.kt new file mode 100644 index 000000000000..13468c839ec8 --- /dev/null +++ b/AnkiDroid/src/test/java/com/ichi2/anki/DeckPickerTabletScreenshotTest.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2026 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +package com.ichi2.anki + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.ichi2.testutils.BackupManagerTestUtilities +import org.junit.After +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Screenshot tests for [DeckPicker] in fragmented mode. + * + * `./gradlew :AnkiDroid:verifyRoborazziPlayDebug -Pscreenshot --tests "com.ichi2.anki.DeckPickerTabletScreenshotTest"` + */ +@RunWith(AndroidJUnit4::class) +class DeckPickerTabletScreenshotTest : ScreenshotTest() { + init { + setTabletQualifiers() + } + + @After + fun tearDownBackup() { + BackupManagerTestUtilities.reset() + } + + @Test + fun deckPickerWith30Decks() = + withDeckPicker(deckCount = 30, withCards = true) { deckPicker -> + deckPicker.simulateEdgeToEdge() + // Allow tryShowStudyOptionsPanel()'s async fragment commit to finalize + // before capturing, otherwise the right pane is empty. + advanceRobolectricLooper() + advanceRobolectricLooper() + captureScreen("30_decks_tablet") + } +} diff --git a/AnkiDroid/src/test/java/com/ichi2/anki/ScreenshotTest.kt b/AnkiDroid/src/test/java/com/ichi2/anki/ScreenshotTest.kt index f9879e791edf..cb86bdfcf82d 100644 --- a/AnkiDroid/src/test/java/com/ichi2/anki/ScreenshotTest.kt +++ b/AnkiDroid/src/test/java/com/ichi2/anki/ScreenshotTest.kt @@ -20,6 +20,7 @@ import com.github.takahirom.roborazzi.RoborazziOptions import com.github.takahirom.roborazzi.captureScreenRoboImage import com.github.takahirom.roborazzi.provideRoborazziContext import org.junit.experimental.categories.Category +import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.GraphicsMode import java.io.File @@ -31,6 +32,16 @@ interface ScreenshotTestCategory @Category(ScreenshotTestCategory::class) @GraphicsMode(GraphicsMode.Mode.NATIVE) abstract class ScreenshotTest : RobolectricTest() { + /** Pixel-class phone in portrait, light theme. */ + protected fun setPhoneQualifiers() { + RuntimeEnvironment.setQualifiers("w411dp-h914dp-notnight-420dpi") + } + + /** Required for [DeckPicker.fragmented] to be true. */ + protected fun setTabletQualifiers() { + RuntimeEnvironment.setQualifiers("w1280dp-h800dp-xlarge-land-notnight-mdpi") + } + /** * Captures a screenshot to `build/outputs/roborazzi//.png`. * diff --git a/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt b/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt index 8262169b7b34..cf00c5842080 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt @@ -22,6 +22,9 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.graphics.Bitmap +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode import android.media.MediaRecorder import android.media.ThumbnailUtils import android.net.Uri @@ -292,6 +295,10 @@ open class BaseCompat : Compat { context: Context, defaultValue: Boolean, ): Boolean = false + + override fun setDstOutBlend(paint: Paint) { + paint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + } } typealias CompatV24 = BaseCompat diff --git a/compat/src/main/java/com/ichi2/anki/compat/Compat.kt b/compat/src/main/java/com/ichi2/anki/compat/Compat.kt index 6923c71632b2..c027b24511c5 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/Compat.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/Compat.kt @@ -25,6 +25,8 @@ import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.ResolveInfo import android.graphics.Bitmap import android.graphics.Bitmap.CompressFormat +import android.graphics.BlendMode +import android.graphics.Paint import android.media.MediaRecorder import android.net.Uri import android.os.Bundle @@ -288,4 +290,7 @@ interface Compat { context: Context, defaultValue: Boolean = false, ): Boolean + + /** Configures [paint] to use [BlendMode.DST_OUT]. */ + fun setDstOutBlend(paint: Paint) } diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt index 6ae88076b6ea..a4132d8937ea 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt @@ -24,6 +24,8 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.ResolveInfo +import android.graphics.BlendMode +import android.graphics.Paint import android.os.Build import android.os.Bundle import android.view.KeyCharacterMap.deviceHasKey @@ -222,3 +224,6 @@ inline fun Bundle.requireSerializableCompat(key: Stri requireNotNull(compat.getSerializable(this, key, T::class.java)) { "key: '$key' not found or null" } + +/** Configures this [Paint] to use [BlendMode.DST_OUT]. */ +fun Paint.setDstOutBlendCompat() = compat.setDstOutBlend(this) diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatV29.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatV29.kt index b678e8b0c85f..96922c22ac16 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/CompatV29.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatV29.kt @@ -18,6 +18,8 @@ package com.ichi2.anki.compat import android.content.ContentValues import android.content.Context import android.graphics.Bitmap +import android.graphics.BlendMode +import android.graphics.Paint import android.media.ThumbnailUtils import android.net.Uri import android.os.Environment @@ -93,6 +95,10 @@ open class CompatV29 : CompatV26() { return Settings.Secure.getInt(context.contentResolver, "navigation_mode", defaultMode) == 2 } + override fun setDstOutBlend(paint: Paint) { + paint.blendMode = BlendMode.DST_OUT + } + companion object { // obtained from AOSP source private val THUMBNAIL_MINI_KIND = Size(512, 384)