Created
April 17, 2016 04:33
-
-
Save sigmabeta/70ce3c83a31907e30eb0b9d8128cafd9 to your computer and use it in GitHub Desktop.
Custom Activity Transition for Picasso CenterCrop'd ImageViews
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.* | |
import android.graphics.Matrix | |
import android.graphics.PointF | |
import android.graphics.Rect | |
import android.transition.Transition | |
import android.transition.TransitionValues | |
import android.util.Property | |
import android.view.View | |
import android.view.ViewGroup | |
import android.widget.ImageView | |
import java.lang.reflect.Method | |
import java.util.* | |
class CustomImageTransform(val enter: Boolean) : Transition() { | |
override fun captureStartValues(transitionValues: TransitionValues) { | |
captureValues(transitionValues) | |
} | |
override fun captureEndValues(transitionValues: TransitionValues) { | |
captureValues(transitionValues) | |
} | |
/** | |
* Places the bounds of the view before and after animation into | |
* the TransitionValues K/V map, which is passed to createAnimator(). | |
*/ | |
private fun captureValues(transitionValues: TransitionValues) { | |
val view = transitionValues.view | |
if (view !is ImageView || view.visibility != View.VISIBLE) { | |
return | |
} | |
val values = transitionValues.values | |
val left = view.getLeft() | |
val top = view.getTop() | |
val right = view.getRight() | |
val bottom = view.getBottom() | |
val bounds = Rect(left, top, right, bottom) | |
values.put(PROPERTY_BOUNDS, bounds) | |
} | |
/** | |
* Pulls out the bounds collected above in captureValues(), then feeds that information to the three | |
* animator creation methods, which hopefully all return an actual animator; but either way, the animators | |
* are combined into an AnimatorSet, and returned to the Transition framework. | |
*/ | |
override fun createAnimator(sceneRoot: ViewGroup, startValues: TransitionValues?, endValues: TransitionValues?): Animator? { | |
startValues ?: return null | |
endValues ?: return null | |
val startBounds = startValues.values[PROPERTY_BOUNDS] as Rect | |
val endBounds = endValues.values[PROPERTY_BOUNDS] as Rect | |
if (startBounds == endBounds) { | |
return null | |
} | |
val imageView = endValues.view as ImageView | |
val drawable = imageView.drawable | |
val drawableWidth = drawable.intrinsicWidth | |
val drawableHeight = drawable.intrinsicHeight | |
val animators = ArrayList<Animator>(3) | |
createMatrixAnimator(imageView, startBounds, endBounds, drawableWidth, drawableHeight).let { animators.add(it) } | |
createPositionAnimator(imageView, startBounds, endBounds)?.let { animators.add(it) } | |
createClipAnimator(imageView, startBounds, endBounds)?.let { animators.add(it) } | |
val set = AnimatorSet() | |
set.playTogether(animators) | |
return set | |
} | |
/** | |
* The main secret sauce here. Since Picasso throws out all the data it doesn't display, it doesn't use | |
* a Matrix centerCrop your image. It doesn't set a Matrix at all, really. So ChangeImageTransform doesn't find | |
* one, and basically gives up. Here, we create Matrices using the information available to us, and create | |
* an animation out of it. Lemonade out of lemons, if you will. | |
* | |
* The Matrix is used to position the bitmap inside the ImageView's bounds, possibly scaling and skewing it as well. | |
* Without this animation, the image stays the same size throughout the transition, and sticks to the top left of | |
* its bounds. | |
*/ | |
private fun createMatrixAnimator(imageView: ImageView, startBounds: Rect, endBounds: Rect, drawableWidth: Int, drawableHeight: Int): ObjectAnimator { | |
val startLeft = startBounds.left | |
val startTop = startBounds.top | |
val startRight = startBounds.right | |
val startBottom = startBounds.bottom | |
val endLeft = endBounds.left | |
val endTop = endBounds.top | |
val endRight = endBounds.right | |
val endBottom = endBounds.bottom | |
val startWidth = startRight - startLeft | |
val startHeight = startBottom - startTop | |
val endWidth = endRight - endLeft | |
val endHeight = endBottom - endTop | |
val smallWidth = Math.min(startWidth, endWidth) | |
val smallHeight = Math.min(startHeight, endHeight) | |
// Determine the direction in which the start & end images are being clipped. | |
val startClipDirection = if ((drawableWidth / drawableHeight) > (startWidth / startHeight)) true else false | |
val endClipDirection = if ((drawableWidth / drawableHeight) > (endWidth / endHeight)) true else false | |
// Create a pair of Identity Matrices. | |
val smallMatrix = Matrix() | |
val bigMatrix = Matrix() | |
val scaleFactor: Float | |
val smallTransX: Float | |
val smallTransY: Float | |
val startMatrix: Matrix | |
val endMatrix: Matrix | |
// The smaller image's matrix is the one that must be configured. | |
// If this is an "enter" transition, that is the starting Matrix. | |
if (enter) { | |
startMatrix = smallMatrix | |
endMatrix = bigMatrix | |
// If the smaller ImageView's aspect ratio is taller than the source Drawable. | |
if (startClipDirection) { | |
scaleFactor = startHeight.toFloat() / endHeight.toFloat() | |
smallTransX = ((scaleFactor * drawableWidth) - smallWidth) / -2.0f | |
smallTransY = 0.0f | |
} else { | |
scaleFactor = startWidth.toFloat() / endWidth.toFloat() | |
smallTransY = ((scaleFactor * drawableHeight) - smallHeight) / -2.0f | |
smallTransX = 0.0f | |
} | |
} else { | |
startMatrix = bigMatrix | |
endMatrix = smallMatrix | |
// If this is a "return" transition, the ending image is the smaller one, | |
// and so we check if its aspect ratio is taller than the source Drawable. | |
if (endClipDirection) { | |
scaleFactor = endHeight.toFloat() / startHeight.toFloat() | |
smallTransX = ((scaleFactor * drawableWidth) - smallWidth) / -2.0f | |
smallTransY = 0.0f | |
} else { | |
scaleFactor = endWidth.toFloat() / startWidth.toFloat() | |
smallTransY = ((scaleFactor * drawableHeight) - smallHeight) / -2.0f | |
smallTransX = 0.0f | |
} | |
} | |
// Configure the "small" matrix. | |
val smallMatrixValues = FloatArray(9) | |
smallMatrix.getValues(smallMatrixValues) | |
smallMatrixValues[Matrix.MTRANS_X] = smallTransX | |
smallMatrixValues[Matrix.MTRANS_Y] = smallTransY | |
smallMatrixValues[Matrix.MSCALE_X] = scaleFactor | |
smallMatrixValues[Matrix.MSCALE_Y] = scaleFactor | |
smallMatrix.setValues(smallMatrixValues) | |
return ObjectAnimator.ofObject(imageView, ANIMATED_TRANSFORM_PROPERTY, CustomMatrixEvaluator(), startMatrix, endMatrix) | |
} | |
/** | |
* Animates the view's position. Without this, the ImageView will resize and clip properly, | |
* but the top left corner of the view will not move to its proper location, or at all. | |
*/ | |
private fun createPositionAnimator(view: View, startBounds: Rect, endBounds: Rect): ObjectAnimator? { | |
val startLeft = startBounds.left | |
val endLeft = endBounds.left | |
val startTop = startBounds.top | |
val endTop = endBounds.top | |
if (startLeft != endLeft || startTop != endTop) { | |
val topLeftPath = pathMotion.getPath(startLeft.toFloat(), startTop.toFloat(), endLeft.toFloat(), endTop.toFloat()) | |
return ObjectAnimator.ofObject<View, PointF>(view, POSITION_PROPERTY, null, topLeftPath) | |
} | |
return null | |
} | |
/** | |
* Animates the view's clipping box. Without this animation, the ImageView will be occluded | |
* by other views on screen. | |
*/ | |
private fun createClipAnimator(view: View, startBounds: Rect, endBounds: Rect): ValueAnimator? { | |
val startLeft = startBounds.left | |
val startTop = startBounds.top | |
val startRight = startBounds.right | |
val startBottom = startBounds.bottom | |
val endLeft = endBounds.left | |
val endTop = endBounds.top | |
val endRight = endBounds.right | |
val endBottom = endBounds.bottom | |
val startWidth = startRight - startLeft | |
val startHeight = startBottom - startTop | |
val endWidth = endRight - endLeft | |
val endHeight = endBottom - endTop | |
val maxWidth = Math.max(startWidth, endWidth) | |
val maxHeight = Math.max(startHeight, endHeight) | |
setLeftTopRightBottom(view, startLeft, startTop, startLeft + maxWidth, startTop + maxHeight) | |
val startClip = Rect(0, 0, startWidth, startHeight) | |
val endClip = Rect(0, 0, endWidth, endHeight) | |
if (startClip != endClip) { | |
view.clipBounds = startClip | |
val clipAnimator = ValueAnimator.ofObject(EVALUATOR_RECT, startClip, endClip) | |
clipAnimator.addUpdateListener { animation -> | |
val clipBounds = animation.animatedValue as Rect | |
view.clipBounds = clipBounds | |
} | |
clipAnimator.addListener(object : AnimatorListenerAdapter() { | |
private var canceled = false | |
override fun onAnimationCancel(animation: Animator) { | |
canceled = true | |
} | |
override fun onAnimationEnd(animation: Animator) { | |
if (!canceled) { | |
view.clipBounds = endClip | |
setLeftTopRightBottom(view, endLeft, endTop, endRight, endBottom) | |
} | |
} | |
}) | |
return clipAnimator | |
} | |
return null | |
} | |
/** | |
* Kotlin-ese for "static stuff goes here" | |
*/ | |
companion object { | |
val PROPERTY_BOUNDS = "${BuildConfig.APPLICATION_ID}.transition.property.bounds" | |
val EVALUATOR_RECT = RectEvaluator() | |
var METHOD_ANIMATE_TRANSFORM: Method? = null | |
private val ANIMATED_TRANSFORM_PROPERTY = object : Property<ImageView, Matrix>(Matrix::class.java, "animatedTransform") { | |
override fun set(image: ImageView, value: Matrix) { | |
animateTransform(image, value) | |
} | |
override fun get(image: ImageView): Matrix? { | |
return null | |
} | |
} | |
private val POSITION_PROPERTY = object : Property<View, PointF>(PointF::class.java, "position") { | |
override fun set(view: View, topLeft: PointF) { | |
val left = Math.round(topLeft.x) | |
val top = Math.round(topLeft.y) | |
val right = left + view.width | |
val bottom = top + view.height | |
setLeftTopRightBottom(view, left, top, right, bottom) | |
} | |
override fun get(view: View): PointF? { | |
return null | |
} | |
} | |
private fun animateTransform(image: ImageView, value: Matrix) { | |
/** | |
* Doesn't really seem to be a way to avoid calling this method using reflection. | |
*/ | |
if (METHOD_ANIMATE_TRANSFORM == null) { | |
val clazz = Class.forName("android.widget.ImageView") | |
METHOD_ANIMATE_TRANSFORM = clazz.getMethod("animateTransform", Matrix::class.java) | |
} | |
METHOD_ANIMATE_TRANSFORM!!.invoke(image, value) | |
} | |
/** | |
* Calling the framework's implementation, as ChangeBounds() does, breaks the Matrix animation | |
* because the Matrix we write to the ImageView in that animation gets overwritten. So we just | |
* set the bounds manually here. | |
*/ | |
private fun setLeftTopRightBottom(view: View, left: Int, top: Int, right: Int, bottom: Int) { | |
view.left = left | |
view.right = right | |
view.top = top | |
view.bottom = bottom | |
view.invalidate() | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.TypeEvaluator | |
import android.graphics.Matrix | |
class CustomMatrixEvaluator : TypeEvaluator<Matrix> { | |
val startValueArray = FloatArray(9) | |
val endValueArray = FloatArray(9) | |
var tempStartValues = FloatArray(9) | |
var tempEndValues = FloatArray(9) | |
var tempMatrix = Matrix() | |
override fun evaluate(fraction: Float, startValue: Matrix, endValue: Matrix): Matrix { | |
startValue.getValues(startValueArray) | |
endValue.getValues(endValueArray) | |
startValue.getValues(tempStartValues) | |
endValue.getValues(tempEndValues) | |
for (i in 0..8) { | |
val diff = tempEndValues[i] - tempStartValues[i] | |
val result = tempStartValues[i] + fraction * diff | |
tempEndValues[i] = result | |
} | |
tempMatrix.setValues(tempEndValues) | |
return tempMatrix | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class DetailActivity() : AppCompatActivity() { | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
// ... | |
val enter = CustomImageTransform(true) | |
val returnTransition = CustomImageTransform(false) | |
window.sharedElementEnterTransition = enter | |
window.sharedElementReturnTransition = returnTransition | |
} | |
// ... | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment