android (realize left-swipe to delete) custom control + event distribution

Swipe left to delete

  • logic behind
    • 1 layout drawing
      • onMeasure
      • onLayout
    • 2 Distribution of events
      • do not deal with
      • dad intercept
        • don’t eat
        • eat
      • Conclusion of event distribution
  • Implementation of the complete code
    • renderings
    • the code

Logic behind

If you want to achieve left-swipe deletion, if the existing controls are not satisfied, you must customize the View.
Then consider the effect that needs to be achieved. There must be two sub-controls in it, one is to display content and the other is to display buttons, so there is no doubt that custom controls need to inherit ViewGroup (layout control).

1 layout drawing

In layout drawing, the most important ones are the onMeasure method and onLayout method. An onMeasure is used to measure the size of the control, and the onLayout method is to arrange the position of the child controls in the control.
Swipe left to delete specific analysis:
1. It should be like this at the beginning (users can’t see things outside the screen)

2. When moving left

First of all, our custom controls must be placed in other controls, and usually this is placed in RecyclerView.
So we assume that this is an item layout, and the complete style of the item layout is as follows.
It’s very simple. There is a CardView outside, and a SwipeView inside, which is our custom control. Then there are two direct sub-controls in SwipeView, one for text and one for buttons.

<androidx.cardview.widget.CardView
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:id="@ + id/card"
        android:layout_margin="3dp"
        app:cardCornerRadius="@dimen/cardCornerRadius"
        tools:ignore="MissingConstraints">

        <com.rengda.sigangapp.SwipeView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            >

        <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@ + id/contentView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:paddingRight="@dimen/mainPadding"
            android:paddingVertical="@dimen/mainPadding"
            android:clickable="true"
            >
            <TextView
                android:id="@ + id/No"
                android:layout_marginLeft="3dp"
                android:layout_marginRight="3dp"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintLeft_toLeftOf="parent"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textSize="@dimen/headTitleFontSize"
                android:textColor="@color/black"
                android:text="serial number">
            </TextView>
        </androidx.constraintlayout.widget.ConstraintLayout>
        <LinearLayout
                android:id="@ + id/deleteButton"
                android:layout_width="100dp"
                android:layout_height="match_parent"
                android:gravity="center"
                android:background="@color/redButton">
                <TextView
                    android:layout_width="match_parent"
                    android:textSize="@dimen/headFontSize"
                    android:layout_height="match_parent"
                    android:textColor="@color/white"
                    android:gravity="center"
                    android:background="@color/redButton"
                    android:text="delete">
                </TextView>
            </LinearLayout>
        </com.rengda.sigangapp.SwipeView>
    </androidx.cardview.widget.CardView>

Then the custom control logic is very clear
1. When just drawing, the first child control should occupy the control, and then the buttons are arranged on the right side of the first control.
2. When the gesture slides to the left, the content of the entire control moves to the left
3. When the gesture slides to the right, the content of the entire control moves to the right

onMeasure

onMeasure is used to determine how big the control should be. This method is when the parent control wants to place the child control, the parent control will ask you how much space you want. So the two parameters in onMeasure are given by the parent control of this control. According to the above layout, CardView is the parent control of the custom control. And obviously, CardView knows how wide it is, but not how tall it is. So in fact, what we focus on is the height, and the width is actually just according to Dad.

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        measureChild(getChildAt(0),widthMeasureSpec, heightMeasureSpec)//Measure the height and width of the child, measure the first child first, because the height of the second child must follow the first child
        var width = MeasureSpec.getSize(widthMeasureSpec)//The width that the parent component can give
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)//How to set the width of the parent component
        var height = MeasureSpec.getSize(heightMeasureSpec)//The height that the parent component can give
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)//How to set the height of the parent component
        var resultWidth=0;
        var resultHeight=0;

        //actually only three types
        if(widthSpecMode==MeasureSpec.EXACTLY){//Indicates that the parent control gives the child view a specific value, and the child view should be set to the size of these values
            resultWidth=width //Listen to Dad
        }else if (widthSpecMode==MeasureSpec.UNSPECIFIED){//The parent component didn't tell you the limit, it's up to you
            resultWidth= getChildAt(0).measuredWidth //use the width of the first child
        }else if(widthSpecMode==MeasureSpec.AT_MOST){//Indicates the maximum specific value of the parent control and child view, and the child view cannot exceed the size of this value
            resultWidth=Math.min(getChildAt(0).measuredWidth,width) //See who is the smallest and listen to whom
        }
        //actually only three types
        if(heightSpecMode==MeasureSpec.EXACTLY){//Indicates that the parent control gives the child view a specific value, and the child view should be set to the size of these values
            resultHeight=height//Listen to Dad
        }else if (heightSpecMode==MeasureSpec.UNSPECIFIED){//The parent component didn't tell you the limit, it's up to you
            resultHeight= getChildAt(0).measuredHeight //Use the height of the first child
        }else if(heightSpecMode==MeasureSpec.AT_MOST){//Indicates the maximum specific value of the parent control and child view, and the child view cannot exceed the size of this value
            resultHeight=Math.min(getChildAt(0).measuredHeight,height) //See who is the smallest and listen to whom
        }

        setMeasuredDimension(resultWidth, resultHeight)

        //Measure the second The height of the second child has already been determined, so the height is inserted EXACTLY and the height of the father, and the width is still done by yourself, so pass in the width request of the grandfather, and the father does not care about your width
        measureChild(getChildAt(1),widthMeasureSpec, MeasureSpec.makeMeasureSpec(resultHeight,MeasureSpec.EXACTLY))//measure the height and width of the child




    }

onLayout

Set the top, bottom, left, and right positions of the control for the first child.
Set the second child to the right of the first child.

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
         if (childCount==2){
            val view = getChildAt(1)
            sendViewWidth=view.measuredWidth;//Record the width of the button
            view.layout(r, t, r + view.measuredWidth, b) //The delete button is arranged after the first View
        }
        getChildAt(0).layout(l, t, r, b)//Set the position of the first control up, down, left, and right
    }

This step is over. In fact, the display of the page just entered is over. As for the display of left-swipe and right-swipe, it needs to be carried out in a certain event.

2 Event distribution

1. Complete event refers to the complete process from pressing the finger down, to moving the finger, and then lifting the finger up. Of course, in this process, move may not be triggered once, or may be triggered multiple times.
2. Event:
When dealing with events, there are three important concepts. 1. Distribution 2. Interception 3. Consumption.
Whether to distribute (dispatchTouchEvent): When you get the event, you must enter dispatchTouchEvent, and the returned result indicates whether to return it to the superior for processing. True is not returned to the superior, false is returned to the superior.
Whether to intercept (onInterceptTouchEvent): When the event is distributed to itself, the layout file has the function of intercepting the event, whether to let this event to itself, not to send it down, if true intercept and consume, if false to continue sending.
Whether to consume (onTouchEvent): Whether to consume this event, if it is true, it means consumption, if it is false, it means no consumption.
It’s very abstract, it doesn’t matter, let’s understand it slowly:
The following figure is an example of a nested layout. A is the father of B, B is the father of C and D, and C and D are brothers.

None

None of these layouts do any processing for events, so when my finger is pressed in the red area (down event).
Well, in fact, A, B, and C all have this red zone, and D is not qualified to participate at all, and his zone has nothing to do with this red zone.
For better understanding, we replace A, B, and C with grandpa, father, and child, and replace events with apples.

The sequence for the last node is as follows:
Get the event: come in dispatchTouchEvent
Think Intercept: Come in onInterceptTouchEvent
Thinking result: onInterceptTouchEvent -> Return value Intercept true Do not intercept false
Think about whether to eat by yourself: come in onTouchEvent
The result is whether to eat or not: onTouchEvent -> return value eat true or not eat false
Not yet returned: dispatchTouchEvent-> return value not returned to dad true returned to dad false

Daddy Intercept

Do not eat

Eat

Conclusion of event distribution

1. If the event takes down as an example and is sent to a certain view, then dispatchTouchEvent must be advanced.
2. When a view has an event, it will enter onInterceptTouchEvent, making you think about whether to intercept it or not. The return value of this method is true for interception, and false for no interception.
3. If you don’t intercept, continue to deliver, and if you intercept, go directly to the onTouchEvent method.
4. OnTouchEvent think about whether you eat or not, if you eat, it is true (consumed), and if you don’t eat, it is false (not consumed).
5. If you don’t eat, you don’t consume, then the return value of your own dispatchTouchEvent is false by default, which means return it to your father for processing. If you eat it by yourself, then dispatchTouchEvent is true by default, which means that Dad should not deal with it, and I have already eaten it.

So onTouchEvent will come in two situations. One, I stop it myself, and then I can judge whether to eat or not. The child can’t even receive this event.
The other is that I sent it all the way to the children, but the children didn’t eat it, so I dealt with it, so I have to think about whether I can eat it myself.

Special Note:
1. In a complete event, that is, when the finger is pressed to the up (from down to up), after a certain view intercepts an event in the complete event, the follow-up actions are directly handed over to the view, no You will be asked whether to block or not. By default, you block directly and do not enter onInterceptTouchEvent, and go directly to onTouchEvent. (eg: The next time you press it, it will still be logical).
2. If the down event is not consumed until the outermost part is returned, there will be no subsequent move or up events. Since everyone doesn’t eat, there is no need to ask.
3. If a view has received a down event, but the father has intercepted other events, then the child will receive a cancel event. eg: When the child receives a down, the father intercepts the subsequent move, and after the father intercepts the move, a cancel event will be sent to the child, telling the child not to do it, and the father has taken over the entire event.

Implementation of complete code

Effect image

Code

package com.rengda.sigangapp

import android. content. Context
import android.util.AttributeSet
import android.util.Log
import android.view.MotionEvent
import android.view.ViewGroup
import android.widget.Scroller
import kotlin.math.abs


class SwipeView(context:Context,attrs:AttributeSet):ViewGroup(context,attrs) {
    private val scroller=Scroller(context);
    private var sendViewWidth=0;
    private var firstX="0".toFloat();//The position of the first contact
    private var isSendViewShow=false;
    private var newX="0".toFloat()


    private var lastX="0".toFloat();
    private var lastY="0".toFloat();
    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
         if (childCount==2){
            val view = getChildAt(1)
            sendViewWidth=view.measuredWidth;//Record the width of the button
            view.layout(r, t, r + view.measuredWidth, b) //The delete button is arranged after the first View
        }
        getChildAt(0).layout(l, t, r, b)//Set the position of the first control up, down, left, and right
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        measureChild(getChildAt(0),widthMeasureSpec, heightMeasureSpec)//Measure the height and width of the child, measure the first child first, because the height of the second child must follow the first child
        var width = MeasureSpec.getSize(widthMeasureSpec)//The width that the parent component can give
        val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)//How to set the width of the parent component
        var height = MeasureSpec.getSize(heightMeasureSpec)//The height that the parent component can give
        val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)//How to set the height of the parent component
        var resultWidth=0;
        var resultHeight=0;

        //actually only three types
        if(widthSpecMode==MeasureSpec.EXACTLY){//Indicates that the parent control gives the child view a specific value, and the child view should be set to the size of these values
            resultWidth=width //Listen to Dad
        }else if (widthSpecMode==MeasureSpec.UNSPECIFIED){//The parent component didn't tell you the limit, it's up to you
            resultWidth= getChildAt(0).measuredWidth //use the width of the first child
        }else if(widthSpecMode==MeasureSpec.AT_MOST){//Indicates the maximum specific value of the parent control and child view, and the child view cannot exceed the size of this value
            resultWidth=Math.min(getChildAt(0).measuredWidth,width) //See who is the smallest and listen to whom
        }
        //actually only three types
        if(heightSpecMode==MeasureSpec.EXACTLY){//Indicates that the parent control gives the child view a specific value, and the child view should be set to the size of these values
            resultHeight=height//Listen to Dad
        }else if (heightSpecMode==MeasureSpec.UNSPECIFIED){//The parent component didn't tell you the limit, it's up to you
            resultHeight= getChildAt(0).measuredHeight //Use the height of the first child
        }else if(heightSpecMode==MeasureSpec.AT_MOST){//Indicates the maximum specific value of the parent control and child view, and the child view cannot exceed the size of this value
            resultHeight=Math.min(getChildAt(0).measuredHeight,height) //See who is the smallest and listen to whom
        }

        setMeasuredDimension(resultWidth, resultHeight)

        //Measure the second The height of the second child has already been determined, so the height is inserted EXACTLY and the height of the father, and the width is still done by yourself, so pass in the width request of the grandfather, and the father does not care about your width
        measureChild(getChildAt(1),widthMeasureSpec, MeasureSpec.makeMeasureSpec(resultHeight,MeasureSpec.EXACTLY))//measure the height and width of the child

        


    }


    //Get this event, do I consume this event? If not, it will be returned to the parent and let the parent handle it
    override fun onTouchEvent(event: MotionEvent): Boolean {

        varconsum=true

        Log.d("SwipeView", "onTouchEvent: " + event.action)
        var x=event.x//x that came in this time
        when (event. action) {
            MotionEvent.ACTION_DOWN -> {//If the child doesn't want this event, he will come here. If the child doesn't want it, he has to consume it himself, otherwise the subsequent events are gone
                consume=true
             }
            MotionEvent.ACTION_MOVE -> {//This will come in multiple times
                var newX = firstX-x;//The position where the finger moves from the first touch point is actually the position where the content needs to be now (left is positive)
                Log.d("SwipeView", "ACTION_MOVE:isSendViewShow " + isSendViewShow)
                Log.d("SwipeView", "ACTION_MOVE:OFFSET " + newX)

                if (!isSendViewShow){//When the button is not displayed, sliding to the left is valid 0<=newX<=sendViewWidth
                    if (newX>sendViewWidth){ //The farthest can only slide the width of the second control
                        newX=sendViewWidth.toFloat()
                    }
                    if (newX<0){//invalid
                        newX="0".toFloat()
                    }
                }


                if (isSendViewShow){//The button has already shown that sliding to the right is the effective effective distance -sendViewWidth<=newX<=0
                    if (newX<-sendViewWidth){
                        newX=-sendViewWidth.toFloat()
                    }
                    if (newX>0){//invalid
                        newX="0".toFloat()
                    }

                    newX=newX + sendViewWidth
                }



                scrollTo(newX.toInt(), 0)//Because this method is to make the content offset from the original (that is, the first drawing) position, so the above newX calculates the offset from the initial
                Log.d("SwipeView", "ACTION_MOVE: newX" + newX)

                consume=true
            }

            MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                if (isSendViewShow){//Originally there was a button
                    if (scrollX <((sendViewWidth/5)*4)) {//It means that you really want to close
                        scrollTo(0, 0)
                        isSendViewShow=false
                    } else {
                        scrollTo(sendViewWidth, 0)
                        isSendViewShow=true
                    }

                }else{//No button displayed
                    if (scrollX >= sendViewWidth/5) {// means really want to open
                        scrollTo(sendViewWidth, 0)
                        isSendViewShow=true
                    } else {//Otherwise do not display
                        scrollTo(0, 0)
                        isSendViewShow=false
                    }
                }

                Log.d("SwipeView", "ACTION_UP: isSendViewShow " + isSendViewShow)

              }
            else -> {consum=false}
        }
        return consumption
    }




    //Do external interception-whether to intercept this event (true to intercept, false not to intercept) Because the child has set the click event, if all the events are sent directly, they will be consumed by the child, and they will not come back to use the father's event. So I have to intercept the ones I use first, and then send the others to my children.
    override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action){
            MotionEvent.ACTION_DOWN -> {//Because the child needs at least one down to trigger the click, so send it first. When you intercept one, the subsequent onInterceptTouchEvent will not be called again
                //Record the X when pressed
                firstX=ev.x;//record

                newX="0".toFloat()
                return false;
            }
            MotionEvent. ACTION_MOVE -> {
                return true;
            }
            else->{return false}
        }

    }



    //Internal interception is to control dad
    override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
        when (ev?.action){
            MotionEvent.ACTION_DOWN -> {//Let Dad not intercept
                lastX=ev.x;
                lastY=ev.y;
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            MotionEvent. ACTION_MOVE -> {
                if (abs(ev.x-lastX)> abs(ev.y-lastY)){//Indicates that the distance of x sliding is greater than the distance of Y, indicating that it is sliding left and right, then dad is not allowed to intercept
                    getParent().requestDisallowInterceptTouchEvent(true);
                }else{//Allow Dad to intercept
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
            }
            MotionEvent.ACTION_UP ->{//Let Dad not intercept
                getParent().requestDisallowInterceptTouchEvent(true);
            }
            else->{return false}
        }

        return super. dispatchTouchEvent(ev)
    }
}