Skip to content

Commit

Permalink
added cancelAnimations method to ZoomApi
Browse files Browse the repository at this point in the history
removed mClearAnimation flag and replaced it with mActiveAnimators which holds a list of all active animators that will be cancelled if a new animation starts or the state changes for other reasons
updated readme
  • Loading branch information
markusressel committed Feb 9, 2019
1 parent a2f9fb1 commit e6e251e
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 56 deletions.
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).|`-`|

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
122 changes: 66 additions & 56 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 @@ -1202,17 +1205,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 +1276,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(animator: Animator) {
cleanup(animator)
}

override fun onAnimationCancel(animation: Animator?) {
private fun cleanup(animator: Animator) {
animator.removeListener(this)
mActiveAnimators.remove(animator)
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 +1327,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 +1348,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,
PropertyValuesHolder.ofObject(
"pan",
TypeEvaluator { fraction: Float, startValue: AbsolutePoint, endValue: AbsolutePoint ->
Expand All @@ -1362,20 +1373,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 +1394,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 +1591,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

0 comments on commit e6e251e

Please sign in to comment.