Image Editor - Multi-line text.

* Two pass rendering for text on top while editing.
This commit is contained in:
Alan Evans 2019-06-14 15:31:03 -04:00 committed by Greyson Parrelli
parent 42a5504f0d
commit b9a10653f1
7 changed files with 502 additions and 276 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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}.
* <p>
* 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<Line> 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<MultiLineTextRenderer> CREATOR = new Creator<MultiLineTextRenderer>() {
@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));
};
}
}

View file

@ -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}.
* <p>
* 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<TextRenderer> CREATOR = new Creator<TextRenderer>() {
@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));
};
}
}

View file

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