/**
* Wire
* Copyright (C) 2016 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.waz.zclient.ui.sketch;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.os.Build;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import com.waz.zclient.ui.R;
public class DrawingCanvasView extends View {
private Bitmap bitmap;
private Bitmap backgroundBitmap;
private Canvas canvas;
private Path path;
private Paint bitmapPaint;
private Paint drawingPaint;
private Paint emojiPaint;
private Paint whitePaint;
private DrawingCanvasCallback drawingCanvasCallback;
//used for drawing path
private float currentX;
private float currentY;
private boolean includeBackgroundImage;
private boolean isBackgroundBitmapLandscape = false;
private boolean isPaintedOn = false;
private boolean touchMoved = false;
private static final float TOUCH_TOLERANCE = 2;
private Bitmap.Config bitmapConfig;
private int trimBuffer;
private final int defaultStrokeWidth = getResources().getDimensionPixelSize(R.dimen.color_picker_small_dot_radius) * 2;
private String emoji;
private boolean drawEmoji;
private final SketchCanvasHistory canvasHistory;
public enum Mode {
SKETCH,
TEXT,
EMOJI
}
private Mode currentMode;
public DrawingCanvasView(Context context) {
this(context, null);
}
public DrawingCanvasView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DrawingCanvasView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
canvasHistory = new SketchCanvasHistory();
init();
}
private void init() {
path = new Path();
bitmapConfig = Bitmap.Config.ARGB_8888;
bitmapPaint = new Paint(Paint.DITHER_FLAG);
drawingPaint = new Paint(Paint.DITHER_FLAG | Paint.ANTI_ALIAS_FLAG);
drawingPaint.setColor(Color.BLACK);
drawingPaint.setStyle(Paint.Style.STROKE);
drawingPaint.setStrokeJoin(Paint.Join.ROUND);
drawingPaint.setStrokeCap(Paint.Cap.ROUND);
drawingPaint.setStrokeWidth(defaultStrokeWidth);
whitePaint = new Paint(Paint.DITHER_FLAG);
whitePaint.setColor(Color.WHITE);
emojiPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
emojiPaint.setStrokeWidth(1);
emoji = null;
currentMode = Mode.SKETCH;
trimBuffer = getResources().getDimensionPixelSize(R.dimen.draw_image_trim_buffer);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
try {
clearBitmapSpace(w, h);
bitmap = Bitmap.createBitmap(w, h, bitmapConfig);
canvas = new Canvas(bitmap);
} catch (OutOfMemoryError outOfMemoryError) {
// Fallback to non-alpha canvas if in memory trouble
if (bitmapConfig == Bitmap.Config.ARGB_8888) {
bitmapConfig = Bitmap.Config.RGB_565;
clearBitmapSpace(w, h);
bitmap = Bitmap.createBitmap(w, h, bitmapConfig);
canvas = new Canvas(bitmap);
}
}
redraw();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(Color.TRANSPARENT);
canvas.drawBitmap(bitmap, 0, 0, bitmapPaint);
if (drawEmoji) {
canvas.drawText(emoji, currentX, currentY, emojiPaint);
} else {
canvas.drawPath(path, drawingPaint);
}
}
public void setBackgroundBitmap(Bitmap bitmap) {
if (bitmap.getWidth() == 0 || bitmap.getHeight() == 0) {
return;
}
backgroundBitmap = bitmap;
if (backgroundBitmap.getWidth() > backgroundBitmap.getHeight()) {
isBackgroundBitmapLandscape = true;
}
drawBackgroundBitmap();
}
public void reset() {
paintedOn(false);
canvasHistory.clear();
canvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), whitePaint);
drawBackgroundBitmap();
invalidate();
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (currentMode == Mode.TEXT) {
return scaleGestureDetector.onTouchEvent(event);
}
if (longPressGestureDetector.onTouchEvent(event) && backgroundBitmap == null) {
invalidate();
return true;
}
int whiteColor;
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
//noinspection deprecation
whiteColor = getResources().getColor(R.color.draw_white);
} else {
whiteColor = getResources().getColor(R.color.draw_white, getContext().getTheme());
}
if (backgroundBitmap == null &&
canvasHistory.size() == 0 &&
drawingPaint.getColor() == whiteColor) {
return true;
}
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
invalidate();
break;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
invalidate();
break;
case MotionEvent.ACTION_UP:
touch_up();
invalidate();
break;
}
return true;
}
private final ScaleGestureDetector scaleGestureDetector = new ScaleGestureDetector(getContext(), new ScaleListener());
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
float scaleFactor = 1f;
@Override
public boolean onScaleBegin(ScaleGestureDetector detector) {
if (drawingCanvasCallback != null) {
drawingCanvasCallback.onScaleStart();
}
return true;
}
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
if (drawingCanvasCallback != null) {
drawingCanvasCallback.onScaleEnd();
}
}
@Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
// Don't let the object get too small or too large.
scaleFactor = Math.max(0.1f, Math.min(scaleFactor, 5.0f));
if (drawingCanvasCallback != null) {
drawingCanvasCallback.onScaleChanged(scaleFactor);
}
return true;
}
}
private final GestureDetector longPressGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
public void onLongPress(MotionEvent e) {
if (backgroundBitmap != null || currentMode != Mode.SKETCH) {
return;
}
drawingPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), drawingPaint);
canvasHistory.addFillScreen(bitmap.getWidth(), bitmap.getHeight(), new Paint(drawingPaint));
paintedOn(true);
drawingPaint.setStyle(Paint.Style.STROKE);
invalidate();
}
});
private void touch_start(float x, float y) {
if (currentMode == Mode.SKETCH) {
path.reset();
path.moveTo(x, y);
currentX = x;
currentY = y;
} else if (currentMode == Mode.EMOJI) {
drawEmoji = true;
currentX = x - emojiPaint.getTextSize() / 2;
currentY = y;
}
}
private void touch_move(float x, float y) {
float dx = Math.abs(x - currentX);
float dy = Math.abs(y - currentY);
if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
if (drawEmoji) {
currentX = x - emojiPaint.getTextSize() / 2;
currentY = y;
} else {
path.quadTo(currentX, currentY, (x + currentX) / 2, (y + currentY) / 2);
currentX = x;
currentY = y;
}
paintedOn(true);
touchMoved = true;
}
}
private void touch_up() {
if (drawEmoji) {
drawEmoji = false;
canvas.drawText(emoji, currentX, currentY, emojiPaint);
canvasHistory.addEmoji(emoji, currentX, currentY, new Paint(emojiPaint));
paintedOn(true);
} else {
path.lineTo(currentX, currentY);
canvas.drawPath(path, drawingPaint);
if (touchMoved) {
touchMoved = false;
RectF bounds = new RectF();
path.computeBounds(bounds, true);
canvasHistory.addStroke(new Path(path), new Paint(drawingPaint), bounds);
}
path.reset();
}
}
public Rect getImageTrimValues() {
int top = bitmap.getHeight();
int left = bitmap.getWidth();
int right = 0;
int bottom = 0;
boolean checkLeftRight = true;
boolean checkTopBottom = true;
int bitmapTop = 0;
int bitmapBottom = 0;
int bitmapLeft = 0;
int bitmapRight = 0;
if (includeBackgroundImage) {
if (isBackgroundBitmapLandscape) {
left = 0;
right = bitmap.getWidth();
checkLeftRight = false;
bitmapTop = bitmap.getHeight() / 2 - backgroundBitmap.getHeight() / 2;
top = bitmapTop;
bitmapBottom = bitmap.getHeight() / 2 + backgroundBitmap.getHeight() / 2;
bottom = bitmapBottom;
} else {
top = 0;
bottom = bitmap.getHeight();
checkTopBottom = false;
float ratio = (float) canvas.getHeight() / backgroundBitmap.getHeight();
int imageWidth = (int) (backgroundBitmap.getWidth() * ratio);
bitmapLeft = bitmap.getWidth() / 2 - imageWidth / 2;
left = bitmapLeft;
bitmapRight = bitmap.getWidth() / 2 + imageWidth / 2;
right = bitmapRight;
}
}
for (SketchCanvasHistory.HistoryItem historyItem: canvasHistory.getHistoryItems()) {
if (historyItem instanceof SketchCanvasHistory.FilledScreen) {
top = 0;
bottom = bitmap.getHeight();
left = 0;
right = bitmap.getWidth();
break;
} else if (historyItem instanceof SketchCanvasHistory.Stroke) {
RectF bounds = ((SketchCanvasHistory.Stroke) historyItem).getBounds();
if (checkTopBottom) {
top = Math.min(top, (int) bounds.top);
bottom = Math.max(bottom, (int) bounds.bottom);
}
if (checkLeftRight) {
left = Math.min(left, (int) bounds.left);
right = Math.max(right, (int) bounds.right);
}
} else if (historyItem instanceof SketchCanvasHistory.Emoji) {
SketchCanvasHistory.Emoji emoji = (SketchCanvasHistory.Emoji) historyItem;
if (checkTopBottom) {
top = Math.min(top, (int) (emoji.y - emoji.paint.getTextSize()));
bottom = Math.max(bottom, (int) (emoji.y));
}
if (checkLeftRight) {
left = Math.min(left, (int) emoji.x);
right = Math.max(right, (int) (emoji.x + emoji.paint.getTextSize()));
}
} else if (historyItem instanceof SketchCanvasHistory.Text) {
SketchCanvasHistory.Text text = (SketchCanvasHistory.Text) historyItem;
if (checkTopBottom) {
top = Math.min(top, (int) (text.y - text.paint.getTextSize()));
bottom = Math.max(bottom, (int) (text.y));
}
if (checkLeftRight) {
left = Math.min(left, (int) text.x);
right = Math.max(right, (int) (text.x + text.paint.getTextSize()));
}
}
}
int topTrimBuffer = trimBuffer;
int bottomTrimBuffer = trimBuffer;
int leftTrimBuffer = trimBuffer;
int rightTrimBuffer = trimBuffer;
if (includeBackgroundImage) {
if (left >= bitmapLeft) {
leftTrimBuffer = 0;
}
if (right <= bitmapRight) {
rightTrimBuffer = 0;
}
if (top >= bitmapTop) {
topTrimBuffer = 0;
}
if (bottom <= bitmapBottom) {
bottomTrimBuffer = 0;
}
}
return new Rect(Math.max(0, left - leftTrimBuffer),
Math.max(0, top - topTrimBuffer),
Math.min(bitmap.getWidth(), right + rightTrimBuffer),
Math.min(bitmap.getHeight(), bottom + bottomTrimBuffer));
}
public void setDrawingColor(int color) {
drawingPaint.setColor(color);
emojiPaint.setColor(color);
}
public void setStrokeSize(int strokeSize) {
drawingPaint.setStrokeWidth(strokeSize);
}
public void setEmoji(String emoji, float size) {
currentMode = Mode.EMOJI;
this.emoji = emoji;
emojiPaint.setTextSize(size);
}
public void setCurrentMode(Mode mode) {
currentMode = mode;
}
public Mode getCurrentMode() {
return currentMode;
}
public boolean undo() {
if (canvasHistory.size() == 0) {
return false;
}
if (canvasHistory.size() == 1) {
paintedOn(false);
}
SketchCanvasHistory.HistoryItem last = canvasHistory.undo();
if (last instanceof SketchCanvasHistory.Text) {
SketchCanvasHistory.Text newLastText = canvasHistory.getLastText();
if (newLastText != null && newLastText.text != null) {
drawingCanvasCallback.onTextChanged(newLastText.text, (int) newLastText.x, (int) newLastText.y, newLastText.scale);
} else {
drawingCanvasCallback.onTextRemoved();
}
}
redraw();
return true;
}
public void drawTextBitmap(Bitmap textBitmap, float x, float y, String text, float scale) {
canvasHistory.addText(textBitmap, x, y, text, scale, bitmapPaint);
redraw();
}
private void paintedOn(boolean isPaintedOn) {
if (this.isPaintedOn == isPaintedOn) {
return;
}
this.isPaintedOn = isPaintedOn;
if (isPaintedOn) {
drawingCanvasCallback.drawingAdded();
} else {
drawingCanvasCallback.drawingCleared();
}
}
public void setDrawingCanvasCallback(DrawingCanvasCallback drawingCanvasCallback) {
this.drawingCanvasCallback = drawingCanvasCallback;
}
public Bitmap getBitmap() {
return bitmap;
}
public void drawBackgroundBitmap() {
if (backgroundBitmap == null || canvas == null) {
return;
}
includeBackgroundImage = true;
RectF src;
RectF dest;
int horizontalMargin;
int imageHeight;
int imageWidth;
if (isBackgroundBitmapLandscape) {
horizontalMargin = 0;
imageWidth = canvas.getWidth();
imageHeight = canvas.getHeight();
src = new RectF(0, 0, backgroundBitmap.getWidth(), backgroundBitmap.getHeight());
dest = new RectF(0, 0, imageWidth, imageHeight);
} else {
float ratio = (float) canvas.getHeight() / backgroundBitmap.getHeight();
imageWidth = (int) (backgroundBitmap.getWidth() * ratio);
imageHeight = canvas.getHeight();
horizontalMargin = (canvas.getWidth() / 2) - (imageWidth / 2);
src = new RectF(0, 0, backgroundBitmap.getWidth() - 1, backgroundBitmap.getHeight() - 1);
dest = new RectF(0, 0, imageWidth, imageHeight);
}
Matrix matrix = new Matrix();
matrix.setRectToRect(src, dest, Matrix.ScaleToFit.CENTER);
matrix.postTranslate(horizontalMargin, 0);
canvas.drawBitmap(backgroundBitmap, matrix, null);
}
public void removeBackgroundBitmap() {
includeBackgroundImage = false;
redraw();
}
public boolean isEmpty() {
return canvasHistory.size() == 0;
}
public void clearBitmapSpace(int width, int height) {
bitmap = null;
canvas = null;
if (drawingCanvasCallback != null) {
drawingCanvasCallback.reserveBitmapMemory(width, height);
}
}
public void hideText() {
canvasHistory.hideText();
redraw();
}
public void showText() {
canvasHistory.showText();
redraw();
}
private void redraw() {
canvas.drawRect(0, 0, bitmap.getWidth(), bitmap.getHeight(), whitePaint);
paintedOn(canvasHistory.size() > 0);
if (includeBackgroundImage) {
drawBackgroundBitmap();
}
drawHistory();
invalidate();
}
private void drawHistory() {
canvasHistory.draw(canvas);
}
public void onDestroy() {
bitmap = null;
backgroundBitmap = null;
canvas = null;
if (canvasHistory != null) {
canvasHistory.clear();
}
}
}