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;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
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}.
|
||||
@ -21,9 +24,51 @@ public final class Bounds {
|
||||
|
||||
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() {
|
||||
return new RectF(LEFT, TOP, RIGHT, BOTTOM);
|
||||
}
|
||||
|
||||
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.PointF;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||
|
||||
@ -18,13 +19,13 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||
*/
|
||||
interface EditSession {
|
||||
|
||||
void movePoint(int p, PointF point);
|
||||
void movePoint(int p, @NonNull PointF point);
|
||||
|
||||
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();
|
||||
}
|
||||
|
@ -31,12 +31,12 @@ final class ElementDragEditSession extends ElementEditSession {
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditSession removePoint(Matrix newInverse, int p) {
|
||||
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||
|
||||
abstract class ElementEditSession implements EditSession {
|
||||
|
||||
private final Matrix inverseMatrix;
|
||||
private final Matrix inverseMatrix;
|
||||
|
||||
final EditorElement selected;
|
||||
|
||||
@ -60,7 +60,7 @@ abstract class ElementEditSession implements EditSession {
|
||||
* @param matrix Matrix to transform point with.
|
||||
* @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[] out = new float[2];
|
||||
matrix.mapPoints(out, in);
|
||||
|
@ -8,7 +8,7 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||
|
||||
final class ElementScaleEditSession extends ElementEditSession {
|
||||
|
||||
private ElementScaleEditSession(EditorElement selected, Matrix inverseMatrix) {
|
||||
private ElementScaleEditSession(@NonNull EditorElement selected, @NonNull Matrix inverseMatrix) {
|
||||
super(selected, inverseMatrix);
|
||||
}
|
||||
|
||||
@ -56,20 +56,20 @@ final class ElementScaleEditSession extends ElementEditSession {
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditSession newPoint(Matrix newInverse, PointF point, int p) {
|
||||
public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditSession removePoint(Matrix newInverse, int p) {
|
||||
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||
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);
|
||||
}
|
||||
|
||||
private ElementDragEditSession convertToDrag(int p, Matrix inverse) {
|
||||
private ElementDragEditSession convertToDrag(int p, @NonNull Matrix inverse) {
|
||||
return ElementDragEditSession.startDrag(selected, inverse, endPointScreen[1 - p]);
|
||||
}
|
||||
|
||||
|
@ -70,6 +70,7 @@ public final class ImageEditorView extends FrameLayout {
|
||||
|
||||
@Nullable
|
||||
private EditSession editSession;
|
||||
private boolean moreThanOnePointerUsedInSession;
|
||||
|
||||
public ImageEditorView(Context context) {
|
||||
super(context);
|
||||
@ -215,6 +216,7 @@ public final class ImageEditorView extends FrameLayout {
|
||||
PointF point = getPoint(event);
|
||||
EditorElement selected = model.findElementAtPoint(point, viewMatrix, inverse);
|
||||
|
||||
moreThanOnePointerUsedInSession = false;
|
||||
model.pushUndoPoint();
|
||||
editSession = startEdit(inverse, point, selected);
|
||||
|
||||
@ -230,9 +232,19 @@ public final class ImageEditorView extends FrameLayout {
|
||||
}
|
||||
case MotionEvent.ACTION_MOVE: {
|
||||
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));
|
||||
}
|
||||
model.moving(editSession.getSelected());
|
||||
invalidate();
|
||||
return true;
|
||||
}
|
||||
@ -240,11 +252,16 @@ public final class ImageEditorView extends FrameLayout {
|
||||
}
|
||||
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||
if (editSession != null && event.getPointerCount() == 2) {
|
||||
moreThanOnePointerUsedInSession = true;
|
||||
editSession.commit();
|
||||
model.pushUndoPoint();
|
||||
|
||||
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) {
|
||||
dragDropRelease();
|
||||
}
|
||||
@ -259,7 +276,11 @@ public final class ImageEditorView extends FrameLayout {
|
||||
dragDropRelease();
|
||||
|
||||
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;
|
||||
}
|
||||
break;
|
||||
@ -270,8 +291,11 @@ public final class ImageEditorView extends FrameLayout {
|
||||
dragDropRelease();
|
||||
|
||||
editSession = null;
|
||||
model.postEdit(moreThanOnePointerUsedInSession);
|
||||
invalidate();
|
||||
return true;
|
||||
} else {
|
||||
model.postEdit(moreThanOnePointerUsedInSession);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -349,6 +373,11 @@ public final class ImageEditorView extends FrameLayout {
|
||||
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() {
|
||||
return model;
|
||||
}
|
||||
|
@ -96,6 +96,10 @@ public final class RendererContext {
|
||||
canvasMatrix.restore();
|
||||
}
|
||||
|
||||
public void getCurrent(@NonNull Matrix into) {
|
||||
canvasMatrix.getCurrent(into);
|
||||
}
|
||||
|
||||
public interface Ready {
|
||||
|
||||
Ready NULL = (renderer, cropMatrix, size) -> {
|
||||
|
@ -39,7 +39,7 @@ class ThumbDragEditSession extends ElementEditSession {
|
||||
|
||||
editorMatrix.postTranslate(-x, -y);
|
||||
|
||||
boolean aspectLocked = selected.getFlags().isAspectLocked();
|
||||
boolean aspectLocked = selected.getFlags().isAspectLocked() && !controlPoint.isCenter();
|
||||
|
||||
float defaultScale = aspectLocked ? 2 : 1;
|
||||
|
||||
@ -57,12 +57,12 @@ class ThumbDragEditSession extends ElementEditSession {
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditSession newPoint(Matrix newInverse, PointF point, int p) {
|
||||
public EditSession newPoint(@NonNull Matrix newInverse, @NonNull PointF point, int p) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EditSession removePoint(Matrix newInverse, int p) {
|
||||
public EditSession removePoint(@NonNull Matrix newInverse, int p) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ final class EditorElementHierarchy {
|
||||
|
||||
private EditorElementHierarchy(@NonNull EditorElement root) {
|
||||
this.root = root;
|
||||
this.view = this.root.getChild(0);
|
||||
this.view = this.root.getChild(0);
|
||||
this.flipRotate = this.view.getChild(0);
|
||||
this.imageRoot = this.flipRotate.getChild(0);
|
||||
this.overlay = this.flipRotate.getChild(1);
|
||||
@ -237,6 +237,30 @@ final class EditorElementHierarchy {
|
||||
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) {
|
||||
if (cropEditorElement.getFlags().isVisible()) {
|
||||
updateViewToCrop(visibleViewPort, invalidate);
|
||||
@ -299,9 +323,10 @@ final class EditorElementHierarchy {
|
||||
|
||||
matrix.preConcat(flipRotate.getLocalMatrix());
|
||||
matrix.preConcat(cropEditorElement.getLocalMatrix());
|
||||
matrix.preConcat(cropEditorElement.getEditorMatrix());
|
||||
EditorElement mainImage = getMainImage();
|
||||
if (mainImage != null) {
|
||||
float xScale = 1f / xScale(mainImage.getLocalMatrix());
|
||||
float xScale = 1f / (xScale(mainImage.getLocalMatrix()) * xScale(mainImage.getEditorMatrix()));
|
||||
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 float MAXIMUM_CROP = 0.20f;
|
||||
|
||||
@NonNull
|
||||
private Runnable invalidate = NULL_RUNNABLE;
|
||||
|
||||
private final UndoRedoStacks undoRedoStacks;
|
||||
private final UndoRedoStacks cropUndoRedoStacks;
|
||||
private final InBoundsMemory inBoundsMemory = new InBoundsMemory();
|
||||
|
||||
private EditorElementHierarchy editorElementHierarchy;
|
||||
|
||||
@ -88,7 +91,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
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);
|
||||
if (inverse != null) {
|
||||
Matrix regular = new Matrix();
|
||||
@ -107,7 +110,12 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
}
|
||||
|
||||
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())) {
|
||||
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
|
||||
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) {
|
||||
Map<UUID, EditorElement> fromMap = getElementMap(fromRootElement);
|
||||
Map<UUID, EditorElement> toMap = getElementMap(toRootElement);
|
||||
Map<UUID, EditorElement> toMap = getElementMap(toRootElement);
|
||||
|
||||
for (EditorElement fromElement : fromMap.values()) {
|
||||
fromElement.stopAnimation();
|
||||
@ -188,6 +198,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
cropUndoRedoStacks.getUndoStack().clear();
|
||||
cropUndoRedoStacks.getUndoStack().clear();
|
||||
editorElementHierarchy.startCrop(invalidate);
|
||||
inBoundsMemory.push(editorElementHierarchy.getMainImage(), editorElementHierarchy.getCropEditorElement());
|
||||
}
|
||||
|
||||
public void doneCrop() {
|
||||
@ -196,9 +207,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
|
||||
public void setCropAspectLock(boolean locked) {
|
||||
EditorFlags flags = editorElementHierarchy.getCropEditorElement().getFlags();
|
||||
int currentState = flags.setAspectLocked(locked).getCurrentState();
|
||||
int currentState = flags.setAspectLocked(locked).getCurrentState();
|
||||
|
||||
flags.reset();
|
||||
flags.setAspectLocked(locked).persist();
|
||||
flags.setAspectLocked(locked)
|
||||
.persist();
|
||||
flags.restoreState(currentState);
|
||||
}
|
||||
|
||||
@ -206,10 +219,113 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
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() {
|
||||
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) {
|
||||
this.visibleViewPort.set(visibleViewPort);
|
||||
this.editorElementHierarchy.updateViewToCrop(visibleViewPort, invalidate);
|
||||
@ -364,23 +480,38 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
|
||||
return findCropRelativeTo(editorElementHierarchy.getRoot());
|
||||
}
|
||||
|
||||
private RectF findCropRelativeTo(EditorElement element) {
|
||||
RectF findCropRelativeTo(EditorElement element) {
|
||||
return findRelativeBounds(editorElementHierarchy.getCropEditorElement(), element);
|
||||
}
|
||||
|
||||
private RectF findRelativeBounds(EditorElement from, EditorElement to) {
|
||||
Matrix matrix1 = findElementMatrix(from, new Matrix());
|
||||
Matrix matrix2 = findElementInverseMatrix(to, new Matrix());
|
||||
RectF findRelativeBounds(EditorElement from, EditorElement to) {
|
||||
Matrix relative = findRelativeMatrix(from, to);
|
||||
|
||||
RectF dst = new RectF(Bounds.FULL_BOUNDS);
|
||||
if (matrix1 != null) {
|
||||
matrix1.preConcat(matrix2);
|
||||
|
||||
matrix1.mapRect(dst, Bounds.FULL_BOUNDS);
|
||||
if (relative != null) {
|
||||
relative.mapRect(dst, Bounds.FULL_BOUNDS);
|
||||
}
|
||||
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() {
|
||||
pushUndoPoint();
|
||||
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() {
|
||||
return this == ControlPoint.TOP_CENTER || this == ControlPoint.BOTTOM_CENTER;
|
||||
}
|
||||
|
||||
public boolean isCenter() {
|
||||
return isHorizontalCenter() || isVerticalCenter();
|
||||
}
|
||||
}
|
||||
|
||||
ControlPoint getControlPoint();
|
||||
|
@ -106,7 +106,7 @@ public final class CropAreaRenderer implements Renderer {
|
||||
|
||||
@Override
|
||||
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>() {
|
||||
|
@ -44,7 +44,7 @@ public final class InverseFillRenderer implements Renderer {
|
||||
|
||||
@Override
|
||||
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>() {
|
||||
|
Loading…
Reference in New Issue
Block a user