Two point thumb control for scale and rotate.

fork-5.53.8
Alan Evans 2021-09-15 16:32:06 -03:00 zatwierdzone przez Alex Hart
rodzic 1031a4e96c
commit 4569011e0b
10 zmienionych plików z 318 dodań i 93 usunięć

Wyświetl plik

@ -30,7 +30,6 @@ import org.signal.imageeditor.core.RendererContext;
import org.signal.imageeditor.core.SelectableRenderer; import org.signal.imageeditor.core.SelectableRenderer;
import org.signal.imageeditor.core.model.EditorElement; import org.signal.imageeditor.core.model.EditorElement;
import org.signal.imageeditor.core.model.EditorModel; 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.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest; import org.thoughtcrime.securesms.mms.GlideRequest;
@ -66,8 +65,6 @@ public final class UriGlideRenderer implements SelectableRenderer {
private boolean selected; private boolean selected;
private final SelectedElementGuideRenderer selectedElementGuideRenderer = new SelectedElementGuideRenderer();
@Nullable private Bitmap bitmap; @Nullable private Bitmap bitmap;
@Nullable private Bitmap blurredBitmap; @Nullable private Bitmap blurredBitmap;
@Nullable private Paint blurPaint; @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. // 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); rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
} }
if (selected && rendererContext.isEditing()) {
selectedElementGuideRenderer.render(rendererContext);
}
} }
private void renderBlurOverlay(RendererContext rendererContext) { private void renderBlurOverlay(RendererContext rendererContext) {
@ -207,7 +200,7 @@ public final class UriGlideRenderer implements SelectableRenderer {
@Override @Override
public boolean hitTest(float x, float y) { 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) { private boolean pixelAlphaNotZero(float x, float y) {
@ -339,4 +332,9 @@ public final class UriGlideRenderer implements SelectableRenderer {
this.selected = selected; this.selected = selected;
} }
} }
@Override
public void getSelectionBounds(@NonNull RectF bounds) {
bounds.set(Bounds.FULL_BOUNDS);
}
} }

Wyświetl plik

@ -121,6 +121,7 @@ public final class ImageEditorView extends FrameLayout {
public void startTextEditing(@NonNull EditorElement editorElement) { public void startTextEditing(@NonNull EditorElement editorElement) {
getModel().addFade(); getModel().addFade();
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) { if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
getModel().setSelectionVisible(false);
editText.setCurrentTextEditorElement(editorElement); editText.setCurrentTextEditorElement(editorElement);
} }
} }
@ -136,7 +137,9 @@ public final class ImageEditorView extends FrameLayout {
public void doneTextEditing() { public void doneTextEditing() {
getModel().zoomOut(); getModel().zoomOut();
getModel().removeFade(); getModel().removeFade();
getModel().setSelectionVisible(true);
if (editText.getCurrentTextEntity() != null) { if (editText.getCurrentTextEntity() != null) {
getModel().setSelected(null);
editText.setCurrentTextEditorElement(null); editText.setCurrentTextEditorElement(null);
editText.hideKeyboard(); editText.hideKeyboard();
} }
@ -391,13 +394,22 @@ public final class ImageEditorView extends FrameLayout {
if (selected.getRenderer() instanceof ThumbRenderer) { if (selected.getRenderer() instanceof ThumbRenderer) {
ThumbRenderer thumb = (ThumbRenderer) selected.getRenderer(); 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); elementInverseMatrix = model.findElementInverseMatrix(selected, viewMatrix);
if (elementInverseMatrix != null) { if (elementInverseMatrix != null) {
return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumb.getControlPoint(), point); return ThumbDragEditSession.startDrag(selected, elementInverseMatrix, thumbContainerRelativeMatrix, thumb.getControlPoint(), point);
} else { } else {
return null; return null;
} }
@ -501,9 +513,11 @@ public final class ImageEditorView extends FrameLayout {
if (editSession != null) { if (editSession != null) {
EditorElement selected = editSession.getSelected(); EditorElement selected = editSession.getSelected();
model.indicateSelected(selected); model.indicateSelected(selected);
model.setSelected(selected);
tapListener.onEntitySingleTap(selected); tapListener.onEntitySingleTap(selected);
} else { } else {
tapListener.onEntitySingleTap(null); tapListener.onEntitySingleTap(null);
model.setSelected(null);
} }
return true; return true;
} }

Wyświetl plik

@ -1,8 +1,15 @@
package org.signal.imageeditor.core package org.signal.imageeditor.core
import android.graphics.RectF
/** /**
* Renderer that can maintain a "selected" state * Renderer that can maintain a "selected" state
*/ */
interface SelectableRenderer : Renderer { interface SelectableRenderer : Renderer {
fun onSelected(selected: Boolean) 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)
} }

Wyświetl plik

@ -10,18 +10,33 @@ import org.signal.imageeditor.core.model.ThumbRenderer;
class ThumbDragEditSession extends ElementEditSession { class ThumbDragEditSession extends ElementEditSession {
private final PointF oppositeControlPoint = new PointF();
private final float[] oppositeControlPointOnControlParent = new float[2];
private final float[] oppositeControlPointOnElement = new float[2];
@NonNull @NonNull
private final ThumbRenderer.ControlPoint controlPoint; private final ThumbRenderer.ControlPoint controlPoint;
@NonNull private final Matrix thumbContainerRelativeMatrix;
private ThumbDragEditSession(@NonNull EditorElement selected, @NonNull ThumbRenderer.ControlPoint controlPoint, @NonNull Matrix inverseMatrix) { private ThumbDragEditSession(@NonNull EditorElement selected,
@NonNull ThumbRenderer.ControlPoint controlPoint,
@NonNull Matrix inverseMatrix,
@NonNull Matrix thumbContainerRelativeMatrix)
{
super(selected, inverseMatrix); 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; 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.setScreenStartPoint(0, point);
elementDragEditSession.setScreenEndPoint(0, point); elementDragEditSession.setScreenEndPoint(0, point);
return elementDragEditSession; return elementDragEditSession;
@ -35,8 +50,16 @@ class ThumbDragEditSession extends ElementEditSession {
editorMatrix.reset(); editorMatrix.reset();
float x = controlPoint.opposite().getX(); // 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.
float y = controlPoint.opposite().getY(); // 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 dx = endPointElement[0].x - startPointElement[0].x;
float dy = endPointElement[0].y - startPointElement[0].y; float dy = endPointElement[0].y - startPointElement[0].y;
@ -44,17 +67,25 @@ class ThumbDragEditSession extends ElementEditSession {
float xEnd = controlPoint.getX() + dx; float xEnd = controlPoint.getX() + dx;
float yEnd = controlPoint.getY() + dy; float yEnd = controlPoint.getY() + dy;
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(); boolean aspectLocked = selected.getFlags().isAspectLocked() && !controlPoint.isCenter();
float defaultScale = aspectLocked ? 2 : 1; float defaultScale = aspectLocked ? 2 : 1;
float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x); float scaleX = controlPoint.isVerticalCenter() ? defaultScale : (xEnd - x) / (controlPoint.getX() - x);
float scaleY = controlPoint.isHorizontalCenter() ? defaultScale : (yEnd - y) / (controlPoint.getY() - y); 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 x = around.getX();
float y = around.getY(); float y = around.getY();
editorMatrix.postTranslate(-x, -y); editorMatrix.postTranslate(-x, -y);
@ -67,6 +98,14 @@ class ThumbDragEditSession extends ElementEditSession {
editorMatrix.postTranslate(x, y); 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 @Override
public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) { public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) {
return null; return null;
@ -76,4 +115,31 @@ class ThumbDragEditSession extends ElementEditSession {
public EditSession removePoint(@NonNull Matrix newInverse, int p) { public EditSession removePoint(@NonNull Matrix newInverse, int p) {
return null; return null;
} }
/**
* Find relative distance between an old and new Point relative to an anchor.
* <p>
* <pre>
* |to - anchor| / |from - anchor|
* </pre>
*
* @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;
}
} }

Wyświetl plik

@ -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) { void deleteChild(@NonNull EditorElement editorElement, @Nullable Runnable invalidate) {
Iterator<EditorElement> iterator = children.iterator(); Iterator<EditorElement> iterator = children.iterator();
while (iterator.hasNext()) { while (iterator.hasNext()) {
@ -267,6 +295,10 @@ public final class EditorElement implements Parcelable {
return zOrder; return zOrder;
} }
public void deleteAllChildren() {
children.clear();
}
public interface PerElementFunction { public interface PerElementFunction {
void apply(EditorElement element); void apply(EditorElement element);
} }

Wyświetl plik

@ -10,15 +10,18 @@ import androidx.annotation.Nullable;
import org.signal.imageeditor.core.Bounds; import org.signal.imageeditor.core.Bounds;
import org.signal.imageeditor.R; import org.signal.imageeditor.R;
import org.signal.imageeditor.core.SelectableRenderer;
import org.signal.imageeditor.core.renderers.CropAreaRenderer; import org.signal.imageeditor.core.renderers.CropAreaRenderer;
import org.signal.imageeditor.core.renderers.FillRenderer; import org.signal.imageeditor.core.renderers.FillRenderer;
import org.signal.imageeditor.core.renderers.InverseFillRenderer; import org.signal.imageeditor.core.renderers.InverseFillRenderer;
import org.signal.imageeditor.core.renderers.OvalGuideRenderer; import org.signal.imageeditor.core.renderers.OvalGuideRenderer;
import org.signal.imageeditor.core.renderers.SelectedElementGuideRenderer;
import org.signal.imageeditor.core.renderers.TrashRenderer; import org.signal.imageeditor.core.renderers.TrashRenderer;
/** /**
* Creates and handles a strict EditorElement Hierarchy. * Creates and handles a strict EditorElement Hierarchy.
* <p> * <p>
* <pre>
* root - always square, contains only temporary zooms for editing. e.g. when the whole editor zooms out for cropping * 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 * |- view - contains persisted adjustments for crops
@ -44,6 +47,9 @@ import org.signal.imageeditor.core.renderers.TrashRenderer;
* | | | | | |- Top right thumb * | | | | | |- Top right thumb
* | | | | | |- Bottom left thumb * | | | | | |- Bottom left thumb
* | | | | | |- Bottom right thumb * | | | | | |- Bottom right thumb
* | | |- selection - matches the aspect and overall matrix of the selected item's selectedBounds
* | | | |- Selection thumbs
* </pre>
*/ */
final class EditorElementHierarchy { final class EditorElementHierarchy {
@ -74,6 +80,9 @@ final class EditorElementHierarchy {
private final EditorElement fade; private final EditorElement fade;
private final EditorElement trash; private final EditorElement trash;
private final EditorElement thumbs; private final EditorElement thumbs;
private final EditorElement selection;
private EditorElement selectedElement;
private EditorElementHierarchy(@NonNull EditorElement root) { private EditorElementHierarchy(@NonNull EditorElement root) {
this.root = root; this.root = root;
@ -82,6 +91,7 @@ final class EditorElementHierarchy {
this.imageRoot = this.flipRotate.getChild(0); this.imageRoot = this.flipRotate.getChild(0);
this.overlay = this.flipRotate.getChild(1); this.overlay = this.flipRotate.getChild(1);
this.imageCrop = this.overlay.getChild(0); this.imageCrop = this.overlay.getChild(0);
this.selection = this.overlay.getChild(1);
this.cropEditorElement = this.imageCrop.getChild(0); this.cropEditorElement = this.imageCrop.getChild(0);
this.blackout = this.cropEditorElement.getChild(0); this.blackout = this.cropEditorElement.getChild(0);
this.thumbs = this.cropEditorElement.getChild(1); this.thumbs = this.cropEditorElement.getChild(1);
@ -124,6 +134,9 @@ final class EditorElementHierarchy {
EditorElement imageCrop = new EditorElement(null); EditorElement imageCrop = new EditorElement(null);
overlay.addElement(imageCrop); overlay.addElement(imageCrop);
EditorElement selection = new EditorElement(null);
overlay.addElement(selection);
boolean renderCenterThumbs = cropStyle == CropStyle.RECTANGLE; boolean renderCenterThumbs = cropStyle == CropStyle.RECTANGLE;
EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, renderCenterThumbs)); EditorElement cropEditorElement = new EditorElement(new CropAreaRenderer(R.color.crop_area_renderer_outer_color, renderCenterThumbs));
@ -203,6 +216,60 @@ final class EditorElementHierarchy {
return thumbs; 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) { private static @NonNull EditorElement newThumb(@NonNull EditorElement toControl, @NonNull ThumbRenderer.ControlPoint controlPoint) {
EditorElement element = new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId())); EditorElement element = new EditorElement(new CropThumbRenderer(controlPoint, toControl.getId()));
@ -223,6 +290,14 @@ final class EditorElementHierarchy {
return imageRoot; return imageRoot;
} }
EditorElement getSelection() {
return selection;
}
public @Nullable EditorElement getSelectedElement() {
return selectedElement;
}
EditorElement getTrash() { EditorElement getTrash() {
return trash; return trash;
} }

Wyświetl plik

@ -67,6 +67,23 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
private final EditingPurpose editingPurpose; private final EditingPurpose editingPurpose;
private float fixedRatio; 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 { private enum EditingPurpose {
IMAGE, IMAGE,
AVATAR_CAPTURE, AVATAR_CAPTURE,
@ -271,7 +288,8 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
final EditorElement popped = fromStack.pop(oldRootElement); final EditorElement popped = fromStack.pop(oldRootElement);
if (popped != null) { if (popped != null) {
editorElementHierarchy = EditorElementHierarchy.create(popped); setEditorElementHierarchy(EditorElementHierarchy.create(popped));
toStack.tryPush(oldRootElement); toStack.tryPush(oldRootElement);
restoreStateWithAnimations(oldRootElement, editorElementHierarchy.getRoot(), invalidate, keepEditorState); 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) { private static void restoreStateWithAnimations(@NonNull EditorElement fromRootElement, @NonNull EditorElement toRootElement, @NonNull Runnable onInvalidate, boolean keepEditorState) {
Map<UUID, EditorElement> fromMap = getElementMap(fromRootElement); Map<UUID, EditorElement> fromMap = getElementMap(fromRootElement);
Map<UUID, EditorElement> toMap = getElementMap(toRootElement); Map<UUID, EditorElement> toMap = getElementMap(toRootElement);
@ -572,7 +597,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
* Called as edits are underway. * Called as edits are underway.
*/ */
public void moving(@NonNull EditorElement editorElement) { public void moving(@NonNull EditorElement editorElement) {
if (!isCropping()) return; if (!isCropping()) {
setSelected(editorElement);
return;
}
EditorElement mainImage = editorElementHierarchy.getMainImage(); EditorElement mainImage = editorElementHierarchy.getMainImage();
EditorElement cropEditorElement = editorElementHierarchy.getCropEditorElement(); EditorElement cropEditorElement = editorElementHierarchy.getCropEditorElement();
@ -863,7 +891,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
* @param to * @param to
* @return * @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 matrix = findElementInverseMatrix(to, new Matrix());
Matrix outOf = findElementMatrix(from, 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) { public void delete(@NonNull EditorElement editorElement) {
editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate)); editorElementHierarchy.getImageRoot().forAllInTree(element -> element.deleteChild(editorElement, invalidate));
setSelected(null);
} }
public @Nullable EditorElement findById(@NonNull UUID uuid) { public @Nullable EditorElement findById(@NonNull UUID uuid) {
return getElementMap(getRoot()).get(uuid); return getRoot().findElementWithId(uuid);
} }
/** /**

Wyświetl plik

@ -16,6 +16,7 @@ public interface ThumbRenderer extends Renderer {
enum ControlPoint { enum ControlPoint {
// 8 point controls
CENTER_LEFT (Bounds.LEFT, Bounds.CENTRE_Y), CENTER_LEFT (Bounds.LEFT, Bounds.CENTRE_Y),
CENTER_RIGHT (Bounds.RIGHT, 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_LEFT (Bounds.LEFT, Bounds.TOP),
TOP_RIGHT (Bounds.RIGHT, Bounds.TOP), TOP_RIGHT (Bounds.RIGHT, Bounds.TOP),
BOTTOM_LEFT (Bounds.LEFT, Bounds.BOTTOM), 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 x;
private final float y; private final float y;
@ -53,6 +59,8 @@ public interface ThumbRenderer extends Renderer {
case TOP_RIGHT: return BOTTOM_LEFT; case TOP_RIGHT: return BOTTOM_LEFT;
case BOTTOM_LEFT: return TOP_RIGHT; case BOTTOM_LEFT: return TOP_RIGHT;
case BOTTOM_RIGHT: return TOP_LEFT; case BOTTOM_RIGHT: return TOP_LEFT;
case SCALE_ROT_LEFT:
case SCALE_ROT_RIGHT: return ORIGIN;
default: default:
throw new RuntimeException(); throw new RuntimeException();
} }
@ -69,6 +77,10 @@ public interface ThumbRenderer extends Renderer {
public boolean isCenter() { public boolean isCenter() {
return isHorizontalCenter() || isVerticalCenter(); return isHorizontalCenter() || isVerticalCenter();
} }
public boolean isScaleAndRotateThumb() {
return this == SCALE_ROT_LEFT || this == SCALE_ROT_RIGHT;
}
} }
ControlPoint getControlPoint(); ControlPoint getControlPoint();

Wyświetl plik

@ -42,6 +42,8 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen
@NonNull @NonNull
private String text = ""; private String text = "";
private static final int PADDING = 10;
@ColorInt @ColorInt
private int color; private int color;
@ -54,7 +56,6 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen
private int selStart; private int selStart;
private int selEnd; private int selEnd;
private boolean hasFocus; private boolean hasFocus;
private boolean selected;
private Mode mode; private Mode mode;
private List<Line> lines = emptyList(); private List<Line> lines = emptyList();
@ -64,7 +65,6 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen
private final Matrix recommendedEditorMatrix = new Matrix(); 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) { public MultiLineTextRenderer(@Nullable String text, @ColorInt int color, @NonNull Mode mode) {
@ -104,10 +104,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen
width = Math.max(line.textBounds.width(), width); width = Math.max(line.textBounds.width(), width);
} }
if (selected && rendererContext.isEditing()) { textBounds.set(-width - PADDING, -PADDING, width + PADDING, height / 2f + PADDING);
textBounds.set(-width, -height / 2f, width, 0f);
selectedElementGuideRenderer.render(rendererContext, textBounds);
}
} }
@NonNull @NonNull
@ -399,19 +396,16 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen
@Override @Override
public void onSelected(boolean selected) { public void onSelected(boolean selected) {
if (this.selected != selected) {
this.selected = selected;
} }
@Override
public void getSelectionBounds(@NonNull RectF bounds) {
bounds.set(textBounds);
} }
@Override @Override
public boolean hitTest(float x, float y) { public boolean hitTest(float x, float y) {
for (Line line : lines) { return textBounds.contains(x, y);
y += line.ascentInBounds;
if (line.contains(x, y)) return true;
y -= line.descentInBounds;
}
return false;
} }
public void setSelection(int selStart, int selEnd) { public void setSelection(int selStart, int selEnd) {

Wyświetl plik

@ -4,16 +4,14 @@ import android.graphics.Color
import android.graphics.DashPathEffect import android.graphics.DashPathEffect
import android.graphics.Paint import android.graphics.Paint
import android.graphics.Path import android.graphics.Path
import android.graphics.RectF
import org.signal.core.util.DimensionUnit 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.Bounds
import org.signal.imageeditor.core.Renderer
import org.signal.imageeditor.core.RendererContext import org.signal.imageeditor.core.RendererContext
class SelectedElementGuideRenderer { class SelectedElementGuideRenderer : Renderer {
companion object {
private const val PADDING: Int = 10
}
private val allPointsOnScreen = FloatArray(8) private val allPointsOnScreen = FloatArray(8)
private val allPointsInLocalCords = floatArrayOf( private val allPointsInLocalCords = floatArrayOf(
@ -46,28 +44,12 @@ class SelectedElementGuideRenderer {
* *
* @param rendererContext The context to draw to. * @param rendererContext The context to draw to.
*/ */
fun render(rendererContext: RendererContext) { override fun render(rendererContext: RendererContext) {
rendererContext.canvasMatrix.mapPoints(allPointsOnScreen, allPointsInLocalCords) rendererContext.canvasMatrix.mapPoints(allPointsOnScreen, allPointsInLocalCords)
performRender(rendererContext) performRender(rendererContext)
} }
fun render(rendererContext: RendererContext, contentBounds: RectF) { override fun hitTest(x: Float, y: Float): Boolean = false
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)
}
private fun performRender(rendererContext: RendererContext) { private fun performRender(rendererContext: RendererContext) {
rendererContext.save() rendererContext.save()
@ -82,20 +64,36 @@ class SelectedElementGuideRenderer {
path.close() path.close()
rendererContext.canvas.drawPath(path, guidePaint) rendererContext.canvas.drawPath(path, guidePaint)
// TODO: Implement scaling rendererContext.canvas.drawCircle(
// rendererContext.canvas.drawCircle( (allPointsOnScreen[6] + allPointsOnScreen[0]) / 2f,
// (allPointsOnScreen[6] + allPointsOnScreen[0]) / 2f, (allPointsOnScreen[7] + allPointsOnScreen[1]) / 2f,
// (allPointsOnScreen[7] + allPointsOnScreen[1]) / 2f, circleRadius,
// circleRadius, circlePaint
// circlePaint )
// ) rendererContext.canvas.drawCircle(
// rendererContext.canvas.drawCircle( (allPointsOnScreen[4] + allPointsOnScreen[2]) / 2f,
// (allPointsOnScreen[4] + allPointsOnScreen[2]) / 2f, (allPointsOnScreen[5] + allPointsOnScreen[3]) / 2f,
// (allPointsOnScreen[5] + allPointsOnScreen[3]) / 2f, circleRadius,
// circleRadius, circlePaint
// circlePaint )
// )
rendererContext.restore() rendererContext.restore()
} }
override fun writeToParcel(parcel: Parcel, flags: Int) {
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SelectedElementGuideRenderer> {
override fun createFromParcel(parcel: Parcel): SelectedElementGuideRenderer {
return SelectedElementGuideRenderer()
}
override fun newArray(size: Int): Array<SelectedElementGuideRenderer?> {
return arrayOfNulls(size)
}
}
} }