0
点赞
收藏
分享

微信扫一扫

浮在页面上可拖拽的View

单调先生 2022-01-23 阅读 58

一、概述

转眼到2022年了,回想第一次在csdn上写东西是2017年,距现在已过去5年。这5年之间,身边的人有的结婚了,有的生小孩了,有的买房了,有的升级做管理不再写代码了。而我,与这些都没有关系,还是一个菜,只不过从一个小菜变成了一个老菜,功不成名不就。
既然已经是这样,多说和感慨也无法改变,不如做好手下的事吧!

去年在项目中写过一个浮在页面上随手指拖动的View,因为需要在很多页面用到,所以封装了一下,感觉还是有点必要记录一下,方便自己日后再次使用,也希望能给有类似需求的你提供参考,当然这并不是很难实现。

效果是这样:
1、浮在页面上,可以随手指拖动;
2、手指释放后,缓慢平移到距离最近的页面边缘;
3、页面上滑时,会缩到页面边缘里面;下滑时,会伸出来。
看效果图:
在这里插入图片描述

二、代码实现

FloatDragView不是继承View,要显示的floatView通过构造方法中传入。里面封装了实现逻辑:
1、先将floatView添加到父View中,根据父View的类型,设置layoutParams,放置在右下角;
2、给floatView设置OnTouchListener,手指移动时,计算出x、y的位移,再检查边界后,改变translationX、translationY实现移动(原本使用offsetLeftAndRight、offsetTopAndBottom,但发现有些许问题);手指释放时,计算距离屏幕最近的水平值,通过动画移动到边缘;
3、监听可滑动的view,滑动时移动到相应位置,达到缩起和伸出效果。

下面上代码:

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//floatView的父View
    private var scrollView: View? = null//可滑动的view,根据他的滑动来折叠和弹出floatView
    private var clickAction: ((View) -> Unit)? = null
    private var isAnimatorRunning = false//是否在执行动画
    private var isCollapse = false//是否已折叠
    private var collapseWidth = 0f//折叠的宽度
    private var rightMargin = 0//左边margin

    private var isDragStatus = false//floatView是否是拖动状态
    private var isOnTheRight = true//floatView是否在右边,true在右边,false在左边

    /**
     * 将floatView加到父view中
     * parentView: 父view
     * canScrollView: 可滑动的view,根据它上下滑动收起和展开floatView。为空时不会有收起展开功能
     * clickAction: floatView点击事件
     */
    fun attach(
        parentView: ViewGroup, canScrollView: View? = null,
        clickAction: ((View) -> Unit)? = null
    ) {
        this.scrollView = canScrollView
        this.parentView = parentView
        this.clickAction = clickAction
        addFloatView()
    }

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

    /**
     * 设置floatView的可见性
     */
    fun setVisibility(visibility: Int) {
        floatView?.visibility = visibility
    }

    /**
     * 执行添加floatView操作,设置相关监听
     */
    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)
            }
        }
    }

    /**
     * 设置floatView的触摸事件,跟随手指移动
     */
    @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)
                        //检查是否超过边界,超过就校正。
                        //view执行过translationX和translationY后top,left,bottom,right不会发生变化,因此要加上tX,tY来检查边界。
                        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
            }
        })
    }

    /**
     * 监听手势,判断view是否上下滑动,来收起和展开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
            }
        })
    }

    /**
     * 监听nestedScrollView的滑动,来收起和展开floatView
     */
    private fun setOnScrollChangeListener(nestedScrollView: NestedScrollView) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            //不管NestedScrollView里面有没有RecyclerView,setOnScrollChangeListener(View.OnScrollChangeListener)都会被调用
            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 {
            //如果NestedScrollView里面没有RecyclerView, setOnTouchListener会被调用,setOnScrollChangeListener(NestedScrollView的OnScrollChangeListener)不会被调用;
            //如果NestedScrollView里面有RecyclerView, setOnScrollChangeListener会被调用,setOnTouchListener却不会被调用。
            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()
                }
            })
        }
    }

    /**
     * 监听recyclerView的滑动,来收起和展开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()
                }
            }
        })
    }

    /**
     * 监听appBarLayout的变化,来收起和展开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
            }
        })
    }

    //收起
    private fun collapse() {
        if (isDragStatus || isCollapse) return
        isCollapse = true
        if (isOnTheRight) {//在右边,往右边折叠
            startAnimator(0f, collapseWidth)
        } else {//在左边,往左边折叠
            val tx = floatView?.translationX ?: return
            startAnimator(tx, tx - collapseWidth)
        }
    }

    //弹出
    private fun floatOut() {
        if (isDragStatus || !isCollapse) return
        isCollapse = false
        if (isOnTheRight) {//在右边,往左边方向弹出
            startAnimator(collapseWidth, 0f)
        } else {//在左边,往右边方向弹出
            val tx = floatView?.translationX ?: return
            startAnimator(tx, tx + collapseWidth)
        }
    }

    /**
     * 执行动画,缓慢的平移到某个位置
     */
    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()
    }
}

三、使用

创建FloatDragView对象,传入一个view,这个view是显示出来的东西;
调用attach方法,需要传入三个参数,一个是父View(支持FrameLayout、RelativeLayout、ConstraintLayout,如果传入的父View是LinearLayout那是没办法浮在页面上面的);第二个参数是可滑动的View(支持ScrollView、NestedScrollView、RecyclerView、AppBarLayout),传入后会根据它的上下滑动进行缩起和伸出。可以不传,便没有这个效果;第三个参数是个lambda,是点击事件的回调。

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

相关推荐

0 条评论