to solve undesired PorterDuff
effect
use the simplest method at first, like the OP's problem, a Path.arcTo(*, *, *, *, false)
is enough -- note it's arcTo
, not addArc
, and the false
means no forceMoveTo
before adding arc -- there is no need for PorterDuff
.
Path arcPath = new Path();
@Override
protected void onDraw(Canvas canvas) {
arcPath.rewind();
arcPath.moveTo(arcRectF.centerX, arcRectF.centerY);
arcPath.arcTo(arcRectF, -90, currentAngleSweep, false);
arcPath.close();
canvas.clipPath(arcPath, Region.Op.DIFFERENCE);
canvas.drawBitmap(bitmap, circleSourceRect, circleDestRect, arcPaint);
}
if you really need PorterDuff, mainly for complex color morphing, like blending gradients, don't draw color or shape or bitmap with PorterDuff filtering effect directly to the default canvas provided in onDraw(Canvas)
, use some buffering/dest bitmap[s] with alpha channel--and setHasAlpha(true)
-- to store the result from PorterDuff filtering, at last draw the bitmap to the default canvas without applying any filtering except matrix changing.
here's a working example to create border blurred round image:
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.widget.ImageView;
/**
* Created by zdave on 6/22/17.
*/
public class BlurredCircleImageViewShader extends ImageView {
private Canvas mCanvas;
private Paint mPaint;
private Matrix matrix;
private static final float GRADIENT_RADIUS = 600f; //any value you like, but should be big enough for better resolution.
private Shader gradientShader;
private Bitmap bitmapGradient;
private Bitmap bitmapDest;
private Canvas canvasDest;
public BlurredCircleImageViewShader(Context context) {
this(context, null);
}
public BlurredCircleImageViewShader(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public BlurredCircleImageViewShader(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
matrix = new Matrix();
int[] colors = new int[]{Color.BLACK, Color.BLACK, Color.TRANSPARENT};
float[] colorStops = new float[]{0f, 0.5f, 1f};
gradientShader = new RadialGradient(GRADIENT_RADIUS, GRADIENT_RADIUS, GRADIENT_RADIUS, colors, colorStops, Shader.TileMode.CLAMP);
mPaint.setShader(gradientShader);
bitmapGradient = Bitmap.createBitmap((int)(GRADIENT_RADIUS * 2), (int)(GRADIENT_RADIUS * 2), Bitmap.Config.ARGB_8888);
bitmapDest = bitmapGradient.copy(Bitmap.Config.ARGB_8888, true);
Canvas canvas = new Canvas(bitmapGradient);
canvas.drawRect(0, 0, GRADIENT_RADIUS * 2, GRADIENT_RADIUS * 2, mPaint);
canvasDest = new Canvas(bitmapDest);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int width = getMeasuredWidth();
setMeasuredDimension(width, width);
}
@Override
protected void onDraw(Canvas canvas){
/*uncomment each of them to show the effect, the first and the third one worked, the second show the same problem as OP's*/
//drawWithLayers(canvas); //unrecommended.
//drawWithBitmap(canvas); //this shows transparent as black
drawWithBitmapS(canvas); //recommended.
}
@SuppressLint("WrongCall")
private void drawWithLayers(Canvas canvas){
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
float width = canvas.getWidth();
float hWidth = width / 2;
//both saveLayerAlpha saveLayer worked here, and if without either of them,
//the transparent area will be black.
//int count = canvas.saveLayerAlpha(0, 0, getWidth(), getHeight(), 255, Canvas.ALL_SAVE_FLAG);
int count = canvas.saveLayer(0, 0, getWidth(), getHeight(), null, Canvas.ALL_SAVE_FLAG);
super.onDraw(canvas);
float scale = hWidth/GRADIENT_RADIUS;
matrix.setTranslate(hWidth - GRADIENT_RADIUS, hWidth - GRADIENT_RADIUS);
matrix.postScale(scale, scale, hWidth, hWidth);
gradientShader.setLocalMatrix(matrix);
canvas.drawRect(0, 0, width, width, mPaint);
canvas.restoreToCount(count);
}
@SuppressLint("WrongCall")
private void drawWithBitmap(Canvas canvas){
super.onDraw(canvas);
float scale = canvas.getWidth() / (GRADIENT_RADIUS * 2);
matrix.setScale(scale, scale);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvas.drawBitmap(bitmapGradient, matrix, mPaint); //transparent area is still black.
}
@SuppressLint("WrongCall")
private void drawWithBitmapS(Canvas canvas){
float scale = canvas.getWidth() / (GRADIENT_RADIUS * 2);
int count = canvasDest.save();
canvasDest.scale(1/scale, 1/scale); //tell super to draw in 1/scale.
super.onDraw(canvasDest);
canvasDest.restoreToCount(count);
mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
canvasDest.drawBitmap(bitmapGradient, 0, 0, mPaint);
matrix.setScale(scale, scale); //to scale bitmapDest to canvas.
canvas.drawBitmap(bitmapDest, matrix, null);
}
}
some notes:
1, this view extends ImageView
not View
, there are some differences.
2, why drawWithLayers
--saveLayer
or saveLayerAlpha
-- is unrecommended: a, they are uncertain, sometimes doesn't work right(show transparent as black), especially for View
whoes onDraw(Canvas)
is empty, while ImageView.onDraw(Canvas)
used a Drawable
to draw some; b, they are expensive, they allocate off-screen bitmap
to store temporary drawing result, and there is no clear clues of any resource recycling mechanism.
3, using your own bitmap[s], is better for customized resource recycling.
Some people said, it's impossible to used PorterDuff without allocation bitmap[s] every drawing, because the bitmap's width, height can't be determined before drawing/layout/measure.
you CAN use buffering bitmap[s] for PorterDuff drawing:
at first, allocate some big enough bitmap[s].
then, draw on the bitmap[s] with some matrix.
and the, draw the bitmap[s] into a dest bitmap with some matrix.
at last, draw the dest bitmap into canvas with some matrix.
some people recommend setLayerType(View.LAYER_TYPE_SOFTWARE, null), which is not an option for me, because it will cause onDraw(Canvas) to be called in a loop.
result image
source image