b9a10653f1
* Two pass rendering for text on top while editing.
383 lines
12 KiB
Java
383 lines
12 KiB
Java
package org.thoughtcrime.securesms.scribbles;
|
|
|
|
import android.content.Intent;
|
|
import android.graphics.Paint;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.fragment.app.Fragment;
|
|
import android.view.LayoutInflater;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
|
|
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.MultiLineTextRenderer;
|
|
import org.thoughtcrime.securesms.logging.Log;
|
|
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
|
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
|
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
|
|
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
|
import org.thoughtcrime.securesms.util.ParcelUtil;
|
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
|
|
|
import static android.app.Activity.RESULT_OK;
|
|
|
|
public final class ImageEditorFragment extends Fragment implements ImageEditorHud.EventListener,
|
|
VerticalSlideColorPicker.OnColorChangeListener,
|
|
MediaSendPageFragment {
|
|
|
|
private static final String TAG = Log.tag(ImageEditorFragment.class);
|
|
|
|
private static final String KEY_IMAGE_URI = "image_uri";
|
|
|
|
public static final int SELECT_STICKER_REQUEST_CODE = 123;
|
|
|
|
private EditorModel restoredModel;
|
|
|
|
@Nullable
|
|
private EditorElement currentSelection;
|
|
private int imageMaxHeight;
|
|
private int imageMaxWidth;
|
|
|
|
public static class Data {
|
|
private final Bundle bundle;
|
|
|
|
Data(Bundle bundle) {
|
|
this.bundle = bundle;
|
|
}
|
|
|
|
public Data() {
|
|
this(new Bundle());
|
|
}
|
|
|
|
void writeModel(@NonNull EditorModel model) {
|
|
byte[] bytes = ParcelUtil.serialize(model);
|
|
bundle.putByteArray("MODEL", bytes);
|
|
}
|
|
|
|
@Nullable
|
|
public EditorModel readModel() {
|
|
byte[] bytes = bundle.getByteArray("MODEL");
|
|
if (bytes == null) {
|
|
return null;
|
|
}
|
|
return ParcelUtil.deserialize(bytes, EditorModel.CREATOR);
|
|
}
|
|
}
|
|
|
|
private Uri imageUri;
|
|
private Controller controller;
|
|
private ImageEditorHud imageEditorHud;
|
|
private ImageEditorView imageEditorView;
|
|
|
|
public static ImageEditorFragment newInstance(@NonNull Uri imageUri) {
|
|
Bundle args = new Bundle();
|
|
args.putParcelable(KEY_IMAGE_URI, imageUri);
|
|
|
|
ImageEditorFragment fragment = new ImageEditorFragment();
|
|
fragment.setArguments(args);
|
|
fragment.setUri(imageUri);
|
|
return fragment;
|
|
}
|
|
|
|
@Override
|
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
|
super.onCreate(savedInstanceState);
|
|
if (!(getActivity() instanceof Controller)) {
|
|
throw new IllegalStateException("Parent activity must implement Controller interface.");
|
|
}
|
|
controller = (Controller) getActivity();
|
|
Bundle arguments = getArguments();
|
|
if (arguments != null) {
|
|
imageUri = arguments.getParcelable(KEY_IMAGE_URI);
|
|
}
|
|
|
|
if (imageUri == null) {
|
|
throw new AssertionError("No KEY_IMAGE_URI supplied");
|
|
}
|
|
|
|
MediaConstraints mediaConstraints = new PushMediaConstraints();
|
|
|
|
imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext());
|
|
imageMaxHeight = mediaConstraints.getImageMaxHeight(requireContext());
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
|
return inflater.inflate(R.layout.image_editor_fragment, container, false);
|
|
}
|
|
|
|
@Override
|
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
|
super.onViewCreated(view, savedInstanceState);
|
|
|
|
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
|
imageEditorView = view.findViewById(R.id.image_editor_view);
|
|
|
|
imageEditorHud.setEventListener(this);
|
|
|
|
imageEditorView.setTapListener(selectionListener);
|
|
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
|
|
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
|
|
|
|
EditorModel editorModel = null;
|
|
|
|
if (restoredModel != null) {
|
|
editorModel = restoredModel;
|
|
restoredModel = null;
|
|
} else if (savedInstanceState != null) {
|
|
editorModel = new Data(savedInstanceState).readModel();
|
|
}
|
|
|
|
if (editorModel == null) {
|
|
editorModel = new EditorModel();
|
|
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
|
|
image.getFlags().setSelectable(false).persist();
|
|
editorModel.addElement(image);
|
|
}
|
|
|
|
imageEditorView.setModel(editorModel);
|
|
|
|
refreshUniqueColors();
|
|
}
|
|
|
|
@Override
|
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
|
super.onSaveInstanceState(outState);
|
|
new Data(outState).writeModel(imageEditorView.getModel());
|
|
}
|
|
|
|
@Override
|
|
public void setUri(@NonNull Uri uri) {
|
|
this.imageUri = uri;
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public Uri getUri() {
|
|
return imageUri;
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public View getPlaybackControls() {
|
|
return null;
|
|
}
|
|
|
|
@Override
|
|
public Object saveState() {
|
|
Data data = new Data();
|
|
data.writeModel(imageEditorView.getModel());
|
|
return data;
|
|
}
|
|
|
|
@Override
|
|
public void restoreState(@NonNull Object state) {
|
|
if (state instanceof Data) {
|
|
|
|
Data data = (Data) state;
|
|
EditorModel model = data.readModel();
|
|
|
|
if (model != null) {
|
|
if (imageEditorView != null) {
|
|
imageEditorView.setModel(model);
|
|
refreshUniqueColors();
|
|
} else {
|
|
this.restoredModel = model;
|
|
}
|
|
}
|
|
} else {
|
|
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
|
|
}
|
|
}
|
|
|
|
private void changeEntityColor(int selectedColor) {
|
|
if (currentSelection != null) {
|
|
Renderer renderer = currentSelection.getRenderer();
|
|
if (renderer instanceof ColorableRenderer) {
|
|
((ColorableRenderer) renderer).setColor(selectedColor);
|
|
refreshUniqueColors();
|
|
}
|
|
}
|
|
}
|
|
|
|
private void startTextEntityEditing(@NonNull EditorElement textElement, boolean selectAll) {
|
|
imageEditorView.startTextEditing(textElement, TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()), selectAll);
|
|
}
|
|
|
|
protected void addText() {
|
|
String initialText = "";
|
|
int color = imageEditorHud.getActiveColor();
|
|
MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color);
|
|
EditorElement element = new EditorElement(renderer);
|
|
|
|
imageEditorView.getModel().addElementCentered(element, 1);
|
|
imageEditorView.invalidate();
|
|
|
|
currentSelection = element;
|
|
|
|
startTextEntityEditing(element, true);
|
|
}
|
|
|
|
@Override
|
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
super.onActivityResult(requestCode, resultCode, data);
|
|
if (resultCode == RESULT_OK && requestCode == SELECT_STICKER_REQUEST_CODE && data != null) {
|
|
final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE);
|
|
|
|
UriGlideRenderer renderer = new UriGlideRenderer(Uri.parse("file:///android_asset/" + stickerFile), false, imageMaxWidth, imageMaxHeight);
|
|
EditorElement element = new EditorElement(renderer);
|
|
imageEditorView.getModel().addElementCentered(element, 0.2f);
|
|
currentSelection = element;
|
|
} else {
|
|
imageEditorHud.enterMode(ImageEditorHud.Mode.NONE);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
|
|
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
|
imageEditorView.doneTextEditing();
|
|
|
|
controller.onTouchEventsNeeded(mode != ImageEditorHud.Mode.NONE);
|
|
|
|
switch (mode) {
|
|
case CROP:
|
|
imageEditorView.getModel().startCrop();
|
|
break;
|
|
|
|
case DRAW:
|
|
imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND);
|
|
break;
|
|
|
|
case HIGHLIGHT:
|
|
imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE);
|
|
break;
|
|
|
|
case TEXT:
|
|
addText();
|
|
break;
|
|
|
|
case MOVE_DELETE:
|
|
Intent intent = new Intent(getContext(), StickerSelectActivity.class);
|
|
startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
|
|
break;
|
|
|
|
case NONE:
|
|
imageEditorView.getModel().doneCrop();
|
|
currentSelection = null;
|
|
break;
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onColorChange(int color) {
|
|
imageEditorView.setDrawingBrushColor(color);
|
|
changeEntityColor(color);
|
|
}
|
|
|
|
@Override
|
|
public void onUndo() {
|
|
imageEditorView.getModel().undo();
|
|
refreshUniqueColors();
|
|
}
|
|
|
|
@Override
|
|
public void onDelete() {
|
|
imageEditorView.deleteElement(currentSelection);
|
|
refreshUniqueColors();
|
|
}
|
|
|
|
@Override
|
|
public void onFlipHorizontal() {
|
|
imageEditorView.getModel().flipHorizontal();
|
|
}
|
|
|
|
@Override
|
|
public void onRotate90AntiClockwise() {
|
|
imageEditorView.getModel().rotate90anticlockwise();
|
|
}
|
|
|
|
@Override
|
|
public void onCropAspectLock(boolean locked) {
|
|
imageEditorView.getModel().setCropAspectLock(locked);
|
|
}
|
|
|
|
@Override
|
|
public boolean isCropAspectLocked() {
|
|
return imageEditorView.getModel().isCropAspectLocked();
|
|
}
|
|
|
|
@Override
|
|
public void onRequestFullScreen(boolean fullScreen) {
|
|
controller.onRequestFullScreen(fullScreen);
|
|
}
|
|
|
|
private void refreshUniqueColors() {
|
|
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
|
}
|
|
|
|
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
|
|
imageEditorHud.setUndoAvailability(undoAvailable);
|
|
}
|
|
|
|
private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() {
|
|
|
|
@Override
|
|
public void onEntityDown(@Nullable EditorElement editorElement) {
|
|
if (editorElement != null) {
|
|
controller.onTouchEventsNeeded(true);
|
|
} else {
|
|
currentSelection = null;
|
|
controller.onTouchEventsNeeded(false);
|
|
imageEditorHud.enterMode(ImageEditorHud.Mode.NONE);
|
|
imageEditorView.doneTextEditing();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
|
|
currentSelection = editorElement;
|
|
if (currentSelection != null) {
|
|
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
|
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
|
|
} else {
|
|
imageEditorHud.enterMode(ImageEditorHud.Mode.MOVE_DELETE);
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
|
|
currentSelection = editorElement;
|
|
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
|
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true);
|
|
}
|
|
}
|
|
|
|
private void setTextElement(@NonNull EditorElement editorElement,
|
|
@NonNull ColorableRenderer colorableRenderer,
|
|
boolean startEditing)
|
|
{
|
|
int color = colorableRenderer.getColor();
|
|
imageEditorHud.enterMode(ImageEditorHud.Mode.TEXT);
|
|
imageEditorHud.setActiveColor(color);
|
|
if (startEditing) {
|
|
startTextEntityEditing(editorElement, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
public interface Controller {
|
|
void onTouchEventsNeeded(boolean needed);
|
|
|
|
void onRequestFullScreen(boolean fullScreen);
|
|
}
|
|
}
|