androidandroid-canvasdrawablexfermode

android : Xfermode masking disappearing on 180 degree rotation of Canvas


I am inspired by the the new Material Design animations and I worked to create a similar drawable that is used in new support v7 Action Bar Drawer Toggle.

I created a CustomDrawable. All I actually did is that I created a Play triangle on canvas and pause logo on the left of the left margin of the visible canvas. I rotate the canvas according to the progress and restore it. Then I used Xfermode to crop the rotated result into a circle.

I cant find the solution to the problem.

The problem is that the xFermode is not applied to the 180 degree rotated result(after calling canvas.restore()).

Here is the code of Activity.

public class MainActivity extends Activity{

ImageView iv;
CustomDrawable drawable = new CustomDrawable();

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity);

    iv = (ImageView) findViewById(R.id.button);
    iv.setLayerType(View.LAYER_TYPE_HARDWARE, null);
    iv.setBackgroundDrawable(drawable);

    iv.setOnClickListener(new OnClickListener() {

        @Override
        public void onClick(View v) {
            float[] values = { 0, 1 };
            if (drawable.getProgress() != 0) {
                values[0] = drawable.getProgress();
                values[1] = 0;
            }
            ObjectAnimator animator = ObjectAnimator.ofFloat(drawable,
                    "progress", values);
            animator.setDuration(2000);
            animator.start();

        }
    });
 }
}

And the code for the CustomDrawable

public class CustomDrawable extends Drawable {
    private float mProgress = 0;
    private Paint mPaint = new Paint();
    private Path mPath = new Path();
    private final float rootTwo = (float) Math.sqrt(2);
    private final float rootThree = (float) Math.sqrt(3);
    private float radius = 0;
    private float side = 0;
    private Point[] triangle = new Point[3];
    Paint xferpaint = new Paint();
    Canvas cropper;
    Bitmap bitmap;
    Interpolator interpolator = new AnticipateOvershootInterpolator();
    private float width;
    Rect rec1, rec2;

public CustomDrawable() {
    mPaint.setAntiAlias(true);
    mPaint.setStyle(Style.FILL);
    xferpaint.setColor(Color.RED);
    xferpaint.setStyle(Style.FILL);
    xferpaint.setAntiAlias(true);
}

@Override
public void draw(Canvas canvas) {
    canvas.getClipBounds(bound);
    boundsf.set(bound);
    if (radius == 0) {
        radius = Math.min(bound.centerX(), bound.centerY());
        radius -= 5;

        bitmap = Bitmap.createBitmap(bound.width(), bound.height(),
                Config.ARGB_8888);
        cropper = new Canvas(bitmap);
        cropper.drawCircle(bound.centerX(), bound.centerY(), radius,
                xferpaint);

        xferpaint.setXfermode(new PorterDuffXfermode(Mode.DST_IN));

        side = rootTwo * radius;

        triangle[0] = new Point(
                (int) (bound.centerX() + (side / rootThree)),
                bound.centerY());
        triangle[1] = new Point(bound.centerX()
                - (int) (side / (2 * rootThree)), bound.centerY()
                - (int) (side / 2));
        triangle[2] = new Point(bound.centerX()
                - (int) (side / (2 * rootThree)), bound.centerY()
                + (int) (side / 2));
        width = side / 4;
        rec1 = new Rect((int) (-bound.centerX() - (3 * width / 2)),
                (int) (bound.centerY() - (side / 2)),
                (int) (-bound.centerX() - (width / 2)),
                (int) (bound.centerY() + (side / 2)));
        rec2 = new Rect((int) (-bound.centerX() + (width / 2)),
                (int) (bound.centerY() - (side / 2)),
                (int) (-bound.centerX() + (3 * width / 2)),
                (int) (bound.centerY() + (side / 2)));
    }

    mPath.rewind();
    mPath.moveTo(triangle[0].x, triangle[0].y);
    mPath.lineTo(triangle[1].x, triangle[1].y);
    mPath.lineTo(triangle[2].x, triangle[2].y);
    mPath.close();
    mPaint.setColor(Color.parseColor("#378585"));
    canvas.drawPaint(mPaint);
    mPaint.setColor(Color.parseColor("#FF0400"));
    canvas.rotate(180 * interpolator.getInterpolation(mProgress), 0,
            bound.centerY());
    canvas.drawPath(mPath, mPaint);
    canvas.drawRect(rec1, mPaint);
    canvas.drawRect(rec2, mPaint);
    canvas.restore();
    canvas.drawBitmap(bitmap, 0, 0, xferpaint);
}

@Override
public int getOpacity() {
    return mPaint.getAlpha();
}

@Override
public void setAlpha(int alpha) {
    mPaint.setAlpha(alpha);
}

@Override
public void setColorFilter(ColorFilter filter) {
    mPaint.setColorFilter(filter);
}

public float getProgress() {
    return mProgress;
}

public void setProgress(float progress) {
    mProgress = progress;
    invalidateSelf();
}

}

Solution

  • this is your simplified Drawable:

    class CustomDrawable extends Drawable implements ValueAnimator.AnimatorUpdateListener {
        private float mProgress = 0;
        private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        private Path mPath;
        private Path mClipPath;
    
        public CustomDrawable() {
            mPaint.setStyle(Paint.Style.FILL);
            mPaint.setColor(0xffFF0400);
        }
    
        @Override
        protected void onBoundsChange(Rect bounds) {
            mClipPath = new Path();
            int cx = bounds.centerX();
            int cy = bounds.centerY();
            int radius = Math.min(cx, cy) - 5;
            mClipPath.addCircle(cx, cy, radius, Path.Direction.CCW);
    
            final float rootTwo = (float) Math.sqrt(2);
            final float rootThree = (float) Math.sqrt(3);
    
            float side = rootTwo * radius;
            mPath = new Path();
            mPath.moveTo(cx + (side / rootThree), cy);
            mPath.lineTo(cx - side / (2 * rootThree), cy - side / 2);
            mPath.lineTo(cx - side / (2 * rootThree), cy + side / 2);
            mPath.close();
    
            float width = side / 4;
            addRect(-cx - (3 * width / 2), cy - (side / 2), width, side);
            addRect(-cx + (width / 2), cy - (side / 2), width, side);
        }
    
        private void addRect(float l, float t, float dx, float dy) {
            mPath.addRect(l, t, l + dx, t + dy, Path.Direction.CCW);
        }
    
        @Override
        public void draw(Canvas canvas) {
            canvas.clipPath(mClipPath);
    
            canvas.drawColor(0xff378585);
            Rect bounds = getBounds();
            canvas.rotate(mProgress, 0, bounds.centerY());
            canvas.drawPath(mPath, mPaint);
        }
    
        public void switchIcons() {
            float[] values = { 0, 180 };
            if (mProgress != 0) {
                values[0] = mProgress;
                values[1] = 0;
            }
            ValueAnimator animator = ValueAnimator.ofFloat(values).setDuration(2000);
            animator.setInterpolator(new AnticipateOvershootInterpolator());
            animator.addUpdateListener(this);
            animator.start();
        }
    
        @Override
        public void onAnimationUpdate(ValueAnimator animation) {
            mProgress = (Float) animation.getAnimatedValue();
            invalidateSelf();
        }
    
        @Override
        public int getOpacity() {
            return mPaint.getAlpha();
        }
    
        @Override
        public void setAlpha(int alpha) {
            mPaint.setAlpha(alpha);
        }
    
        @Override
        public void setColorFilter(ColorFilter filter) {
            mPaint.setColorFilter(filter);
        }
    }
    

    EDIT: this is draw() method without Canvas.clipPath and without creating a "mask" Bitmap:

        @Override
        public void draw(Canvas canvas) {
            canvas.drawColor(0xff378585);
    
            canvas.save();
            canvas.rotate(mProgress, 0, cy);
            canvas.drawPath(mPath, mPaint);
            canvas.restore();
    
            canvas.saveLayer(null, mDstInPaint, 0);
            canvas.drawCircle(cx, cy, radius, mPaint);
            canvas.restore();
        }
    

    where: mDstInPaint is a Paint object with xfermode set in Drawable's constructor:

    mDstInPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));