From 715ad0d4593f3c6b0ad0d4db2f3aec10c03fcf66 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Wed, 8 Sep 2021 17:01:20 -0300 Subject: [PATCH] Add text styles support to image editor. Co-authored-by: Greyson Parrelli --- .../renderers/MultiLineTextRenderer.java | 140 ++++++++++++++++-- .../scribbles/ImageEditorFragment.java | 14 +- .../securesms/scribbles/ImageEditorHudV2.kt | 7 +- .../scribbles/TextEntryDialogFragment.kt | 5 + app/src/main/res/drawable/ic_text_tool_24.xml | 12 ++ .../res/layout/v2_media_image_editor_hud.xml | 38 ++++- ...media_image_editor_text_entry_fragment.xml | 17 ++- app/src/main/res/values/strings.xml | 1 + 8 files changed, 212 insertions(+), 22 deletions(-) create mode 100644 app/src/main/res/drawable/ic_text_tool_24.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java index 5553443dce..3b13f895a2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java +++ b/app/src/main/java/org/thoughtcrime/securesms/imageeditor/renderers/MultiLineTextRenderer.java @@ -7,6 +7,7 @@ import android.graphics.Paint; import android.graphics.Rect; import android.graphics.RectF; import android.graphics.Typeface; +import android.os.Build; import android.os.Parcel; import android.view.animation.Interpolator; @@ -32,7 +33,11 @@ import static java.util.Collections.emptyList; */ public final class MultiLineTextRenderer extends InvalidateableRenderer implements ColorableRenderer, SelectableRenderer { - private static final float HIT_PADDING = ViewUtil.dpToPx(30); + private static final float HIT_PADDING = ViewUtil.dpToPx(30); + private static final float HIGHLIGHT_HORIZONTAL_PADDING = ViewUtil.dpToPx(8); + private static final float HIGHLIGHT_TOP_PADDING = ViewUtil.dpToPx(10); + private static final float HIGHLIGHT_BOTTOM_PADDING = ViewUtil.dpToPx(6); + private static final float HIGHLIGHT_CORNER_RADIUS = ViewUtil.dpToPx(4); @NonNull private String text = ""; @@ -42,6 +47,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private final Paint paint = new Paint(); private final Paint selectionPaint = new Paint(); + private final Paint modePaint = new Paint(); private final float textScale; @@ -49,6 +55,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private int selEnd; private boolean hasFocus; private boolean selected; + private Mode mode; private List lines = emptyList(); @@ -60,14 +67,27 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private final SelectedElementGuideRenderer selectedElementGuideRenderer = new SelectedElementGuideRenderer(); private final RectF textBounds = new RectF(); - public MultiLineTextRenderer(@Nullable String text, @ColorInt int color) { - setColor(color); + public MultiLineTextRenderer(@Nullable String text, @ColorInt int color, @NonNull Mode mode) { + this.mode = mode; + + Typeface typeface = getTypeface(); + + modePaint.setAntiAlias(true); + modePaint.setTextSize(100); + modePaint.setTypeface(typeface); + + setColorInternal(color); + float regularTextSize = paint.getTextSize(); + paint.setAntiAlias(true); paint.setTextSize(100); - paint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + paint.setTypeface(typeface); + textScale = paint.getTextSize() / regularTextSize; + selectionPaint.setAntiAlias(true); + setText(text != null ? text : ""); createLinesForText(); } @@ -102,6 +122,14 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen } } + public void nextMode() { + setMode(Mode.fromCode(mode.code + 1)); + } + + public @NonNull Mode getMode() { + return mode; + } + /** * Post concats an additional matrix to the supplied matrix that scales and positions the editor * so that all the text is visible. @@ -158,6 +186,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen private final RectF selectionBounds = new RectF(); private final RectF textBounds = new RectF(); private final RectF hitBounds = new RectF(); + private final RectF modeBounds = new RectF(); private String text; private int selStart; @@ -293,6 +322,31 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen rendererContext.canvasMatrix.concat(projectionMatrix); + if (mode == Mode.HIGHLIGHT) { + modeBounds.set(textBounds.left - HIGHLIGHT_HORIZONTAL_PADDING, + selectionBounds.top - HIGHLIGHT_TOP_PADDING, + textBounds.right + HIGHLIGHT_HORIZONTAL_PADDING, + selectionBounds.bottom + HIGHLIGHT_BOTTOM_PADDING); + + int alpha = modePaint.getAlpha(); + modePaint.setAlpha(rendererContext.getAlpha(alpha)); + rendererContext.canvas.drawRoundRect(modeBounds, HIGHLIGHT_CORNER_RADIUS, HIGHLIGHT_CORNER_RADIUS, modePaint); + modePaint.setAlpha(alpha); + } else if (mode == Mode.UNDERLINE) { + modeBounds.set(textBounds.left, selectionBounds.top, textBounds.right, selectionBounds.bottom); + modeBounds.inset(-ViewUtil.dpToPx(2), -ViewUtil.dpToPx(2)); + + modeBounds.set(modeBounds.left, + Math.max(modeBounds.top, modeBounds.bottom - ViewUtil.dpToPx(6)), + modeBounds.right, + modeBounds.bottom - ViewUtil.dpToPx(2)); + + int alpha = modePaint.getAlpha(); + modePaint.setAlpha(rendererContext.getAlpha(alpha)); + rendererContext.canvas.drawRect(modeBounds, modePaint); + modePaint.setAlpha(alpha); + } + if (hasFocus && showSelectionOrCursor()) { if (selStart == selEnd) { selectionPaint.setAlpha((int) (cursorAnimatedValue * 128)); @@ -309,6 +363,13 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen paint.setAlpha(alpha); + if (mode == Mode.OUTLINE) { + int modeAlpha = modePaint.getAlpha(); + modePaint.setAlpha(rendererContext.getAlpha(alpha)); + rendererContext.canvas.drawText(text, 0, 0, modePaint); + modePaint.setAlpha(modeAlpha); + } + rendererContext.restore(); // add our descent for the next lines @@ -332,10 +393,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen @Override public void setColor(@ColorInt int color) { if (this.color != color) { - this.color = color; - paint.setColor(color); - selectionPaint.setColor(color); - invalidate(); + setColorInternal(color); } } @@ -392,10 +450,39 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen } } + private void setMode(@NonNull Mode mode) { + if (this.mode != mode) { + this.mode = mode; + setColorInternal(color); + } + } + + private void setColorInternal(@ColorInt int color) { + this.color = color; + + if (mode == Mode.REGULAR) { + paint.setColor(color); + selectionPaint.setColor(color); + } else { + paint.setColor(Color.WHITE); + selectionPaint.setColor(Color.WHITE); + } + + if (mode == Mode.OUTLINE) { + modePaint.setStrokeWidth(ViewUtil.dpToPx(15) / 10f); + modePaint.setStyle(Paint.Style.STROKE); + } else { + modePaint.setStyle(Paint.Style.FILL); + } + + modePaint.setColor(color); + invalidate(); + } + public static final Creator CREATOR = new Creator() { @Override public MultiLineTextRenderer createFromParcel(Parcel in) { - return new MultiLineTextRenderer(in.readString(), in.readInt()); + return new MultiLineTextRenderer(in.readString(), in.readInt(), Mode.fromCode(in.readInt())); } @Override @@ -413,6 +500,7 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen public void writeToParcel(Parcel dest, int flags) { dest.writeString(text); dest.writeInt(color); + dest.writeInt(mode.code); } private static Interpolator pulseInterpolator() { @@ -424,4 +512,38 @@ public final class MultiLineTextRenderer extends InvalidateableRenderer implemen return Math.max(0, Math.min(1, input)); }; } + + private static @NonNull Typeface getTypeface() { + if (Build.VERSION.SDK_INT < 26) { + return Typeface.create(Typeface.DEFAULT, Typeface.BOLD); + } else { + return new Typeface.Builder("") + .setFallback("sans-serif") + .setWeight(900) + .build(); + } + } + + public enum Mode { + REGULAR(0), + HIGHLIGHT(1), + UNDERLINE(2), + OUTLINE(3); + + private final int code; + + Mode(int code) { + this.code = code; + } + + private static Mode fromCode(int code) { + for (final Mode value : Mode.values()) { + if (value.code == code) { + return value; + } + } + + return REGULAR; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 04425432bb..0fc64d94c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -1,7 +1,5 @@ package org.thoughtcrime.securesms.scribbles; -import static android.app.Activity.RESULT_OK; - import android.Manifest; import android.content.Intent; import android.content.res.Configuration; @@ -14,7 +12,6 @@ import android.net.Uri; import android.os.Bundle; import android.view.HapticFeedbackConstants; import android.view.LayoutInflater; -import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; @@ -71,6 +68,8 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import static android.app.Activity.RESULT_OK; + public final class ImageEditorFragment extends Fragment implements ImageEditorHudV2.EventListener, MediaSendPageFragment, TextEntryDialogFragment.Controller @@ -351,6 +350,13 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu imageEditorView.zoomToFitText(editorElement, textRenderer); } + @Override + public void onTextStyleToggle() { + if (currentSelection != null && currentSelection.getRenderer() instanceof MultiLineTextRenderer) { + ((MultiLineTextRenderer) currentSelection.getRenderer()).nextMode(); + } + } + @Override public void onTextEntryDialogDismissed(boolean hasText) { imageEditorView.doneTextEditing(); @@ -366,7 +372,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu protected void addText() { String initialText = ""; int color = imageEditorHud.getActiveColor(); - MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color); + MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color, MultiLineTextRenderer.Mode.REGULAR); EditorElement element = new EditorElement(renderer, EditorModel.Z_TEXT); imageEditorView.getModel().addElementCentered(element, 1); diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHudV2.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHudV2.kt index d48f180995..eff052eb02 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHudV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorHudV2.kt @@ -64,6 +64,7 @@ class ImageEditorHudV2 @JvmOverloads constructor( private val colorIndicator: ImageView = findViewById(R.id.image_editor_hud_color_indicator) private val bottomGuideline: Guideline = findViewById(R.id.image_editor_bottom_guide) private val brushPreview: BrushWidthPreviewView = findViewById(R.id.image_editor_hud_brush_preview) + private val textStyleToggle: ImageView = findViewById(R.id.image_editor_hud_text_style_button) private val selectableSet: Set = setOf(drawButton, textButton, stickerButton, blurButton) @@ -73,7 +74,7 @@ class ImageEditorHudV2 @JvmOverloads constructor( private val drawButtonRow: Set = setOf(cancelButton, doneButton, drawButton, textButton, stickerButton, blurButton) private val cropButtonRow: Set = setOf(cancelButton, doneButton, cropRotateButton, cropFlipButton, cropAspectLockButton) - private val allModeTools: Set = drawTools + blurTools + drawButtonRow + cropButtonRow + private val allModeTools: Set = drawTools + blurTools + drawButtonRow + cropButtonRow + textStyleToggle private val viewsToSlide: Set = drawButtonRow + cropButtonRow @@ -93,6 +94,7 @@ class ImageEditorHudV2 @JvmOverloads constructor( clearAllButton.setOnClickListener { listener?.onClearAll() } cancelButton.setOnClickListener { listener?.onCancel() } + textStyleToggle.setOnClickListener { listener?.onTextStyleToggle() } drawButton.setOnClickListener { setMode(Mode.DRAW) } blurButton.setOnClickListener { setMode(Mode.BLUR) } textButton.setOnClickListener { setMode(Mode.TEXT) } @@ -370,7 +372,7 @@ class ImageEditorHudV2 @JvmOverloads constructor( private fun presentModeText() { animateModeChange( - inSet = drawButtonRow + setOf(drawSeekBar), + inSet = drawButtonRow + setOf(drawSeekBar, textStyleToggle), outSet = allModeTools ) animateInUndoTools() @@ -520,6 +522,7 @@ class ImageEditorHudV2 @JvmOverloads constructor( fun onFlipHorizontal() fun onRotate90AntiClockwise() fun onCropAspectLock() + fun onTextStyleToggle() val isCropAspectLocked: Boolean fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean) diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/TextEntryDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/TextEntryDialogFragment.kt index ad6228fa87..f1a3e8a876 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/TextEntryDialogFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/TextEntryDialogFragment.kt @@ -61,6 +61,7 @@ class TextEntryDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_im val slider: AppCompatSeekBar = view.findViewById(R.id.image_editor_hud_draw_color_bar) val colorIndicator: ImageView = view.findViewById(R.id.image_editor_hud_color_indicator) + val styleToggle: ImageView = view.findViewById(R.id.image_editor_hud_text_style_button) slider.setUpForColor( Color.WHITE, { @@ -83,6 +84,10 @@ class TextEntryDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_im ) slider.progress = requireArguments().getInt("color_index") + + styleToggle.setOnClickListener { + (element.renderer as MultiLineTextRenderer).nextMode() + } } override fun onDismiss(dialog: DialogInterface) { diff --git a/app/src/main/res/drawable/ic_text_tool_24.xml b/app/src/main/res/drawable/ic_text_tool_24.xml new file mode 100644 index 0000000000..994d26ae12 --- /dev/null +++ b/app/src/main/res/drawable/ic_text_tool_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/v2_media_image_editor_hud.xml b/app/src/main/res/layout/v2_media_image_editor_hud.xml index 2a1ce63e85..5820897117 100644 --- a/app/src/main/res/layout/v2_media_image_editor_hud.xml +++ b/app/src/main/res/layout/v2_media_image_editor_hud.xml @@ -203,16 +203,24 @@ android:splitTrack="false" android:visibility="gone" app:layout_constraintBottom_toTopOf="@id/image_editor_hud_top_of_button_bar_spacing" - app:layout_constraintEnd_toStartOf="@+id/image_editor_hud_draw_brush" + app:layout_constraintEnd_toStartOf="@+id/toggle_button_barrier" app:layout_constraintStart_toStartOf="parent" - app:layout_goneMarginEnd="32dp" tools:visibility="visible" /> + + + + + android:visibility="gone" + tools:alpha="1" + tools:visibility="visible" /> \ No newline at end of file diff --git a/app/src/main/res/layout/v2_media_image_editor_text_entry_fragment.xml b/app/src/main/res/layout/v2_media_image_editor_text_entry_fragment.xml index 0b5aae3d5a..dc8b1b7a8e 100644 --- a/app/src/main/res/layout/v2_media_image_editor_text_entry_fragment.xml +++ b/app/src/main/res/layout/v2_media_image_editor_text_entry_fragment.xml @@ -31,11 +31,24 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="32dp" - android:layout_marginEnd="32dp" android:splitTrack="false" app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/image_editor_hud_text_style_button" app:layout_constraintStart_toStartOf="parent" /> + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7855510ff5..5e2264a39f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3826,6 +3826,7 @@ You\'ll lose any changes you\'ve made to this photo. Delete Failed to open camera + Toggle between text styles