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
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.
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
}