com.mylikes.likes.etchasketch.Slate.java Source code

Java tutorial

Introduction

Here is the source code for com.mylikes.likes.etchasketch.Slate.java

Source

/*
 * Copyright (C) 2012 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.mylikes.likes.etchasketch;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathMeasure;
import android.graphics.Point;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.os.Build;
import android.support.v4.view.MotionEventCompat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.EditText;

import java.util.ArrayList;

public class Slate extends View {

    static final boolean DEBUG = false;
    static final String TAG = "Slate";

    public static final boolean HWLAYER = true;
    public static final boolean SWLAYER = false;
    public static final boolean FANCY_INVALIDATES = false; // doesn't work
    public static final boolean INVALIDATE_ALL_THE_THINGS = true; // invalidate() every frame

    public static final int FLAG_DEBUG_STROKES = 1;
    public static final int FLAG_DEBUG_PRESSURE = 1 << 1;
    public static final int FLAG_DEBUG_INVALIDATES = 1 << 2;
    public static final int FLAG_DEBUG_TILES = 1 << 3;
    public static final int FLAG_DEBUG_EVERYTHING = ~0;

    public static final int MAX_POINTERS = 10;

    static final int DENSITY = 2;

    private static final int SMOOTHING_FILTER_WLEN = 6;
    private static final float SMOOTHING_FILTER_POS_DECAY = 0.65f;
    private static final float SMOOTHING_FILTER_PRESSURE_DECAY = 0.9f;

    private static final int FIXED_DIMENSION = 0; // 1024;

    private static final float INVALIDATE_PADDING = 4.0f;
    public static final boolean ASSUME_STYLUS_CALIBRATED = true;

    // keep these in sync with penType in values/attrs.xml
    public static final int TYPE_WHITEBOARD = 0;
    public static final int TYPE_FELTTIP = 1;
    public static final int TYPE_AIRBRUSH = 2;
    public static final int TYPE_FOUNTAIN_PEN = 3;
    public static final int TYPE_ERASER = 4;

    public static final int SHAPE_CIRCLE = 0;
    public static final int SHAPE_SQUARE = 1;
    //    public static final int SHAPE_BITMAP_CIRCLE = 2;
    public static final int SHAPE_BITMAP_AIRBRUSH = 3;
    public static final int SHAPE_FOUNTAIN_PEN = 4;

    private float mPressureExponent = 2.0f;

    private float mRadiusMin;
    private float mRadiusMax;

    int mDebugFlags = 0;

    private TiledBitmapCanvas mTiledCanvas;
    private final Paint mDebugPaints[] = new Paint[10];

    private Bitmap mPendingPaintBitmap;

    //    private Bitmap mCircleBits;
    //    private Rect mCircleBitsFrame;
    private Bitmap mAirbrushBits;
    private Rect mAirbrushBitsFrame;
    private Bitmap mFountainPenBits;
    private Rect mFountainPenBitsFrame;

    private PressureCooker mPressureCooker;

    private boolean mZoomMode;

    private boolean mEmpty;

    private Region mDirtyRegion = new Region();

    private Paint mBlitPaint;
    private Paint mWorkspacePaint;
    private Matrix mZoomMatrix = new Matrix();
    private Matrix mZoomMatrixInv = new Matrix();
    private float mPanX = 0f, mPanY = 0f;
    private int mMemClass;
    private boolean mLowMem;

    private ArrayList<MoveableDrawing> overlays = new ArrayList<MoveableDrawing>();
    private boolean moveMode = false;
    private boolean inStickerChooser = true;
    private int moveDrawingIndex;
    private float moveDrawingStartX, moveDrawingStartY;
    private long touchStartTime;
    private int resizeDrawingIndex;
    private String resizeDrawingCorner;
    private MoveableDrawing selectedDrawing = null;
    private int currentColor;
    private PointF firstFinger;
    private PointF secondFinger;

    public interface SlateListener {
        void strokeStarted();

        void strokeEnded();
    }

    private class MarkersPlotter implements SpotFilter.Plotter {
        // Plotter receives pointer coordinates and draws them.
        // It implements the necessary interface to receive filtered Spots from the SpotFilter.
        // It hands off the drawing command to the renderer.

        private SpotFilter mCoordBuffer;
        private SmoothStroker mRenderer;

        private float mLastPressure = -1f;
        private int mLastTool = 0;
        final float[] mTmpPoint = new float[2];

        public MarkersPlotter() {
            mCoordBuffer = new SpotFilter(SMOOTHING_FILTER_WLEN, SMOOTHING_FILTER_POS_DECAY,
                    SMOOTHING_FILTER_PRESSURE_DECAY, this);
            mRenderer = new SmoothStroker();
        }

        //        final Rect tmpDirtyRect = new Rect();
        @Override
        public void plot(Spot s) {
            final float pressureNorm;

            if (ASSUME_STYLUS_CALIBRATED && s.tool == MotionEvent.TOOL_TYPE_STYLUS) {
                pressureNorm = s.pressure;
            } else {
                pressureNorm = mPressureCooker.getAdjustedPressure(s.pressure);
            }

            final float radius = lerp(mRadiusMin, mRadiusMax, (float) Math.pow(pressureNorm, mPressureExponent));

            mTmpPoint[0] = s.x - mPanX;
            mTmpPoint[1] = s.y - mPanY;
            mZoomMatrixInv.mapPoints(mTmpPoint);

            final RectF dirtyF = mRenderer.strokeTo(mTiledCanvas, mTmpPoint[0], mTmpPoint[1], radius);
            dirty(dirtyF);
            //            dirtyF.roundOut(tmpDirtyRect);
            //            tmpDirtyRect.inset((int)-INVALIDATE_PADDING,(int)-INVALIDATE_PADDING);
            //            invalidate(tmpDirtyRect);
        }

        public void setPenColor(int color) {
            mRenderer.setPenColor(color);
        }

        public void finish(long time) {
            mLastPressure = -1f;
            mCoordBuffer.finish();
            mRenderer.reset();
        }

        //        public void addCoords(MotionEvent.PointerCoords pt, long time) {
        //            mCoordBuffer.add(pt, time);
        //            mLastPressure = pt.pressure;
        //        }

        public void add(Spot s) {
            mCoordBuffer.add(s);
            mLastPressure = s.pressure;
            mLastTool = s.tool;
        }

        //        public float getRadius() {
        //            return mRenderer.getRadius();
        //        }

        public float getLastPressure() {
            return mLastPressure;
        }

        public int getLastTool() {
            return mLastTool;
        }

        public void setPenType(int shape) {
            mRenderer.setPenType(shape);
        }

        public int getPenType() {
            return mRenderer.getPenType();
        }
    }

    private class SmoothStroker {
        // The renderer. Given a stream of filtered points, converts it into draw calls.

        private float mLastX = 0, mLastY = 0, mLastLen = 0, mLastR = -1;
        private float mTan[] = new float[2];

        private int mPenColor;
        private int mPenType;

        private int mShape = SHAPE_CIRCLE; // SHAPE_BITMAP_AIRBRUSH;

        private Path mWorkPath = new Path();
        private PathMeasure mWorkPathMeasure = new PathMeasure();

        private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

        int mInkDensity = 0xff; // set to 0x20 or so for a felt-tip look, 0xff for traditional Markers

        public SmoothStroker() {
        }

        public void setPenColor(int color) {
            if (mPaint.getColor() == Color.TRANSPARENT && mPenType == TYPE_ERASER) {
                return;
            }
            if (color == Color.TRANSPARENT) {
                mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
                mPaint.setColor(color);
                return;//don't set mPenColor
            } else if (color == 0) {
                // eraser: DST_OUT
                mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
                mPaint.setColor(Color.BLACK);
            } else {
                mPaint.setXfermode(null);

                //mPaint.setColor(color); 
                mPaint.setColor(Color.BLACK); // or collor? or color & (mInkDensity << 24)?
                mPaint.setAlpha(mInkDensity);

                //                mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN));
                mPaint.setColorFilter(new PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP));
            }
            mPenColor = color;
        }

        public int getPenColor() {
            return mPenColor;
        }

        public void setPenType(int type) {
            mPenType = type;
            switch (type) {
            case TYPE_WHITEBOARD:
                mShape = SHAPE_CIRCLE;
                mInkDensity = 0xff;
                break;
            case TYPE_FELTTIP:
                mShape = SHAPE_CIRCLE;
                mInkDensity = 0x10;
                break;
            case TYPE_AIRBRUSH:
                mShape = SHAPE_BITMAP_AIRBRUSH;
                mInkDensity = 0x80;
                break;
            case TYPE_FOUNTAIN_PEN:
                mShape = SHAPE_FOUNTAIN_PEN;
                mInkDensity = 0xff;
                break;
            case TYPE_ERASER:
                mShape = SHAPE_CIRCLE;
                mInkDensity = 0xff;

            }
            if (type == TYPE_ERASER) {
                setPenColor(Color.TRANSPARENT);
            } else {
                setPenColor(mPenColor);
            }
        }

        public int getPenType() {
            return mPenType;
        }

        public void setDebugMode(boolean debug) {
        }

        public void reset() {
            mLastX = mLastY = mTan[0] = mTan[1] = 0;
            mLastR = -1;
        }

        final float dist(float x1, float y1, float x2, float y2) {
            x2 -= x1;
            y2 -= y1;
            return (float) Math.sqrt(x2 * x2 + y2 * y2);
        }

        private final RectF tmpRF = new RectF();

        final void drawStrokePoint(CanvasLite c, float x, float y, float r, RectF dirty) {
            switch (mShape) {
            case SHAPE_SQUARE:
                c.drawRect(x - r, y - r, x + r, y + r, mPaint);
                break;
            //            case SHAPE_BITMAP_CIRCLE:
            //                tmpRF.set(x-r,y-r,x+r,y+r);
            //                if (mCircleBits == null || mCircleBitsFrame == null) {
            //                    throw new RuntimeException("Slate.drawStrokePoint: no circle bitmap - frame=" + mCircleBitsFrame);
            //                }
            //                c.drawBitmap(mCircleBits, mCircleBitsFrame, tmpRF, mPaint);
            //                break;
            case SHAPE_BITMAP_AIRBRUSH:
                tmpRF.set(x - r, y - r, x + r, y + r);
                if (mAirbrushBits == null || mAirbrushBitsFrame == null) {
                    throw new RuntimeException(
                            "Slate.drawStrokePoint: no airbrush bitmap - frame=" + mAirbrushBitsFrame);
                }
                c.drawBitmap(mAirbrushBits, mAirbrushBitsFrame, tmpRF, mPaint);
                break;
            case SHAPE_FOUNTAIN_PEN:
                tmpRF.set(x - r, y - r, x + r, y + r);
                if (mFountainPenBits == null || mFountainPenBitsFrame == null) {
                    throw new RuntimeException(
                            "Slate.drawStrokePoint: no fountainpen bitmap - frame=" + mFountainPenBitsFrame);
                }
                c.drawBitmap(mFountainPenBits, mFountainPenBitsFrame, tmpRF, mPaint);
                break;
            case SHAPE_CIRCLE:
            default:
                c.drawCircle(x, y, r, mPaint);
                break;
            }
            dirty.union(x - r, y - r, x + r, y + r);
        }

        private final RectF tmpDirtyRectF = new RectF();

        public RectF strokeTo(CanvasLite c, float x, float y, float r) {
            final RectF dirty = tmpDirtyRectF;
            dirty.setEmpty();

            if (mLastR < 0) {
                // always draw the first point
                drawStrokePoint(c, x, y, r, dirty);
            } else {
                // connect the dots, la-la-la

                mLastLen = dist(mLastX, mLastY, x, y);
                float xi, yi, ri, frac;
                float d = 0;
                while (true) {
                    if (d > mLastLen) {
                        break;
                    }
                    frac = d == 0 ? 0 : (d / mLastLen);
                    ri = lerp(mLastR, r, frac);
                    xi = lerp(mLastX, x, frac);
                    yi = lerp(mLastY, y, frac);
                    drawStrokePoint(c, xi, yi, ri, dirty);

                    // for very narrow lines we must step (not much more than) one radius at a time
                    final float MIN = 1f;
                    final float THRESH = 16f;
                    final float SLOPE = 0.1f; // asymptote: the spacing will increase as SLOPE*x
                    if (ri <= THRESH) {
                        d += MIN;
                    } else {
                        d += Math.sqrt(SLOPE * Math.pow(ri - THRESH, 2) + MIN);
                    }
                }

                /* 
                // for curved paths
                Path p = mWorkPath;
                p.reset();
                p.moveTo(mLastX, mLastY);
                p.lineTo(x, y);
                    
                PathMeasure pm = mWorkPathMeasure;
                pm.setPath(p, false);
                mLastLen = pm.getLength();
                float d = 0;
                float posOut[] = new float[2];
                float ri;
                while (true) {
                if (d > mLastLen) {
                    d = mLastLen;
                }
                pm.getPosTan(d, posOut, mTan);
                // denormalize
                mTan[0] *= mLastLen; mTan[1] *= mLastLen;
                    
                ri = lerp(mLastR, r, d / mLastLen);
                c.drawCircle(posOut[0], posOut[1], ri, mPaint);
                dirty.union(posOut[0] - ri, posOut[1] - ri, posOut[0] + ri, posOut[1] + ri);
                    
                if (d == mLastLen) break;
                d += Math.min(ri, WALK_STEP_PX); // for very narrow lines we must step one radius at a time
                }
                */
            }

            mLastX = x;
            mLastY = y;
            mLastR = r;

            return dirty;
        }

        public float getRadius() {
            return mLastR;
        }
    }

    private MarkersPlotter[] mStrokes;

    Spot mTmpSpot = new Spot();

    private static Paint sBitmapPaint = new Paint(
            Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);

    public Slate(Context c, AttributeSet as) {
        super(c, as);
        init();
    }

    public Slate(Context c) {
        super(c);
        init();
    }

    @SuppressLint("NewApi")
    private void init() {
        //        setWillNotCacheDrawing(true);
        //        setDrawingCacheEnabled(false);

        mEmpty = true;

        // setup brush bitmaps
        final ActivityManager am = (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            mMemClass = am.getLargeMemoryClass();
        } else {
            mMemClass = am.getMemoryClass();
        }
        mLowMem = (mMemClass <= 16);
        if (true || DEBUG) {
            Log.v(TAG, "Slate.init: memClass=" + mMemClass + (mLowMem ? " (LOW)" : ""));
        }

        final Resources res = getContext().getResources();

        //        mCircleBits = BitmapFactory.decodeResource(res, R.drawable.circle_1bpp);
        //        if (mCircleBits == null) { Log.e(TAG, "SmoothStroker: Couldn't load circle bitmap"); }
        //        mCircleBitsFrame = new Rect(0, 0, mCircleBits.getWidth(), mCircleBits.getHeight());

        BitmapFactory.Options opts = new BitmapFactory.Options();
        opts.inPreferredConfig = Bitmap.Config.ALPHA_8;
        if (mLowMem) { // let's see how this works in practice
            opts.inSampleSize = 4;
        }
        mAirbrushBits = BitmapFactory.decodeResource(res, R.drawable.airbrush_light, opts);
        if (mAirbrushBits == null) {
            Log.e(TAG, "SmoothStroker: Couldn't load airbrush bitmap");
        }
        mAirbrushBitsFrame = new Rect(0, 0, mAirbrushBits.getWidth(), mAirbrushBits.getHeight());
        //Log.v(TAG, "airbrush: " + mAirbrushBitsFrame.right + "x" + mAirbrushBitsFrame.bottom);
        mFountainPenBits = BitmapFactory.decodeResource(res, R.drawable.fountainpen, opts);
        if (mFountainPenBits == null) {
            Log.e(TAG, "SmoothStroker: Couldn't load fountainpen bitmap");
        }
        mFountainPenBitsFrame = new Rect(0, 0, mFountainPenBits.getWidth(), mFountainPenBits.getHeight());

        // set up individual strokers for each pointer
        mStrokes = new MarkersPlotter[MAX_POINTERS]; // TODO: don't bother unless hasSystemFeature(MULTITOUCH_DISTINCT)
        for (int i = 0; i < mStrokes.length; i++) {
            mStrokes[i] = new MarkersPlotter();
        }

        mPressureCooker = new PressureCooker(getContext());

        setFocusable(true);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
            if (HWLAYER) {
                setLayerType(View.LAYER_TYPE_HARDWARE, null);
            } else if (SWLAYER) {
                setLayerType(View.LAYER_TYPE_SOFTWARE, null);
            } else {
                setLayerType(View.LAYER_TYPE_NONE, null);
            }
        }

        mWorkspacePaint = new Paint();
        mWorkspacePaint.setColor(0x40606060);

        mBlitPaint = new Paint();

        if (true) {
            mDebugPaints[0] = new Paint();
            mDebugPaints[0].setStyle(Paint.Style.STROKE);
            mDebugPaints[0].setStrokeWidth(2.0f);
            mDebugPaints[0].setARGB(255, 0, 255, 255);
            mDebugPaints[1] = new Paint(mDebugPaints[0]);
            mDebugPaints[1].setARGB(255, 255, 0, 128);
            mDebugPaints[2] = new Paint(mDebugPaints[0]);
            mDebugPaints[2].setARGB(255, 0, 255, 0);
            mDebugPaints[3] = new Paint(mDebugPaints[0]);
            mDebugPaints[3].setARGB(255, 30, 30, 255);
            mDebugPaints[4] = new Paint();
            mDebugPaints[4].setStyle(Paint.Style.FILL);
            mDebugPaints[4].setARGB(255, 128, 128, 128);
        }
    }

    public boolean isEmpty() {
        return mEmpty;
    }

    public void resetZoom() {
        mPanX = mPanY = 0;
        final Matrix m = new Matrix();
        m.postScale(1f / DENSITY, 1f / DENSITY);
        setZoom(m);
        invalidate();
    }

    public void setZoomPosNoInval(float x, float y) {
        mPanX = x;
        mPanY = y;
    }

    public void setZoomPos(float x, float y) {
        setZoomPosNoInval(x, y);
        invalidate();
    }

    public void setZoomPosNoInval(float[] pos) {
        setZoomPosNoInval(pos[0], pos[1]);
    }

    public void setZoomPos(float[] pos) {
        setZoomPosNoInval(pos);
        invalidate();
    }

    public float[] getZoomPos(float[] pos) {
        if (pos == null)
            pos = new float[2];
        pos[0] = mPanX;
        pos[1] = mPanY;
        return pos;
    }

    public float getZoomPosX() {
        return mPanX;
    }

    public float getZoomPosY() {
        return mPanY;
    }

    public Matrix getZoom() {
        return mZoomMatrix;
    }

    public Matrix getZoomInv() {
        return mZoomMatrixInv;
    }

    public void setZoom(Matrix m) {
        mZoomMatrix.set(m);
        mZoomMatrix.invert(mZoomMatrixInv);
    }

    public void setPenSize(float min, float max) {
        mRadiusMin = min * 0.5f;
        mRadiusMax = max * 0.5f;
    }

    public void recycle() {
        // WARNING: the slate will not be usable until you call load() or clear() or something
        if (mTiledCanvas != null) {
            mTiledCanvas.recycleBitmaps();
            mTiledCanvas = null;
        }
    }

    public void clear() {
        if (mTiledCanvas != null) {
            commitStroke();
            mTiledCanvas.drawColor(0x00000000, PorterDuff.Mode.SRC);
            invalidate();
        } else if (mPendingPaintBitmap != null) { // FIXME for tiling
            mPendingPaintBitmap.recycle();
            mPendingPaintBitmap = null;
        }
        mEmpty = true;

        // reset the zoom when clearing
        resetZoom();
    }

    public int getDebugFlags() {
        return mDebugFlags;
    }

    public void setDebugFlags(int f) {
        if (f != mDebugFlags) {
            mDebugFlags = f;
            mTiledCanvas.setDebug(0 != (f & FLAG_DEBUG_TILES));
            invalidate();
        }
    }

    private Bitmap mStrokeDebugGraph;
    private int mGraphX = 0;
    private Paint mGraphPaint1;
    private int mBackgroundColor = Color.TRANSPARENT;

    private void drawStrokeDebugInfo(Canvas c) {
        final int ROW_HEIGHT = 24;
        final int ROW_MARGIN = 6;
        final int COLUMN_WIDTH = 55;

        final float FIRM_PRESSURE_LOW = 0.85f;
        final float FIRM_PRESSURE_HIGH = 1.25f;

        if (mStrokeDebugGraph == null) {
            final int width = c.getWidth() - 128;
            final int height = ROW_HEIGHT * mStrokes.length + 2 * ROW_MARGIN;
            mStrokeDebugGraph = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
            if (mStrokeDebugGraph == null) {
                throw new RuntimeException(
                        "drawStrokeDebugInfo: couldn't create debug bitmap (" + width + "x" + height + ")");
            }
            mGraphPaint1 = new Paint(Paint.ANTI_ALIAS_FLAG);
        }

        Canvas graph = new Canvas(mStrokeDebugGraph);
        graph.save();
        graph.clipRect(new Rect(0, 0, COLUMN_WIDTH, graph.getHeight()));
        graph.drawColor(0, PorterDuff.Mode.CLEAR);
        graph.restore();

        int left = 4;
        int bottom = graph.getHeight() - ROW_MARGIN;
        final int STEP = 4;
        for (MarkersPlotter st : mStrokes) {
            float r = st.getLastPressure();

            if (r >= FIRM_PRESSURE_LOW && r <= FIRM_PRESSURE_HIGH)
                mGraphPaint1.setColor(0xFF33FF33);
            else if (r < FIRM_PRESSURE_LOW)
                mGraphPaint1.setColor(0xFF808080);
            else
                mGraphPaint1.setColor(0xFFFF8000);

            String s = (r < 0) ? "--"
                    : String.format("%s %.4f", ((st.getLastTool() == MotionEvent.TOOL_TYPE_STYLUS) ? "S" : "F"), r);

            graph.drawText(s, left, bottom - 2, mGraphPaint1);

            if (mGraphX + COLUMN_WIDTH > graph.getWidth()) {
                mGraphX = 0;
                graph.save();
                graph.clipRect(new Rect(30, 0, graph.getWidth(), graph.getHeight()));
                graph.drawColor(0, PorterDuff.Mode.CLEAR);
                graph.restore();
            }

            if (r >= 0) {
                int barsize = (int) (r * ROW_HEIGHT);
                graph.drawRect(mGraphX + COLUMN_WIDTH, bottom - barsize, mGraphX + COLUMN_WIDTH + STEP, bottom,
                        mGraphPaint1);
            } else {
                graph.drawPoint(mGraphX + COLUMN_WIDTH + STEP, bottom, mGraphPaint1);
            }
            bottom -= (ROW_HEIGHT + ROW_MARGIN);
        }

        mGraphX += STEP;

        final int x = 96;
        final int y = 64;

        c.drawBitmap(mStrokeDebugGraph, x, y, null);
        invalidate(new Rect(x, y, x + c.getWidth(), y + c.getHeight()));
    }

    public void commitStroke() {
        if (mTiledCanvas == null) {
            final Throwable e = new Throwable();
            e.fillInStackTrace();
            Log.v(TAG, "commitStroke before mTiledCanvas inited", e);
            return;
        }
        mTiledCanvas.commit();
    }

    public void undo() {
        if (mTiledCanvas == null) {
            Log.v(TAG, "undo before mTiledCanvas inited");
        }
        mTiledCanvas.step(-1);

        invalidate();
    }

    public void addSticker(StickerDrawing stick) {
        overlays.add(stick);
        selectedDrawing = stick;
        invalidate();
    }

    public void addText(int x, int y, String text) {
        TextDrawing drawing = new TextDrawing(getContext(), text, x, y);
        drawing.setColor(currentColor);
        overlays.add(drawing);
        selectedDrawing = drawing;
        invalidate();
        // TODO: add to undo stack
    }

    public void renderDrawing() {
        if (selectedDrawing != null) {
            Bitmap bm = Bitmap.createBitmap((int) (getWidth() * getDrawingDensity()),
                    (int) (getHeight() * getDrawingDensity()), Bitmap.Config.ARGB_8888);
            Canvas canvas = new Canvas(bm);
            selectedDrawing.renderInto(canvas, false);
            paintBitmap(bm);
            removeMoveable();
        }
    }

    public void promptForText(final int x, final int y) {
        promptForText(x, y, null);
    }

    public void promptForText(TextDrawing drawing) {
        promptForText(0, 0, drawing);
    }

    public void promptForText(final int x, final int y, final TextDrawing drawing) {
        LayoutInflater li = LayoutInflater.from(getContext());
        View promptsView = li.inflate(R.layout.prompts, null);
        AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(getContext());

        // set prompts.xml to alertdialog builder
        alertDialogBuilder.setView(promptsView);

        final EditText userInput = (EditText) promptsView.findViewById(R.id.editTextDialogUserInput);
        if (drawing != null) {
            String text = drawing.getText();
            userInput.setText(text);
            userInput.setSelection(text.length());
        }

        // TODO: add font dropdown to dialog
        alertDialogBuilder.setCancelable(false).setPositiveButton("OK", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                if (drawing != null) {
                    drawing.setText(userInput.getText().toString());
                    invalidate();
                } else {
                    addText(x, y, userInput.getText().toString());
                }
            }
        }).setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
            public void onClick(DialogInterface dialog, int id) {
                dialog.cancel();
            }
        });

        alertDialogBuilder.create().show();
    }

    public void maybeRemoveDrawing(final MoveableDrawing drawing) {
        overlays.remove(drawing);
        // TODO: add to undo stack
        if (selectedDrawing == drawing)
            selectedDrawing = null;
        invalidate();
    }

    public void addSticker(int x, int y, String path) {
        // TODO: create StickerDrawing class
    }

    public void paintBitmap(Bitmap b) {
        if (mTiledCanvas == null) {
            mPendingPaintBitmap = b;
            return;
        }

        commitStroke();

        Matrix m = new Matrix();
        RectF s = new RectF(0, 0, b.getWidth(), b.getHeight());
        RectF d = new RectF(0, 0, mTiledCanvas.getWidth(), mTiledCanvas.getHeight());
        m.setRectToRect(s, d, Matrix.ScaleToFit.CENTER);

        if (DEBUG) {
            Log.v(TAG, "paintBitmap: drawing new bits into current canvas");
        }
        mTiledCanvas.drawBitmap(b, m, sBitmapPaint);
        invalidate();

        if (DEBUG)
            Log.d(TAG, String.format("paintBitmap(%s, %dx%d): canvas=%s", b.toString(), b.getWidth(), b.getHeight(),
                    mTiledCanvas.toString()));
    }

    public void setDrawingBackground(int color) {
        mBackgroundColor = color;
        setBackgroundColor(color);
        invalidate();
    }

    public Bitmap getBitmap() {
        if (mTiledCanvas != null) {
            commitStroke();
            Bitmap bitmap = mTiledCanvas.toBitmap();
            Canvas canvas = new Canvas(bitmap);
            for (MoveableDrawing drawing : overlays) {
                drawing.renderInto(canvas, false);
            }
            return bitmap;
        }
        return null;
    }

    public Bitmap copyBitmap(boolean withBackground) {
        Bitmap newb = null;
        Bitmap b = getBitmap();
        if (b != null) {
            newb = Bitmap.createBitmap(b.getWidth(), b.getHeight(), b.getConfig());
        }
        if (newb != null) {
            Canvas newc = new Canvas(newb);
            if (mBackgroundColor != Color.TRANSPARENT && withBackground) {
                newc.drawColor(mBackgroundColor);
            }
            newc.drawBitmap(b, 0, 0, null);
        }
        return newb;
    }

    public void setPenColor(int color) {
        for (MarkersPlotter plotter : mStrokes) {
            // XXX: todo: only do this if the stroke hasn't begun already
            // ...or not; the current behavior allows RAINBOW MODE!!!1!
            plotter.setPenColor(color);
        }
        Log.d(TAG, "Set pen to : " + Integer.toHexString(color));
        if (moveMode && selectedDrawing != null) {
            selectedDrawing.setColor(color);
            invalidate();
        }
        currentColor = color;
    }

    public void setPenType(int shape) {
        for (MarkersPlotter plotter : mStrokes) {
            plotter.setPenType(shape);
        }
    }

    public int getPenType() {
        for (MarkersPlotter plotter : mStrokes) {
            return plotter.getPenType();
        }
        return -1;
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if (mTiledCanvas != null)
            return;

        final int widthPx = DENSITY * w;
        final int heightPx = DENSITY * h;
        final int bytesPerCanvas = widthPx * heightPx * 4;
        int numVersions = TiledBitmapCanvas.DEFAULT_NUM_VERSIONS;
        final int memCeiling = (mMemClass * 1024 * 1024);
        if (bytesPerCanvas * (numVersions + 2) > memCeiling) {
            numVersions = memCeiling / bytesPerCanvas - 2;
        }
        if (numVersions < 1) { // uh get some RAM already
            numVersions = 1;
        }

        Log.v(TAG, String.format("About to init tiled %dx canvas: %dx%d x 32bpp x %d = %d bytes (ceiling: %d)",
                DENSITY, widthPx, heightPx, numVersions, widthPx * heightPx * 4 * numVersions, memCeiling));
        mTiledCanvas = new TiledBitmapCanvas(widthPx, heightPx, Bitmap.Config.ARGB_8888,
                TiledBitmapCanvas.DEFAULT_TILE_SIZE, numVersions);
        if (mTiledCanvas == null) {
            throw new RuntimeException("onSizeChanged: Unable to allocate main buffer (" + w + "x" + h + ")");
        }

        final Bitmap b = mPendingPaintBitmap;
        if (b != null) {
            mPendingPaintBitmap = null;
            paintBitmap(b);
        }

        resetZoom();
    }

    @Override
    protected void onDraw(Canvas canvas) {
        if (mTiledCanvas != null) {
            canvas.save(Canvas.MATRIX_SAVE_FLAG);

            if (mPanX != 0 || mPanY != 0 || !mZoomMatrix.isIdentity()) {
                canvas.translate(mPanX, mPanY);
                canvas.concat(mZoomMatrix);

                canvas.drawRect(-20000, -20000, 20000, 0, mWorkspacePaint);
                canvas.drawRect(-20000, 0, 0, mTiledCanvas.getHeight(), mWorkspacePaint);
                canvas.drawRect(mTiledCanvas.getWidth(), 0, 20000, mTiledCanvas.getHeight(), mWorkspacePaint);
                canvas.drawRect(-20000, mTiledCanvas.getHeight(), 20000, 20000, mWorkspacePaint);
            }

            if (!mDirtyRegion.isEmpty()) {
                canvas.clipRegion(mDirtyRegion);
                mDirtyRegion.setEmpty();
            }
            // TODO: tune this threshold based on the device density
            mBlitPaint.setFilterBitmap(getScale(mZoomMatrix) < 3f);
            mTiledCanvas.drawTo(canvas, 0, 0, mBlitPaint, false); // @@ set to true for dirty tile updates
            if (0 != (mDebugFlags & FLAG_DEBUG_STROKES)) {
                drawStrokeDebugInfo(canvas);
            }
            for (MoveableDrawing drawing : overlays) {
                drawing.renderInto(canvas, moveMode && drawing == selectedDrawing);
            }

            canvas.restore();

            if (0 != (mDebugFlags & FLAG_DEBUG_PRESSURE)) {
                mPressureCooker.drawDebug(canvas);
            }
        }
    }

    private static final float[] mvals = new float[9];

    public static float getScale(Matrix m) {
        m.getValues(mvals);
        return mvals[0];
    }

    float dbgX = -1, dbgY = -1;
    RectF dbgRect = new RectF();

    final static boolean hasPointerCoords() {
        return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR_MR1);
    }

    final static boolean hasToolType() {
        return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH);
    }

    @SuppressLint("NewApi")
    final static int getToolTypeCompat(MotionEvent me, int index) {
        if (hasToolType()) {
            return me.getToolType(index);
        }

        // dirty hack for the HTC Flyer
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) {
            if ("flyer".equals(Build.HARDWARE)) {
                if (me.getSize(index) <= 0.1f) {
                    // with very high probability this is the stylus
                    return MotionEvent.TOOL_TYPE_STYLUS;
                }
            }
        }

        return MotionEvent.TOOL_TYPE_FINGER;
    }

    PointF getCenter(MotionEvent event, PointF out) {
        int P = event.getPointerCount();
        PointF pt = ((out == null) ? new PointF() : out);
        pt.set(event.getX(0), event.getY(0));
        final int zero[] = { 0, 0 };
        getLocationOnScreen(zero);
        for (int j = 1; j < P; j++) {
            pt.x += event.getX(j) + zero[0];
            pt.y += event.getY(j) + zero[1];
        }
        pt.x /= P;
        pt.y /= P;
        return pt;
    }

    double getSpan(MotionEvent event) {
        int P = event.getPointerCount();
        if (P < 2)
            return 0;
        final int zero[] = { 0, 0 };
        getLocationOnScreen(zero);
        final double x0 = event.getX(0) + zero[0];
        final double x1 = event.getX(1) + zero[0];
        final double y0 = event.getY(0) + zero[1];
        final double y1 = event.getY(1) + zero[1];
        final double span = Math.hypot(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1));
        Log.v(TAG, String.format("zoom: p0=(%g,%g) p1=(%g,%g) span=%g", x0, y0, x1, y1, span));
        return span;
    }

    public void setMoveMode(boolean moveMode) {
        this.moveMode = moveMode;
        invalidate();
    }

    public void setInStickerChooser(boolean x) {
        this.inStickerChooser = x;
    }

    public void removeMoveable() {
        if (selectedDrawing != null) {
            maybeRemoveDrawing(selectedDrawing);
        }
    }

    @SuppressLint("NewApi")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        final int action = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) ? event.getActionMasked()
                : event.getAction();
        int N = event.getHistorySize();
        int P = event.getPointerCount();
        long time = event.getEventTime();

        mEmpty = false;

        // starting a new touch? commit the previous state of the canvas
        if (action == MotionEvent.ACTION_DOWN) {
            commitStroke();
        }

        if (mZoomMode) {
            return false;
        }

        int pointerIndex = MotionEventCompat.getActionIndex(event);
        int pointerId = event.getPointerId(pointerIndex);
        if (moveMode) {
            if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN) {
                if (firstFinger != null) {
                    MotionEvent.PointerCoords coords1 = new MotionEvent.PointerCoords();
                    event.getPointerCoords(0, coords1);
                    Log.d(TAG, "coords1: " + coords1.x + " " + coords1.y);
                    MotionEvent.PointerCoords coords2 = new MotionEvent.PointerCoords();
                    event.getPointerCoords(1, coords2);
                    firstFinger.set(coords1.x, coords1.y);
                    secondFinger = new PointF(coords2.x, coords2.y);
                } else {
                    touchStartTime = System.currentTimeMillis();
                    moveDrawingStartX = event.getX();
                    moveDrawingStartY = event.getY();
                    int i = 0;
                    moveDrawingIndex = -1;
                    resizeDrawingIndex = -1;
                    resizeDrawingCorner = null;
                    if (selectedDrawing != null) {
                        moveDrawingIndex = 0;
                    }
                    if (i >= overlays.size()) {
                        return true;
                    }
                    Log.d(TAG, "Start dragging overlay");
                    firstFinger = new PointF(event.getX(), event.getY());
                }
                return true;
            } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
                if (secondFinger != null) {
                    secondFinger = null;
                    MotionEvent.PointerCoords coords1 = new MotionEvent.PointerCoords();
                    event.getPointerCoords(0, coords1);
                    moveDrawingStartX = coords1.x;
                    moveDrawingStartY = coords1.y;
                    return true;
                }
                if (firstFinger != null) {
                    firstFinger = null;
                }
                if (moveDrawingIndex == -1 && resizeDrawingIndex == -1
                        && System.currentTimeMillis() - touchStartTime < 400
                        && Math.abs(event.getX() - moveDrawingStartX)
                                + Math.abs(event.getY() - moveDrawingStartY) < 8) {
                    if (resizeDrawingCorner != null && selectedDrawing != null) {
                        if (resizeDrawingCorner == "tl" && selectedDrawing instanceof TextDrawing) {
                            //promptForText((TextDrawing)selectedDrawing);
                        } else if (resizeDrawingCorner == "tr") {
                            //maybeRemoveDrawing(selectedDrawing);
                        }
                    } else {
                        //promptForText((int) event.getX(), (int) event.getY());
                    }
                }
                moveDrawingIndex = -1;
                Log.d(TAG, "Stop dragging overlay");
                // TODO: add to undo stack
                return true;
            } else if (firstFinger != null && secondFinger != null && action == MotionEvent.ACTION_MOVE) {
                MotionEvent.PointerCoords coords1 = new MotionEvent.PointerCoords();
                event.getPointerCoords(0, coords1);
                Log.d(TAG, "coords1: " + coords1.x + " " + coords1.y);
                MotionEvent.PointerCoords coords2 = new MotionEvent.PointerCoords();
                event.getPointerCoords(1, coords2);
                Log.d(TAG, "coords2: " + coords2.x + " " + coords2.y);
                float xDist = firstFinger.x - secondFinger.x, yDist = firstFinger.y - secondFinger.y;
                float origAngle = (float) Math.atan2(yDist, xDist);
                if (origAngle < 0)
                    origAngle += Math.PI * 2;
                float lastDistance = (float) Math.sqrt(xDist * xDist + yDist * yDist);

                xDist = coords2.x - coords1.x;
                yDist = coords2.y - coords1.y;
                float newDistance = (float) Math.sqrt(xDist * xDist + yDist * yDist);
                float newAngle = (float) Math.atan2(yDist, xDist);
                if (newAngle < 0)
                    newAngle += Math.PI * 2;
                if (newAngle - origAngle > Math.PI / 2) {
                    origAngle += Math.PI;
                } else if (origAngle - newAngle > Math.PI / 2) {
                    newAngle += Math.PI;
                }

                firstFinger.set(coords1.x, coords1.y);
                secondFinger = new PointF(coords2.x, coords2.y);
                if (selectedDrawing != null) {
                    selectedDrawing.resizeBy(newDistance / lastDistance);
                    selectedDrawing.rotateBy(newAngle - origAngle);
                    invalidate();
                }
            } else if (moveDrawingIndex >= 0 && moveDrawingIndex < overlays.size()
                    && action == MotionEvent.ACTION_MOVE) {
                float x = event.getX();
                float y = event.getY();
                overlays.get(moveDrawingIndex).moveBy((int) ((x - moveDrawingStartX) * getDrawingDensity()),
                        (int) ((y - moveDrawingStartY) * getDrawingDensity()));
                // TODO: only invalidate relevant Rect
                invalidate();
                moveDrawingStartX = x;
                moveDrawingStartY = y;
                return true;
            } else if (resizeDrawingIndex >= 0 && resizeDrawingIndex < overlays.size()
                    && action == MotionEvent.ACTION_MOVE) {
                float x = event.getX();
                float y = event.getY();
                overlays.get(resizeDrawingIndex).resizeCorner(resizeDrawingCorner, (int) (x - moveDrawingStartX),
                        (int) (y - moveDrawingStartY));
                // TODO: only invalidate relevant Rect
                invalidate();
                moveDrawingStartX = x;
                moveDrawingStartY = y;
                return true;
            }
            return false;
        }

        if (action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_POINTER_DOWN
                || action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
            int j = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO) ? event.getActionIndex() : 0;

            mTmpSpot.update(event.getX(j), event.getY(j), event.getSize(j), event.getPressure(j) + event.getSize(j),
                    time, getToolTypeCompat(event, j));
            mStrokes[event.getPointerId(j)].add(mTmpSpot);
            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_POINTER_UP) {
                mStrokes[event.getPointerId(j)].finish(time);
            }
        } else if (action == MotionEvent.ACTION_MOVE) {
            if (dbgX >= 0) {
                dbgRect.set(dbgX - 1, dbgY - 1, dbgX + 1, dbgY + 1);
            }

            for (int i = 0; i < N; i++) {
                for (int j = 0; j < P; j++) {
                    mTmpSpot.update(event.getHistoricalX(j, i), event.getHistoricalY(j, i),
                            event.getHistoricalSize(j, i),
                            event.getHistoricalPressure(j, i) + event.getHistoricalSize(j, i),
                            event.getHistoricalEventTime(i), getToolTypeCompat(event, j));
                    if ((mDebugFlags & FLAG_DEBUG_STROKES) != 0) {
                        if (dbgX >= 0) {
                            //mTiledCanvas.drawLine(dbgX, dbgY, mTmpSpot.x, mTmpSpot.y, mDebugPaints[3]);
                        }
                        dbgX = mTmpSpot.x;
                        dbgY = mTmpSpot.y;
                        dbgRect.union(dbgX - 1, dbgY - 1, dbgX + 1, dbgY + 1);
                    }
                    mStrokes[event.getPointerId(j)].add(mTmpSpot);
                }
            }
            for (int j = 0; j < P; j++) {
                mTmpSpot.update(event.getX(j), event.getY(j), event.getSize(j),
                        event.getPressure(j) + event.getSize(j), time, getToolTypeCompat(event, j));
                if ((mDebugFlags & FLAG_DEBUG_STROKES) != 0) {
                    if (dbgX >= 0) {
                        //mTiledCanvas.drawLine(dbgX, dbgY, mTmpSpot.x, mTmpSpot.y, mDebugPaints[3]);
                    }
                    dbgX = mTmpSpot.x;
                    dbgY = mTmpSpot.y;
                    dbgRect.union(dbgX - 1, dbgY - 1, dbgX + 1, dbgY + 1);
                }
                mStrokes[event.getPointerId(j)].add(mTmpSpot);
            }

            if ((mDebugFlags & FLAG_DEBUG_STROKES) != 0) {
                Rect dirty = new Rect();
                dbgRect.roundOut(dirty);
                invalidate(dirty);
            }
        }

        if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
            for (int j = 0; j < P; j++) {
                mStrokes[event.getPointerId(j)].finish(time);
            }
            dbgX = dbgY = -1;
        }
        return true;
    }

    public static float lerp(float a, float b, float f) {
        return a + f * (b - a);
    }

    public static float clamp(float a, float b, float f) {
        return f < a ? a : (f > b ? b : f);
    }

    @Override
    public void invalidate(Rect r) {
        if (r.isEmpty()) {
            Log.w(TAG, "invalidating empty rect!");
        }
        super.invalidate(r);
    }

    final Rect tmpDirtyRect = new Rect();

    private void dirty(RectF r) {
        r.roundOut(tmpDirtyRect);
        tmpDirtyRect.inset((int) -INVALIDATE_PADDING, (int) -INVALIDATE_PADDING);
        if (INVALIDATE_ALL_THE_THINGS) {
            invalidate();
        } else if (FANCY_INVALIDATES) {
            mDirtyRegion.union(tmpDirtyRect);
            invalidate(); // enqueue invalidation
        } else {
            invalidate(tmpDirtyRect);
        }
    }

    public void setZoomMode(boolean b) {
        mZoomMode = b;
    }

    public float getDrawingDensity() {
        return (float) DENSITY;
    }
}