mirror of
https://github.com/oxen-io/session-android.git
synced 2024-11-28 04:25:18 +00:00
Image Editor - Keep image within crop bounds.
* 4% of original pixels must be visible. * The entire crop must be within the image. * On release, try to scale crop area and image to fit if the crop is invalid. * Undo to last valid position if that didn't work. * Additionally, center thumbs now do not respect aspect ratio lock.
This commit is contained in:
parent
068ffc2167
commit
bf759711ef
@ -1,6 +1,9 @@
|
|||||||
package org.thoughtcrime.securesms.imageeditor;
|
package org.thoughtcrime.securesms.imageeditor;
|
||||||
|
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The local extent of a {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}.
|
* The local extent of a {@link org.thoughtcrime.securesms.imageeditor.model.EditorElement}.
|
||||||
@ -21,9 +24,51 @@ public final class Bounds {
|
|||||||
|
|
||||||
public static final float[] CENTRE = new float[]{ CENTRE_X, CENTRE_Y };
|
public static final float[] CENTRE = new float[]{ CENTRE_X, CENTRE_Y };
|
||||||
|
|
||||||
|
private static final float[] POINTS = { Bounds.LEFT, Bounds.TOP,
|
||||||
|
Bounds.RIGHT, Bounds.TOP,
|
||||||
|
Bounds.RIGHT, Bounds.BOTTOM,
|
||||||
|
Bounds.LEFT, Bounds.BOTTOM };
|
||||||
|
|
||||||
static RectF newFullBounds() {
|
static RectF newFullBounds() {
|
||||||
return new RectF(LEFT, TOP, RIGHT, BOTTOM);
|
return new RectF(LEFT, TOP, RIGHT, BOTTOM);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RectF FULL_BOUNDS = newFullBounds();
|
public static RectF FULL_BOUNDS = newFullBounds();
|
||||||
|
|
||||||
|
public static boolean contains(float x, float y) {
|
||||||
|
return x >= FULL_BOUNDS.left && x <= FULL_BOUNDS.right &&
|
||||||
|
y >= FULL_BOUNDS.top && y <= FULL_BOUNDS.bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps all the points of bounds with the supplied matrix and determines whether they are still in bounds.
|
||||||
|
*
|
||||||
|
* @param matrix matrix to transform points by, null is treated as identity.
|
||||||
|
* @return true iff all points remain in bounds after transformation.
|
||||||
|
*/
|
||||||
|
public static boolean boundsRemainInBounds(@Nullable Matrix matrix) {
|
||||||
|
if (matrix == null) return true;
|
||||||
|
|
||||||
|
float[] dst = new float[POINTS.length];
|
||||||
|
|
||||||
|
matrix.mapPoints(dst, POINTS);
|
||||||
|
|
||||||
|
return allWithinBounds(dst);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean allWithinBounds(@NonNull float[] points) {
|
||||||
|
boolean allHit = true;
|
||||||
|
|
||||||
|
for (int i = 0; i < points.length / 2; i++) {
|
||||||
|
float x = points[2 * i];
|
||||||
|
float y = points[2 * i + 1];
|
||||||
|
|
||||||
|
if (!Bounds.contains(x, y)) {
|
||||||
|
allHit = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allHit;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.imageeditor;
|
|||||||
|
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.graphics.PointF;
|
import android.graphics.PointF;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||||
|
|
||||||
@ -18,13 +19,13 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
|||||||
*/
|
*/
|
||||||
interface EditSession {
|
interface EditSession {
|
||||||
|
|
||||||
void movePoint(int p, PointF point);
|
void movePoint(int p, @NonNull PointF point);
|
||||||
|
|
||||||
EditorElement getSelected();
|
EditorElement getSelected();
|
||||||
|
|
||||||
EditSession newPoint(Matrix newInverse, PointF point, int p);
|
EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p);
|
||||||
|
|
||||||
EditSession removePoint(Matrix newInverse, int p);
|
EditSession removePoint(@NonNull Matrix newInverse, int p);
|
||||||
|
|
||||||
void commit();
|
void commit();
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,12 @@ final class ElementDragEditSession extends ElementEditSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession newPoint(Matrix newInverse, PointF point, int p) {
|
public EditSession newPoint(@NonNull Matrix newInverse, PointF point, int p) {
|
||||||
return ElementScaleEditSession.startScale(this, newInverse, point, p);
|
return ElementScaleEditSession.startScale(this, newInverse, point, p);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession removePoint(Matrix newInverse, int p) {
|
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
|||||||
|
|
||||||
abstract class ElementEditSession implements EditSession {
|
abstract class ElementEditSession implements EditSession {
|
||||||
|
|
||||||
private final Matrix inverseMatrix;
|
private final Matrix inverseMatrix;
|
||||||
|
|
||||||
final EditorElement selected;
|
final EditorElement selected;
|
||||||
|
|
||||||
@ -60,7 +60,7 @@ abstract class ElementEditSession implements EditSession {
|
|||||||
* @param matrix Matrix to transform point with.
|
* @param matrix Matrix to transform point with.
|
||||||
* @param src Input point.
|
* @param src Input point.
|
||||||
*/
|
*/
|
||||||
static void mapPoint(@NonNull PointF dst, @NonNull Matrix matrix, @NonNull PointF src) {
|
private static void mapPoint(@NonNull PointF dst, @NonNull Matrix matrix, @NonNull PointF src) {
|
||||||
float[] in = { src.x, src.y };
|
float[] in = { src.x, src.y };
|
||||||
float[] out = new float[2];
|
float[] out = new float[2];
|
||||||
matrix.mapPoints(out, in);
|
matrix.mapPoints(out, in);
|
||||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
|||||||
|
|
||||||
final class ElementScaleEditSession extends ElementEditSession {
|
final class ElementScaleEditSession extends ElementEditSession {
|
||||||
|
|
||||||
private ElementScaleEditSession(EditorElement selected, Matrix inverseMatrix) {
|
private ElementScaleEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix) {
|
||||||
super(selected, inverseMatrix);
|
super(selected, inverseMatrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,20 +56,20 @@ final class ElementScaleEditSession extends ElementEditSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession newPoint(Matrix newInverse, PointF point, int p) {
|
public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession removePoint(Matrix newInverse, int p) {
|
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||||
return convertToDrag(p, newInverse);
|
return convertToDrag(p, newInverse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static double angle(PointF a, PointF b) {
|
private static double angle(@NonNull PointF a, @NonNull PointF b) {
|
||||||
return Math.atan2(a.y - b.y, a.x - b.x);
|
return Math.atan2(a.y - b.y, a.x - b.x);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ElementDragEditSession convertToDrag(int p, Matrix inverse) {
|
private ElementDragEditSession convertToDrag(int p, @NonNull Matrix inverse) {
|
||||||
return ElementDragEditSession.startDrag(selected, inverse, endPointScreen[1 - p]);
|
return ElementDragEditSession.startDrag(selected, inverse, endPointScreen[1 - p]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,6 +70,7 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private EditSession editSession;
|
private EditSession editSession;
|
||||||
|
private boolean moreThanOnePointerUsedInSession;
|
||||||
|
|
||||||
public ImageEditorView(Context context) {
|
public ImageEditorView(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@ -215,6 +216,7 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
PointF point = getPoint(event);
|
PointF point = getPoint(event);
|
||||||
EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse);
|
EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse);
|
||||||
|
|
||||||
|
moreThanOnePointerUsedInSession = false;
|
||||||
model.pushUndoPoint();
|
model.pushUndoPoint();
|
||||||
editSession = startEdit(inverse, point, selected);
|
editSession = startEdit(inverse, point, selected);
|
||||||
|
|
||||||
@ -230,9 +232,19 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
case MotionEvent.ACTION_MOVE: {
|
case MotionEvent.ACTION_MOVE: {
|
||||||
if (editSession != null) {
|
if (editSession != null) {
|
||||||
for (int p = 0; p < Math.min(2, event.getPointerCount()); p++) {
|
int historySize = event.getHistorySize();
|
||||||
|
int pointerCount = Math.min(2, event.getPointerCount());
|
||||||
|
|
||||||
|
for (int h = 0; h < historySize; h++) {
|
||||||
|
for (int p = 0; p < pointerCount; p++) {
|
||||||
|
editSession.movePoint(p, getHistoricalPoint(event, p, h));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int p = 0; p < pointerCount; p++) {
|
||||||
editSession.movePoint(p, getPoint(event, p));
|
editSession.movePoint(p, getPoint(event, p));
|
||||||
}
|
}
|
||||||
|
model.moving(editSession.getSelected());
|
||||||
invalidate();
|
invalidate();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -240,11 +252,16 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
if (editSession != null && event.getPointerCount() == 2) {
|
if (editSession != null && event.getPointerCount() == 2) {
|
||||||
|
moreThanOnePointerUsedInSession = true;
|
||||||
editSession.commit();
|
editSession.commit();
|
||||||
model.pushUndoPoint();
|
model.pushUndoPoint();
|
||||||
|
|
||||||
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
||||||
editSession = editSession.newPoint(newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex());
|
if (newInverse != null) {
|
||||||
|
editSession = editSession.newPoint(newInverse, getPoint(event, event.getActionIndex()), event.getActionIndex());
|
||||||
|
} else {
|
||||||
|
editSession = null;
|
||||||
|
}
|
||||||
if (editSession == null) {
|
if (editSession == null) {
|
||||||
dragDropRelease();
|
dragDropRelease();
|
||||||
}
|
}
|
||||||
@ -259,7 +276,11 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
dragDropRelease();
|
dragDropRelease();
|
||||||
|
|
||||||
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
Matrix newInverse = model.findElementInverseMatrix(editSession.getSelected(), viewMatrix);
|
||||||
editSession = editSession.removePoint(newInverse, event.getActionIndex());
|
if (newInverse != null) {
|
||||||
|
editSession = editSession.removePoint(newInverse, event.getActionIndex());
|
||||||
|
} else {
|
||||||
|
editSession = null;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -270,8 +291,11 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
dragDropRelease();
|
dragDropRelease();
|
||||||
|
|
||||||
editSession = null;
|
editSession = null;
|
||||||
|
model.postEdit(moreThanOnePointerUsedInSession);
|
||||||
invalidate();
|
invalidate();
|
||||||
return true;
|
return true;
|
||||||
|
} else {
|
||||||
|
model.postEdit(moreThanOnePointerUsedInSession);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -349,6 +373,11 @@ public final class ImageEditorView extends FrameLayout {
|
|||||||
return new PointF(event.getX(p), event.getY(p));
|
return new PointF(event.getX(p), event.getY(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PointF getHistoricalPoint(MotionEvent event, int p, int historicalIndex) {
|
||||||
|
return new PointF(event.getHistoricalX(p, historicalIndex),
|
||||||
|
event.getHistoricalY(p, historicalIndex));
|
||||||
|
}
|
||||||
|
|
||||||
public EditorModel getModel() {
|
public EditorModel getModel() {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,10 @@ public final class RendererContext {
|
|||||||
canvasMatrix.restore();
|
canvasMatrix.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void getCurrent(@NonNull Matrix into) {
|
||||||
|
canvasMatrix.getCurrent(into);
|
||||||
|
}
|
||||||
|
|
||||||
public interface Ready {
|
public interface Ready {
|
||||||
|
|
||||||
Ready NULL = (renderer, cropMatrix, size) -> {
|
Ready NULL = (renderer, cropMatrix, size) -> {
|
||||||
|
@ -39,7 +39,7 @@ class ThumbDragEditSession extends ElementEditSession {
|
|||||||
|
|
||||||
editorMatrix.postTranslate(-x, -y);
|
editorMatrix.postTranslate(-x, -y);
|
||||||
|
|
||||||
boolean aspectLocked = selected.getFlags().isAspectLocked();
|
boolean aspectLocked = selected.getFlags().isAspectLocked() && !controlPoint.isCenter();
|
||||||
|
|
||||||
float defaultScale = aspectLocked ? 2 : 1;
|
float defaultScale = aspectLocked ? 2 : 1;
|
||||||
|
|
||||||
@ -57,12 +57,12 @@ class ThumbDragEditSession extends ElementEditSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession newPoint(Matrix newInverse, PointF point, int p) {
|
public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public EditSession removePoint(Matrix newInverse, int p) {
|
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ final class EditorElementHierarchy {
|
|||||||
|
|
||||||
private EditorElementHierarchy(@NonNull EditorElement root) {
|
private EditorElementHierarchy(@NonNull EditorElement root) {
|
||||||
this.root = root;
|
this.root = root;
|
||||||
this.view = this.root.getChild(0);
|
this.view = this.root.getChild(0);
|
||||||
this.flipRotate = this.view.getChild(0);
|
this.flipRotate = this.view.getChild(0);
|
||||||
this.imageRoot = this.flipRotate.getChild(0);
|
this.imageRoot = this.flipRotate.getChild(0);
|
||||||
this.overlay = this.flipRotate.getChild(1);
|
this.overlay = this.flipRotate.getChild(1);
|
||||||
@ -237,6 +237,30 @@ final class EditorElementHierarchy {
|
|||||||
return matrix;
|
return matrix;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a matrix that maps points from the crop on to the visible image.
|
||||||
|
* <p>
|
||||||
|
* i.e. if a mapped point is in bounds, then the point is on the visible image.
|
||||||
|
*/
|
||||||
|
@Nullable Matrix imageMatrixRelativeToCrop() {
|
||||||
|
EditorElement mainImage = getMainImage();
|
||||||
|
if (mainImage == null) return null;
|
||||||
|
|
||||||
|
Matrix matrix1 = new Matrix(imageCrop.getLocalMatrix());
|
||||||
|
matrix1.preConcat(cropEditorElement.getLocalMatrix());
|
||||||
|
matrix1.preConcat(cropEditorElement.getEditorMatrix());
|
||||||
|
|
||||||
|
Matrix matrix2 = new Matrix(mainImage.getLocalMatrix());
|
||||||
|
matrix2.preConcat(mainImage.getEditorMatrix());
|
||||||
|
matrix2.preConcat(imageCrop.getLocalMatrix());
|
||||||
|
|
||||||
|
Matrix inverse = new Matrix();
|
||||||
|
matrix2.invert(inverse);
|
||||||
|
inverse.preConcat(matrix1);
|
||||||
|
|
||||||
|
return inverse;
|
||||||
|
}
|
||||||
|
|
||||||
void dragDropRelease(@NonNull RectF visibleViewPort, @NonNull Runnable invalidate) {
|
void dragDropRelease(@NonNull RectF visibleViewPort, @NonNull Runnable invalidate) {
|
||||||
if (cropEditorElement.getFlags().isVisible()) {
|
if (cropEditorElement.getFlags().isVisible()) {
|
||||||
updateViewToCrop(visibleViewPort, invalidate);
|
updateViewToCrop(visibleViewPort, invalidate);
|
||||||
@ -299,9 +323,10 @@ final class EditorElementHierarchy {
|
|||||||
|
|
||||||
matrix.preConcat(flipRotate.getLocalMatrix());
|
matrix.preConcat(flipRotate.getLocalMatrix());
|
||||||
matrix.preConcat(cropEditorElement.getLocalMatrix());
|
matrix.preConcat(cropEditorElement.getLocalMatrix());
|
||||||
|
matrix.preConcat(cropEditorElement.getEditorMatrix());
|
||||||
EditorElement mainImage = getMainImage();
|
EditorElement mainImage = getMainImage();
|
||||||
if (mainImage != null) {
|
if (mainImage != null) {
|
||||||
float xScale = 1f / xScale(mainImage.getLocalMatrix());
|
float xScale = 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix()));
|
||||||
matrix.preScale(xScale, xScale);
|
matrix.preScale(xScale, xScale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,11 +37,14 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
|
|
||||||
private static final int MINIMUM_OUTPUT_WIDTH = 0;
|
private static final int MINIMUM_OUTPUT_WIDTH = 0;
|
||||||
|
|
||||||
|
private static final float MAXIMUM_CROP = 0.20f;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Runnable invalidate = NULL_RUNNABLE;
|
private Runnable invalidate = NULL_RUNNABLE;
|
||||||
|
|
||||||
private final UndoRedoStacks undoRedoStacks;
|
private final UndoRedoStacks undoRedoStacks;
|
||||||
private final UndoRedoStacks cropUndoRedoStacks;
|
private final UndoRedoStacks cropUndoRedoStacks;
|
||||||
|
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
||||||
|
|
||||||
private EditorElementHierarchy editorElementHierarchy;
|
private EditorElementHierarchy editorElementHierarchy;
|
||||||
|
|
||||||
@ -88,7 +91,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private @Nullable Matrix findElementMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) {
|
public @Nullable Matrix findElementMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) {
|
||||||
Matrix inverse = findElementInverseMatrix(element, viewMatrix);
|
Matrix inverse = findElementInverseMatrix(element, viewMatrix);
|
||||||
if (inverse != null) {
|
if (inverse != null) {
|
||||||
Matrix regular = new Matrix();
|
Matrix regular = new Matrix();
|
||||||
@ -107,7 +110,12 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void pushUndoPoint() {
|
public void pushUndoPoint() {
|
||||||
UndoRedoStacks stacks = isCropping() ? cropUndoRedoStacks : undoRedoStacks;
|
boolean cropping = isCropping();
|
||||||
|
if (cropping && !currentCropIsAcceptable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UndoRedoStacks stacks = cropping ? cropUndoRedoStacks : undoRedoStacks;
|
||||||
|
|
||||||
if (stacks.getUndoStack().tryPush(editorElementHierarchy.getRoot())) {
|
if (stacks.getUndoStack().tryPush(editorElementHierarchy.getRoot())) {
|
||||||
stacks.getRedoStack().clear();
|
stacks.getRedoStack().clear();
|
||||||
@ -140,12 +148,14 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
|
|
||||||
// re-zoom image root as the view port might be different now
|
// re-zoom image root as the view port might be different now
|
||||||
editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate);
|
editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate);
|
||||||
|
|
||||||
|
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
for (EditorElement fromElement : fromMap.values()) {
|
for (EditorElement fromElement : fromMap.values()) {
|
||||||
fromElement.stopAnimation();
|
fromElement.stopAnimation();
|
||||||
@ -188,6 +198,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
cropUndoRedoStacks.getUndoStack().clear();
|
cropUndoRedoStacks.getUndoStack().clear();
|
||||||
cropUndoRedoStacks.getUndoStack().clear();
|
cropUndoRedoStacks.getUndoStack().clear();
|
||||||
editorElementHierarchy.startCrop(invalidate);
|
editorElementHierarchy.startCrop(invalidate);
|
||||||
|
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
|
||||||
}
|
}
|
||||||
|
|
||||||
public void doneCrop() {
|
public void doneCrop() {
|
||||||
@ -196,9 +207,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
|
|
||||||
public void setCropAspectLock(boolean locked) {
|
public void setCropAspectLock(boolean locked) {
|
||||||
EditorFlags flags = editorElementHierarchy.getCropEditorElement().getFlags();
|
EditorFlags flags = editorElementHierarchy.getCropEditorElement().getFlags();
|
||||||
int currentState = flags.setAspectLocked(locked).getCurrentState();
|
int currentState = flags.setAspectLocked(locked).getCurrentState();
|
||||||
|
|
||||||
flags.reset();
|
flags.reset();
|
||||||
flags.setAspectLocked(locked).persist();
|
flags.setAspectLocked(locked)
|
||||||
|
.persist();
|
||||||
flags.restoreState(currentState);
|
flags.restoreState(currentState);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -206,10 +219,113 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
return editorElementHierarchy.getCropEditorElement().getFlags().isAspectLocked();
|
return editorElementHierarchy.getCropEditorElement().getFlags().isAspectLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void postEdit(boolean allowScaleToRepairCrop) {
|
||||||
|
if (isCropping()) {
|
||||||
|
ensureFitsBounds(allowScaleToRepairCrop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ensureFitsBounds(boolean allowScaleToRepairCrop) {
|
||||||
|
EditorElement mainImage = editorElementHierarchy.getMainImage();
|
||||||
|
if (mainImage == null) return;
|
||||||
|
|
||||||
|
EditorElement cropEditorElement = editorElementHierarchy.getCropEditorElement();
|
||||||
|
|
||||||
|
if (!currentCropIsAcceptable()) {
|
||||||
|
if (allowScaleToRepairCrop) {
|
||||||
|
if (!tryToScaleToFit(cropEditorElement, 0.9f)) {
|
||||||
|
tryToScaleToFit(mainImage, 2f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentCropIsAcceptable()) {
|
||||||
|
inBoundsMemory.restore(mainImage, cropEditorElement, invalidate);
|
||||||
|
} else {
|
||||||
|
inBoundsMemory.push(mainImage, cropEditorElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editorElementHierarchy.dragDropRelease(visibleViewPort, invalidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to scale the supplied element such that {@link #cropIsWithinMainImageBounds} is true.
|
||||||
|
* <p>
|
||||||
|
* Does not respect minimum scale, so does need a further check to {@link #currentCropIsAcceptable} afterwards.
|
||||||
|
*
|
||||||
|
* @param element The element to be scaled. If successful, it will be animated to the correct position.
|
||||||
|
* @param scaleAtMost The amount of scale to apply at most. Use < 1 for the crop, and > 1 for the image.
|
||||||
|
* @return true if successfully scaled the element. false if the element was left unchanged.
|
||||||
|
*/
|
||||||
|
private boolean tryToScaleToFit(@NonNull EditorElement element, float scaleAtMost) {
|
||||||
|
Matrix elementMatrix = element.getLocalMatrix();
|
||||||
|
Matrix original = new Matrix(elementMatrix);
|
||||||
|
Matrix lastSuccessful = new Matrix();
|
||||||
|
boolean success = false;
|
||||||
|
float unsuccessfulScale = 1;
|
||||||
|
int attempt = 0;
|
||||||
|
|
||||||
|
do {
|
||||||
|
float tryScale = (scaleAtMost + unsuccessfulScale) / 2f;
|
||||||
|
elementMatrix.set(original);
|
||||||
|
elementMatrix.preScale(tryScale, tryScale);
|
||||||
|
|
||||||
|
if (cropIsWithinMainImageBounds(editorElementHierarchy)) {
|
||||||
|
scaleAtMost = tryScale;
|
||||||
|
success = true;
|
||||||
|
lastSuccessful.set(elementMatrix);
|
||||||
|
} else {
|
||||||
|
unsuccessfulScale = tryScale;
|
||||||
|
}
|
||||||
|
attempt++;
|
||||||
|
} while (attempt < 16 && Math.abs(scaleAtMost - unsuccessfulScale) > 0.001f);
|
||||||
|
|
||||||
|
elementMatrix.set(original);
|
||||||
|
if (success) {
|
||||||
|
element.animateLocalTo(lastSuccessful, invalidate);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
public void dragDropRelease() {
|
public void dragDropRelease() {
|
||||||
editorElementHierarchy.dragDropRelease(visibleViewPort, invalidate);
|
editorElementHierarchy.dragDropRelease(visibleViewPort, invalidate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pixel count must be no smaller than the MAXIMUM_CROP of its original size and all points must be within the bounds.
|
||||||
|
*/
|
||||||
|
private boolean currentCropIsAcceptable() {
|
||||||
|
Point outputSize = getOutputSize();
|
||||||
|
int outputPixelCount = outputSize.x * outputSize.y;
|
||||||
|
int minimumPixelCount = (int) (size.x * size.y * MAXIMUM_CROP * MAXIMUM_CROP);
|
||||||
|
|
||||||
|
return outputPixelCount >= minimumPixelCount &&
|
||||||
|
cropIsWithinMainImageBounds(editorElementHierarchy);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return true if and only if the current crop rect is fully in the bounds.
|
||||||
|
*/
|
||||||
|
private static boolean cropIsWithinMainImageBounds(@NonNull EditorElementHierarchy hierarchy) {
|
||||||
|
return Bounds.boundsRemainInBounds(hierarchy.imageMatrixRelativeToCrop());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called as edits are underway.
|
||||||
|
*/
|
||||||
|
public void moving(@NonNull EditorElement editorElement) {
|
||||||
|
if (!isCropping()) return;
|
||||||
|
|
||||||
|
EditorElement mainImage = editorElementHierarchy.getMainImage();
|
||||||
|
EditorElement cropEditorElement = editorElementHierarchy.getCropEditorElement();
|
||||||
|
|
||||||
|
if (editorElement == mainImage || editorElement == cropEditorElement) {
|
||||||
|
if (currentCropIsAcceptable()) {
|
||||||
|
inBoundsMemory.push(mainImage, cropEditorElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void setVisibleViewPort(@NonNull RectF visibleViewPort) {
|
public void setVisibleViewPort(@NonNull RectF visibleViewPort) {
|
||||||
this.visibleViewPort.set(visibleViewPort);
|
this.visibleViewPort.set(visibleViewPort);
|
||||||
this.editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate);
|
this.editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate);
|
||||||
@ -364,23 +480,38 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
|||||||
return findCropRelativeTo(editorElementHierarchy.getRoot());
|
return findCropRelativeTo(editorElementHierarchy.getRoot());
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectF findCropRelativeTo(EditorElement element) {
|
RectF findCropRelativeTo(EditorElement element) {
|
||||||
return findRelativeBounds(editorElementHierarchy.getCropEditorElement(), element);
|
return findRelativeBounds(editorElementHierarchy.getCropEditorElement(), element);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RectF findRelativeBounds(EditorElement from, EditorElement to) {
|
RectF findRelativeBounds(EditorElement from, EditorElement to) {
|
||||||
Matrix matrix1 = findElementMatrix(from, new Matrix());
|
Matrix relative = findRelativeMatrix(from, to);
|
||||||
Matrix matrix2 = findElementInverseMatrix(to, new Matrix());
|
|
||||||
|
|
||||||
RectF dst = new RectF(Bounds.FULL_BOUNDS);
|
RectF dst = new RectF(Bounds.FULL_BOUNDS);
|
||||||
if (matrix1 != null) {
|
if (relative != null) {
|
||||||
matrix1.preConcat(matrix2);
|
relative.mapRect(dst, Bounds.FULL_BOUNDS);
|
||||||
|
|
||||||
matrix1.mapRect(dst, Bounds.FULL_BOUNDS);
|
|
||||||
}
|
}
|
||||||
return dst;
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a matrix that maps points in the {@param from} element in to points in the {@param to} element.
|
||||||
|
*
|
||||||
|
* @param from
|
||||||
|
* @param to
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
@Nullable Matrix findRelativeMatrix(@NonNull EditorElement from, @NonNull EditorElement to) {
|
||||||
|
Matrix matrix = findElementInverseMatrix(to, new Matrix());
|
||||||
|
Matrix outOf = findElementMatrix(from, new Matrix());
|
||||||
|
|
||||||
|
if (outOf != null && matrix != null) {
|
||||||
|
matrix.preConcat(outOf);
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public void rotate90clockwise() {
|
public void rotate90clockwise() {
|
||||||
pushUndoPoint();
|
pushUndoPoint();
|
||||||
editorElementHierarchy.flipRotate(90, 1, 1, visibleViewPort, invalidate);
|
editorElementHierarchy.flipRotate(90, 1, 1, visibleViewPort, invalidate);
|
||||||
|
@ -0,0 +1,30 @@
|
|||||||
|
package org.thoughtcrime.securesms.imageeditor.model;
|
||||||
|
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
final class InBoundsMemory {
|
||||||
|
|
||||||
|
private final Matrix lastGoodUserCrop = new Matrix();
|
||||||
|
private final Matrix lastGoodMainImage = new Matrix();
|
||||||
|
|
||||||
|
void push(@Nullable EditorElement mainImage, @NonNull EditorElement userCrop) {
|
||||||
|
if (mainImage == null) {
|
||||||
|
lastGoodMainImage.reset();
|
||||||
|
} else {
|
||||||
|
lastGoodMainImage.set(mainImage.getLocalMatrix());
|
||||||
|
lastGoodMainImage.preConcat(mainImage.getEditorMatrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
lastGoodUserCrop.set(userCrop.getLocalMatrix());
|
||||||
|
lastGoodUserCrop.preConcat(userCrop.getEditorMatrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
void restore(@Nullable EditorElement mainImage, @NonNull EditorElement cropEditorElement, @Nullable Runnable invalidate) {
|
||||||
|
if (mainImage != null) {
|
||||||
|
mainImage.animateLocalTo(lastGoodMainImage, invalidate);
|
||||||
|
}
|
||||||
|
cropEditorElement.animateLocalTo(lastGoodUserCrop, invalidate);
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,10 @@ public interface ThumbRenderer extends Renderer {
|
|||||||
public boolean isVerticalCenter() {
|
public boolean isVerticalCenter() {
|
||||||
return this == ControlPoint.TOP_CENTER || this == ControlPoint.BOTTOM_CENTER;
|
return this == ControlPoint.TOP_CENTER || this == ControlPoint.BOTTOM_CENTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isCenter() {
|
||||||
|
return isHorizontalCenter() || isVerticalCenter();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ControlPoint getControlPoint();
|
ControlPoint getControlPoint();
|
||||||
|
@ -106,7 +106,7 @@ public final class CropAreaRenderer implements Renderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hitTest(float x, float y) {
|
public boolean hitTest(float x, float y) {
|
||||||
return !Bounds.FULL_BOUNDS.contains(x, y);
|
return !Bounds.contains(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<CropAreaRenderer> CREATOR = new Creator<CropAreaRenderer>() {
|
public static final Creator<CropAreaRenderer> CREATOR = new Creator<CropAreaRenderer>() {
|
||||||
|
@ -44,7 +44,7 @@ public final class InverseFillRenderer implements Renderer {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hitTest(float x, float y) {
|
public boolean hitTest(float x, float y) {
|
||||||
return !Bounds.FULL_BOUNDS.contains(x, y);
|
return !Bounds.contains(x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<InverseFillRenderer> CREATOR = new Creator<InverseFillRenderer>() {
|
public static final Creator<InverseFillRenderer> CREATOR = new Creator<InverseFillRenderer>() {
|
||||||
|
Loading…
Reference in New Issue
Block a user