Question
I've spent about 6 hours on this so far, and been hitting nothing but
roadblocks. The general premise is that there is some row in a ListView
(whether it's generated by the adapter, or added as a header view) that
contains an EditText
widget and a Button
. All I want to do is be able to
use the jogball/arrows, to navigate the selector to individual items like
normal, but when I get to a particular row -- even if I have to explicitly
identify the row -- that has a focusable child, I want that child to take
focus instead of indicating the position with the selector.
I've tried many possibilities, and have so far had no luck.
layout:
<ListView
android:id="@android:id/list"
android:layout_height="fill_parent"
android:layout_width="fill_parent"
/>
Header view:
EditText view = new EditText(this);
listView.addHeaderView(view, null, true);
Assuming there are other items in the adapter, using the arrow keys will move
the selection up/down in the list, as expected; but when getting to the header
row, it is also displayed with the selector, and no way to focus into the
EditText
using the jogball. Note: tapping on the EditText
will focus it
at that point, however that relies on a touchscreen, which should not be a
requirement.
ListView
apparently has two modes in this regard:
1. setItemsCanFocus(true)
: selector is never displayed, but the EditText
can get focus when using the arrows. Focus search algorithm is hard to
predict, and no visual feedback (on any rows: having focusable children or
not) on which item is selected, both of which can give the user an unexpected
experience.
2. setItemsCanFocus(false)
: selector is always drawn in non-touch-mode, and
EditText
can never get focus -- even if you tap on it.
To make matters worse, calling editTextView.requestFocus()
returns true, but
in fact does not give the EditText focus.
What I'm envisioning is basically a hybrid of 1 & 2, where rather than the list setting if all items are focusable or not, I want to set focusability for a single item in the list, so that the selector seamlessly transitions from selecting the entire row for non-focusable items, and traversing the focus tree for items that contain focusable children.
Any takers?
Answer
Sorry, answered my own question. It may not be the most correct or most elegant solution, but it works for me, and gives a pretty solid user experience. I looked into the code for ListView to see why the two behaviors are so different, and came across this from ListView.java:
public void setItemsCanFocus(boolean itemsCanFocus) {
mItemsCanFocus = itemsCanFocus;
if (!itemsCanFocus) {
setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
}
So, when calling setItemsCanFocus(false)
, it's also setting descendant
focusability such that no child can get focus. This explains why I couldn't
just toggle mItemsCanFocus
in the ListView's OnItemSelectedListener --
because the ListView was then blocking focus to all children.
What I have now:
<ListView
android:id="@android:id/list"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:descendantFocusability="beforeDescendants"
/>
I use beforeDescendants
because the selector will only be drawn when the
ListView itself (not a child) has focus, so the default behavior needs to be
that the ListView takes focus first and draws selectors.
Then in the OnItemSelectedListener, since I know which header view I want to override the selector (would take more work to dynamically determine if any given position contains a focusable view), I can change descendant focusability, and set focus on the EditText. And when I navigate out of that header, change it back it again.
public void onItemSelected(AdapterView<?> listView, View view, int position, long id)
{
if (position == 1)
{
// listView.setItemsCanFocus(true);
// Use afterDescendants, because I don't want the ListView to steal focus
listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
myEditText.requestFocus();
}
else
{
if (!listView.isFocused())
{
// listView.setItemsCanFocus(false);
// Use beforeDescendants so that the EditText doesn't re-take focus
listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
listView.requestFocus();
}
}
}
public void onNothingSelected(AdapterView<?> listView)
{
// This happens when you start scrolling, so we need to prevent it from staying
// in the afterDescendants mode if the EditText was focused
listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
}
Note the commented-out setItemsCanFocus
calls. With those calls, I got the
correct behavior, but setItemsCanFocus(false)
caused focus to jump from the
EditText, to another widget outside of the ListView, back to the ListView and
displayed the selector on the next selected item, and that jumping focus was
distracting. Removing the ItemsCanFocus change, and just toggling descendant
focusability got me the desired behavior. All items draw the selector as
normal, but when getting to the row with the EditText, it focused on the text
field instead. Then when continuing out of that EditText, it started drawing
the selector again.