0

我想用我的自定义画笔使用下面的代码在画布上绘制,但正如你在图片中看到的那样,我的画笔的背景是黑色的,尽管没有颜色。

虽然我将画笔颜色指定为 Color.TRANSPARENT 或 Color.parseColor ("#00000000"),但画笔背景仍然变黑。

如何使画笔的背景颜色透明?

点击查看图片

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;
    }
}
4

1 回答 1

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));
于 2021-01-15T17:54:58.183 回答