Add text styles support to image editor.
Co-authored-by: Greyson Parrelli <greyson@signal.org>
This commit is contained in:
parent
05f7dce503
commit
715ad0d459
8 changed files with 212 additions and 22 deletions
|
@ -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<Line> 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<MultiLineTextRenderer> CREATOR = new Creator<MultiLineTextRenderer>() {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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<View> = setOf(drawButton, textButton, stickerButton, blurButton)
|
||||
|
||||
|
@ -73,7 +74,7 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
|||
private val drawButtonRow: Set<View> = setOf(cancelButton, doneButton, drawButton, textButton, stickerButton, blurButton)
|
||||
private val cropButtonRow: Set<View> = setOf(cancelButton, doneButton, cropRotateButton, cropFlipButton, cropAspectLockButton)
|
||||
|
||||
private val allModeTools: Set<View> = drawTools + blurTools + drawButtonRow + cropButtonRow
|
||||
private val allModeTools: Set<View> = drawTools + blurTools + drawButtonRow + cropButtonRow + textStyleToggle
|
||||
|
||||
private val viewsToSlide: Set<View> = 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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
12
app/src/main/res/drawable/ic_text_tool_24.xml
Normal file
12
app/src/main/res/drawable/ic_text_tool_24.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17.202,9.988l-1.6,0.2L15.202,7.988L13.002,7.988v8h1.9v1.7L9.202,17.688L9.202,15.988L11.002,15.988L11.002,7.988L8.802,7.988l-0.4,2.2L6.802,9.988l0.3,-3.7h9.8Z"
|
||||
android:fillColor="#000600"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.015,1.5A6.5,6.5 0,0 1,22.502 7.995v8.011a6.5,6.5 0,0 1,-6.487 6.494L7.986,22.5A6.5,6.5 0,0 1,1.502 16.006L1.502,7.995A6.5,6.5 0,0 1,7.986 1.5h8.029m0,-1.5L7.986,-0A7.991,7.991 0,0 0,0.002 7.995v8.011a7.989,7.989 0,0 0,7.986 7.994h8.029A7.989,7.989 0,0 0,24.002 16.006L24.002,7.995A7.991,7.991 0,0 0,16.015 0Z"/>
|
||||
</vector>
|
|
@ -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" />
|
||||
|
||||
<androidx.constraintlayout.widget.Barrier
|
||||
android:id="@+id/toggle_button_barrier"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:barrierDirection="start"
|
||||
app:constraint_referenced_ids="image_editor_hud_draw_brush,image_editor_hud_text_style_button" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_editor_hud_draw_brush"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/circle_tintable_padded"
|
||||
android:contentDescription="@string/ImageEditorHud__toggle_between_marker_and_highlighter"
|
||||
android:padding="12dp"
|
||||
|
@ -222,6 +230,26 @@
|
|||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/image_editor_hud_draw_color_bar"
|
||||
app:srcCompat="@drawable/ic_draw_white_24"
|
||||
tools:alpha="1"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_editor_hud_text_style_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:alpha="0"
|
||||
android:background="@drawable/circle_tintable_padded"
|
||||
android:contentDescription="@string/ImageEditorHud__toggle_between_text_styles"
|
||||
android:padding="12dp"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/transparent_black_40"
|
||||
app:layout_constraintBottom_toBottomOf="@id/image_editor_hud_draw_color_bar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/image_editor_hud_draw_color_bar"
|
||||
app:srcCompat="@drawable/ic_text_tool_24"
|
||||
app:tint="@color/core_white"
|
||||
tools:alpha="1"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatSeekBar
|
||||
|
@ -380,8 +408,8 @@
|
|||
android:id="@+id/image_editor_hud_brush_preview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
android:alpha="0"
|
||||
tools:alpha="1" />
|
||||
android:visibility="gone"
|
||||
tools:alpha="1"
|
||||
tools:visibility="visible" />
|
||||
</merge>
|
|
@ -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" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image_editor_hud_text_style_button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="@drawable/circle_tintable_padded"
|
||||
android:contentDescription="@string/ImageEditorHud__toggle_between_text_styles"
|
||||
android:padding="12dp"
|
||||
app:backgroundTint="@color/transparent_black_40"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:srcCompat="@drawable/ic_text_tool_24"
|
||||
app:tint="@color/core_white" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
|
@ -3826,6 +3826,7 @@
|
|||
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
||||
<string name="ImageEditorHud__delete">Delete</string>
|
||||
<string name="CameraFragment__failed_to_open_camera">Failed to open camera</string>
|
||||
<string name="ImageEditorHud__toggle_between_text_styles">Toggle between text styles</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue