mpao
6/27/2018 - 12:06 PM

Material Image Loading treatment for Android

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
            }
        }
    }
}