今天主要来分析下光晕动画的实现类:
package io.supercharge.shimmerlayout;
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.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.Shader;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
public class ShimmerLayout extends FrameLayout {
private static final int DEFAULT_ANIMATION_DURATION = 1500;
private static final byte DEFAULT_ANGLE = 20;
private static final byte MIN_ANGLE_VALUE = -45;
private static final byte MAX_ANGLE_VALUE = 45;
private static final byte MIN_MASK_WIDTH_VALUE = 0;
private static final byte MAX_MASK_WIDTH_VALUE = 1;
private static final byte MIN_GRADIENT_CENTER_COLOR_WIDTH_VALUE = 0;
private static final byte MAX_GRADIENT_CENTER_COLOR_WIDTH_VALUE = 1;
private int maskOffsetX;
private Rect maskRect;
private Paint gradientTexturePaint;
private ValueAnimator maskAnimator;
private Bitmap localMaskBitmap;
private Bitmap maskBitmap;
private Canvas canvasForShimmerMask;
private boolean isAnimationReversed;
private boolean isAnimationStarted;
private boolean autoStart;
private int shimmerAnimationDuration;
private int shimmerColor;
private int shimmerAngle;
private float maskWidth;
private float gradientCenterColorWidth;
private ViewTreeObserver.OnPreDrawListener startAnimationPreDrawListener;
public ShimmerLayout(Context context) {
this(context, null);
}
public ShimmerLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ShimmerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
setWillNotDraw(false);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.ShimmerLayout,
0, 0);
try {
shimmerAngle = a.getInteger(R.styleable.ShimmerLayout_shimmer_angle, DEFAULT_ANGLE);
shimmerAnimationDuration = a.getInteger(R.styleable.ShimmerLayout_shimmer_animation_duration, DEFAULT_ANIMATION_DURATION);
shimmerColor = a.getColor(R.styleable.ShimmerLayout_shimmer_color, getColor(R.color.shimmer_color));
autoStart = a.getBoolean(R.styleable.ShimmerLayout_shimmer_auto_start, false);
maskWidth = a.getFloat(R.styleable.ShimmerLayout_shimmer_mask_width, 0.5F);
gradientCenterColorWidth = a.getFloat(R.styleable.ShimmerLayout_shimmer_gradient_center_color_width, 0.1F);
isAnimationReversed = a.getBoolean(R.styleable.ShimmerLayout_shimmer_reverse_animation, false);
} finally {
a.recycle();
}
setMaskWidth(maskWidth);
setGradientCenterColorWidth(gradientCenterColorWidth);
setShimmerAngle(shimmerAngle);
if (autoStart && getVisibility() == VISIBLE) {
startShimmerAnimation();
}
}
@Override
protected void onDetachedFromWindow() {
resetShimmering();
super.onDetachedFromWindow();
}
@Override
protected void dispatchDraw(Canvas canvas) {
if (!isAnimationStarted || getWidth() <= 0 || getHeight() <= 0) {
super.dispatchDraw(canvas);
} else {
dispatchDrawShimmer(canvas);
}
}
@Override
public void setVisibility(int visibility) {
super.setVisibility(visibility);
if (visibility == VISIBLE) {
if (autoStart) {
startShimmerAnimation();
}
} else {
stopShimmerAnimation();
}
}
public void startShimmerAnimation() {
if (isAnimationStarted) {
return;
}
if (getWidth() == 0) {
startAnimationPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
startShimmerAnimation();
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(startAnimationPreDrawListener);
return;
}
Animator animator = getShimmerAnimation();
animator.start();
isAnimationStarted = true;
}
public void stopShimmerAnimation() {
if (startAnimationPreDrawListener != null) {
getViewTreeObserver().removeOnPreDrawListener(startAnimationPreDrawListener);
}
resetShimmering();
}
public void setShimmerColor(int shimmerColor) {
this.shimmerColor = shimmerColor;
resetIfStarted();
}
public void setShimmerAnimationDuration(int durationMillis) {
this.shimmerAnimationDuration = durationMillis;
resetIfStarted();
}
public void setAnimationReversed(boolean animationReversed) {
this.isAnimationReversed = animationReversed;
resetIfStarted();
}
/**
* Set the angle of the shimmer effect in clockwise direction in degrees.
* The angle must be between {@value #MIN_ANGLE_VALUE} and {@value #MAX_ANGLE_VALUE}.
*
* @param angle The angle to be set
*/
public void setShimmerAngle(int angle) {
if (angle < MIN_ANGLE_VALUE || MAX_ANGLE_VALUE < angle) {
throw new IllegalArgumentException(String.format("shimmerAngle value must be between %d and %d",
MIN_ANGLE_VALUE,
MAX_ANGLE_VALUE));
}
this.shimmerAngle = angle;
resetIfStarted();
}
/**
* Sets the width of the shimmer line to a value higher than 0 to less or equal to 1.
* 1 means the width of the shimmer line is equal to half of the width of the ShimmerLayout.
* The default value is 0.5.
*
* @param maskWidth The width of the shimmer line.
*/
public void setMaskWidth(float maskWidth) {
if (maskWidth <= MIN_MASK_WIDTH_VALUE || MAX_MASK_WIDTH_VALUE < maskWidth) {
throw new IllegalArgumentException(String.format("maskWidth value must be higher than %d and less or equal to %d",
MIN_MASK_WIDTH_VALUE, MAX_MASK_WIDTH_VALUE));
}
this.maskWidth = maskWidth;
resetIfStarted();
}
/**
* Sets the width of the center gradient color to a value higher than 0 to less than 1.
* 0.99 means that the whole shimmer line will have this color with a little transparent edges.
* The default value is 0.1.
*
* @param gradientCenterColorWidth The width of the center gradient color.
*/
public void setGradientCenterColorWidth(float gradientCenterColorWidth) {
if (gradientCenterColorWidth <= MIN_GRADIENT_CENTER_COLOR_WIDTH_VALUE
|| MAX_GRADIENT_CENTER_COLOR_WIDTH_VALUE <= gradientCenterColorWidth) {
throw new IllegalArgumentException(String.format("gradientCenterColorWidth value must be higher than %d and less than %d",
MIN_GRADIENT_CENTER_COLOR_WIDTH_VALUE, MAX_GRADIENT_CENTER_COLOR_WIDTH_VALUE));
}
this.gradientCenterColorWidth = gradientCenterColorWidth;
resetIfStarted();
}
private void resetIfStarted() {
if (isAnimationStarted) {
resetShimmering();
startShimmerAnimation();
}
}
private void dispatchDrawShimmer(Canvas canvas) {
super.dispatchDraw(canvas);
localMaskBitmap = getMaskBitmap();
if (localMaskBitmap == null) {
return;
}
if (canvasForShimmerMask == null) {
canvasForShimmerMask = new Canvas(localMaskBitmap);
}
canvasForShimmerMask.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
canvasForShimmerMask.save();
canvasForShimmerMask.translate(-maskOffsetX, 0);
super.dispatchDraw(canvasForShimmerMask);
canvasForShimmerMask.restore();
drawShimmer(canvas);
localMaskBitmap = null;
}
private void drawShimmer(Canvas destinationCanvas) {
createShimmerPaint();
destinationCanvas.save();
destinationCanvas.translate(maskOffsetX, 0);
destinationCanvas.drawRect(maskRect.left, 0, maskRect.width(), maskRect.height(), gradientTexturePaint);
destinationCanvas.restore();
}
private void resetShimmering() {
if (maskAnimator != null) {
maskAnimator.end();
maskAnimator.removeAllUpdateListeners();
}
maskAnimator = null;
gradientTexturePaint = null;
isAnimationStarted = false;
releaseBitMaps();
}
private void releaseBitMaps() {
canvasForShimmerMask = null;
if (maskBitmap != null) {
maskBitmap.recycle();
maskBitmap = null;
}
}
private Bitmap getMaskBitmap() {
if (maskBitmap == null) {
maskBitmap = createBitmap(maskRect.width(), getHeight());
}
return maskBitmap;
}
private void createShimmerPaint() {
if (gradientTexturePaint != null) {
return;
}
final int edgeColor = reduceColorAlphaValueToZero(shimmerColor);
final float shimmerLineWidth = getWidth() / 2 * maskWidth;
final float yPosition = (0 <= shimmerAngle) ? getHeight() : 0;
LinearGradient gradient = new LinearGradient(
0, yPosition,
(float) Math.cos(Math.toRadians(shimmerAngle)) * shimmerLineWidth,
yPosition + (float) Math.sin(Math.toRadians(shimmerAngle)) * shimmerLineWidth,
new int[]{edgeColor, shimmerColor, shimmerColor, edgeColor},
getGradientColorDistribution(),
Shader.TileMode.CLAMP);
BitmapShader maskBitmapShader = new BitmapShader(localMaskBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
ComposeShader composeShader = new ComposeShader(gradient, maskBitmapShader, PorterDuff.Mode.DST_IN);
gradientTexturePaint = new Paint();
gradientTexturePaint.setAntiAlias(true);
gradientTexturePaint.setDither(true);
gradientTexturePaint.setFilterBitmap(true);
gradientTexturePaint.setShader(composeShader);
}
private Animator getShimmerAnimation() {
if (maskAnimator != null) {
return maskAnimator;
}
if (maskRect == null) {
maskRect = calculateBitmapMaskRect();
}
final int animationToX = getWidth();
final int animationFromX;
if (getWidth() > maskRect.width()) {
animationFromX = -animationToX;
} else {
animationFromX = -maskRect.width();
}
final int shimmerBitmapWidth = maskRect.width();
final int shimmerAnimationFullLength = animationToX - animationFromX;
maskAnimator = isAnimationReversed ? ValueAnimator.ofInt(shimmerAnimationFullLength, 0)
: ValueAnimator.ofInt(0, shimmerAnimationFullLength);
maskAnimator.setDuration(shimmerAnimationDuration);
maskAnimator.setRepeatCount(ObjectAnimator.INFINITE);
maskAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
maskOffsetX = animationFromX + (int) animation.getAnimatedValue();
if (maskOffsetX + shimmerBitmapWidth >= 0) {
invalidate();
}
}
});
return maskAnimator;
}
private Bitmap createBitmap(int width, int height) {
try {
return Bitmap.createBitmap(width, height, Bitmap.Config.ALPHA_8);
} catch (OutOfMemoryError e) {
System.gc();
return null;
}
}
private int getColor(int id) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getContext().getColor(id);
} else {
//noinspection deprecation
return getResources().getColor(id);
}
}
private int reduceColorAlphaValueToZero(int actualColor) {
return Color.argb(0, Color.red(actualColor), Color.green(actualColor), Color.blue(actualColor));
}
private Rect calculateBitmapMaskRect() {
return new Rect(0, 0, calculateMaskWidth(), getHeight());
}
private int calculateMaskWidth() {
final double shimmerLineBottomWidth = (getWidth() / 2 * maskWidth) / Math.cos(Math.toRadians(Math.abs(shimmerAngle)));
final double shimmerLineRemainingTopWidth = getHeight() * Math.tan(Math.toRadians(Math.abs(shimmerAngle)));
return (int) (shimmerLineBottomWidth + shimmerLineRemainingTopWidth);
}
private float[] getGradientColorDistribution() {
final float[] colorDistribution = new float[4];
colorDistribution[0] = 0;
colorDistribution[3] = 1;
colorDistribution[1] = 0.5F - gradientCenterColorWidth / 2F;
colorDistribution[2] = 0.5F + gradientCenterColorWidth / 2F;
return colorDistribution;
}
}
先看构造方法吧:
public ShimmerLayout(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//ViewGroup为了提升性能默认情况下是不开启绘制的,这里需要绘制,设置为false
setWillNotDraw(false);
TypedArray a = context.getTheme().obtainStyledAttributes(
attrs,
R.styleable.ShimmerLayout,
0, 0);
try {
shimmerAngle = a.getInteger(R.styleable.ShimmerLayout_shimmer_angle, DEFAULT_ANGLE);
shimmerAnimationDuration = a.getInteger(R.styleable.ShimmerLayout_shimmer_animation_duration, DEFAULT_ANIMATION_DURATION);
shimmerColor = a.getColor(R.styleable.ShimmerLayout_shimmer_color, getColor(R.color.shimmer_color));
autoStart = a.getBoolean(R.styleable.ShimmerLayout_shimmer_auto_start, false);
maskWidth = a.getFloat(R.styleable.ShimmerLayout_shimmer_mask_width, 0.5F);
gradientCenterColorWidth = a.getFloat(R.styleable.ShimmerLayout_shimmer_gradient_center_color_width, 0.1F);
isAnimationReversed = a.getBoolean(R.styleable.ShimmerLayout_shimmer_reverse_animation, false);
} finally {
a.recycle();
}
//接下来都是设置一些属性值。我们拿一个看看
setMaskWidth(maskWidth);
setGradientCenterColorWidth(gradientCenterColorWidth);
setShimmerAngle(shimmerAngle);
if (autoStart && getVisibility() == VISIBLE) {
startShimmerAnimation();
}
}
public void setMaskWidth(float maskWidth) {
//不在范围内,就直接抛异常
if (maskWidth <= MIN_MASK_WIDTH_VALUE || MAX_MASK_WIDTH_VALUE < maskWidth) {
throw new IllegalArgumentException(String.format("maskWidth value must be higher than %d and less or equal to %d",
MIN_MASK_WIDTH_VALUE, MAX_MASK_WIDTH_VALUE));
}
this.maskWidth = maskWidth;
resetIfStarted();
}
private void resetIfStarted() {
if (isAnimationStarted) {//如果需要这个时候开启动画,那么就先重置动画,然后再开启。
resetShimmering();
startShimmerAnimation();
}
}
接下来看下
public void startShimmerAnimation() {
if (isAnimationStarted) {
return;
}
if (getWidth() == 0) {
startAnimationPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
getViewTreeObserver().removeOnPreDrawListener(this);
startShimmerAnimation();
return true;
}
};
getViewTreeObserver().addOnPreDrawListener(startAnimationPreDrawListener);
return;
}
Animator animator = getShimmerAnimation();
animator.start();
isAnimationStarted = true;
}
进入判断,如果动画是开启的,那么直接返回,如果宽度为0,又一遍调用startShimmerAnimation递归,直到绘制完成宽度不为0,这时候去获取动画,开启动画
private Animator getShimmerAnimation() {
if (maskAnimator != null) {
return maskAnimator;
}
if (maskRect == null) {
maskRect = calculateBitmapMaskRect();
}
final int animationToX = getWidth();
final int animationFromX;
if (getWidth() > maskRect.width()) {
animationFromX = -animationToX;
} else {
animationFromX = -maskRect.width();
}
final int shimmerBitmapWidth = maskRect.width();
final int shimmerAnimationFullLength = animationToX - animationFromX;
maskAnimator = isAnimationReversed ? ValueAnimator.ofInt(shimmerAnimationFullLength, 0)
: ValueAnimator.ofInt(0, shimmerAnimationFullLength);
maskAnimator.setDuration(shimmerAnimationDuration);
maskAnimator.setRepeatCount(ObjectAnimator.INFINITE);
maskAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
maskOffsetX = animationFromX + (int) animation.getAnimatedValue();
if (maskOffsetX + shimmerBitmapWidth >= 0) {
invalidate();
}
}
});
return maskAnimator;
}
这个方法其实就是创建一个属性动画,其中calculateBitmapMaskRect返回了一个根据角度生成的一个矩形,矩形会根据属性动画不断地从左往右,根据属性动画传入的时长,不断的右向位移,直到位移大于0重新绘制,最后将该动画返回并开启。
private Rect calculateBitmapMaskRect() {
return new Rect(0, 0, calculateMaskWidth(), getHeight());
}
private int calculateMaskWidth() {
final double shimmerLineBottomWidth = (getWidth() / 2 * maskWidth) / Math.cos(Math.toRadians(Math.abs(shimmerAngle)));
final double shimmerLineRemainingTopWidth = getHeight() * Math.tan(Math.toRadians(Math.abs(shimmerAngle)));
return (int) (shimmerLineBottomWidth + shimmerLineRemainingTopWidth);
}