From 4569011e0bade620e6a433edfff3d52fa783c29f Mon Sep 17 00:00:00 2001 From: Alan Evans Date: Wed, 15 Sep 2021 16:32:06 -0300 Subject: [PATCH] Two point thumb control for scale and rotate. --- .../securesms/scribbles/UriGlideRenderer.java | 14 ++- .../imageeditor/core/ImageEditorView.java | 20 +++- .../imageeditor/core/SelectableRenderer.kt | 7 ++ .../core/ThumbDragEditSession.java | 98 ++++++++++++++++--- .../imageeditor/core/model/EditorElement.java | 32 ++++++ .../core/model/EditorElementHierarchy.java | 75 ++++++++++++++ .../imageeditor/core/model/EditorModel.java | 37 ++++++- .../imageeditor/core/model/ThumbRenderer.java | 30 ++++-- .../core/renderers/MultiLineTextRenderer.java | 26 ++--- .../renderers/SelectedElementGuideRenderer.kt | 72 +++++++------- 10 files changed, 318 insertions(+), 93 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java index 5bece0f868..b93f9a44f2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/UriGlideRenderer.java @@ -30,7 +30,6 @@ import org.signal.imageeditor.core.RendererContext; import org.signal.imageeditor.core.SelectableRenderer; import org.signal.imageeditor.core.model.EditorElement; import org.signal.imageeditor.core.model.EditorModel; -import org.signal.imageeditor.core.renderers.SelectedElementGuideRenderer; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequest; @@ -66,8 +65,6 @@ public final class UriGlideRenderer implements SelectableRenderer { private boolean selected; - private final SelectedElementGuideRenderer selectedElementGuideRenderer = new SelectedElementGuideRenderer(); - @Nullable private Bitmap bitmap; @Nullable private Bitmap blurredBitmap; @Nullable private Paint blurPaint; @@ -141,10 +138,6 @@ public final class UriGlideRenderer implements SelectableRenderer { // If failed to load, we draw a black out, in case image was sticker positioned to cover private info. rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint); } - - if (selected && rendererContext.isEditing()) { - selectedElementGuideRenderer.render(rendererContext); - } } private void renderBlurOverlay(RendererContext rendererContext) { @@ -207,7 +200,7 @@ public final class UriGlideRenderer implements SelectableRenderer { @Override public boolean hitTest(float x, float y) { - return pixelAlphaNotZero(x, y); + return selected ? Bounds.contains(x, y) : pixelAlphaNotZero(x, y); } private boolean pixelAlphaNotZero(float x, float y) { @@ -339,4 +332,9 @@ public final class UriGlideRenderer implements SelectableRenderer { this.selected = selected; } } + + @Override + public void getSelectionBounds(@NonNull RectF bounds) { + bounds.set(Bounds.FULL_BOUNDS); + } } diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java index f03fd9bb6f..11f1235ee8 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ImageEditorView.java @@ -121,6 +121,7 @@ public final class ImageEditorView extends FrameLayout { public void startTextEditing(@NonNull EditorElement editorElement) { getModel().addFade(); if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { + getModel().setSelectionVisible(false); editText.setCurrentTextEditorElement(editorElement); } } @@ -136,7 +137,9 @@ public final class ImageEditorView extends FrameLayout { public void doneTextEditing() { getModel().zoomOut(); getModel().removeFade(); + getModel().setSelectionVisible(true); if (editText.getCurrentTextEntity() != null) { + getModel().setSelected(null); editText.setCurrentTextEditorElement(null); editText.hideKeyboard(); } @@ -391,13 +394,22 @@ public final class ImageEditorView extends FrameLayout { if (selected.getRenderer() instanceof ThumbRenderer) { ThumbRenderer thumb = (ThumbRenderer) selected.getRenderer(); - selected = getModel().findById(thumb.getElementToControl()); + EditorElement thumbControlledElement = getModel().findById(thumb.getElementToControl()); + if (thumbControlledElement == null) return null; - if (selected == null) return null; + EditorElement thumbsParent = getModel().getRoot().findParent(selected); + + if (thumbsParent == null) return null; + + Matrix thumbContainerRelativeMatrix = model.findRelativeMatrix(thumbsParent, thumbControlledElement); + + if (thumbContainerRelativeMatrix == null) return null; + + selected = thumbControlledElement; elementInverseMatrix = model.findElementInverseMatrix(selected, viewMatrix); if (elementInverseMatrix != null) { - return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumb.getControlPoint(), point); + return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumbContainerRelativeMatrix, thumb.getControlPoint(), point); } else { return null; } @@ -501,9 +513,11 @@ public final class ImageEditorView extends FrameLayout { if (editSession != null) { EditorElement selected = editSession.getSelected(); model.indicateSelected(selected); + model.setSelected(selected); tapListener.onEntitySingleTap(selected); } else { tapListener.onEntitySingleTap(null); + model.setSelected(null); } return true; } diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/SelectableRenderer.kt b/image-editor/lib/src/main/java/org/signal/imageeditor/core/SelectableRenderer.kt index 3d16cb2153..30748c6cce 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/SelectableRenderer.kt +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/SelectableRenderer.kt @@ -1,8 +1,15 @@ package org.signal.imageeditor.core +import android.graphics.RectF + /** * Renderer that can maintain a "selected" state */ interface SelectableRenderer : Renderer { fun onSelected(selected: Boolean) + + /** + * Get the sub bounds in local coordinates in case the selection should be shown smaller than full bounds + */ + fun getSelectionBounds(bounds: RectF) } diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java index 1a2f76a8e2..07a2c447ef 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/ThumbDragEditSession.java @@ -10,18 +10,33 @@ import org.signal.imageeditor.core.model.ThumbRenderer; class ThumbDragEditSession extends ElementEditSession { - @NonNull - private final ThumbRenderer.ControlPoint controlPoint; + private final PointF oppositeControlPoint = new PointF(); + private final float[] oppositeControlPointOnControlParent = new float[2]; + private final float[] oppositeControlPointOnElement = new float[2]; - private ThumbDragEditSession(@NonNull EditorElement selected, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull Matrix inverseMatrix) { + @NonNull + private final ThumbRenderer.ControlPoint controlPoint; + @NonNull private final Matrix thumbContainerRelativeMatrix; + + private ThumbDragEditSession(@NonNull EditorElement selected, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull Matrix inverseMatrix, + @NonNull Matrix thumbContainerRelativeMatrix) + { super(selected, inverseMatrix); - this.controlPoint = controlPoint; + this.controlPoint = controlPoint; + this.thumbContainerRelativeMatrix = thumbContainerRelativeMatrix; } - static EditSession startDrag(@NonNull EditorElement selected, @NonNull Matrix inverseViewModelMatrix, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull PointF point) { + static EditSession startDrag(@NonNull EditorElement selected, + @NonNull Matrix inverseViewModelMatrix, + @NonNull Matrix thumbContainerRelativeMatrix, + @NonNull ThumbRenderer.ControlPoint controlPoint, + @NonNull PointF point) + { if (!selected.getFlags().isEditable()) return null; - ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix); + ElementEditSession elementDragEditSession = new ThumbDragEditSession(selected, controlPoint, inverseViewModelMatrix, thumbContainerRelativeMatrix); elementDragEditSession.setScreenStartPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point); return elementDragEditSession; @@ -35,8 +50,16 @@ class ThumbDragEditSession extends ElementEditSession { editorMatrix.reset(); - float x = controlPoint.opposite().getX(); - float y = controlPoint.opposite().getY(); + // Think of this process as a pinch to zoom/rotate, one finger being on the control point being manipulated, and the other on its opposite. + // Even if the opposite thumb doesn't exist on the tree, the position it would be at gives the virtual second finger position for the pinch. + + // The opposite control point needs an additional mapping to put it in to the same coordinate system as the dragged thumb + oppositeControlPointOnControlParent[0] = controlPoint.opposite().getX(); + oppositeControlPointOnControlParent[1] = controlPoint.opposite().getY(); + thumbContainerRelativeMatrix.mapPoints(oppositeControlPointOnElement, oppositeControlPointOnControlParent); + float x = oppositeControlPointOnElement[0]; + float y = oppositeControlPointOnElement[1]; + oppositeControlPoint.set(x, y); float dx = endPointElement[0].x - startPointElement[0].x; float dy = endPointElement[0].y - startPointElement[0].y; @@ -44,17 +67,25 @@ class ThumbDragEditSession extends ElementEditSession { float xEnd = controlPoint.getX() + dx; float yEnd = controlPoint.getY() + dy; - boolean aspectLocked = selected.getFlags().isAspectLocked() && !controlPoint.isCenter(); + if (controlPoint.isScaleAndRotateThumb()) { + float scale = findScale(oppositeControlPoint, startPointElement[0], endPointElement[0]); + editorMatrix.postTranslate(-oppositeControlPoint.x, -oppositeControlPoint.y); + editorMatrix.postScale(scale, scale); + double angle = angle(endPointElement[0], oppositeControlPoint) - angle(startPointElement[0], oppositeControlPoint); + rotate(editorMatrix, angle); + editorMatrix.postTranslate(oppositeControlPoint.x, oppositeControlPoint.y); + } else { + // 8 point controls, where edges scale in just one dimension and corners scale in both, optionally fixed aspect ratio + boolean aspectLocked = selected.getFlags().isAspectLocked() && !controlPoint.isCenter(); + float defaultScale = aspectLocked ? 2 : 1; + float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); + float scaleY = controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); - float defaultScale = aspectLocked ? 2 : 1; - - float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); - float scaleY = controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); - - scale(editorMatrix, aspectLocked, scaleX, scaleY, controlPoint.opposite()); + scale(editorMatrix, aspectLocked, scaleX, scaleY, controlPoint.opposite()); + } } - private void scale(Matrix editorMatrix, boolean aspectLocked, float scaleX, float scaleY, ThumbRenderer.ControlPoint around) { + private static void scale(Matrix editorMatrix, boolean aspectLocked, float scaleX, float scaleY, @NonNull ThumbRenderer.ControlPoint around) { float x = around.getX(); float y = around.getY(); editorMatrix.postTranslate(-x, -y); @@ -67,6 +98,14 @@ class ThumbDragEditSession extends ElementEditSession { editorMatrix.postTranslate(x, y); } + private static void rotate(Matrix editorMatrix, double angle) { + editorMatrix.postRotate((float) Math.toDegrees(angle)); + } + + private static double angle(@NonNull PointF a, @NonNull PointF b) { + return Math.atan2(a.y - b.y, a.x - b.x); + } + @Override public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) { return null; @@ -76,4 +115,31 @@ class ThumbDragEditSession extends ElementEditSession { public EditSession removePoint(@NonNull Matrix newInverse, int p) { return null; } + + /** + * Find relative distance between an old and new Point relative to an anchor. + *

+ *

+   * |to - anchor| / |from - anchor|
+   * 
+ * + * @param anchor Fixed point. + * @param from Starting point. + * @param to Ending point. + * @return Scale required to scale a line anchor->from to reach the to point from anchor. + */ + private static float findScale(@NonNull PointF anchor, @NonNull PointF from, @NonNull PointF to) { + float originalD2 = getDistanceSquared(from, anchor); + float newD2 = getDistanceSquared(to, anchor); + return (float) Math.sqrt(newD2 / originalD2); + } + + /** + * Distance between two points squared. + */ + private static float getDistanceSquared(@NonNull PointF a, @NonNull PointF b) { + float dx = a.x - b.x; + float dy = a.y - b.y; + return dx * dx + dy * dy; + } } \ No newline at end of file diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElement.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElement.java index 978ff371f9..5d1f1356b7 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElement.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElement.java @@ -212,6 +212,34 @@ public final class EditorElement implements Parcelable { } } + public @Nullable EditorElement findParent(@NonNull EditorElement editorElement) { + for (EditorElement child : children) { + if (child == editorElement) { + return this; + } else { + EditorElement element = child.findParent(editorElement); + if (element != null) { + return element; + } + } + } + return null; + } + + public @Nullable EditorElement findElementWithId(@NonNull UUID id) { + for (EditorElement child : children) { + if (id.equals(child.id)) { + return child; + } else { + EditorElement element = child.findElementWithId(id); + if (element != null) { + return element; + } + } + } + return null; + } + void deleteChild(@NonNull EditorElement editorElement, @Nullable Runnable invalidate) { Iterator iterator = children.iterator(); while (iterator.hasNext()) { @@ -267,6 +295,10 @@ public final class EditorElement implements Parcelable { return zOrder; } + public void deleteAllChildren() { + children.clear(); + } + public interface PerElementFunction { void apply(EditorElement element); } diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElementHierarchy.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElementHierarchy.java index b8c9a43eb8..e274d3150d 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElementHierarchy.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorElementHierarchy.java @@ -10,15 +10,18 @@ import androidx.annotation.Nullable; import org.signal.imageeditor.core.Bounds; import org.signal.imageeditor.R; +import org.signal.imageeditor.core.SelectableRenderer; import org.signal.imageeditor.core.renderers.CropAreaRenderer; import org.signal.imageeditor.core.renderers.FillRenderer; import org.signal.imageeditor.core.renderers.InverseFillRenderer; import org.signal.imageeditor.core.renderers.OvalGuideRenderer; +import org.signal.imageeditor.core.renderers.SelectedElementGuideRenderer; import org.signal.imageeditor.core.renderers.TrashRenderer; /** * Creates and handles a strict EditorElement Hierarchy. *

+ *

  * root - always square, contains only temporary zooms for editing. e.g. when the whole editor zooms out for cropping
  * |
  * |- view - contains persisted adjustments for crops
@@ -44,6 +47,9 @@ import org.signal.imageeditor.core.renderers.TrashRenderer;
  * |     |  |  |  |  |- Top right thumb
  * |     |  |  |  |  |- Bottom left thumb
  * |     |  |  |  |  |- Bottom right thumb
+ * |     |  |- selection - matches the aspect and overall matrix of the selected item's selectedBounds
+ * |     |  |  |- Selection thumbs
+ * 
*/ final class EditorElementHierarchy { @@ -74,6 +80,9 @@ final class EditorElementHierarchy { private final EditorElement fade; private final EditorElement trash; private final EditorElement thumbs; + private final EditorElement selection; + + private EditorElement selectedElement; private EditorElementHierarchy(@NonNull EditorElement root) { this.root = root; @@ -82,6 +91,7 @@ final class EditorElementHierarchy { this.imageRoot = this.flipRotate.getChild(0); this.overlay = this.flipRotate.getChild(1); this.imageCrop = this.overlay.getChild(0); + this.selection = this.overlay.getChild(1); this.cropEditorElement = this.imageCrop.getChild(0); this.blackout = this.cropEditorElement.getChild(0); this.thumbs = this.cropEditorElement.getChild(1); @@ -124,6 +134,9 @@ final class EditorElementHierarchy { EditorElement imageCrop = new EditorElement(null); overlay.addElement(imageCrop); + EditorElement selection = new EditorElement(null); + overlay.addElement(selection); + boolean renderCenterThumbs = cropStyle == CropStyle.RECTANGLE; EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, renderCenterThumbs)); @@ -203,6 +216,60 @@ final class EditorElementHierarchy { return thumbs; } + void removeAllSelectionArtifacts() { + selection.deleteAllChildren(); + selectedElement = null; + } + + void setOrUpdateSelectionThumbsForElement(@NonNull EditorElement element, @Nullable Matrix overlayMappingMatrix) { + if (selectedElement != element) { + removeAllSelectionArtifacts(); + + if (element.getRenderer() instanceof SelectableRenderer) { + selectedElement = element; + } else { + selectedElement = null; + } + + if (selectedElement == null) return; + + selection.addElement(createSelectionBox()); + selection.addElement(createScaleControlThumb(element)); + selection.addElement(createRotateControlThumb(element)); + } + + if (overlayMappingMatrix != null) { + Matrix selectionMatrix = selection.getLocalMatrix(); + + if (selectedElement.getRenderer() instanceof SelectableRenderer) { + SelectableRenderer renderer = (SelectableRenderer) selectedElement.getRenderer(); + RectF bounds = new RectF(); + renderer.getSelectionBounds(bounds); + selectionMatrix.setRectToRect(Bounds.FULL_BOUNDS, bounds, Matrix.ScaleToFit.FILL); + } + + selectionMatrix.postConcat(overlayMappingMatrix); + } + } + + private static @NonNull EditorElement createSelectionBox() { + return new EditorElement(new SelectedElementGuideRenderer()); + } + + private static @NonNull EditorElement createScaleControlThumb(@NonNull EditorElement element) { + ThumbRenderer.ControlPoint controlPoint = ThumbRenderer.ControlPoint.SCALE_ROT_RIGHT; + EditorElement thumbElement = new EditorElement(new CropThumbRenderer(controlPoint, element.getId())); + thumbElement.getLocalMatrix().preTranslate(controlPoint.getX(), controlPoint.getY()); + return thumbElement; + } + + private static @NonNull EditorElement createRotateControlThumb(@NonNull EditorElement element) { + ThumbRenderer.ControlPoint controlPoint = ThumbRenderer.ControlPoint.SCALE_ROT_LEFT; + EditorElement rotateThumbElement = new EditorElement(new CropThumbRenderer(controlPoint, element.getId())); + rotateThumbElement.getLocalMatrix().preTranslate(controlPoint.getX(), controlPoint.getY()); + return rotateThumbElement; + } + private static @NonNull EditorElement newThumb(@NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) { EditorElement element = new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); @@ -223,6 +290,14 @@ final class EditorElementHierarchy { return imageRoot; } + EditorElement getSelection() { + return selection; + } + + public @Nullable EditorElement getSelectedElement() { + return selectedElement; + } + EditorElement getTrash() { return trash; } diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java index cf7862c452..310881ab90 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/EditorModel.java @@ -67,6 +67,23 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { private final EditingPurpose editingPurpose; private float fixedRatio; + public void setSelected(@Nullable EditorElement editorElement) { + if (editorElement == null) { + editorElementHierarchy.removeAllSelectionArtifacts(); + } else { + Matrix overlayMappingMatrix = findRelativeMatrix(editorElement, editorElementHierarchy.getOverlay()); + editorElementHierarchy.setOrUpdateSelectionThumbsForElement(editorElement, overlayMappingMatrix); + } + } + + public void setSelectionVisible(boolean visible) { + editorElementHierarchy.getSelection() + .getFlags() + .setVisible(visible) + .setChildrenVisible(visible) + .persist(); + } + private enum EditingPurpose { IMAGE, AVATAR_CAPTURE, @@ -271,7 +288,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { final EditorElement popped = fromStack.pop(oldRootElement); if (popped != null) { - editorElementHierarchy = EditorElementHierarchy.create(popped); + setEditorElementHierarchy(EditorElementHierarchy.create(popped)); + toStack.tryPush(oldRootElement); restoreStateWithAnimations(oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); @@ -284,6 +302,13 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { } } + /** Replaces the hierarchy, maintaining any selection if possible */ + private void setEditorElementHierarchy(@NonNull EditorElementHierarchy hierarchy) { + EditorElement selectedElement = editorElementHierarchy.getSelectedElement(); + editorElementHierarchy = hierarchy; + setSelected(selectedElement != null ? findById(selectedElement.getId()) : null); + } + private static void restoreStateWithAnimations(@NonNull EditorElement fromRootElement, @NonNull EditorElement toRootElement, @NonNull Runnable onInvalidate, boolean keepEditorState) { Map fromMap = getElementMap(fromRootElement); Map toMap = getElementMap(toRootElement); @@ -572,7 +597,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { * Called as edits are underway. */ public void moving(@NonNull EditorElement editorElement) { - if (!isCropping()) return; + if (!isCropping()) { + setSelected(editorElement); + return; + } EditorElement mainImage = editorElementHierarchy.getMainImage(); EditorElement cropEditorElement = editorElementHierarchy.getCropEditorElement(); @@ -863,7 +891,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { * @param to * @return */ - @Nullable Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { + public @Nullable Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) { Matrix matrix = findElementInverseMatrix(to, new Matrix()); Matrix outOf = findElementMatrix(from, new Matrix()); @@ -910,10 +938,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready { public void delete(@NonNull EditorElement editorElement) { editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate)); + setSelected(null); } public @Nullable EditorElement findById(@NonNull UUID uuid) { - return getElementMap(getRoot()).get(uuid); + return getRoot().findElementWithId(uuid); } /** diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/ThumbRenderer.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/ThumbRenderer.java index 382657f26c..7298f5c278 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/ThumbRenderer.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/model/ThumbRenderer.java @@ -16,6 +16,7 @@ public interface ThumbRenderer extends Renderer { enum ControlPoint { + // 8 point controls CENTER_LEFT (Bounds.LEFT, Bounds.CENTRE_Y), CENTER_RIGHT (Bounds.RIGHT, Bounds.CENTRE_Y), @@ -25,7 +26,12 @@ public interface ThumbRenderer extends Renderer { TOP_LEFT (Bounds.LEFT, Bounds.TOP), TOP_RIGHT (Bounds.RIGHT, Bounds.TOP), BOTTOM_LEFT (Bounds.LEFT, Bounds.BOTTOM), - BOTTOM_RIGHT (Bounds.RIGHT, Bounds.BOTTOM); + BOTTOM_RIGHT (Bounds.RIGHT, Bounds.BOTTOM), + + // 2 point controls + SCALE_ROT_LEFT (Bounds.LEFT, Bounds.CENTRE_Y), + SCALE_ROT_RIGHT (Bounds.RIGHT, Bounds.CENTRE_Y), + ORIGIN (0, 0); private final float x; private final float y; @@ -45,14 +51,16 @@ public interface ThumbRenderer extends Renderer { public ControlPoint opposite() { switch (this) { - case CENTER_LEFT: return CENTER_RIGHT; - case CENTER_RIGHT: return CENTER_LEFT; - case TOP_CENTER: return BOTTOM_CENTER; - case BOTTOM_CENTER: return TOP_CENTER; - case TOP_LEFT: return BOTTOM_RIGHT; - case TOP_RIGHT: return BOTTOM_LEFT; - case BOTTOM_LEFT: return TOP_RIGHT; - case BOTTOM_RIGHT: return TOP_LEFT; + case CENTER_LEFT: return CENTER_RIGHT; + case CENTER_RIGHT: return CENTER_LEFT; + case TOP_CENTER: return BOTTOM_CENTER; + case BOTTOM_CENTER: return TOP_CENTER; + case TOP_LEFT: return BOTTOM_RIGHT; + case TOP_RIGHT: return BOTTOM_LEFT; + case BOTTOM_LEFT: return TOP_RIGHT; + case BOTTOM_RIGHT: return TOP_LEFT; + case SCALE_ROT_LEFT: + case SCALE_ROT_RIGHT: return ORIGIN; default: throw new RuntimeException(); } @@ -69,6 +77,10 @@ public interface ThumbRenderer extends Renderer { public boolean isCenter() { return isHorizontalCenter() || isVerticalCenter(); } + + public boolean isScaleAndRotateThumb() { + return this == SCALE_ROT_LEFT || this == SCALE_ROT_RIGHT; + } } ControlPoint getControlPoint(); diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java index d65d45ae28..2122b19929 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/MultiLineTextRenderer.java @@ -42,6 +42,8 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen @NonNull private String text = ""; + private static final int PADDING = 10; + @ColorInt private int color; @@ -54,7 +56,6 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private int selStart; private int selEnd; private boolean hasFocus; - private boolean selected; private Mode mode; private List lines = emptyList(); @@ -64,8 +65,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private final Matrix recommendedEditorMatrix = new Matrix(); - private final SelectedElementGuideRenderer selectedElementGuideRenderer = new SelectedElementGuideRenderer(); - private final RectF textBounds = new RectF(); + private final RectF textBounds = new RectF(); public MultiLineTextRenderer(@Nullable String text, @ColorInt int color, @NonNull Mode mode) { this.mode = mode; @@ -104,10 +104,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen width = Math.max(line.textBounds.width(), width); } - if (selected && rendererContext.isEditing()) { - textBounds.set(-width, -height / 2f, width, 0f); - selectedElementGuideRenderer.render(rendererContext, textBounds); - } + textBounds.set(-width - PADDING, -PADDING, width + PADDING, height / 2f + PADDING); } @NonNull @@ -399,19 +396,16 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen @Override public void onSelected(boolean selected) { - if (this.selected != selected) { - this.selected = selected; - } + } + + @Override + public void getSelectionBounds(@NonNull RectF bounds) { + bounds.set(textBounds); } @Override public boolean hitTest(float x, float y) { - for (Line line : lines) { - y += line.ascentInBounds; - if (line.contains(x, y)) return true; - y -= line.descentInBounds; - } - return false; + return textBounds.contains(x, y); } public void setSelection(int selStart, int selEnd) { diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/SelectedElementGuideRenderer.kt b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/SelectedElementGuideRenderer.kt index 14746c0350..190cee2dcf 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/SelectedElementGuideRenderer.kt +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/SelectedElementGuideRenderer.kt @@ -4,16 +4,14 @@ import android.graphics.Color import android.graphics.DashPathEffect import android.graphics.Paint import android.graphics.Path -import android.graphics.RectF import org.signal.core.util.DimensionUnit +import android.os.Parcel +import android.os.Parcelable import org.signal.imageeditor.core.Bounds +import org.signal.imageeditor.core.Renderer import org.signal.imageeditor.core.RendererContext -class SelectedElementGuideRenderer { - - companion object { - private const val PADDING: Int = 10 - } +class SelectedElementGuideRenderer : Renderer { private val allPointsOnScreen = FloatArray(8) private val allPointsInLocalCords = floatArrayOf( @@ -46,28 +44,12 @@ class SelectedElementGuideRenderer { * * @param rendererContext The context to draw to. */ - fun render(rendererContext: RendererContext) { + override fun render(rendererContext: RendererContext) { rendererContext.canvasMatrix.mapPoints(allPointsOnScreen, allPointsInLocalCords) performRender(rendererContext) } - fun render(rendererContext: RendererContext, contentBounds: RectF) { - rendererContext.canvasMatrix.mapPoints( - allPointsOnScreen, - floatArrayOf( - contentBounds.left - PADDING, - contentBounds.top - PADDING, - contentBounds.right + PADDING, - contentBounds.top - PADDING, - contentBounds.right + PADDING, - contentBounds.bottom + PADDING, - contentBounds.left - PADDING, - contentBounds.bottom + PADDING - ) - ) - - performRender(rendererContext) - } + override fun hitTest(x: Float, y: Float): Boolean = false private fun performRender(rendererContext: RendererContext) { rendererContext.save() @@ -82,20 +64,36 @@ class SelectedElementGuideRenderer { path.close() rendererContext.canvas.drawPath(path, guidePaint) - // TODO: Implement scaling -// rendererContext.canvas.drawCircle( -// (allPointsOnScreen[6] + allPointsOnScreen[0]) / 2f, -// (allPointsOnScreen[7] + allPointsOnScreen[1]) / 2f, -// circleRadius, -// circlePaint -// ) -// rendererContext.canvas.drawCircle( -// (allPointsOnScreen[4] + allPointsOnScreen[2]) / 2f, -// (allPointsOnScreen[5] + allPointsOnScreen[3]) / 2f, -// circleRadius, -// circlePaint -// ) + rendererContext.canvas.drawCircle( + (allPointsOnScreen[6] + allPointsOnScreen[0]) / 2f, + (allPointsOnScreen[7] + allPointsOnScreen[1]) / 2f, + circleRadius, + circlePaint + ) + rendererContext.canvas.drawCircle( + (allPointsOnScreen[4] + allPointsOnScreen[2]) / 2f, + (allPointsOnScreen[5] + allPointsOnScreen[3]) / 2f, + circleRadius, + circlePaint + ) rendererContext.restore() } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SelectedElementGuideRenderer { + return SelectedElementGuideRenderer() + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } }