Skip to content

Commit 42c831f

Browse files
feat: Implement dynamic and accessible AZ sidebar
This commit introduces significant enhancements to the `AZSidebarView` to make it dynamic and more accessible. The sidebar now only displays letters corresponding to the available items in the app or contact lists. Key changes include: - **Dynamic Letter Filtering**: A new `setAvailableLetters` method in `AZSidebarView` allows it to dynamically hide letters that are not present in the current data set. - **Improved Sizing**: The sidebar's item spacing is now calculated within `onSizeChanged` using the view's actual height, ensuring it renders correctly regardless of screen size. - **Accessibility**: When a letter is selected, it is now announced for accessibility services. - **Code Refinements**: - Touch event handling has been streamlined into a `handleSelection` method to avoid duplicate code. - The `setSelectedLetter` function now correctly handles the "★" symbol. - Typeface lookups are now more efficient, occurring once per draw cycle. - **Integration**: The `AppDrawerFragment` has been updated to call `setAvailableLetters` whenever the app or contact list is populated or updated, keeping the sidebar in sync. closes #956
1 parent 28b9db5 commit 42c831f

File tree

2 files changed

+127
-36
lines changed

2 files changed

+127
-36
lines changed

app/src/main/java/com/github/codeworkscreativehub/mlauncher/ui/AppDrawerFragment.kt

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -268,11 +268,8 @@ class AppDrawerFragment : BaseFragment() {
268268
}
269269
}
270270

271-
272-
when (binding.menuView.displayedChild) {
273-
0 -> appAdapter?.let { appsAdapter = it }
274-
1 -> contactAdapter?.let { contactsAdapter = it }
275-
}
271+
appAdapter?.let { appsAdapter = it }
272+
contactAdapter?.let { contactsAdapter = it }
276273

277274
val searchTextView = binding.search.findViewById<TextView>(R.id.search_src_text)
278275

@@ -587,15 +584,18 @@ class AppDrawerFragment : BaseFragment() {
587584
when (menuView.displayedChild) {
588585
0 -> {
589586
setAppViewDetails()
587+
updateAZSidebarForApps(appsAdapter.appsList)
590588
}
591589

592590
1 -> {
593591
setContactViewDetails()
592+
updateAZSidebarForContacts(contactsAdapter.contactsList)
594593
}
595594
}
596595
}
597596
}
598597

598+
599599
private fun setAppViewDetails() {
600600
binding.apply {
601601
searchSwitcher.setImageResource(R.drawable.ic_contacts)
@@ -772,13 +772,19 @@ class AppDrawerFragment : BaseFragment() {
772772
AnimationUtils.loadLayoutAnimation(requireContext(), R.anim.layout_anim_from_bottom)
773773
binding.appsRecyclerView.layoutAnimation = animation
774774
appAdapter.setAppList(apps.toMutableList())
775+
776+
// ✅ ENABLE dynamic AZ letters
777+
updateAZSidebarForApps(apps)
775778
}
776779

777780
private fun populateContactList(contacts: List<ContactListItem>, contactAdapter: ContactDrawerAdapter) {
778781
val animation =
779782
AnimationUtils.loadLayoutAnimation(requireContext(), R.anim.layout_anim_from_bottom)
780783
binding.contactsRecyclerView.layoutAnimation = animation
781784
contactAdapter.setContactList(contacts.toMutableList())
785+
786+
// ✅ ENABLE dynamic AZ letters
787+
updateAZSidebarForContacts(contacts)
782788
}
783789

784790
private fun appClickListener(
@@ -863,4 +869,30 @@ class AppDrawerFragment : BaseFragment() {
863869
// Close the drawer or fragment after selection
864870
findNavController().popBackStack()
865871
}
872+
873+
private fun updateAZSidebarForApps(apps: List<AppListItem>) {
874+
val letters = mutableSetOf<String>()
875+
876+
apps.forEach { item ->
877+
when (item.category) {
878+
AppCategory.PINNED -> letters.add("")
879+
else -> {
880+
item.label.firstOrNull()
881+
?.uppercaseChar()
882+
?.toString()
883+
?.let { letters.add(it) }
884+
}
885+
}
886+
}
887+
888+
binding.azSidebar.setAvailableLetters(letters)
889+
}
890+
891+
private fun updateAZSidebarForContacts(contacts: List<ContactListItem>) {
892+
val letters = contacts.mapNotNull {
893+
it.displayName.firstOrNull()?.uppercaseChar()?.toString()
894+
}.toSet()
895+
896+
binding.azSidebar.setAvailableLetters(letters)
897+
}
866898
}

app/src/main/java/com/github/codeworkscreativehub/mlauncher/ui/components/AZSidebarView.kt

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import android.graphics.Typeface
99
import android.util.AttributeSet
1010
import android.view.MotionEvent
1111
import android.view.View
12+
import android.view.accessibility.AccessibilityEvent
1213
import com.github.codeworkscreativehub.mlauncher.helper.CustomFontView
1314
import com.github.codeworkscreativehub.mlauncher.helper.FontManager
1415
import com.github.codeworkscreativehub.mlauncher.helper.sp2px
@@ -20,103 +21,161 @@ class AZSidebarView @JvmOverloads constructor(
2021

2122
var onTouchStart: (() -> Unit)? = null
2223
var onTouchEnd: (() -> Unit)? = null
24+
var onLetterSelected: ((String) -> Unit)? = null
25+
26+
private val allLetters = listOf('') + ('A'..'Z')
27+
private var letters: List<Char> = allLetters
2328

24-
private val letters = listOf('') + ('A'..'Z')
2529
private val baseTextSizeSp = 20f
2630
private val selectedTextSizeSp = baseTextSizeSp + 2f
2731

2832
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
2933
color = Color.GRAY
30-
textSize = sp2px(resources, baseTextSizeSp)
3134
textAlign = Paint.Align.CENTER
3235
style = Paint.Style.FILL
36+
textSize = sp2px(resources, baseTextSizeSp)
3337
typeface = FontManager.getTypeface(context)
3438
}
3539

36-
var onLetterSelected: ((String) -> Unit)? = null
3740
private var spacingFactor = 1f
38-
private var selectedIndex: Int = -1
41+
private var selectedIndex = -1
42+
private var itemHeight = 0f
43+
44+
// Accessibility
45+
init {
46+
isFocusable = true
47+
importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES
48+
49+
// 🔗 Register for global font updates
50+
FontManager.register(this)
51+
}
3952

4053
val topBottomPaddingPx: Float
41-
get() = 180 * resources.displayMetrics.density
54+
get() = 180f * resources.displayMetrics.density
4255

43-
init {
44-
val displayMetrics = resources.displayMetrics
45-
val screenHeight = displayMetrics.heightPixels.toFloat()
46-
val density = displayMetrics.density
56+
// ✅ FIX: Calculate spacing using ACTUAL view height
57+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
58+
super.onSizeChanged(w, h, oldw, oldh)
4759

48-
val topBottomPadding = topBottomPaddingPx
60+
val density = resources.displayMetrics.density
4961
val interLetterSpacing = (letters.size - 1) * density
50-
val availableHeight = screenHeight - topBottomPadding - interLetterSpacing
5162
val baseTextHeight = sp2px(resources, baseTextSizeSp)
5263

64+
val availableHeight = h - topBottomPaddingPx - interLetterSpacing
5365
spacingFactor = availableHeight / (letters.size * baseTextHeight)
5466

55-
// 🔗 Register for global font updates
56-
FontManager.register(this)
67+
itemHeight = baseTextHeight * spacingFactor
5768
}
5869

5970
override fun onDraw(canvas: Canvas) {
6071
super.onDraw(canvas)
6172

62-
val itemHeight = sp2px(resources, baseTextSizeSp) * spacingFactor
73+
if (itemHeight <= 0f) return
74+
6375
val totalHeight = itemHeight * letters.size
6476
val startY = (height - totalHeight) / 2f
6577

66-
letters.forEachIndexed { i, letter ->
67-
val isSelected = i == selectedIndex
78+
// ✅ FIX: Typeface resolved ONCE per draw
79+
paint.typeface = FontManager.getTypeface(context)
80+
81+
letters.forEachIndexed { index, letter ->
82+
val isSelected = index == selectedIndex
6883

6984
paint.isFakeBoldText = isSelected
70-
paint.textSize = sp2px(resources, if (isSelected) selectedTextSizeSp else baseTextSizeSp)
85+
paint.textSize = sp2px(
86+
resources,
87+
if (isSelected) selectedTextSizeSp else baseTextSizeSp
88+
)
7189
paint.color = if (isSelected) Color.WHITE else Color.GRAY
72-
paint.typeface = FontManager.getTypeface(context) // Refresh font if needed
7390

7491
val x = width / 2f
75-
val y = startY + itemHeight * i - (paint.descent() + paint.ascent()) / 2
92+
val y = startY + itemHeight * index -
93+
(paint.descent() + paint.ascent()) / 2f
7694

7795
canvas.drawText(letter.toString(), x, y, paint)
7896
}
7997
}
8098

8199
@SuppressLint("ClickableViewAccessibility")
82100
override fun onTouchEvent(event: MotionEvent): Boolean {
83-
val itemHeight = sp2px(resources, baseTextSizeSp) * spacingFactor
101+
if (itemHeight <= 0f) return false
102+
84103
val totalHeight = itemHeight * letters.size
85104
val startY = (height - totalHeight) / 2f
86105

87106
val relativeY = event.y - startY
88-
val index = (relativeY / itemHeight).toInt().coerceIn(0, letters.size - 1)
107+
val index = (relativeY / itemHeight)
108+
.toInt()
109+
.coerceIn(0, letters.size - 1)
89110

90111
when (event.action) {
91-
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
92-
if (index != selectedIndex) {
93-
selectedIndex = index
94-
onLetterSelected?.invoke(letters[index].toString())
95-
invalidate()
96-
}
112+
MotionEvent.ACTION_DOWN -> {
97113
onTouchStart?.invoke()
114+
handleSelection(index)
98115
}
99116

100-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
117+
MotionEvent.ACTION_MOVE -> {
118+
handleSelection(index)
119+
}
120+
121+
MotionEvent.ACTION_UP,
122+
MotionEvent.ACTION_CANCEL -> {
101123
onTouchEnd?.invoke()
102124
}
103125
}
104126

105127
return true
106128
}
107129

130+
private fun handleSelection(index: Int) {
131+
if (index == selectedIndex) return
132+
133+
selectedIndex = index
134+
val letter = letters[index].toString()
135+
136+
onLetterSelected?.invoke(letter)
137+
138+
// ✅ FIX: Accessibility announcement
139+
announceLetterForAccessibility(letter)
140+
141+
invalidate()
142+
}
143+
144+
// ✅ FIX: Works for ★ and letters
108145
fun setSelectedLetter(letter: String) {
109-
val char = letter.firstOrNull()?.uppercaseChar() ?: return
110-
val index = letters.indexOf(char)
111-
if (index != -1 && selectedIndex != index) {
146+
val index = letters.indexOfFirst { it.toString() == letter }
147+
if (index != -1 && index != selectedIndex) {
112148
selectedIndex = index
113149
invalidate()
114150
}
115151
}
116152

153+
/**
154+
* Update sidebar letters based on available app sections.
155+
*
156+
* Example input: setOf("★", "A", "C", "D", "M")
157+
*/
158+
fun setAvailableLetters(available: Set<String>) {
159+
letters = allLetters.filter { it.toString() in available }
160+
161+
// Reset selection safely
162+
if (selectedIndex >= letters.size) {
163+
selectedIndex = -1
164+
}
165+
166+
requestLayout()
167+
invalidate()
168+
}
169+
117170
/** 🔁 Called by FontManager to update font */
118171
override fun applyFont(typeface: Typeface?) {
119172
paint.typeface = typeface
120173
invalidate()
121174
}
175+
176+
private fun announceLetterForAccessibility(text: String) {
177+
contentDescription = text
178+
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED)
179+
}
180+
122181
}

0 commit comments

Comments
 (0)