Android custom View to achieve drag adsorption effect

1. Foreword:

picture.png

Effect:

aaa.gif

2. Analysis

1. Idea

1. Since the control dragging is to be realized, the onTouchEvent() method is inseparable, and the press and slide events in it need to be monitored.
2. To realize automatic bordering, it is necessary to monitor the event that the finger leaves the screen in onTouchEvent(). For the process of bonding, we use attribute animation to solve it.
3. The conflict of events also needs to be considered. Dragging and clicking are related to the interception of events.

2. Key points

1. Pay attention to the response to the event
2. Calculation of sliding boundaries

3. Java complete code:

/**
 * Customize View to achieve dragging and automatic edge suction effect
 * <p>
 * Handle swipe and snap {@link #onTouchEvent(MotionEvent)}
 * Handle event dispatch {@link #dispatchTouchEvent(MotionEvent)}
 *</p>
 *
 * @attr customIsAttach //Whether automatic edge suction is required
 * @attr customIsDrag //Whether it can be dragged
 */
public class AttachButton extends View {
    private float mLastRawX;
    private float mLastRawY;
    private final String TAG = "AttachButton";
    private boolean isDrug = false;
    private int mRootMeasuredWidth = 0;
    private int mRootMeasuredHeight = 0;
    private int mRootTopY = 0;
    private boolean customIsAttach;
    private boolean customIsDrag;

    public AttachButton(Context context) {
        this(context, null);
    }

    public AttachButton(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public AttachButton(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        setClickable(true);
        initAttrs(context, attrs);
    }

    /**
     * Initialize custom properties
     */
    private void initAttrs(Context context, AttributeSet attrs) {
        TypedArray mTypedAttay = context.obtainStyledAttributes(attrs, R.styleable.AttachButton);
        customIsAttach = mTypedAttay.getBoolean(R.styleable.AttachButton_customIsAttach, true);
        customIsDrag = mTypedAttay.getBoolean(R.styleable.AttachButton_customIsDrag, true);
        mTypedAttay. recycle();
    }

    @Override
    public boolean dispatchTouchEvent(MotionEvent event) {
        super.dispatchTouchEvent(event);
        return true;
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        //Determine whether to slide
        if (customIsDrag) {
            //The coordinates of the current finger
            float mRawX = ev. getRawX();
            float mRawY = ev. getRawY();
            switch (ev. getAction()) {
                case MotionEvent.ACTION_DOWN://finger press
                    isDrug = false;
                    //Record the pressed position
                    mLastRawX = mRawX;
                    mLastRawY = mRawY;
                    ViewGroup mViewGroup = (ViewGroup) getParent();
                    if (mViewGroup != null) {
                        int[] location = new int[2];
                        mViewGroup.getLocationInWindow(location);
                        // Get the height of the parent layout
                        mRootMeasuredHeight = mViewGroup.getMeasuredHeight();
                        mRootMeasuredWidth = mViewGroup.getMeasuredWidth();
                        / / Get the coordinates of the parent layout vertex
                        mRootTopY = location[1];
                    }
                    break;
                case MotionEvent.ACTION_MOVE://finger slide
                    if (mRawX >= 0 & amp; & amp; mRawX <= mRootMeasuredWidth & amp; & amp; mRawY >= mRootTopY & amp; & amp; mRawY <= (mRootMeasuredHeight + mRootTopY)) {
                        //finger X-axis sliding distance
                        float differenceValueX = mRawX - mLastRawX;
                        //Finger Y-axis sliding distance
                        float differenceValueY = mRawY - mLastRawY;
                        //Determine whether it is a drag operation
                        if (!isDrug) {
                            if (Math. sqrt(differenceValueX * differenceValueX + differenceValueY * differenceValueY) < 2) {
                                isDrug = false;
                            } else {
                                isDrug = true;
                            }
                        }
                        //Get the distance between the distance pressed by the finger and the X-axis of the control itself
                        float ownX = getX();
                        //Get the distance between the distance pressed by the finger and the Y axis of the control itself
                        float ownY = getY();
                        //The distance dragged by the X axis in theory
                        float endX = ownX + differenceValueX;
                        //The distance dragged by the Y axis in theory
                        float endY = ownY + differenceValueY;
                        //The maximum distance that the X axis can drag
                        float maxX = mRootMeasuredWidth - getWidth();
                        //The maximum distance that the Y axis can drag
                        float maxY = mRootMeasuredHeight - getHeight();
                        //X-axis boundary limit
                        endX = endX < 0 ? 0 : endX > maxX ? maxX : endX;
                        //Y-axis boundary limit
                        endY = endY < 0 ? 0 : endY > maxY ? maxY : endY;
                        // start moving
                        setX(endX);
                        setY(endY);
                        //record location
                        mLastRawX = mRawX;
                        mLastRawY = mRawY;
                    }

                    break;
                case MotionEvent.ACTION_UP://finger leave
                    //Determine whether we need to paste according to the custom attribute
                    if (customIsAttach) {
                        //Determine whether it is a click event
                        if (isDrug) {
                            float center = mRootMeasuredWidth / 2;
                            //Automatic border
                            if (mLastRawX <= center) {
                                //paste to the left
                                AttachButton. this. animate()
                                        .setInterpolator(new BounceInterpolator())
                                        .setDuration(500)
                                        .x(0)
                                        .start();
                            } else {
                                // stick to the right
                                AttachButton. this. animate()
                                        .setInterpolator(new BounceInterpolator())
                                        .setDuration(500)
                                        .x(mRootMeasuredWidth - getWidth())
                                        .start();
                            }
                        }
                    }
                    break;
            }
        }
        //Whether to intercept the event
        return isDrug ? isDrug : super.onTouchEvent(ev);
    }
}

4. Kotlin complete code:

class AttachButton: View {
    private var mLastRawX: Float = 0F
    private var mLastRawY: Float = 0F
    private var isDrug = false
    private var mRootMeasuredWidth = 0
    private var mRootMeasuredHeight = 0
    private var mRootTopY = 0
    private var customIsAttach = false
    private var customIsDrag = false

    constructor(context: Context) : this(context, null)
    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
        context,
        attrs,
        defStyleAttr
    ) {
        isClickable = true
        initAttrs(context, attrs)
    }

    private fun initAttrs(context: Context, attrs: AttributeSet?) {
        attrs?.let {
            val mTypedAttay = context.obtainStyledAttributes(it, R.styleable.AttachButton)
            customIsAttach =
                mTypedAttay.getBoolean(R.styleable.AttachButton_customIsAttach, true)
            customIsDrag =
                mTypedAttay.getBoolean(R.styleable.AttachButton_customIsDrag, true)
            mTypedAttay. recycle()
        }
    }

    override fun dispatchTouchEvent(event: MotionEvent?): Boolean {
        super. dispatchTouchEvent(event)
        return true
    }

    override fun onTouchEvent(event: MotionEvent?): Boolean {
        event?.let {
            //Determine whether to slide
            if (customIsDrag) {
                //The coordinates of the current finger
                val mRawX = it.rawX
                val mRawY = it.rawY
                when (it. action) {
                    MotionEvent.ACTION_DOWN -> {//finger press
                        isDrug = false
                        //Record the pressed position
                        mLastRawX = mRawX
                        mLastRawY = mRawY
                        if (parent is ViewGroup) {
                            val mViewGroup = parent as ViewGroup
                            val location = IntArray(2)
                            mViewGroup. getLocationInWindow(location)
                            // Get the height of the parent layout
                            mRootMeasuredHeight = mViewGroup.measuredHeight
                            mRootMeasuredWidth = mViewGroup.measuredWidth
                            / / Get the coordinates of the parent layout vertex
                            mRootTopY = location[1]
                        }
                    }
                    MotionEvent.ACTION_MOVE -> {//finger slide
                        if (mRawX >= 0 & amp; & amp; mRawX <= mRootMeasuredWidth & amp; & amp; mRawY >= mRootTopY & amp; & amp; mRawY <= (mRootMeasuredHeight + mRootTopY)) {
                            //finger X-axis sliding distance
                            val differenceValueX: Float = mRawX - mLastRawX
                            //Finger Y-axis sliding distance
                            val differenceValueY: Float = mRawY - mLastRawY
                            //Determine whether it is a drag operation
                            if (!isDrug) {
                                isDrug =
                                    sqrt(((differenceValueX * differenceValueX) + (differenceValueY * differenceValueY)).toDouble()) >= 2
                            }
                            //Get the distance between the distance pressed by the finger and the X-axis of the control itself
                            val ownX = x
                            //Get the distance between the distance pressed by the finger and the Y axis of the control itself
                            val ownY = y
                            //The distance dragged by the X axis in theory
                            var endX: Float = ownX + differenceValueX
                            //The distance dragged by the Y axis in theory
                            var endY: Float = ownY + differenceValueY
                            //The maximum distance that the X axis can drag
                            val maxX: Float = mRootMeasuredWidth - width.toFloat()
                            //The maximum distance that the Y axis can drag
                            val maxY: Float = mRootMeasuredHeight - height.toFloat()
                            //X-axis boundary limit
                            endX = if (endX < 0) 0F else (if (endX > maxX) maxX else endX)
                            //Y-axis boundary limit
                            endY = if (endY < 0) 0F else (if (endY > maxY) maxY else endY)
                            // start moving
                            x = endX
                            y = endY
                            //record location
                            mLastRawX = mRawX
                            mLastRawY = mRawY
                        }
                    }

                    MotionEvent.ACTION_UP -> {//finger leave
                        if (customIsAttach) {
                            //Determine whether it is a click event
                            if (isDrug) {
                                val center = mRootMeasuredWidth / 2
                                //Automatic border
                                if (mLastRawX <= center) {
                                    //paste to the left
                                    animate()
                                        .setInterpolator(BounceInterpolator())
                                        .setDuration(500)
                                        .x(0F)
                                        .start()
                                } else {
                                    // stick to the right
                                    animate()
                                        .setInterpolator(BounceInterpolator())
                                        .setDuration(500)
                                        .x(mRootMeasuredWidth - width.toFloat())
                                        .start()
                                }
                            }
                        }
                    }
                }
            }
        }
        //Whether to intercept the event
        return if (isDrug) isDrug else super.onTouchEvent(event)
    }
}

4. Simple drag

import android. content. Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;

public class AdsorbentViews extends ImageView {
 
 private int maxWidth;
  private int maxHeight;
  private int viewWidth;
  private int viewHeight;
  private float downx;
  private float downy;
  private Context mContext;
  public CustomViews(Context context) {
    this(context, null);
  }
 
  public CustomViews(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
  }
 
  public CustomViews(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    mContext = context;
  }
  @Override
  protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    DisplayMetrics outMetrics = new DisplayMetrics();
    WindowManager windowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
    windowManager.getDefaultDisplay().getRealMetrics(outMetrics);
    //screen width
    maxWidth = outMetrics. widthPixels;
    // the height of the screen
    maxHeight = outMetrics. heightPixels;
    /**
     * Width and height of the control
     */
    viewWidth = canvas. getWidth();
    viewHeight = canvas. getHeight();
  }
  @Override
  public boolean onTouchEvent(MotionEvent event) {
    switch (event. getAction()) {
      case MotionEvent. ACTION_DOWN:
        clearAnimation();
        downx = event. getX();
        downy = event. getY();
        return true;
      case MotionEvent. ACTION_MOVE:
        float moveX = event.getRawX() - downx;
        float moveY = event.getRawY() - downy;
        moveX = moveX < 0 ? 0 : (moveX + viewWidth > maxWidth) ? (maxWidth - viewWidth) : moveX;
        moveY = moveY < 0 ? 0 : (moveY + viewHeight) > maxHeight ? (maxHeight - viewHeight) : moveY;
        this.setY(moveY);
        this.setX(moveX);
        return true;
      case MotionEvent. ACTION_UP:
        // do adsorption effect
        float centerX = getX() + viewWidth / 2;
        if (centerX > maxWidth/2){
          // snap to the right
          animate().setInterpolator(new DecelerateInterpolator())
              .setDuration(500)
              .x(maxWidth-viewWidth)
              .y(maxHeight-viewHeight)
              .start();
        } else {
          animate().setInterpolator(new DecelerateInterpolator())
              .setDuration(500)
              .x(0)
              .y(maxHeight-viewHeight)
              .start();
        }
        return true;
      default:
        return super.onTouchEvent(event);
    }
  }
}

Last

If you want to become an architect or want to break through the 20-30K salary range, then don’t be limited to coding and business, but you must be able to select models, expand, and improve programming thinking. In addition, a good career plan is also very important, and the habit of learning is very important, but the most important thing is to be able to persevere. Any plan that cannot be implemented consistently is empty talk.

If you have no direction, here I would like to share with you a set of “Advanced Notes on the Eight Major Modules of Android” written by the senior architect of Ali, to help you organize the messy, scattered and fragmented knowledge systematically, so that you can systematically and efficiently Master the various knowledge points of Android development.

Compared with the fragmented content we usually read, the knowledge points of this note are more systematic, easier to understand and remember, and are arranged strictly according to the knowledge system.

Full set of video materials:

1. Interview Collection


2. Collection of source code analysis

3. Collection of open source frameworks

Welcome everyone to support with one click and three links. If you need the information in the article, you can directly scan the CSDN official certification WeChat card at the end of the article to get it for free↓↓↓