Image Editor - Multi-line text.
* Two pass rendering for text on top while editing.
This commit is contained in:
parent
42a5504f0d
commit
b9a10653f1
7 changed files with 502 additions and 276 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue