Skip to content

Instantly share code, notes, and snippets.

@NathanWalker
Last active August 5, 2022 03:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NathanWalker/5309671b8d80a10ea88b5da9730e3476 to your computer and use it in GitHub Desktop.
Save NathanWalker/5309671b8d80a10ea88b5da9730e3476 to your computer and use it in GitHub Desktop.
Shimmer ported to Kotlin
package io.nstudio.ui
import android.animation.ValueAnimator
import android.animation.ValueAnimator.AnimatorUpdateListener
import android.annotation.TargetApi
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.graphics.RectF
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.ColorFilter
import android.graphics.LinearGradient
import android.graphics.Matrix
import android.graphics.PixelFormat
import android.graphics.PorterDuff
import android.graphics.PorterDuffXfermode
import android.graphics.RadialGradient
import android.graphics.Rect
import android.graphics.Shader
import android.graphics.drawable.Drawable
import android.os.Build
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import android.view.animation.LinearInterpolator
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.annotation.NonNull
import androidx.annotation.Nullable
class Shimmer internal constructor() {
/**
* The shape of the shimmer's highlight. By default LINEAR is used.
*/
enum class Shape(private val value: Int) {
/**
* Linear gives a ray reflection effect.
*/
LINEAR(0),
/**
* Radial gives a spotlight effect.
*/
RADIAL(1);
}
/**
* Direction of the shimmer's sweep.
*/
enum class Direction(private val value: Int) {
LEFT_TO_RIGHT(0), TOP_TO_BOTTOM(1), RIGHT_TO_LEFT(2), BOTTOM_TO_TOP(3);
}
@JvmField
val positions = FloatArray(COMPONENT_COUNT)
@JvmField
val colors = IntArray(COMPONENT_COUNT)
val bounds: RectF = RectF()
@JvmField
var direction = Direction.LEFT_TO_RIGHT
@ColorInt
var highlightColor: Int = Color.WHITE
@ColorInt
var baseColor = 0x4cffffff
@JvmField
var shape = Shape.LINEAR
var fixedWidth = 0
var fixedHeight = 0
var widthRatio = 1f
var heightRatio = 1f
var intensity = 0f
var dropoff = 0.5f
@JvmField
var tilt = 20f
@JvmField
var clipToChildren = true
@JvmField
var autoStart = false
@JvmField
var alphaShimmer = true
@JvmField
var repeatCount: Int = ValueAnimator.INFINITE
@JvmField
var repeatMode: Int = ValueAnimator.RESTART
@JvmField
var animationDuration = 1100L
@JvmField
var repeatDelay: Long = 0
@JvmField
var startDelay: Long = 0
fun width(width: Int): Int {
return if (fixedWidth > 0) fixedWidth else Math.round(widthRatio * width)
}
fun height(height: Int): Int {
return if (fixedHeight > 0) fixedHeight else Math.round(heightRatio * height)
}
fun updateColors() {
colors[0] = baseColor
colors[1] = highlightColor
colors[2] = highlightColor
colors[3] = baseColor
}
fun updatePositions() {
positions[0] = Math.max((1f - intensity - dropoff) / 2f, 0f)
positions[1] = Math.max((1f - intensity - 0.001f) / 2f, 0f)
positions[2] = Math.min((1f + intensity + 0.001f) / 2f, 1f)
positions[3] = Math.min((1f + intensity + dropoff) / 2f, 1f)
}
fun updateBounds(viewWidth: Int, viewHeight: Int) {
val magnitude: Int = Math.max(viewWidth, viewHeight)
val rad: Double = Math.PI / 2f - Math.toRadians(tilt % 90f)
val hyp: Float = magnitude / Math.sin(rad)
val padding: Float = 3 * Math.round((hyp - magnitude).toFloat() / 2f)
bounds.set(-padding, -padding, width(viewWidth) + padding, height(viewHeight) + padding)
}
abstract class Builder<T : Builder<T>?> {
val mShimmer = Shimmer()
// Gets around unchecked cast
protected abstract val `this`: T
/**
* Copies the configuration of an already built Shimmer to this builder
*/
fun copyFrom(other: Shimmer): T {
setDirection(other.direction)
setShape(other.shape)
setFixedWidth(other.fixedWidth)
setFixedHeight(other.fixedHeight)
setWidthRatio(other.widthRatio)
setHeightRatio(other.heightRatio)
setIntensity(other.intensity)
setDropoff(other.dropoff)
setTilt(other.tilt)
setClipToChildren(other.clipToChildren)
setAutoStart(other.autoStart)
setRepeatCount(other.repeatCount)
setRepeatMode(other.repeatMode)
setRepeatDelay(other.repeatDelay)
setStartDelay(other.startDelay)
setDuration(other.animationDuration)
setBaseColor(other.baseColor)
setHighLightColor(other.highlightColor)
return `this`
}
open fun setBaseColor(@ColorInt color: Int): T {
mShimmer.baseColor = color
return `this`
}
fun setHighLightColor(@ColorInt color: Int): T {
mShimmer.highlightColor = color
return `this`
}
/**
* Sets the direction of the shimmer's sweep. See [Direction].
*/
fun setDirection(direction: Int): T {
mShimmer.direction = direction
return `this`
}
/**
* Sets the fixed width of the shimmer, in pixels.
*/
fun setFixedWidth(fixedWidth: Int): T {
if (fixedWidth < 0) {
throw IllegalArgumentException("Given invalid width: $fixedWidth")
}
mShimmer.fixedWidth = fixedWidth
return `this`
}
/**
* Sets the fixed height of the shimmer, in pixels.
*/
fun setFixedHeight(fixedHeight: Int): T {
if (fixedHeight < 0) {
throw IllegalArgumentException("Given invalid height: $fixedHeight")
}
mShimmer.fixedHeight = fixedHeight
return `this`
}
/**
* Sets the width ratio of the shimmer, multiplied against the total width of the layout.
*/
fun setWidthRatio(widthRatio: Float): T {
if (widthRatio < 0f) {
throw IllegalArgumentException("Given invalid width ratio: $widthRatio")
}
mShimmer.widthRatio = widthRatio
return `this`
}
/**
* Sets the height ratio of the shimmer, multiplied against the total height of the layout.
*/
fun setHeightRatio(heightRatio: Float): T {
if (heightRatio < 0f) {
throw IllegalArgumentException("Given invalid height ratio: $heightRatio")
}
mShimmer.heightRatio = heightRatio
return `this`
}
/**
* Sets the intensity of the shimmer. A larger value causes the shimmer to be larger.
*/
fun setIntensity(intensity: Float): T {
if (intensity < 0f) {
throw IllegalArgumentException("Given invalid intensity value: $intensity")
}
mShimmer.intensity = intensity
return `this`
}
/**
* Sets how quickly the shimmer's gradient drops-off. A larger value causes a sharper drop-off.
*/
fun setDropoff(dropoff: Float): T {
if (dropoff < 0f) {
throw IllegalArgumentException("Given invalid dropoff value: $dropoff")
}
mShimmer.dropoff = dropoff
return `this`
}
/**
* Sets the tilt angle of the shimmer in degrees.
*/
fun setTilt(tilt: Float): T {
mShimmer.tilt = tilt
return `this`
}
/**
* Sets the base alpha, which is the alpha of the underlying children, amount in the range [0,
* 1].
*/
fun setBaseAlpha(@FloatRange(from = 0, to = 1) alpha: Float): T {
val intAlpha = (clamp(0f, 1f, alpha) * 255f).toInt()
mShimmer.baseColor = intAlpha shl 24 or (mShimmer.baseColor and 0x00FFFFFF)
return `this`
}
/**
* Sets the shimmer alpha amount in the range [0, 1].
*/
fun setHighlightAlpha(@FloatRange(from = 0, to = 1) alpha: Float): T {
val intAlpha = (clamp(0f, 1f, alpha) * 255f).toInt()
mShimmer.highlightColor = intAlpha shl 24 or (mShimmer.highlightColor and 0x00FFFFFF)
return `this`
}
/**
* Sets whether the shimmer will clip to the childrens' contents, or if it will opaquely draw on
* top of the children.
*/
fun setClipToChildren(status: Boolean): T {
mShimmer.clipToChildren = status
return `this`
}
/**
* Sets whether the shimmering animation will start automatically.
*/
fun setAutoStart(status: Boolean): T {
mShimmer.autoStart = status
return `this`
}
/**
* Sets how often the shimmering animation will repeat. See [ ][android.animation.ValueAnimator.setRepeatCount].
*/
fun setRepeatCount(repeatCount: Int): T {
mShimmer.repeatCount = repeatCount
return `this`
}
/**
* Sets how the shimmering animation will repeat. See [ ][android.animation.ValueAnimator.setRepeatMode].
*/
fun setRepeatMode(mode: Int): T {
mShimmer.repeatMode = mode
return `this`
}
/**
* Sets how long to wait in between repeats of the shimmering animation.
*/
fun setRepeatDelay(millis: Long): T {
if (millis < 0) {
throw IllegalArgumentException("Given a negative repeat delay: $millis")
}
mShimmer.repeatDelay = millis
return `this`
}
/**
* Sets how long to wait for starting the shimmering animation.
*/
fun setStartDelay(millis: Long): T {
if (millis < 0) {
throw IllegalArgumentException("Given a negative start delay: $millis")
}
mShimmer.startDelay = millis
return `this`
}
/**
* Sets how long the shimmering animation takes to do one full sweep.
*/
fun setDuration(millis: Long): T {
if (millis < 0) {
throw IllegalArgumentException("Given a negative duration: $millis")
}
mShimmer.animationDuration = millis
return `this`
}
fun build(): Shimmer {
mShimmer.updateColors()
mShimmer.updatePositions()
return mShimmer
}
companion object {
private fun clamp(min: Float, max: Float, value: Float): Float {
return Math.min(max, Math.max(min, value))
}
}
}
class AlphaHighlightBuilder : Builder<AlphaHighlightBuilder>() {
@get:Override
override val `this`: T
protected get() = this
init {
mShimmer.alphaShimmer = true
}
}
class ColorHighlightBuilder : Builder<ColorHighlightBuilder>() {
/**
* Sets the highlight color for the shimmer.
*/
fun setHighlightColor(@ColorInt color: Int): ColorHighlightBuilder {
mShimmer.highlightColor = color
return `this`
}
/**
* Sets the base color for the shimmer.
*/
override fun setBaseColor(@ColorInt color: Int): ColorHighlightBuilder {
mShimmer.baseColor = mShimmer.baseColor and -0x1000000 or (color and 0x00FFFFFF)
return `this`
}
@get:Override
override val `this`: T
protected get() = this
init {
mShimmer.alphaShimmer = false
}
}
companion object {
private const val COMPONENT_COUNT = 4
}
}
class ShimmerDrawable : Drawable() {
private val mUpdateListener: ValueAnimator.AnimatorUpdateListener =
object : AnimatorUpdateListener() {
@Override
override fun onAnimationUpdate(animation: ValueAnimator?) {
invalidateSelf()
}
}
private val mShimmerPaint: Paint = Paint()
private val mDrawRect: Rect = Rect()
private val mShaderMatrix: Matrix = Matrix()
@Nullable
private var mValueAnimator: ValueAnimator? = null
@Nullable
private var mShimmer: Shimmer? = null
@get:Nullable
var shimmer: Shimmer?
get() = mShimmer
set(shimmer) {
mShimmer = shimmer
if (mShimmer != null) {
mShimmerPaint.setXfermode(
PorterDuffXfermode(
if (mShimmer!!.alphaShimmer) PorterDuff.Mode.DST_IN else PorterDuff.Mode.SRC_IN
)
)
}
updateShader()
updateValueAnimator()
invalidateSelf()
}
/**
* Starts the shimmer animation.
*/
fun startShimmer() {
if (mValueAnimator != null && !isShimmerStarted && getCallback() != null) {
mValueAnimator!!.start()
}
}
/**
* Stops the shimmer animation.
*/
fun stopShimmer() {
if (mValueAnimator != null && isShimmerStarted) {
mValueAnimator!!.cancel()
}
}
/**
* Return whether the shimmer animation has been started.
*/
val isShimmerStarted: Boolean
get() = mValueAnimator != null && mValueAnimator!!.isStarted()
@Override
override fun onBoundsChange(bounds: Rect?) {
super.onBoundsChange(bounds)
mDrawRect.set(bounds)
updateShader()
maybeStartShimmer()
}
@Override
override fun draw(@NonNull canvas: Canvas) {
if (mShimmer == null || mShimmerPaint.getShader() == null) {
return
}
val tiltTan = Math.tan(Math.toRadians(mShimmer!!.tilt)) as Float
val translateHeight: Float = mDrawRect.height() + tiltTan * mDrawRect.width()
val translateWidth: Float = mDrawRect.width() + tiltTan * mDrawRect.height()
val dx: Float
val dy: Float
val animatedValue = if (mValueAnimator != null) mValueAnimator!!.getAnimatedValue() else 0f
dx = offset(-translateWidth, translateWidth, animatedValue)
dy = 0f
mShaderMatrix.reset()
mShaderMatrix.setRotate(mShimmer!!.tilt, mDrawRect.width() / 2f, mDrawRect.height() / 2f)
mShaderMatrix.postTranslate(dx, dy)
mShimmerPaint.getShader().setLocalMatrix(mShaderMatrix)
canvas.drawRect(mDrawRect, mShimmerPaint)
}
@Override
override fun setAlpha(alpha: Int) {
// No-op, modify the Shimmer object you pass in instead
}
@Override
override fun setColorFilter(@Nullable colorFilter: ColorFilter?) {
// No-op, modify the Shimmer object you pass in instead
}
@Override
override fun getOpacity(): Int {
if (mShimmer != null && (mShimmer!!.clipToChildren || mShimmer!!.alphaShimmer)) {
return PixelFormat.TRANSLUCENT
} else {
return PixelFormat.OPAQUE
}
}
//@get:Override
val opacity: Int
get() = if (mShimmer != null && (mShimmer!!.clipToChildren || mShimmer!!.alphaShimmer)) PixelFormat.TRANSLUCENT else PixelFormat.OPAQUE
private fun offset(start: Float, end: Float, percent: Float): Float {
return start + (end - start) * percent
}
private fun updateValueAnimator() {
if (mShimmer == null) {
return
}
val started: Boolean
if (mValueAnimator != null) {
started = mValueAnimator!!.isStarted()
mValueAnimator!!.cancel()
mValueAnimator!!.removeAllUpdateListeners()
} else {
started = false
}
mValueAnimator = ValueAnimator.ofFloat(
0f,
1f + (mShimmer!!.repeatDelay / mShimmer!!.animationDuration).toFloat()
)
mValueAnimator!!.setInterpolator(LinearInterpolator())
mValueAnimator!!.setRepeatMode(mShimmer!!.repeatMode)
mValueAnimator!!.setStartDelay(mShimmer!!.startDelay)
mValueAnimator!!.setRepeatCount(mShimmer!!.repeatCount)
mValueAnimator!!.setDuration(mShimmer!!.animationDuration + mShimmer!!.repeatDelay)
mValueAnimator!!.addUpdateListener(mUpdateListener)
if (started) {
mValueAnimator!!.start()
}
}
fun maybeStartShimmer() {
if (mValueAnimator != null && !mValueAnimator!!.isStarted()
&& mShimmer != null && mShimmer!!.autoStart
&& getCallback() != null
) {
mValueAnimator!!.start()
}
}
private fun updateShader() {
val bounds: Rect = getBounds()
val boundsWidth: Int = bounds.width()
val boundsHeight: Int = bounds.height()
if (boundsWidth == 0 || boundsHeight == 0 || mShimmer == null) {
return
}
val width = mShimmer!!.width(boundsWidth)
val height = mShimmer!!.height(boundsHeight)
val shader: Shader
val vertical = false
val endX = if (vertical) 0 else width
val endY = if (vertical) height else 0
shader = LinearGradient(
0, 0, endX, endY, mShimmer!!.colors, mShimmer!!.positions, Shader.TileMode.CLAMP
)
mShimmerPaint.setShader(shader)
}
init {
mShimmerPaint.setAntiAlias(true)
}
}
class ShimmerView : FrameLayout {
private val mContentPaint: Paint = Paint()
private val mShimmerDrawable: ShimmerDrawable? = ShimmerDrawable()
/**
* Return whether the shimmer drawable is visible.
*/
var isShimmerVisible = true
private var mStoppedShimmerBecauseVisibility = false
constructor(context: Context) : super(context) {
init(context, null)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
init(context, attrs)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
init(context, attrs)
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
constructor(
context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
) : super(context, attrs, defStyleAttr, defStyleRes) {
init(context, attrs)
}
private fun init(context: Context, @Nullable attrs: AttributeSet?) {
setWillNotDraw(false)
mShimmerDrawable.setCallback(this)
if (attrs == null) {
setShimmer(Shimmer.AlphaHighlightBuilder().build())
return
}
}
fun setShimmer(@Nullable shimmer: Shimmer?): ShimmerView {
mShimmerDrawable!!.shimmer = shimmer
if (shimmer != null && shimmer.clipToChildren) {
setLayerType(LAYER_TYPE_HARDWARE, mContentPaint)
} else {
setLayerType(LAYER_TYPE_NONE, null)
}
return this
}
@get:Nullable
val shimmer: Shimmer?
get() = mShimmerDrawable!!.shimmer
private var mSpeed: Long = 1100
fun setSpeed(speed: Long) {
if (speed > 0) {
mSpeed = speed
}
if (shimmer != null) {
val builder: Shimmer.Builder<*> = Shimmer.AlphaHighlightBuilder()
builder.copyFrom(shimmer!!)
builder.setDuration(speed)
setShimmer(builder.build())
}
}
fun setLightColor(@ColorInt color: Int) {
if (shimmer != null) {
val builder: Shimmer.Builder<*> = Shimmer.AlphaHighlightBuilder()
builder.copyFrom(shimmer!!)
builder.setHighLightColor(color)
setShimmer(builder.build())
}
}
fun setDarkColor(@ColorInt color: Int) {
if (shimmer != null) {
val builder: Shimmer.Builder<*> = Shimmer.AlphaHighlightBuilder()
builder.copyFrom(shimmer!!)
builder.setBaseColor(color)
setShimmer(builder.build())
}
}
fun start(
speed: Long,
direction: Int,
repeatCount: Int,
@ColorInt lightColor: Int,
@ColorInt blackColor: Int
) {
if (shimmer != null) {
val builder: Shimmer.Builder<*> = Shimmer.AlphaHighlightBuilder()
builder.copyFrom(shimmer!!)
builder.setDuration(speed)
when (direction) {
0 -> builder.setDirection(0)
else -> {}
}
builder.setRepeatCount(repeatCount)
builder.setHighLightColor(lightColor)
builder.setBaseColor(blackColor)
setShimmer(builder.build())
}
showShimmer(true)
}
/**
* Starts the shimmer animation.
*/
fun startShimmer() {
mShimmerDrawable!!.startShimmer()
}
/**
* Stops the shimmer animation.
*/
fun stopShimmer() {
mStoppedShimmerBecauseVisibility = false
mShimmerDrawable!!.stopShimmer()
}
/**
* Return whether the shimmer animation has been started.
*/
val isShimmerStarted: Boolean
get() = mShimmerDrawable!!.isShimmerStarted
/**
* Sets the ShimmerDrawable to be visible.
*
* @param startShimmer Whether to start the shimmer again.
*/
fun showShimmer(startShimmer: Boolean) {
isShimmerVisible = true
if (startShimmer) {
startShimmer()
}
invalidate()
}
/**
* Sets the ShimmerDrawable to be invisible, stopping it in the process.
*/
fun hideShimmer() {
stopShimmer()
isShimmerVisible = false
invalidate()
}
@Override
fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
super.onLayout(changed, left, top, right, bottom)
val width: Int = getWidth()
val height: Int = getHeight()
mShimmerDrawable.setBounds(0, 0, width, height)
}
@Override
protected fun onVisibilityChanged(@NonNull changedView: View?, visibility: Int) {
super.onVisibilityChanged(changedView, visibility)
// View's constructor directly invokes this method, in which case no fields on
// this class have been fully initialized yet.
if (mShimmerDrawable == null) {
return
}
if (visibility != VISIBLE) {
// GONE or INVISIBLE
if (isShimmerStarted) {
stopShimmer()
mStoppedShimmerBecauseVisibility = true
}
} else if (mStoppedShimmerBecauseVisibility) {
mShimmerDrawable.maybeStartShimmer()
mStoppedShimmerBecauseVisibility = false
}
}
@Override
override fun onAttachedToWindow() {
super.onAttachedToWindow()
mShimmerDrawable!!.maybeStartShimmer()
}
@Override
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
stopShimmer()
}
@Override
override fun dispatchDraw(canvas: Canvas?) {
super.dispatchDraw(canvas)
if (isShimmerVisible) {
mShimmerDrawable!!.draw(canvas)
}
}
@Override
protected override fun verifyDrawable(@NonNull who: Drawable): Boolean {
return super.verifyDrawable(who) || who === mShimmerDrawable
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment