Search code examples
kotlinviewandroid-custom-viewviewgroupmotionevent

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.


Solution

  • Finally after one day of trial and error and searching the web, I found the solution. Problem was that I wasn't using the raw x and y in my calculation.

    Here is the MotionEvent handler code that fixed it:

     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
    }