Implement radial dial.

Co-authored-by: Alan Evans <alan@signal.org>
This commit is contained in:
Alex Hart
2021-09-17 13:09:13 -03:00
committed by GitHub
parent ce2c2002c6
commit 7bcc338a49
9 changed files with 491 additions and 14 deletions

View File

@@ -427,6 +427,11 @@ public final class ImageEditorView extends FrameLayout {
this.mode = mode;
}
public void setMainImageEditorMatrixRotation(float angle, float minScaleDown) {
model.setMainImageEditorMatrixRotation(angle, minScaleDown);
invalidate();
}
public void startDrawing(float thickness, @NonNull Paint.Cap cap, boolean blur) {
this.thickness = thickness;
this.cap = cap;

View File

@@ -0,0 +1,37 @@
package org.signal.imageeditor.core;
import android.graphics.Matrix;
import androidx.annotation.NonNull;
public final class MatrixUtils {
private static final ThreadLocal<float[]> tempMatrixValues = new ThreadLocal<>();
protected static @NonNull float[] getTempMatrixValues() {
float[] floats = tempMatrixValues.get();
if(floats == null) {
floats = new float[9];
tempMatrixValues.set(floats);
}
return floats;
}
/**
* Extracts the angle from a matrix in radians.
*/
public static float getRotationAngle(@NonNull Matrix matrix) {
float[] matrixValues = getTempMatrixValues();
matrix.getValues(matrixValues);
return (float) -Math.atan2(matrixValues[Matrix.MSKEW_X], matrixValues[Matrix.MSCALE_X]);
}
/** Gets the scale on the X axis */
public static float getScaleX(@NonNull Matrix matrix) {
float[] matrixValues = getTempMatrixValues();
matrix.getValues(matrixValues);
float scaleX = matrixValues[Matrix.MSCALE_X];
float skewX = matrixValues[Matrix.MSKEW_X];
return (float) Math.sqrt(scaleX * scaleX + skewX * skewX);
}
}

View File

@@ -7,6 +7,7 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.imageeditor.core.MatrixUtils;
import org.signal.imageeditor.core.Renderer;
import org.signal.imageeditor.core.RendererContext;
@@ -299,6 +300,14 @@ public final class EditorElement implements Parcelable {
children.clear();
}
public float getLocalRotationAngle() {
return MatrixUtils.getRotationAngle(localMatrix);
}
public float getLocalScaleX() {
return MatrixUtils.getScaleX(localMatrix);
}
public interface PerElementFunction {
void apply(EditorElement element);
}

View File

@@ -221,6 +221,12 @@ final class EditorElementHierarchy {
selectedElement = null;
}
void updateSelectionThumbsForElement(@NonNull EditorElement element, @Nullable Matrix overlayMappingMatrix) {
if (element == selectedElement) {
setOrUpdateSelectionThumbsForElement(element, overlayMappingMatrix);
}
}
void setOrUpdateSelectionThumbsForElement(@NonNull EditorElement element, @Nullable Matrix overlayMappingMatrix) {
if (selectedElement != element) {
removeAllSelectionArtifacts();
@@ -433,7 +439,7 @@ final class EditorElementHierarchy {
return dst;
}
void flipRotate(int degrees, int scaleX, int scaleY, @NonNull RectF visibleViewPort, @Nullable Runnable invalidate) {
void flipRotate(float degrees, int scaleX, int scaleY, @NonNull RectF visibleViewPort, @Nullable Runnable invalidate) {
Matrix newLocal = new Matrix(flipRotate.getLocalMatrix());
if (degrees != 0) {
newLocal.postRotate(degrees);

View File

@@ -76,6 +76,11 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
}
public void updateSelectionThumbsIfSelected(@NonNull EditorElement editorElement) {
Matrix overlayMappingMatrix = findRelativeMatrix(editorElement, editorElementHierarchy.getOverlay());
editorElementHierarchy.updateSelectionThumbsForElement(editorElement, overlayMappingMatrix);
}
public void setSelectionVisible(boolean visible) {
editorElementHierarchy.getSelection()
.getFlags()
@@ -145,6 +150,51 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
updateUndoRedoAvailableState(getActiveUndoRedoStacks(isCropping()));
}
/** Keeps the image within the crop bounds as it rotates */
public void setMainImageEditorMatrixRotation(float angle, float minScaleDown) {
setEditorMatrixToRotationMatrixAboutParentsOrigin(editorElementHierarchy.getMainImage(), angle);
scaleMainImageEditorMatrixToFitInsideCropBounds(minScaleDown, 2f);
}
private void scaleMainImageEditorMatrixToFitInsideCropBounds(float minScaleDown, float maxScaleUp) {
EditorElement mainImage = editorElementHierarchy.getMainImage();
Matrix mainImageLocalBackup = new Matrix(mainImage.getLocalMatrix());
Matrix mainImageEditorBackup = new Matrix(mainImage.getEditorMatrix());
mainImage.commitEditorMatrix();
Matrix combinedLocal = new Matrix(mainImage.getLocalMatrix());
Matrix newLocal = Bisect.bisectToTest(mainImage,
minScaleDown,
maxScaleUp,
this::cropIsWithinMainImageBounds,
(matrix, scale) -> matrix.preScale(scale, scale));
Matrix invertLocal = new Matrix();
if (newLocal != null && combinedLocal.invert(invertLocal)) {
invertLocal.preConcat(newLocal); // L^-1 (L * Scale) -> Scale
mainImageEditorBackup.preConcat(invertLocal); // add the scale to editor matrix to keep this image within crop
}
mainImage.getLocalMatrix().set(mainImageLocalBackup);
mainImage.getEditorMatrix().set(mainImageEditorBackup);
}
/**
* Sets the editor matrix for the element to a rotation of the degrees but does so that we are rotating around the
* parents elements origin.
*/
private void setEditorMatrixToRotationMatrixAboutParentsOrigin(@NonNull EditorElement element, float degrees) {
Matrix localMatrix = element.getLocalMatrix();
Matrix editorMatrix = element.getEditorMatrix();
localMatrix.invert(editorMatrix);
editorMatrix.preRotate(degrees);
editorMatrix.preConcat(localMatrix);
// Editor Matrix is then: Local^-1 * Rotate(degrees) * Local
// So you end up with this overall for the element: Local * Local^-1 * Rotate(degrees) * Local
// Meaning the rotate applies after existing effects of the local matrix
// Where as simply setting the editor matrix rotate gives this: Local * Rotate(degrees)
// which rotates around local origin first
}
/**
* Renders tree with the following matrix:
* <p>
@@ -233,6 +283,10 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
getActiveUndoRedoStacks(cropping).pushState(editorElementHierarchy.getRoot());
}
public void updateUndoRedoAvailabilityState() {
updateUndoRedoAvailableState(getActiveUndoRedoStacks(isCropping()));
}
public void clearUndoStack() {
EditorElement root = editorElementHierarchy.getRoot();
EditorElement original = root;
@@ -598,7 +652,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
*/
public void moving(@NonNull EditorElement editorElement) {
if (!isCropping()) {
setSelected(editorElement);
updateSelectionThumbsIfSelected(editorElement);
return;
}
@@ -902,10 +956,6 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
return null;
}
public void rotate90clockwise() {
flipRotate(90, 1, 1);
}
public void rotate90anticlockwise() {
flipRotate(-90, 1, 1);
}
@@ -914,11 +964,7 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
flipRotate(0, -1, 1);
}
public void flipVertical() {
flipRotate(0, 1, -1);
}
private void flipRotate(int degrees, int scaleX, int scaleY) {
private void flipRotate(float degrees, int scaleX, int scaleY) {
pushUndoPoint();
editorElementHierarchy.flipRotate(degrees, scaleX, scaleY, visibleViewPort, invalidate);
updateUndoRedoAvailableState(getActiveUndoRedoStacks(isCropping()));