64 milliseconds after clicking

    If your application downloads data from the Internet, displays it in a ListView and processes clicks on cells, then you can continue reading. This is a story about how you can paint over for 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 of

    image
    Random picch 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

    Data was downloaded from the server, displayed in the ListView, when you click on a cell, a separate screen opens with a detailed description.
    To process clicks, we used AdapterView.OnItemClickListener. Our adapters in getItem returned objects that were passed on to the screens of the detailed description.

    Click processing was done like this:
    public void onItemClick(AdapterView parent, View view, int position, long id) {
        Description desc = parent.getItemAtPosition(position);
        DescriptionActivity.open(context, desc);
    }
    


    ClassCastException crashes (String -> Description) began to appear in crashlytics. This meant that we clicked on non-clickable subheadings in the lists, and instead of the Description object we got String. Non-clickable cells can be clicked using performItemClick, but we did not use such methods and there were crashes on all screens with lists and subheadings, although there were few of them.

    Further we will delve into 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;
            } 
            ...
        }
    


    It’s better not to go into the android source without beer, apparently the developers of the os were guided by the same principle.
    Here we see that if we click on the list box and it is enabled, then we call PefrormClick after a certain interval. In android 4.2.2, this interval is 64 ms.

    It looks like Runnable PerformClick
    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 calls performItemClick, where our OnItemClickListener is already called. We see that if the data in the adapter has changed, then it is poured. Check adapter boundaries and more. 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 false, it’s worth noting that there is no check for isEnabled cells.

    Those. we click on the cell, within 64 ms we change the adapter, this runnable is executed and as a result, the click occurs not according to the data that we saw on the phone, but according to the new. Moreover, if the cell has isEnabled = false in the new adapter, then it still clicks, onItemClickListener is called.

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

    we miraculously got a ClassCastException

    Solution: obviously this is a bug, 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 that moment new data was downloaded from the server and installed in the list, then you clicked on the new data (if you created the adapter again).
    Always check the result of the getItemAtPosition method on null and on instanceof if you have several cell types and item objects.

    Also popular now: