Last active
August 5, 2021 16:37
-
-
Save f3401pal/25c46a138447e778d0f05c8f3fe5fad4 to your computer and use it in GitHub Desktop.
ListShimmerView
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.ValueAnimator; | |
import android.content.Context; | |
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.RectF; | |
import android.graphics.Shader; | |
import android.support.annotation.NonNull; | |
import android.util.AttributeSet; | |
import android.util.DisplayMetrics; | |
import android.util.TypedValue; | |
import android.view.View; | |
import android.view.animation.LinearInterpolator; | |
public class ListShimmerView extends View implements ValueAnimator.AnimatorUpdateListener { | |
private static final String TAG = ListShimmerView.class.getSimpleName(); | |
private static final int V_SPACING_DP = 16; | |
private static final int H_SPACING_DP = 16; | |
private static final int IMAGE_SIZE_DP = 40; | |
private static final int LINE_HEIGHT_SP = 15; | |
private static final int CORNER_RADIUS_DP = 2; | |
private static final int ITEM_PATTERN_BG_COLOR = Color.WHITE; | |
private static final int CENTER_ALPHA = 50; | |
private static final int EDGE_ALPHA = 12; | |
private static final int SHADER_COLOR_R = 170; | |
private static final int SHADER_COLOR_G = 170; | |
private static final int SHADER_COLOR_B = 170; | |
private static final int CENTER_COLOR = Color.argb(CENTER_ALPHA, SHADER_COLOR_R, SHADER_COLOR_G, SHADER_COLOR_B); | |
private static final int EDGE_COLOR = Color.argb(EDGE_ALPHA, SHADER_COLOR_R, SHADER_COLOR_G, SHADER_COLOR_B); | |
private static final int ANIMATION_DURATION = 1500; | |
private static final int LIST_ITEM_LINES = 3; | |
private float vSpacing; | |
private float hSpacing; | |
private float lineHeight; | |
private float imageSize; | |
private float cornerRadius; | |
private Bitmap listItemPattern; | |
private Paint paint; | |
private Paint shaderPaint; | |
private int[] shaderColors; | |
private ValueAnimator animator; | |
public ListShimmerView(Context context) { | |
super(context); | |
init(context); | |
} | |
public ListShimmerView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
init(context); | |
} | |
public ListShimmerView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
init(context); | |
} | |
private void init(Context context) { | |
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); | |
vSpacing = dpToPixels(metrics, V_SPACING_DP); | |
hSpacing = dpToPixels(metrics, H_SPACING_DP); | |
lineHeight = spToPixels(metrics, LINE_HEIGHT_SP); | |
imageSize = dpToPixels(metrics, IMAGE_SIZE_DP); | |
cornerRadius = dpToPixels(metrics, CORNER_RADIUS_DP); | |
paint = new Paint(); | |
shaderPaint = new Paint(); | |
shaderPaint.setAntiAlias(true); | |
shaderColors = new int[]{EDGE_COLOR, CENTER_COLOR, EDGE_COLOR}; | |
animator = ValueAnimator.ofFloat(-1f, 2f); | |
animator.setDuration(ANIMATION_DURATION); | |
animator.setInterpolator(new LinearInterpolator()); | |
animator.setRepeatCount(ValueAnimator.INFINITE); | |
animator.addUpdateListener(this); | |
} | |
@Override | |
public void onAnimationUpdate(ValueAnimator valueAnimator) { | |
if(isAttachedToWindow()) { | |
float f = (float) valueAnimator.getAnimatedValue(); | |
updateShader(getWidth(), f); | |
invalidate(); | |
} else { | |
animator.cancel(); | |
} | |
} | |
@Override | |
protected void onVisibilityChanged(@NonNull View changedView, int visibility) { | |
super.onVisibilityChanged(changedView, visibility); | |
switch (visibility) { | |
case VISIBLE: | |
animator.start(); | |
break; | |
case INVISIBLE: | |
case GONE: | |
animator.cancel(); | |
break; | |
} | |
} | |
@Override | |
protected void onSizeChanged(int w, int h, int oldw, int oldh) { | |
super.onSizeChanged(w, h, oldw, oldh); | |
updateShader(w, -1f); | |
if(h > 0 && w > 0) { | |
preDrawItemPattern(w, h); | |
} else { | |
listItemPattern = null; | |
animator.cancel(); | |
} | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
canvas.drawColor(EDGE_COLOR); | |
// draw gradient background | |
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), shaderPaint); | |
if(listItemPattern != null) { | |
// draw list item pattern | |
canvas.drawBitmap(listItemPattern, 0, 0, paint); | |
} | |
} | |
private void updateShader(float w, float f) { | |
float left = w * f; | |
LinearGradient shader = new LinearGradient(left, 0f, left + w, 0f, | |
shaderColors, new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP); | |
shaderPaint.setShader(shader); | |
} | |
private void preDrawItemPattern(int w, int h) { | |
listItemPattern = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888); | |
// draw list items into the bitmap | |
Canvas canvas = new Canvas(listItemPattern); | |
Bitmap item = getItemBitmap(w); | |
int top = 0; | |
do { | |
canvas.drawBitmap(item, 0, top, paint); | |
top = top + item.getHeight(); | |
} while(top < canvas.getHeight()); | |
// only fill the rectangles with the background color | |
canvas.drawColor(ITEM_PATTERN_BG_COLOR, PorterDuff.Mode.SRC_IN); | |
} | |
private Bitmap getItemBitmap(int w) { | |
int h = calculatePatternHeight(LIST_ITEM_LINES); | |
// we only need Alpha value in this bitmap | |
Bitmap item = Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8); | |
Canvas canvas = new Canvas(item); | |
canvas.drawColor(Color.argb(255, 0, 0, 0)); | |
Paint itemPaint = new Paint(); | |
itemPaint.setAntiAlias(true); | |
itemPaint.setColor(Color.argb(0, 0, 0, 0)); | |
itemPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); | |
// avatar | |
RectF rectF = new RectF(vSpacing, hSpacing, vSpacing + imageSize, hSpacing + imageSize); | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint); | |
float textLeft = rectF.right + hSpacing; | |
float textRight = canvas.getWidth() - vSpacing; | |
// title line | |
float titleWidth = (float) ((textRight - textLeft) * 0.5); | |
rectF.set(textLeft, hSpacing, textLeft + titleWidth, hSpacing + lineHeight); | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint); | |
// timestamp | |
float timeWidth = (float) ((textRight - textLeft) * 0.2); | |
rectF.set(textRight - timeWidth, hSpacing, textRight, hSpacing + lineHeight); | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint); | |
// text lines | |
int line = LIST_ITEM_LINES - 1; | |
while(line > 0) { | |
float lineTop = rectF.bottom + hSpacing; | |
rectF.set(textLeft, lineTop, textRight, lineTop + lineHeight); | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint); | |
line--; | |
} | |
return item; | |
} | |
private int calculatePatternHeight(int lines) { | |
return (int) ((lines * lineHeight) + (hSpacing * (lines + 1))); | |
} | |
private float dpToPixels(DisplayMetrics metrics, int dp) { | |
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, metrics); | |
} | |
private float spToPixels(DisplayMetrics metrics, int sp) { | |
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, metrics); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.animation.ValueAnimator | |
import android.content.Context | |
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.RectF | |
import android.graphics.Shader | |
import android.util.AttributeSet | |
import android.util.DisplayMetrics | |
import android.util.TypedValue | |
import android.view.View | |
import android.view.animation.LinearInterpolator | |
class ListShimmerView : View, ValueAnimator.AnimatorUpdateListener { | |
private val centerColor = Color.argb(CENTER_ALPHA, SHADER_COLOR_R, SHADER_COLOR_G, SHADER_COLOR_B) | |
private val edgeColor = Color.argb(EDGE_ALPHA, SHADER_COLOR_R, SHADER_COLOR_G, SHADER_COLOR_B) | |
private val vSpacing: Float = dpToPixels(context.resources.displayMetrics, V_SPACING_DP) | |
private val hSpacing: Float = dpToPixels(context.resources.displayMetrics, H_SPACING_DP) | |
private val lineHeight: Float = spToPixels(context.resources.displayMetrics, LINE_HEIGHT_SP) | |
private val imageSize: Float = dpToPixels(context.resources.displayMetrics, IMAGE_SIZE_DP) | |
private val cornerRadius: Float = dpToPixels(context.resources.displayMetrics, CORNER_RADIUS_DP) | |
private var listItemPattern: Bitmap? = null | |
private val paint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) | |
private val shaderPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) | |
private val shaderColors: IntArray = intArrayOf(edgeColor, centerColor, edgeColor) | |
private val animator: ValueAnimator = ValueAnimator.ofFloat(-1f, 2f).apply { | |
duration = ANIMATION_DURATION.toLong() | |
interpolator = LinearInterpolator() | |
repeatCount = ValueAnimator.INFINITE | |
addUpdateListener(this@ListShimmerView) | |
} | |
constructor(context: Context) : super(context) | |
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) | |
constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) | |
override fun onAnimationUpdate(valueAnimator: ValueAnimator) { | |
if (isAttachedToWindow) { | |
val f = valueAnimator.animatedValue as Float | |
updateShader(width.toFloat(), f) | |
invalidate() | |
} else { | |
animator.cancel() | |
} | |
} | |
override fun onVisibilityChanged(changedView: View, visibility: Int) { | |
super.onVisibilityChanged(changedView, visibility) | |
when (visibility) { | |
View.VISIBLE -> animator.start() | |
View.INVISIBLE, View.GONE -> animator.cancel() | |
} | |
} | |
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { | |
super.onSizeChanged(w, h, oldw, oldh) | |
updateShader(w.toFloat(), -1f) | |
if (h > 0 && w > 0) { | |
preDrawItemPattern(w, h) | |
} | |
} | |
override fun onDetachedFromWindow() { | |
super.onDetachedFromWindow() | |
listItemPattern?.recycle() | |
animator.cancel() | |
} | |
override fun onDraw(canvas: Canvas) { | |
canvas.drawColor(edgeColor) | |
// draw gradient background | |
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), shaderPaint) | |
listItemPattern?.let { | |
// draw list item pattern | |
canvas.drawBitmap(it, 0f, 0f, paint) | |
} | |
} | |
private fun updateShader(w: Float, f: Float) { | |
val left = w * f | |
val shader = LinearGradient(left, 0f, left + w, 0f, | |
shaderColors, floatArrayOf(0f, .5f, 1f), Shader.TileMode.CLAMP) | |
shaderPaint.shader = shader | |
} | |
private fun preDrawItemPattern(w: Int, h: Int) { | |
listItemPattern = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888).apply { | |
// draw list items into the bitmap | |
val canvas = Canvas(this) | |
val item = getItemBitmap(w) | |
var top = 0 | |
do { | |
canvas.drawBitmap(item, 0f, top.toFloat(), paint) | |
top += item.height | |
} while (top < canvas.height) | |
// only fill the rectangles with the background color | |
canvas.drawColor(ITEM_PATTERN_BG_COLOR, PorterDuff.Mode.SRC_IN) | |
} | |
} | |
private fun getItemBitmap(w: Int): Bitmap { | |
val h = calculatePatternHeight(LIST_ITEM_LINES) | |
// we only need Alpha value in this bitmap | |
val item = Bitmap.createBitmap(w, h, Bitmap.Config.ALPHA_8) | |
val canvas = Canvas(item) | |
canvas.drawColor(Color.argb(255, 0, 0, 0)) | |
val itemPaint = Paint() | |
itemPaint.isAntiAlias = true | |
itemPaint.color = Color.argb(0, 0, 0, 0) | |
itemPaint.xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_IN) | |
// avatar | |
val rectF = RectF(vSpacing, hSpacing, vSpacing + imageSize, hSpacing + imageSize) | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint) | |
val textLeft = rectF.right + hSpacing | |
val textRight = canvas.width - vSpacing | |
// title line | |
val titleWidth = ((textRight - textLeft) * 0.5).toFloat() | |
rectF.set(textLeft, hSpacing, textLeft + titleWidth, hSpacing + lineHeight) | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint) | |
// timestamp | |
val timeWidth = ((textRight - textLeft) * 0.2).toFloat() | |
rectF.set(textRight - timeWidth, hSpacing, textRight, hSpacing + lineHeight) | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint) | |
// text lines | |
var line = LIST_ITEM_LINES - 1 | |
while (line > 0) { | |
val lineTop = rectF.bottom + hSpacing | |
rectF.set(textLeft, lineTop, textRight, lineTop + lineHeight) | |
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint) | |
line-- | |
} | |
return item | |
} | |
private fun calculatePatternHeight(lines: Int): Int { | |
return (lines * lineHeight + hSpacing * (lines + 1)).toInt() | |
} | |
companion object { | |
private const val TAG = "ListShimmerView" | |
private const val V_SPACING_DP = 16 | |
private const val H_SPACING_DP = 16 | |
private const val IMAGE_SIZE_DP = 40 | |
private const val LINE_HEIGHT_SP = 15 | |
private const val CORNER_RADIUS_DP = 2 | |
private const val ITEM_PATTERN_BG_COLOR = Color.WHITE | |
private const val CENTER_ALPHA = 50 | |
private const val EDGE_ALPHA = 12 | |
private const val SHADER_COLOR_R = 170 | |
private const val SHADER_COLOR_G = 170 | |
private const val SHADER_COLOR_B = 170 | |
private const val ANIMATION_DURATION = 1500 | |
private const val LIST_ITEM_LINES = 3 | |
private fun dpToPixels(metrics: DisplayMetrics, dp: Int): Float { | |
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), metrics) | |
} | |
private fun spToPixels(metrics: DisplayMetrics, sp: Int): Float { | |
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp.toFloat(), metrics) | |
} | |
} | |
} |
Free up bitmap memory while detaching the view from window. Above code throws OOM after reloading the view multiple times. Add the following piece of code to free up the bitmap memory.
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (listItemPattern != null && !listItemPattern.isRecycled()) { listItemPattern.recycle(); listItemPattern = null; } }
Thank you. I will make the change.
How can I use this ListShimmerView?
for showing and dismissing it
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Free up bitmap memory while detaching the view from window. Above code throws OOM after reloading the view multiple times. Add the following piece of code to free up the bitmap memory.
@Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); if (listItemPattern != null && !listItemPattern.isRecycled()) { listItemPattern.recycle(); listItemPattern = null; } }