feat: Add accessibility options for tab justification and content (#1035)

Provide two new lab preferences for controlling the layout and content
of main navigation tabs.

Tabs can now be justfied to start, end, or fully (if room). Start/end
justification may put the tabs closer to the user's fingers, depending
on how they hold the device. Fully justified uses the full width of the
tab bar (if the tabs don't require scrolling).

The content can be set to one of:

- Icon only (previous behaviour)
- Text only
- Icon with text beside
- Icon with text below

Fixes #336
This commit is contained in:
Nik Clayton 2024-10-21 17:49:19 +02:00 committed by GitHub
parent 8fe2850229
commit 2234c4c782
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 245 additions and 4 deletions

View File

@ -112,6 +112,9 @@ import app.pachli.core.network.model.Account
import app.pachli.core.network.model.Notification
import app.pachli.core.preferences.MainNavigationPosition
import app.pachli.core.preferences.PrefKeys.FONT_FAMILY
import app.pachli.core.preferences.TabAlignment
import app.pachli.core.preferences.TabContents
import app.pachli.core.ui.AlignableTabLayoutAlignment
import app.pachli.core.ui.extensions.reduceSwipeSensitivity
import app.pachli.core.ui.makeIcon
import app.pachli.databinding.ActivityMainBinding
@ -951,6 +954,14 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
}
}
activeTabLayout.alignment = when (sharedPreferencesRepository.tabAlignment) {
TabAlignment.START -> AlignableTabLayoutAlignment.START
TabAlignment.JUSTIFY_IF_POSSIBLE -> AlignableTabLayoutAlignment.JUSTIFY_IF_POSSIBLE
TabAlignment.END -> AlignableTabLayoutAlignment.END
}
val tabContents = sharedPreferencesRepository.tabContents
activeTabLayout.isInlineLabel = tabContents == TabContents.ICON_TEXT_INLINE
// Save the previous tab so it can be restored later
val previousTabIndex = binding.viewPager.currentItem
val previousTab = tabAdapter.tabs.getOrNull(previousTabIndex)
@ -970,7 +981,12 @@ class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider {
binding.viewPager,
true,
) { tab: TabLayout.Tab, position: Int ->
tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon)
if (tabContents != TabContents.TEXT_ONLY) {
tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon)
}
if (tabContents != TabContents.ICON_ONLY) {
tab.text = tabs[position].title(this@MainActivity)
}
tab.contentDescription = tabs[position].title(this@MainActivity)
}.also { it.attach() }

View File

@ -25,6 +25,8 @@ import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreferenceCompat
import app.pachli.R
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.TabAlignment
import app.pachli.core.preferences.TabContents
import app.pachli.core.preferences.TabTapBehaviour
import app.pachli.databinding.FragmentLabPreferencesWarningBinding
import app.pachli.settings.enumListPreference
@ -65,6 +67,20 @@ class LabPreferencesFragment : PreferenceFragmentCompat() {
key = PrefKeys.TAB_TAP_BEHAVIOUR
isIconSpaceReserved = false
}
enumListPreference<TabAlignment> {
setDefaultValue(TabAlignment.START)
setTitle(app.pachli.core.preferences.R.string.pref_title_tab_alignment)
key = PrefKeys.TAB_ALIGNMENT
isIconSpaceReserved = false
}
enumListPreference<TabContents> {
setDefaultValue(TabContents.ICON_ONLY)
setTitle(app.pachli.core.preferences.R.string.pref_title_tab_contents)
key = PrefKeys.TAB_CONTENTS
isIconSpaceReserved = false
}
}
}

View File

@ -118,6 +118,7 @@ class PreferencesActivity :
PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH,
PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES,
PrefKeys.ENABLE_SWIPE_FOR_TABS, PrefKeys.MAIN_NAV_POSITION, PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE,
PrefKeys.TAB_ALIGNMENT, PrefKeys.TAB_CONTENTS,
-> {
restartActivitiesOnBackPressedCallback.isEnabled = true
}

View File

@ -31,13 +31,13 @@
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/topNav"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:contentInsetStart="0dp"
app:contentInsetStartWithNavigation="0dp"
app:navigationContentDescription="@string/action_open_drawer">
<com.google.android.material.tabs.TabLayout
<app.pachli.core.ui.AlignableTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -65,7 +65,7 @@
app:navigationContentDescription="@string/action_open_drawer"
app:fabAlignmentMode="end">
<com.google.android.material.tabs.TabLayout
<app.pachli.core.ui.AlignableTabLayout
android:id="@+id/bottomTabLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"

View File

@ -131,6 +131,12 @@ object PrefKeys {
*/
const val CONFIRM_STATUS_LANGUAGE = "confirmStatusLanguage"
/** Tab alignment. See [TabAlignment]. */
const val TAB_ALIGNMENT = "tabAlignment"
/** Tab contents. See [TabContents]. */
const val TAB_CONTENTS = "tabContents"
/** Keys that are no longer used (e.g., the preference has been removed */
object Deprecated {
const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications"

View File

@ -85,6 +85,15 @@ class SharedPreferencesRepository @Inject constructor(
val showSelfUsername: ShowSelfUsername
get() = getEnum(PrefKeys.SHOW_SELF_USERNAME, ShowSelfUsername.DISAMBIGUATE)
/** How to align tabs. */
val tabAlignment: TabAlignment
get() = getEnum(PrefKeys.TAB_ALIGNMENT, TabAlignment.START)
/** How to display tabs. */
val tabContents: TabContents
get() = getEnum(PrefKeys.TAB_CONTENTS, TabContents.ICON_ONLY)
/** Behaviour when tapping on a tab. */
val tabTapBehaviour: TabTapBehaviour
get() = getEnum(PrefKeys.TAB_TAP_BEHAVIOUR, TabTapBehaviour.JUMP_TO_NEXT_PAGE)

View File

@ -0,0 +1,40 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* 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.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.preferences
enum class TabAlignment(
override val displayResource: Int,
override val value: String? = null,
) : PreferenceEnum {
/**
* Tabs take required width, align with start of writing direction
* (i.e., left in LTR locales, right in RTL locales).
*/
START(R.string.pref_tab_alignment_start),
/**
* Tabs expand to fill available width, if space.
*/
JUSTIFY_IF_POSSIBLE(R.string.pref_tab_alignment_justify_if_possible),
/**
* Tabs take required width, align with end of writing direction
* (i.e., left in LTR locales, right in RTL locales).
*/
END(R.string.pref_tab_alignment_end),
}

View File

@ -0,0 +1,28 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* 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.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.preferences
enum class TabContents(
override val displayResource: Int,
override val value: String? = null,
) : PreferenceEnum {
ICON_ONLY(R.string.pref_tab_contents_icon_only),
TEXT_ONLY(R.string.pref_tab_contents_text_only),
ICON_TEXT_INLINE(R.string.pref_tab_contents_icon_text_inline),
ICON_TEXT_BELOW(R.string.pref_tab_contents_icon_text_below),
}

View File

@ -37,4 +37,15 @@
<string name="pref_show_self_username_always">Always</string>
<string name="pref_show_self_username_disambiguate">When multiple accounts logged in</string>
<string name="pref_show_self_username_never">Never</string>
<string name="pref_title_tab_alignment">Align main navigation tabs</string>
<string name="pref_tab_alignment_start">Start of text direction</string>
<string name="pref_tab_alignment_justify_if_possible">Expand to full width</string>
<string name="pref_tab_alignment_end">End of text direction</string>
<string name="pref_title_tab_contents">Content of main navigation tabs</string>
<string name="pref_tab_contents_icon_only">Icon only</string>
<string name="pref_tab_contents_text_only">Text only</string>
<string name="pref_tab_contents_icon_text_inline">Icon with text beside</string>
<string name="pref_tab_contents_icon_text_below">Icon with text below</string>
</resources>

View File

@ -0,0 +1,114 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* 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.
*
* Pachli 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 Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/
package app.pachli.core.ui
import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import androidx.core.text.TextUtilsCompat
import androidx.core.view.children
import app.pachli.core.ui.AlignableTabLayoutAlignment.END
import app.pachli.core.ui.AlignableTabLayoutAlignment.JUSTIFY_IF_POSSIBLE
import app.pachli.core.ui.AlignableTabLayoutAlignment.START
import com.google.android.material.tabs.TabLayout
import java.util.Locale
/** How to align tabs. */
enum class AlignableTabLayoutAlignment {
/** Tabs align with start of writing direction. */
START,
/** Tabs expand to full width if possible. */
JUSTIFY_IF_POSSIBLE,
/** Tabs align with end of writing direction. */
END,
}
/**
* Specalised [TabLayout] that can align the tabs.
*
* Ignores [setTabMode] in favour of [alignment].
*
* [START] is equivalent to setting tabMode to [TabLayout.MODE_SCROLLABLE].
*
* [JUSTIFY_IF_POSSIBLE] uses [TabLayout.MODE_SCROLLABLE] if there is not
* enough space to show all tabs, and [TabLayout.MODE_FIXED] if there is.
* Effectively justifying the tabs.
*
* [END] is equivalent to [START], but adds additional left or right
* padding (depending on the text direction) to push the start of the tabs
* to the correct end of the layout.
*/
class AlignableTabLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = com.google.android.material.R.attr.tabStyle,
) : TabLayout(context, attrs, defStyleAttr) {
var alignment: AlignableTabLayoutAlignment = START
set(value) {
field = value
invalidate()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when (alignment) {
START -> {
tabMode = MODE_SCROLLABLE
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
JUSTIFY_IF_POSSIBLE -> {
tabMode = MODE_SCROLLABLE
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
if (tabCount < 2) return
val tabLayout = getChildAt(0) as ViewGroup
val totalWidth = tabLayout.children.fold(0) { i, v -> i + v.measuredWidth }
if (totalWidth <= measuredWidth) {
tabMode = MODE_FIXED
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
END -> {
tabMode = MODE_SCROLLABLE
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val tabLayout = getChildAt(0) as ViewGroup
val totalWidth = tabLayout.children.fold(0) { i, v -> i + v.measuredWidth }
if (totalWidth < measuredWidth) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
val isLeftToRight = TextUtilsCompat.getLayoutDirectionFromLocale(Locale.getDefault()) == View.LAYOUT_DIRECTION_LTR
val padding = measuredWidth - totalWidth
if (isLeftToRight) {
tabLayout.setPadding(padding, 0, 0, 0)
} else {
tabLayout.setPadding(0, 0, padding, 0)
}
}
}
}
}
}