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
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ package com.ichi2.anki
import android.animation.Animator
import android.content.Context
import android.content.res.ColorStateList
import android.provider.Settings
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.View
Expand All @@ -27,6 +26,7 @@ import android.widget.TextView
import com.google.android.material.color.MaterialColors
import com.google.android.material.floatingactionbutton.FloatingActionButton
import com.ichi2.anki.ui.DoubleTapListener
import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled
import timber.log.Timber

class DeckPickerFloatingActionMenu(
Expand Down Expand Up @@ -79,7 +79,7 @@ class DeckPickerFloatingActionMenu(
/**
* If system animations are true changes the FAB color otherwise it remains the same
*/
if (areSystemAnimationsEnabled()) {
if (areSystemAnimationsEnabled(context)) {
fabMain.backgroundTintList = ColorStateList.valueOf(fabPressedColor)
} else {
// Changes the background color of FAB
Expand Down Expand Up @@ -327,35 +327,6 @@ class DeckPickerFloatingActionMenu(
}
}

/**
* This function returns false if any of the mentioned system animations are disabled (0f)
*
* ANIMATION_DURATION_SCALE - controls app switching animation speed.
* TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed
* WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed
*/
private fun areSystemAnimationsEnabled(): Boolean {
val animDuration: Float =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
val animTransition: Float =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.TRANSITION_ANIMATION_SCALE,
1f,
)
val animWindow: Float =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.WINDOW_ANIMATION_SCALE,
1f,
)
return animDuration != 0f && animTransition != 0f && animWindow != 0f
}

private fun createActivationKeyListener(
logMessage: String,
action: () -> Unit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ class ControlsSettingsFragment :
setupNewStudyScreenSettings()
}

/**
* Selects the appropriate tab based on a preference key from search results.
* This allows search navigation to automatically switch to the correct tab.
*/
fun selectTabForPreference(key: String) {
val targetTabIndex = actionToScreenMap[key]?.ordinal
if (targetTabIndex == null) {
Timber.w("Could not find the preference with %s key", key)
return
}

view?.post {
requirePreference<ControlsTabPreference>(
R.string.pref_controls_tab_layout_key,
).selectTab(targetTabIndex)
}
}

private fun setControlPreferencesDefaultValues(screen: ControlPreferenceScreen) {
val commands = screen.getActions().associateBy { it.preferenceKey }
val prefs = sharedPrefs()
Expand Down Expand Up @@ -157,6 +175,13 @@ class ControlsSettingsFragment :
}

companion object {
val actionToScreenMap: Map<String, ControlPreferenceScreen> by lazy {
ControlPreferenceScreen.entries
.flatMap { screen ->
screen.getActions().map { action -> action.preferenceKey to screen }
}.toMap()
}

val legacyStudyScreenSettings =
listOf(
R.string.save_voice_command_key,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ class ControlsTabPreference
tabLayout?.addOnTabSelectedListener(listener)
}

/**
* Selects a tab programmatically by position.
* @param tabPosition The position of the tab to select.
*/
fun selectTab(tabPosition: Int) {
tabLayout?.selectTab(tabLayout?.getTabAt(tabPosition))
}

override fun onBindViewHolder(holder: PreferenceViewHolder) {
super.onBindViewHolder(holder)
tabLayout = holder.itemView as? TabLayout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ class HeaderFragment : SettingsFragment() {
.addBreadcrumb(R.string.pref_cat_appearance)
}
index(R.xml.preferences_controls)
index(R.xml.preferences_reviewer_controls)
.addBreadcrumb(activity.getString(R.string.pref_cat_controls))
.addBreadcrumb(activity.getString(R.string.pref_controls_reviews_tab))
index(R.xml.preferences_previewer_controls)
.addBreadcrumb(activity.getString(R.string.pref_cat_controls))
.addBreadcrumb(activity.getString(R.string.pref_controls_previews_tab))
index(R.xml.preferences_accessibility)
index(R.xml.preferences_backup_limits)
ignorePreference(activity.getString(R.string.pref_backups_help_key))
Expand Down
24 changes: 22 additions & 2 deletions AnkiDroid/src/main/java/com/ichi2/anki/preferences/Preferences.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import androidx.fragment.app.FragmentFactory
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.bytehamster.lib.preferencesearch.SearchConfiguration
Expand All @@ -46,10 +47,13 @@ import com.ichi2.anki.common.annotations.LegacyNotifications
import com.ichi2.anki.preferences.HeaderFragment.Companion.getHeaderKeyForFragment
import com.ichi2.anki.reviewreminders.ReviewReminderScope
import com.ichi2.anki.reviewreminders.ScheduleReminders
import com.ichi2.anki.utils.AnimationUtils.areSystemAnimationsEnabled
import com.ichi2.anki.utils.ext.sharedPrefs
import com.ichi2.anki.utils.isWindowCompact
import com.ichi2.themes.Themes
import com.ichi2.utils.FragmentFactoryUtils
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import timber.log.Timber
import kotlin.reflect.KClass
import kotlin.reflect.jvm.jvmName
Expand Down Expand Up @@ -137,8 +141,22 @@ class PreferencesFragment :
addToBackStack(fragment.javaClass.name)
}

Timber.i("Highlighting key '%s' on %s", result.key, fragment)
result.highlight(fragment as PreferenceFragmentCompat)
if (fragment is ControlsSettingsFragment) {
fragment.lifecycleScope.launch {
if (areSystemAnimationsEnabled(requireContext())) {
Copy link
Member

Choose a reason for hiding this comment

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

Why does this only check system animations, ignoring the in-app 'safe display mode'

/**
* Whether animations should not be displayed
* This is used to improve the UX for e-ink devices
* Can be tested via Settings - Advanced - Safe display mode
*
* @see .animationEnabled
*/
fun animationDisabled(): Boolean {
val preferences = this.sharedPrefs()
return preferences.getBoolean("safeDisplay", false)
}

Copy link
Member Author

Choose a reason for hiding this comment

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

When I tested with system animations disabled, the tab selection and preference highlighting don't work in the 'Animations disabled - do everything immediately' case. I think we need delays regardless of the
areSystemAnimationsEnabled condition

Copy link
Member

@david-allison david-allison Nov 25, 2025

Choose a reason for hiding this comment

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

Sorry this sat, could you provide reproduction steps or a video

delay(100)
fragment.selectTabForPreference(result.key)
delay(150)
result.highlight(fragment as PreferenceFragmentCompat)
} else {
// Animations disabled - do everything immediately
fragment.selectTabForPreference(result.key)
result.highlight(fragment as PreferenceFragmentCompat)
}
}
} else {
result.highlight(fragment as PreferenceFragmentCompat)
}
}

private fun setupBackCallbacks() {
Expand Down Expand Up @@ -282,6 +300,8 @@ fun getFragmentFromXmlRes(
R.xml.preferences_notifications -> NotificationsSettingsFragment()
R.xml.preferences_appearance -> AppearanceSettingsFragment()
R.xml.preferences_controls -> ControlsSettingsFragment()
R.xml.preferences_reviewer_controls -> ControlsSettingsFragment()
R.xml.preferences_previewer_controls -> ControlsSettingsFragment()
R.xml.preferences_advanced -> AdvancedSettingsFragment()
R.xml.preferences_accessibility -> AccessibilitySettingsFragment()
R.xml.preferences_dev_options -> DevOptionsFragment()
Expand Down
62 changes: 62 additions & 0 deletions AnkiDroid/src/main/java/com/ichi2/anki/utils/AnimationUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Sanjay Sargam <[email protected]>
*
* 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.utils

import android.content.Context
import android.provider.Settings

/**
* Utility class for animation-related helper functions
*/
object AnimationUtils {
/**
* Checks if system animations are enabled by verifying all animation scale settings.
*
* This function returns false if any of the mentioned system animations are disabled (0f),
* which addresses safe display mode and accessibility concerns.
*
* ANIMATION_DURATION_SCALE - controls app switching animation speed.
Copy link
Member

Choose a reason for hiding this comment

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

From the docs, apparently checking only this one is necessary.

https://developer.android.com/reference/android/provider/Settings.Global#TRANSITION_ANIMATION_SCALE

Scaling factor for Animator-based animations. This affects both the start delay and duration of all such animations. The value is a float. Setting to 0.0f will cause animations to end immediately. The default value is 1.0f.

On my phone (Android 16, Samsung), those scales can be changed only in the phone developer options. The public user facing setting only allows reducing animations in general.

You don't need to take any action based on this comment, but it's something that I think it's worth noting so the method may be simplified or better documented later.

* TRANSITION_ANIMATION_SCALE - controls app window opening and closing animation speed
* WINDOW_ANIMATION_SCALE - controls pop-up window opening and closing animation speed
*
* @param context The context used to access system settings
* @return true if all animation scales are non-zero, false otherwise
*/
fun areSystemAnimationsEnabled(context: Context): Boolean =
try {
val animDuration =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.ANIMATOR_DURATION_SCALE,
1f,
)
val animTransition =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.TRANSITION_ANIMATION_SCALE,
1f,
)
val animWindow =
Settings.Global.getFloat(
context.contentResolver,
Settings.Global.WINDOW_ANIMATION_SCALE,
1f,
)
animDuration != 0f && animTransition != 0f && animWindow != 0f
} catch (e: Exception) {
true // Default to animations enabled if unable to read settings
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,18 @@ class PrefsSearchBarTest : RobolectricTest() {
val fragment = getFragmentFromXmlRes(resId)

assertNotNull(fragment)

// Special handling for ControlsSettingsFragment which handles multiple XML resources
val expectedResourceId =
when (fragment) {
is ControlsSettingsFragment -> fragment.preferenceResource
else -> resId
}

assertThat(
"${targetContext.resources.getResourceName(resId)} should match the preferenceResource of ${fragment::class.simpleName}",
"${targetContext.resources.getResourceName(resId)} should be handled by ${fragment::class.simpleName}",
fragment.preferenceResource,
equalTo(resId),
equalTo(expectedResourceId),
)
}
}
Expand Down