// Copyright 2004-present Facebook. All Rights Reserved. package com.marshalchen.common.ui; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RadialGradient; import android.graphics.Shader; import android.util.AttributeSet; import android.util.Log; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import com.marshalchen.common.R; public class ShimmerFrameLayout extends FrameLayout { private static final String TAG = "ShimmerFrameLayout"; private static final PorterDuffXfermode DST_IN_PORTER_DUFF_XFERMODE = new PorterDuffXfermode(PorterDuff.Mode.DST_IN); // enum specifying the shape of the highlight mask applied to the contained view public enum MaskShape { LINEAR, RADIAL } // enum controlling the angle of the highlight mask animation public enum MaskAngle { CW_0, // left to right CW_90, // top to bottom CW_180, // right to left CW_270, // bottom to top } // struct storing various mask related parameters, which are used to construct the mask bitmap private static class Mask { public MaskAngle angle; public float tilt; public float dropoff; public int fixedWidth; public int fixedHeight; public float intensity; public float relativeWidth; public float relativeHeight; public MaskShape shape; public int maskWidth(int width) { return fixedWidth > 0 ? fixedWidth : (int) (width * relativeWidth); } public int maskHeight(int height) { return fixedHeight > 0 ? fixedHeight : (int) (height * relativeHeight); } /** * Get the array of colors to be distributed along the gradient of the mask bitmap * * @return An array of black and transparent colors */ public int[] getGradientColors() { switch (shape) { default: case LINEAR: return new int[]{Color.TRANSPARENT, Color.BLACK, Color.BLACK, Color.TRANSPARENT}; case RADIAL: return new int[]{Color.BLACK, Color.BLACK, Color.TRANSPARENT}; } } /** * Get the array of relative positions [0..1] of each corresponding color in the colors array * * @return A array of float values in the [0..1] range */ public float[] getGradientPositions() { switch (shape) { default: case LINEAR: return new float[]{ Math.max((1.0f - intensity - dropoff) / 2, 0.0f), Math.max((1.0f - intensity) / 2, 0.0f), Math.min((1.0f + intensity) / 2, 1.0f), Math.min((1.0f + intensity + dropoff) / 2, 1.0f)}; case RADIAL: return new float[]{ 0.0f, Math.min(intensity, 1.0f), Math.min(intensity + dropoff, 1.0f)}; } } } // struct for storing the mask translation animation values private static class MaskTranslation { public int fromX; public int fromY; public int toX; public int toY; public void set(int fromX, int fromY, int toX, int toY) { this.fromX = fromX; this.fromY = fromY; this.toX = toX; this.toY = toY; } } private Paint mAlphaPaint; private Paint mMaskPaint; private Mask mMask; private MaskTranslation mMaskTranslation; private Bitmap mRenderMaskBitmap; private Bitmap mRenderUnmaskBitmap; private boolean mAutoStart; private int mDuration; private int mRepeatCount; private int mRepeatDelay; private int mRepeatMode; private int mMaskOffsetX; private int mMaskOffsetY; private boolean mAnimationStarted; private ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener; protected ValueAnimator mAnimator; protected Bitmap mMaskBitmap; public ShimmerFrameLayout(Context context) { this(context, null, 0); } public ShimmerFrameLayout(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ShimmerFrameLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); setWillNotDraw(false); mMask = new Mask(); mAlphaPaint = new Paint(); mMaskPaint = new Paint(); mMaskPaint.setAntiAlias(true); mMaskPaint.setDither(true); mMaskPaint.setFilterBitmap(true); mMaskPaint.setXfermode(DST_IN_PORTER_DUFF_XFERMODE); useDefaults(); if (attrs != null) { TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ShimmerFrameLayout, 0, 0); try { if (a.hasValue(R.styleable.ShimmerFrameLayout_auto_start)) { setAutoStart(a.getBoolean(R.styleable.ShimmerFrameLayout_auto_start, false)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_base_alpha)) { setBaseAlpha(a.getFloat(R.styleable.ShimmerFrameLayout_base_alpha, 0)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_shimmerduration)) { setDuration(a.getInt(R.styleable.ShimmerFrameLayout_shimmerduration, 0)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_repeat_count)) { setRepeatCount(a.getInt(R.styleable.ShimmerFrameLayout_repeat_count, 0)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_repeat_delay)) { setRepeatDelay(a.getInt(R.styleable.ShimmerFrameLayout_repeat_delay, 0)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_repeat_mode)) { setRepeatMode(a.getInt(R.styleable.ShimmerFrameLayout_repeat_mode, 0)); } if (a.hasValue(R.styleable.ShimmerFrameLayout_shimmerangle)) { int angle = a.getInt(R.styleable.ShimmerFrameLayout_shimmerangle, 0); switch (angle) { default: case 0: mMask.angle = MaskAngle.CW_0; break; case 90: mMask.angle = MaskAngle.CW_90; break; case 180: mMask.angle = MaskAngle.CW_180; break; case 270: mMask.angle = MaskAngle.CW_270; break; } } if (a.hasValue(R.styleable.ShimmerFrameLayout_shimmershape)) { int shape = a.getInt(R.styleable.ShimmerFrameLayout_shimmerangle, 0); switch (shape) { default: case 0: mMask.shape = MaskShape.LINEAR; break; case 1: mMask.shape = MaskShape.RADIAL; break; } } if (a.hasValue(R.styleable.ShimmerFrameLayout_shimmerdropoff)) { mMask.dropoff = a.getFloat(R.styleable.ShimmerFrameLayout_shimmerdropoff, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_fixed_width)) { mMask.fixedWidth = a.getDimensionPixelSize(R.styleable.ShimmerFrameLayout_fixed_width, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_fixed_height)) { mMask.fixedHeight = a.getDimensionPixelSize(R.styleable.ShimmerFrameLayout_fixed_height, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_shimmerintensity)) { mMask.intensity = a.getFloat(R.styleable.ShimmerFrameLayout_shimmerintensity, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_relative_width)) { mMask.relativeWidth = a.getFloat(R.styleable.ShimmerFrameLayout_relative_width, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_relative_height)) { mMask.relativeHeight = a.getFloat(R.styleable.ShimmerFrameLayout_relative_height, 0); } if (a.hasValue(R.styleable.ShimmerFrameLayout_tilt)) { mMask.tilt = a.getFloat(R.styleable.ShimmerFrameLayout_tilt, 0); } } finally { a.recycle(); } } } /** * Resets the layout to its default state. Any parameters that were set or modified will be reverted back to their * original value. Also, stops the shimmer animation if it is currently playing. */ public void useDefaults() { // Set defaults setAutoStart(false); setDuration(1000); setRepeatCount(ObjectAnimator.INFINITE); setRepeatDelay(0); setRepeatMode(ObjectAnimator.RESTART); mMask.angle = MaskAngle.CW_0; mMask.shape = MaskShape.LINEAR; mMask.dropoff = 0.5f; mMask.fixedWidth = 0; mMask.fixedHeight = 0; mMask.intensity = 0.0f; mMask.relativeWidth = 1.0f; mMask.relativeHeight = 1.0f; mMask.tilt = 20; mMaskTranslation = new MaskTranslation(); setBaseAlpha(0.3f); resetAll(); } /** * Is 'auto start' enabled for this layout. When auto start is enabled, the layout will start animating automatically * whenever it is attached to the current window. * * @return True if 'auto start' is enabled, false otherwise */ public boolean isAutoStart() { return mAutoStart; } /** * Enable or disable 'auto start' for this layout. When auto start is enabled, the layout will start animating * automatically whenever it is attached to the current window. * * @param autoStart Whether auto start should be enabled or not */ public void setAutoStart(boolean autoStart) { mAutoStart = autoStart; resetAll(); } /** * Get the alpha currently used to render the base view i.e. the unhighlighted view over which the highlight is drawn. * * @return Alpha (opacity) of the base view */ public float getBaseAlpha() { return (float) mAlphaPaint.getAlpha() / 0xff; } /** * Set the alpha to be used to render the base view i.e. the unhighlighted view over which the highlight is drawn. * * @param alpha Alpha (opacity) of the base view */ public void setBaseAlpha(float alpha) { mAlphaPaint.setAlpha((int) (clamp(0, 1, alpha) * 0xff)); resetAll(); } /** * Get the duration of the current animation i.e. the time it takes for the highlight to move from one end * of the layout to the other. The default value is 1000 ms. * * @return Duration of the animation, in milliseconds */ public int getDuration() { return mDuration; } /** * Set the duration of the animation i.e. the time it will take for the highlight to move from one end of the layout * to the other. * * @param duration Duration of the animation, in milliseconds */ public void setDuration(int duration) { mDuration = duration; resetAll(); } /** * Get the number of times of the current animation will repeat. The default value is -1, which means the animation * will repeat indefinitely. * * @return Number of times the current animation will repeat, or -1 for indefinite. */ public int getRepeatCount() { return mRepeatCount; } /** * Set the number of times the animation should repeat. If the repeat count is 0, the animation stops after reaching * the end. If greater than 0, or -1 (for infinite), the repeat mode is taken into account. * * @param repeatCount Number of times the current animation should repeat, or -1 for indefinite. */ public void setRepeatCount(int repeatCount) { mRepeatCount = repeatCount; resetAll(); } /** * Get the delay after which the current animation will repeat. The default value is 0, which means the animation * will repeat immediately, unless it has ended. * * @return Delay after which the current animation will repeat, in milliseconds. */ public int getRepeatDelay() { return mRepeatDelay; } /** * Set the delay after which the animation repeat, unless it has ended. * * @param repeatDelay Delay after which the animation should repeat, in milliseconds. */ public void setRepeatDelay(int repeatDelay) { mRepeatDelay = repeatDelay; resetAll(); } /** * Get what the current animation will do after reaching the end. One of * <a href="http://developer.android.com/reference/android/animation/ValueAnimator.html#REVERSE">REVERSE</a> or * <a href="http://developer.android.com/reference/android/animation/ValueAnimator.html#RESTART">RESTART</a> * * @return Repeat mode of the current animation */ public int getRepeatMode() { return mRepeatMode; } /** * Set what the animation should do after reaching the end. One of * <a href="http://developer.android.com/reference/android/animation/ValueAnimator.html#REVERSE">REVERSE</a> or * <a href="http://developer.android.com/reference/android/animation/ValueAnimator.html#RESTART">RESTART</a> * * @param repeatMode Repeat mode of the animation */ public void setRepeatMode(int repeatMode) { mRepeatMode = repeatMode; resetAll(); } /** * Get the shape of the current animation's highlight mask. One of {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskShape#LINEAR} or * {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskShape#RADIAL} * * @return The shape of the highlight mask */ public MaskShape getMaskShape() { return mMask.shape; } /** * Set the shape of the animation's highlight mask. One of {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskShape#LINEAR} or {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskShape#RADIAL} * * @param shape The shape of the highlight mask */ public void setMaskShape(MaskShape shape) { mMask.shape = shape; resetAll(); } /** * Get the angle at which the highlight mask is animated. One of: * <ul> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_0} which animates left to right,</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_90} which animates top to bottom,</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_180} which animates right to left, or</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_270} which animates bottom to top</li> * </ul> * * @return The {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle} of the current animation */ public MaskAngle getAngle() { return mMask.angle; } /** * Set the angle of the highlight mask animation. One of: * <ul> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_0} which animates left to right,</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_90} which animates top to bottom,</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_180} which animates right to left, or</li> * <li>{@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle#CW_270} which animates bottom to top</li> * </ul> * * @param angle The {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle} of the new animation */ public void setAngle(MaskAngle angle) { mMask.angle = angle; resetAll(); } /** * Get the dropoff of the current animation's highlight mask. Dropoff controls the size of the fading edge of the * highlight. * <p/> * The default value of dropoff is 0.5. * * @return Dropoff of the highlight mask */ public float getDropoff() { return mMask.dropoff; } /** * Set the dropoff of the animation's highlight mask, which defines the size of the highlight's fading edge. * <p/> * It is the relative distance from the center at which the highlight mask's opacity is 0 i.e it is fully transparent. * For a linear mask, the distance is relative to the center towards the edges. For a radial mask, the distance is * relative to the center towards the circumference. So a dropoff of 0.5 on a linear mask will create a band that * is half the size of the corresponding edge (depending on the {@link com.marshalchen.common.ui.ShimmerFrameLayout.MaskAngle}), centered in the layout. * * @param dropoff */ public void setDropoff(float dropoff) { mMask.dropoff = dropoff; resetAll(); } /** * Get the fixed width of the highlight mask, or 0 if it is not set. By default it is 0. * * @return The width of the highlight mask if set, in pixels. */ public int getFixedWidth() { return mMask.fixedWidth; } /** * Set the fixed width of the highlight mask, regardless of the size of the layout. * * @param fixedWidth The width of the highlight mask in pixels. */ public void setFixedWidth(int fixedWidth) { mMask.fixedWidth = fixedWidth; resetAll(); } /** * Get the fixed height of the highlight mask, or 0 if it is not set. By default it is 0. * * @return The height of the highlight mask if set, in pixels. */ public int getFixedHeight() { return mMask.fixedHeight; } /** * Set the fixed height of the highlight mask, regardless of the size of the layout. * * @param fixedHeight The height of the highlight mask in pixels. */ public void setFixedHeight(int fixedHeight) { mMask.fixedHeight = fixedHeight; resetAll(); } /** * Get the intensity of the highlight mask, in the [0..1] range. The intensity controls the brightness of the * highlight; the higher it is, the greater is the opaque region in the highlight. The default value is 0. * * @return The intensity of the highlight mask */ public float getIntensity() { return mMask.intensity; } /** * Set the intensity of the highlight mask, in the [0..1] range. * <p/> * Intensity is the point relative to the center where opacity starts dropping off, so an intensity of 0 would mean * that the highlight starts becoming translucent immediately from the center (the spread is controlled by 'dropoff'). * * @param intensity The intensity of the highlight mask. */ public void setIntensity(float intensity) { mMask.intensity = intensity; resetAll(); } /** * Get the width of the highlight mask relative to the layout's width. The default is 1.0, meaning that the mask is * of the same width as the layout. * * @return Relative width of the highlight mask. */ public float getRelativeWidth() { return mMask.relativeWidth; } /** * Set the width of the highlight mask relative to the layout's width, in the [0..1] range. * * @param relativeWidth Relative width of the highlight mask. */ public void setRelativeWidth(int relativeWidth) { mMask.relativeWidth = relativeWidth; resetAll(); } /** * Get the height of the highlight mask relative to the layout's height. The default is 1.0, meaning that the mask is * of the same height as the layout. * * @return Relative height of the highlight mask. */ public float getRelativeHeight() { return mMask.relativeHeight; } /** * Set the height of the highlight mask relative to the layout's height, in the [0..1] range. * * @param relativeHeight Relative height of the highlight mask. */ public void setRelativeHeight(int relativeHeight) { mMask.relativeHeight = relativeHeight; resetAll(); } /** * Get the tilt angle of the highlight, in degrees. The default value is 20. * * @return The highlight's tilt angle, in degrees. */ public float getTilt() { return mMask.tilt; } /** * Set the tile angle of the highlight, in degrees. * * @param tilt The highlight's tilt angle, in degrees. */ public void setTilt(float tilt) { mMask.tilt = tilt; resetAll(); } /** * Start the shimmer animation. If the 'auto start' property is set, this method is called automatically when the * layout is attached to the current window. Calling this method has no effect if the animation is already playing. */ public void startShimmerAnimation() { if (mAnimationStarted) { return; } Animator animator = getShimmerAnimation(); animator.start(); mAnimationStarted = true; } /** * Stop the shimmer animation. Calling this method has no effect if the animation hasn't been started yet. */ public void stopShimmerAnimation() { if (mAnimator != null) { mAnimator.end(); mAnimator.removeAllUpdateListeners(); mAnimator.cancel(); } mAnimator = null; mAnimationStarted = false; } /** * Whether the shimmer animation is currently underway. * * @return True if the shimmer animation is playing, false otherwise. */ public boolean isAnimationStarted() { return mAnimationStarted; } /** * Translate the mask offset horizontally. Used by the animator. * * @param maskOffsetX Horizontal translation offset of the mask */ private void setMaskOffsetX(int maskOffsetX) { if (mMaskOffsetX == maskOffsetX) { return; } mMaskOffsetX = maskOffsetX; invalidate(); } /** * Translate the mask offset vertically. Used by the animator. * * @param maskOffsetY Vertical translation offset of the mask */ private void setMaskOffsetY(int maskOffsetY) { if (mMaskOffsetY == maskOffsetY) { return; } mMaskOffsetY = maskOffsetY; invalidate(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if (mGlobalLayoutListener == null) { mGlobalLayoutListener = getLayoutListener(); } getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener); } private ViewTreeObserver.OnGlobalLayoutListener getLayoutListener() { return new ViewTreeObserver.OnGlobalLayoutListener() { @Override public void onGlobalLayout() { boolean animationStarted = mAnimationStarted; resetAll(); if (mAutoStart || animationStarted) { startShimmerAnimation(); } } }; } @Override protected void onDetachedFromWindow() { stopShimmerAnimation(); if (mGlobalLayoutListener != null) { getViewTreeObserver().removeGlobalOnLayoutListener(mGlobalLayoutListener); mGlobalLayoutListener = null; } super.onDetachedFromWindow(); } @Override protected void dispatchDraw(Canvas canvas) { if (!mAnimationStarted || getWidth() <= 0 || getHeight() <= 0) { super.dispatchDraw(canvas); return; } dispatchDrawUsingBitmap(canvas); } private static float clamp(float min, float max, float value) { return Math.min(max, Math.max(min, value)); } /** * Draws and masks the children using a Bitmap. * * @param canvas Canvas that the masked children will end up being drawn to. */ private boolean dispatchDrawUsingBitmap(Canvas canvas) { Bitmap unmaskBitmap = tryObtainRenderUnmaskBitmap(); Bitmap maskBitmap = tryObtainRenderMaskBitmap(); if (unmaskBitmap == null || maskBitmap == null) { return false; } // First draw a desaturated version drawUnmasked(new Canvas(unmaskBitmap)); canvas.drawBitmap(unmaskBitmap, 0, 0, mAlphaPaint); // Then draw the masked version drawMasked(new Canvas(maskBitmap)); canvas.drawBitmap(maskBitmap, 0, 0, null); return true; } private Bitmap tryObtainRenderUnmaskBitmap() { if (mRenderUnmaskBitmap == null) { mRenderUnmaskBitmap = tryCreateRenderBitmap(); } return mRenderUnmaskBitmap; } private Bitmap tryObtainRenderMaskBitmap() { if (mRenderMaskBitmap == null) { mRenderMaskBitmap = tryCreateRenderBitmap(); } return mRenderMaskBitmap; } private Bitmap tryCreateRenderBitmap() { int width = getWidth(); int height = getHeight(); try { return createBitmapAndGcIfNecessary(width, height); } catch (OutOfMemoryError e) { String logMessage = "ShimmerFrameLayout failed to create working bitmap"; StringBuilder logMessageStringBuilder = new StringBuilder(logMessage); logMessageStringBuilder.append(" (width = "); logMessageStringBuilder.append(width); logMessageStringBuilder.append(", height = "); logMessageStringBuilder.append(height); logMessageStringBuilder.append(")\n\n"); for (StackTraceElement stackTraceElement : Thread.currentThread().getStackTrace()) { logMessageStringBuilder.append(stackTraceElement.toString()); logMessageStringBuilder.append("\n"); } logMessage = logMessageStringBuilder.toString(); Log.d(TAG, logMessage); } return null; } // Draws the children without any mask. private void drawUnmasked(Canvas renderCanvas) { super.dispatchDraw(renderCanvas); } // Draws the children and masks them on the given Canvas. private void drawMasked(Canvas renderCanvas) { Bitmap maskBitmap = getMaskBitmap(); if (maskBitmap == null) { return; } renderCanvas.clipRect( mMaskOffsetX, mMaskOffsetY, mMaskOffsetX + maskBitmap.getWidth(), mMaskOffsetY + maskBitmap.getHeight()); super.dispatchDraw(renderCanvas); renderCanvas.drawBitmap(maskBitmap, mMaskOffsetX, mMaskOffsetY, mMaskPaint); } private void resetAll() { stopShimmerAnimation(); resetMaskBitmap(); resetRenderedView(); } // If a mask bitmap was created, it's recycled and set to null so it will be recreated when needed. private void resetMaskBitmap() { if (mMaskBitmap != null) { mMaskBitmap.recycle(); mMaskBitmap = null; } } // If a working bitmap was created, it's recycled and set to null so it will be recreated when needed. private void resetRenderedView() { if (mRenderUnmaskBitmap != null) { mRenderUnmaskBitmap.recycle(); mRenderUnmaskBitmap = null; } if (mRenderMaskBitmap != null) { mRenderMaskBitmap.recycle(); mRenderMaskBitmap = null; } } // Return the mask bitmap, creating it if necessary. private Bitmap getMaskBitmap() { if (mMaskBitmap != null) { return mMaskBitmap; } int width = mMask.maskWidth(getWidth()); int height = mMask.maskHeight(getHeight()); mMaskBitmap = createBitmapAndGcIfNecessary(width, height); Canvas canvas = new Canvas(mMaskBitmap); Shader gradient; switch (mMask.shape) { default: case LINEAR: { int x1, y1; int x2, y2; switch (mMask.angle) { default: case CW_0: x1 = 0; y1 = 0; x2 = width; y2 = 0; break; case CW_90: x1 = 0; y1 = 0; x2 = 0; y2 = height; break; case CW_180: x1 = width; y1 = 0; x2 = 0; y2 = 0; break; case CW_270: x1 = 0; y1 = height; x2 = 0; y2 = 0; break; } gradient = new LinearGradient( x1, y1, x2, y2, mMask.getGradientColors(), mMask.getGradientPositions(), Shader.TileMode.REPEAT); break; } case RADIAL: { int x = width / 2; int y = height / 2; gradient = new RadialGradient( x, y, (float) (Math.max(width, height) / Math.sqrt(2)), mMask.getGradientColors(), mMask.getGradientPositions(), Shader.TileMode.REPEAT); break; } } canvas.rotate(mMask.tilt, width / 2, height / 2); Paint paint = new Paint(); paint.setShader(gradient); // We need to increase the rect size to account for the tilt int padding = (int) (Math.sqrt(2) * Math.max(width, height)) / 2; canvas.drawRect(-padding, -padding, width + padding, height + padding, paint); return mMaskBitmap; } // Get the shimmer <a href="http://developer.android.com/reference/android/animation/Animator.html">Animator</a> // object, which is responsible for driving the highlight mask animation. private Animator getShimmerAnimation() { if (mAnimator != null) { return mAnimator; } int width = getWidth(); int height = getHeight(); switch (mMask.shape) { default: case LINEAR: switch (mMask.angle) { default: case CW_0: mMaskTranslation.set(-width, 0, width, 0); break; case CW_90: mMaskTranslation.set(0, -height, 0, height); break; case CW_180: mMaskTranslation.set(width, 0, -width, 0); break; case CW_270: mMaskTranslation.set(0, height, 0, -height); break; } } mAnimator = ValueAnimator.ofFloat(0.0f, 1.0f + (float) mRepeatDelay / mDuration); mAnimator.setDuration(mDuration + mRepeatDelay); mAnimator.setRepeatCount(mRepeatCount); mAnimator.setRepeatMode(mRepeatMode); mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { @Override public void onAnimationUpdate(ValueAnimator animation) { float value = Math.max(0.0f, Math.min(1.0f, (Float) animation.getAnimatedValue())); setMaskOffsetX((int) (mMaskTranslation.fromX * (1 - value) + mMaskTranslation.toX * value)); setMaskOffsetY((int) (mMaskTranslation.fromY * (1 - value) + mMaskTranslation.toY * value)); } }); return mAnimator; } /** * Creates a bitmap with the given width and height. * <p/> * If it fails with an OutOfMemory error, it will force a GC and then try to create the bitmap * one more time. * * @param width width of the bitmap * @param height height of the bitmap */ protected static Bitmap createBitmapAndGcIfNecessary(int width, int height) { try { return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } catch (OutOfMemoryError e) { System.gc(); return Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } } }