Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Oct 21, 2024
Merged
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
18 changes: 17 additions & 1 deletion app/src/main/java/app/pachli/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 3 additions & 3 deletions app/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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),
}
Original file line number Diff line number Diff line change
@@ -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),
}
11 changes: 11 additions & 0 deletions core/preferences/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
114 changes: 114 additions & 0 deletions core/ui/src/main/kotlin/app/pachli/core/ui/AlignableTabLayout.kt
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}
}
}