package com.adamnickle.deck; import android.annotation.SuppressLint; import android.app.Activity; 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.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.RectF; import android.support.annotation.NonNull; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import java.util.Stack; public class ScratchPadView extends View { public static final int DEFAULT_PAINT_COLOR = Color.BLACK; private final int MAX_STROKE_SIZE; private final int MIN_STROKE_SIZE; private Bitmap mBaseBitmap; private Bitmap mCacheBitmap; private final Path mDrawPath; private Paint mCurrentPaint; private final Paint mDrawingPaint; private final Paint mErasingPaint; private final Paint mEraserPointPaint; private final Canvas mCacheCanvas; private RectF mPathBounds; private boolean mEraser; private boolean mDrawEraser; private int mX; private int mY; private final Stack<DrawingStep> mDrawingSteps; private final Stack<DrawingStep> mUndoneDrawingSteps; public ScratchPadView( Context context ) { this( context, null ); } public ScratchPadView( Context context, AttributeSet attrs ) { this( context, attrs, 0 ); } public ScratchPadView( Context context, AttributeSet attrs, int defStyleAttr ) { super( context, attrs, defStyleAttr ); mDrawPath = new Path(); MAX_STROKE_SIZE = getResources().getDimensionPixelSize( R.dimen.max_stroke_size ); MIN_STROKE_SIZE = getResources().getDimensionPixelSize( R.dimen.min_stroke_size ); mDrawingPaint = new Paint(); mDrawingPaint.setColor( DEFAULT_PAINT_COLOR ); mDrawingPaint.setAntiAlias( true ); mDrawingPaint.setStrokeWidth( getResources().getDimensionPixelSize( R.dimen.default_paint_stroke_size ) ); mDrawingPaint.setStyle( Paint.Style.STROKE ); mDrawingPaint.setStrokeJoin( Paint.Join.ROUND ); mDrawingPaint.setStrokeCap( Paint.Cap.ROUND ); mEraserPointPaint = new Paint( mDrawingPaint ); mEraserPointPaint.setStrokeWidth( getResources().getDimensionPixelSize( R.dimen.default_eraser_stroke_size ) ); mEraserPointPaint.setColor( getResources().getColor( R.color.PaleGreen ) ); mErasingPaint = new Paint( mDrawingPaint ); mErasingPaint.setStrokeWidth( getResources().getDimensionPixelSize( R.dimen.default_eraser_stroke_size ) ); mErasingPaint.setColor( Color.TRANSPARENT ); mErasingPaint.setXfermode( new PorterDuffXfermode( PorterDuff.Mode.CLEAR ) ); mCurrentPaint = mDrawingPaint; mPathBounds = new RectF(); mEraser = false; mX = 0; mY = 0; mDrawEraser = false; mDrawingSteps = new Stack< DrawingStep >(); mUndoneDrawingSteps = new Stack< DrawingStep >(); mCacheCanvas = new Canvas(); } public static class DrawingStep { private Paint Paint; Path Path; float X; float Y; private DrawingStep( Paint paint ) { this.Paint = new Paint( paint ); } public static DrawingStep create( Paint paint, Path path ) { DrawingStep drawingStep = new DrawingStep( paint ); drawingStep.Path = new Path( path ); drawingStep.X = 0.0f; drawingStep.Y = 0.0f; return drawingStep; } public static DrawingStep create( Paint paint, float x, float y ) { DrawingStep drawingStep = new DrawingStep( paint ); drawingStep.Path = null; drawingStep.X = x; drawingStep.Y = y; return drawingStep; } public void drawToCanvas( Canvas canvas ) { if( Path == null ) { canvas.drawPoint( X, Y, Paint ); } else { canvas.drawPath( Path, Paint ); } } } public boolean canUndo() { return !mDrawingSteps.empty(); } public void undo() { if( !mDrawingSteps.empty() ) { mUndoneDrawingSteps.push( mDrawingSteps.pop() ); invalidate(); if( getContext() instanceof Activity ) { ( (Activity) getContext() ).invalidateOptionsMenu(); } } } public boolean canRedo() { return !mUndoneDrawingSteps.empty(); } public void redo() { if( !mUndoneDrawingSteps.empty() ) { mDrawingSteps.push( mUndoneDrawingSteps.pop() ); invalidate(); if( getContext() instanceof Activity ) { ( (Activity) getContext() ).invalidateOptionsMenu(); } } } @SuppressLint( "DrawAllocation" ) @Override protected void onLayout( boolean changed, int left, int top, int right, int bottom ) { super.onLayout( changed, left, top, right, bottom ); if( changed ) { final int drawingViewWidth = right - left; final int drawingViewHeight = bottom - top; final int bitmapWidth = mBaseBitmap != null ? mBaseBitmap.getWidth() : 0; final int bitmapHeight = mBaseBitmap != null ? mBaseBitmap.getHeight() : 0; final int width = Math.max( drawingViewWidth, bitmapWidth ); final int height = Math.max( drawingViewHeight, bitmapHeight ); if( width > bitmapWidth || height > bitmapHeight ) { final Bitmap bitmap = Bitmap.createBitmap( width, height, Bitmap.Config.ARGB_8888 ); if( mBaseBitmap != null ) { final Canvas canvas = new Canvas( bitmap ); canvas.drawBitmap( mBaseBitmap, 0, 0, null ); mBaseBitmap.recycle(); } mBaseBitmap = bitmap; if( mCacheBitmap != null ) { mCacheBitmap.recycle(); } mCacheBitmap = Bitmap.createBitmap( mBaseBitmap.getWidth(), mBaseBitmap.getHeight(), Bitmap.Config.ARGB_8888 ); mCacheCanvas.setBitmap( mCacheBitmap ); invalidate(); } } } public void setScratchPadBitmap( Bitmap bitmap ) { mDrawingSteps.clear(); mUndoneDrawingSteps.clear(); if( mBaseBitmap != null ) { mBaseBitmap.recycle(); } mBaseBitmap = bitmap; if( mCacheBitmap != null && mCacheBitmap.getWidth() == mBaseBitmap.getWidth() && mCacheBitmap.getHeight() == mBaseBitmap.getHeight() ) { clearCacheBitmap(); } else { if( mCacheBitmap != null ) { mCacheBitmap.recycle(); } mCacheBitmap = Bitmap.createBitmap( mBaseBitmap.getWidth(), mBaseBitmap.getHeight(), Bitmap.Config.ARGB_8888 ); mCacheCanvas.setBitmap( mCacheBitmap ); } invalidate(); } public Bitmap getBitmap() { return mCacheBitmap; } public void toggleEraser() { mEraser = !mEraser; if( mEraser ) { mCurrentPaint = mErasingPaint; } else { mCurrentPaint = mDrawingPaint; } } public int getStrokeSize() { return (int) mCurrentPaint.getStrokeWidth(); } public void setStrokeSize( int strokeSize ) { if( strokeSize < MIN_STROKE_SIZE ) { strokeSize = MIN_STROKE_SIZE; } else if( strokeSize > MAX_STROKE_SIZE ) { strokeSize = MAX_STROKE_SIZE; } mCurrentPaint.setStrokeWidth( strokeSize ); if( isEraser() ) { mEraserPointPaint.setStrokeWidth( strokeSize ); } } public int getPaintColor() { return mDrawingPaint.getColor(); } public void setPaintColor( int paintColor ) { mDrawingPaint.setColor( paintColor ); } public boolean isEraser() { return mEraser; } private void clearCacheBitmap() { mCacheCanvas.drawColor( Color.TRANSPARENT, PorterDuff.Mode.CLEAR ); } public void clearDrawing() { mDrawingSteps.clear(); mUndoneDrawingSteps.clear(); clearCacheBitmap(); if( mBaseBitmap != null ) { mBaseBitmap.recycle(); mBaseBitmap = null; } invalidate(); if( getContext() instanceof Activity ) { ( (Activity) getContext() ).invalidateOptionsMenu(); } System.gc(); } @Override protected void onDraw( Canvas canvas ) { clearCacheBitmap(); if( mBaseBitmap != null ) { mCacheCanvas.drawBitmap( mBaseBitmap, 0, 0, null ); } for( DrawingStep drawingStep : mDrawingSteps ) { drawingStep.drawToCanvas( mCacheCanvas ); } mCacheCanvas.drawPath( mDrawPath, mCurrentPaint ); if( mDrawEraser ) { mCacheCanvas.drawPoint( mX, mY, mEraserPointPaint ); } canvas.drawBitmap( mCacheBitmap, 0, 0, null ); } @Override public boolean onTouchEvent( @NonNull MotionEvent event ) { mX = (int) event.getX(); mY = (int) event.getY(); switch( event.getActionMasked() ) { case MotionEvent.ACTION_DOWN: mDrawEraser = mEraser; mDrawPath.moveTo( mX, mY ); break; case MotionEvent.ACTION_MOVE: mDrawPath.lineTo( mX, mY ); break; case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: mDrawPath.computeBounds( mPathBounds, false ); if( mPathBounds.width() < 10 || mPathBounds.height() < 10 ) { mDrawingSteps.push( DrawingStep.create( mCurrentPaint, mPathBounds.left, mPathBounds.top ) ); } else { mDrawingSteps.push( DrawingStep.create( mCurrentPaint, mDrawPath ) ); } if( getContext() instanceof Activity ) { ( (Activity) getContext() ).invalidateOptionsMenu(); } mUndoneDrawingSteps.clear(); mDrawPath.reset(); mDrawEraser = false; break; } invalidate(); return true; } }