Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cancel active fling animation when appropriate #85

Merged
merged 7 commits into from
Feb 12, 2019
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ will make more sense than the other - e. g., in a PDF viewer, you might want to
|`zoomOut()`|Applies a small, animated zoom-out.|`-`|
|`setZoomEnabled(boolean)`|If true, the content will be allowed to zoom in and out by user input.|`true`|


The `moveTo(float, float, float, boolean)` API will let you animate both zoom and [pan](#pan) at the same time.

### Pan
Expand All @@ -267,6 +268,7 @@ In any case the current scale is not considered, so your system won't change if
|`setAllowFlingInOverscroll(boolean)`|If true, fling gestures will be allowed even when detected while overscrolled. This might cause artifacts so it is disabled by default.|`false`|
|`panTo(float, float, boolean)`|Pans to the given values, animating if needed.|`-`|
|`panBy(float, float, boolean)`|Applies the given deltas to the current pan, animating if needed.|`-`|
|`cancelAnimations()`|Cancels all currently active code driven animations (including fling animations).|`-`|
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say a finger fling is not code driven?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well the user isn't touching the screen anymore so... I would argue it is 😄 but I see your point.
Do you have a suggestion? Would you just write:

Cancels all currently active animations.

?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would be more specific and say like, animations triggered by API calls with animate = true and fling animations triggered by touch input.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@natario1 Ok with the current vesion?

Copy link
Owner

@natario1 natario1 Feb 12, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me! @markusressel feel free to create a new release if you think so


The `moveTo(float, float, float, boolean)` API will let you animate both [zoom](#zoom) and pan at the same time.

Expand Down
8 changes: 8 additions & 0 deletions library/src/main/java/com/otaliastudios/zoom/ZoomApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,14 @@ interface ZoomApi {
*/
fun setAnimationDuration(duration: Long)

/**
* Cancels all currently active code driven animations (including fling animations)
* If no animation is currently active this is a no-op.
*
* @return true if anything was cancelled, false otherwise
*/
fun cancelAnimations(): Boolean

companion object {

/**
Expand Down
127 changes: 70 additions & 57 deletions library/src/main/java/com/otaliastudios/zoom/ZoomEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
private var mInitialized = false
private var mContentScaledRect = RectF()
private var mContentRect = RectF()
private var mClearAnimation = false
private var mAnimationDuration = DEFAULT_ANIMATION_DURATION

// Gestures
Expand Down Expand Up @@ -677,27 +676,31 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener

// Returns true if we should go to that mode.
@SuppressLint("SwitchIntDef")
private fun setState(@State state: Int): Boolean {
LOG.v("trySetState:", state.toStateName())
private fun setState(@State newState: Int): Boolean {
LOG.v("trySetState:", newState.toStateName())
if (!mInitialized) return false
if (state == mState) return true
val oldMode = mState

when (state) {
SCROLLING -> if (oldMode == PINCHING || oldMode == ANIMATING) return false
FLINGING -> if (oldMode == ANIMATING) return false
PINCHING -> if (oldMode == ANIMATING) return false
// we need to do some cleanup in case of ANIMATING so we can't return just yet
if (newState == mState && newState != ANIMATING) return true
val oldState = mState

when (newState) {
SCROLLING -> if (oldState == PINCHING || oldState == ANIMATING) return false
FLINGING -> if (oldState == ANIMATING) return false
PINCHING -> if (oldState == ANIMATING) return false
NONE -> dispatchOnIdle()
}

// Now that it succeeded, do some cleanup.
when (oldMode) {
when (oldState) {
ANIMATING -> {
mActiveAnimators.forEach { it.cancel() }
mActiveAnimators.clear()
}
FLINGING -> mFlingScroller.forceFinished(true)
ANIMATING -> mClearAnimation = true
}

LOG.i("setState:", state.toStateName())
mState = state
LOG.i("setState:", newState.toStateName())
mState = newState
return true
}

Expand Down Expand Up @@ -1131,6 +1134,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
if (animate) {
animateZoomAndAbsolutePan(zoom, x, y, allowOverScroll = false)
} else {
cancelAnimations()
applyZoomAndAbsolutePan(zoom, x, y, allowOverScroll = false)
}
}
Expand Down Expand Up @@ -1166,6 +1170,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
if (animate) {
animateZoomAndAbsolutePan(zoom, panX + dx, panY + dy, allowOverScroll = false)
} else {
cancelAnimations()
applyZoomAndAbsolutePan(zoom, panX + dx, panY + dy, allowOverScroll = false)
}
}
Expand All @@ -1182,6 +1187,7 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
if (animate) {
animateZoom(zoom, allowOverPinch = false)
} else {
cancelAnimations()
applyZoom(zoom, allowOverPinch = false)
}
}
Expand All @@ -1202,17 +1208,13 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
* Applies a small, animated zoom-in.
* Shorthand for [zoomBy] with factor 1.3.
*/
override fun zoomIn() {
zoomBy(1.3f, animate = true)
}
override fun zoomIn() = zoomBy(1.3f, animate = true)

/**
* Applies a small, animated zoom-out.
* Shorthand for [zoomBy] with factor 0.7.
*/
override fun zoomOut() {
zoomBy(0.7f, animate = true)
}
override fun zoomOut() = zoomBy(0.7f, animate = true)

/**
* Animates the actual matrix zoom to the given value.
Expand Down Expand Up @@ -1277,24 +1279,46 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener

//region Apply values

private val mActiveAnimators = mutableSetOf<ValueAnimator>()
private val mCancelAnimationListener = object : AnimatorListenerAdapter() {

override fun onAnimationEnd(animation: Animator?) {
setState(NONE)
override fun onAnimationEnd(animator: Animator) {
cleanup(animator)
}

override fun onAnimationCancel(animation: Animator?) {
setState(NONE)
override fun onAnimationCancel(animator: Animator) {
cleanup(animator)
}

private fun cleanup(animator: Animator) {
animator.removeListener(this)
mActiveAnimators.remove(animator)
if (mActiveAnimators.isEmpty()) setState(NONE)
}
}

/**
* Prepares a [ValueAnimator] for the first run
*
* @return itself (for chaining)
*/
private fun ValueAnimator.prepare() {
private fun ValueAnimator.prepare(): ValueAnimator {
this.duration = mAnimationDuration
this.addListener(mCancelAnimationListener)
this.interpolator = ANIMATION_INTERPOLATOR
return this
}

/**
* Starts a [ValueAnimator] with the given update function
*
* @return itself (for chaining)
*/
private fun ValueAnimator.start(onUpdate: (ValueAnimator) -> Unit): ValueAnimator {
this.addUpdateListener(onUpdate)
this.start()
mActiveAnimators.add(this)
return this
}

/**
Expand All @@ -1306,21 +1330,12 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
*/
private fun animateZoom(@Zoom zoom: Float, allowOverPinch: Boolean) {
if (setState(ANIMATING)) {
mClearAnimation = false
@Zoom val startZoom = this.zoom
@Zoom val endZoom = checkZoomBounds(zoom, allowOverPinch)
val zoomAnimator = ValueAnimator.ofFloat(startZoom, endZoom)
zoomAnimator.prepare()
zoomAnimator.addUpdateListener {
ValueAnimator.ofFloat(startZoom, endZoom).prepare().start {
LOG.v("animateZoom:", "animationStep:", it.animatedFraction)
if (mClearAnimation) {
it.cancel()
return@addUpdateListener
}
applyZoom(it.animatedValue as Float, allowOverPinch)
}

zoomAnimator.start()
}
}

Expand All @@ -1336,23 +1351,22 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
* @param zoomTargetX the x-axis zoom target
* @param zoomTargetY the y-axis zoom target
*/
@SuppressLint("ObjectAnimatorBinding")
private fun animateZoomAndAbsolutePan(@Zoom zoom: Float,
@AbsolutePan x: Float, @AbsolutePan y: Float,
allowOverScroll: Boolean,
allowOverPinch: Boolean = false,
zoomTargetX: Float? = null,
zoomTargetY: Float? = null) {
if (setState(ANIMATING)) {
mClearAnimation = false
@Zoom val startZoom = this.zoom
@Zoom val endZoom = checkZoomBounds(zoom, allowOverScroll)
val startPan = pan
val targetPan = AbsolutePoint(x, y)
LOG.i("animateZoomAndAbsolutePan:", "starting.", "startX:", startPan.x, "endX:", x, "startY:", startPan.y, "endY:", y)
LOG.i("animateZoomAndAbsolutePan:", "starting.", "startZoom:", startZoom, "endZoom:", endZoom)

@SuppressLint("ObjectAnimatorBinding")
val animator = ObjectAnimator.ofPropertyValuesHolder(mContainer,
ObjectAnimator.ofPropertyValuesHolder(mContainer,
markusressel marked this conversation as resolved.
Show resolved Hide resolved
PropertyValuesHolder.ofObject(
"pan",
TypeEvaluator { fraction: Float, startValue: AbsolutePoint, endValue: AbsolutePoint ->
Expand All @@ -1362,20 +1376,13 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
PropertyValuesHolder.ofFloat(
"zoom",
startZoom, endZoom)
)
animator.prepare()
animator.addUpdateListener {
if (mClearAnimation) {
it.cancel()
return@addUpdateListener
}
).prepare().start {
val newZoom = it.getAnimatedValue("zoom") as Float
val currentPan = it.getAnimatedValue("pan") as AbsolutePoint
applyZoomAndAbsolutePan(newZoom, currentPan.x, currentPan.y,
allowOverScroll, allowOverPinch,
zoomTargetX, zoomTargetY)
}
animator.start()
}
}

Expand All @@ -1390,25 +1397,16 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
private fun animateScaledPan(@ScaledPan deltaX: Float, @ScaledPan deltaY: Float,
allowOverScroll: Boolean) {
if (setState(ANIMATING)) {
mClearAnimation = false
val startPan = scaledPan
val endPan = startPan + ScaledPoint(deltaX, deltaY)

val panAnimator = ValueAnimator.ofObject(TypeEvaluator { fraction, startValue: ScaledPoint, endValue: ScaledPoint ->
ValueAnimator.ofObject(TypeEvaluator { fraction, startValue: ScaledPoint, endValue: ScaledPoint ->
startValue + (endValue - startValue) * fraction - scaledPan
}, startPan, endPan)
panAnimator.prepare()
panAnimator.addUpdateListener {
}, startPan, endPan).prepare().start {
LOG.v("animateScaledPan:", "animationStep:", it.animatedFraction)
if (mClearAnimation) {
it.cancel()
return@addUpdateListener
}
val currentPan = it.animatedValue as ScaledPoint
applyScaledPan(currentPan.x, currentPan.y, allowOverScroll)
}

panAnimator.start()
}
}

Expand Down Expand Up @@ -1596,6 +1594,21 @@ internal constructor(context: Context) : ViewTreeObserver.OnGlobalLayoutListener
return true
}

/**
* Cancels all currently active code driven animations (including fling animations)
* If no animation is currently active this is a no-op.
*
* @return true if anything was cancelled, false otherwise
*/
override fun cancelAnimations(): Boolean {
if (mState == FLINGING || mState == ANIMATING) {
setState(NONE)
return true
}

return false
}

//endregion

//region scrollbars helpers
Expand Down