Two point thumb control for scale and rotate.

This commit is contained in:
Alan Evans 2021-09-15 16:32:06 -03:00 committed by Alex Hart
parent 1031a4e96c
commit 4569011e0b
10 changed files with 318 additions and 93 deletions

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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)
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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;
}

View file

@ -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);
}
/**

View file

@ -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();

View file

@ -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) {

View file

@ -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)
}
}
}