📜 ⬆️ ⬇️

64 milliseconds after pressing

If your application downloads data from the Internet, displays it in a ListView and processes cell clicks, you can continue to read. This is a story about how you can paint over 64 ms after clicking on a list cell.

We had a regular list in which there were 2 types of cells: non-clickable categories and clickable cells

image
Random piccha with subcategories

The adapter we used can be seen here:
github.com/siyusong/foodtruck-master-android/blob/master/src/com/foodtruckmaster/android/adapter/SeparatedListAdapter.java
')
The data was downloaded from the server, displayed in the ListView, when you click on a cell, a separate screen opens with a detailed description.
AdapterView.OnItemClickListener was used to handle clicks. Our adapters in getItem returned objects that were passed on to detailed description screens.

Handling is done like this:
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { Description desc = parent.getItemAtPosition(position); DescriptionActivity.open(context, desc); } 


Crashes of ClassCastException (String -> Description) began to appear in crashlytics. This meant that all the same people clicked on non-clickable subtitles in the lists, and instead of the Description object, we got a String. Non-clickable cells can be clicked using performItemClick, but we didn’t use such methods and there were crashes on all screens with lists and subtitles, even though there were not many of them.

Further we will dig in source codes 4.2.2
AbsListView, onTouchEvent method
 case MotionEvent.ACTION_UP: { switch (mTouchMode) { case TOUCH_MODE_DOWN: case TOUCH_MODE_TAP: case TOUCH_MODE_DONE_WAITING: ... final AbsListView.PerformClick performClick = mPerformClick; ... if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) { ... if (mTouchModeReset != null) { removeCallbacks(mTouchModeReset); } mTouchModeReset = new Runnable() { @Override public void run() { mTouchMode = TOUCH_MODE_REST; child.setPressed(false); setPressed(false); if (!mDataChanged) { performClick.run(); } } }; if (!mDataChanged && mAdapter.isEnabled(motionPosition)) { ... postDelayed(mTouchModeReset, ViewConfiguration.getPressedStateDuration()); } ... return true; } ... } 


The source android without beer is better not to go, apparently the developers of the OS were guided by the same principle.
Here we see that if we clicked on the list cell and it is enabled, then we call PefrormClick at a certain interval. In android 4.2.2 this interval is 64 ms.

This is what Runnable PerformClick looks like.
 private class PerformClick extends WindowRunnnable implements Runnable { int mClickMotionPosition; public void run() { // The data has changed since we posted this action in the event queue, // bail out before bad things happen if (mDataChanged) return; final ListAdapter adapter = mAdapter; final int motionPosition = mClickMotionPosition; if (adapter != null && mItemCount > 0 && motionPosition != INVALID_POSITION && motionPosition < adapter.getCount() && sameWindow()) { final View view = getChildAt(motionPosition - mFirstPosition); // If there is no view, something bad happened (the view scrolled off the // screen, etc.) and we should cancel the click if (view != null) { performItemClick(view, motionPosition, adapter.getItemId(motionPosition)); } } } } 


This runnable invokes performItemClick, where our OnItemClickListener is already called. We see that if the data in the adapter is changed, then it is flipped. Checking the boundaries of the adapter and so on. The most interesting thing is that if you install a new adapter, and not change the data in the old one, then mDataChanged will be equal to false, it is still worth noting that there is no check on the isEnabled cell.

Those. we click on the cell, change the adapter for 64 ms, this runnable is executed and, as a result, the click is not on the data that we saw on the phone, but on new ones. And if the new adapter has a cell isEnabled = false, then it will still click, onItemClickListener will be called.

So, in the line:
 Description desc = parent.getItemAtPosition(position); 

we miraculously got ClassCastException

Solution: Obviously, this is a bug OS, and the simplest solution would be to set the mDataChanged flag to true, or to clear the message queue when changing the adapter.

Conclusion:
If you clicked on a cell, and at this moment new data was downloaded from the server and installed in the list, then you clicked on new data (if you created the adapter again).
Always check the result of the getItemAtPosition method for null and for instanceof if you have several types of cells and item objects.

Source: https://habr.com/ru/post/226897/


All Articles