onListItemClick stops working after one click - android

LAST EDIT
I found the problem with solution here: http://code.google.com/p/android/issues/detail?id=3414#c27
ORIGINAL POST
I have a ListView (actually a ListFragment) where I want to expand a object when I click on it, and collaps it again on a second click.
This is my onListItemClick
#Override
public void onListItemClick(ListView l, View v, int position, long id) {
//mAdapter.toggleItem(id); // removed this
ListView list = getListView();
int start = list.getFirstVisiblePosition();
for (int i = start, j = list.getLastVisiblePosition(); i <= j; i++) {
Cursor item = (Cursor) list.getItemAtPosition(i);
if (id == item.getLong(item.getColumnIndex(ItemColumns._ID))) {
View view = list.getChildAt(i - start);
ViewStub stub = (ViewStub) view.findViewById(R.id.stub);
// View inflated;
if (stub.getVisibility() == View.VISIBLE)
stub.setVisibility(View.GONE);
else
stub.inflate();
// list.getAdapter().getView(i, view, list);
// break;
}
}
}
It works, but it only fires once. Once the ListItem has expanded the onListItemClick wont get fired again. Any idea why?
best
Marcus
EDIT: It's the actual line: stub.inflate(); which disables the click event
EDIT2: I foudn the problem was due to the ViewStub
it look like this:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="#CC0000"
android:orientation="vertical">
<SeekBar android:id="#+id/progress"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
The LinearLayout does not shadow the original listItem. But if I remove the seekbar it works great.

If your listview contains a control that can steal the focus (Seekbar in your situation), you just need to make it not focusable
<SeekBar
android:focusable="false" />

Related

Android TV ListView does not return to previously selected item

enter image description hereI am trying to get ListView returning to the previously selected item. I have a screen split in two parts, where on the left part is a ListView and on the right part is TextView, showing selected text.
Use case: an item is selected and with Dpad right arrow is made move to the TextView, then back with Dpad left arrow to the ListView, move several items down with Dpad arrow down. Then again with Dpad right arrow move to the TextView and left arrow back. The problem is that a different item is selected, not the last one selected, which is not intuitive, moreover it is randomly selected position instead of the position of the last selected item.
Here is listing of my test ListView:
public class MainActivity extends AppCompatActivity {
private static final String LOG_TAG = MainActivity.class.getSimpleName();
private TextView m_program_description;
ListView listView;
ArrayList<String> arrayList = new ArrayList<>(Arrays.asList("C-Language", "Java", "Data Structure",
"Networking", "Operating System", "Compiler Design", "Theory Of Computation",
"Software Engineering", "Web Engineering"));
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = (ListView) findViewById(R.id.listView);
// simple_list_item_1 is a built in layout. It is part of Android OS, instead of creating our own
// xml layout we are using built-in layout
ArrayAdapter arrayAdapter = new ArrayAdapter(this, android.R.layout.simple_list_item_1, arrayList);
listView.setAdapter(arrayAdapter);
listView.setOnItemClickListener((adapterView, view, i, l) -> {
view.setSelected(true);
view.setActivated(true);
});
listView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
Log.e(LOG_TAG, "onItemSelected() called");
m_program_description = findViewById(R.id.program_description);
m_program_description.setMovementMethod(new ScrollingMovementMethod());
m_program_description.setText(arrayList.get(position));
}
#Override
public void onNothingSelected(AdapterView<?> parent)
{
}
});
}
}
and layout :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:weightSum="2">
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<!--ListView to store list items-->
<ListView
android:id="#+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="#+id/program_description"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="10dp"
android:layout_marginStart="20dp"
android:layout_marginEnd="20dp"
android:layout_marginBottom="25dp"
android:focusable="true"
android:clickable="true"
android:foreground="?attr/selectableItemBackground"
android:scrollbars="vertical"
android:fadeScrollbars="false"
android:textSize="16sp"
android:gravity="start" >
</TextView>
</LinearLayout>
</LinearLayout>
I appreciate your help.
By behaviour observation (i.e.,without drilling the ListView source code from AOSP website),
I believe you shall store the selected position in an instance variable and then reselect the item in ListView if it is not focus then focus again.
In order to store the selected position:
int mItemSelected = -1; // new instance variable
...
listView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener()
{
#Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id)
{
mItemSelected = position; // store position to mItemSelected
Log.e(LOG_TAG, "onItemSelected() called position=" + position);
TextView m_program_description = findViewById(R.id.program_description);
m_program_description.setMovementMethod(new ScrollingMovementMethod());
m_program_description.setText(arrayList.get(position));
}
#Override
public void onNothingSelected(AdapterView<?> parent)
{
}
});
In order to re-select the item when focus (With hack, will be explained after the code sample):
listView.setOnFocusChangeListener(new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View view, boolean isFocus) {
Log.d(LOG_TAG, "onFocusChange: isFocus=" + isFocus + ", mItemSelected=" + mItemSelected);
if (isFocus && mItemSelected != -1) {
TimerTask timerTask = new TimerTask() {
#Override
public void run() {
${YOUR_ACTIVITY}.this.runOnUiThread(() -> {
listView.setItemChecked(mItemSelected, true);
listView.setSelection(mItemSelected);
listView.getChildAt(mItemSelected).requestFocus();
});
}
};
Timer timer = new Timer();
timer.schedule(timerTask, 0);
}
}
});
The related code shall be the three lines using mItemSelected as argument. Maybe any of them will work and I am just too lazy to verify it.
The concern here is not about the implementation of selection, it is more about the life cycle of ListView when focus has changed.
Originally, I hadn't use a Timer to run the code. Same issue was met then. I guess there is a life cycle event which will set the position incorrectly.
Flow
Focus ListView
Move to item position X
Trigger ListView#setOnItemSelectedListener
Set mItemSelected to X
Press DPad to move to TextView
Press DPad to move back to ListView
Trigger ListView#setOnFocusChangeListener
ListView select item X
Unknown life cycle event moves the item unintentionally
So after adding a Timer, step 9 and 8 will be swapped and therefore the result will be expected. This is the hacking done here and you can see I even didn't add a delay to the Timer.
If anyone can provide WHY step 9 occurs, please also let me know.

Android EditText: enable only scrolling but stop comsuming clicks

I am contributing to an app project and I have this issue.
I have a Horizontal LinearLayout with an ArrayAdapter list of Items: image -- EditText -- image
I use EditText to enable scrolling horizontally a long text field while disabling cursor and click events. The parent layout needs to manage the click event on the items + horizontal swipe to change tabs in Main View
The code works properly:
If EditText has a long text: I can scroll horizontally the text to read it all. At the end of the text, the parent layout takes the swipe and can change tabs
I can long touch on the EditText to enable a select checkmark from the parent layout
The cursor and slider are properly hidden in the EditText
The only issue is that EditText consumes the simple click events and never passes them to the parent layout. When I click the images or when I click on the right/left of the EditText, the parent layout properly captures the click action on the adapter item. If I click directly on the EditText, the event is consumed and never reaches the parent layout setOnItemClickListener.
Here's a short video of how it is working now. The click at 10 seconds position in video must be done outside the text: https://www.youtube.com/watch?v=P306p7AcwAI
Here is the xml sample:
https://github.com/PhilZ-cwm6/SMBSync2/blob/philz/SMBSync2/src/main/res/layout/sync_task_item_view.xml
<LinearLayout
android:id="#+id/profile_list_sync_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginRight="5dp"
android:layout_marginLeft="5dp" >
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="#+id/sync_task_master_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="center_vertical|top"
android:src="#drawable/ic_16_server" />
<EditText
android:id="#+id/sync_task_master_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="none"
android:background="#android:color/transparent"
android:cursorVisible="false"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="true"
android:ellipsize="end"
android:lines="1"
android:text="Master"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >
<ImageView
android:id="#+id/sync_task_direction_image"
android:layout_width="12dp"
android:layout_height="18dp"
android:layout_gravity="center"
android:scaleType="fitXY"
android:src="#drawable/arrow_right_enabled" />
<ImageView
android:id="#+id/sync_task_target_icon"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_gravity="top"
android:src="#drawable/ic_16_server" />
<EditText
android:id="#+id/sync_task_target_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="none"
android:background="#android:color/transparent"
android:cursorVisible="false"
android:clickable="false"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="true"
android:ellipsize="end"
android:lines="1"
android:text="Target"
android:textAppearance="?android:attr/textAppearanceSmall" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
the adapter code extract:
https://github.com/PhilZ-cwm6/SMBSync2/blob/philz/SMBSync2/src/main/java/com/sentaroh/android/SMBSync2/AdapterSyncTask.java
public class AdapterSyncTask extends ArrayAdapter<SyncTaskItem> {
final public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder holder;
final SyncTaskItem o = getItem(position);
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(id, null);
holder = new ViewHolder();
holder.tv_row_master = (EditText) v.findViewById(R.id.sync_task_master_info);
holder.tv_row_target = (EditText) v.findViewById(R.id.sync_task_target_info);
}
if (o != null) {
holder.tv_row_master.setText("source dir path");
holder.tv_row_master.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); //disable auto-correct highlight in EditText
holder.tv_row_target.setText(destination dir path);
holder.tv_row_target.setTextColor(mTextColor);
holder.tv_row_target.setInputType(InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS); //disable auto-correct highlight in EditText
}
return v;
}
static class ViewHolder {
EditText tv_row_master, tv_row_target;
}
}
And the ActivityMain that needs the onClick events:
https://github.com/PhilZ-cwm6/SMBSync2/blob/philz/SMBSync2/src/main/java/com/sentaroh/android/SMBSync2/ActivityMain.java
private void setSyncTaskListItemClickListener() {
mGp.syncTaskListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
SyncTaskItem item = mGp.syncTaskAdapter.getItem(position);
editSyncTask(item.getSyncTaskName(), item.isSyncTaskAuto(), position);
}
});
}
These are really simple code snippets
Basically, the original code works properly with a TextView but long text path cannot be viewed/scrolled in a single line. Using HorizontalScrollView works also but doesn't pass any event, include swipe and long press to the parent if the click happens on the text field.
The EditText trick fixes all except the simple click events when they happen on the EditTex box. Wired because long touch and swipe are properly passed. Also, the setOnItemClickListener doesn't capture any event when clicking on the EditText, but captures them if it is a TextView
Any idea how to fix this ? I thought about a custom EditText but ended up unsure how would the ArrayAdapter get the EditText position. Am I obliged to use a second adapter for the EditText ?
best regards
Edit: I can add this code to the getView() of Adapter:
holder.tv_row_target.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
//click events of EditText are captured
}
});
However, I cannot propagate them to the ListView or a Parent view from the onClick()
This would avoid a heavy rewrite in code based on actions that Parent should do on click
Edit: see next post with proper fix to preserve the native ListView animation
I fixed it this way:
in adapter:
holder.tv_row_target.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((ActivityMain) mContext).dispatchSyncTaskListItemClick(o, position);
}
});
In MainActivity:
public void dispatchSyncTaskListItemClick(SyncTaskItem item, int position) {
// my actions
}
That way, I can pass the needed info (position, object item) to the activity
I am open to any other suggestion to simply propagate the click to the parent ListView so that it handles it directly
Proper fix:
I post this for any one having the same issue. I saw so many threads on this but no one ever came with a good fix
My previous answer has an issue: it doesn't preserve the native ListView onClick fading animation
I tried the performclick() but it is the same.
I finally fixed it completely using MotionEvent to pass the touch events to the list view
In Adapter: I set the onClick listeners for click and long click and send the action to MainActivity where the ListView action code is present
Adapter listeners:
holder.tv_row_target.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
((ActivityMain) mContext).dispatchSyncTaskListItemClick(o, position);
}
});
holder.tv_row_target.setOnLongClickListener(new View.OnLongClickListener() {
public boolean onLongClick(View v) {
v.setHapticFeedbackEnabled(false);
((ActivityMain) mContext).dispatchSyncTaskListLongClick(o, position);
return true;//notify long touch event is consumed
}
});
In Main Activity: The setOnItemLongClickListener and setOnItemClickListener must be set there. The issue was to click the EditText or a an icon which handles touch and perform the ListView action and animation
// simulate click on teh ListView item
public void dispatchSyncTaskListItemClick(SyncTaskItem item, int position) {
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis();
int first_visible_pos = mGp.syncTaskListView.getFirstVisiblePosition();
View v = mGp.syncTaskListView.getChildAt(position - first_visible_pos);
float x = v.getX() + 1;//view items start at 0, MotionEvent doesn't handle the UP action at 0
float y = v.getY();
//first touch
MotionEvent motionDown = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, x, y, 0);
motionDown.setSource(InputDevice.SOURCE_TOUCHSCREEN);
mGp.syncTaskListView.onTouchEvent(motionDown);
//release touch
downTime = SystemClock.uptimeMillis();
eventTime = SystemClock.uptimeMillis();
MotionEvent motionUp = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_UP, x, y, 0);
motionUp.setSource(InputDevice.SOURCE_TOUCHSCREEN);
mGp.syncTaskListView.onTouchEvent(motionUp);
motionUp.recycle();
motionDown.recycle();
}
// simulate long touch
public void dispatchSyncTaskListLongClick(SyncTaskItem item, int position) {
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis();
int first_visible_pos = mGp.syncTaskListView.getFirstVisiblePosition();
View v = mGp.syncTaskListView.getChildAt(position - first_visible_pos);
MotionEvent motion = MotionEvent.obtain(downTime, eventTime, MotionEvent.ACTION_DOWN, v.getX(), v.getY(), 0);
motion.setSource(InputDevice.SOURCE_TOUCHSCREEN);
mGp.syncTaskListView.onTouchEvent(motion);
motion.recycle();
}
The hardest part was the x+1 because my View started at float x = 0.0, and the UP action was not considered until it is > 0. Not sure why ? ACTION_DOWN handles it properly
The other thing to take care of is the getChildAt: it returns the view position starting from first visible position and not the adapter passed position. Else, you get a null returned view.
Now, it works just like when interacting with the ListView
New edit: added v.setHapticFeedbackEnabled(false); to remove the duplicate haptic feedback on long touch !
Remaining issue: I feel all this is a workaround. There is still a small delay on the animation trigger when long click. The ideal would be the EditText View completely letting ListView to handle the onClick
So probably only a custom HorizontalScrollView or a custom EditText would fix it

Multiple EditText in ListView, tap to focus on one EditText and focus jumps to the first

I've got EditTexts in my rows in a ListView. When I tap on one of the EditTexts the soft keyboard appears and the focus jumps to the first EditText in the list instead of staying in the field where I tapped.
Here is a video of it:
https://youtu.be/ZwuFrX-WWBo
I created a completely stripped down app to demonstrate the problem. The full code is here: https://pastebin.com/YT8rxqKa
I'm not doing anything to alter the focus in my code:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = layoutInflater.inflate(R.layout.cell_textfield, parent, false);
}
TextView label = (TextView) convertView.findViewById(R.id.textview1);
EditText textfield = (EditText) convertView.findViewById(R.id.textview2);
String text = String.format("Row %d", position);
label.setText(text);
textfield.setText(text);
return convertView;
}
I found another post on StackOverflow giving a workaround for this dumb Android behavior, which involves putting an OnFocusChangedListener on all of the textfields so they can retake focus if it's taken from them improperly.
That worked to regain focus, but then I discovered that when a textfield retakes focus the cursor ends up at the start of the text instead of end, which is unnatural and annoying to my users.
Here is a video of that:
https://youtu.be/A35wLqbuIac
Here's the code for that OnFocusChangeListener. It works to fight the stupid Android behavior of moving focus, but the cursor is misplaced after it regains focus.
View.OnFocusChangeListener onFocusChangeListener = new View.OnFocusChangeListener() {
#Override
public void onFocusChange(View view, boolean hasFocus) {
long t = System.currentTimeMillis();
long delta = t - focusTime;
if (hasFocus) { // gained focus
if (delta > minDeltaForReFocus) {
focusTime = t;
focusTarget = view;
}
}
else { // lost focus
if (delta <= minDeltaForReFocus && view == focusTarget) {
focusTarget.post(new Runnable() { // reset focus to target
public void run() {
Log.d("BA", "requesting focus");
focusTarget.requestFocus();
}
});
}
}
}
};
I hate having to put a bandaid on a bandaid on a bandaid to try to get Android to just behave as it would naturally be expected to behave, but I'll take what I can get.
1) Is there something I can do to fix this problem at the source and not have to have the OnFocusChangeListener at all?
2) If (1) isn't possible, then how can I make sure that when I force focus back to the correct field that I make sure the cursor is placed at the end? I tried using setSelection() right after requestFocus() but since the textfield wasn't yet focused the selection is ignored.
Here was my "solution." In short: ListViews are stupid and will always be a total nightmare when EditTexts are involved, so I changed my Fragment/Adapter code to be able to adapt to either a ListView layout or a ScrollView layout. It only works if you have a small number of rows, because the scrollview implementation isn't able to take advantage of lazy-loading and view recycling. Thankfully, any situation wherein I want EditTexts in a ListView, I rarely have more than 20 rows or so.
When inflating my view in my BaseListFragment, I get my layout id via a method that relies on a hasTextFields() method:
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(getLayoutId(), container, false);
return view;
}
public boolean hasTextfields() {
return false;
}
public int getLayoutId() {
if (hasTextfields()) {
return R.layout.scrollfragment;
} else {
return R.layout.listfragment;
}
}
In my various subclasses of my BaseListFragment, if I need to have an EditText in one of my fields, I just override the hasTextFields() method to return true and then my fragment/adapter switchs over to using the basic scrollview implementation.
From there, it's a matter of making sure that the Adapter handles the standard ListView actions for both the ListView and the ScrollView scenarios. Like this:
public void notifyDataSetChanged() {
// If scrollContainer is not null, that means we're in a ScrollView setup
if (this.scrollContainer != null) {
// intentionally not calling super
this.scrollContainer.removeAllViews();
this.setupRows();
} else {
// use the real ListView
super.notifyDataSetChanged();
}
}
public void setupRows() {
for (int i = 0; i < this.getCount(); i++) {
View view = this.getView(i, null, this.scrollContainer);
view.setOnClickListener(myItemClickListener);
this.scrollContainer.addView(view);
}
}
One issue that the click listener presented is that a ListView wants an AdapterView.OnItemClickListener, but arbitrary Views inside a ScrollView want a simple View.OnClickListener. So, I made my ItemClickListener also implement View.OnClickListener and then just dispatched the OnClick to the OnItemClick method:
public class MyItemClickListener implements AdapterView.OnItemClickListener, View.OnClickListener {
#Override
public void onClick(View v) {
// You can either have your Adapter set the tag on the View to be its position
// or you could have your click listener use v.getParent() and iterate through
// the children to find the position. I find its faster and easier to have my
// adapter set the Tag on the view.
int position = v.getTag();
this.onItemClick(null, v, config.getPosition(), 0);
}
#Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// ...
}
}
Then in MyEditTextListFragment, I create the adapter like this:
listener = createClickListener();
adapter = createListAdapter();
if (scrollContainer != null) {
adapter.setScrollContainer(scrollContainer);
adapter.setMenuItemClickListener(listener);
adapter.setupRows();
} else {
getListView().setOnItemClickListener(listener);
getListView().setAdapter(adapter);
}
Here is my scrollfragment.xml for reference:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
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="match_parent"
android:background="#fff"
android:clickable="true"
>
<!--
The following LinearLayout as a focus catcher that won't cause the keyboard to
show without it, the virtual keyboard shows up immediately/always which means we
never get to the enjoy the full size of our screen while scrolling, and
that sucks.
-->
<LinearLayout
android:focusable="true"
android:focusableInTouchMode="true"
android:layout_width="0px"
android:layout_height="0px"/>
<!--
This ListView is still included in the layout but set to visibility=gone. List
fragments require a standard ListView in the layout, so this gets us past that
check and allows us to use the same adapter code in both listview and scrollview
situations.
-->
<ListView android:id="#id/android:list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:drawSelectorOnTop="false"
android:background="#null"
android:layout_alignParentTop="true"
android:descendantFocusability="afterDescendants"
android:visibility="gone"
/>
<!--
This scrollview will act as our fake listview so that we don't have to deal with
all the stupid crap that comes along with having EditTexts inside a ListView.
-->
<ScrollView
android:id="#+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:descendantFocusability="afterDescendants"
>
<LinearLayout
android:id="#+id/scrollContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
</LinearLayout>
</ScrollView>
</RelativeLayout>
Try this once, it worked for me:
public void setCursorPosition() {
focusTarget.requestFocus();
focusTarget.setCursorVisible(true);
other.setCursorVisible(false);
} else {
other.setCursorVisible(true);
focusTarget.setCursorVisible(false);
}
}

ListView receives touches, but onItemClickListener does not receive clicks after a pause and resume

We generate several ListViews that hold info for a user to filter information in another fragment. It works fine, unless you pause and resume the app (say, backgrounding it, or locking the screen). Once you do that, the list can be scrolled, but not clicked.
List generating code:
private View addList(LayoutInflater inflater, ViewGroup container, final FilterValue.SearchCategory type, final String[] labels) {
ArrayAdapter<String> adapter = generateArrayAdapter(inflater, labels, type);
if(adapter == null) {
return null;
}
filterAdapters.add(adapter);
ListView list = (ListView) inflater.inflate(R.layout.on_demand_filter_list, container, false);
list.setAdapter(adapter);
list.setItemsCanFocus(false);
list.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
list.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
list.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(final View view, final MotionEvent motionEvent) {
LOG.d(TAG, "NO TOUCHING!");
return false; //To change body of implemented methods use File | Settings | File Templates.
}
});
list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
LOG.d(TAG, "onItemClick!");
CheckedTextView textView = (CheckedTextView) view.findViewById(R.id.title);
textView.toggle();
if (textView.isChecked()) {
filterValue.addToSelectedList(labels[i], type);
} else {
filterValue.removeFromSelectedList(labels[i], type);
}
}
});
list.setAdapter(adapter);
list.setVisibility(View.GONE);
filterListContainer.addView(list);
return list;
}
The onTouch listener only exists to ensure the Touch is received. (It is.) The DescendantFocusability appears to have no effect, this bug exists before and after it was added.
Each is tied to a button that shows or hides the list.
titleHeader.setOnClickListener(new View.OnClickListener() {
public void onClick(View clickedView) {
closeNetworkList();
closeGenreList();
titlesOpen = !titlesOpen;
ImageView indicator = (ImageView) clickedView.findViewById(R.id.filter_expansion_indicator_icon);
if (indicator != null) {
if (titlesOpen) {
indicator.setImageResource(R.drawable.arrow_filter_up);
} else {
indicator.setImageResource(R.drawable.arrow_filter_down);
}
}
if (titlesOpen) {
titlesListView.setVisibility(View.VISIBLE);
} else {
titlesListView.setVisibility(View.GONE);
}
}
});
Tapping this button to hide and then show the listView (which was generated with addList) resets something, and the items can be clicked again.
XML for an item row:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:focusableInTouchMode="false"
android:padding="8dp"
android:orientation="horizontal">
<CheckedTextView
android:id="#+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:textColor="#drawable/on_demand_filter_checked_text_sel"
android:gravity="center_vertical"
android:paddingLeft="76dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:drawableLeft="#drawable/checkbox_sel"
android:drawablePadding="14dp"
style="#style/LargeRegular"/>
</LinearLayout>
The focusables are new additions, but neither worked. The problem occurred before they were added.
The ListView itself:
<ListView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/list"
android:layout_width="fill_parent"
android:layout_height="275dp"
android:layout_marginLeft="5dp"
android:layout_marginRight="5dp"
android:divider="#color/Transparent"
android:descendantFocusability="blocksDescendants"
android:cacheColorHint="#ffffff"/>
I am at my absolute wits' end. No one on my team has a sensible solution to this. It works fine, right up until you pause and resume. We do absolutely nothing that touches the views in resume or pause. Can anyone help? I can provide more detail as needed.
I had similar problem with my app (extended SurfaceView which lost touch events after resume) and resolved it by calling the setFocusable( true ) in the onResume() implementation. Apparently the view didn't get the focus and therefore did not receive the touch events. Not sure whether this is the case here, but worth trying.
Remembered that I had had a similar problem with fragment activities. I had a case when layout requests were blocked, they did not cause actual layout traverse.
I've fixed it in Enroscar library (BaseFragment class) with the following snippet of code in a fragment class:
#Override
public void onStart() {
// ... other staff ...
super.onStart();
/*
XXX I don't know the reason but sometimes after coming back here from other activity all layout requests are blocked. :(
It looks like some concurrency issue or a views tree traversal bug
*/
final View contentView = getActivity().findViewById(android.R.id.content);
if (contentView != null) {
final ViewParent root = contentView.getParent();
if (contentView.isLayoutRequested() && !root.isLayoutRequested()) {
if (DebugFlags.DEBUG_GUI) { Log.i("View", "fix layout request"); }
root.requestLayout();
}
}
}

Indeterminate progressbar in listview

I'm trying to create a list that shows an indeterminate progress bar as the last entry while it's fetching more data. I can show the bar and get/add the data, but scrolling up and down while it's loading causes multiple progress bars to show up.
I have a ListActivity that uses an ArrayAdapter. Each row has a layout as follows.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent" android:layout_height="wrap_content"
android:background="#drawable/textlines" android:padding="2dip">
<LinearLayout android:layout_width="fill_parent"
android:layout_height="wrap_content" android:id="#+id/rowContent">
<TextView android:height="20sp" android:text=""
android:id="#+id/search_display" android:layout_width="fill_parent"
android:textSize="16sp" android:layout_height="20sp" android:gravity="left" />
</LinearLayout>
<LinearLayout android:layout_height="wrap_content"
android:layout_width="fill_parent" android:id="#+id/rowSpinner"
android:padding="3px" android:gravity="center" android:visibility="gone">
<ProgressBar android:layout_width="wrap_content"
android:layout_height="wrap_content" android:id="#+id/progress"
android:indeterminate="true" />
</LinearLayout>
</RelativeLayout>
The ListView has an OnScrollListener with the following onScroll method.
public void onScroll(final AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
{
// detect if last item is visible
if ((visibleItemCount < totalItemCount)
&& (firstVisibleItem + visibleItemCount == totalItemCount))
{
if (false == scrollTaskRunning)
{
scrollTaskRunning = true;
getMoreData(totalItemCount);
}
}
}
getMoreData invokes an AsyncTask that gets some more data to add to adapter. In its onPreExecute I call showSpinner() --
private void showSpinner()
{
// nothing to do if there's already a spinner visible
if (isSpinVisible == true) return;
// hide the progress spinner
if (0 < lvList.getChildCount())
{
View vRow = lvList.getChildAt(lvList.getChildCount() - 1);
vRow.findViewById(R.id.rowContent).setVisibility(View.GONE);
vRow.findViewById(R.id.rowSpinner).setVisibility(View.VISIBLE);
}
isSpinVisible = true;
}
and in its onPostExecute/onCancelled I call hideSpinner() which is the same code, except checking the isSpinVisible flag the other way, and GONE and VISIBLE swapped. The swapping code, so far as I can tell, only gets called once, but multiple entries in the last show up with the progress bar visible if you scroll up and down.
I tried doing this instead for hideSpinner() --
private void hideSpinner()
{
// nothing to do if there's no spinner visible
if (isSpinVisible == false) return;
// show the progress spinner
int iChildCount = lvList.getChildCount();
if (0 < lvList.getChildCount())
{
for (int i = 0; i < iChildCount; i++)
{
View vRow = lvList.getChildAt(iChildCount);
if (null != vRow)
{
vRow.findViewById(R.id.rowContent).setVisibility(View.VISIBLE);
vRow.findViewById(R.id.rowSpinner).setVisibility(View.GONE);
}
}
}
else if (null != pbSearch)
{
pbSearch.setVisibility(View.GONE);
}
isSpinVisible = false;
}
but vRow is null, and some of the progress bars still show up. How do I fix this? Alternatively, is there a better way to do this? (I thought I might be able to do something with my ArrayAdapter's getView() method, but I couldn't work it out.)
ETA: this answer seems to explain the problem I'm having, but knowing that hasn't helped me find a way around it.
ETA2: I tried doing this:
final LayoutInflater mInflater = (LayoutInflater)
getSystemService(Context.LAYOUT_INFLATER_SERVICE);
aapSearchResults = new ArrayAdapter<ParsedXML>(this, R.layout.search_row, saData)
{
#Override
public View getView(int position, View convertView, ViewGroup parent)
{
View row;
// get the view
if (null == convertView)
{
row = mInflater.inflate(R.layout.search_row, null);
}
else
{
row = convertView;
}
// bind the data to the view
TextView tv = (TextView) row.findViewById(R.id.search_display);
tv.setText(getItem(position).name);
// show data, hide spinner
row.findViewById(R.id.rowContent).setVisibility(View.VISIBLE);
row.findViewById(R.id.rowSpinner).setVisibility(View.GONE);
// if the current position is the last, and a task is running,
// show the progress bar
if (taskRunning && (position == this.getCount()-1))
{
row.findViewById(R.id.rowContent).setVisibility(View.GONE);
row.findViewById(R.id.rowSpinner).setVisibility(View.VISIBLE);
}
return row;
}
};
lvList.setAdapter(aapSearchResults);
but I'm clearly making a logic error still, because sometimes scrolling up and down now gets me blank entries. (Is it because the count has changed?)
Generally speaking you should never touch the children of a ListView directly. In fact, after they leave your adapter's getView() nest, you should consider them to be fully independent and entirely divorced from your direct control. The only way you can control them is by calling notifyDataSetChanged() and letting the ListView make new ones.
To get around your problem, do something like:
public class MyAdapter extends ArrayAdapter[…]{
//[…]
private boolean mIsLoading = false;
public void setIsLoading(boolean isLoading){
if (mIsLoading != isLoading){
mIsLoading = isLoading;
notifyDataSetChanged();
}
}
#Override
public int getCount(){
return super.getCount() + (isLoading ? 1 : 0);
}
#Override
public View getView(int position, View convertView, ViewGroup parent) {
if (mIsLoading && position == (getCount() - 1)){
//return your progress view goes here. Ensure that it has the ID R.id.progress;
}else{
if (convertView != null && convertView.getId() == R.id.progress){
convertView = null;
}
return super.getView(position, convertView, parent);
}
}
}
I would suggest you use a BaseAdapter instead. It automatically loads more elements when the last one on the list is shown.

Categories

Resources