自定义View-可拖拽的小球

850 阅读3分钟

使用场景及效果

自定义一个可拖拽的View,常见于游戏相关的场景,例如现在一些小众品牌游戏助手的"一键连招"功能。我们用100行代码实现一个可拖动的View,且拖动结束后固定View的位置。

效果如下

小球正常.gif

实现可拖拽的小球

需求是当我们拖拽时小球跟手运动,当我们抬手时小球停在抬手的位置,如果我们刷新当前页面(例如页面中某View从GONE到VISIBLE)小球仍然停留在抬手的位置。

step1 实现可拖拽的小球,

代码很简单,全文如下

class FloatDragView : FrameLayout, View.OnTouchListener {

    private var mViewWidth = 0
    private var mViewHeight = 0
    private var mDownX = 0F
    private var mDownY = 0F
    private var isMove = false

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(
        context,
        attributeSet,
        defStyle
    ) {
        initView()
    }

    private fun initView() {
        val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        layoutParams = lp

        val childView = getChildView()
        addView(childView)
        setOnTouchListener(this)

        post {
            // 获取一下view宽高备用
            mViewWidth = this.width
            mViewHeight = this.height
        }
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mDownX = event.x
                mDownY = event.y
            }

            MotionEvent.ACTION_MOVE -> {
                isMove = true
                //上下偏移,传入值>0 向下偏移,传入值<0向上偏移,会触发重绘
                offsetTopAndBottom((y - mDownY).toInt())
                offsetLeftAndRight((x - mDownX).toInt())
            }

            MotionEvent.ACTION_UP -> {
                if (isMove) {
                    //do something
                }
                isMove = false
            }
        }
        return true
    }

    private fun getChildView(): View {
        val imageView = ShapeableImageView(context)
        imageView.setImageResource(R.drawable.icon_float)
        imageView.post {
            val radius = imageView.width / 2f
            imageView.shapeAppearanceModel =
                ShapeAppearanceModel().toBuilder().setAllCornerSizes(radius).build()
        }
        return imageView
    }
}

于是我们可以看到如下效果

小球归位.gif

图怎么这么大:(

拖拽跟手实现了,可为啥一点击刷新(按钮下方的TextView从GONE变VISIBLE)小球又回到了初始位置?因为我们只是调用API offsetTopAndBottom移动了小球的位置,小球实际的LayoutParams参数没变,所以View树一刷新小球又回到初始位置了。

所以需要记录小球初始的位置坐标(绝对坐标or相对坐标都可以,我记录绝对坐标) -> 然后在抬手后获取当前坐标,计算出小球移动的距离 -> 回调出小球移动的距离 -> 业务层根据小球移动的距离固定小球的当前位置。

step2:计算回调小球移动的距离,直接上小球完整代码

class FloatDragView : FrameLayout, View.OnTouchListener {

    private var mViewWidth = 0
    private var mViewHeight = 0
    private var mDownX = 0F
    private var mDownY = 0F
    private var isMove = false

    private var mStartX = 0
    private var mStartY = 0

    var mLocationListener: LocationListener? = null

    constructor(context: Context) : this(context, null)

    constructor(context: Context, attributeSet: AttributeSet?) : this(context, attributeSet, 0)

    constructor(context: Context, attributeSet: AttributeSet?, defStyle: Int) : super(
        context,
        attributeSet,
        defStyle
    ) {
        initView()
    }

    private fun initView() {
        val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
        layoutParams = lp

        val childView = getChildView()
        addView(childView)
        setOnTouchListener(this)

        post {
            // 获取一下view宽高备用
            mViewWidth = this.width
            mViewHeight = this.height
            // 获取初始绝对坐标
            initStartLocation()
        }
    }

    private fun initStartLocation() {
        val locations = getAbsLocation()
        mStartX = locations[0]
        mStartY = locations[1]
    }

    override fun onTouch(v: View, event: MotionEvent): Boolean {
        val x = event.x
        val y = event.y
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                mDownX = event.x
                mDownY = event.y
            }

            MotionEvent.ACTION_MOVE -> {
                isMove = true
                //上下偏移,传入值>0 向下偏移,传入值<0向上偏移,会触发重绘
                offsetTopAndBottom((y - mDownY).toInt())
                offsetLeftAndRight((x - mDownX).toInt())
            }

            MotionEvent.ACTION_UP -> {
                if (isMove) {
                    //do something
                }
                isMove = false
                val locations = getAbsLocation()
                mLocationListener?.onLocation(
                    locations[0] - mStartX,
                    locations[1] - mStartY
                )
            }
        }
        return true
    }

    private fun getChildView(): View {
        val imageView = ShapeableImageView(context)
        imageView.setImageResource(R.drawable.icon_float)
        imageView.post {
            val radius = imageView.width / 2f
            imageView.shapeAppearanceModel =
                ShapeAppearanceModel().toBuilder().setAllCornerSizes(radius).build()
        }
        return imageView
    }

    private fun getAbsLocation(): IntArray {
        val locations = IntArray(2)
        getLocationOnScreen(locations)
        return locations
    }

    interface LocationListener {
        /**
         * 回调当前移动距离
         * @param x:X轴移动距离
         * @param y:Y轴移动距离
         */
        fun onLocation(x: Int, y: Int)
    }
}

step3 业务层调用

业务层需要在小球初始化时记录LayouParams信息,然后抬手时根据小球回调的移动距离固定其位置。代码如下,在onCreate生命周期中调用

/**拖动结束后 固定小球当前的layout,防止刷新UI后小球回到初始位置*/
private fun setDragViewLayout() {
    //先记录小球的位置
    val params = binding.dragView.layoutParams as ConstraintLayout.LayoutParams
    marginStart = params.marginStart
    marginTop = params.topMargin
    //再根据小球回调移动距离固定其位置
    binding.dragView.mLocationListener = object : FloatDragView.LocationListener {
        override fun onLocation(x: Int, y: Int) {
            params.marginStart = marginStart + x
            params.topMargin = marginTop + y
            binding.dragView.layoutParams = params
        }
    }
}

代码写完了,如果看了有帮助,帮忙点个赞呀

github完整代码

参考文章:juejin.cn/post/712647…