How to use Mr PorterDuff to draw complex shape?
Source: Hasibuan, StackOverflow, StackOverflow
Question: How to use Mr PorterDuff to draw complex shape?
Answer:
The key is to use PorterDuffXfermode
and setLayerType(View.LAYER_TYPE_SOFTWARE, null)
. Look at the code example below for a nice hole in middle of a rectangle.
public class MyMenuView extends View {
private static final String TAG = "MyMenuView";
private Paint mRectPaint;
private Paint mClearCirclePaint;
private Paint mCircle;
private Bitmap mCircleBitmap;
private float mClearCircleRadius;
private Rect mTouchRegion;
public MyMenuView(Context context) {
super(context);
init();
}
public MyMenuView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyMenuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init(){
if (android.os.Build.VERSION.SDK_INT >= 11){
setLayerType(View.LAYER_TYPE_SOFTWARE, null); //without this our hole will be all black
}
mRectPaint = new Paint();
mRectPaint.setColor(Color.WHITE);
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setAntiAlias(true);
mClearCirclePaint = new Paint();
mClearCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));//this will make a nice hole in our rectangle
mClearCirclePaint.setAntiAlias(true);
mCircle = new Paint();
mCircle.setColor(Color.WHITE);
mCircle.setAntiAlias(true);
mCircle.setStyle(Paint.Style.STROKE);
mCircleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mycwac_cam2_ic_video_record);
//tell us when the View has finished initializing so we can get its height
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() { //this get called multiple time
if(mClearCircleRadius == 0){ //run the code below one time only
mClearCircleRadius = getHeight()/2;
mTouchRegion = new Rect(getWidth()/2 - (int)mClearCircleRadius, 0, getWidth()/2 + (int)mClearCircleRadius, getHeight());
mCircle.setStrokeWidth(mClearCircleRadius*0.1f);
getViewTreeObserver().removeOnGlobalLayoutListener(this); //remove the listener after fishing calculating
}
}
});
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if(event.getAction() == MotionEvent.ACTION_DOWN){
int x = (int)event.getX();
int y = (int)event.getY();
if(x >= mTouchRegion.left && x <= mTouchRegion.right && y >= mTouchRegion.top && y <= mTouchRegion.bottom ){
Log.d(TAG, "Correct");
}
}
return true;
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, getHeight()*0.23f, getWidth(), getHeight()-(getHeight()*0.23f), mRectPaint);
canvas.drawCircle(getWidth()/2, getHeight()/2, mClearCircleRadius, mClearCirclePaint);
canvas.drawCircle(getWidth()/2, getHeight()/2, mClearCircleRadius*0.9f, mCircle);
canvas.drawBitmap(mCircleBitmap, getWidth()/2 - (mCircleBitmap.getWidth()/2), getHeight()/2 - (mCircleBitmap.getHeight()/2), null);
}
}
For a complex version with added shadow and clicked animation. For the Bitmap's shadow see 3rd link.
public class MyMenuView extends View {
private static final float OFFSET_X = 7.0f; //to offset the shadow
private static final float OFFSET_Y = 7.0f;
private static final float RADIUS = 5.0f; //the radius of the shadow
private static final float RECT_SCALE_FACTOR = 0.23f; //to control the size of the rectangle, should be smaller than the view
private static final float CLEAR_CIRCLE_SCALE_FACTOR = 0.95f; // to control the size of the hole
private static final float CIRCLE_SCALE_FACTOR = 0.8f; //used for creating smaller circle when clicked
private static final int BITMAP_ALPHA = 125; //for used when clicked
private Paint mRectPaint;
private Paint mClearCirclePaint;
private Paint mCirclePaint;
private Bitmap mCircleBitmap;
private Bitmap mCircleBitmapShadow;
private Bitmap mCircleBitmapSmall;
private Paint mBitmapAlphaPaint;
private float mClearCircleRadius;
private Rect mTouchRegion;
private boolean isClicked;
public MyMenuView(Context context) {
super(context);
init();
}
public MyMenuView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MyMenuView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
if (android.os.Build.VERSION.SDK_INT >= 11) {
setLayerType(View.LAYER_TYPE_SOFTWARE, null);
}
mRectPaint = new Paint();
mRectPaint.setColor(Color.WHITE);
mRectPaint.setStyle(Paint.Style.FILL);
mRectPaint.setAntiAlias(true);
mRectPaint.setShadowLayer(RADIUS, OFFSET_X, OFFSET_Y, ContextCompat.getColor(getContext(), R.color.black_50));
mClearCirclePaint = new Paint();
mClearCirclePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));//this will make a nice hole in our rectangle
mClearCirclePaint.setAntiAlias(true);
mCirclePaint = new Paint();
mCirclePaint.setColor(Color.WHITE);
mCirclePaint.setAntiAlias(true);
mCirclePaint.setStyle(Paint.Style.STROKE);
mCirclePaint.setShadowLayer(RADIUS, OFFSET_X, OFFSET_Y, ContextCompat.getColor(getContext(), R.color.black_50));
mCircleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mycwac_cam2_ic_video_record);
mCircleBitmapSmall = Bitmap.createScaledBitmap(mCircleBitmap, (int)(mCircleBitmap.getWidth() * CIRCLE_SCALE_FACTOR), //used to simulate clicked animation
(int)(mCircleBitmap.getHeight() * CIRCLE_SCALE_FACTOR), false);
mCircleBitmapShadow = addShadow(mCircleBitmap, mCircleBitmap.getHeight(), mCircleBitmap.getWidth(), Color.BLACK, 3, 1, 3);
mCircleBitmap = mCircleBitmapShadow;
mBitmapAlphaPaint = new Paint();
mBitmapAlphaPaint.setAlpha(BITMAP_ALPHA);
//tell us when the View has finished initializing so we can get its height
getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() { //this get called multiple time
if (mClearCircleRadius == 0) { //run the code below one time only
mClearCircleRadius = getHeight() / 2 * CLEAR_CIRCLE_SCALE_FACTOR; //our hole should be relative to the View's height so it can sacle on diffrent screen
mTouchRegion = new Rect(getWidth() / 2 - (int) mClearCircleRadius, 0, getWidth() / 2 + (int) mClearCircleRadius, getHeight());
mCirclePaint.setStrokeWidth(mClearCircleRadius * 0.1f);
getViewTreeObserver().removeOnGlobalLayoutListener(this); //remove the listener after fishing calculating
}
}
});
setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(final View v, final MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN) {
if(isInsideTouchRegion(event.getX(), event.getY())){
//remove shadow
mRectPaint.setShadowLayer(0, 0, 0, 0);
mCirclePaint.setShadowLayer(0, 0, 0, 0);
mCircleBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.mycwac_cam2_ic_video_record);
isClicked = true;
invalidate();
}
}
if(event.getAction() == MotionEvent.ACTION_UP){
//add shadow back
mRectPaint.setShadowLayer(RADIUS, OFFSET_X, OFFSET_Y, ContextCompat.getColor(getContext(), R.color.black_50));
mCirclePaint.setShadowLayer(RADIUS, OFFSET_X, OFFSET_Y, ContextCompat.getColor(getContext(), R.color.black_50));
mCircleBitmap = mCircleBitmapShadow;
invalidate();
final int x = (int) event.getX(); //have to do this because after delay time, the touch event get lost, some random coordination is generated
final int y = (int) event.getY();
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
if(isInsideTouchRegion(x, y)){
((HomeActivity) getContext()).seeToolMenu();
}
}
}, ShuttaConstants.ANIMATION_DELAY_TIME);
isClicked = false;
}
return true;
}
});
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, getHeight() * RECT_SCALE_FACTOR, getWidth(), getHeight() - (getHeight() * RECT_SCALE_FACTOR), mRectPaint);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mClearCircleRadius, mClearCirclePaint);
if(!isClicked){
mCirclePaint.setColor(Color.WHITE);
canvas.drawCircle(getWidth() / 2, getHeight() / 2, mClearCircleRadius * 0.9f, mCirclePaint);
canvas.drawBitmap(mCircleBitmap, getWidth() / 2 - (mCircleBitmap.getWidth() / 2), getHeight() / 2 - (mCircleBitmap.getHeight() / 2), null);
}else{
mCirclePaint.setColor(ContextCompat.getColor(getContext(), R.color.white_50));
canvas.drawCircle(getWidth() / 2 , getHeight() / 2, mClearCircleRadius * CIRCLE_SCALE_FACTOR, mCirclePaint);
canvas.drawBitmap(mCircleBitmapSmall, getWidth() / 2 - (mCircleBitmapSmall.getWidth() / 2), getHeight() / 2 - (mCircleBitmapSmall.getHeight() / 2), mBitmapAlphaPaint);
}
}
public Bitmap addShadow(final Bitmap bm, final int dstHeight, final int dstWidth, int color, int size, float dx, float dy) {
final Bitmap mask = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ALPHA_8);
final Matrix scaleToFit = new Matrix();
final RectF src = new RectF(0, 0, bm.getWidth(), bm.getHeight());
final RectF dst = new RectF(0, 0, dstWidth - dx, dstHeight - dy);
scaleToFit.setRectToRect(src, dst, Matrix.ScaleToFit.CENTER);
final Matrix dropShadow = new Matrix(scaleToFit);
dropShadow.postTranslate(dx, dy);
final Canvas maskCanvas = new Canvas(mask);
final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
maskCanvas.drawBitmap(bm, scaleToFit, paint);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OUT));
maskCanvas.drawBitmap(bm, dropShadow, paint);
final BlurMaskFilter filter = new BlurMaskFilter(size, BlurMaskFilter.Blur.NORMAL);
paint.reset();
paint.setAntiAlias(true);
paint.setColor(color);
paint.setMaskFilter(filter);
paint.setFilterBitmap(true);
final Bitmap ret = Bitmap.createBitmap(dstWidth, dstHeight, Bitmap.Config.ARGB_8888);
final Canvas retCanvas = new Canvas(ret);
retCanvas.drawBitmap(mask, 0, 0, paint);
retCanvas.drawBitmap(bm, scaleToFit, null);
mask.recycle();
return ret;
}
private boolean isInsideTouchRegion(float eventX, float eventY){
int x = (int) eventX;
int y = (int) eventY;
if (x >= mTouchRegion.left && x <= mTouchRegion.right && y >= mTouchRegion.top && y <= mTouchRegion.bottom) {
return true;
}else{
return false;
}
}
}