mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
Talkback improvements for New Habit and New Daily screens (#1317)
* Include state in content description of positive/negative habit buttons - Now instead of announcing just 'Positive' or 'Negative' when these buttons are focused with Talkback on, it also announces their current state (e.g. 'Positive On'). - Also, when the state is changed, it now sends an accessibility event so the new state is announced immediately (as advised at https://developer.android.com/guide/topics/ui/accessibility/custom-views#send-events). * Make headings in Task Form be read out as headings by Talkback * Include state in content description of task difficulty buttons - Now instead of announcing just 'Trivial', 'Medium' etc when these buttons are focused with Talkback on, it also announces their current state (e.g. 'Medium, Selected' / 'Trivial, Not Selected'). - Also, when a difficulty button is clicked, it now sends an accessibility event for the button corresponding to that difficulty level, so the new state is announced immediately. - Note that since buttons are recreated every time one is clicked, we can't simply send the event from the button's OnClickListener. Instead we have to record which button is selected and send the event for that button once it has been recreated. * Include state in content description of reset streak buttons - Now instead of announcing just 'Daily', 'Weekly' etc when these buttons are focused with Talkback on, it also announces their current state (e.g. 'Daily, Selected' / 'Weekly, Not Selected'). - Also, when a reset streak button is clicked, it now sends an accessibility event with the new content description, so the new state is announced immediately. - Note that since buttons are recreated every time one is clicked, we can't simply send the event from the button's OnClickListener. Instead we have to record which button is selected and send the event for that button once it has been recreated. * Add labelFor attributes for repeats_every_spinner/edittext - This adds more context for the drop-down menu and the edit-text content descriptions read out by Talkback when editing how often dailies repeat. This is advised by the accessibility principle described at https://developer.android.com/guide/topics/ui/accessibility/principles#content-pairs - For example instead of reading out 'Drop-down menu, Weekly' it now reads out 'Drop-down menu, Weekly, for Repeats'. And instead of reading out '1, Edit box' it now reads out '1, Edit box for Weeks'. * Improve content descriptions of weekday buttons on New Daily screen - Now instead of 'Capital M', 'Capital T', etc. Talkback reads out 'Monday, Selected', 'Tuesday, Not selected', etc. - It also reads out the new status whenever a day is selected or deselected. * Mini refactor: reduce duplication in configureMonthlyRepeatViews * Improve content description of weekly repeats buttons - Now the content descriptions of the weekly repeats buttons in the New Daily screen include whether they are Selected / Not selected. - Also an accessibility event is sent when they are toggled, so the content description is read out again with the updated status. * Improve content descriptions of ReminderItemFormView - Previously, the '+' button was focusable, even though it didn't do anything, and Talkback read out 'Unlabelled button', regardless of whether it was a '+' button or a 'x' button. - Now, we only make it focusable when it becomes a delete button, and Talkback reads out 'Delete Reminder, Button for 6.16pm' (or whatever the time of the reminder it relates to is - this is achieved by setting the android:labelFor property on the reminder TextView). * Improve content descriptions of ChecklistItemFormView - Previously, the '+' button was focusable, even though it didn't do anything, and Talkback read out 'Unlabelled button', regardless of whether it was a '+' button or a 'x' button. - Now, we only make it focusable when it becomes a delete button, and Talkback reads out 'Delete Checklist Entry, Button for Checklist Text' (or whatever the checklist text is - this is achieved by setting the android:labelFor property on the checklist EditText).
This commit is contained in:
parent
348b7fd19b
commit
89f40defb8
9 changed files with 112 additions and 20 deletions
|
|
@ -60,7 +60,8 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:text="@string/repeats"
|
||||
android:textColor="@color/gray_300"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="12sp"
|
||||
android:labelFor="@+id/repeats_every_spinner"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/repeats_every_spinner"
|
||||
|
|
@ -108,8 +109,9 @@
|
|||
android:id="@+id/repeats_every_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="@string/days"
|
||||
android:textColor="@color/gray_300"/>
|
||||
android:text="@string/days"
|
||||
android:textColor="@color/gray_300"
|
||||
android:labelFor="@+id/repeats_every_edittext"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -86,6 +86,10 @@
|
|||
<string name="start_date">Start Date</string>
|
||||
<string name="positive_habit_form">Positive</string>
|
||||
<string name="negative_habit_form">Negative</string>
|
||||
<string name="on">On</string>
|
||||
<string name="off">Off</string>
|
||||
<string name="selected">Selected</string>
|
||||
<string name="not_selected">Not selected</string>
|
||||
<string name="checklist">Checklist</string>
|
||||
<string name="reminders">Reminders</string>
|
||||
|
||||
|
|
@ -821,6 +825,7 @@
|
|||
<string name="invite_to_party">Invite to Party</string>
|
||||
<string name="are_you_sure">Are you sure?</string>
|
||||
<string name="delete_task">Delete Task</string>
|
||||
<string name="delete_reminder">Delete Reminder</string>
|
||||
<string name="not_now">Not right now</string>
|
||||
<string name="levelup_detail_10">Choose a class that fits your playstyle and unlock special skills and armor to help you on your journey</string>
|
||||
<string name="levelup_title_10">Class System unlocked!</string>
|
||||
|
|
@ -1009,4 +1014,5 @@
|
|||
<string name="read_more">Read More</string>
|
||||
<string name="purchase_amount_error">You are unable to buy that amount.</string>
|
||||
<string name="still_questions">Still have a question?</string>
|
||||
<string name="delete_checklist_entry">Delete Checklist entry</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@
|
|||
<item name="android:textColor">@color/gray_10</item>
|
||||
<item name="android:layout_marginTop">@dimen/spacing_large</item>
|
||||
<item name="android:layout_marginBottom">@dimen/spacing_medium</item>
|
||||
<item name="android:accessibilityHeading">true</item>
|
||||
</style>
|
||||
|
||||
<style name="TaskFormToggle">
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ class ChecklistItemFormView @JvmOverloads constructor(
|
|||
var animDuration = 0L
|
||||
var isAddButton: Boolean = true
|
||||
set(value) {
|
||||
// Button is only clickable when it is *not* an add button (ie when it is a delete button),
|
||||
// so make screenreaders skip it when it is an add button.
|
||||
button.importantForAccessibility =
|
||||
if (value) View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
else View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||
if (field == value) {
|
||||
return
|
||||
}
|
||||
|
|
@ -83,11 +88,15 @@ class ChecklistItemFormView @JvmOverloads constructor(
|
|||
(parent as? ViewGroup)?.removeView(this)
|
||||
}
|
||||
}
|
||||
// It's ok to make the description always be 'Delete ..' because when this button is
|
||||
// a plus button we set it as 'unimportant for accessibility' so it can't be focused.
|
||||
button.contentDescription = context.getString(R.string.delete_checklist_entry)
|
||||
button.drawable.mutate().setTint(tintColor)
|
||||
|
||||
editText.addTextChangedListener(OnChangeTextWatcher { s, _, _, _ ->
|
||||
item.text = s.toString()
|
||||
textChangedListener?.let { it(s.toString()) }
|
||||
})
|
||||
editText.labelFor = button.id
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -22,8 +23,12 @@ class HabitResetStreakButtons @JvmOverloads constructor(
|
|||
field = value
|
||||
removeAllViews()
|
||||
addAllButtons()
|
||||
selectedButton.sendAccessibilityEvent(
|
||||
AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
|
||||
}
|
||||
|
||||
private lateinit var selectedButton: TextView
|
||||
|
||||
init {
|
||||
addAllButtons()
|
||||
}
|
||||
|
|
@ -43,14 +48,22 @@ class HabitResetStreakButtons @JvmOverloads constructor(
|
|||
button.gravity = Gravity.CENTER
|
||||
button.layoutParams = layoutParams
|
||||
addView(button)
|
||||
if (resetOption == selectedResetOption) {
|
||||
selectedButton = button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createButton(resetOption: HabitResetOption): TextView {
|
||||
val isActive = selectedResetOption == resetOption
|
||||
|
||||
val button = TextView(context)
|
||||
button.text = context.getString(resetOption.nameRes)
|
||||
val buttonText = context.getString(resetOption.nameRes)
|
||||
button.text = buttonText
|
||||
button.contentDescription = toContentDescription(buttonText, isActive)
|
||||
button.background = ContextCompat.getDrawable(context, R.drawable.layout_rounded_bg_white)
|
||||
if (selectedResetOption == resetOption) {
|
||||
|
||||
if (isActive) {
|
||||
button.background.setTint(tintColor)
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
} else {
|
||||
|
|
@ -62,4 +75,11 @@ class HabitResetStreakButtons @JvmOverloads constructor(
|
|||
}
|
||||
return button
|
||||
}
|
||||
|
||||
private fun toContentDescription(buttonText: CharSequence, isActive: Boolean): String {
|
||||
val statusString = if (isActive) {
|
||||
context.getString(R.string.selected)
|
||||
} else context.getString(R.string.not_selected)
|
||||
return "$buttonText, $statusString"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import android.util.AttributeSet
|
|||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
|
|
@ -18,7 +19,7 @@ class HabitScoringButtonsView @JvmOverloads constructor(
|
|||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : LinearLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private val positiveVew: ViewGroup by bindView(R.id.positive_view)
|
||||
private val positiveView: ViewGroup by bindView(R.id.positive_view)
|
||||
private val negativeView: ViewGroup by bindView(R.id.negative_view)
|
||||
private val positiveImageView: ImageView by bindView(R.id.positive_image_view)
|
||||
private val negativeImageView: ImageView by bindView(R.id.negative_image_view)
|
||||
|
|
@ -34,8 +35,10 @@ class HabitScoringButtonsView @JvmOverloads constructor(
|
|||
positiveImageView.setImageDrawable(HabiticaIconsHelper.imageOfHabitControlPlus(tintColor, value).asDrawable(resources))
|
||||
if (value) {
|
||||
positiveTextView.setTextColor(tintColor)
|
||||
positiveView.contentDescription = toContentDescription(R.string.positive_habit_form, R.string.on)
|
||||
} else {
|
||||
positiveTextView.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
|
||||
positiveView.contentDescription = toContentDescription(R.string.positive_habit_form, R.string.off)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -45,20 +48,28 @@ class HabitScoringButtonsView @JvmOverloads constructor(
|
|||
negativeImageView.setImageDrawable(HabiticaIconsHelper.imageOfHabitControlMinus(tintColor, value).asDrawable(resources))
|
||||
if (value) {
|
||||
negativeTextView.setTextColor(tintColor)
|
||||
negativeView.contentDescription = toContentDescription(R.string.negative_habit_form, R.string.on)
|
||||
} else {
|
||||
negativeTextView.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
|
||||
negativeView.contentDescription = toContentDescription(R.string.negative_habit_form, R.string.off)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toContentDescription(descriptionStringId: Int, statusStringId: Int): String {
|
||||
return context.getString(descriptionStringId) + ", " + context.getString(statusStringId)
|
||||
}
|
||||
|
||||
init {
|
||||
View.inflate(context, R.layout.task_form_habit_scoring, this)
|
||||
gravity = Gravity.CENTER
|
||||
|
||||
positiveVew.setOnClickListener {
|
||||
positiveView.setOnClickListener {
|
||||
isPositive = !isPositive
|
||||
sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
|
||||
}
|
||||
negativeView.setOnClickListener {
|
||||
isNegative = !isNegative
|
||||
sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
|
||||
}
|
||||
|
||||
isPositive = true
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import android.app.TimePickerDialog
|
|||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.LinearInterpolator
|
||||
|
|
@ -59,12 +60,16 @@ class ReminderItemFormView @JvmOverloads constructor(
|
|||
rotate.interpolator = LinearInterpolator()
|
||||
rotate.fillAfter = true
|
||||
button.startAnimation(rotate)
|
||||
// This button is not clickable in this state, so make screen readers skip it.
|
||||
button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
} else {
|
||||
val rotate = RotateAnimation(0f, 135f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
|
||||
rotate.duration = animDuration
|
||||
rotate.interpolator = LinearInterpolator()
|
||||
rotate.fillAfter = true
|
||||
button.startAnimation(rotate)
|
||||
// This button IS now clickable, so allow screen readers to focus it.
|
||||
button.importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -80,6 +85,9 @@ class ReminderItemFormView @JvmOverloads constructor(
|
|||
(parent as? ViewGroup)?.removeView(this)
|
||||
}
|
||||
}
|
||||
// It's ok to make the description always be 'Delete Reminder' because when this button is
|
||||
// a plus button we set it as 'unimportant for accessibility' so it can't be focused.
|
||||
button.contentDescription = context.getString(R.string.delete_reminder)
|
||||
button.drawable.mutate().setTint(tintColor)
|
||||
|
||||
textView.setOnClickListener {
|
||||
|
|
@ -99,6 +107,7 @@ class ReminderItemFormView @JvmOverloads constructor(
|
|||
timePickerDialog.show()
|
||||
}
|
||||
}
|
||||
textView.labelFor = button.id
|
||||
}
|
||||
override fun onTimeSet(view: TimePicker?, hourOfDay: Int, minute: Int) {
|
||||
valueChangedListener?.let {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package com.habitrpg.android.habitica.ui.views.tasks.form
|
|||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Space
|
||||
|
|
@ -24,7 +25,9 @@ class TaskDifficultyButtons @JvmOverloads constructor(
|
|||
field = value
|
||||
removeAllViews()
|
||||
addAllButtons()
|
||||
selectedButton.sendAccessibilityEvent(AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
|
||||
}
|
||||
private lateinit var selectedButton: View
|
||||
|
||||
init {
|
||||
addAllButtons()
|
||||
|
|
@ -42,6 +45,9 @@ class TaskDifficultyButtons @JvmOverloads constructor(
|
|||
space.layoutParams = layoutParams
|
||||
addView(space)
|
||||
}
|
||||
if (difficulty.value == selectedDifficulty) {
|
||||
selectedButton = button;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,10 +65,21 @@ class TaskDifficultyButtons @JvmOverloads constructor(
|
|||
}
|
||||
val drawable = HabiticaIconsHelper.imageOfTaskDifficultyStars(difficultyColor, difficulty.value, true).asDrawable(resources)
|
||||
view.findViewById<ImageView>(R.id.image_view).setImageDrawable(drawable)
|
||||
view.findViewById<TextView>(R.id.text_view).text = context.getText(difficulty.nameRes)
|
||||
|
||||
val buttonText = context.getText(difficulty.nameRes)
|
||||
view.findViewById<TextView>(R.id.text_view).text = buttonText
|
||||
view.contentDescription = toContentDescription(buttonText, isActive)
|
||||
|
||||
view.setOnClickListener {
|
||||
selectedDifficulty = difficulty.value
|
||||
}
|
||||
return view
|
||||
}
|
||||
|
||||
private fun toContentDescription(buttonText: CharSequence, isActive: Boolean): String {
|
||||
val statusString = if (isActive) {
|
||||
context.getString(R.string.selected)
|
||||
} else context.getString(R.string.not_selected)
|
||||
return "$buttonText, $statusString"
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import android.text.TextUtils
|
|||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.accessibility.AccessibilityEvent
|
||||
import android.widget.*
|
||||
import androidx.appcompat.widget.AppCompatEditText
|
||||
import androidx.core.content.ContextCompat
|
||||
|
|
@ -226,6 +227,8 @@ class TaskSchedulingControls @JvmOverloads constructor(
|
|||
1 -> weeklyRepeat.su = isActive
|
||||
}
|
||||
createWeeklyRepeatViews()
|
||||
weeklyRepeatWrapper.findViewWithTag<TextView>(weekday).sendAccessibilityEvent(
|
||||
AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION)
|
||||
}
|
||||
|
||||
private fun isWeekdayActive(weekday: Int): Boolean {
|
||||
|
|
@ -247,10 +250,12 @@ class TaskSchedulingControls @JvmOverloads constructor(
|
|||
val lastWeekday = weekdayOrder.last()
|
||||
for (weekdayCode in weekdayOrder) {
|
||||
val button = TextView(context, null, 0, R.style.TaskFormWeekdayButton)
|
||||
val isActive = isWeekdayActive(weekdayCode)
|
||||
val layoutParams = LayoutParams(size, size)
|
||||
button.layoutParams = layoutParams
|
||||
button.text = weekdays[weekdayCode].first().toUpperCase().toString()
|
||||
val isActive = isWeekdayActive(weekdayCode)
|
||||
button.contentDescription = toContentDescription(weekdays[weekdayCode], isActive)
|
||||
button.tag = weekdayCode
|
||||
if (isActive) {
|
||||
button.background = context.getDrawable(R.drawable.habit_scoring_circle_selected)
|
||||
button.background.mutate().setTint(tintColor)
|
||||
|
|
@ -274,25 +279,37 @@ class TaskSchedulingControls @JvmOverloads constructor(
|
|||
}
|
||||
|
||||
private fun configureMonthlyRepeatViews() {
|
||||
val white = ContextCompat.getColor(context, R.color.white)
|
||||
val unselectedText = ContextCompat.getColor(context, R.color.gray_100)
|
||||
val unselectedBackground = ContextCompat.getColor(context, R.color.taskform_gray)
|
||||
if (daysOfMonth != null && daysOfMonth?.isEmpty() != true) {
|
||||
monthlyRepeatDaysButton.setTextColor(white)
|
||||
monthlyRepeatDaysButton.background.mutate().setTint(tintColor)
|
||||
styleButtonAsActive(monthlyRepeatDaysButton)
|
||||
} else {
|
||||
monthlyRepeatDaysButton.setTextColor(unselectedText)
|
||||
monthlyRepeatDaysButton.background.mutate().setTint(unselectedBackground)
|
||||
styleButtonAsInactive(monthlyRepeatDaysButton)
|
||||
}
|
||||
if (weeksOfMonth != null && weeksOfMonth?.isEmpty() != true) {
|
||||
monthlyRepeatWeeksButton.setTextColor(white)
|
||||
monthlyRepeatWeeksButton.background.mutate().setTint(tintColor)
|
||||
styleButtonAsActive(monthlyRepeatWeeksButton)
|
||||
} else {
|
||||
monthlyRepeatWeeksButton.setTextColor(unselectedText)
|
||||
monthlyRepeatWeeksButton.background.mutate().setTint(unselectedBackground)
|
||||
styleButtonAsInactive(monthlyRepeatWeeksButton)
|
||||
}
|
||||
}
|
||||
|
||||
private fun styleButtonAsActive(button: TextView) {
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.white))
|
||||
button.background.mutate().setTint(tintColor)
|
||||
button.contentDescription = toContentDescription(button.text, true)
|
||||
}
|
||||
|
||||
private fun styleButtonAsInactive(button: TextView) {
|
||||
button.setTextColor(ContextCompat.getColor(context, R.color.gray_100))
|
||||
button.background.mutate().setTint(ContextCompat.getColor(context, R.color.taskform_gray))
|
||||
button.contentDescription = toContentDescription(button.text, false)
|
||||
}
|
||||
|
||||
private fun toContentDescription(buttonText: CharSequence, isActive: Boolean): String {
|
||||
val statusString = if (isActive) {
|
||||
context.getString(R.string.selected)
|
||||
} else context.getString(R.string.not_selected)
|
||||
return "$buttonText, $statusString"
|
||||
}
|
||||
|
||||
private fun generateSummary() {
|
||||
var frequencyQualifier = ""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue