Skip to content

Instantly share code, notes, and snippets.

@triniwiz
Created December 5, 2021 06:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save triniwiz/b3f87c8d3d07d0c57f5f2c13ae14d71d to your computer and use it in GitHub Desktop.
Save triniwiz/b3f87c8d3d07d0c57f5f2c13ae14d71d to your computer and use it in GitHub Desktop.
Shimmer ported to Typescript
/**
* The shape of the shimmer's highlight. By default LINEAR is used.
*/
enum Shape {
/**
* Linear gives a ray reflection effect.
*/
LINEAR,
/**
* Radial gives a spotlight effect.
*/
RADIAL
}
/**
* Direction of the shimmer's sweep.
*/
enum Direction {
LEFT_TO_RIGHT,
TOP_TO_BOTTOM,
RIGHT_TO_LEFT,
BOTTOM_TO_TOP
}
const INFINITE = -1;
const RESTART = 1;
class TNSShimmer {
static COMPONENT_COUNT = 4;
positions = Array.create('float', TNSShimmer.COMPONENT_COUNT);
colors = Array.create('int', TNSShimmer.COMPONENT_COUNT);
bounds = new android.graphics.RectF();
direction = Direction.LEFT_TO_RIGHT;
highlightColor = new Color('white');
baseColor = new Color(0x4cffffff);
shape = Shape.LINEAR;
fixedWidth = 0;
fixedHeight = 0;
widthRatio = 1;
heightRatio = 1;
intensity = 0;
dropoff = 0.5;
tilt = 20;
clipToChildren = true;
autoStart = false;
alphaShimmer = true;
repeatCount = INFINITE;
repeatMode = RESTART;
animationDuration = 1100;
repeatDelay = 0;
startDelay = 0;
constructor() { }
width(width: number) {
return this.fixedWidth > 0 ? this.fixedWidth : Math.round(this.widthRatio * width);
}
height(height: number) {
return this.fixedHeight > 0 ? this.fixedHeight : Math.round(this.heightRatio * height);
}
updateColors() {
switch (this.shape) {
default:
case Shape.LINEAR:
this.colors[0] = this.baseColor.android;
this.colors[1] = this.highlightColor.android;
this.colors[2] = this.highlightColor.android;
this.colors[3] = this.baseColor.android;
break;
case Shape.RADIAL:
this.colors[0] = this.highlightColor.android;
this.colors[1] = this.highlightColor.android;
this.colors[2] = this.baseColor.android;
this.colors[3] = this.baseColor.android;
break;
}
}
updatePositions() {
switch (this.shape) {
default:
case Shape.LINEAR:
this.positions[0] = Math.max((1 - this.intensity - this.dropoff) / 2, 0);
this.positions[1] = Math.max((1 - this.intensity - 0.001) / 2, 0);
this.positions[2] = Math.min((1 + this.intensity + 0.001) / 2, 1);
this.positions[3] = Math.min((1 + this.intensity + this.dropoff) / 2, 1);
break;
case Shape.RADIAL:
this.positions[0] = 0;
this.positions[1] = Math.min(this.intensity, 1);
this.positions[2] = Math.min(this.intensity + this.dropoff, 1);
this.positions[3] = 1;
break;
}
}
updateBounds(viewWidth: number, viewHeight: number) {
const magnitude = Math.max(viewWidth, viewHeight);
const rad = Math.PI / 2 - java.lang.Math.toRadians(this.tilt % 90);
const hyp = magnitude / Math.sin(rad);
const padding = 3 * Math.round((hyp - magnitude) / 2);
this.bounds.set(-padding, -padding, this.width(viewWidth) + padding, this.height(viewHeight) + padding);
}
}
abstract class Builder<T extends Builder<T>> {
mShimmer: TNSShimmer;
constructor() {
this.mShimmer = new TNSShimmer();
}
// Gets around unchecked cast
protected abstract getThis(): T;
/**
* Copies the configuration of an already built Shimmer to this builder
*/
public copyFrom(other: TNSShimmer): T {
this.setDirection(other.direction);
this.setShape(other.shape);
this.setFixedWidth(other.fixedWidth);
this.setFixedHeight(other.fixedHeight);
this.setWidthRatio(other.widthRatio);
this.setHeightRatio(other.heightRatio);
this.setIntensity(other.intensity);
this.setDropoff(other.dropoff);
this.setTilt(other.tilt);
this.setClipToChildren(other.clipToChildren);
this.setAutoStart(other.autoStart);
this.setRepeatCount(other.repeatCount);
this.setRepeatMode(other.repeatMode);
this.setRepeatDelay(other.repeatDelay);
this.setStartDelay(other.startDelay);
this.setDuration(other.animationDuration);
this.setBaseColor(other.baseColor);
this.setHighLightColor(other.highlightColor);
return this.getThis();
}
public setBaseColor(color: number | string | Color): T {
if (color instanceof Color) {
this.mShimmer.baseColor = color;
} else if (typeof color === 'string' || typeof color === 'number') {
this.mShimmer.baseColor = new Color(color as any);
}
return this.getThis();
}
public setHighLightColor(color: number | string | Color): T {
if (color instanceof Color) {
this.mShimmer.highlightColor = color;
} else if (typeof color === 'string' || typeof color === 'number') {
this.mShimmer.highlightColor = new Color(color as any);
}
return this.getThis();
}
/**
* Sets the direction of the shimmer's sweep. See {@link Direction}.
*/
public setDirection(direction: Direction) {
this.mShimmer.direction = direction;
return this.getThis();
}
/**
* Sets the shape of the shimmer. See {@link Shape}.
*/
public setShape(shape: Shape) {
this.mShimmer.shape = shape;
return this.getThis();
}
/**
* Sets the fixed width of the shimmer, in pixels.
*/
public setFixedWidth(fixedWidth: number) {
if (fixedWidth < 0) {
throw new Error("Given invalid width: " + fixedWidth);
}
this.mShimmer.fixedWidth = fixedWidth;
return this.getThis();
}
/**
* Sets the fixed height of the shimmer, in pixels.
*/
public setFixedHeight(fixedHeight: number) {
if (fixedHeight < 0) {
throw new Error("Given invalid height: " + fixedHeight);
}
this.mShimmer.fixedHeight = fixedHeight;
return this.getThis();
}
/**
* Sets the width ratio of the shimmer, multiplied against the total width of the layout.
*/
public setWidthRatio(widthRatio: number) {
if (widthRatio < 0) {
throw new Error("Given invalid width ratio: " + widthRatio);
}
this.mShimmer.widthRatio = widthRatio;
return this.getThis();
}
/**
* Sets the height ratio of the shimmer, multiplied against the total height of the layout.
*/
public setHeightRatio(heightRatio) {
if (heightRatio < 0) {
throw new Error("Given invalid height ratio: " + heightRatio);
}
this.mShimmer.heightRatio = heightRatio;
return this.getThis();
}
/**
* Sets the intensity of the shimmer. A larger value causes the shimmer to be larger.
*/
public setIntensity(intensity: number) {
if (intensity < 0) {
throw new Error("Given invalid intensity value: " + intensity);
}
this.mShimmer.intensity = intensity;
return this.getThis();
}
/**
* Sets how quickly the shimmer's gradient drops-off. A larger value causes a sharper drop-off.
*/
public setDropoff(dropoff: number) {
if (dropoff < 0) {
throw new Error("Given invalid dropoff value: " + dropoff);
}
this.mShimmer.dropoff = dropoff;
return this.getThis();
}
/**
* Sets the tilt angle of the shimmer in degrees.
*/
public setTilt(tilt: number) {
this.mShimmer.tilt = tilt;
return this.getThis();
}
/**
* Sets the base alpha, which is the alpha of the underlying children, amount in the range [0,
* 1].
*/
public setBaseAlpha(alpha: number) {
if (alpha < 0) {
alpha = 0;
} else if (alpha > 1) {
alpha = 1;
}
const intAlpha = (Builder.clamp(0, 1, alpha) * 255);
this.mShimmer.baseColor = new Color(intAlpha << 24 | (this.mShimmer.baseColor.android & 0x00FFFFFF));
return this.getThis();
}
/**
* Sets the shimmer alpha amount in the range [0, 1].
*/
public setHighlightAlpha(alpha: number) {
if (alpha < 0) {
alpha = 0;
} else if (alpha > 1) {
alpha = 1;
}
const intAlpha = Builder.clamp(0, 1, alpha) * 255;
this.mShimmer.highlightColor = new Color(intAlpha << 24 | (this.mShimmer.highlightColor.android & 0x00FFFFFF));
return this.getThis();
}
/**
* Sets whether the shimmer will clip to the childrens' contents, or if it will opaquely draw on
* top of the children.
*/
public setClipToChildren(status: boolean) {
this.mShimmer.clipToChildren = status;
return this.getThis();
}
/**
* Sets whether the shimmering animation will start automatically.
*/
public setAutoStart(status) {
this.mShimmer.autoStart = status;
return this.getThis();
}
/**
* Sets how often the shimmering animation will repeat. See {@link
* android.animation.ValueAnimator#setRepeatCount(int)}.
*/
public setRepeatCount(repeatCount: number) {
this.mShimmer.repeatCount = repeatCount;
return this.getThis();
}
/**
* Sets how the shimmering animation will repeat. See {@link
* android.animation.ValueAnimator#setRepeatMode(int)}.
*/
public setRepeatMode(mode) {
this.mShimmer.repeatMode = mode;
return this.getThis();
}
/**
* Sets how long to wait in between repeats of the shimmering animation.
*/
public setRepeatDelay(millis: number) {
if (millis < 0) {
throw new Error("Given a negative repeat delay: " + millis);
}
this.mShimmer.repeatDelay = millis;
return this.getThis();
}
/**
* Sets how long to wait for starting the shimmering animation.
*/
public setStartDelay(millis) {
if (millis < 0) {
throw new Error("Given a negative start delay: " + millis);
}
this.mShimmer.startDelay = millis;
return this.getThis();
}
/**
* Sets how long the shimmering animation takes to do one full sweep.
*/
public setDuration(millis: number) {
if (millis < 0) {
throw new Error("Given a negative duration: " + millis);
}
this.mShimmer.animationDuration = millis;
return this.getThis();
}
public build(): TNSShimmer {
this.mShimmer.updateColors();
this.mShimmer.updatePositions();
return this.mShimmer;
}
private static clamp(min: number, max: number, value: number) {
return Math.min(max, Math.max(min, value));
}
}
class AlphaHighlightBuilder extends Builder<AlphaHighlightBuilder> {
public constructor() {
super();
this.mShimmer.alphaShimmer = true;
}
protected getThis(): AlphaHighlightBuilder {
return this;
}
}
class ColorHighlightBuilder extends Builder<ColorHighlightBuilder> {
constructor() {
super();
this.mShimmer.alphaShimmer = false;
}
/**
* Sets the highlight color for the shimmer.
*/
public setHighlightColor(color: number | string | Color): ColorHighlightBuilder {
if (color instanceof Color) {
this.mShimmer.highlightColor = color;
} else if (typeof color === 'string' || typeof color === 'number') {
this.mShimmer.highlightColor = new Color(color as any);
}
return this.getThis();
}
/**
* Sets the base color for the shimmer.
*/
public setBaseColor(color: number | string | Color): ColorHighlightBuilder {
if (color instanceof Color) {
this.mShimmer.baseColor = color;
} else if (typeof color === 'string' || typeof color === 'number') {
this.mShimmer.baseColor = new Color((this.mShimmer.baseColor.android & 0xFF000000) | (new Color(color as any).android & 0x00FFFFFF));
}
return this.getThis();
}
protected getThis(): ColorHighlightBuilder {
return this;
}
}
@NativeClass()
class ShimmerDrawable extends android.graphics.drawable.Drawable {
private mUpdateListener: android.animation.ValueAnimator.AnimatorUpdateListener;
private mShimmerPaint: android.graphics.Paint;
private mDrawRect: android.graphics.Rect;
private mShaderMatrix: android.graphics.Matrix;
mValueAnimator: android.animation.ValueAnimator;
mShimmer: TNSShimmer;
constructor() {
super();
this.init();
this.mShimmerPaint.setAntiAlias(true);
(<any>this.mUpdateListener)._owner = new WeakRef(this);
return global.__native(this);
}
init() {
this.mUpdateListener =
new android.animation.ValueAnimator.AnimatorUpdateListener({
onAnimationUpdate(animation) {
this._owner?.get?.().invalidateSelf();
}
});
this.mShimmerPaint = new android.graphics.Paint();
this.mDrawRect = new android.graphics.Rect();
this.mShaderMatrix = new android.graphics.Matrix();
}
public setShimmer(shimmer?: TNSShimmer) {
this.mShimmer = shimmer;
if (this.mShimmer !== null) {
this.mShimmerPaint.setXfermode(
new android.graphics.PorterDuffXfermode(
this.mShimmer.alphaShimmer ? android.graphics.PorterDuff.Mode.DST_IN : android.graphics.PorterDuff.Mode.SRC_IN));
}
this.updateShader();
this.updateValueAnimator();
this.invalidateSelf();
}
public getShimmer() {
return this.mShimmer;
}
/**
* Starts the shimmer animation.
*/
public startShimmer() {
if (this.mValueAnimator != null && !this.isShimmerStarted() && this.getCallback() != null) {
this.mValueAnimator.start();
}
}
/**
* Stops the shimmer animation.
*/
public stopShimmer() {
if (this.mValueAnimator != null && this.isShimmerStarted()) {
this.mValueAnimator.cancel();
}
}
/**
* Return whether the shimmer animation has been started.
*/
public isShimmerStarted() {
return this.mValueAnimator != null && this.mValueAnimator.isStarted();
}
public onBoundsChange(bounds: android.graphics.Rect) {
super.onBoundsChange(bounds);
this.mDrawRect.set(bounds);
this.updateShader();
this.maybeStartShimmer();
}
public draw(canvas: android.graphics.Canvas) {
if (this.mShimmer === null || this.mShimmerPaint.getShader() === null) {
return;
}
const tiltTan = Math.tan(java.lang.Math.toRadians(this.mShimmer.tilt));
const translateHeight = this.mDrawRect.height() + tiltTan * this.mDrawRect.width();
const translateWidth = this.mDrawRect.width() + tiltTan * this.mDrawRect.height();
let dx;
let dy;
const animatedValue =
this.mValueAnimator != null ? this.mValueAnimator.getAnimatedValue() : 0;
switch (this.mShimmer.direction) {
default:
case Direction.LEFT_TO_RIGHT:
dx = this.offset(-translateWidth, translateWidth, animatedValue);
dy = 0;
break;
case Direction.RIGHT_TO_LEFT:
dx = this.offset(translateWidth, -translateWidth, animatedValue);
dy = 0;
break;
case Direction.TOP_TO_BOTTOM:
dx = 0;
dy = this.offset(-translateHeight, translateHeight, animatedValue);
break;
case Direction.BOTTOM_TO_TOP:
dx = 0;
dy = this.offset(translateHeight, -translateHeight, animatedValue);
break;
}
this.mShaderMatrix.reset();
this.mShaderMatrix.setRotate(this.mShimmer.tilt, this.mDrawRect.width() / 2, this.mDrawRect.height() / 2);
this.mShaderMatrix.postTranslate(dx, dy);
this.mShimmerPaint.getShader().setLocalMatrix(this.mShaderMatrix);
canvas.drawRect(this.mDrawRect, this.mShimmerPaint);
}
public setAlpha(alpha: number) {
// No-op, modify the Shimmer object you pass in instead
}
public setColorFilter(...args) {
// No-op, modify the Shimmer object you pass in instead
}
public getOpacity() {
return this.mShimmer != null && (this.mShimmer.clipToChildren || this.mShimmer.alphaShimmer)
? android.graphics.PixelFormat.TRANSLUCENT
: android.graphics.PixelFormat.OPAQUE;
}
private offset(start: number, end: number, percent: number) {
return start + (end - start) * percent;
}
private updateValueAnimator() {
if (this.mShimmer == null) {
return;
}
let started: boolean;
if (this.mValueAnimator != null) {
started = this.mValueAnimator.isStarted();
this.mValueAnimator.cancel();
this.mValueAnimator.removeAllUpdateListeners();
} else {
started = false;
}
const arr = Array.create('float', 2);
arr[0] = 0;
arr[1] = 1 + (this.mShimmer.repeatDelay / this.mShimmer.animationDuration)
this.mValueAnimator =
android.animation.ValueAnimator.ofFloat(arr);
this.mValueAnimator.setInterpolator(new android.view.animation.LinearInterpolator());
this.mValueAnimator.setRepeatMode(this.mShimmer.repeatMode);
this.mValueAnimator.setStartDelay(this.mShimmer.startDelay);
this.mValueAnimator.setRepeatCount(this.mShimmer.repeatCount);
this.mValueAnimator.setDuration(this.mShimmer.animationDuration + this.mShimmer.repeatDelay);
this.mValueAnimator.addUpdateListener(this.mUpdateListener);
if (started) {
this.mValueAnimator.start();
}
}
maybeStartShimmer() {
if (this.mValueAnimator != null
&& !this.mValueAnimator.isStarted()
&& this.mShimmer != null
&& this.mShimmer.autoStart
&& this.getCallback() != null) {
this.mValueAnimator.start();
}
}
private updateShader() {
const bounds = this.getBounds();
const boundsWidth = bounds.width();
const boundsHeight = bounds.height();
if (boundsWidth == 0 || boundsHeight == 0 || this.mShimmer == null) {
return;
}
const width = this.mShimmer.width(boundsWidth);
const height = this.mShimmer.height(boundsHeight);
let shader: android.graphics.Shader;
switch (this.mShimmer.shape) {
default:
case Shape.LINEAR:
const vertical =
this.mShimmer.direction == Direction.TOP_TO_BOTTOM
|| this.mShimmer.direction == Direction.BOTTOM_TO_TOP;
const endX = vertical ? 0 : width;
const endY = vertical ? height : 0;
shader =
new android.graphics.LinearGradient(
0, 0, endX, endY, this.mShimmer.colors, this.mShimmer.positions, android.graphics.Shader.TileMode.CLAMP);
break;
case Shape.RADIAL:
shader =
new android.graphics.RadialGradient(
width / 2,
height / 2,
(Math.max(width, height) / Math.sqrt(2)),
this.mShimmer.colors,
this.mShimmer.positions,
android.graphics.Shader.TileMode.CLAMP);
break;
}
this.mShimmerPaint.setShader(shader);
}
}
@NativeClass()
class ShimmerView extends android.widget.FrameLayout {
private mContentPaint = new android.graphics.Paint();
private mShimmerDrawable = new ShimmerDrawable();
private mShowShimmer = true;
private mStoppedShimmerBecauseVisibility = false;
constructor(param0: android.content.Context);
constructor(param0: android.content.Context, param1?: android.util.AttributeSet) {
super(param0, param1 || null);
this.init(
param0, param1 || null
);
return global.__native(this);
}
private init(context, attrs) {
this.setWillNotDraw(false);
if (!this.mShimmerDrawable) {
this.mShimmerDrawable = new ShimmerDrawable();
}
if (!this.mContentPaint) {
this.mContentPaint = new android.graphics.Paint();
}
this.mShimmerDrawable.setCallback(this);
if (attrs == null) {
this.setShimmer(new AlphaHighlightBuilder().build());
return;
}
}
public setShimmer(shimmer: TNSShimmer): ShimmerView {
this.mShimmerDrawable.setShimmer(shimmer);
if (shimmer != null && shimmer.clipToChildren) {
this.setLayerType(android.view.View.LAYER_TYPE_HARDWARE, this.mContentPaint);
} else {
this.setLayerType(android.view.View.LAYER_TYPE_NONE, null);
}
return this;
}
getShimmer(): TNSShimmer {
return this.mShimmerDrawable.getShimmer();
}
private mSpeed = 1100;
public setSpeed(speed: number) {
if (speed > 0) {
this.mSpeed = speed;
}
if (this.getShimmer() != null) {
const builder = new AlphaHighlightBuilder();
builder.copyFrom(this.getShimmer());
builder.setDuration(speed);
this.setShimmer(builder.build());
}
}
public setLightColor(color: number | string | Color) {
if (this.getShimmer() != null) {
const builder = new AlphaHighlightBuilder();
builder.copyFrom(this.getShimmer());
builder.setHighLightColor(color);
this.setShimmer(builder.build());
}
}
public setDarkColor(color: number | string | Color) {
if (this.getShimmer() != null) {
const builder = new AlphaHighlightBuilder();
builder.copyFrom(this.getShimmer());
builder.setBaseColor(color);
this.setShimmer(builder.build());
}
}
public start(speed: number, direction: Direction, repeatCount: number, lightColor, blackColor) {
if (this.getShimmer() != null) {
const builder = new AlphaHighlightBuilder();
builder.copyFrom(this.getShimmer());
builder.setDuration(speed);
builder.setDirection(direction);
builder.setRepeatCount(repeatCount);
builder.setHighLightColor(lightColor);
builder.setBaseColor(blackColor);
this.setShimmer(builder.build());
}
this.showShimmer(true);
}
/**
* Starts the shimmer animation.
*/
public startShimmer() {
this.mShimmerDrawable.startShimmer();
}
/**
* Stops the shimmer animation.
*/
public stopShimmer() {
this.mStoppedShimmerBecauseVisibility = false;
this.mShimmerDrawable.stopShimmer();
}
/**
* Return whether the shimmer animation has been started.
*/
public isShimmerStarted() {
return this.mShimmerDrawable.isShimmerStarted();
}
/**
* Sets the ShimmerDrawable to be visible.
*
* @param startShimmer Whether to start the shimmer again.
*/
public showShimmer(startShimmer: boolean) {
this.mShowShimmer = true;
if (startShimmer) {
this.startShimmer();
}
this.invalidate();
}
/**
* Sets the ShimmerDrawable to be invisible, stopping it in the process.
*/
public hideShimmer() {
this.stopShimmer();
this.mShowShimmer = false;
this.invalidate();
}
/**
* Return whether the shimmer drawable is visible.
*/
public isShimmerVisible() {
return this.mShowShimmer;
}
public onLayout(changed: boolean, left: number, top: number, right: number, bottom: number) {
super.onLayout(changed, left, top, right, bottom);
const width = this.getWidth();
const height = this.getHeight();
this.mShimmerDrawable.setBounds(0, 0, width, height);
}
onVisibilityChanged(changedView, visibility: number) {
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 (this.mShimmerDrawable == null) {
return;
}
if (visibility != ShimmerView.VISIBLE) {
// GONE or INVISIBLE
if (this.isShimmerStarted()) {
this.stopShimmer();
this.mStoppedShimmerBecauseVisibility = true;
}
} else if (this.mStoppedShimmerBecauseVisibility) {
this.mShimmerDrawable.maybeStartShimmer();
this.mStoppedShimmerBecauseVisibility = false;
}
}
public onAttachedToWindow() {
super.onAttachedToWindow();
this.mShimmerDrawable.maybeStartShimmer();
}
public onDetachedFromWindow() {
super.onDetachedFromWindow();
this.stopShimmer();
}
public dispatchDraw(canvas: android.graphics.Canvas) {
super.dispatchDraw(canvas);
if (this.mShowShimmer) {
this.mShimmerDrawable.draw(canvas);
}
}
verifyDrawable(who: android.graphics.drawable.Drawable): boolean {
return super.verifyDrawable(who) || who === this.mShimmerDrawable;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment