Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/DeckPicker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,19 @@ 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
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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)
}
116 changes: 116 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/ui/BottomFadeFrameLayout.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright (c) 2026 David Allison <davidallisongithub@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/

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,
)
Comment on lines +105 to +108
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have { Themes.isNightTheme } to handle a user selecting a theme manually

} 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))
}
}
}
23 changes: 23 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/themes/Themes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/card_browser_frame"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
android:layout_height="match_parent"
android:orientation="vertical">

<include layout="@layout/include_toolbar" />
<FrameLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/appBarColor">

<include layout="@layout/include_toolbar" />
</FrameLayout>

<LinearLayout
android:id="@+id/deckpicker_xl_view"
Expand Down
9 changes: 8 additions & 1 deletion AnkiDroid/src/main/res/layout/activity_homescreen.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@
android:layout_height="match_parent"
android:orientation="vertical">

<include layout="@layout/include_toolbar" />
<FrameLayout
android:id="@+id/toolbar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/appBarColor">

<include layout="@layout/include_toolbar" />
</FrameLayout>

<include
android:id="@+id/deck_picker_pane"
Expand Down
Loading
Loading