package ch.threema.app.ui;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.core.view.ViewCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PaintView extends View {
private float mX, mY;
private int currentColor, currentStrokeWidth, currentWidth, currentHeight;
private boolean isTransparent = false ;
private static final float TOUCH_TOLERANCE = 4;
private boolean isActive = true , hasMoved;
private TouchListener onTouchListener;
private final List<Rect> drawingRect = Collections.singletonList(new Rect());
private final ArrayList<Path> paths = new ArrayList<>();
private final ArrayList<Paint> paints = new ArrayList<>();
public PaintView(Context context) {
super (context);
init();
}
public PaintView(Context context, AttributeSet attrs) {
super (context, attrs);
init();
}
public PaintView(Context context, AttributeSet attrs, int defStyle) {
super (context, attrs, defStyle);
init();
}
private void init() {
createPath();
createPaint();
// defaults
currentColor = 0xFFFF0000;
currentStrokeWidth = 15;
}
private Path createPath() {
Path path = new Path();
paths.add(path);
return path;
}
private void createPaint() {
Paint paint = new Paint();
paints.add(paint);
paint.setAntiAlias(true );
paint.setDither(true );
paint.setColor(currentColor);
if (isTransparent) {
paint.setAlpha(128);
}
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeJoin(Paint.Join.ROUND);
paint.setStrokeCap(Paint.Cap.ROUND);
paint.setStrokeWidth(currentStrokeWidth);
}
private Path getCurrentPath() {
return paths.get(paths.size() - 1);
}
@Override
protected void onDraw(Canvas canvas) {
super .onDraw(canvas);
currentWidth = canvas.getWidth();
currentHeight = canvas.getHeight();
for (int i = 0; i < paths.size(); i++) {
canvas.drawPath(paths.get(i), paints.get(i));
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super .onLayout(changed, left, top, right, bottom);
getDrawingRect(drawingRect.get(0));
ViewCompat.setSystemGestureExclusionRects(this , drawingRect);
}
private void touch_start(float x, float y) {
// new path
Path path = createPath();
createPaint();
path.moveTo(x, y);
mX = x;
mY = y;
hasMoved = false ;
}
private void touch_move(float x, float y) {
if (isRealMovement(x, y)) {
getCurrentPath().quadTo(mX, mY, (x + mX) / 2, (y + mY) / 2);
mX = x;
mY = y;
hasMoved = true ;
}
}
private void touch_up(float x, float y) {
if (isRealMovement(x, y) || hasMoved) {
getCurrentPath().lineTo(mX, mY);
onTouchListener.onAdded();
} else {
int pathIndex = paths.size() - 1;
paths.remove(pathIndex);
paints.remove(pathIndex);
invalidate();
}
}
private boolean isRealMovement(float x, float y) {
float dx = Math.abs(x - mX);
float dy = Math.abs(y - mY);
return (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isActive) {
return false ;
}
float x = event.getX();
float y = event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
touch_start(x, y);
this .onTouchListener.onTouchDown();
break ;
case MotionEvent.ACTION_MOVE:
touch_move(x, y);
break ;
case MotionEvent.ACTION_CANCEL:
// Handle a cancel action the same as action up (for system gestures)
case MotionEvent.ACTION_UP:
touch_up(x, y);
this .onTouchListener.onTouchUp();
break ;
default :
return false ;
}
invalidate();
return true ;
}
public void undo() {
int pathIndex = paths.size() - 1;
if (pathIndex > 0) {
paths.remove(pathIndex);
paints.remove(pathIndex);
invalidate();
onTouchListener.onDeleted();
}
}
public void renderOverlay(Canvas combinedCanvas, int srcWidth, int srcHeight) {
// render overlay to original canvas
float factorX = (float ) combinedCanvas.getWidth() / (float ) srcWidth;
float factorY = (float ) combinedCanvas.getHeight() / (float ) srcHeight;
Matrix matrix = new Matrix();
matrix.setScale(factorX, factorY);
for (int i = 0; i < paths.size(); i++) {
Path path = paths.get(i);
Path scaledPath = new Path();
path.transform(matrix, scaledPath);
Paint paint = paints.get(i);
Paint scaledPaint = new Paint(paint);
scaledPaint.setStrokeWidth(scaledPaint.getStrokeWidth() * factorX);
combinedCanvas.drawPath(scaledPath, scaledPaint);
}
}
public void recalculate(int newWidth, int newHeight) {
if (currentHeight != 0 && currentWidth != 0) {
float factorX = (float ) newWidth / (float ) currentWidth;
float factorY = (float ) newHeight / (float ) currentHeight;
Matrix matrix = new Matrix();
matrix.setScale(factorX, factorY);
for (int i = 0; i < paths.size(); i++) {
Path path = paths.get(i);
Path scaledPath = new Path();
path.transform(matrix, scaledPath);
paths.get(i).set(scaledPath);
Paint paint = paints.get(i);
Paint scaledPaint = new Paint(paint);
scaledPaint.setStrokeWidth(scaledPaint.getStrokeWidth() * factorX);
paints.get(i).set(scaledPaint);
}
invalidate();
}
}
public void flip() {
Matrix flipMatrix = new Matrix();
flipMatrix.setScale(-1, 1);
flipMatrix.postTranslate(getWidth(), 0);
for (Path path : paths) {
path.transform(flipMatrix);
}
}
public void setColor(int color) {
currentColor = color;
}
public void setTransparent(boolean isTransparent) {
this .isTransparent = isTransparent;
}
public void setStrokeWidth(int width) {
currentStrokeWidth = width;
}
public void setActive(boolean active) {
isActive = active;
}
public boolean getActive() {
return isActive;
}
public int getNumPaths() {
return paths.size() - 1;
}
public void setTouchListener(TouchListener touchListener) {
this .onTouchListener = touchListener;
}
public interface TouchListener {
void onTouchUp();
void onTouchDown();
void onAdded();
void onDeleted();
}
}
Messung V0.5 in Prozent C=95 H=96 G=95
¤ Dauer der Verarbeitung: 0.0 Sekunden
(vorverarbeitet am 2026-04-27)
¤
*© Formatika GbR, Deutschland