首页 > 解决方案 > View does not move correctly in x and y axis after it's been rotated

问题描述

I'm currently writing a code in a viewgroup that has a framelayout as child that has a view inside it. this viewgroup is responsible for rotating, scaling, moving the views by implementing MotionEvent inside it. so far I've been able to implement rotating,scaling,moving inside it and they work just fine until I rotate the whole viewgroup, after that it seems that it doesn't move quite as expected (scaling is ok btw).

I guess the problem is that after I rotate the view by 180 degree (or even a little bit), x and y position kinda get swaped and it doesn't work anymore (until it's rotated back to it's original position). Thank you in advance.

if rotation is not applied image

if rotation is applied image

MotionEvent code:

  private val motionEventHandler: (view: View, event: MotionEvent) -> Boolean = { v, event ->
    // For scaling
    scaleDetector.onTouchEvent(event)

    val pointerCount = event.pointerCount

    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            // if view is in editing state
            if (drawFrame) {
                // Save the initial x and y of that touched point
                initialX = event.x
                initialY = event.y
            }
            performClick()
        }
        MotionEvent.ACTION_MOVE -> {
            // If view is in editing state (got clicked)
            if (drawFrame) {

                /* Moving the view by touch */

                // and if there is only 1 pointer on the screen
                if (pointerCount == 1) {

                    // Move the view
                    v.x += event.x - initialX
                    v.y += event.y - initialY

                    // Don't let the view go beyond the phone's display and limit it's x and y
                    (parent as FrameLayout).let { parent ->
                        val parentWidth = parent.width
                        val parentHeight = parent.height

                        if ((v.x + v.width) >= parentWidth) v.x =
                            (parentWidth - v.width).toFloat()

                        if ((v.y + v.height) >= parentHeight) v.y =
                            (parentHeight - v.height).toFloat()

                        if (v.x <= parent.x) v.x = parent.x
                        if (v.y <= parent.y) v.y = parent.y
                    }
                }

                /* Rotating the view by touch */
                // If there are total of two pointer on the screen
                if (pointerCount == 2) {
                    rotatedDegree =
                        event.run { /* <----- I think problem is in that code block */
                            // Get the first pointer x and y
                            val (firstX, firstY) = getPointerInfoAt(getPointerId(0))
                            // Get the second pointer x and y 
                            val (secondX, secondY) = getPointerInfoAt(getPointerId(1))

                            // Calculate the difference between those points
                            val deltaX = firstX - secondX
                            val deltaY = secondY - firstY

                            // Get the total degree that view got rotated 
                            val totalDegreeOfRotation =
                                Math.toDegrees(atan2(deltaX, deltaY).toDouble()).toFloat()

                            Log.i(
                                "MotionEvent",
                                "Total degree of rotation is $totalDegreeOfRotation  " +
                                        "first x : "
                            )
                            totalDegreeOfRotation
                        }
                    // Rotate the ViewGroup 
                    rotation += rotatedDegree
                }
            }
        }
    }
    true
}

Scaling code:

  private val scaleListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {

    override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
        if (!isChildMeasured) {
            initialScaleHeight = child.height
            initialScaleWidth = child.width
            isChildMeasured = !isChildMeasured
        }

        Log.i(
            "SCALE",
            "onScaleBegin: InitialScaleWidth $initialScaleWidth || InitialScaleHeigh $initialScaleHeight"
        )
        return true
    }

    override fun onScale(detector: ScaleGestureDetector?): Boolean {
        scaleFactor *= detector!!.scaleFactor
        scaleFactor = max(0.1f, min(scaleFactor, 2.0f))

        var childTextSize = child.textSize
        childTextSize *= scaleFactor

        if (childTextSize < 18f) childTextSize = 18f
        if (childTextSize > 85f) childTextSize = 85f

        child.textSize = childTextSize
        // In views we should only change the property that determines the view size, not the actual view size

        requestLayout()

        return true
    }
}

EditableView.kt (all of the code):

class EditableView(context: Context, attr: AttributeSet?) : ViewGroup(context, attr) {

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

private var drawFrame: Boolean = true

private var scaleFactor = 1f

private var initialScaleWidth = 0
private var initialScaleHeight = 0

private var rotatedDegree = 0f

private val child: TextView
    get() =
        mainViewHolder.children.first() as TextView


private var isChildMeasured: Boolean = false

private val scaleListener = object : ScaleGestureDetector.SimpleOnScaleGestureListener() {

    override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean {
        if (!isChildMeasured) {
            initialScaleHeight = child.height
            initialScaleWidth = child.width
            isChildMeasured = !isChildMeasured
        }

        Log.i(
            "SCALE",
            "onScaleBegin: InitialScaleWidth $initialScaleWidth || InitialScaleHeigh $initialScaleHeight"
        )
        return true
    }

    override fun onScale(detector: ScaleGestureDetector?): Boolean {
        scaleFactor *= detector!!.scaleFactor
        scaleFactor = max(0.1f, min(scaleFactor, 2.0f))

        var childTextSize = child.textSize
        childTextSize *= scaleFactor

        if (childTextSize < 18f) childTextSize = 18f
        if (childTextSize > 85f) childTextSize = 85f

        child.textSize = childTextSize
        // In views we should only change the property that determines the view size, not the actual view size

        requestLayout()

        return true
    }
}

private val scaleDetector = ScaleGestureDetector(context, scaleListener)

private var initialX = 0f
private var initialY = 0f

private val motionEventHandler: (view: View, event: MotionEvent) -> Boolean = { v, event ->
    // For scaling
    scaleDetector.onTouchEvent(event)
    val pointerCount = event.pointerCount
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            // if view is in editing state
            if (drawFrame) {
                // Save the initial x and y of that touched point
                initialX = event.x
                initialY = event.y
            }
            performClick()
        }
        MotionEvent.ACTION_MOVE -> {
            // If view is in editing state (got clicked)
            if (drawFrame) {

                /* Moving the view by touch */

                // and if there is only 1 pointer on the screen
                if (pointerCount == 1) {

                    // Move the view
                    v.x += event.x - initialX
                    v.y += event.y - initialY

                    // Don't let the view go beyond the phone's display and limit it's x and y
                    (parent as FrameLayout).let { parent ->
                        val parentWidth = parent.width
                        val parentHeight = parent.height

                        if ((v.x + v.width) >= parentWidth) v.x =
                            (parentWidth - v.width).toFloat()

                        if ((v.y + v.height) >= parentHeight) v.y =
                            (parentHeight - v.height).toFloat()

                        if (v.x <= parent.x) v.x = parent.x
                        if (v.y <= parent.y) v.y = parent.y
                    }
                }

                /* Rotating the view by touch */
                // If there are total of two pointer on the screen
                if (pointerCount == 2) {
                    rotatedDegree =
                        event.run { /* <----- I think problem is in that code block */
                            // Get the first pointer x and y
                            val (firstX, firstY) = getPointerInfoAt(getPointerId(0))
                            // Get the second pointer x and y
                            val (secondX, secondY) = getPointerInfoAt(getPointerId(1))

                            // Calculate the difference between those points
                            val deltaX = firstX - secondX
                            val deltaY = secondY - firstY

                            // Get the total degree that view got rotated
                            val totalDegreeOfRotation =
                                Math.toDegrees(atan2(deltaX, deltaY).toDouble()).toFloat()

                            Log.i(
                                "MotionEvent",
                                "Total degree of rotation is $totalDegreeOfRotation  " +
                                        "first x : "
                            )
                            totalDegreeOfRotation
                        }
                    // Rotate the ViewGroup
                    rotation += rotatedDegree
                }
            }
        }
    }
    true
}

private val mainViewHolder = FrameLayout(context).apply {
    layoutParams =
        FrameLayout.LayoutParams(
            LayoutParams.WRAP_CONTENT,
            LayoutParams.WRAP_CONTENT,
        )

}

private val mainFrameBoundaryPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    color = Color.BLACK
    strokeWidth = 2.dp
    style = Paint.Style.STROKE
}

private val frameLayoutRectangle = RectF()

init {
    setOnTouchListener(motionEventHandler)
    setWillNotDraw(false)
}

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    addView(mainViewHolder)
}

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    measureChild(mainViewHolder, widthMeasureSpec, heightMeasureSpec)
    setMeasuredDimension(
        resolveSize(
            mainViewHolder.measuredWidth,
            widthMeasureSpec
        ),
        resolveSize(mainViewHolder.height, heightMeasureSpec)
    )
}

override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
    var x = 0
    mainViewHolder.layout(x, t, x + mainViewHolder.measuredWidth, mainViewHolder.measuredHeight)
    x += mainViewHolder.measuredWidth

    frameLayoutRectangle.set(
        0f, 0f,
        x.toFloat(),
        mainViewHolder.measuredHeight.toFloat()
    )
}

override fun dispatchDraw(canvas: Canvas?) {
    super.dispatchDraw(canvas)

    if (drawFrame)
        canvas!!.apply {
            drawRoundRect(frameLayoutRectangle, 2.dp, 2.dp, mainFrameBoundaryPaint)
        }
}

fun addToFrame(view: View) {
    // Let the canvas draw it's rectangle meaning that view is getting edited
    drawFrame = true
    // Add the view that's going to get edited to the FrameLayout
    mainViewHolder.addView(view)
}

fun showFrameAroundView() {
    // Show the rectangle frame around the view
    if (!drawFrame) {
        drawFrame = true
        invalidate()
    }
}

fun hideFrameAroundView() {
    
    // Hide the rectangle around the view (meaning it's not longer in editing state)
    if (drawFrame) {
        drawFrame = false
        invalidate()
    }
}

fun doesHaveChild(): Boolean {
    return childCount > 0
}

}

I would appreciated it if you could help me with better implementation for that scenario.

标签: kotlinviewandroid-custom-viewviewgroupmotionevent

解决方案


终于经过一天的反复试验并在网上搜索,我找到了解决方案。问题是我在计算中没有使用原始的 x 和 y。

这是修复它的 MotionEvent 处理程序代码:

 private val motionEventHandler: (view: View, event: MotionEvent) -> Boolean = { v, event ->
    // For scaling
    scaleDetector.onTouchEvent(event)
    val pointerCount = event.pointerCount
    when (event.actionMasked) {
        MotionEvent.ACTION_DOWN -> {
            if (drawFrame) {
                initialX = v.x - event.rawX
                initialY = v.y - event.rawY
            }
            performClick()
        }
        MotionEvent.ACTION_MOVE -> {
            // If view is in editing state (got clicked)
            if (drawFrame) {

                /* Moving the view by touch */

                // and if there is only 1 pointer on the screen
                if (pointerCount == 1) {

                    val viewParent = parent as ViewGroup

                    // Move the view
                    v.x = event.rawX + initialX
                    v.y = event.rawY + initialY

                    // Don't let the view go beyond the phone's display and limit it's x and y
                    viewParent.let { parent ->
                        val parentHeight = parent.height

                        if ((v.y + v.height) >= parentHeight) v.y =
                            (parentHeight - v.height).toFloat()

                        if (v.y <= parent.y) v.y = parent.y
                    }
                }

                /* Rotating the view by touch */
                // If there are total of two pointer on the screen
                if (pointerCount == 2) {
                    rotatedDegree =
                        event.run { /* <----- I think problem is in that code block */
                            // Get the first pointer x and y
                            val (firstX, firstY) = getPointerInfoAt(getPointerId(0))
                            // Get the second pointer x and y
                            val (secondX, secondY) = getPointerInfoAt(getPointerId(1))

                            // Calculate the difference between those points
                            val deltaX = firstX - secondX
                            val deltaY = secondY - firstY

                            // Get the total degree that view got rotated
                            val totalDegreeOfRotation =
                                Math.toDegrees(atan2(deltaX, deltaY).toDouble()).toFloat()

                            Log.i(
                                "MotionEvent",
                                "Total degree of rotation is $totalDegreeOfRotation  " +
                                        "first x : "
                            )
                            totalDegreeOfRotation
                        }
                    // Rotate the ViewGroup
                    rotation += rotatedDegree
                }
            }
        }
    }
    true
}

推荐阅读