Drag and drop View floating on the page

1, Overview

In the twinkling of an eye, it is 2022. Looking back, the first time I wrote on csdn was 2017, five years have passed. In the past five years, some people around them got married, some had children, some bought a house, some upgraded to management and no longer wrote code. And I have nothing to do with these. I'm still a dish. I just changed from a small dish to an old dish. If I don't become famous, I won't be famous.
Since it's already like this, more words and feelings can't be changed. It's better to do a good job under your hand!

Last year, I wrote a View floating on the page and dragging with my finger in the project. It needs to be used on many pages, so it is encapsulated. I feel it is still necessary to record it for my reuse in the future. I also hope it can provide a reference for you with similar needs. Of course, it is not difficult to achieve.

The effect is as follows:
1. Floating on the page, you can drag with your fingers;
2. After the finger is released, slowly translate to the nearest page edge;
3. When the page slides up, it will shrink to the edge of the page; When sliding, it will stretch out.
See the renderings:

2, Code implementation

FloatDragView is not an inherited View. The floatView to be displayed is passed in through the constructor. It encapsulates the implementation logic:
1. First, add the floatView to the parent View, set layoutParams according to the type of the parent View, and place it in the lower right corner;
2. Set OnTouchListener for floatView. When the finger moves, calculate the displacement of x and y. after checking the boundary, change translationX and translationY to realize the movement (offsetLeftAndRight and offsetTopAndBottom were originally used, but some problems were found); When the finger is released, the horizontal value closest to the screen is calculated and moved to the edge through animation;
3. Monitor the slidable view and move to the corresponding position during sliding to achieve the effect of retraction and extension.

The following code is:

class FloatDragView(private var floatView: View?) {

    private val touchSlop by lazy(LazyThreadSafetyMode.NONE) {
        val context = floatView?.context
        if (context != null)
            ViewConfiguration.get(context).scaledTouchSlop
        else
            0
    }

    private var parentView: ViewGroup? = null//Parent View of floatView
    private var scrollView: View? = null//Slidable view, which folds and pops up the floatView according to its sliding
    private var clickAction: ((View) -> Unit)? = null
    private var isAnimatorRunning = false//Are you performing animation
    private var isCollapse = false//Is it folded
    private var collapseWidth = 0f//Width of the fold
    private var rightMargin = 0//Left margin

    private var isDragStatus = false//Is floatView in drag state
    private var isOnTheRight = true//Whether the floatView is on the right, true on the right and false on the left

    /**
     * Add floatView to parent view
     * parentView: Parent view
     * canScrollView: A slidable view, which can be slid up and down to stow and expand the floatView. When it is empty, it will not have the function of stowing and expanding
     * clickAction: floatView Click event
     */
    fun attach(
        parentView: ViewGroup, canScrollView: View? = null,
        clickAction: ((View) -> Unit)? = null
    ) {
        this.scrollView = canScrollView
        this.parentView = parentView
        this.clickAction = clickAction
        addFloatView()
    }

    /**
     * Remove floatView
     */
    fun removeFloatView() {
        parentView?.removeView(floatView)
        floatView = null
    }

    /**
     * Set the visibility of the floatView
     */
    fun setVisibility(visibility: Int) {
        floatView?.visibility = visibility
    }

    /**
     * Execute the operation of adding floatView and set related listening
     */
    private fun addFloatView() {
        val contentView = parentView ?: return
        val floatView = floatView ?: return
        if (floatView.parent != null) {
            contentView.removeView(floatView)
        }
        val context = contentView.context
        floatView.setOnClickListener {
            if (isCollapse) {
                floatOut()
            } else {
                clickAction?.invoke(it)
            }
        }
        setupDrag(floatView)
        val resource = context.resources
        val width = resource.getDimensionPixelSize(R.dimen.dp_56)
        val height = resource.getDimensionPixelSize(R.dimen.dp_44)
        rightMargin = resource.getDimensionPixelSize(R.dimen.dp_8)
        val bMargin = resource.getDimensionPixelSize(R.dimen.dp_70)
        collapseWidth = rightMargin + width * 0.6f
        val params = when (contentView) {
            is FrameLayout -> {
                FrameLayout.LayoutParams(width, height).apply {
                    gravity = Gravity.BOTTOM or Gravity.END
                }
            }
            is RelativeLayout -> {
                RelativeLayout.LayoutParams(width, height).apply {
                    addRule(RelativeLayout.ALIGN_PARENT_BOTTOM)
                    addRule(RelativeLayout.ALIGN_PARENT_RIGHT)
                }
            }
            is ConstraintLayout -> {
                ConstraintLayout.LayoutParams(width, height).apply {
                    endToEnd = ConstraintLayout.LayoutParams.PARENT_ID
                    bottomToBottom = ConstraintLayout.LayoutParams.PARENT_ID
                }
            }
            else -> {
                ViewGroup.MarginLayoutParams(width, height)
            }
        }
        params.rightMargin = rightMargin
        params.bottomMargin = bMargin
        contentView.addView(floatView, params)
        val canScrollView = scrollView ?: return
        when (canScrollView) {
            is ScrollView -> {
                setOnTouchListener(canScrollView)
            }
            is NestedScrollView -> {
                setOnScrollChangeListener(canScrollView)
            }
            is RecyclerView -> {
                addOnScrollListener(canScrollView)
            }
            is AppBarLayout -> {
                addOnOffsetChangedListener(canScrollView)
            }
        }
    }

    /**
     * Set the touch event of floatView to follow the finger
     */
    @SuppressLint("ClickableViewAccessibility")
    private fun setupDrag(view: View) {
        view.setOnTouchListener(object : View.OnTouchListener {
            private var lastX = 0f
            private var lastY = 0f
            override fun onTouch(v: View, event: MotionEvent): Boolean {
                when (event.action) {
                    MotionEvent.ACTION_DOWN -> {
                        lastX = event.x
                        lastY = event.y
                    }
                    MotionEvent.ACTION_MOVE -> {
                        if (isCollapse) return false
                        val width = parentView?.width ?: return false
                        val height = parentView?.height ?: return false
                        val x = event.x
                        val y = event.y
                        if (!isDragStatus) {
                            if (abs(x - lastX) >= touchSlop || abs(y - lastY) >= touchSlop) {
                                isDragStatus = true
                            } else {
                                return false
                            }
                        }
                        var tX = v.translationX + (x - lastX)
                        var tY = v.translationY + (y - lastY)
                        //Check whether it exceeds the boundary and correct it.
                        //After the view executes translationX and translationY, the top,left, bottom and right will not change, so tX and tY should be added to check the boundary.
                        if ((v.top + tY) < 0) {//top
                            tY += 0 - (v.top + tY)
                        }
                        if ((v.left + tX) < 0) {//left
                            tX += 0 - (v.left + tX)
                        }
                        if ((v.bottom + tY) > height) {//bottom
                            tY -= (v.bottom + tY) - height
                        }
                        if ((v.right + tX) > width) {//right
                            tX -= (v.right + tX) - width
                        }
                        v.translationX = tX
                        v.translationY = tY
                        return true
                    }
                    MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
                        if (isDragStatus) {
                            isDragStatus = false
                            val tx = v.translationX
                            val parentCenterX = (parentView?.width ?: return false) / 2
                            val myCenterX = v.left + tx + v.width / 2
                            if (myCenterX > parentCenterX) {
                                isOnTheRight = true
                                startAnimator(tx, 0f)
                            } else {
                                isOnTheRight = false
                                val to = tx - (v.left + tx) + rightMargin
                                startAnimator(tx, to)
                            }
                            return true
                        }
                    }
                }
                return false
            }
        })
    }

    /**
     * Monitor the gesture and judge whether the view slides up and down to stow and expand the floatView
     */
    @SuppressLint("ClickableViewAccessibility")
    private fun setOnTouchListener(view: View) {
        view.setOnTouchListener(object : View.OnTouchListener {
            var lastScrollY = 0
            override fun onTouch(v: View, event: MotionEvent): Boolean {
                if (event.action == MotionEvent.ACTION_MOVE) {
                    val scrollY = view.scrollY
                    val dy = scrollY - lastScrollY
                    if (!isCollapse && dy >= touchSlop) {
                        collapse()
                    } else if (isCollapse && dy <= -touchSlop) {
                        floatOut()
                    }
                    lastScrollY = view.scrollY
                }
                return false
            }
        })
    }

    /**
     * Listen to the sliding of nestedScrollView to stow and expand floatView
     */
    private fun setOnScrollChangeListener(nestedScrollView: NestedScrollView) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //setOnScrollChangeListener(View.OnScrollChangeListener) will be called regardless of whether there is a RecyclerView in NestedScrollView
            nestedScrollView.setOnScrollChangeListener(View.OnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
                val dy = scrollY - oldScrollY
                if (!isCollapse && dy >= touchSlop) {
                    collapse()
                } else if (isCollapse && dy <= -touchSlop) {
                    floatOut()
                }
            })
        } else {
            //If there is no recyclerview in NestedScrollView, setontouchlistener will be called, and setonscrollchangelistener (onscrollchangelistener of NestedScrollView) will not be called;
            //If there is recyclerview in NestedScrollView, setonscrollchangelistener will be called, but setOnTouchListener will not be called.
            setOnTouchListener(nestedScrollView)
            nestedScrollView.setOnScrollChangeListener(NestedScrollView.OnScrollChangeListener { v, scrollX, scrollY, oldScrollX, oldScrollY ->
                val dy = scrollY - oldScrollY
                if (!isCollapse && dy >= touchSlop) {
                    collapse()
                } else if (isCollapse && dy <= -touchSlop) {
                    floatOut()
                }
            })
        }
    }

    /**
     * Listen to the slide of recyclerView to stow and expand floatView
     */
    private fun addOnScrollListener(recyclerView: RecyclerView) {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                if (!isCollapse && dy >= touchSlop) {
                    collapse()
                } else if (isCollapse && dy <= -touchSlop) {
                    floatOut()
                }
            }
        })
    }

    /**
     * Listen for changes in appBarLayout to collapse and expand floatView
     */
    private fun addOnOffsetChangedListener(appBarLayout: AppBarLayout) {
        appBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener {
            var lastOffset = 0
            override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
                if (lastOffset - verticalOffset > touchSlop / 2) {
                    collapse()
                } else if (verticalOffset - lastOffset > touchSlop / 2) {
                    floatOut()
                }
                lastOffset = verticalOffset
            }
        })
    }

    //Put away
    private fun collapse() {
        if (isDragStatus || isCollapse) return
        isCollapse = true
        if (isOnTheRight) {//On the right, fold to the right
            startAnimator(0f, collapseWidth)
        } else {//On the left, fold to the left
            val tx = floatView?.translationX ?: return
            startAnimator(tx, tx - collapseWidth)
        }
    }

    //eject
    private fun floatOut() {
        if (isDragStatus || !isCollapse) return
        isCollapse = false
        if (isOnTheRight) {//On the right, pop up to the left
            startAnimator(collapseWidth, 0f)
        } else {//On the left, pop up to the right
            val tx = floatView?.translationX ?: return
            startAnimator(tx, tx + collapseWidth)
        }
    }

    /**
     * Execute the animation and slowly pan to a certain position
     */
    private fun startAnimator(form: Float, to: Float) {
        if (isAnimatorRunning) return
        isAnimatorRunning = true
        val view = floatView ?: return
        val animator = ObjectAnimator.ofFloat(view, "translationX", form, to)
        animator.duration = 180
        animator.addListener(object : AnimatorListenerAdapter() {
            override fun onAnimationEnd(animation: Animator?) {
                isAnimatorRunning = false
            }
        })
        animator.start()
    }
}

3, Use

Create a FloatDragView object and pass in a view, which is what is displayed;
To call the attach method, three parameters need to be passed in. One is the parent View (FrameLayout, RelativeLayout and ConstraintLayout are supported. If the passed in parent View is LinearLayout, it cannot float on the page); The second parameter is a slidable View (ScrollView, NestedScrollView, RecyclerView and appbarlayout are supported). After passing in, it will be retracted and extended according to its up and down sliding. Can not pass, there will be no such effect; The third parameter is a lambda, which is the callback of the click event.

val contentView = findViewById<ViewGroup>(R.id.contentView)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerView)
recyclerView.setHasFixedSize(true)
recyclerView.itemAnimator = null
recyclerView.layoutManager = LinearLayoutManager(this)
recyclerView.adapter = RvAdapter()
val iv = ImageView(this)
iv.setImageResource(R.mipmap.red_packget)
FloatDragView(iv).attach(contentView, recyclerView) {
    Toast.makeText(this, "click", Toast.LENGTH_SHORT).show()
}

Keywords: Android kotlin

Added by 7khat on Sun, 23 Jan 2022 12:14:39 +0200