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() }