Introduction
Recently I had a chance to make a custom view for a list loading state. We want something specific for list loading and a bit more exciting than the Android default spin indicator. So we find this shimmer animation on some skeleton UI element, like what the Facebook Android app does when loading your HOME screen.
There is already an open source Android library on Github does a similar thing, i.e. shimmer-android. It will apply shimmer animation to any view you added to the ShimmerFrameLayout
.
The example code used in this article could be found here.
Why Custom View?
If you are not familiar with Android Custom View yet, I suggest to reading this article. I choose to do a custom view because of better performance, simple view hierarchy and relatively simple graphic to draws.
Layers
To take this view apart, it consists 3 layers.
Choose some non-transparent color, so that the list skeleton has a base background color. i.e. #AAAAAA with 5% alpha.
A linear gradient shader with the edge colors same as the background color. Center color could have bigger alpha value. i.e. #AAAAAA with 20% alpha.
Note that the background is transparent instead of white shown here. The rectangle shapes have the base background color. #AAAAAA with 5% alpha
Efficient drawing
In Android, the View.onDraw
method is what all drawing happens. This method will be called multiple times on Android UI thread. Moreover, the Android system will drop frames if the animation drawing cannot keep up with the Vsync. So there are a few tips to keep your custom view more efficient.
- Absolutely no memory allocation in
onDraw
. i.e. create any new object instance - Do as much as calculations/logic outside of
onDraw
. i.e. update values related to view size inonSizeChanged
- Pre-draw any static graphic into a bitmap. So you only draw the bitmap instead of multiple drawings. i.e. the list skeleton
The shimmer view
Let’s started with prepare a static list skeleton bitmap and a gradient shader.
List skeleton
- Draw a list item skeleton. Note that the bitmap only needs alpha value.
// we only need Alpha value in this bitmap
Bitmap.Config conf = Bitmap.Config.ALPHA_8;
Bitmap item = Bitmap.createBitmap(w, h, conf);
Canvas canvas = new Canvas(item);
// fill canvas with non-transparent color
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));
// draw avatar
RectF rectF = new RectF(vSpacing, hSpacing, vSpacing + imageSize, hSpacing + imageSize);
canvas.drawRoundRect(rectF, cornerRadius, cornerRadius, itemPaint);//draw the reset of the elements...
- Repeat the list item pattern base on the view height.
// create a new bitmap
Bitmap 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);
Gradient shader
Create a new Paint
instance and set LinearGradient
on it.
Paint shaderPaint = new Paint();
shaderPaint.setAntiAlias(true);
int[] shaderColors = new int[]{EDGE_COLOR, CENTER_COLOR, EDGE_COLOR};
LinearGradient shader = new LinearGradient(0f, 0f, width, 0f,
shaderColors, new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP);
shaderPaint.setShader(shader);
onDraw
Now we have the pre-draw list skeleton bitmap and the gradient shader prepared. We can draw each layer in the onDraw
method.
canvas.drawColor(EDGE_COLOR);// draw gradient background
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), shaderPaint);// draw list item pattern
canvas.drawBitmap(listItemPattern, 0, 0, paint);
Animation
Now we have a static list shimmer view, we can start applying a left to right animation on the gradient shader.
- create a simple
ValueAnimator
ValueAnimator animator = ValueAnimator.ofFloat(-1f, 1f);
animator.setDuration(ANIMATION_DURATION);
animator.setInterpolator(new LinearInterpolator());
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.addUpdateListener(animatorUpdateListener);
- update the left position of gradient shader on animation update callback
animatorUpdateListener = new ValueAnimator.AnimatorUpdateListener { @Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
if(isAttachedToWindow()) {
float f = (float) valueAnimator.getAnimatedValue();
updateShader(getWidth(), f);
invalidate();
} else {
animator.cancel();
}
}}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);
}
- start animation
onVisibilityChanged
@Override
protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
switch (visibility) {
case VISIBLE:
animator.start();
break;
case INVISIBLE:
case GONE:
animator.cancel();
break;
}
}
Conclusion
Android Custom View is powerful, especially combining with animators. We could build any cool UI effects with them. However, implementing a custom view is more time consuming than using standard Android Widgets. Moreover, before choose to do a custom view, we should really understand how UI rendering on Android works and following the best practice to make sure the custom view is efficient.
Thank you
Andy Wang, Software Engineer