0

I wanted to draw on the canvas with the code below with my custom brush, but as you can see in the picture, the background of my brush is black, albeit without color.

Although I specified the brush color as Color.TRANSPARENT or Color.parseColor ("# 00000000"), the brush background still turns black.

How can I make the background color of my brush transparent?

click to see the picture

import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import androidx.annotation.ColorInt;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.view.View;

import java.util.Stack;

public class BrushDrawingView extends View {

    static final float DEFAULT_BRUSH_SIZE = 50.0f;
    static final float DEFAULT_ERASER_SIZE = 50.0f;
    static final int DEFAULT_OPACITY = 255;

    private float mBrushSize = DEFAULT_BRUSH_SIZE;
    private float mBrushEraserSize = DEFAULT_ERASER_SIZE;
    private int mOpacity = DEFAULT_OPACITY;

    private final Stack<BrushLinePath> mDrawnPaths = new Stack<>();
    private final Stack<BrushLinePath> mRedoPaths = new Stack<>();
    private final Paint mDrawPaint = new Paint();

    private Canvas mDrawCanvas;
    private boolean mBrushDrawMode;
    private Bitmap brushBitmap;

    private Path mPath;
    private float mTouchX, mTouchY;
    private static final float TOUCH_TOLERANCE = 4;

    private BrushViewChangeListener mBrushViewChangeListener;

    public BrushDrawingView(Context context) {
        this(context, null);
    }

    public BrushDrawingView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public BrushDrawingView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        setupBrushDrawing();
    }

    private void setupBrushDrawing() {
        //Caution: This line is to disable hardware acceleration to make eraser feature work properly
        setupPathAndPaint();
        setVisibility(View.GONE);
    }

    private void setupPathAndPaint() {
        mPath = new Path();
        mDrawPaint.setAntiAlias(true);
        mDrawPaint.setStyle(Paint.Style.STROKE);
        mDrawPaint.setStrokeJoin(Paint.Join.ROUND);
        mDrawPaint.setStrokeCap(Paint.Cap.ROUND);
        mDrawPaint.setStrokeWidth(mBrushSize);
        mDrawPaint.setAlpha(mOpacity);
    }

    private void refreshBrushDrawing() {
        mBrushDrawMode = true;
        setupPathAndPaint();
    }

    void brushEraser() {
        mBrushDrawMode = true;
        mDrawPaint.setStrokeWidth(mBrushEraserSize);
        mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
    }

    public void setBrushDrawingMode(boolean brushDrawMode) {
        this.mBrushDrawMode = brushDrawMode;
        if (brushDrawMode) {
            this.setVisibility(View.VISIBLE);
            refreshBrushDrawing();
        }
    }

    public Bitmap getBrushBitmap() {
        return brushBitmap;
    }

    public void setBrushBitmap(Bitmap brushBitmap) {
        this.brushBitmap = brushBitmap;
    }

    public void setOpacity(@IntRange(from = 0, to = 255) int opacity) {
        this.mOpacity = (int) (opacity * 2.55f);
        setBrushDrawingMode(true);
    }

    public int getOpacity() {
        return mOpacity;
    }

    boolean getBrushDrawingMode() {
        return mBrushDrawMode;
    }

    public void setBrushSize(float size) {
        mBrushSize = 5 + (int) (size);
        setBrushDrawingMode(true);
    }

    void setBrushColor(@ColorInt int color) {
        mDrawPaint.setColor(color);
        setBrushDrawingMode(true);
    }

    void setBrushEraserSize(float brushEraserSize) {
        this.mBrushEraserSize = brushEraserSize;
        setBrushDrawingMode(true);
    }

    void setBrushEraserColor(@ColorInt int color) {
        mDrawPaint.setColor(color);
        setBrushDrawingMode(true);
    }

    float getEraserSize() {
        return mBrushEraserSize;
    }

    public float getBrushSize() {
        return mBrushSize;
    }

    int getBrushColor() {
        return mDrawPaint.getColor();
    }

    public void clearAll() {
        mDrawnPaths.clear();
        mRedoPaths.clear();
        if (mDrawCanvas != null) {
            mDrawCanvas.drawColor(0, PorterDuff.Mode.CLEAR);
        }
        invalidate();
    }

    void setBrushViewChangeListener(BrushViewChangeListener brushViewChangeListener) {
        mBrushViewChangeListener = brushViewChangeListener;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Bitmap canvasBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
        mDrawCanvas = new Canvas(canvasBitmap);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        for (BrushLinePath linePath : mDrawnPaths) {
            canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
        }
        canvas.drawPath(mPath, mDrawPaint);
        /////
        final Bitmap scaledBitmap = getScaledBitmap();

        final float centerX = scaledBitmap.getWidth() / 2;
        final float centerY = scaledBitmap.getHeight() / 2;

        final PathMeasure pathMeasure = new PathMeasure(mPath, false);

        float distance = scaledBitmap.getWidth() / 2;

        float[] position = new float[2];
        float[] slope = new float[2];

        float slopeDegree;

        while (distance < pathMeasure.getLength())
        {
            pathMeasure.getPosTan(distance, position, slope);
            slopeDegree = (float)((Math.atan2(slope[1], slope[0]) * 180f) / Math.PI);
            canvas.save();
            canvas.translate(position[0] - centerX, position[1] - centerY);
            canvas.rotate(slopeDegree, centerX, centerY);
            canvas.drawBitmap(scaledBitmap, 0, 0, mDrawPaint);
            canvas.restore();
            distance += scaledBitmap.getWidth() + 10;
        }

    }

    /////

    private Bitmap getScaledBitmap()
    {
        // width / height of the bitmap[
        float width = brushBitmap.getWidth();
        float height = brushBitmap.getHeight();

        // ratio of the bitmap
        float ratio = width / height;

        // set the height of the bitmap to the width of the path (from the paint object).
        float scaledHeight = mDrawPaint.getStrokeWidth();

        // to maintain aspect ratio of the bitmap, use the height * ratio for the width.
        float scaledWidth = scaledHeight * ratio;

        // return the generated bitmap, scaled to the correct size.
        return Bitmap.createScaledBitmap(brushBitmap, (int)scaledWidth, (int)scaledHeight, true);
    }

    /**
     * Handle touch event to draw paint on canvas i.e brush drawing
     *
     * @param event points having touch info
     * @return true if handling touch events
     */
    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(@NonNull MotionEvent event) {
        if (mBrushDrawMode) {
            float touchX = event.getX();
            float touchY = event.getY();
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    touchStart(touchX, touchY);
                    break;
                case MotionEvent.ACTION_MOVE:
                    touchMove(touchX, touchY);
                    break;
                case MotionEvent.ACTION_UP:
                    touchUp();
                    break;
            }
            invalidate();
            return true;
        } else {
            return false;
        }
    }

    boolean undo() {
        if (!mDrawnPaths.empty()) {
            mRedoPaths.push(mDrawnPaths.pop());
            invalidate();
        }
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onViewRemoved(this);
        }
        return !mDrawnPaths.empty();
    }

    boolean redo() {
        if (!mRedoPaths.empty()) {
            mDrawnPaths.push(mRedoPaths.pop());
            invalidate();
        }

        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onViewAdd(this);
        }
        return !mRedoPaths.empty();
    }


    private void touchStart(float x, float y) {
        mRedoPaths.clear();
        mPath.reset();
        mPath.moveTo(x, y);
        mTouchX = x;
        mTouchY = y;
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onStartDrawing();
        }
    }

    private void touchMove(float x, float y) {
        float dx = Math.abs(x - mTouchX);
        float dy = Math.abs(y - mTouchY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mTouchX, mTouchY, (x + mTouchX) / 2, (y + mTouchY) / 2);
            mTouchX = x;
            mTouchY = y;
        }
    }

    private void touchUp() {
        mPath.lineTo(mTouchX, mTouchY);
        // Commit the path to our offscreen
        mDrawCanvas.drawPath(mPath, mDrawPaint);
        // kill this so we don't double draw

        mDrawnPaths.push(new BrushLinePath(mPath, mDrawPaint));

        /////

        final Bitmap scaledBitmap = getScaledBitmap();

        final float centerX = scaledBitmap.getWidth() / 2;
        final float centerY = scaledBitmap.getHeight() / 2;

        final PathMeasure pathMeasure = new PathMeasure(mPath, false);

        float distance = scaledBitmap.getWidth() / 2;

        float[] position = new float[2];
        float[] slope = new float[2];

        float slopeDegree;

        while (distance < pathMeasure.getLength())
        {
            pathMeasure.getPosTan(distance, position, slope);
            slopeDegree = (float)((Math.atan2(slope[1], slope[0]) * 180f) / Math.PI);
            mDrawCanvas.save();
            mDrawCanvas.translate(position[0] - centerX, position[1] - centerY);
            mDrawCanvas.rotate(slopeDegree, centerX, centerY);
            mDrawCanvas.drawBitmap(scaledBitmap, 0, 0, mDrawPaint);
            mDrawCanvas.restore();
            distance += scaledBitmap.getWidth() + 10;
        }

        /////

        mPath = new Path();
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onStopDrawing();
            mBrushViewChangeListener.onViewAdd(this);
        }
    }

    @VisibleForTesting
    Paint getDrawingPaint() {
        return mDrawPaint;
    }

    @VisibleForTesting
    Pair<Stack<BrushLinePath>, Stack<BrushLinePath>> getDrawingPath() {
        return new Pair<>(mDrawnPaths, mRedoPaths);
    }
}

public interface BrushViewChangeListener {
    void onViewAdd(BrushDrawingView brushDrawingView);

    void onViewRemoved(BrushDrawingView brushDrawingView);

    void onStartDrawing();

    void onStopDrawing();
}

class BrushLinePath {
    private final Paint mDrawPaint;
    private final Path mDrawPath;

    BrushLinePath(final Path drawPath, final Paint drawPaints) {
        mDrawPaint = new Paint(drawPaints);
        mDrawPath = new Path(drawPath);
    }

    Paint getDrawPaint() {
        return mDrawPaint;
    }

    Path getDrawPath() {
        return mDrawPath;
    }
}

1 Answers1

1

The reason it happens is because Paint doesn't have an alpha composing mode set by default. Thus, when you're trying to paint a bitmap over your canvas it will replace the destination pixels with your brush pixels, which in your case is #00000000. And that will result in pixel being displayed as black. Have a look into this documentation: https://developer.android.com/reference/android/graphics/PorterDuff.Mode

By the first glance it seems you're looking for PorterDuff.Mode.SRC_OVER or PorterDuff.Mode.SRC_ATOP - this way transparent pixels from your source image (your brush) will not over-draw the pixels from your destination (canvas). In case your background is always non-transparent, you will see no difference between SRC_OVER and SRC_ATOP, but if it isn't - choose the one which fits your needs. Then you can modify setupPathAndPaint method by adding this line to its end:

    mDrawPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
santoni7
  • 365
  • 2
  • 11
  • Thank you for your answer but this code was already added but I removed it –  Jan 16 '21 at 08:31
  • Although the code was attached, the background was still black –  Jan 16 '21 at 08:32
  • Hm, i see. Looking at your screenshot and code, I have a question: how do you draw your photo background? If you're using an ImageView under BrushDrawingView or setting background to BrushDrawingView, that could explain why it's still black. You need to draw your photo background at the very beginning of BrushDrawingView's onDraw method, so that canvas it populated with non-transparent pixels before you apply brush paint with PorterDuff.Mode.SRC_OVER – santoni7 Jan 16 '21 at 10:41
  • Oh, and also @HaticeHanım where do you set your Paint color to transparent? Seems that `setBrushColor()` method sets the color at first but then `setupPathAndPaint()` will be also called and it has the line `mDrawPaint.setAlpha(mOpacity);`. This line will override the opacity of color you've set inside `setBrushColor` – santoni7 Jan 16 '21 at 11:50
  • I use imageview under brushdrawingview as background. Do I have to do it as shown below? @Override protected void onDraw(Canvas canvas) { canvas.draw Bitmap(bgBitmap,0,0,null); for (BrushLinePath linePath : mDrawnPaths) { canvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint()); } canvas.drawPath(mPath, mDrawPaint); /////other codes ......... } –  Jan 16 '21 at 14:36
  • Yeah seems correct. Try that and setting SRC_ATOP/SRC_OVER. – santoni7 Jan 16 '21 at 17:23
  • Isn't there any other way around this? My goal is to take a screenshot of the canvas while saving and merge it with a bitmap. Example: canvas size (1000 * 1500), bitmap size (2000 * 3000), I would make the canvas size the same as the bitmap and combine it, but if I draw a bitmap on canvas on the drawing canvas, I can't do that. –  Jan 17 '21 at 11:10
  • I see. I'll have to import the code into IDE and examine it to be able to help you, but not sure when exactly I'll have time. You can look into brush color and opacity comment i've written above – santoni7 Jan 19 '21 at 16:35
  • I found the solution, I set the transparency of an image to 0 with Photoshop and I introduced that image to painte as a shader and the background became transparent. –  Feb 02 '21 at 07:21