Two point thumb control for scale and rotate.
This commit is contained in:
parent
1031a4e96c
commit
4569011e0b
10 changed files with 318 additions and 93 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <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;
|
||||
}
|
||||
}
|
|
@ -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<EditorElement> 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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
* <p>
|
||||
* <pre>
|
||||
* 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
|
||||
* </pre>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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<UUID, EditorElement> fromMap = getElementMap(fromRootElement);
|
||||
Map<UUID, EditorElement> 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<Line> 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) {
|
||||
|
|
|
@ -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<SelectedElementGuideRenderer> {
|
||||
override fun createFromParcel(parcel: Parcel): SelectedElementGuideRenderer {
|
||||
return SelectedElementGuideRenderer()
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<SelectedElementGuideRenderer?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue