Slide delete is a common feature in apps, and understanding how it works will give you a better understanding of how to handle custom ViewGroup measurements, placement, and touch events.

Why are there two implementations? This feature can be implemented from different angles:

  • One is the parent layout to handle, distribute events, control the location of the child view, that is, through the custom RecyclerView implementation
  • The other way to do this is by intercepting and handling events through sub-viewgroups, which is to customize the layout of ItemView

The general idea of both methods is the same, content occupies the screen, menu View is outside the screen, when sliding, content slides on the screen, menu enters the screen, and the desired effect is achieved. The layout sketch is as follows:

First, customize RecyclerView

There are three key points to customize RecyclerView mode:

  • Find the ItemView of the touch based on the touch point
  • When to intercept events
  • How to make the hidden Menu slide out

1.1. Find the ItemView of the touch based on the touch point

RecyclerView recycles, reuse ItemView to avoid creating a large number of objects, improve performance, so its internal child view is a screen can see those ItemView, you can find all ItemView through RecyclerView traversal. We can get its Bound from the ItemView, which is a Rect, and with that Rect, we can tell if the touch point is in that ItemView, and we can find the ItemView that the touch point is in. The code is as follows:

Rect frame = new Rect();

final int count = getChildCount();
for (int i = count - 1; i >= 0; i--) {
	final View child = getChildAt(i);
	if (child.getVisibility() == View.VISIBLE) {
		// Get the bound for the child view
		child.getHitRect(frame);
        // Check whether the touch point is in the subview
        if (frame.contains(x, y)) {
            returni; }}}Copy the code

1.2. When to intercept events

The RecyclerView needs to process gesture events, and the internal ItemVIew needs to process events, so when to intercept events? There are two cases as follows:

  • In ACTION_DOWN, if an ItemView is already in the expanded state, and the clicked object is not the same ItemView that is already open, intercept the event and close the expanded ItemView.

  • In the case of ACTION_MOVE, there are two judgments. If one is satisfied, it is considered to be sideslip: 1. The speed in the x direction is greater than the speed in the Y direction and greater than the minimum speed limit; 2. 2. The sideslip distance in the x direction is greater than the y direction, and the x direction reaches the minimum slip distance;

The code is as follows:

public class SwipeDeleteRecyclerView extends RecyclerView {
    @Override
    public boolean onInterceptTouchEvent(MotionEvent e) {...switch (e.getAction()) {
            // The first case
            case MotionEvent.ACTION_DOWN:
                ...
                // There is already an ItemView expanded, and this time the clicked object is not the same ItemView that is already open
                if(view ! =null&& mFlingView ! = view && view.getScrollX() ! =0) {
                    // Close the expanded ItemView
                    view.scrollTo(0.0);
                    // Intercepts the event
                    return true;
                }
             	break;
             // The second case
             case MotionEvent.ACTION_MOVE:
                mVelocityTracker.computeCurrentVelocity(1000);
                // If one of the two judgments is satisfied, it is considered as sideslip:
                // 1. If the velocity in the x direction is greater than the velocity in the Y direction and is greater than the minimum speed limit;
                // 2. If the sideslip distance in the x direction is greater than the slip distance in the Y direction and the minimum slip distance in the X direction is reached;
                float xVelocity = mVelocityTracker.getXVelocity();
                float yVelocity = mVelocityTracker.getYVelocity();
                if(Math.abs(xVelocity) > SNAP_VELOCITY && Math.abs(xVelocity) > Math.abs(yVelocity) || Math.abs(x - mFirstX) >= mTouchSlop  && Math.abs(x - mFirstX) > Math.abs(y - mFirstY)) { mIsSlide =true;
                    return true;
                }
                break; . }... }}Copy the code

After intercepting the event, it’s time to process the event, so let’s move on.

1.3. Let the hidden Menu slide out

Then handle the event in onTouchEvent and let the hidden Menu slide out.

  • First, in ACTION_MOVE, if it’s in the slide state, let the target ItemView follow the gesture by scrollBy(), paying attention to the bounds

  • In ACTION_UP, this produces two results: one is to continue expanding the menu, and the other is to close the menu. These two results are divided into two cases:

    1. When the slide speed to the left exceeds the threshold on the release, let the target ItemView continue expanding at the same speed as when the release occurred.

    2. When the slide speed to the right exceeds the threshold on the release, close the target ItemView.

    3. When the move exceeds half of the hidden width (that is, half of the maximum distance that can be moved), let the ItemVIew continue to expand.

    4, when the release moves less than half the width of the hidden, let the ItemVIew close.

public boolean onTouchEvent(MotionEvent e) {
	obtainVelocity(e);
	switch (e.getAction()) {
		case MotionEvent.ACTION_MOVE:
			float dx = mLastX - x;
			// Determine the boundary
			if (mFlingView.getScrollX() + dx <= mMenuViewWidth
					&& mFlingView.getScrollX() + dx > 0) {
				// Swipe with fingers
				mFlingView.scrollBy((int) dx, 0);
			}
			break;
		case MotionEvent.ACTION_UP:
			int scrollX = mFlingView.getScrollX();
			mVelocityTracker.computeCurrentVelocity(1000);
			
			if (mVelocityTracker.getXVelocity() < -SNAP_VELOCITY) {    // Open when the minimum speed of left sideslip is reached
				// Calculate the remaining distance to move
				int delt = Math.abs(mMenuViewWidth - scrollX);
				// Calculate the time to move according to the speed of the release
				int t = (int) (delt / mVelocityTracker.getXVelocity() * 1000);
				/ / move
				mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(t));
			} else if (mVelocityTracker.getXVelocity() >= SNAP_VELOCITY) {  // Slide to the right reaches the minimum speed of sideslip, then close
				mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
			} else if (scrollX >= mMenuViewWidth / 2) { // If there is more than half of the delete button, open it
				mScroller.startScroll(scrollX, 0, mMenuViewWidth - scrollX, 0, Math.abs(mMenuViewWidth - scrollX));
			} else {    // Otherwise, it is closed
				mScroller.startScroll(scrollX, 0, -scrollX, 0, Math.abs(scrollX));
			}
			invalidate();
			releaseVelocity();  // Release the trace
			break;
	}
	return true;
}
Copy the code

Here the VelocityTracker is used to obtain the sliding speed, and the Scroller is used to control the Sliding of ItemView.

1.4, delete Item

Add a click event to the slide out menu in the onBindViewHolder() of the Holder of RecyclerView to respond to deletion

override fun onBindViewHolder(holder: ViewHolder, position: Int) {

	holder.tvDelete.setOnClickListener {
		onDelete(holder.adapterPosition)
	}
}
Copy the code

Due to the reuse mechanism of RecyclerView, need to be in the point of the delete menu to delete Item, let Item closed, otherwise it will appear to delete an Item scroll down, will come out again an expanded Item.

fun onDelete(it:Int){
	mData.removeAt(it)
	adapter.notifyItemRemoved(it)
    // Call closeMenu() to close the item
	mBinding.rvAll.closeMenu()
}
Copy the code

The closing method is as simple as scrollTo(0, 0) for the Item

public void closeMenu(a) {
    if(mFlingView ! =null&& mFlingView.getScrollX() ! =0) {
        / / close
        mFlingView.scrollTo(0.0); }}Copy the code

1.5, the last

Note: When the sliding speed to the left exceeds the threshold, the target ItemView should continue to expand at the speed it was at when it let go. In this case, the remaining distance to slide should be calculated, and then the remaining sliding time should be calculated based on the speed it was at when it let go, as the scroller.startScroll () time. Otherwise, you may get stuck.

A final look at the effect:

Two, custom ItemView

Custom ItemView mode and custom RecyclerView mode the general idea is the same, the differences are:

  • Custom ItemView inherits from ViewGroup
  • Custom ItemView requires measurement placement of subviews
  • A custom ItemView needs to not only intercept events down (intercepting events for the child View), but also up (intercepting events for the parent View)

2.1. Measurement layout

The measurement process is simple, and the contentView and menuView are measured separately. The contentView directly uses measureChildWithMargins() to measure the height of the entire item, so the menuView height should also follow its height. To measure a menuView, construct its corresponding widthMeasureSpec and widthMeasureSpec. Add the width of all menuViews to mMenuViewWidth.

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	// The width of the hidden menu
	mMenuViewWidth = 0;
    // The height of the content section
	mHeight = 0;
    // The height of the content section
	int contentWidth = 0;
    
	int childCount = getChildCount();
	for (int i = 0; i < childCount; i++) {
		View childView = getChildAt(i);
		if (i == 0) {
			/ / measuring ContentView
			measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
			contentWidth = childView.getMeasuredWidth();
			mHeight = Math.max(mHeight, childView.getMeasuredHeight());
		} else {
			/ / measurement menu
			LayoutParams layoutParams = childView.getLayoutParams();
			int widthSpec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
            // mHeight as its exact height
			intheightSpec = MeasureSpec.makeMeasureSpec(mHeight, MeasureSpec.EXACTLY); childView.measure(widthSpec, heightSpec); mMenuViewWidth += childView.getMeasuredWidth(); }}// Take the width of the first Item(Content)
	setMeasuredDimension(getPaddingLeft() + getPaddingRight() + contentWidth,
			mHeight + getPaddingTop() + getPaddingBottom());
}
Copy the code

2.2. Layout

Since the width and height of all sub-views have been determined in the measurement process, it is necessary to directly place sub-views.

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    int childCount = getChildCount();
    int left = getPaddingLeft();
    for (int i = 0; i < childCount; i++) { View childView = getChildAt(i); childView.layout(left, getPaddingTop(), left + childView.getMeasuredWidth(), getPaddingTop() + childView.getMeasuredHeight()); left = left + childView.getMeasuredWidth(); }}Copy the code

2.3 intercept events

The custom ItemView implementation intercepts events in two ways:

1, return true in onInterceptTouchEvent(

2, through the getParent (). RequestDisallowInterceptTouchEvent (true); Prevents a parent view from intercepting events

So what is the case for interception? In fact, and the custom RecyclerView way is similar, divided into two cases:

  • In ACTION_DOWN, if an ItemView is already in the expanded state, and the clicked object is not the same ItemView that is already open, intercept the event and close the expanded ItemView.

  • In the case of ACTION_MOVE, there are two judgments. If one is satisfied, it is considered to be sideslip: 1. The speed in the x direction is greater than the speed in the Y direction and greater than the minimum speed limit; 2. 2. The sideslip distance in the x direction is greater than the y direction, and the x direction reaches the minimum slip distance;

Unlike a custom RecyclerView, a custom RecyclerView can hold references to open ItemViews. Custom ItemViews need to hold open ItemViews through frequent variables. I’m not going to put any code here. See: SwipeMenuLayout

2.4. Consumption events

The so-called consumption event is to process the event in onTouchEvent to achieve the sideslip effect. The realization of ideas and custom RecyclerView method is basically the same, here is not much to say.

2.5, delete Item

Delete is also achieved by adding click events to menuView, and the RecyclerView customization is different in that there is no need to manually call the operation to close the ItemView. Just close the onDetachedFromWindow of the custom ItemView and destroy it. The code is as follows:

@Override
protected void onDetachedFromWindow(a) {
    if (this == mViewCache) {
        mViewCache.smoothClose();
        mViewCache = null;
    }
    super.onDetachedFromWindow();
}
Copy the code

2.6, the limited

One limitation of this method is that click events added via holder-.ItemView are invalid. Instead, click events need to be added to the contentView.

// Make the click event invalid for itemView
holder.itemView.setOnClickListener {
    onClick(item)
}
// Set the click event for the content
holder.itemContent.setOnClickListener {
    onClick(item)
}
Copy the code

Three,

1. Common ground

The general idea is the same:

  1. layout

    The width of the content section of the layout takes up the entire width of the ItemView, and the menu section is hidden to the right of the Content section.

  2. Events to intercept

    Occurs in onInterceptTouchEvent

    • ACTION_DOWN, determine whether there is an open menu, if there is and it is not the Item of the current event, intercept the event, and close the menu.

    • In ACTION_MOVE, if the velocity in the X direction is greater than the speed threshold and greater than the velocity in the Y direction, or if the distance in the X direction is greater than the distance threshold and greater than the distance in the Y direction, intercept the event.

  3. Incident response

    Happens in an onTouchEvent

    • With ACTION_MOVE, scrollBy() makes the current ItemView move with your finger, paying attention to the bounds.

    • In ACTION_UP, if the left sliding speed is greater than the threshold and no menu is not fully open, make it open by scroller. You need to calculate the time required for expansion based on the speed and remaining distance.

    • Also when the sliding speed to the right is greater than the threshold and no menu is not completely closed, it is closed by scroller.

    • ACTION_UP, if the sliding speed is less than the threshold, and the sliding distance is more than half of the width of the menu part, it is opened through scroller; Closes if the slide distance is less than half the width of the menu section.

2. Differences
  • Custom RecyclerView needs to find the corresponding itemView according to the location of the touch point, and the expanded itemView object is saved in it;

Customizing an ItemView simply saves the currently open ItemView object through a static variable.

  • Custom RecyclerView triggers deletion by manually closing the current itemView menu at the business layer. Custom ItemView can be closed automatically.
  • Custom RecyclerView can implement layouts through XML. Customizing ItemView requires you to measure the placement of subviews.
3. Aggressiveness

Both ways need to be referenced through XML, but the custom RecyclerView way in touch delete need to manually close the menu, more invasive than the custom ItemView.

4. Be careful

The scroller’s time to slide is calculated based on the speed at which the finger is lifted and the remaining distance to slide, so that the free slide speed is consistent with the speed at which the hand is delivered. You can avoid getting stuck.

The final code here: custom RecyclerView, custom ItemView

Reference:

Blog.csdn.net/dapangzao/a…