Change reading order of elements within a focusable ViewGroup - android

I have some screens in my app where TalkBack does not infer the correct reading order. According to the documentation, I can use android:accessibilityTraversalAfter and friends to alter the reading order. But it does not work for me for elements within a focusable ViewGroup that should be read together.
The entire layout looks like that:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:accessibilityTraversalBefore="#id/before"
android:focusable="true"
tools:context=".MainActivity">
<TextView
android:id="#+id/before"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:accessibilityTraversalAfter="#id/before"
android:text="Before"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
<TextView
android:id="#+id/after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:accessibilityTraversalBefore="#id/after"
android:text="After"
app:layout_constraintBottom_toTopOf="#+id/before"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
It renders After in the middle of the screen, Before at the bottom. I want TalkBack to treat the entire screen as a single contiguous element, hence I set android:focusable to true. By default, TalkBack reads: "After, before". But I want it to read "Before, after". Although I've added android:accessibilityTraversalBefore and android:accessibilityTraversalAfter, it still reads "After, before". That's the output of the Node Tree Debugging:
TreeDebug: (-2147455381)429.FrameLayout:(0, 0 - 1080, 1920):A
TreeDebug: (30189)429.TextView:(42, 101 - 397, 172):TEXT{My Application}:A:supportsTextLocation
TreeDebug: (31150)429.ViewGroup:(0, 210 - 1080, 1794):Fa:focusable:accessibilityFocused
TreeDebug: (33072)429.TextView:(499, 951 - 581, 1002):TEXT{After}:A:supportsTextLocation
TreeDebug: (32111)429.TextView:(485, 1743 - 595, 1794):TEXT{Before}:A:supportsTextLocation
What am I doing wrong?
Just for completeness: minSdkVersion is 26, targetSdkVersion is 29.

I've dug into the problem and found out following: accessibilityTraversalBefore and accessibilityTraversalAfter flags in deed take effect, but only for what they are intended for - they are intended for Accessibility Service app (e.g. Talkback). In other words if you remove focusable attribute from root layout you'll see that navigation is correct.
But those flags do not affect as to how AccessibilityNode is constructed for root ViewGroup. As can be seen in sources of ViewGroup#onInitializeAccessibilityNodeInfoInternal() the actual text construction logic does not regard how children construct their navigation using upper mentioned flags.
In order to solve the problem I've removed excessive flags from layout xml as such:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="#+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:focusable="true"
tools:context=".MainActivity">
<TextView
android:id="#+id/before"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="Before"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="#+id/after"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:importantForAccessibility="no"
android:text="After"
app:layout_constraintBottom_toTopOf="#+id/before"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
And inside the host activity/fragment:
val root = findViewById<ViewGroup>(R.id.root)
ViewCompat.setAccessibilityDelegate(root, object : AccessibilityDelegateCompat() {
override fun onInitializeAccessibilityNodeInfo(
host: View?,
info: AccessibilityNodeInfoCompat?
) {
val stringBuilder = StringBuilder()
root.children.forEach { view ->
val label = if (view is TextView) view.text else ""
stringBuilder.append("$label, ")
}
info?.text = stringBuilder.toString()
super.onInitializeAccessibilityNodeInfo(host, info)
}
})
This will result in the desired outcome: Talkback will pronounce "Before, After".
Unfortunately, this is an error prone code, meaning that if you restructure the view hierarchy in a way, that order of children gets swapped then this node text construction logic will become broken. Nevertheless, I couldn't come up with a better solution and cannot see it's possible to instruct parent to regard child ordering flags (based on sources).

Related

How to get a reference to an object in a NavigationView Header

In my activity, I am inflating the layout via data binding. The binding itself works correctly, and I am able to get references to objects in the layout file easily.
One of those objects is a NavigationView. It has a headerLayout:
<com.google.android.material.navigation.NavigationView
android:id="#+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#android:color/white"
android:fitsSystemWindows="true"
app:itemIconTint="#color/navigation_view_color"
app:itemTextColor="#color/navigation_view_color"
app:headerLayout="#layout/nav_view_header" />
The header layout looks like this (extra objects removed for simplicity):
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#color/menu_blue"
android:paddingBottom="32dp">
<ImageView
android:id="#+id/navViewLogOutButton"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="12dp"
android:src="#drawable/logout"
app:tint="#color/white"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="16dp"
android:contentDescription="#string/sz.proApp.support.logOut" />
</androidx.constraintlayout.widget.ConstraintLayout>
I need to set a click listener on this image view, but I can't get a reference to it. This is what I am using:
binding.navView.findViewById<ImageView>(R.id.navViewLogOutButton)?.setOnClickListener{
doSomething()
}
I've verified that binding.navView is not null. Also, by setting a break point there, I can see what the memory address for this ImageView is. When I change that line to this:
binding.navView.findViewById<ImageView>(213123403)
I get the reference that I want. But obviously, I can't just use the integer since that is dynamic. The code compiles and the app launches. But when it hits that line, findViewById returns null every time.
I read that findViewById may not work with data binding in a activity, but if that is true, then how can I get this reference so that I can set the listener?
app:headerLayout="#layout/nav_view_header"
So, the generated binding class should beNavViewHeaderBinding, you can get the binding with:
val headerBinding = NavViewHeaderBinding.bind(binding.navView.getHeaderView(0))
And access the header views through headerBinding:
headerBinding.navViewLogOutButton

Popup menu uses original size of Image as anchor, instead of actual size

I am having an image view displaying a nice image and opening a popup-menu on tap.
The images I use have different sizes in original, so the imageView has a static width.
However when I anchor the pop-up-menu at the imageView, it uses the original sized image as anchor, which results in very weird views. (Screenshots appended)
Of course I could size all images on the same size, but since the images are having sizes in px and not dp, it would lead to problems on screens with a different screen-resolution.
Is this an error in my code or a bug of the pop-up menu? Wanted to file it as bug, but it´s pretty much impossible to find the place to do so either.
Here´s my xml for the imageView (image view having a default icon):
<ImageView
android:id="#+id/languageFlag"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/activity_vertical_margin"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="#id/searchView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/searchView"
app:layout_constraintTop_toTopOf="#+id/searchView"
app:srcCompat="#drawable/ic_baseline_casino_24" />
Here´s my xml for the whole fragment (Just in case this matters):
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="bi.deutsch_kirundi_app.fragments.DictionaryFragment">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#color/lightgrey">
<ImageView
android:id="#+id/languageFlag"
android:layout_width="24dp"
android:layout_height="wrap_content"
android:layout_margin="#dimen/activity_vertical_margin"
android:clickable="true"
app:layout_constraintBottom_toBottomOf="#id/searchView"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="#+id/searchView"
app:layout_constraintTop_toTopOf="#+id/searchView"
app:srcCompat="#drawable/ic_baseline_casino_24" />
<SearchView
android:id="#+id/searchView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="#dimen/activity_vertical_margin"
android:layout_marginLeft="#dimen/activity_vertical_margin"
android:layout_marginBottom="#dimen/activity_vertical_margin"
android:background="#color/white"
android:iconifiedByDefault="false"
app:layout_constraintEnd_toStartOf="#id/languageFlag"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
>
</SearchView>
<androidx.fragment.app.FragmentContainerView
android:id="#+id/result_fragment"
android:name="bi.deutsch_kirundi_app.fragments.dictionary.AllVocabularyFragment"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="#dimen/activity_vertical_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="#id/searchView"
tools:layout="#layout/fragment_all_vocabulary" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>
In the "onCreateView"-method of my fragment, I get the imageView, get the images ids and set the one for the corresponding language saved in SharedPreferences. (Logically)
languagePicker = view.findViewById(R.id.languageFlag)
val sharedPreferences = view.context.getSharedPreferences(App.APP_PREFERENCES, Context.MODE_PRIVATE)
val lastChosenTranslationDirection = sharedPreferences.getInt(App.TRANSLAT_KEY, 0)
val ids = view.context.resources.getStringArray(R.array.translation_direction_flags)
images = Array(ids.size) {
val imageId = resources.getIdentifier(ids[it], "drawable", activity?.packageName)
ContextCompat.getDrawable(view.context, imageId)
}
languagePicker.setImageDrawable(images[lastChosenTranslationDirection])
Then I am setting the onclick-listener on the imageView (languagePicker) the following:
languagePicker.setOnClickListener {
val dropDownMenu = PopupMenu(it.context, languagePicker)
val languages = it.resources.getStringArray(R.array.translation_directions)
for(index in languages.indices) {
dropDownMenu.menu.add(0, index, index,
languages[index])
dropDownMenu.show()
dropDownMenu.setOnMenuItemClickListener(this)
}
}
Seems fine to me, but maybe I am blinded. Is there a need to add some specific property to the imageview, so the size of the Drawable is also adapted?
EDIT: I don´t want the width of the PopUp-Menu to be changed. Rather I want the Popup-Menu to use the ImageView as anchor, not the originally sized image. As visible on the screenshot with the German Flag, it is pushed to the bottom instead of sticking to the ImageView.
The width of the PopupMenu item layout actually doesn't have anything to do with the size of the original images set on the ImageView. The item layouts for PopupMenus simply have a minimum default width, and I don't think you can change it.
I would suggest that you switch to using Spinner, PopupWindow, or ListPopupWindow. These allow you to set your own item layouts, so you can choose exactly how you want each item to look.
I believe the default item layout for a Spinner just has its width set to the widest item in the list, so that might be the easiest route:
https://developer.android.com/guide/topics/ui/controls/spinner

Complicated RecyclerView issue. Eliding some fields

Trying to build a recyclerview where one of the widgets (a TextView holding a date) is gone (View.GONE) depending on a variable. What I have for the item in the recyclerview looks like this:
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="#+id/historyItemLayout"
android:weightSum="100"
android:layout_marginTop="3dp"
android:orientation="vertical"
xmlns:android="http://schemas.android.com/apk/res/android">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:id="#+id/historyItemDateTV"
android:background="#color/grey_300"
android:text="09-24-2020"
/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:weightSum="100"
android:layout_marginTop="3dp">
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_weight="20"
android:layout_marginStart="15dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/historyItemTeamNameTV"
android:text="#string/team_name"
android:textColor="#color/black"
/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="#+id/historyItemPlayerNameTV"
android:text="#string/player_name"
android:textColor="#color/black"
/>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.appcompat.widget.LinearLayoutCompat>
There is a list of entries that each holds all of the data that will be pushed to the adapter. So, each entry into that list holds a date field, a player name a team name and some other data (elided here for brevity).
What I want to do is display the date the first time it comes up, but hide (actually, View.GONE) it until the date changes.
Here is an example:
Entry1 2020-09-27 other data
Entry2 2020-09-27 other data
Entry3 2020-09-27 other data
Entry4 2020-08-01 other data
Entry5 2020-05-05 other data
So, for this, I would display a line showing 2020-09-27, then have three lines under it that show the data for the three entries that each have 2020-09-27 as their date. Then, I'd display another line holding just the date 2020-08-01 then the data for that date on the line following, and finally display another line with just 2020-05-05 and another line with the data for that final date.
I've got all of the data sorted correctly using a comparator for insertion into my adapter, but how upon display do I check to see if the date line needs to be hidden or not?
Think I have the answer...will verify.
In the RecyclerView I'll save off the last date I display, and check that in onBindViewHolder. If the dates match, I'll hide that TextView (or perhaps its layout).
Something like this:
//Now handle hiding of extraneous dates
if(lastDate == child.date) {
holder.dateTV.visibility = View.GONE
}
else {
lastDate = child.date
}
Not sure if I understood that correctly (a screenshot probably would have explained it better).
But I think, that you should use View.INVISIBLE instead of View.GONE, in order not to mess up the whole layout. View.INVISIBLE and View.GONE both literally do what they say - and when the parent node (the recycler-item) has wrap_content set, this may result in a mess (uneven heights).

How to include an Android layout but also access its elements in binding?

I'm doind this on my activity:
<include
android:id="#+id/withGyroLayout"
layout="#layout/with_gyro_layout"/>
Where with_gyro_layout.xml is
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.util.FixedTransformerViewPager
android:id="#+id/viewPagerTop"
android:layout_width="0dp"
android:layout_height="143dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.example.util.FixedTransformerViewPager
android:id="#+id/viewPagerBottom"
android:layout_width="0dp"
android:layout_height="143dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="#+id/viewPagerTop" />
</androidx.constraintlayout.widget.ConstraintLayout>
However, I can't access the elements viewPagerBottom or viewPagerTop from the binding for my activity:
binding.viewPagerBottom.setVisibility(View.VISIBLE);
binding.viewPagerTop.setVisibility(View.VISIBLE);
I tried putting with_gyro_layout.xml around <merge>...</merge> but it also didn't solve.
I want to be able to change programatically between with_gyro_layout.xml and without_gyro_layout.xml and also access its inner elements by the binding. How can I do that?
Two things are required in order to use ViewBinding with an included layout.
<merge> is not supported
The documentation only covers Data Binding, and not View Binding, but it does appear to be applicable to both. See https://developer.android.com/topic/libraries/data-binding/expressions#includes
Data binding doesn't support include as a direct child of a merge element.
In other words, the layout must have a real, concrete view as its root element. The following is supported:
<LinearLayout ...>
<TextView ... />
<TextView ... />
</LinearLayout>
But a layout with a <merge> root is not supported:
<merge ...>
<TextView ... />
<TextView ... />
</merge>
The <include> tag must specify an ID
It is, in general, possible to include a layout without explicitly specifying an ID. View Binding does not support this:
<include layout="#layout/included_layout"/>
Even if the included layout has an ID on its root element, it is still not supported. Instead, you must explicitly specify the ID on the <include> tag:
<include
android:id="#+id/some_id"
layout="#layout/included_layout"/>
Once both of these conditions are satisfied, your generated binding for the outer layout will include a reference to the binding of the included layout. Let's say our two files are outer_layout.xml and included_layout.xml. Then these two files would be generated:
OuterLayoutBinding.java
IncludedLayoutBinding.java
And you could reference the included views like this:
val outerBinding = OuterLayoutBinding.inflate(layoutInflater)
val innerBinding = binding.someId // uses the id specified on the include tag
val innerView = innerBinding.viewPagerTop
Or, for short:
val binding = OuterLayoutBinding.inflate(layoutInflater)
val innerView = binding.someId.viewPagerTop

RecyclerView not updating the view properly with ConstraintLayout Group Visibility

Problem:
It seems when I have a specific combination of views and setVisibility between Visible and Gone the RecyclerView does not update properly after initial load. I have a RelativeLayout->ConstraintLayout->Constraint Group(with visibility dynamically set). The view within the constraint group is not updating properly.
Example Use Case of Problem:
So the linked code below it will show a search view at the top. The initial state of empty shows the view properly(With the search icon showing). Then if you type "T" then the search icon will disappear(it shouldn't). So if you either delete the T or type the next letter "E" then it shows again. Also if you delete all search text and type T again it will show.
View Code:
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:bind="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.example.davidcorrado.myapplication.PlayerVM" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:id="#+id/playerCell_main_layout"
android:layout_width="match_parent"
android:layout_height="84dp"
android:layout_alignParentTop="true">
<android.support.constraint.Group
android:id="#+id/playerCell_status_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="#{vm.showingStatusIcons}"
app:constraint_referenced_ids="playerCell_lineup_status_image" />
<ImageView
android:id="#+id/playerCell_lineup_status_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:src="#drawable/ic_search" />
</android.support.constraint.ConstraintLayout>
</RelativeLayout>
</layout>
Quirks that might help:
1) If I move the visibility line to the relativeLayout or the ConstraintLayout the things seem to work
2) If I remove the RelativeLayout view things seem to work.
See the below for the full code example:
https://github.com/DavidCorrado/LayoutIssue
As far as I can tell, the reason the search icon disappears is due to the call to ObservableList.clear() then ObservableList.addAll(*)
So the disappear happens on the clear, then the reappear happens on the addAll.
As the callbacks in the adapter kick in on both calls, my hunch is it animates the view to disappear, then animates to show when the addAll is triggered.
I have verified this by not actioning a 'List.clear()' and instead in your textChange listener simply adding more views to the list.
I'm not sure how you would clear and add items to the list without animating the clear, perhaps there are some settings you can toggle in the RecyclerAdapter to ignore the remove of Views or not animate the entry of Views?
My adjusted code in your callback for class PlayersListVM
class PlayersListVM {
val filteredVms: ObservableList<ViewModel> = ObservableArrayList<ViewModel>()
val showing = PlayerVM(isShowing = true)
val missing = PlayerVM()
init {
filteredVms.add(showing)
}
fun onQueryChanged(newText: String) {
if (filteredVms.size % 2 == 1) filteredVms.add(missing)
else filteredVms.add(showing)
}
}

Categories

Resources