mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 08:39:22 +01:00
Implement proper text-entry component for large screen media send flow.
This commit is contained in:
committed by
jeffrey-signal
parent
2a8bd20bb0
commit
b21a72153a
@@ -1,196 +0,0 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.imageeditor.core;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PointF;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.imageeditor.core.model.EditorElement;
|
||||
import org.signal.imageeditor.core.model.EditorModel;
|
||||
import org.signal.imageeditor.core.model.ThumbRenderer;
|
||||
import org.signal.imageeditor.core.renderers.BezierDrawingRenderer;
|
||||
|
||||
/**
|
||||
* Public facade for touch handling on an {@link EditorModel}.
|
||||
* <p>
|
||||
* Encapsulates the {@link EditSession} creation and lifecycle so that callers outside
|
||||
* this package (e.g. a Compose component) can drive the editor without accessing
|
||||
* package-private classes directly.
|
||||
* <p>
|
||||
* Usage: call the on* methods in order as pointer events arrive. The handler manages
|
||||
* edit session state internally.
|
||||
*/
|
||||
public final class ImageEditorTouchHandler {
|
||||
|
||||
private boolean drawing;
|
||||
private boolean blur;
|
||||
private int drawColor = 0xff000000;
|
||||
private float drawThickness = 0.02f;
|
||||
@NonNull
|
||||
private Paint.Cap drawCap = Paint.Cap.ROUND;
|
||||
|
||||
@Nullable private EditSession editSession;
|
||||
private boolean moreThanOnePointerUsedInSession;
|
||||
|
||||
/** Configures whether the next gesture should create a drawing session if no element is hit. */
|
||||
public void setDrawing(boolean drawing, boolean blur) {
|
||||
this.drawing = drawing;
|
||||
this.blur = blur;
|
||||
}
|
||||
|
||||
/** Sets the brush parameters used when creating new drawing sessions. */
|
||||
public void setDrawingBrush(int color, float thickness, @NonNull Paint.Cap cap) {
|
||||
this.drawColor = color;
|
||||
this.drawThickness = thickness;
|
||||
this.drawCap = cap;
|
||||
}
|
||||
|
||||
/** Begins a new gesture. Creates either a move/resize, thumb drag, or drawing session. */
|
||||
@Nullable
|
||||
public EditorElement onDown(@NonNull EditorModel model, @NonNull Matrix viewMatrix, @NonNull PointF point) {
|
||||
Matrix inverse = new Matrix();
|
||||
EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse);
|
||||
|
||||
moreThanOnePointerUsedInSession = false;
|
||||
model.pushUndoPoint();
|
||||
editSession = startEdit(model, viewMatrix, inverse, point, selected);
|
||||
|
||||
return editSession != null ? editSession.getSelected() : null;
|
||||
}
|
||||
|
||||
/** Feeds pointer positions to the active session. Call for every move event. */
|
||||
public void onMove(@NonNull EditorModel model, @NonNull PointF[] pointers) {
|
||||
if (editSession == null) return;
|
||||
|
||||
int pointerCount = Math.min(2, pointers.length);
|
||||
for (int p = 0; p < pointerCount; p++) {
|
||||
editSession.movePoint(p, pointers[p]);
|
||||
}
|
||||
model.moving(editSession.getSelected());
|
||||
}
|
||||
|
||||
/** Transitions a single-finger session to a two-finger session (e.g. pinch-to-zoom). */
|
||||
public void onSecondPointerDown(@NonNull EditorModel model, @NonNull Matrix viewMatrix, @NonNull PointF newPointerPoint, int pointerIndex) {
|
||||
if (editSession == null) return;
|
||||
|
||||
moreThanOnePointerUsedInSession = true;
|
||||
editSession.commit();
|
||||
model.pushUndoPoint();
|
||||
|
||||
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
||||
if (newInverse != null) {
|
||||
editSession = editSession.newPoint(newInverse, newPointerPoint, pointerIndex);
|
||||
} else {
|
||||
editSession = null;
|
||||
}
|
||||
|
||||
if (editSession == null) {
|
||||
model.dragDropRelease();
|
||||
}
|
||||
}
|
||||
|
||||
/** Transitions a two-finger session back to single-finger when one pointer lifts. */
|
||||
public void onSecondPointerUp(@NonNull EditorModel model, @NonNull Matrix viewMatrix, int releasedIndex) {
|
||||
if (editSession == null) return;
|
||||
|
||||
editSession.commit();
|
||||
model.pushUndoPoint();
|
||||
model.dragDropRelease();
|
||||
|
||||
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
||||
if (newInverse != null) {
|
||||
editSession = editSession.removePoint(newInverse, releasedIndex);
|
||||
} else {
|
||||
editSession = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the current gesture: commits the session and calls {@link EditorModel#postEdit}. */
|
||||
public void onUp(@NonNull EditorModel model) {
|
||||
if (editSession != null) {
|
||||
editSession.commit();
|
||||
model.dragDropRelease();
|
||||
editSession = null;
|
||||
}
|
||||
model.postEdit(moreThanOnePointerUsedInSession);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
editSession = null;
|
||||
}
|
||||
|
||||
public boolean hasActiveSession() {
|
||||
return editSession != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public EditorElement getSelected() {
|
||||
return editSession != null ? editSession.getSelected() : null;
|
||||
}
|
||||
|
||||
private @Nullable EditSession startEdit(
|
||||
@NonNull EditorModel model,
|
||||
@NonNull Matrix viewMatrix,
|
||||
@NonNull Matrix inverse,
|
||||
@NonNull PointF point,
|
||||
@Nullable EditorElement selected
|
||||
) {
|
||||
EditSession session = startMoveAndResizeSession(model, viewMatrix, inverse, point, selected);
|
||||
if (session == null && drawing) {
|
||||
return startDrawingSession(model, viewMatrix, point);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private @Nullable EditSession startDrawingSession(@NonNull EditorModel model, @NonNull Matrix viewMatrix, @NonNull PointF point) {
|
||||
BezierDrawingRenderer renderer = new BezierDrawingRenderer(drawColor, drawThickness * Bounds.FULL_BOUNDS.width(), drawCap, model.findCropRelativeToRoot());
|
||||
EditorElement element = new EditorElement(renderer, blur ? EditorModel.Z_MASK : EditorModel.Z_DRAWING);
|
||||
|
||||
model.addElementCentered(element, 1);
|
||||
|
||||
Matrix elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix);
|
||||
|
||||
return DrawingSession.start(element, renderer, elementInverseMatrix, point);
|
||||
}
|
||||
|
||||
private static @Nullable EditSession startMoveAndResizeSession(
|
||||
@NonNull EditorModel model,
|
||||
@NonNull Matrix viewMatrix,
|
||||
@NonNull Matrix inverse,
|
||||
@NonNull PointF point,
|
||||
@Nullable EditorElement selected
|
||||
) {
|
||||
if (selected == null) return null;
|
||||
|
||||
if (selected.getRenderer() instanceof ThumbRenderer) {
|
||||
ThumbRenderer thumb = (ThumbRenderer) selected.getRenderer();
|
||||
|
||||
EditorElement thumbControlledElement = model.findById(thumb.getElementToControl());
|
||||
if (thumbControlledElement == null) return null;
|
||||
|
||||
EditorElement thumbsParent = model.getRoot().findParent(selected);
|
||||
if (thumbsParent == null) return null;
|
||||
|
||||
Matrix thumbContainerRelativeMatrix = model.findRelativeMatrix(thumbsParent, thumbControlledElement);
|
||||
if (thumbContainerRelativeMatrix == null) return null;
|
||||
|
||||
selected = thumbControlledElement;
|
||||
|
||||
Matrix elementInverseMatrix = model.findElementInverseMatrix(selected, viewMatrix);
|
||||
if (elementInverseMatrix != null) {
|
||||
return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumbContainerRelativeMatrix, thumb.getControlPoint(), point);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return ElementDragEditSession.startDrag(selected, inverse, point);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* Copyright 2026 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.imageeditor.core
|
||||
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PointF
|
||||
import org.signal.imageeditor.core.model.EditorElement
|
||||
import org.signal.imageeditor.core.model.EditorModel
|
||||
import org.signal.imageeditor.core.model.ThumbRenderer
|
||||
import org.signal.imageeditor.core.renderers.BezierDrawingRenderer
|
||||
|
||||
/**
|
||||
* Public facade for touch handling on an [EditorModel].
|
||||
*
|
||||
* Encapsulates the [EditSession] creation and lifecycle so that callers outside
|
||||
* this package (e.g. a Compose component) can drive the editor without accessing
|
||||
* package-private classes directly.
|
||||
*
|
||||
* Usage: call the on* methods in order as pointer events arrive. The handler manages
|
||||
* edit session state internally.
|
||||
*/
|
||||
class ImageEditorTouchHandler {
|
||||
|
||||
private var drawing: Boolean = false
|
||||
private var blur: Boolean = false
|
||||
private var drawColor: Int = 0xff000000.toInt()
|
||||
private var drawThickness: Float = 0.02f
|
||||
private var drawCap: Paint.Cap = Paint.Cap.ROUND
|
||||
|
||||
private var editSession: EditSession? = null
|
||||
private var moreThanOnePointerUsedInSession: Boolean = false
|
||||
|
||||
/** Configures whether the next gesture should create a drawing session if no element is hit. */
|
||||
fun setDrawing(drawing: Boolean, blur: Boolean) {
|
||||
this.drawing = drawing
|
||||
this.blur = blur
|
||||
}
|
||||
|
||||
/** Sets the brush parameters used when creating new drawing sessions. */
|
||||
fun setDrawingBrush(color: Int, thickness: Float, cap: Paint.Cap) {
|
||||
drawColor = color
|
||||
drawThickness = thickness
|
||||
drawCap = cap
|
||||
}
|
||||
|
||||
/** Begins a new gesture. Creates either a move/resize, thumb drag, or drawing session. */
|
||||
fun onDown(model: EditorModel, viewMatrix: Matrix, point: PointF): EditorElement? {
|
||||
val inverse = Matrix()
|
||||
val selected = model.findElementAtPoint(point, viewMatrix, inverse)
|
||||
|
||||
moreThanOnePointerUsedInSession = false
|
||||
model.pushUndoPoint()
|
||||
editSession = startEdit(model, viewMatrix, inverse, point, selected)
|
||||
|
||||
return editSession?.selected
|
||||
}
|
||||
|
||||
/** Feeds pointer positions to the active session. Call for every move event. */
|
||||
fun onMove(model: EditorModel, pointers: Array<PointF>) {
|
||||
val currentEditSession = editSession ?: return
|
||||
|
||||
val pointerCount = minOf(2, pointers.size)
|
||||
for (p in 0 until pointerCount) {
|
||||
currentEditSession.movePoint(p, pointers[p])
|
||||
}
|
||||
model.moving(currentEditSession.selected)
|
||||
}
|
||||
|
||||
/** Transitions a single-finger session to a two-finger session (e.g. pinch-to-zoom). */
|
||||
fun onSecondPointerDown(model: EditorModel, viewMatrix: Matrix, newPointerPoint: PointF, pointerIndex: Int) {
|
||||
val currentEditSession = editSession ?: return
|
||||
|
||||
moreThanOnePointerUsedInSession = true
|
||||
currentEditSession.commit()
|
||||
model.pushUndoPoint()
|
||||
|
||||
val newInverse = model.findElementInverseMatrix(currentEditSession.selected, viewMatrix)
|
||||
editSession = if (newInverse != null) {
|
||||
currentEditSession.newPoint(newInverse, newPointerPoint, pointerIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (editSession == null) {
|
||||
model.dragDropRelease()
|
||||
}
|
||||
}
|
||||
|
||||
/** Transitions a two-finger session back to single-finger when one pointer lifts. */
|
||||
fun onSecondPointerUp(model: EditorModel, viewMatrix: Matrix, releasedIndex: Int) {
|
||||
val currentEditSession = editSession ?: return
|
||||
|
||||
currentEditSession.commit()
|
||||
model.pushUndoPoint()
|
||||
model.dragDropRelease()
|
||||
|
||||
val newInverse = model.findElementInverseMatrix(currentEditSession.selected, viewMatrix)
|
||||
editSession = if (newInverse != null) {
|
||||
currentEditSession.removePoint(newInverse, releasedIndex)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/** Ends the current gesture: commits the session and calls [EditorModel.postEdit]. */
|
||||
fun onUp(model: EditorModel) {
|
||||
editSession?.let {
|
||||
it.commit()
|
||||
model.dragDropRelease()
|
||||
editSession = null
|
||||
}
|
||||
model.postEdit(moreThanOnePointerUsedInSession)
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
editSession = null
|
||||
}
|
||||
|
||||
fun hasActiveSession(): Boolean {
|
||||
return editSession != null
|
||||
}
|
||||
|
||||
fun getSelected(): EditorElement? {
|
||||
return editSession?.selected
|
||||
}
|
||||
|
||||
private fun startEdit(
|
||||
model: EditorModel,
|
||||
viewMatrix: Matrix,
|
||||
inverse: Matrix,
|
||||
point: PointF,
|
||||
selected: EditorElement?
|
||||
): EditSession? {
|
||||
val session = startMoveAndResizeSession(model, viewMatrix, inverse, point, selected)
|
||||
if (session == null && drawing) {
|
||||
return startDrawingSession(model, viewMatrix, point)
|
||||
}
|
||||
return session
|
||||
}
|
||||
|
||||
private fun startDrawingSession(model: EditorModel, viewMatrix: Matrix, point: PointF): EditSession {
|
||||
val renderer = BezierDrawingRenderer(
|
||||
drawColor,
|
||||
drawThickness * Bounds.FULL_BOUNDS.width(),
|
||||
drawCap,
|
||||
model.findCropRelativeToRoot()
|
||||
)
|
||||
val element = EditorElement(renderer, if (blur) EditorModel.Z_MASK else EditorModel.Z_DRAWING)
|
||||
|
||||
model.addElementCentered(element, 1f)
|
||||
|
||||
val elementInverseMatrix = model.findElementInverseMatrix(element, viewMatrix)
|
||||
|
||||
return DrawingSession.start(element, renderer, elementInverseMatrix, point)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
fun startMoveAndResizeSession(
|
||||
model: EditorModel,
|
||||
viewMatrix: Matrix,
|
||||
inverse: Matrix,
|
||||
point: PointF,
|
||||
selected: EditorElement?
|
||||
): EditSession? {
|
||||
if (selected == null) return null
|
||||
|
||||
if (selected.renderer is ThumbRenderer) {
|
||||
val thumb = selected.renderer as ThumbRenderer
|
||||
|
||||
val thumbControlledElement = model.findById(thumb.elementToControl) ?: return null
|
||||
val thumbsParent = model.root.findParent(selected) ?: return null
|
||||
val thumbContainerRelativeMatrix = model.findRelativeMatrix(thumbsParent, thumbControlledElement) ?: return null
|
||||
|
||||
val elementInverseMatrix = model.findElementInverseMatrix(thumbControlledElement, viewMatrix)
|
||||
return if (elementInverseMatrix != null) {
|
||||
ThumbDragEditSession.startDrag(
|
||||
thumbControlledElement,
|
||||
elementInverseMatrix,
|
||||
thumbContainerRelativeMatrix,
|
||||
thumb.controlPoint,
|
||||
point
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
return ElementDragEditSession.startDrag(selected, inverse, point)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user