vxh.viet
5/27/2016 - 5:41 AM

How to use Mr PorterDuff to draw complex shape?

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