Android 拖拽布局DragLayout

发布时间 2023-11-22 15:34:13作者: 徐影魔
class DraggableFrameLayout @JvmOverloads constructor(
    context: Context, attrs: AttributeSet? = null,
) : FrameLayout(context, attrs) {

    enum class DragState {
        Draging, Idel
    }

    interface OnDragListener {
        fun onDragStart(){}
        fun onDragEnd(){}
        fun onDragStageChanged(state: DragState){}
    }

    private val observable = XObservableImpl<OnDragListener>()

    private var downX = 0f
    private var downY = 0f
    private var mLastX = 0f
    private var mLastY = 0f
    private val TAG = "DragableFrameLayout"
    private val mSlop = ViewConfiguration.get(context).scaledTouchSlop
    private var mDragging = false
        set(value) {
            field = value
            LogUtil.d("drag:$value", TAG)
        }

    private var disableHorizontalDrag = false
    private val edge = Rect()

    init {
        context.obtainStyledAttributes(attrs, R.styleable.DraggableFrameLayout).use {
            disableHorizontalDrag = it.getBoolean(R.styleable.DraggableFrameLayout_disableHorizontalDrag, false)
            it.getString(R.styleable.DraggableFrameLayout_edges)?.let { str ->
                val edges = str.split(",").map { s -> s.toInt().dp }
                edge.set(edges[0], edges[1], edges[2], edges[3])
            }
        }
    }

    fun addDragListener(listener: OnDragListener) {
        observable.addListener(listener)
    }

    fun removeDragListener(listener: OnDragListener) {
        observable.removeListener(listener)
    }

    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
        handleEvent(ev)
        return super.dispatchTouchEvent(ev)
    }

    override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
        return mDragging || super.onInterceptTouchEvent(ev)
    }

    private fun handleEvent(event: MotionEvent): Boolean {
        when (event.action) {
            MotionEvent.ACTION_DOWN -> {
                LogUtil.d("ACTION_DOWN", TAG)
                mLastX = event.rawX
                mLastY = event.rawY
                downX = mLastX
                downY = mLastY
                mDragging = false
            }

            MotionEvent.ACTION_MOVE -> {
                val dx = event.rawX - mLastX
                val dy = event.rawY - mLastY
                mLastX = event.rawX
                mLastY = event.rawY
                if (!mDragging) {
                    val absX = abs(downX - event.rawX)
                    val absY = abs(downY - event.rawY)
                    LogUtil.d("ACTION_MOVE abx:$absX,aby:$absY,slop:$mSlop", TAG)
                    if (absY >= mSlop && absY > absX) {
                        mDragging = true
                    } else if (disableHorizontalDrag.not() && absX >= mSlop) {
                        mDragging = true
                    }
                    if (mDragging) {
                        dispatchDragStart()
                    }
                }
                if (mDragging) {
                    offsetLeftAndRight(resizeDx(dx))
                    offsetTopAndBottom(resizeDy(dy))
                }
            }

            MotionEvent.ACTION_UP -> {
                LogUtil.d("ACTION_UP", TAG)
                if ((left + measuredWidth / 2f) > parentWidth / 2f) {
                    slideToRight()
                } else {
                    slideToLeft()
                }
                dispatchDragEnd()
            }

            else -> {}
        }
        return mDragging
    }

    private fun dispatchDragEnd() {
        observable.notify {
            it.onDragStageChanged(DragState.Idel)
            it.onDragEnd()
        }
    }

    private fun dispatchDragStart() {
        observable.notify {
            it.onDragStageChanged(DragState.Draging)
            it.onDragStart()
        }
    }

    private fun slideToRight() {
        LogUtil.d("slideToRight", TAG)
        val layoutParams = layoutParams
        var alignStart = true
        if (layoutParams is LayoutParams) {
            val flag = layoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK
            alignStart = flag == Gravity.LEFT
        } else if (layoutParams is ConstraintLayout.LayoutParams) {
            alignStart = layoutParams.startToStart != ConstraintLayout.LayoutParams.UNSET
        }
        if (alignStart) {
            animatedToLeft(left, parentWidth - measuredWidth - edge.left)
        } else {
            animateToRight(parentWidth - right, edge.right)
        }
    }

    private fun slideToLeft() {
        LogUtil.d("slide to Left", TAG)
        val layoutParams = layoutParams
        val start: Int = left
        val end: Int = edge.left
        var alignStart = true
        if (layoutParams is LayoutParams) {
            val flag = layoutParams.gravity and Gravity.HORIZONTAL_GRAVITY_MASK
            alignStart = flag == Gravity.LEFT
        } else if (layoutParams is ConstraintLayout.LayoutParams) {
            alignStart = layoutParams.startToStart != ConstraintLayout.LayoutParams.UNSET
        }
        if (alignStart) {
            animatedToLeft(start, end)
        } else {
            animateToRight(parentWidth - right, parentWidth - measuredWidth - edge.left)
        }
    }


    private fun animateToRight(start: Int, end: Int) {
        LogUtil.d("animateToRight:$start,$end", TAG)
        val marginTop = top
        val width = measuredWidth
        val height = measuredHeight
        val marginBottom = parentHeight - bottom
        val lp = layoutParams
        ValueAnimator.ofInt(start, end)
            .apply {
                duration = 350
                addUpdateListener {
                    updateLayoutParams<MarginLayoutParams> {
                        this.rightMargin = it.animatedValue as Int
                        this.leftMargin = 0
                        this.bottomMargin = marginBottom
                        this.topMargin = marginTop
                        this.width = width
                        this.height = height
                    }
                }
                start()
            }
    }

    private fun animatedToLeft(start: Int, end: Int) {
        LogUtil.d("animatedToLeft:$start,$end", TAG)
        val marginTop = top
        val width = measuredWidth
        val height = measuredHeight
        val marginBottom = parentHeight - bottom
        ValueAnimator.ofInt(start, end)
            .apply {
                duration = 350
                addUpdateListener {
                    updateLayoutParams<MarginLayoutParams> {
                        this.leftMargin = it.animatedValue as Int
                        this.rightMargin = 0
                        this.bottomMargin = marginBottom
                        this.topMargin = marginTop
                        this.width = width
                        this.height = height
                    }
                }
                start()
            }
    }

    private val parentWidth: Int
        get() = (parent as View).measuredWidth
    private val parentHeight: Int
        get() = (parent as View).measuredHeight

    private fun resizeDx(dx: Float): Int {
        return MathUtils.clamp(dx, -(left - edge.left).toFloat(), (parentWidth - edge.right - right).toFloat()).toInt()
    }

    private fun resizeDy(dy: Float): Int {
        return MathUtils.clamp(dy, -(top - edge.top).toFloat(), (parentHeight - bottom - edge.bottom).toFloat()).toInt()
    }
}