Issue
I am creating a pixel art app, which has the following layout:
Input events are detected inside the pixel art board, meaning that if the user swipes from the root layout and travels their finger inside the pixel art board, it doesn't detect it. This is obviously a minor issue.
To fix this, I looked online and I found the following code which kind of fixed the problem:
binding.root.setOnTouchListener { _, motionEvent ->
val hitRect = Rect()
binding.activityCanvasCardView.getHitRect(hitRect)
if (hitRect.contains(motionEvent.x.toInt(), motionEvent.y.toInt())) {
Log.d("LOG123", "Hi ${motionEvent.xPrecision} ${motionEvent.yPrecision}")
binding.activityCanvasPixelGridView.onTouchEvent(motionEvent)
}
true
}
Note that the view coordinates are converted into pixel coordinates in the onTouchEvent
method.
Simple enough, right? In a perfect world, that code should fix the issue.
There's only one problem, for some reason, there is an offset with the y value:
I am unsure why I am having this strange delay with the Y coordinates.
I've tried to fix this issue, some of the things I tried were:
- manually applying offset values
- using different rect functions of the
View
class - look online to see if anyone has a similar issue
I'm following things by the book.
I tried the code that Sergei Kozelko gave me, I don't know if it's because I'm scaling/sizing the view in onCreate
, but the code isn't working:
Code I tried:
binding.root.setOnTouchListener { _, motionEvent ->
val hitRect = Rect()
binding.activityCanvasCardView.getHitRect(hitRect)
if (hitRect.contains(motionEvent.x.toInt(), motionEvent.y.toInt())) {
val offsetX = motionEvent.x - binding.activityCanvasPixelGridView.left
val offsetY = motionEvent.y - binding.activityCanvasPixelGridView.top
motionEvent.offsetLocation(offsetX, offsetY)
val inverseCopy = Matrix()
if (!binding.activityCanvasPixelGridView.matrix.isIdentity) {
binding.activityCanvasPixelGridView.matrix.invert(inverseCopy)
motionEvent.transform(inverseCopy)
}
binding.activityCanvasPixelGridView.dispatchTouchEvent(motionEvent)
}
true
}
Solution
The problem is likely that you are calling activityCanvasPixelGridView.onTouchEvent
with unmodified parent event. The coordinates are in parent's coordinate space while onTouchEvent
expects them to be in this view's coordinate space.
Android does this automatically in ViewGroup#dispatchTransformedTouchEvent
:
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
There is also getTransformedMotionEvent
which just transforms the event but it is marked private. So you'll have to reimplement it yourself. The only non-straightforward moment is dealing with transformation, as both hasIdentityMatrix
and getInverseMatrix
are marked with @UnsupportedAppUsage
and should not be used. Instead you'll need to use Matrix#isIdentity
and Matrix#invert
which do the same calculations, just with more overhead.
Combined together it looks like this:
binding.root.setOnTouchListener { _, motionEvent ->
val hitRect = Rect()
binding.activityCanvasCardView.getHitRect(hitRect)
if (hitRect.contains(motionEvent.x.toInt(), motionEvent.y.toInt())) {
val parent = binding.root
val child = binding.activityCanvasCardView
val offsetX = parent.scrollX - child.left
val offsetY = parent.scrollY - child.top
motionEvent.offsetLocation(offsetX, offsetY)
val inverseCopy = Matrix()
if (!child.matrix.isIdentity) {
child.matrix.invert(inverseCopy)
motionEvent.transform(inverseCopy)
}
child.dispatchTouchEvent(motionEvent)
}
true
}
Now, all this works only if child
(activityCanvasCardView
) is a direct child of parent
(root
). If it is not you'll need to apply the above transformation for every view between them. Again, there is nothing in Android that does exactly what you need, the closest thing is ViewGroup#offsetRectIntoDescendantCoords
. But it operates on Rect
s and doesn't account for transformations. And again you can implement it yourself like this:
private fun transformToChild(parent: View, child: View, event: MotionEvent) {
val offsetX = parent.scrollX - child.left;
val offsetY = parent.scrollY - child.top;
event.offsetLocation(offsetX.toFloat(), offsetY.toFloat());
if (!child.matrix.isIdentity) {
val inverse = Matrix()
child.matrix.invert(inverse)
event.transform(inverse);
}
}
private fun transformToDescendant(parent: View, descendant: View, event: MotionEvent) {
if (parent == descendant) {
return
}
var currentDescendant = descendant
val stack = mutableListOf<View>()
while (currentDescendant != parent) {
stack.add(currentDescendant)
// safe to cast to View as parent function argument is View
currentDescendant = currentDescendant.parent as View
}
for (view in stack.asReversed()) {
transformToChild(view.parent as View, view, event)
}
}
and call like transformToDescendant(binding.root, binding.activityCanvasPixelGridView, event)
inside setOnTouchListener
.
Please note that this code was not properly tested, throws ClassCastException
if descendant
is not actually a descendant of parent
and maybe could be optimized better to remove list. However it should work, or at least give you general idea how to implement it yourself.
Finally, this doesn't happen on X axis because the canvas fills full width, so the X coordinates in parent and child views are the same.
Answered By - Sergei Kozelko
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.