diff --git a/src/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java b/src/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java
index 7a25d0cf5b..ab1f2d1e94 100644
--- a/src/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java
+++ b/src/org/thoughtcrime/securesms/imageeditor/HiddenEditText.java
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Color;
import android.graphics.Rect;
-import androidx.annotation.Nullable;
import android.text.InputType;
import android.util.TypedValue;
import android.view.Gravity;
@@ -12,7 +11,11 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.FrameLayout;
-import org.thoughtcrime.securesms.imageeditor.renderers.TextRenderer;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
+import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
/**
* Invisible {@link android.widget.EditText} that is used during in-image text editing.
@@ -23,11 +26,17 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
private static final int INCOGNITO_KEYBOARD_IME = EditorInfo.IME_FLAG_NO_PERSONALIZED_LEARNING;
@Nullable
- private TextRenderer currentTextEntity;
+ private EditorElement currentTextEditorElement;
+
+ @Nullable
+ private MultiLineTextRenderer currentTextEntity;
@Nullable
private Runnable onEndEdit;
+ @Nullable
+ private OnEditOrSelectionChange onEditOrSelectionChange;
+
public HiddenEditText(Context context) {
super(context);
setAlpha(0);
@@ -37,8 +46,7 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
setFocusableInTouchMode(true);
setBackgroundColor(Color.TRANSPARENT);
setTextSize(TypedValue.COMPLEX_UNIT_SP, 1);
- setInputType(InputType.TYPE_CLASS_TEXT);
- setImeOptions(EditorInfo.IME_ACTION_DONE);
+ setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE);
clearFocus();
}
@@ -47,6 +55,7 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if (currentTextEntity != null) {
currentTextEntity.setText(text.toString());
+ postEditOrSelectionChange();
}
}
@@ -76,11 +85,33 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
}
}
- @Nullable TextRenderer getCurrentTextEntity() {
+ private void postEditOrSelectionChange() {
+ if (currentTextEditorElement != null && currentTextEntity != null && onEditOrSelectionChange != null) {
+ onEditOrSelectionChange.onChange(currentTextEditorElement, currentTextEntity);
+ }
+ }
+
+ @Nullable MultiLineTextRenderer getCurrentTextEntity() {
return currentTextEntity;
}
- void setCurrentTextEntity(@Nullable TextRenderer currentTextEntity) {
+ @Nullable EditorElement getCurrentTextEditorElement() {
+ return currentTextEditorElement;
+ }
+
+ public void setCurrentTextEditorElement(@Nullable EditorElement currentTextEditorElement) {
+ if (currentTextEditorElement != null && currentTextEditorElement.getRenderer() instanceof MultiLineTextRenderer) {
+ this.currentTextEditorElement = currentTextEditorElement;
+ setCurrentTextEntity((MultiLineTextRenderer) currentTextEditorElement.getRenderer());
+ } else {
+ this.currentTextEditorElement = null;
+ setCurrentTextEntity(null);
+ }
+
+ postEditOrSelectionChange();
+ }
+
+ private void setCurrentTextEntity(@Nullable MultiLineTextRenderer currentTextEntity) {
if (this.currentTextEntity != currentTextEntity) {
if (this.currentTextEntity != null) {
this.currentTextEntity.setFocused(false);
@@ -101,6 +132,7 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
super.onSelectionChanged(selStart, selEnd);
if (currentTextEntity != null) {
currentTextEntity.setSelection(selStart, selEnd);
+ postEditOrSelectionChange();
}
}
@@ -133,4 +165,12 @@ final class HiddenEditText extends androidx.appcompat.widget.AppCompatEditText {
public void setOnEndEdit(@Nullable Runnable onEndEdit) {
this.onEndEdit = onEndEdit;
}
+
+ public void setOnEditOrSelectionChange(@Nullable OnEditOrSelectionChange onEditOrSelectionChange) {
+ this.onEditOrSelectionChange = onEditOrSelectionChange;
+ }
+
+ public interface OnEditOrSelectionChange {
+ void onChange(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java b/src/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
index 3ab56a89a5..c66908fc04 100644
--- a/src/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
+++ b/src/org/thoughtcrime/securesms/imageeditor/ImageEditorView.java
@@ -20,7 +20,7 @@ import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
import org.thoughtcrime.securesms.imageeditor.model.ThumbRenderer;
import org.thoughtcrime.securesms.imageeditor.renderers.BezierDrawingRenderer;
-import org.thoughtcrime.securesms.imageeditor.renderers.TextRenderer;
+import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
/**
* ImageEditorView
@@ -106,25 +106,25 @@ public final class ImageEditorView extends FrameLayout {
addView(editText);
editText.clearFocus();
editText.setOnEndEdit(this::doneTextEditing);
+ editText.setOnEditOrSelectionChange(this::zoomToFitText);
return editText;
}
public void startTextEditing(@NonNull EditorElement editorElement, boolean incognitoKeyboardEnabled, boolean selectAll) {
- Renderer renderer = editorElement.getRenderer();
- if (renderer instanceof TextRenderer) {
- TextRenderer textRenderer = (TextRenderer) renderer;
-
+ if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
editText.setIncognitoKeyboardEnabled(incognitoKeyboardEnabled);
- editText.setCurrentTextEntity(textRenderer);
+ editText.setCurrentTextEditorElement(editorElement);
if (selectAll) {
editText.selectAll();
}
editText.requestFocus();
-
- getModel().zoomTo(editorElement, Bounds.TOP / 2, true);
}
}
+ private void zoomToFitText(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) {
+ getModel().zoomToTextElement(editorElement, textRenderer);
+ }
+
public boolean isTextEditing() {
return editText.getCurrentTextEntity() != null;
}
@@ -132,7 +132,7 @@ public final class ImageEditorView extends FrameLayout {
public void doneTextEditing() {
getModel().zoomOut();
if (editText.getCurrentTextEntity() != null) {
- editText.setCurrentTextEntity(null);
+ editText.setCurrentTextEditorElement(null);
editText.hideKeyboard();
if (tapListener != null) {
tapListener.onEntityDown(null);
@@ -148,7 +148,8 @@ public final class ImageEditorView extends FrameLayout {
rendererContext.save();
try {
rendererContext.canvasMatrix.initial(viewMatrix);
- model.draw(rendererContext);
+
+ model.draw(rendererContext, editText.getCurrentTextEditorElement());
} finally {
rendererContext.restore();
}
diff --git a/src/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java b/src/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java
index 6d5020ce87..61802bd96b 100644
--- a/src/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java
+++ b/src/org/thoughtcrime/securesms/imageeditor/model/EditorFlags.java
@@ -19,6 +19,7 @@ public final class EditorFlags {
private static final int EDITABLE = 32;
private int flags;
+ private int markedFlags;
private int persistedFlags;
EditorFlags() {
@@ -116,6 +117,14 @@ public final class EditorFlags {
this.flags = flags;
}
+ void mark() {
+ markedFlags = flags;
+ }
+
+ void restore() {
+ flags = markedFlags;
+ }
+
public void set(@NonNull EditorFlags from) {
this.persistedFlags = from.persistedFlags;
this.flags = from.flags;
diff --git a/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java b/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
index 6ee09d63e1..8cbd9d0a16 100644
--- a/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
+++ b/src/org/thoughtcrime/securesms/imageeditor/model/EditorModel.java
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.RendererContext;
import org.thoughtcrime.securesms.imageeditor.UndoRedoStackListener;
+import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
import java.util.HashMap;
import java.util.LinkedHashSet;
@@ -88,9 +89,30 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
* Child nodes are supplied with a viewModelMatrix' = viewModelMatrix * matrix * editorMatrix
*
* @param rendererContext Canvas to draw on to.
+ * @param renderOnTop This element will appear on top of the overlay.
*/
- public void draw(@NonNull RendererContext rendererContext) {
- editorElementHierarchy.getRoot().draw(rendererContext);
+ public void draw(@NonNull RendererContext rendererContext, @Nullable EditorElement renderOnTop) {
+ EditorElement root = editorElementHierarchy.getRoot();
+ if (renderOnTop != null) {
+ root.forAllInTree(element -> element.getFlags().mark());
+
+ renderOnTop.getFlags().setVisible(false);
+ }
+
+ // pass 1
+ root.draw(rendererContext);
+
+ if (renderOnTop != null) {
+ // hide all
+ try {
+ root.forAllInTree(element -> element.getFlags().setVisible(renderOnTop == element));
+
+ // pass 2
+ root.draw(rendererContext);
+ } finally {
+ root.forAllInTree(element -> element.getFlags().restore());
+ }
+ }
}
public @Nullable Matrix findElementInverseMatrix(@NonNull EditorElement element, @NonNull Matrix viewMatrix) {
@@ -676,25 +698,22 @@ public final class EditorModel implements Parcelable, RendererContext.Ready {
}
/**
- * Changes the temporary view so that the element is centered in it.
+ * Changes the temporary view so that the text element is centered in it.
*
- * @param entity Entity to center on.
- * @param y An optional extra value to translate the view by to leave space for the keyboard for example.
- * @param doNotZoomOut Iff true, undoes any zoom out
+ * @param entity Entity to center on.
+ * @param textRenderer The text renderer, which can make additional adjustments to the zoom matrix
+ * to leave space for the keyboard for example.
*/
- public void zoomTo(@NonNull EditorElement entity, float y, boolean doNotZoomOut) {
+ public void zoomToTextElement(@NonNull EditorElement entity, @NonNull MultiLineTextRenderer textRenderer) {
Matrix elementInverseMatrix = findElementInverseMatrix(entity, new Matrix());
if (elementInverseMatrix != null) {
- elementInverseMatrix.preConcat(editorElementHierarchy.getRoot().getEditorMatrix());
+ EditorElement root = editorElementHierarchy.getRoot();
- float xScale = EditorElementHierarchy.xScale(elementInverseMatrix);
- if (doNotZoomOut && xScale < 1) {
- elementInverseMatrix.postScale(1 / xScale, 1 / xScale);
- }
+ elementInverseMatrix.preConcat(root.getEditorMatrix());
- elementInverseMatrix.postTranslate(0, y);
+ textRenderer.applyRecommendedEditorMatrix(elementInverseMatrix);
- editorElementHierarchy.getRoot().animateEditorTo(elementInverseMatrix, invalidate);
+ root.animateEditorTo(elementInverseMatrix, invalidate);
}
}
diff --git a/src/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java b/src/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java
new file mode 100644
index 0000000000..91d878bfda
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java
@@ -0,0 +1,395 @@
+package org.thoughtcrime.securesms.imageeditor.renderers;
+
+import android.animation.ValueAnimator;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.os.Parcel;
+import android.view.animation.Interpolator;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import org.thoughtcrime.securesms.imageeditor.Bounds;
+import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
+import org.thoughtcrime.securesms.imageeditor.RendererContext;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.util.Collections.emptyList;
+
+/**
+ * Renders multiple lines of {@link #text} in ths specified {@link #color}.
+ *
+ * Scales down the text size of long lines to fit inside the {@link Bounds} width.
+ */
+public final class MultiLineTextRenderer extends InvalidateableRenderer implements ColorableRenderer {
+
+ @NonNull
+ private String text = "";
+
+ @ColorInt
+ private int color;
+
+ private final Paint paint = new Paint();
+ private final Paint selectionPaint = new Paint();
+
+ private final float textScale;
+
+ private int selStart;
+ private int selEnd;
+ private boolean hasFocus;
+
+ private List lines = emptyList();
+
+ private ValueAnimator cursorAnimator;
+ private float cursorAnimatedValue;
+
+ private final Matrix recommendedEditorMatrix = new Matrix();
+
+ public MultiLineTextRenderer(@Nullable String text, @ColorInt int color) {
+ setColor(color);
+ float regularTextSize = paint.getTextSize();
+ paint.setAntiAlias(true);
+ paint.setTextSize(100);
+ paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
+ textScale = paint.getTextSize() / regularTextSize;
+ selectionPaint.setAntiAlias(true);
+ setText(text != null ? text : "");
+ createLinesForText();
+ }
+
+ @Override
+ public void render(@NonNull RendererContext rendererContext) {
+ super.render(rendererContext);
+
+ for (Line line : lines) {
+ line.render(rendererContext);
+ }
+ }
+
+ @NonNull
+ public String getText() {
+ return text;
+ }
+
+ public void setText(@NonNull String text) {
+ if (!this.text.equals(text)) {
+ this.text = text;
+ createLinesForText();
+ }
+ }
+
+ /**
+ * Post concats an additional matrix to the supplied matrix that scales and positions the editor
+ * so that all the text is visible.
+ *
+ * @param matrix editor matrix, already zoomed and positioned to fit the regular bounds.
+ */
+ public void applyRecommendedEditorMatrix(@NonNull Matrix matrix) {
+ recommendedEditorMatrix.reset();
+
+ float scale = 1f;
+ for (Line line : lines) {
+ if (line.scale < scale) {
+ scale = line.scale;
+ }
+ }
+
+ float yOff = 0;
+ for (Line line : lines) {
+ if (line.containsSelectionEnd()) {
+ break;
+ } else {
+ yOff -= line.heightInBounds;
+ }
+ }
+
+ recommendedEditorMatrix.postTranslate(0, Bounds.TOP / 1.5f + yOff);
+
+ recommendedEditorMatrix.postScale(scale, scale);
+
+ matrix.postConcat(recommendedEditorMatrix);
+ }
+
+ private void createLinesForText() {
+ String[] split = text.split("\n", -1);
+
+ if (split.length == lines.size()) {
+ for (int i = 0; i < split.length; i++) {
+ lines.get(i).setText(split[i]);
+ }
+ } else {
+ lines = new ArrayList<>(split.length);
+ for (String s : split) {
+ lines.add(new Line(s));
+ }
+ }
+ setSelection(selStart, selEnd);
+ }
+
+ private class Line {
+ private final Matrix accentMatrix = new Matrix();
+ private final Matrix decentMatrix = new Matrix();
+ private final Matrix projectionMatrix = new Matrix();
+ private final Matrix inverseProjectionMatrix = new Matrix();
+ private final RectF selectionBounds = new RectF();
+ private final RectF textBounds = new RectF();
+
+ private String text;
+ private int selStart;
+ private int selEnd;
+ private float ascentInBounds;
+ private float descentInBounds;
+ private float scale = 1f;
+ private float heightInBounds;
+
+ Line(String text) {
+ this.text = text;
+ recalculate();
+ }
+
+ private void recalculate() {
+ RectF maxTextBounds = new RectF();
+ Rect temp = new Rect();
+
+ getTextBoundsWithoutTrim(text, 0, text.length(), temp);
+ textBounds.set(temp);
+
+ maxTextBounds.set(textBounds);
+ float widthLimit = 150 * textScale;
+
+ scale = 1f / Math.max(1, maxTextBounds.right / widthLimit);
+
+ maxTextBounds.right = widthLimit;
+
+ if (showSelectionOrCursor()) {
+ Rect startTemp = new Rect();
+ int startInString = Math.min(text.length(), Math.max(0, selStart));
+ int endInString = Math.min(text.length(), Math.max(0, selEnd));
+ String startText = this.text.substring(0, startInString);
+
+ getTextBoundsWithoutTrim(startText, 0, startInString, startTemp);
+
+ if (selStart != selEnd) {
+ // selection
+ getTextBoundsWithoutTrim(text, startInString, endInString, temp);
+ } else {
+ // cursor
+ paint.getTextBounds("|", 0, 1, temp);
+ int width = temp.width();
+
+ temp.left -= width;
+ temp.right -= width;
+ }
+
+ temp.left += startTemp.right;
+ temp.right += startTemp.right;
+ selectionBounds.set(temp);
+ }
+
+ projectionMatrix.setRectToRect(new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
+ removeTranslate(projectionMatrix);
+
+ float[] pts = { 0, paint.ascent(), 0, paint.descent() };
+ projectionMatrix.mapPoints(pts);
+ ascentInBounds = pts[1];
+ descentInBounds = pts[3];
+ heightInBounds = descentInBounds - ascentInBounds;
+
+ projectionMatrix.preTranslate(-textBounds.centerX(), 0);
+ projectionMatrix.invert(inverseProjectionMatrix);
+
+ accentMatrix.setTranslate(0, -ascentInBounds);
+ decentMatrix.setTranslate(0, descentInBounds);
+
+ invalidate();
+ }
+
+ private void removeTranslate(Matrix matrix) {
+ float[] values = new float[9];
+
+ matrix.getValues(values);
+ values[2] = 0;
+ values[5] = 0;
+ matrix.setValues(values);
+ }
+
+ private boolean showSelectionOrCursor() {
+ return (selStart >= 0 || selEnd >= 0) &&
+ (selStart <= text.length() || selEnd <= text.length());
+ }
+
+ private boolean containsSelectionEnd() {
+ return (selEnd >= 0) &&
+ (selEnd <= text.length());
+ }
+
+ private void getTextBoundsWithoutTrim(String text, int start, int end, Rect result) {
+ Rect extra = new Rect();
+ Rect xBounds = new Rect();
+
+ String cannotBeTrimmed = "x" + text.substring(Math.max(0, start), Math.min(text.length(), end)) + "x";
+
+ paint.getTextBounds(cannotBeTrimmed, 0, cannotBeTrimmed.length(), extra);
+ paint.getTextBounds("x", 0, 1, xBounds);
+ result.set(extra);
+ result.right -= 2 * xBounds.width();
+
+ int temp = result.left;
+ result.left -= temp;
+ result.right -= temp;
+ }
+
+ public boolean contains(float x, float y) {
+ float[] dst = new float[2];
+
+ inverseProjectionMatrix.mapPoints(dst, new float[]{ x, y });
+
+ return textBounds.contains(dst[0], dst[1]);
+ }
+
+ void setText(String text) {
+ if (!this.text.equals(text)) {
+ this.text = text;
+ recalculate();
+ }
+ }
+
+ public void render(@NonNull RendererContext rendererContext) {
+ // add our ascent for ourselves and the next lines
+ rendererContext.canvasMatrix.concat(accentMatrix);
+
+ rendererContext.save();
+
+ rendererContext.canvasMatrix.concat(projectionMatrix);
+
+ if (hasFocus && showSelectionOrCursor()) {
+ if (selStart == selEnd) {
+ selectionPaint.setAlpha((int) (cursorAnimatedValue * 128));
+ } else {
+ selectionPaint.setAlpha(128);
+ }
+ rendererContext.canvas.drawRect(selectionBounds, selectionPaint);
+ }
+
+ int alpha = paint.getAlpha();
+ paint.setAlpha(rendererContext.getAlpha(alpha));
+
+ rendererContext.canvas.drawText(text, 0, 0, paint);
+
+ paint.setAlpha(alpha);
+
+ rendererContext.restore();
+
+ // add our descent for the next lines
+ rendererContext.canvasMatrix.concat(decentMatrix);
+ }
+
+ void setSelection(int selStart, int selEnd) {
+ if (selStart != this.selStart || selEnd != this.selEnd) {
+ this.selStart = selStart;
+ this.selEnd = selEnd;
+ recalculate();
+ }
+ }
+ }
+
+ @Override
+ public int getColor() {
+ return color;
+ }
+
+ @Override
+ public void setColor(@ColorInt int color) {
+ if (this.color != color) {
+ this.color = color;
+ paint.setColor(color);
+ selectionPaint.setColor(color);
+ invalidate();
+ }
+ }
+
+ @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;
+ }
+
+ public void setSelection(int selStart, int selEnd) {
+ this.selStart = selStart;
+ this.selEnd = selEnd;
+ for (Line line : lines) {
+ line.setSelection(selStart, selEnd);
+
+ int length = line.text.length() + 1; // one for new line
+
+ selStart -= length;
+ selEnd -= length;
+ }
+ }
+
+ public void setFocused(boolean hasFocus) {
+ if (this.hasFocus != hasFocus) {
+ this.hasFocus = hasFocus;
+ if (cursorAnimator != null) {
+ cursorAnimator.cancel();
+ cursorAnimator = null;
+ }
+ if (hasFocus) {
+ cursorAnimator = ValueAnimator.ofFloat(0, 1);
+ cursorAnimator.setInterpolator(pulseInterpolator());
+ cursorAnimator.setRepeatCount(ValueAnimator.INFINITE);
+ cursorAnimator.setDuration(1000);
+ cursorAnimator.addUpdateListener(animation -> {
+ cursorAnimatedValue = (float) animation.getAnimatedValue();
+ invalidate();
+ });
+ cursorAnimator.start();
+ } else {
+ invalidate();
+ }
+ }
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public MultiLineTextRenderer createFromParcel(Parcel in) {
+ return new MultiLineTextRenderer(in.readString(), in.readInt());
+ }
+
+ @Override
+ public MultiLineTextRenderer[] newArray(int size) {
+ return new MultiLineTextRenderer[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(text);
+ dest.writeInt(color);
+ }
+
+ private static Interpolator pulseInterpolator() {
+ return input -> {
+ input *= 5;
+ if (input > 1) {
+ input = 4 - input;
+ }
+ return Math.max(0, Math.min(1, input));
+ };
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/imageeditor/renderers/TextRenderer.java b/src/org/thoughtcrime/securesms/imageeditor/renderers/TextRenderer.java
deleted file mode 100644
index b32823f9a0..0000000000
--- a/src/org/thoughtcrime/securesms/imageeditor/renderers/TextRenderer.java
+++ /dev/null
@@ -1,238 +0,0 @@
-package org.thoughtcrime.securesms.imageeditor.renderers;
-
-import android.animation.ValueAnimator;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.os.Parcel;
-import androidx.annotation.ColorInt;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import android.view.animation.Interpolator;
-
-import org.thoughtcrime.securesms.imageeditor.Bounds;
-import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
-import org.thoughtcrime.securesms.imageeditor.RendererContext;
-
-/**
- * Renders a single line of {@link #text} in ths specified {@link #color}.
- *
- * Scales down the text size to fit inside the {@link Bounds} width.
- */
-public final class TextRenderer extends InvalidateableRenderer implements ColorableRenderer {
-
- @NonNull
- private String text = "";
-
- @ColorInt
- private int color;
-
- private final Paint paint = new Paint();
- private final Paint selectionPaint = new Paint();
- private final RectF textBounds = new RectF();
- private final RectF selectionBounds = new RectF();
- private final RectF maxTextBounds = new RectF();
- private final Matrix projectionMatrix = new Matrix();
- private final Matrix inverseProjectionMatrix = new Matrix();
-
- private final float textScale;
-
- private float xForCentre;
- private int selStart;
- private int selEnd;
- private boolean hasFocus;
-
- private ValueAnimator cursorAnimator;
- private float cursorAnimatedValue;
-
- public TextRenderer(@Nullable String text, @ColorInt int color) {
- setColor(color);
- float regularTextSize = paint.getTextSize();
- paint.setAntiAlias(true);
- paint.setTextSize(100);
- paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
- textScale = paint.getTextSize() / regularTextSize;
- selectionPaint.setAntiAlias(true);
- setText(text != null ? text : "");
- }
-
- private TextRenderer(Parcel in) {
- this(in.readString(), in.readInt());
- }
-
- public static final Creator CREATOR = new Creator() {
- @Override
- public TextRenderer createFromParcel(Parcel in) {
- return new TextRenderer(in);
- }
-
- @Override
- public TextRenderer[] newArray(int size) {
- return new TextRenderer[size];
- }
- };
-
- @Override
- public void render(@NonNull RendererContext rendererContext) {
- super.render(rendererContext);
- rendererContext.save();
- Canvas canvas = rendererContext.canvas;
-
- rendererContext.canvasMatrix.concat(projectionMatrix);
-
- if (hasFocus) {
- if (selStart == selEnd) {
- selectionPaint.setAlpha((int) (cursorAnimatedValue * 128));
- } else {
- selectionPaint.setAlpha(128);
- }
- canvas.drawRect(selectionBounds, selectionPaint);
- }
-
- int alpha = paint.getAlpha();
- paint.setAlpha(rendererContext.getAlpha(alpha));
-
- canvas.drawText(text, xForCentre, 0, paint);
-
- paint.setAlpha(alpha);
-
- rendererContext.restore();
- }
-
- @NonNull
- public String getText() {
- return text;
- }
-
- public void setText(@NonNull String text) {
- if (!this.text.equals(text)) {
- this.text = text;
- recalculate();
- }
- }
-
- private void recalculate() {
- Rect temp = new Rect();
-
- getTextBoundsWithoutTrim(text, 0, text.length(), temp);
- textBounds.set(temp);
-
- maxTextBounds.set(textBounds);
- maxTextBounds.right = Math.max(150 * textScale, maxTextBounds.right);
-
- xForCentre = maxTextBounds.centerX() - textBounds.centerX();
-
- textBounds.left += xForCentre;
- textBounds.right += xForCentre;
-
- if (selStart != selEnd) {
- getTextBoundsWithoutTrim(text, Math.min(text.length(), selStart), Math.min(text.length(), selEnd), temp);
- } else {
- Rect startTemp = new Rect();
- int start = Math.min(text.length(), selStart);
- String text = this.text.substring(0, start);
-
- getTextBoundsWithoutTrim(text, 0, start, startTemp);
- paint.getTextBounds("|", 0, 1, temp);
-
- int width = temp.width();
-
- temp.left -= width;
- temp.right -= width;
- temp.left += startTemp.right;
- temp.right += startTemp.right;
- }
- selectionBounds.set(temp);
- selectionBounds.left += xForCentre;
- selectionBounds.right += xForCentre;
-
- projectionMatrix.setRectToRect(new RectF(maxTextBounds), Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
- projectionMatrix.invert(inverseProjectionMatrix);
- invalidate();
- }
-
- private void getTextBoundsWithoutTrim(String text, int start, int end, Rect result) {
- Rect extra = new Rect();
- Rect xBounds = new Rect();
- String cannotBeTrimmed = "x" + text.substring(start, end) + "x";
- paint.getTextBounds(cannotBeTrimmed, 0, cannotBeTrimmed.length(), extra);
- paint.getTextBounds("x", 0, 1, xBounds);
- result.set(extra);
- result.right -= 2 * xBounds.width();
- }
-
- @Override
- public int getColor() {
- return color;
- }
-
- @Override
- public void setColor(@ColorInt int color) {
- if (this.color != color) {
- this.color = color;
- paint.setColor(color);
- selectionPaint.setColor(color);
- invalidate();
- }
- }
-
- @Override
- public boolean hitTest(float x, float y) {
- float[] dst = new float[2];
- inverseProjectionMatrix.mapPoints(dst, new float[]{ x, y });
- return textBounds.contains(dst[0], dst[1]);
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeString(text);
- dest.writeInt(color);
- }
-
- public void setSelection(int selStart, int selEnd) {
- this.selStart = selStart;
- this.selEnd = selEnd;
- recalculate();
- }
-
- public void setFocused(boolean hasFocus) {
- if (this.hasFocus != hasFocus) {
- this.hasFocus = hasFocus;
- if (cursorAnimator != null) {
- cursorAnimator.cancel();
- cursorAnimator = null;
- }
- if (hasFocus) {
- cursorAnimator = ValueAnimator.ofFloat(0, 1);
- cursorAnimator.setInterpolator(pulseInterpolator());
- cursorAnimator.setRepeatCount(ValueAnimator.INFINITE);
- cursorAnimator.setDuration(1000);
- cursorAnimator.addUpdateListener(animation -> {
- cursorAnimatedValue = (float) animation.getAnimatedValue();
- invalidate();
- });
- cursorAnimator.start();
- } else {
- invalidate();
- }
- }
- }
-
- private static Interpolator pulseInterpolator() {
- return input -> {
- input *= 5;
- if (input > 1) {
- input = 4 - input;
- }
- return Math.max(0, Math.min(1, input));
- };
- }
-}
diff --git a/src/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/src/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
index 85085182ed..a20f0ef3f7 100644
--- a/src/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
+++ b/src/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java
@@ -17,7 +17,7 @@ import org.thoughtcrime.securesms.imageeditor.ImageEditorView;
import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
-import org.thoughtcrime.securesms.imageeditor.renderers.TextRenderer;
+import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
import org.thoughtcrime.securesms.mms.MediaConstraints;
@@ -213,10 +213,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
}
protected void addText() {
- String initialText = "";
- int color = imageEditorHud.getActiveColor();
- TextRenderer renderer = new TextRenderer(initialText, color);
- EditorElement element = new EditorElement(renderer);
+ String initialText = "";
+ int color = imageEditorHud.getActiveColor();
+ MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color);
+ EditorElement element = new EditorElement(renderer);
imageEditorView.getModel().addElementCentered(element, 1);
imageEditorView.invalidate();
@@ -346,7 +346,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
currentSelection = editorElement;
if (currentSelection != null) {
- if (editorElement.getRenderer() instanceof TextRenderer) {
+ if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
} else {
imageEditorHud.enterMode(ImageEditorHud.Mode.MOVE_DELETE);
@@ -357,7 +357,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
@Override
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
currentSelection = editorElement;
- if (editorElement.getRenderer() instanceof TextRenderer) {
+ if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true);
}
}