Lesson 167. Drawing. Canvas saveLayer method.
Android Lessons

Lesson 167. Drawing. Canvas saveLayer method.


In this lesson:

– we use Canvas.saveLayer method

In Lesson 146, we dealt with the save method. It can save the state of the canvas, perform various transformations and return to the saved state by the restore method. The saveLayer method creates a separate Bitmap canvas and forwards all subsequent canvas operations to it. And then to write the received result from Bitmap on an outline, it is necessary to call method restore. Those who have worked at least with graphic editors can draw an analogy with layers. You create a separate layer, draw something on it, then merge it with the main image. Actually, the method is called saveLayer – “save layer”.

At first, it seems that this method is absolutely ridiculous. What’s the point of highlighting a single layer to draw something on it and then bring it to the main canvas anyway? It’s easier to draw and draw right away. It turns out the meaning is there. And now we will find an example, after which it becomes clear why this mechanism may be needed.

take the picture

and try to draw such an effect.

That is, a picture overlays a frame. In the center, the frame is transparent and the edges become darkened translucent.

I kindly put the idea behind this example from here. But not to smoke at all, the picture was taken by another.

The plan is as follows:

1) Canvas image

2) Create a layer on which to draw a translucent frame

3) Impose a layer-frame on the picture

Let’s start by creating a translucent frame.

class MainActivity:

public class MainActivity extends Activity {

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(new DrawView(this));
  }

  class DrawView extends View {

    Paint mShaderPaint;
    Paint mBlackPaint;
    Paint mPaint;
    Bitmap mBitmap;
    Rect mRect = new Rect(0, 40, 750, 370);
    RectF mRectF = new RectF(mRect);

    public DrawView(Context context) {
      super(context);
      setLayerType(LAYER_TYPE_SOFTWARE, null);
      init();
    }

    private void init() {
      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
    }

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawOval(mRectF, mShaderPaint);
    }

    private Shader createShader() {
      final int[] colors = new int[] { 0xff000000, 0xff000000, 0 };
      final float[] anchors = new float[] { 0, 0.5f, 1 };
      Shader shader = new android.graphics.RadialGradient(0, 0, 1,
          colors, anchors, Shader.TileMode.CLAMP);

      Matrix matrix = new Matrix();
      matrix.postTranslate(mRect.centerX(), mRect.centerY());
      matrix.postScale(mRect.width() / 2, mRect.height() / 2,
          mRect.centerX(), mRect.centerY());

      shader.setLocalMatrix(matrix);
      return shader;
    }

  }

}

In the method init create a brush and install the shader created in the createShader method.

To create a shader in createShader we use a gradient shader (Lesson 165). It is black (ff000000) in the center and will become transparent (00000000) to the edges. Notice that we created it at a point (0,0) and its radius is only 1. Then we apply a matrix (Lesson 144) to it to place it at the desired point and give it the necessary dimensions.

mRect is the coordinates of the rectangle in which the image will be displayed. Accordingly, we need to place the center of the gradient in the center of mRect, and the size of the gradient must be equal to the size of mRect.

In the method onDraw draw an oval on the screen using the created shader.

The center of the gradient is at the center of the mRect rectangle. And the gradient shape, initially circular, is slightly compressed vertically to fit into a rectangle. This is the result of the matrix.

We will now use this gradient to create the frame we need. To do this, we will take a translucent black background

and draw a gradient from above on PorterDuff.Mode.DST_OUT (Lesson 154).

Here is the formula for calculating alpha and color for DST_OUT mode: [Da * (1 – Sa), Dc * (1 – Sa)].

In our case:
Da is the transparency level of the black background
Dc – color black background
Sa is the level of transparency of the gradient

Note that the gradient in the formula uses only alpha. That is, the color there may be at least red-green. It will be ignored by this overlay mode.

That is, where the gradient is most opaque, the expression (1 – Sa) tends to zero, and therefore, tends to zero the final alpha and color values ​​resulting from the overlay. And the pixels there will be as transparent as possible.

And, where the gradient is least transparent, expression (1 – Sa) tends to be unity, and thus the final alpha and color values ​​tend to Da and Dc. That is, the pixels there will be the same as in the black background.

As a result, we will get a black background with a transparent center, and the edges will remain almost unchanged.

We implement this in code. rewrite the method init:

    private void init() {
      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
      mShaderPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
      
      mBlackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBlackPaint.setColor(Color.BLACK);
      mBlackPaint.setAlpha(100);
    }

Add DST_OUT to the shader brush. And create a brush with translucent black.

and method onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
    }

Output the background, and on it an oval with a shader.

We received a layer with a transparency frame.

The center of this frame is not white but transparent, with a white background shining through it.

Let’s try to draw all this on the image at once, without using the saveLayer method.

rewrite init:

    private void init() {
      mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBitmap = BitmapFactory.decodeResource(getResources(),
          R.drawable.image);
      mBitmap = Bitmap.createScaledBitmap(mBitmap, mRect.width(),
          mRect.height(), true);

      mShaderPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mShaderPaint.setShader(createShader());
      mShaderPaint.setXfermode(new PorterDuffXfermode(
          PorterDuff.Mode.DST_OUT));

      mBlackPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
      mBlackPaint.setColor(Color.BLACK);
      mBlackPaint.setAlpha(100);
    }

We add creation of a picture and a usual brush for its deduction.

and onDraw

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawBitmap(mBitmap, 0, 0, mPaint);
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
    }

We first draw a picture, then a background, then an oval.

It didn’t turn out exactly what we expected. The image in the center also became transparent. It happened because at first we drew a dark background into the picture, getting a just a darkened picture, and then did the DST_OUT gradient overlay. And this overlay affected the colors and alpha of the picture itself, making it transparent in the center.

That is why it is necessary to create a separate layer, to draw a frame there and then to paint it on top of the picture without any modes. Let’s check.

rewrite onDraw:

    @Override
    protected void onDraw(Canvas canvas) {
      canvas.drawBitmap(mBitmap, 0, 0, mPaint);
      canvas.saveLayer(mRectF, mPaint, Canvas.ALL_SAVE_FLAG);
      canvas.drawRect(mRect, mBlackPaint);
      canvas.drawOval(mRectF, mShaderPaint);
      canvas.restore();
    }

We draw a picture. Then it switches to a separate layer with the saveLayer method, we draw a frame on it (background + oval with gradient in DST_OUT mode) and with the restore method we overlay this frame in the picture.

result:

The frame rests on top, providing the right level of transparency and without blurring the original.

If, instead of saveLayer, you simply create a Bitmap, draw a frame on it, and then just overlay that Bitmap on the picture, you get the same result. In principle, the saveLayer method does exactly that, judging by its help description.




Discuss in the forum [1 reply]

Leave a Reply

Your email address will not be published. Required fields are marked *