When Accessibility Talkback is ON. I have a requirement to show my view with some custom actions when the user draws angular gesture i.e. swipe up and right.
Similar like Gmail messages as shown in the pic below
enter image description here
this popup is shown when the user draws action gesture i.e. swipe up and right when the focus is on any message.
Action menu
You can add a custom action to the AccessibilityNodeInfo object inside the onInitializeAccessibilityNodeInfo method of an AccessibilityDelegate.
If the action is then selected by the user, the performAccessibilityAction method is called on the View.
In the example "myCustomAction" is the text that is displayed to the user.
MyCustomView.kt
init {
accessibilityDelegate = object : View.AccessibilityDelegate() {
override fun onInitializeAccessibilityNodeInfo(host: View?, info: AccessibilityNodeInfo?) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.addAction(AccessibilityNodeInfo.AccessibilityAction(R.id.myCustomAccessibilityEvent, "myCustomAction"));
}
}
}
override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean {
if (action == R.id.myCustomAccessibilityEvent) Log.d("TAG", "Accessibility event triggered")
return true;
}
res/values/accessibility.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="myCustomAccessibilityEvent"/>
</resources>
In order to add an accessibility action the below line of code suffices, using ViewCompat which is backwards compatible to API 21.
ViewCompat.addAccessibilityAction(viewToAddActionTo,"String to describe what the action does", (v,b) -> actionCalled);
//a sample request would look like
ViewCompat.addAccessibilityAction(swipeLayout,"Delete Item in List",(v,b) -> deleteItem(position);
Hopefully this suffices, the action to be done i.e delete, star, archive (Similar to Gmail) can be added to custom views using this.
Key Points are to figure out which View needs the action and what should the action perform.
Related
I am working on the accessibility and currently I want to set the Button Role on the MenuItem. We have checked but not got the proper solution for the same. I tried by setting the custom action layout and then giving the custom action layout Button Role it detect it as button but click need to be handled by setting the click listener on action layout. Which I want to avoid, is there any possibility that we can set role to MenuItem. So it will announce like "Setting Button Double Tap To Activate"
`#JvmStatic
fun View.setCustomRole(roleInfo: String) {
ViewCompat.setAccessibilityDelegate(this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
v: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(v, info)
info.roleDescription = roleInfo
}
})
}`
Tried above method by setting action layout which work but I need to change the click handling from app which I want to avoid.
This was answered fairly recently, however I think I can clean it up somewhat.
Option 1
You can make MenuItem's buttons by default by ensuring you have the latest material library imported.
implementation 'com.google.android.material:material:1.7.0'
The sample app I created for my answer was 1.5.0 and it still had the default "button" announcement.
Option 2
In Material 1.7.0:
I didn't need any of this code to achieve the solution to the question
onInitializeAccessibilityNodeInfo host and info are not nullable!
Ensure that your MenuItem has an actionViewClass associated with it.
<item
...
android:icon="ICON_REFERENCE"
app:showAsAction="ifRoom"
app:actionViewClass="android.widget.ImageButton"
...
/>
Bonus to option 2:
To be able to customize a11y attributes, you can then get the item and assign custom role descriptions or extra actions:
// inside onCreateOptionsMenu
val menuActionView = menu
.findItem(R.id.action_settings)
.actionView as ImageButton
ViewCompat.setAccessibilityDelegate(menuActionView, object: AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfoCompat?
) {
super.onInitializeAccessibilityNodeInfo(host, info)
info?.apply {
// not required as this is already a button
// always use a built in class as this will be localized
// automatically for you
// roleDescription = Button::class.java.simpleName
// I found I had to set this here, and not in the menu xml
contentDescription = getString(R.string.action_settings)
// to replace the term "activate" in "double tap to activate"
// in production apps, use a localized string!
addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK, "Open menu"
)
)
}
}
})
I am trying to change accessibility content description for Android menu item.
Here is my code and talk back announcing => Test search, Search, double tap to activate.
<item
android:id="#+id/menuItemSearch"
android:icon="#drawable/search"
android:iconTintMode="src_atop"
android:title="Search"
android:visible="false"
app:iconTint="#color/primary"
app:contentDescription="Test Search"
app:showAsAction="always"/>
How can change it to => Search. Button. Double tap to search.
Solution
Add -> app:actionViewClass="android.widget.ImageButton"
<item
android:id="#+id/menuItemSearch"
android:icon="#drawable/icongel_search"
android:iconTintMode="src_atop"
android:title="#string/toolbarSearchIcon"
android:visible="false"
app:iconTint="#color/primary"
app:showAsAction="always"
app:actionViewClass="android.widget.ImageButton"/>
Then
menu.findItem(R.id.menuItemSearch).apply {
val searchIcon = this.actionView as ImageButton
searchIcon.apply {
setImageResource(R.drawable.search)
setColorFilter(ContextCompat.getColor(context, R.color.primary), PorterDuff.Mode.SRC_ATOP)
contentDescription = getString(R.string.toolbarSearchIcon)
setBackgroundColor(ContextCompat.getColor(context, R.color.transparent))
ViewCompat.setAccessibilityDelegate(
this,
object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
info.addAction(
AccessibilityNodeInfoCompat.AccessibilityActionCompat(
AccessibilityNodeInfoCompat.ACTION_CLICK, getString(R.string.toolbarSearchIcon)
)
)
}
}
)
}
}
There are 2 issues here:
1. Menu items not announcing as a button
You need to import the latest material library in your app's build.gradle file.
implementation 'com.google.android.material:material:1.7.0'
Be wary as there may be other dependencies.
2. Create a custom action label
Currently this is not possible as you would need to get access to the view in the Toolbar. Then you could use the following method as described in the documentation:
ViewCompat.replaceAccessibilityAction(
// View that contains touch & hold action
itemView, // <-- this is what we don't have
AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_LONG_CLICK,
// Announcement read by TalkBack to surface this action
getText(R.string.favorite),
null
)
You could probably raise a bug on the Issue Tracker for problem 2.
I am trying to improve accessibility for a text view used for terms and privacy policy which looks something like this:
As mentioned on the Google Support page, the user will need to access the Local Context Menu via a TalkBack gesture (default gesture is Swipe up then right) to activate a TextView link.
But I want to use a swipe gesture to navigate between links something like:
Currently, I am using clickable span to add the links, it is working fine with Local Context Menu but not with a swipe gesture.
My code looks something like this:
val clickableSpan: ClickableSpan = object : ClickableSpan() {
override fun onClick(widget: View) {
// handle link click.
}
}
val spannableStringBuilder = SpannableStringBuilder(text).apply {
setSpan(clickableSpan, 99, 109, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
}
tv_term_and_privacy_policy.apply {
setText(spannableStringBuilder)
movementMethod = LinkMovementMethod.getInstance()
}
Please help me with this.
I want to show a badge on a toolbar action. The badge number is updated by a LiveData value.
This is how I attach the badge:
BadgeUtils.attachBadgeDrawable(inboxBadgeDrawable, toolbar, R.id.menu_inbox);
I tried different places for that call, including Activity.onCreateOptionsMenu(), Activity.onPrepareOptionsMenu() and androidx.lifgecycle.Observer.onChanged().
When anything changes (toolbar or badge content), the badge is misplaced, traveling down left. Or it is duplicated to another action.
I guess attachBadgeDrawable tries to find the container view of R.id.menu_inbox inside the toolbar, inserts the badge and updates it's offsets. If the container view of the menu item changes, the old container view still has the old badge and there is no (sensible) way to remove it. Also, application of the offsets seems to stack.
So, is there any other intended way of using the BadgeDrawable on a toolbar action icon?
I understand that this feature is still experimental. Will this issue be addressed and if yes, how long will it approximately take? (I use com.google.android.material:material:1.3.0-beta01 right now.)
This question is mainly addressed to the developers of the component because usage questions should be asked here according to https://github.com/material-components/material-components-android.
EDIT: I also created an issue (feature request) on the project's tracker: https://github.com/material-components/material-components-android/issues/1967
I'm not sure it is an official solution but this is still a workaround. I ended up with detaching the BadgeDrawable on every onPrepareOptionsMenu, in case the menu items were changed or rearranged
// This is an indicator of whether we need to show the badge or not
private var isFilterOn: Boolean = false
private var filterBadge: BadgeDrawable? = null
#SuppressLint("UnsafeExperimentalUsageError")
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
val filterItem = menu.findItem(R.id.action_filter)
val toolbar = requireActivity().findViewById<Toolbar>(R.id.toolbar)
if(filterBadge != null) {
BadgeUtils.detachBadgeDrawable(filterBadge!!, toolbar, R.id.action_filter)
filterBadge = null
}
if(isFilterOn) {
filterBadge = BadgeDrawable.create(requireContext()).also {
BadgeUtils.attachBadgeDrawable(it, toolbar, R.id.action_filter)
}
}
}
I am using the Drag & Drop API's introduced in 3.0 - however, I only want the drag shadow to follow the user's finger around while they are inside one particular area of the screen. If they drift outside that, I would like the shadow to go away and the drag event to end. I have already done some searching and seen that the API does not support updating the drag shadow after creation, which was my first plan, but is there any way to stop the DragEvent without the user actually lifting their finger?
There are two API calls for API 24+ that should be useful:
View#cancelDragAndDrop
Cancels an ongoing drag and drop operation.
A DragEvent object with DragEvent.getAction() value of DragEvent.ACTION_DRAG_ENDED and DragEvent.getResult() value of false will be sent to every View that received DragEvent.ACTION_DRAG_STARTED even if they are not currently visible.
This method can be called on any View in the same window as the View on which startDragAndDrop was called.
and View#updateDragShadow
Updates the drag shadow for the ongoing drag and drop operation.
Which you use and how you use them will depend upon your intentions. If you simply need the drag shadow to disappear when it is moved outside a defined area without ending the drag and drop operation so that the drag shadow will appear again if the user moves it back into the area of interest, updateDragShadow should suffice. If you want to cancel the drag and drop operation completely, use cancelDragAndDrop.
The question doesn't define what "inside one particular area of the screen" means. I assume that it is part of a view. In the drag listener, you can use the drag view coordinates to determine if the drag view is inside or outside the area. The drag view coordinates can be found in DragEvent. Another option would be to define an empty view that covers the area of the screen and take action when the drag view exits that view. (You will need to set a drag and drop listener on that empty view as well.)
The above does not apply to API 23 and below and the solution is not immediately obvious for these lower APIs. The drag shadow has an internal representation that is not accessible through the API as far as I can tell, although you might be able to do something with Reflection. View.java has internal information about the drag and drop operation.
The question is old but since there is no answer; I want to bring attention that this is now the default behaviour according to the official documentation and API here
This behavior can be achieved using the following quoted code:
val imageView = ImageView(this)
// Set the drag event listener for the View.
imageView.setOnDragListener { v, e ->
// Handles each of the expected events.
when (e.action) {
DragEvent.ACTION_DRAG_STARTED -> {
// Determines if this View can accept the dragged data.
if (e.clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) {
// As an example of what your application might do, applies a blue color tint
// to the View to indicate that it can accept data.
(v as? ImageView)?.setColorFilter(Color.BLUE)
// Invalidate the view to force a redraw in the new tint.
v.invalidate()
// Returns true to indicate that the View can accept the dragged data.
true
} else {
// Returns false to indicate that, during the current drag and drop operation,
// this View will not receive events again until ACTION_DRAG_ENDED is sent.
false
}
}
DragEvent.ACTION_DRAG_ENTERED -> {
// Applies a green tint to the View.
(v as? ImageView)?.setColorFilter(Color.GREEN)
// Invalidates the view to force a redraw in the new tint.
v.invalidate()
// Returns true; the value is ignored.
true
}
DragEvent.ACTION_DRAG_LOCATION ->
// Ignore the event.
true
DragEvent.ACTION_DRAG_EXITED -> {
// Resets the color tint to blue.
(v as? ImageView)?.setColorFilter(Color.BLUE)
// Invalidates the view to force a redraw in the new tint.
v.invalidate()
// Returns true; the value is ignored.
true
}
DragEvent.ACTION_DROP -> {
// Gets the item containing the dragged data.
val item: ClipData.Item = e.clipData.getItemAt(0)
// Gets the text data from the item.
val dragData = item.text
// Displays a message containing the dragged data.
Toast.makeText(this, "Dragged data is $dragData", Toast.LENGTH_LONG).show()
// Turns off any color tints.
(v as? ImageView)?.clearColorFilter()
// Invalidates the view to force a redraw.
v.invalidate()
// Returns true. DragEvent.getResult() will return true.
true
}
DragEvent.ACTION_DRAG_ENDED -> {
// Turns off any color tinting.
(v as? ImageView)?.clearColorFilter()
// Invalidates the view to force a redraw.
v.invalidate()
// Does a getResult(), and displays what happened.
when(e.result) {
true ->
Toast.makeText(this, "The drop was handled.", Toast.LENGTH_LONG)
else ->
Toast.makeText(this, "The drop didn't work.", Toast.LENGTH_LONG)
}.show()
// Returns true; the value is ignored.
true
}
else -> {
// An unknown action type was received.
Log.e("DragDrop Example", "Unknown action type received by View.OnDragListener.")
false
}
}
}