Skip to content

Instantly share code, notes, and snippets.

@ericleong
Created October 26, 2015 04:17
Show Gist options
  • Save ericleong/f1a73495245f8c968e98 to your computer and use it in GitHub Desktop.
Save ericleong/f1a73495245f8c968e98 to your computer and use it in GitHub Desktop.
Render a list of drawables as layers on a canvas, with support for different blend modes.

about

These utility methods will render a list of drawables to a canvas, stretching the drawables to "cover" the canvas while maintaining their aspect ratio. The layers are blended by setting the PorterDuff blend mode on the Paint object. The code can be easily updated to support different blend modes for different layers.

Visit giferator for a web demo (with more blend modes).

It may be possible to use a LayerDrawable to accomplish this, but it isn't quite as configurable.

boilerplate

Below is that boilerplate view and surface rendering code, which usually lives in your fragment or activity's onCreate():

List<Drawable> drawables = new List<>();
SurfaceView surfaceView = new SurfaceView(context);
final Layer layer;
surfaceView.getHolder().addCallback(new SurfaceHolder.Callback() {
    public void surfaceCreated(SurfaceHolder holder) {
        ImageUtil.render(surfaceView, layer, drawables, paint);
    }
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        layer = new Layer(width, height);
        ImageUtil.render(surfaceView, layer, drawables, paint);
    }
    public void surfaceDestroyed(SurfaceHolder holder) {
        // clean up.
    }
});

Notice that paint is missing, and it can be initialized like this:

paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SCREEN));

gifs

For those using Glide's GifDrawable, make sure to set a callback like this:

drawable.setCallback(callback);
drawable.start();

Also, create a new thread to use in your callback:

HandlerThread thread = new HandlerThread("Gifs");
thread.start();
handler = new Handler(thread.getLooper());

and use that handler inside your callback. This keeps the rendering off the main thread.

package com.eleong.giferator;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import java.util.List;
/**
* Basic image rendering utilities.
* <p/>
* Created by Eric on 9/23/2015.
*/
public class ImageUtil {
private static final String TAG = ImageUtil.class.getSimpleName();
/**
* Sets the bounds on the drawable to "cover" the width and height.
*
* @param drawable the drawable to render.
* @param width the target width.
* @param height the target height.
* @see <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Background_and_Borders/Scaling_background_images#cover">Mozilla Documentation</a>
*/
public static void cover(@NonNull final Drawable drawable, final int width, final int height) {
if (width > 0 && height > 0 && drawable.getIntrinsicWidth() > 0 &&
drawable.getIntrinsicHeight() > 0) {
final double targetRatio = (double) width / height;
final double drawableRatio = (double) drawable.getIntrinsicWidth()
/ drawable.getIntrinsicHeight();
if (drawableRatio > targetRatio) { // wider
final int fullWidth = (int) Math.ceil(height * drawableRatio);
drawable.setBounds((width - fullWidth) / 2, 0,
(fullWidth - width) / 2 + width, height);
} else if (drawableRatio < targetRatio) { // taller
final int fullHeight = (int) Math.ceil(width / drawableRatio);
drawable.setBounds(0, (height - fullHeight) / 2,
width, (fullHeight - height) / 2 + height);
} else { // same ratio
drawable.setBounds(0, 0, width, height);
}
}
}
/**
* Renders a list of drawable to a {@link SurfaceView} using the provided {@link Paint}.
* {@link Layer} is an optimization to reuse offscreen buffers.
*
* @param surfaceView the surface to draw on.
* @param layer the layer object to reuse.
* @param drawables the list of drawables to draw.
* @param paint the paint to draw with for the drawables after the first one.
*/
public static void render(@Nullable final SurfaceView surfaceView, @Nullable final Layer layer,
@NonNull final List<? extends Drawable> drawables,
@NonNull final Paint paint) {
if (surfaceView != null && surfaceView.getHolder() != null) {
Canvas canvas = null;
final SurfaceHolder holder = surfaceView.getHolder();
try {
synchronized (holder) {
canvas = holder.lockCanvas();
if (canvas != null) {
draw(canvas, layer, drawables, paint);
} else {
Log.e(TAG, "Could not lock canvas!");
}
}
} finally {
if (canvas != null && holder != null) {
holder.unlockCanvasAndPost(canvas);
}
}
} else {
Log.e(TAG, "No surface or surface holder!");
}
}
/**
* @param canvas
* the canvas to draw to.
* @param layer
* the layer to reuse.
* @param drawables
* the drawables to draw.
* @param paint
* the paint to use.
*/
private static void draw(@NonNull final Canvas canvas, @Nullable final Layer layer,
@NonNull final List<? extends Drawable> drawables,
@NonNull final Paint paint) {
for (int i = 0; i < drawables.size(); i++) {
final Drawable drawable = drawables.get(i);
cover(drawable, canvas.getWidth(), canvas.getHeight());
if (i == 0) {
drawable.draw(canvas);
} else if (layer != null) {
// Clear the layer canvas.
layer.mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
// Render drawable to layer canvas.
drawable.draw(layer.mCanvas);
// Render layer canvas to the primary canvas.
paint.setShader(layer.mShader);
canvas.drawRect(0, 0, layer.mCanvas.getWidth(), layer.mCanvas.getHeight(), paint);
}
}
}
}
package com.eleong.giferator;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Shader;
import android.support.annotation.NonNull;
/**
* Simple struct that stores the objects needed
* <p/>
* Created by Eric on 9/23/2015.
*/
public class Layer {
@NonNull
protected final Canvas mCanvas;
@NonNull
protected final BitmapShader mShader;
/**
* @param width desired width of the layer.
* @param height desired height of the layer.
*/
public Layer(final int width, final int height) {
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
mCanvas = new Canvas(bitmap);
mShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment