[Android] RecyclerView sliding event processing and source code analysis

question

There is a current requirement, that is, some side-sliding functions of item need to be completed in RV , but some interesting situations have been encountered in the middle, so here is a record, first of all, there is a very strange situation, that is, when we are in the item of RV When there is a sliding event, the ACTION_DOWN event will be processed no matter what (the same is true for not adding or not intercepting in the RV intercept method).

Catch up with the onInterceptTouchEvent method of RV. First, the comment indicates that when ACTION_DOWN occurs, that is, when a click occurs, RV will not intercept this event, and will actively pass this DOWN event on.

...
if (mLayoutSuppressed) {
    
    
  // When layout is suppressed,  RV does not intercept the motion event.
  // A child view e.g. a button may still get the click.
  return false;
}
...

Then I have another question, why I made a judgment on the current event in ACTION_MOVE to judge whether the current event should be intercepted, if intercepted, why would this event be passed to the self-control? First of all, we need to understand that our events must be carried out according to the process: for example, ACTION_DOWN->ACTION_MOVE->ACTION_UP. If interception is performed in ACTION_MOVE, it is indeed intercepted, and the subsequent MOVE will not be passed on, but the previous ACTION_DOWN event has been passed to the child control. Since it has been passed, as long as the onTouchEvent( ) returns true, indicating that the event has been consumed. So it doesn't feel very reliable to judge event interception in ACTION_MOVE in RV.

So where can we actually intercept an event? ACTION_DOWN from his source of course ! If we directly return a true there, then subsequent events can completely say goodbye to the child control! Because as long as RV intercepts the current event, it will go to his touchEvent method to process this event.

MotionEvent.ACTION_DOWN -> {
    
    
    Log.d("MainActivity", "RV down")
    return true
}

So what exactly do we have to do to fulfill our needs in RV?

First of all, there are two solutions, one is to get the currently clicked item in RV, and then directly operate on this item in RV, but this method is less flexible, so I will focus on the first The second way is to process the event mainly in the item. Next, I will introduce how to process the event. In order to process the onTouchEvent method of the layout, we first need to introduce the layout (equivalent to a custom view now. ).

class LeftSlideItem(context: Context, attributeSet: AttributeSet?): LinearLayout(context, attributeSet) {
    
    
    val mView:View = LayoutInflater.from(context).inflate(R.layout.left_slide_item, this, true)
    val content = mView.findViewById<LinearLayout>(R.id.content)
    val delete = mView.findViewById<TextView>(R.id.deleteText)
    var dx = 0f
    var dy = 0f
    var curX = 0f
    var curY = 0f
    var preX = 0f
    var preY = 0f
  ...
}

The focus of handling events lies in the onTouchEvent method. We don't need to use the requestDisallowInterceptTouchEvent method in RV, because RV has already handled it for us. we just need to be in

The onTouchEvent method returns true according to the current situation ( true means that this event is consumed! Only when we consume the ACTION_DOWN event, subsequent events will follow ), the following code means that when the horizontal sliding distance is higher than the vertical , start to process the sliding in the item, otherwise the event will be handed over to the parent container (that is, RecyclerView) for processing.

override fun onTouchEvent(event: MotionEvent): Boolean {
    
    
  ...
  if(abs(curX - preX) > abs(curY - preY)){
    
    
    preX = curX
    preY = curY
    Log.d("MainActivity", "子控件消耗事件")
    return true     //只有返回true后续才会有事件进来?
  }
  preX = curX
  preY = curY
	return super.onTouchEvent(event)
}

The whole process is over here, and the above is an overall idea for completing the event processing in the requirements.


Watch the fucking source code

Next, I will introduce the source code for intercepting sliding events in RV, and let's take a look at the upper part of processing action first.

public boolean onInterceptTouchEvent(MotionEvent e) {
    
    
  if (mLayoutSuppressed) {
    
    
    //1
    // When layout is suppressed,  RV does not intercept the motion event.
    // A child view e.g. a button may still get the click.
    return false;
  }

  // Clear the active onInterceptTouchListener.  None should be set at this time, and if one
  // is, it's because some other code didn't follow the standard contract.
  mInterceptingOnItemTouchListener = null;
  if (findInterceptingOnItemTouchListener(e)) {
    
    
    //2
    cancelScroll();
    return true;
  }

  if (mLayout == null) {
    
    
    return false;	//3
  }

  //4
  final boolean canScrollHorizontally = mLayout.canScrollHorizontally();
  final boolean canScrollVertically = mLayout.canScrollVertically();

  if (mVelocityTracker == null) {
    
    
    mVelocityTracker = VelocityTracker.obtain();
  }
  mVelocityTracker.addMovement(e);

  final int action = e.getActionMasked();
  final int actionIndex = e.getActionIndex();
	
  ...
	
  return mScrollState == SCROLL_STATE_DRAGGING;
}

Note 1 mentioned that if the layout is clicked, he will not handle this event first and let the event be handled by the child control.

Then in Note 2, we can add some click events to the item (the addOnItemTouchListener method will be called). If the ItemTouchListener is added, we will see if the ItemTouchListener will intercept it according to the current event. If it is intercepted, then RV will intercept it. Current Events! After intercepting the event, the onTouchEvent corresponding to the ItemTouchListener will be executed in the onTouchEvent. If the item here can consume the event, the event will end here. So in general, note 2 is to see if the ItemTouchListener has been added. Take the appropriate action.

private boolean findInterceptingOnItemTouchListener(MotionEvent e) {
    
    
  int action = e.getAction();
  final int listenerCount = mOnItemTouchListeners.size();
  for (int i = 0; i < listenerCount; i++) {
    
    
    final OnItemTouchListener listener = mOnItemTouchListeners.get(i);
    if (listener.onInterceptTouchEvent(this, e) && action != MotionEvent.ACTION_CANCEL) {
    
    
      mInterceptingOnItemTouchListener = listener;
      return true;
    }
  }
  return false;
}

Then the mLayout in Note 3 is the LayoutManager. If we do not set this member variable, RV will not intercept the event. Going further down, the note 4 is basically some preparations for the subsequent judgment of the current action. Now we come to the code for judging the event action, and then explain it by event.

First look at the starting point of the event, the first line of code getPointerId is to get the first touch point (some devices support multi-touch, official documentation https://developer.android.com/reference/android/view/MotionEvent) . The state of mScrollState is mentioned later, and there are three states in total. Then judge whether it is automatic scrolling below, if it is, stop scrolling, and set the current state to finger scrolling, and start sliding.

//停止滚动
public static final int SCROLL_STATE_IDLE = 0;
 
//正在被外部拖拽,一般为用户正在用手指滚动
public static final int SCROLL_STATE_DRAGGING = 1;
 
//自动滚动开始
public static final int SCROLL_STATE_SETTLING = 2;
case MotionEvent.ACTION_DOWN:
  if (mIgnoreMotionEventTillDown) {
    
    
    mIgnoreMotionEventTillDown = false;
  }
  mScrollPointerId = e.getPointerId(0);
  mInitialTouchX = mLastTouchX = (int) (e.getX() + 0.5f);
  mInitialTouchY = mLastTouchY = (int) (e.getY() + 0.5f);

  if (mScrollState == SCROLL_STATE_SETTLING) {
    
    
    getParent().requestDisallowInterceptTouchEvent(true);
    setScrollState(SCROLL_STATE_DRAGGING);
    stopNestedScroll(TYPE_NON_TOUCH);
  }

  // Clear the nested offsets
  mNestedOffsets[0] = mNestedOffsets[1] = 0;

  int nestedScrollAxis = ViewCompat.SCROLL_AXIS_NONE;
  if (canScrollHorizontally) {
    
    
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_HORIZONTAL;
  }
  if (canScrollVertically) {
    
    
    nestedScrollAxis |= ViewCompat.SCROLL_AXIS_VERTICAL;
  }
  startNestedScroll(nestedScrollAxis, TYPE_TOUCH);
  break;

Then look at the ACTION_MOVE code, which is constantly calculating the changes between the current x-axis and y-axis and the previous one, and setting the state of scrolling

case MotionEvent.ACTION_MOVE: {
    
    
  final int index = e.findPointerIndex(mScrollPointerId);
  if (index < 0) {
    
    
    Log.e(TAG, "Error processing scroll; pointer index for id "
          + mScrollPointerId + " not found. Did any MotionEvents get skipped?");
    return false;
  }

  final int x = (int) (e.getX(index) + 0.5f);
  final int y = (int) (e.getY(index) + 0.5f);
  if (mScrollState != SCROLL_STATE_DRAGGING) {
    
    
    final int dx = x - mInitialTouchX;
    final int dy = y - mInitialTouchY;
    boolean startScroll = false;
    if (canScrollHorizontally && Math.abs(dx) > mTouchSlop) {
    
    
      mLastTouchX = x;
      startScroll = true;
    }
    if (canScrollVertically && Math.abs(dy) > mTouchSlop) {
    
    
      mLastTouchY = y;
      startScroll = true;
    }
    if (startScroll) {
    
    
      setScrollState(SCROLL_STATE_DRAGGING);
    }
  }
} break;

Then it comes to ACTION_UP, which is also very simple, that is, some marks are cleared.

case MotionEvent.ACTION_UP: {
    
    
    mVelocityTracker.clear();
    stopNestedScroll(TYPE_TOUCH);
} break;

Generally speaking, there are two main things to do in the onIntercept method. The first thing is whether the current event needs to be processed by RV. The second thing is that if you handle this event by yourself, set some related variables.

Guess you like

Origin blog.csdn.net/qq_42788340/article/details/125171443