Skip to content

Instantly share code, notes, and snippets.

@chrisbanes
Last active March 7, 2024 11:13
Show Gist options
  • Star 27 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save chrisbanes/b0db5e852035ab8c2a49803bac526019 to your computer and use it in GitHub Desktop.
Save chrisbanes/b0db5e852035ab8c2a49803bac526019 to your computer and use it in GitHub Desktop.
Material Image Loading treatment for Android
/*
* Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ObjectAnimator
import android.graphics.ColorMatrixColorFilter
import android.graphics.drawable.Drawable
import android.support.v4.view.animation.FastOutSlowInInterpolator
import android.view.View
import androidx.core.animation.doOnEnd
import kotlin.math.roundToLong
private val fastOutSlowInInterpolator = FastOutSlowInInterpolator()
fun saturateDrawableAnimator(current: Drawable, view: View): Animator {
view.setHasTransientState(true)
val cm = ImageLoadingColorMatrix()
val duration = 1500L
val satAnim = ObjectAnimator.ofFloat(cm, ImageLoadingColorMatrix.PROP_SATURATION, 0f, 1f)
satAnim.duration = duration
satAnim.interpolator = fastOutSlowInInterpolator
satAnim.addUpdateListener { current.colorFilter = ColorMatrixColorFilter(cm) }
val alphaAnim = ObjectAnimator.ofFloat(cm, ImageLoadingColorMatrix.PROP_ALPHA, 0f, 1f)
alphaAnim.duration = duration / 2
alphaAnim.interpolator = fastOutSlowInInterpolator
val darkenAnim = ObjectAnimator.ofFloat(cm, ImageLoadingColorMatrix.PROP_DARKEN, 0f, 1f)
darkenAnim.duration = (duration * 0.75f).roundToLong()
darkenAnim.interpolator = fastOutSlowInInterpolator
val set = AnimatorSet()
set.playTogether(satAnim, alphaAnim, darkenAnim)
set.doOnEnd {
current.clearColorFilter()
view.setHasTransientState(false)
}
return set
}
/*
* Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.request.transition.NoTransition
import com.bumptech.glide.request.transition.Transition
import com.bumptech.glide.request.transition.TransitionFactory
class SaturationTransitionFactory : TransitionFactory<Drawable> {
override fun build(dataSource: DataSource, isFirstResource: Boolean): Transition<Drawable> {
return if (isFirstResource && dataSource != DataSource.MEMORY_CACHE) {
// Only start the transition if this is not a recent load. We approximate that by
// checking if the image is from the memory cache
SaturationTransition()
} else {
NoTransition<Drawable>()
}
}
}
internal class SaturationTransition : Transition<Drawable> {
override fun transition(current: Drawable, adapter: Transition.ViewAdapter): Boolean {
saturateDrawableAnimator(current, adapter.view).also {
it.start()
}
// We want Glide to still set the drawable
return false
}
}
@GlideExtension
object GlideExtensions {
@JvmStatic
@GlideType(Drawable::class)
fun saturateOnLoad(requestBuilder: RequestBuilder<Drawable>) {
requestBuilder.transition(DrawableTransitionOptions.with(SaturationTransitionFactory()))
}
}
/*
* Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.graphics.ColorMatrix
/**
* An extension to [ColorMatrix] which implements the Material image loading pattern
*/
class ImageLoadingColorMatrix : ColorMatrix() {
private val elements = FloatArray(20)
var saturationFraction = 1f
set(value) {
System.arraycopy(array, 0, elements, 0, 20)
// Taken from ColorMatrix.setSaturation. We can't use that though since it resets the matrix
// before applying the values
val invSat = 1 - value
val r = 0.213f * invSat
val g = 0.715f * invSat
val b = 0.072f * invSat
elements[0] = r + value
elements[1] = g
elements[2] = b
elements[5] = r
elements[6] = g + value
elements[7] = b
elements[10] = r
elements[11] = g
elements[12] = b + value
set(elements)
}
var alphaFraction = 1f
set(value) {
System.arraycopy(array, 0, elements, 0, 20)
elements[18] = value
set(elements)
}
var darkenFraction = 1f
set(value) {
System.arraycopy(array, 0, elements, 0, 20)
// We substract to make the picture look darker, it will automatically clamp
val darkening = (1 - value) * MAX_DARKEN_PERCENTAGE * 255
elements[4] = -darkening
elements[9] = -darkening
elements[14] = -darkening
set(elements)
}
companion object {
private val saturationFloatProp = object : FloatProp<ImageLoadingColorMatrix>("saturation") {
override operator fun get(o: ImageLoadingColorMatrix): Float = o.saturationFraction
override operator fun set(o: ImageLoadingColorMatrix, value: Float) {
o.saturationFraction = value
}
}
private val alphaFloatProp = object : FloatProp<ImageLoadingColorMatrix>("alpha") {
override operator fun get(o: ImageLoadingColorMatrix): Float = o.alphaFraction
override operator fun set(o: ImageLoadingColorMatrix, value: Float) {
o.alphaFraction = value
}
}
private val darkenFloatProp = object : FloatProp<ImageLoadingColorMatrix>("darken") {
override operator fun get(o: ImageLoadingColorMatrix): Float = o.darkenFraction
override operator fun set(o: ImageLoadingColorMatrix, value: Float) {
o.darkenFraction = value
}
}
val PROP_SATURATION = createFloatProperty(saturationFloatProp)
val PROP_ALPHA = createFloatProperty(alphaFloatProp)
val PROP_DARKEN = createFloatProperty(darkenFloatProp)
// This means that we darken the image by 20%
private const val MAX_DARKEN_PERCENTAGE = 0.20f
}
}
/*
* Copyright 2018 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.os.Build
import android.util.FloatProperty
import android.util.Property
/**
* A delegate for creating a [Property] of `float` type
*/
abstract class FloatProp<T>(val name: String) {
abstract operator fun set(o: T, value: Float)
abstract operator fun get(o: T): Float
}
fun <T> createFloatProperty(impl: FloatProp<T>): Property<T, Float> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
object : FloatProperty<T>(impl.name) {
override fun get(o: T): Float = impl[o]
override fun setValue(o: T, value: Float) {
impl[o] = value
}
}
} else {
object : Property<T, Float>(Float::class.java, impl.name) {
override fun get(o: T): Float = impl[o]
override fun set(o: T, value: Float) {
impl[o] = value
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment