Hello! Thank you for reading.
I've been looking for ways to correctly create a multi-direction scrollable Android layout of a certain size (5x that of the user's screen), that can be flinged (& panned) and that can also be zoomed on.
To note that having the child (ConstraintLayout) be the wanted size (5x the user's screen) is not the problem here.
Obviously, I found many answers online for ways to do this with different ways to integrate it. Originally, I was about to post all the different integration I had unsuccessfully tried so far, until I encountered Oded's solution. In this one, everything that didn't work correctly worked.
This means:
- The layout allowed scrolling the entire width and height of the child but no more
- The bounds/borders of the scrollable area scaled correctly with the Zooming (this was the deal-breaker, almost no custom layout could do this)
- The layout could be panned
- The pivot point was correct (in the middle).
This was a huge relief, as modifying different already-made but lacking integrations (to fix bounds+zooming issues mostly) had already taken days.
I would recommend anyone having a similar issue in the future to use this integration.
In any case, there are a few issues, including: goes back to (0,0) upon screen orientation changes, no double-tap-to-zoom, and no flinging. I don't believe it to be hard to fix those two first issues, however this third one is quite problematic. I want the user to be able to freely view this large area. Not having the ability to fling will make moving in the app slower than preferable.
I've already tried different ways to do this, such as using the Scroller class with a GestureDetector, using a "Flinger" runnable class (containing a Scroller), using some form of the Choreographer, and others.
I've been unable to get it to work, and solutions often completely break the way this layout is doing scrolling (for instance, I've noticed scrollX never changes, and no scrollTo function is present (it is replaced by mPosX)). Usually correct solutions to the addition of Flinging seem impossible to use here, and in some cases, implementations have caused the scroller to go several thousand units from where it should be.
Here is my backup copy of Odid's solution, with the only changes being minor adjustments to avoid Android Studio warnings.
Snippet (mandatory):
@Override
public boolean onTouchEvent(MotionEvent ev) {
mOnTouchEventWorkingArray[0] = ev.getX();
mOnTouchEventWorkingArray[1] = ev.getY();
mOnTouchEventWorkingArray = scaledPointsToScreenPoints(mOnTouchEventWorkingArray);
ev.setLocation(mOnTouchEventWorkingArray[0], mOnTouchEventWorkingArray[1]);
mScaleDetector.onTouchEvent(ev);
final int action = ev.getAction();
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
final float x = ev.getX();
final float y = ev.getY();
mLastTouchX = x;
mLastTouchY = y;
// Save the ID of this pointer
mActivePointerId = ev.getPointerId(0);
break;
}
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float y = ev.getY(pointerIndex);
if (mIsScaling && ev.getPointerCount() == 1) {
// Don't move during a QuickScale.
mLastTouchX = x;
mLastTouchY = y;
break;
}
float dx = x - mLastTouchX;
float dy = y - mLastTouchY;
float[] topLeft = {0f, 0f};
float[] bottomRight = {getWidth(), getHeight()};
/*
* Corners of the view in screen coordinates, so dx/dy should not be allowed to
* push these beyond the canvas bounds.
*/
float[] scaledTopLeft = screenPointsToScaledPoints(topLeft);
float[] scaledBottomRight = screenPointsToScaledPoints(bottomRight);
dx = Math.min(Math.max(dx, scaledBottomRight[0] - mCanvasWidth), scaledTopLeft[0]);
dy = Math.min(Math.max(dy, scaledBottomRight[1] - mCanvasHeight), scaledTopLeft[1]);
mPosX += dx;
mPosY += dy;
mTranslateMatrix.preTranslate(dx, dy);
mTranslateMatrix.invert(mTranslateMatrixInverse);
mLastTouchX = x;
mLastTouchY = y;
invalidate();
break;
}
case MotionEvent.ACTION_UP: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_CANCEL: {
mActivePointerId = INVALID_POINTER_ID;
break;
}
case MotionEvent.ACTION_POINTER_UP: {
// Extract the index of the pointer that left the touch sensor
final int pointerIndex = (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = ev.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
// This was our active pointer going up. Choose a new
// active pointer and adjust accordingly.
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
mLastTouchX = ev.getX(newPointerIndex);
mLastTouchY = ev.getY(newPointerIndex);
mActivePointerId = ev.getPointerId(newPointerIndex);
}
break;
}
}
return true;
}
Thank you very much for your help.