Skip to content

Commit

Permalink
moved code to fix overpinch and overscroll outside of PinchListener
Browse files Browse the repository at this point in the history
relocate overscroll/pan correction to processTouchEvent when the user releases touch
removed onScrollEnd (replaced by fixOverScrollAndPan)
  • Loading branch information
markusressel committed Feb 8, 2019
1 parent 8fa83df commit aebc591
Showing 1 changed file with 111 additions and 123 deletions.
234 changes: 111 additions & 123 deletions library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -824,32 +824,26 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener

private fun processTouchEvent(event: MotionEvent): Int {
LOG.v("processTouchEvent:", "start.")
// this line prevents user touch from cancelling the "overscroll/pinch correction" animation
// TODO: in some cases the overscroll animation is still not started :(
if (mState == ANIMATING && event.action == MotionEvent.ACTION_UP) return TOUCH_STEAL

// if we are animating, cancel the animation and process touch as normal
if (cancelAnimations()) {
return TOUCH_STEAL
}
var result = cancelAnimations()

var result = mScaleDetector.onTouchEvent(event)
result = result or mScaleDetector.onTouchEvent(event)
LOG.v("processTouchEvent:", "scaleResult:", result)

// Pinch detector always returns true. If we actually started a pinch,
// Pinch detector always returns true.
// If we actually started a pinch,
// Don't pass to fling detector.
if (mState != PINCHING) {
result = result or mFlingDragDetector.onTouchEvent(event)
LOG.v("processTouchEvent:", "flingResult:", result)
}

// Detect scroll ends, this appears to be the only way.
if (mState == SCROLLING) {
val a = event.actionMasked
if (a == MotionEvent.ACTION_UP || a == MotionEvent.ACTION_CANCEL) {
LOG.i("processTouchEvent:", "up event while scrolling, dispatching onScrollEnd.")
onScrollEnd()
}
// when user releases touch
// and there is no fling to start
// fix overscroll/pinch in a nice animation
if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL
&& mState != FLINGING) {
result = result or fixOverScrollAndPan()
}

return if (result && mState != NONE) {
Expand Down Expand Up @@ -920,78 +914,8 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
"mInitialAbsFocusPoint.y:", mInitialAbsFocusPoint.y,
"mOverPinchable;", mOverPinchable)

try {
if (mOverPinchable || mOverScrollVertical || mOverScrollHorizontal) {
// We might have over pinched/scrolled. Animate back to reasonable value.
@Zoom val maxZoom = getMaxZoom()
@Zoom val minZoom = getMinZoom()

// check what zoom needs to be applied
// to get into a non-overpinched state
@Zoom val newZoom = checkZoomBounds(zoom, allowOverPinch = false)

LOG.i("onScaleEnd:",
"zoom:", zoom,
"newZoom:", newZoom,
"max:", maxZoom,
"min:", minZoom)

// check what pan needs to be applied
// to get into a non-overscrolled state
val panFix = mCurrentPanCorrection.toAbsolute()

if (panFix.x == 0F && panFix.y == 0F && newZoom.compareTo(zoom) == 0) {
// nothing to correct, we can stop right here
setState(NONE)
return
}

// select zoom pivot point based on what edge of the screen is currently overscrolled
val zoomTarget = calculateZoomPivotPoint(panFix)

// calculate the new pan position
val newPan = pan + panFix
if (newZoom.compareTo(zoom) != 0) {
// we have overpinched. to calculate how much pan needs to be applied
// to fix overscrolling we need to simulate the target zoom (when overpinching has been corrected)
// to calculate the needed pan correction for that zoom level

// remember current pan and zoom value to reset to that state later
val oldPan = AbsolutePoint(pan)
val oldZoom = zoom

// apply the target zoom with the currently known pivot point
applyZoom(newZoom, true, true, zoomTarget.x, zoomTarget.y, notifyListeners = false)

// recalculate pan fix to account for additional borders that might overscroll when zooming out
panFix.set(mCurrentPanCorrection.toAbsolute())

// recalculate new pan location using the simulated target zoom level
newPan.set(pan + panFix)

// revert simulation
applyZoomAndAbsolutePan(oldZoom, oldPan.x, oldPan.y, true, true, notifyListeners = false)
}

if (panFix.x == 0F && panFix.y == 0F) {
// no overscroll to correct
// only fix overpinch
animateZoom(newZoom, allowOverPinch = true)
} else {
// fix overscroll (overpinch is also corrected in here if necessary)
animateZoomAndAbsolutePan(newZoom,
newPan.x, newPan.y,
zoomTargetX = zoomTarget.x,
zoomTargetY = zoomTarget.y,
allowOverScroll = true, allowOverPinch = true)
}
// return here because new state will be ANIMATING
return
}
setState(NONE)
} finally {
resetPinchListenerState()
}
setState(NONE)
resetPinchListenerState()
}

/**
Expand All @@ -1004,35 +928,111 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
mCurrentAbsFocusOffset.set(0F, 0F)
}

/**
* Calculate pivot point to use for zoom based on pan fixes to be applied
*
* @param fixPan the amount of pan to apply to get into a valid state (no overscroll)
* @return x-axis and y-axis view coordinates
*/
private fun calculateZoomPivotPoint(fixPan: AbsolutePoint): PointF {
if (zoom <= 1F) {
// The zoom pivot point here should be based on the gravity that is used
// to initially transform the content.
// Currently this is always [View.Gravity.CENTER] as indicated by [mTransformationGravity]
// but this might be changed by the user.
return AbsolutePoint(-mContentWidth / 2F, -mContentHeight / 2F).toViewCoordinate()
}

/**
* Animates back from overscrolled or overpinched state, if necessary.
*
* @return true if something needs to be fixed, false otherwise
*/
private fun fixOverScrollAndPan(): Boolean {
if (mOverPinchable || mOverScrollVertical || mOverScrollHorizontal) {
// We might have over pinched/scrolled. Animate back to reasonable value.
@Zoom val maxZoom = getMaxZoom()
@Zoom val minZoom = getMinZoom()

// check what zoom needs to be applied
// to get into a non-overpinched state
@Zoom val newZoom = checkZoomBounds(zoom, allowOverPinch = false)

LOG.i("onScaleEnd:",
"zoom:", zoom,
"newZoom:", newZoom,
"max:", maxZoom,
"min:", minZoom)

// check what pan needs to be applied
// to get into a non-overscrolled state
val panFix = mCurrentPanCorrection.toAbsolute()

if (panFix.x == 0F && panFix.y == 0F && newZoom.compareTo(zoom) == 0) {
// nothing to correct, we can stop right here
return false
}

val x = when {
fixPan.x > 0 -> mContainerWidth // content needs to be moved left, use the right border as target
fixPan.x < 0 -> 0F // content needs to move right, use the left border as target
else -> mContainerWidth / 2F // axis is not changed, use center as target
// select zoom pivot point based on what edge of the screen is currently overscrolled
val zoomTarget = calculateZoomPivotPoint(panFix)

// calculate the new pan position
val newPan = pan + panFix
if (newZoom.compareTo(zoom) != 0) {
// we have overpinched. to calculate how much pan needs to be applied
// to fix overscrolling we need to simulate the target zoom (when overpinching has been corrected)
// to calculate the needed pan correction for that zoom level

// remember current pan and zoom value to reset to that state later
val oldPan = AbsolutePoint(pan)
val oldZoom = zoom

// apply the target zoom with the currently known pivot point
applyZoom(newZoom, true, true, zoomTarget.x, zoomTarget.y, notifyListeners = false)

// recalculate pan fix to account for additional borders that might overscroll when zooming out
panFix.set(mCurrentPanCorrection.toAbsolute())

// recalculate new pan location using the simulated target zoom level
newPan.set(pan + panFix)

// revert simulation
applyZoomAndAbsolutePan(oldZoom, oldPan.x, oldPan.y, true, true, notifyListeners = false)
}

val y = when {
fixPan.y > 0 -> mContainerHeight // content needs to be moved up, use the bottom border as target
fixPan.y < 0 -> 0F // content needs to move down, use the top border as target
else -> mContainerHeight / 2F // axis is not changed, use center as target
if (panFix.x == 0F && panFix.y == 0F) {
// no overscroll to correct
// only fix overpinch
animateZoom(newZoom, allowOverPinch = true)
} else {
// fix overscroll (overpinch is also corrected in here if necessary)
animateZoomAndAbsolutePan(newZoom,
newPan.x, newPan.y,
zoomTargetX = zoomTarget.x,
zoomTargetY = zoomTarget.y,
allowOverScroll = true, allowOverPinch = true)
}
return true
}

return false
}

/**
* Calculate pivot point to use for zoom based on pan fixes to be applied
*
* @param fixPan the amount of pan to apply to get into a valid state (no overscroll)
* @return x-axis and y-axis view coordinates
*/
private fun calculateZoomPivotPoint(fixPan: AbsolutePoint): PointF {
if (zoom <= 1F) {
// The zoom pivot point here should be based on the gravity that is used
// to initially transform the content.
// Currently this is always [View.Gravity.CENTER] as indicated by [mTransformationGravity]
// but this might be changed by the user.
return AbsolutePoint(-mContentWidth / 2F, -mContentHeight / 2F).toViewCoordinate()
}

return PointF(x, y)
val x = when {
fixPan.x > 0 -> mContainerWidth // content needs to be moved left, use the right border as target
fixPan.x < 0 -> 0F // content needs to move right, use the left border as target
else -> mContainerWidth / 2F // axis is not changed, use center as target
}

val y = when {
fixPan.y > 0 -> mContainerHeight // content needs to be moved up, use the bottom border as target
fixPan.y < 0 -> 0F // content needs to move down, use the top border as target
else -> mContainerHeight / 2F // axis is not changed, use center as target
}

return PointF(x, y)
}

/**
Expand Down Expand Up @@ -1067,7 +1067,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
/**
* Scroll event detected.
*
* We assume overScroll is true. If this is the case, it will be reset in [onScrollEnd].
* We assume overScroll is true. If this is the case, it will be reset in [fixOverScrollAndPan].
* If not, the [applyScaledPan] function will ignore our delta.
*
* TODO this this not true! ^
Expand Down Expand Up @@ -1112,18 +1112,6 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
}
}

private fun onScrollEnd() {
if (mOverScrollHorizontal || mOverScrollVertical) {
// We might have over scrolled. Animate back to reasonable value.
val panFix = mCurrentPanCorrection
if (panFix.x != 0f || panFix.y != 0f) {
animateScaledPan(panFix.x, panFix.y, true)
return
}
}
setState(NONE)
}

//region Position APIs

/**
Expand Down

0 comments on commit aebc591

Please sign in to comment.