Replace image editor.

This commit is contained in:
Alan Evans
2019-05-09 14:11:11 -03:00
parent 196ef60a82
commit 93e6ccb9e4
154 changed files with 4655 additions and 4092 deletions

View File

@@ -0,0 +1,376 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Intent;
import android.graphics.Paint;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.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.TextRenderer;
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);
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 = requireContext().getString(R.string.ImageEditorFragment_initial_text);
int color = imageEditorHud.getActiveColor();
TextRenderer renderer = new TextRenderer(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;
}
}
@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 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 TextRenderer) {
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 TextRenderer) {
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);
}
}

View File

@@ -0,0 +1,274 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* The HUD (heads-up display) that contains all of the tools for interacting with
* {@link org.thoughtcrime.securesms.imageeditor.ImageEditorView}
*/
public final class ImageEditorHud extends LinearLayout {
private View cropButton;
private View cropFlipButton;
private View cropRotateButton;
private ImageView cropAspectLock;
private View drawButton;
private View highlightButton;
private View textButton;
private View stickerButton;
private View undoButton;
private View deleteButton;
private View confirmButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
@NonNull
private EventListener eventListener = NULL_EVENT_LISTENER;
@Nullable
private ColorPaletteAdapter colorPaletteAdapter;
private final Map<Mode, Set<View>> visibilityModeMap = new HashMap<>();
private final Set<View> allViews = new HashSet<>();
public ImageEditorHud(@NonNull Context context) {
super(context);
initialize();
}
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.image_editor_hud, this);
setOrientation(VERTICAL);
cropButton = findViewById(R.id.scribble_crop_button);
cropFlipButton = findViewById(R.id.scribble_crop_flip);
cropRotateButton = findViewById(R.id.scribble_crop_rotate);
cropAspectLock = findViewById(R.id.scribble_crop_aspect_lock);
colorPalette = findViewById(R.id.scribble_color_palette);
drawButton = findViewById(R.id.scribble_draw_button);
highlightButton = findViewById(R.id.scribble_highlight_button);
textButton = findViewById(R.id.scribble_text_button);
stickerButton = findViewById(R.id.scribble_sticker_button);
undoButton = findViewById(R.id.scribble_undo_button);
deleteButton = findViewById(R.id.scribble_delete_button);
confirmButton = findViewById(R.id.scribble_confirm_button);
colorPicker = findViewById(R.id.scribble_color_picker);
cropAspectLock.setOnClickListener(v -> {
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
updateCropAspectLockImage(eventListener.isCropAspectLocked());
});
initializeViews();
initializeVisibilityMap();
setMode(Mode.NONE);
}
private void updateCropAspectLockImage(boolean cropAspectLocked) {
cropAspectLock.setImageDrawable(getResources().getDrawable(cropAspectLocked ? R.drawable.ic_crop_lock_32 : R.drawable.ic_crop_unlock_32));
}
private void initializeVisibilityMap() {
setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, stickerButton, cropButton, undoButton);
setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.HIGHLIGHT, confirmButton, undoButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.TEXT, confirmButton, deleteButton, colorPicker, colorPalette);
setVisibleViewsWhenInMode(Mode.MOVE_DELETE, confirmButton, deleteButton);
setVisibleViewsWhenInMode(Mode.CROP, confirmButton, cropFlipButton, cropRotateButton, cropAspectLock);
for (Set<View> views : visibilityModeMap.values()) {
allViews.addAll(views);
}
}
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
visibilityModeMap.put(mode, new HashSet<>(Arrays.asList(views)));
}
private void initializeViews() {
undoButton.setOnClickListener(v -> eventListener.onUndo());
deleteButton.setOnClickListener(v -> {
eventListener.onDelete();
setMode(Mode.NONE);
});
cropButton.setOnClickListener(v -> setMode(Mode.CROP));
cropFlipButton.setOnClickListener(v -> eventListener.onFlipHorizontal());
cropRotateButton.setOnClickListener(v -> eventListener.onRotate90AntiClockwise());
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPaletteAdapter = new ColorPaletteAdapter();
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
colorPalette.setAdapter(colorPaletteAdapter);
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
stickerButton.setOnClickListener(v -> setMode(Mode.MOVE_DELETE));
}
public void setColorPalette(@NonNull Set<Integer> colors) {
if (colorPaletteAdapter != null) {
colorPaletteAdapter.setColors(colors);
}
}
public int getActiveColor() {
return colorPicker.getActiveColor();
}
public void setActiveColor(int color) {
colorPicker.setActiveColor(color);
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener != null ? eventListener : NULL_EVENT_LISTENER;
}
public void enterMode(@NonNull Mode mode) {
setMode(mode, false);
}
private void setMode(@NonNull Mode mode) {
setMode(mode, true);
}
private void setMode(@NonNull Mode mode, boolean notify) {
Set<View> visibleButtons = visibilityModeMap.get(mode);
for (View button : allViews) {
button.setVisibility(visibleButtons != null && visibleButtons.contains(button) ? VISIBLE : GONE);
}
switch (mode) {
case CROP: presentModeCrop(); break;
case DRAW: presentModeDraw(); break;
case HIGHLIGHT: presentModeHighlight(); break;
case TEXT: presentModeText(); break;
}
if (notify) {
eventListener.onModeStarted(mode);
}
eventListener.onRequestFullScreen(mode != Mode.NONE);
}
private void presentModeCrop() {
updateCropAspectLockImage(eventListener.isCropAspectLocked());
}
private void presentModeDraw() {
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.RED);
}
private void presentModeHighlight() {
colorPicker.setOnColorChangeListener(highlightOnColorChangeListener);
colorPicker.setActiveColor(Color.YELLOW);
}
private void presentModeText() {
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.WHITE);
}
private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = selectedColor -> eventListener.onColorChange(selectedColor);
private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = selectedColor -> eventListener.onColorChange(replaceAlphaWith128(selectedColor));
private static int replaceAlphaWith128(int color) {
return color & ~0xff000000 | 0x80000000;
}
public enum Mode {
NONE, DRAW, HIGHLIGHT, TEXT, MOVE_DELETE, CROP
}
public interface EventListener {
void onModeStarted(@NonNull Mode mode);
void onColorChange(int color);
void onUndo();
void onDelete();
void onFlipHorizontal();
void onRotate90AntiClockwise();
void onCropAspectLock(boolean locked);
boolean isCropAspectLocked();
void onRequestFullScreen(boolean fullScreen);
}
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
@Override
public void onModeStarted(@NonNull Mode mode) {
}
@Override
public void onColorChange(int color) {
}
@Override
public void onUndo() {
}
@Override
public void onDelete() {
}
@Override
public void onFlipHorizontal() {
}
@Override
public void onRotate90AntiClockwise() {
}
@Override
public void onCropAspectLock(boolean locked) {
}
@Override
public boolean isCropAspectLocked() {
return false;
}
@Override
public void onRequestFullScreen(boolean fullScreen) {
}
};
}

View File

@@ -1,318 +0,0 @@
package org.thoughtcrime.securesms.scribbles;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.PointF;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.DeprecatedPersistentBlobProvider;
import org.thoughtcrime.securesms.scribbles.viewmodel.Font;
import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
import org.thoughtcrime.securesms.scribbles.widget.MotionView;
import org.thoughtcrime.securesms.scribbles.widget.ScribbleView;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import org.thoughtcrime.securesms.scribbles.widget.entity.ImageEntity;
import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import static android.app.Activity.RESULT_OK;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class ScribbleFragment extends Fragment implements ScribbleHud.EventListener,
VerticalSlideColorPicker.OnColorChangeListener,
MediaSendPageFragment
{
private static final String TAG = ScribbleFragment.class.getSimpleName();
private static final String KEY_IMAGE_URI = "image_uri";
public static final int SELECT_STICKER_REQUEST_CODE = 123;
private Controller controller;
private ScribbleHud scribbleHud;
private ScribbleView scribbleView;
private GlideRequests glideRequests;
private Uri imageUri;
private ScribbleView.SavedState savedState;
public static ScribbleFragment newInstance(@NonNull Uri imageUri) {
Bundle args = new Bundle();
args.putParcelable(KEY_IMAGE_URI, imageUri);
ScribbleFragment fragment = new ScribbleFragment();
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();
imageUri = getArguments().getParcelable(KEY_IMAGE_URI);
}
@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return inflater.inflate(R.layout.scribble_fragment, container, false);
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
this.glideRequests = GlideApp.with(this);
this.scribbleHud = view.findViewById(R.id.scribble_hud);
this.scribbleView = view.findViewById(R.id.scribble_view);
scribbleHud.setEventListener(this);
scribbleView.setMotionViewCallback(motionViewCallback);
scribbleView.setDrawingChangedListener(() -> scribbleHud.setColorPalette(scribbleView.getUniqueColors()));
scribbleView.setDrawingMode(false);
scribbleView.setImage(glideRequests, imageUri);
if (savedState != null) {
scribbleView.restoreState(savedState);
}
}
@Override
public void setUri(@NonNull Uri uri) {
this.imageUri = uri;
}
@Override
public @NonNull Uri getUri() {
return imageUri;
}
@Override
public @Nullable View getPlaybackControls() {
return null;
}
@Override
public @Nullable Object saveState() {
return scribbleView.saveState();
}
@Override
public void restoreState(@NonNull Object state) {
if (state instanceof ScribbleView.SavedState) {
savedState = (ScribbleView.SavedState) state;
if (scribbleView != null) {
scribbleView.restoreState(savedState);
}
} else {
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
}
}
private void addSticker(final Bitmap pica) {
Util.runOnMain(() -> {
Layer layer = new Layer();
ImageEntity entity = new ImageEntity(layer, pica, scribbleView.getWidth(), scribbleView.getHeight());
scribbleView.addEntityAndPosition(entity);
});
}
private void changeTextEntityColor(int selectedColor) {
TextEntity textEntity = currentTextEntity();
if (textEntity == null) {
return;
}
textEntity.getLayer().getFont().setColor(selectedColor);
textEntity.updateEntity();
scribbleView.invalidate();
scribbleHud.setColorPalette(scribbleView.getUniqueColors());
}
private void startTextEntityEditing() {
TextEntity textEntity = currentTextEntity();
if (textEntity != null) {
scribbleView.startEditing(textEntity);
}
}
@Nullable
private TextEntity currentTextEntity() {
if (scribbleView != null && scribbleView.getSelectedEntity() instanceof TextEntity) {
return ((TextEntity) scribbleView.getSelectedEntity());
} else {
return null;
}
}
protected void addTextSticker() {
TextLayer textLayer = createTextLayer();
TextEntity textEntity = new TextEntity(textLayer, scribbleView.getWidth(), scribbleView.getHeight());
scribbleView.addEntityAndPosition(textEntity);
PointF center = textEntity.absoluteCenter();
center.y = center.y * 0.5F;
textEntity.moveCenterTo(center);
scribbleView.invalidate();
startTextEntityEditing();
changeTextEntityColor(scribbleHud.getActiveColor());
}
private TextLayer createTextLayer() {
TextLayer textLayer = new TextLayer();
Font font = new Font();
font.setColor(scribbleHud.getActiveColor());
font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE);
textLayer.setFont(font);
return textLayer;
}
@SuppressLint("StaticFieldLeak")
@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);
SimpleTask.run(getLifecycle(), () -> {
try {
return BitmapFactory.decodeStream(getContext().getAssets().open(stickerFile));
} catch (IOException e) {
Log.w(TAG, e);
return null;
}
}, bitmap -> {
if (bitmap != null) {
addSticker(bitmap);
}
});
}
}
@Override
public void onModeStarted(@NonNull ScribbleHud.Mode mode) {
switch (mode) {
case DRAW:
controller.onTouchEventsNeeded(true);
scribbleView.setDrawingMode(true);
scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH);
break;
case HIGHLIGHT:
controller.onTouchEventsNeeded(true);
scribbleView.setDrawingMode(true);
scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH * 3);
break;
case TEXT:
controller.onTouchEventsNeeded(true);
scribbleView.setDrawingMode(false);
addTextSticker();
break;
case STICKER:
controller.onTouchEventsNeeded(true);
scribbleView.setDrawingMode(false);
Intent intent = new Intent(getContext(), StickerSelectActivity.class);
startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
break;
case NONE:
controller.onTouchEventsNeeded(false);
scribbleView.clearSelection();
scribbleView.setDrawingMode(false);
break;
}
}
@Override
public void onColorChange(int color) {
scribbleView.setDrawingBrushColor(color);
changeTextEntityColor(color);
}
@Override
public void onUndo() {
scribbleView.undoDrawing();
scribbleHud.setColorPalette(scribbleView.getUniqueColors());
}
@Override
public void onDelete() {
scribbleView.deleteSelected();
scribbleHud.setColorPalette(scribbleView.getUniqueColors());
}
private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() {
@Override
public void onEntitySelected(@Nullable MotionEntity entity) {
if (entity == null) {
scribbleHud.enterMode(ScribbleHud.Mode.NONE);
controller.onTouchEventsNeeded(false);
} else if (entity instanceof TextEntity) {
int textColor = ((TextEntity) entity).getLayer().getFont().getColor();
scribbleHud.enterMode(ScribbleHud.Mode.TEXT);
scribbleHud.setActiveColor(textColor);
controller.onTouchEventsNeeded(true);
} else {
scribbleHud.enterMode(ScribbleHud.Mode.STICKER);
controller.onTouchEventsNeeded(true);
}
}
@Override
public void onEntityDoubleTap(@NonNull MotionEntity entity) {
startTextEntityEditing();
}
};
public interface Controller {
void onTouchEventsNeeded(boolean needed);
}
}

View File

@@ -1,264 +0,0 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.TransportOption;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.views.Stub;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Locale;
import java.util.Set;
/**
* The HUD (heads-up display) that contains all of the tools for interacting with
* {@link org.thoughtcrime.securesms.scribbles.widget.ScribbleView}
*/
public class ScribbleHud extends LinearLayout {
private View drawButton;
private View highlightButton;
private View textButton;
private View stickerButton;
private View undoButton;
private View deleteButton;
private View confirmButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
private EventListener eventListener;
private ColorPaletteAdapter colorPaletteAdapter;
public ScribbleHud(@NonNull Context context) {
super(context);
initialize();
}
public ScribbleHud(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initialize();
}
public ScribbleHud(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize();
}
private void initialize() {
inflate(getContext(), R.layout.scribble_hud, this);
setOrientation(VERTICAL);
drawButton = findViewById(R.id.scribble_draw_button);
highlightButton = findViewById(R.id.scribble_highlight_button);
textButton = findViewById(R.id.scribble_text_button);
stickerButton = findViewById(R.id.scribble_sticker_button);
undoButton = findViewById(R.id.scribble_undo_button);
deleteButton = findViewById(R.id.scribble_delete_button);
confirmButton = findViewById(R.id.scribble_confirm_button);
colorPicker = findViewById(R.id.scribble_color_picker);
colorPalette = findViewById(R.id.scribble_color_palette);
initializeViews();
setMode(Mode.NONE);
}
private void initializeViews() {
undoButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onUndo();
}
});
deleteButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onDelete();
}
setMode(Mode.NONE);
});
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
colorPaletteAdapter = new ColorPaletteAdapter();
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
colorPalette.setAdapter(colorPaletteAdapter);
}
public void setColorPalette(@NonNull Set<Integer> colors) {
colorPaletteAdapter.setColors(colors);
}
public int getActiveColor() {
return colorPicker.getActiveColor();
}
public void setActiveColor(int color) {
colorPicker.setActiveColor(color);
}
public void setEventListener(@Nullable EventListener eventListener) {
this.eventListener = eventListener;
}
public void enterMode(@NonNull Mode mode) {
setMode(mode, false);
}
private void setMode(@NonNull Mode mode) {
setMode(mode, true);
}
private void setMode(@NonNull Mode mode, boolean notify) {
switch (mode) {
case NONE: presentModeNone(); break;
case DRAW: presentModeDraw(); break;
case HIGHLIGHT: presentModeHighlight(); break;
case TEXT: presentModeText(); break;
case STICKER: presentModeSticker(); break;
}
if (notify && eventListener != null) {
eventListener.onModeStarted(mode);
}
}
private void presentModeNone() {
drawButton.setVisibility(VISIBLE);
highlightButton.setVisibility(VISIBLE);
textButton.setVisibility(VISIBLE);
stickerButton.setVisibility(VISIBLE);
undoButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
confirmButton.setVisibility(GONE);
colorPicker.setVisibility(GONE);
colorPalette.setVisibility(GONE);
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
stickerButton.setOnClickListener(v -> setMode(Mode.STICKER));
}
private void presentModeDraw() {
confirmButton.setVisibility(VISIBLE);
undoButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
textButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.RED);
}
private void presentModeHighlight() {
confirmButton.setVisibility(VISIBLE);
undoButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
textButton.setVisibility(GONE);
deleteButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
colorPicker.setOnColorChangeListener(highlightOnColorChangeListener);
colorPicker.setActiveColor(Color.YELLOW);
}
private void presentModeText() {
confirmButton.setVisibility(VISIBLE);
deleteButton.setVisibility(VISIBLE);
colorPicker.setVisibility(VISIBLE);
colorPalette.setVisibility(VISIBLE);
textButton.setVisibility(GONE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
undoButton.setVisibility(GONE);
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
colorPicker.setActiveColor(Color.WHITE);
}
private void presentModeSticker() {
deleteButton.setVisibility(VISIBLE);
confirmButton.setVisibility(VISIBLE);
drawButton.setVisibility(GONE);
highlightButton.setVisibility(GONE);
textButton.setVisibility(GONE);
stickerButton.setVisibility(GONE);
undoButton.setVisibility(GONE);
colorPicker.setVisibility(GONE);
colorPalette.setVisibility(GONE);
}
private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = new VerticalSlideColorPicker.OnColorChangeListener() {
@Override
public void onColorChange(int selectedColor) {
if (eventListener != null) {
eventListener.onColorChange(selectedColor);
}
}
};
private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = new VerticalSlideColorPicker.OnColorChangeListener() {
@Override
public void onColorChange(int selectedColor) {
if (eventListener != null) {
int r = Color.red(selectedColor);
int g = Color.green(selectedColor);
int b = Color.blue(selectedColor);
int a = 128;
eventListener.onColorChange(Color.argb(a, r, g, b));
}
}
};
public enum Mode {
NONE, DRAW, HIGHLIGHT, TEXT, STICKER
}
public interface EventListener {
void onModeStarted(@NonNull Mode mode);
void onColorChange(int color);
void onUndo();
void onDelete();
}
}

View File

@@ -0,0 +1,186 @@
package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.net.Uri;
import android.os.Parcel;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.imageeditor.Bounds;
import org.thoughtcrime.securesms.imageeditor.Renderer;
import org.thoughtcrime.securesms.imageeditor.RendererContext;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequest;
import java.util.concurrent.ExecutionException;
/**
* Uses Glide to load an image and implements a {@link Renderer}.
*
* The image can be encrypted.
*/
final class UriGlideRenderer implements Renderer {
private final Uri imageUri;
private final Paint paint = new Paint();
private final Matrix imageProjectionMatrix = new Matrix();
private final Matrix temp = new Matrix();
private final boolean decryptable;
private final int maxWidth;
private final int maxHeight;
@Nullable
private Bitmap bitmap;
UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
this.imageUri = imageUri;
this.decryptable = decryptable;
this.maxWidth = maxWidth;
this.maxHeight = maxHeight;
paint.setAntiAlias(true);
}
@Override
public void render(@NonNull RendererContext rendererContext) {
if (getBitmap() == null) {
if (rendererContext.isBlockingLoad()) {
try {
Bitmap bitmap = getBitmapGlideRequest(rendererContext.context).submit().get();
setBitmap(rendererContext, bitmap);
} catch (ExecutionException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
getBitmapGlideRequest(rendererContext.context).into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
setBitmap(rendererContext, resource);
}
});
}
}
final Bitmap bitmap = getBitmap();
if (bitmap != null) {
rendererContext.save();
rendererContext.canvasMatrix.concat(imageProjectionMatrix);
// Units are image level pixels at this point.
int alpha = paint.getAlpha();
paint.setAlpha(rendererContext.getAlpha(alpha));
rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
paint.setAlpha(alpha);
rendererContext.restore();
} else {
// If failed to load, we draw a black out, in case image was sticker positioned to cover private info.
rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
}
}
private GlideRequest<Bitmap> getBitmapGlideRequest(@NonNull Context context) {
return GlideApp.with(context)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.override(maxWidth, maxHeight)
.centerInside()
.load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri);
}
@Override
public boolean hitTest(float x, float y) {
return pixelAlphaNotZero(x, y);
}
private boolean pixelAlphaNotZero(float x, float y) {
Bitmap bitmap = getBitmap();
if (bitmap == null) return false;
imageProjectionMatrix.invert(temp);
float[] onBmp = new float[2];
temp.mapPoints(onBmp, new float[]{ x, y });
int xInt = (int) onBmp[0];
int yInt = (int) onBmp[1];
if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
} else {
return false;
}
}
@Nullable
private Bitmap getBitmap() {
if (bitmap != null && bitmap.isRecycled()) {
bitmap = null;
}
return bitmap;
}
private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) {
this.bitmap = bitmap;
if (bitmap != null) {
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
imageProjectionMatrix.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
rendererContext.rendererReady.onReady(UriGlideRenderer.this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
}
}
private static Matrix cropMatrix(Bitmap bitmap) {
Matrix matrix = new Matrix();
if (bitmap.getWidth() > bitmap.getHeight()) {
matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
} else {
matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
}
return matrix;
}
public static final Creator<UriGlideRenderer> CREATOR = new Creator<UriGlideRenderer>() {
@Override
public UriGlideRenderer createFromParcel(Parcel in) {
return new UriGlideRenderer(Uri.parse(in.readString()),
in.readInt() == 1,
in.readInt(),
in.readInt()
);
}
@Override
public UriGlideRenderer[] newArray(int size) {
return new UriGlideRenderer[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(imageUri.toString());
dest.writeInt(decryptable ? 1 : 0);
dest.writeInt(maxWidth);
dest.writeInt(maxHeight);
}
}

View File

@@ -1,149 +0,0 @@
package org.thoughtcrime.securesms.scribbles.multitouch;
import android.content.Context;
import android.view.MotionEvent;
/**
* @author Almer Thie (code.almeros.com)
* Copyright (c) 2013, Almer Thie (code.almeros.com)
* <p>
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* <p>
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*/
public abstract class BaseGestureDetector {
/**
* This value is the threshold ratio between the previous combined pressure
* and the current combined pressure. When pressure decreases rapidly
* between events the position values can often be imprecise, as it usually
* indicates that the user is in the process of lifting a pointer off of the
* device. This value was tuned experimentally.
*/
protected static final float PRESSURE_THRESHOLD = 0.67f;
protected final Context mContext;
protected boolean mGestureInProgress;
protected MotionEvent mPrevEvent;
protected MotionEvent mCurrEvent;
protected float mCurrPressure;
protected float mPrevPressure;
protected long mTimeDelta;
public BaseGestureDetector(Context context) {
mContext = context;
}
/**
* All gesture detectors need to be called through this method to be able to
* detect gestures. This method delegates work to handler methods
* (handleStartProgressEvent, handleInProgressEvent) implemented in
* extending classes.
*
* @param event
* @return
*/
public boolean onTouchEvent(MotionEvent event) {
final int actionCode = event.getAction() & MotionEvent.ACTION_MASK;
if (!mGestureInProgress) {
handleStartProgressEvent(actionCode, event);
} else {
handleInProgressEvent(actionCode, event);
}
return true;
}
/**
* Called when the current event occurred when NO gesture is in progress
* yet. The handling in this implementation may set the gesture in progress
* (via mGestureInProgress) or out of progress
*
* @param actionCode
* @param event
*/
protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event);
/**
* Called when the current event occurred when a gesture IS in progress. The
* handling in this implementation may set the gesture out of progress (via
* mGestureInProgress).
*
* @param actionCode
* @param event
*/
protected abstract void handleInProgressEvent(int actionCode, MotionEvent event);
protected void updateStateByEvent(MotionEvent curr) {
final MotionEvent prev = mPrevEvent;
// Reset mCurrEvent
if (mCurrEvent != null) {
mCurrEvent.recycle();
mCurrEvent = null;
}
mCurrEvent = MotionEvent.obtain(curr);
// Delta time
mTimeDelta = curr.getEventTime() - prev.getEventTime();
// Pressure
mCurrPressure = curr.getPressure(curr.getActionIndex());
mPrevPressure = prev.getPressure(prev.getActionIndex());
}
protected void resetState() {
if (mPrevEvent != null) {
mPrevEvent.recycle();
mPrevEvent = null;
}
if (mCurrEvent != null) {
mCurrEvent.recycle();
mCurrEvent = null;
}
mGestureInProgress = false;
}
/**
* Returns {@code true} if a gesture is currently in progress.
*
* @return {@code true} if a gesture is currently in progress, {@code false} otherwise.
*/
public boolean isInProgress() {
return mGestureInProgress;
}
/**
* Return the time difference in milliseconds between the previous accepted
* GestureDetector event and the current GestureDetector event.
*
* @return Time difference since the last move event in milliseconds.
*/
public long getTimeDelta() {
return mTimeDelta;
}
/**
* Return the event time of the current GestureDetector event being
* processed.
*
* @return Current GestureDetector event time in milliseconds.
*/
public long getEventTime() {
return mCurrEvent.getEventTime();
}
}

View File

@@ -1,170 +0,0 @@
package org.thoughtcrime.securesms.scribbles.multitouch;
import android.content.Context;
import android.graphics.PointF;
import android.view.MotionEvent;
/**
* @author Almer Thie (code.almeros.com)
* Copyright (c) 2013, Almer Thie (code.almeros.com)
* <p>
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* <p>
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*/
public class MoveGestureDetector extends BaseGestureDetector {
private static final PointF FOCUS_DELTA_ZERO = new PointF();
private final OnMoveGestureListener mListener;
private PointF mCurrFocusInternal;
private PointF mPrevFocusInternal;
private PointF mFocusExternal = new PointF();
private PointF mFocusDeltaExternal = new PointF();
public MoveGestureDetector(Context context, OnMoveGestureListener listener) {
super(context);
mListener = listener;
}
@Override
protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_DOWN:
resetState(); // In case we missed an UP/CANCEL event
mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;
updateStateByEvent(event);
break;
case MotionEvent.ACTION_MOVE:
mGestureInProgress = mListener.onMoveBegin(this);
break;
}
}
@Override
protected void handleInProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mListener.onMoveEnd(this);
resetState();
break;
case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);
// Only accept the event if our relative pressure is within
// a certain limit. This can help filter shaky data as a
// finger is lifted.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
final boolean updatePrevious = mListener.onMove(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
break;
}
}
protected void updateStateByEvent(MotionEvent curr) {
super.updateStateByEvent(curr);
final MotionEvent prev = mPrevEvent;
// Focus intenal
mCurrFocusInternal = determineFocalPoint(curr);
mPrevFocusInternal = determineFocalPoint(prev);
// Focus external
// - Prevent skipping of focus delta when a finger is added or removed
boolean mSkipNextMoveEvent = prev.getPointerCount() != curr.getPointerCount();
mFocusDeltaExternal = mSkipNextMoveEvent ? FOCUS_DELTA_ZERO : new PointF(mCurrFocusInternal.x - mPrevFocusInternal.x, mCurrFocusInternal.y - mPrevFocusInternal.y);
// - Don't directly use mFocusInternal (or skipping will occur). Add
// unskipped delta values to mFocusExternal instead.
mFocusExternal.x += mFocusDeltaExternal.x;
mFocusExternal.y += mFocusDeltaExternal.y;
}
/**
* Determine (multi)finger focal point (a.k.a. center point between all
* fingers)
*
* @return PointF focal point
*/
private PointF determineFocalPoint(MotionEvent e) {
// Number of fingers on screen
final int pCount = e.getPointerCount();
float x = 0f;
float y = 0f;
for (int i = 0; i < pCount; i++) {
x += e.getX(i);
y += e.getY(i);
}
return new PointF(x / pCount, y / pCount);
}
public float getFocusX() {
return mFocusExternal.x;
}
public float getFocusY() {
return mFocusExternal.y;
}
public PointF getFocusDelta() {
return mFocusDeltaExternal;
}
/**
* Listener which must be implemented which is used by MoveGestureDetector
* to perform callbacks to any implementing class which is registered to a
* MoveGestureDetector via the constructor.
*
* @see MoveGestureDetector.SimpleOnMoveGestureListener
*/
public interface OnMoveGestureListener {
public boolean onMove(MoveGestureDetector detector);
public boolean onMoveBegin(MoveGestureDetector detector);
public void onMoveEnd(MoveGestureDetector detector);
}
/**
* Helper class which may be extended and where the methods may be
* implemented. This way it is not necessary to implement all methods
* of OnMoveGestureListener.
*/
public static class SimpleOnMoveGestureListener implements OnMoveGestureListener {
public boolean onMove(MoveGestureDetector detector) {
return false;
}
public boolean onMoveBegin(MoveGestureDetector detector) {
return true;
}
public void onMoveEnd(MoveGestureDetector detector) {
// Do nothing, overridden implementation may be used
}
}
}

View File

@@ -1,170 +0,0 @@
package org.thoughtcrime.securesms.scribbles.multitouch;
import android.content.Context;
import android.view.MotionEvent;
/**
* @author Almer Thie (code.almeros.com)
* Copyright (c) 2013, Almer Thie (code.almeros.com)
* <p>
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* <p>
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*/
public class RotateGestureDetector extends TwoFingerGestureDetector {
private static final String TAG = RotateGestureDetector.class.getSimpleName();
private final OnRotateGestureListener mListener;
private boolean mSloppyGesture;
public RotateGestureDetector(Context context, OnRotateGestureListener listener) {
super(context);
mListener = listener;
}
@Override
protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_POINTER_DOWN:
// At least the second finger is on screen now
resetState(); // In case we missed an UP/CANCEL event
mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;
updateStateByEvent(event);
// See if we have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if (!mSloppyGesture) {
// No, start gesture now
mGestureInProgress = mListener.onRotateBegin(this);
}
break;
case MotionEvent.ACTION_MOVE:
if (!mSloppyGesture) {
break;
}
// See if we still have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if (!mSloppyGesture) {
// No, start normal gesture now
mGestureInProgress = mListener.onRotateBegin(this);
}
break;
case MotionEvent.ACTION_POINTER_UP:
if (!mSloppyGesture) {
break;
}
break;
}
}
@Override
protected void handleInProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_POINTER_UP:
// Gesture ended but
updateStateByEvent(event);
if (!mSloppyGesture) {
mListener.onRotateEnd(this);
}
resetState();
break;
case MotionEvent.ACTION_CANCEL:
if (!mSloppyGesture) {
mListener.onRotateEnd(this);
}
resetState();
break;
case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);
// Only accept the event if our relative pressure is within
// a certain limit. This can help filter shaky data as a
// finger is lifted.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD) {
final boolean updatePrevious = mListener.onRotate(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
break;
}
}
@Override
protected void resetState() {
super.resetState();
mSloppyGesture = false;
}
/**
* Return the rotation difference from the previous rotate event to the current
* event.
*
* @return The current rotation //difference in degrees.
*/
public float getRotationDegreesDelta() {
double diffRadians = Math.atan2(mPrevFingerDiffY, mPrevFingerDiffX) - Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX);
return (float) (diffRadians * 180 / Math.PI);
}
/**
* Listener which must be implemented which is used by RotateGestureDetector
* to perform callbacks to any implementing class which is registered to a
* RotateGestureDetector via the constructor.
*
* @see RotateGestureDetector.SimpleOnRotateGestureListener
*/
public interface OnRotateGestureListener {
public boolean onRotate(RotateGestureDetector detector);
public boolean onRotateBegin(RotateGestureDetector detector);
public void onRotateEnd(RotateGestureDetector detector);
}
/**
* Helper class which may be extended and where the methods may be
* implemented. This way it is not necessary to implement all methods
* of OnRotateGestureListener.
*/
public static class SimpleOnRotateGestureListener implements OnRotateGestureListener {
public boolean onRotate(RotateGestureDetector detector) {
return false;
}
public boolean onRotateBegin(RotateGestureDetector detector) {
return true;
}
public void onRotateEnd(RotateGestureDetector detector) {
// Do nothing, overridden implementation may be used
}
}
}

View File

@@ -1,201 +0,0 @@
package org.thoughtcrime.securesms.scribbles.multitouch;
import android.content.Context;
import android.view.MotionEvent;
/**
* @author Robert Nordan (robert.nordan@norkart.no)
* <p>
* Copyright (c) 2013, Norkart AS
* <p>
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* <p>
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*/
public class ShoveGestureDetector extends TwoFingerGestureDetector {
private final OnShoveGestureListener mListener;
private float mPrevAverageY;
private float mCurrAverageY;
private boolean mSloppyGesture;
public ShoveGestureDetector(Context context, OnShoveGestureListener listener) {
super(context);
mListener = listener;
}
@Override
protected void handleStartProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_POINTER_DOWN:
// At least the second finger is on screen now
resetState(); // In case we missed an UP/CANCEL event
mPrevEvent = MotionEvent.obtain(event);
mTimeDelta = 0;
updateStateByEvent(event);
// See if we have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if (!mSloppyGesture) {
// No, start gesture now
mGestureInProgress = mListener.onShoveBegin(this);
}
break;
case MotionEvent.ACTION_MOVE:
if (!mSloppyGesture) {
break;
}
// See if we still have a sloppy gesture
mSloppyGesture = isSloppyGesture(event);
if (!mSloppyGesture) {
// No, start normal gesture now
mGestureInProgress = mListener.onShoveBegin(this);
}
break;
case MotionEvent.ACTION_POINTER_UP:
if (!mSloppyGesture) {
break;
}
break;
}
}
@Override
protected void handleInProgressEvent(int actionCode, MotionEvent event) {
switch (actionCode) {
case MotionEvent.ACTION_POINTER_UP:
// Gesture ended but
updateStateByEvent(event);
if (!mSloppyGesture) {
mListener.onShoveEnd(this);
}
resetState();
break;
case MotionEvent.ACTION_CANCEL:
if (!mSloppyGesture) {
mListener.onShoveEnd(this);
}
resetState();
break;
case MotionEvent.ACTION_MOVE:
updateStateByEvent(event);
// Only accept the event if our relative pressure is within
// a certain limit. This can help filter shaky data as a
// finger is lifted. Also check that shove is meaningful.
if (mCurrPressure / mPrevPressure > PRESSURE_THRESHOLD
&& Math.abs(getShovePixelsDelta()) > 0.5f) {
final boolean updatePrevious = mListener.onShove(this);
if (updatePrevious) {
mPrevEvent.recycle();
mPrevEvent = MotionEvent.obtain(event);
}
}
break;
}
}
@Override
protected void updateStateByEvent(MotionEvent curr) {
super.updateStateByEvent(curr);
final MotionEvent prev = mPrevEvent;
float py0 = prev.getY(0);
float py1 = prev.getY(1);
mPrevAverageY = (py0 + py1) / 2.0f;
float cy0 = curr.getY(0);
float cy1 = curr.getY(1);
mCurrAverageY = (cy0 + cy1) / 2.0f;
}
@Override
protected boolean isSloppyGesture(MotionEvent event) {
boolean sloppy = super.isSloppyGesture(event);
if (sloppy)
return true;
// If it's not traditionally sloppy, we check if the angle between fingers
// is acceptable.
double angle = Math.abs(Math.atan2(mCurrFingerDiffY, mCurrFingerDiffX));
//about 20 degrees, left or right
return !((0.0f < angle && angle < 0.35f)
|| 2.79f < angle && angle < Math.PI);
}
/**
* Return the distance in pixels from the previous shove event to the current
* event.
*
* @return The current distance in pixels.
*/
public float getShovePixelsDelta() {
return mCurrAverageY - mPrevAverageY;
}
@Override
protected void resetState() {
super.resetState();
mSloppyGesture = false;
mPrevAverageY = 0.0f;
mCurrAverageY = 0.0f;
}
/**
* Listener which must be implemented which is used by ShoveGestureDetector
* to perform callbacks to any implementing class which is registered to a
* ShoveGestureDetector via the constructor.
*
* @see ShoveGestureDetector.SimpleOnShoveGestureListener
*/
public interface OnShoveGestureListener {
public boolean onShove(ShoveGestureDetector detector);
public boolean onShoveBegin(ShoveGestureDetector detector);
public void onShoveEnd(ShoveGestureDetector detector);
}
/**
* Helper class which may be extended and where the methods may be
* implemented. This way it is not necessary to implement all methods
* of OnShoveGestureListener.
*/
public static class SimpleOnShoveGestureListener implements OnShoveGestureListener {
public boolean onShove(ShoveGestureDetector detector) {
return false;
}
public boolean onShoveBegin(ShoveGestureDetector detector) {
return true;
}
public void onShoveEnd(ShoveGestureDetector detector) {
// Do nothing, overridden implementation may be used
}
}
}

View File

@@ -1,189 +0,0 @@
package org.thoughtcrime.securesms.scribbles.multitouch;
import android.content.Context;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import org.thoughtcrime.securesms.logging.Log;
/**
* @author Almer Thie (code.almeros.com)
* Copyright (c) 2013, Almer Thie (code.almeros.com)
* <p>
* All rights reserved.
* <p>
* Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
* <p>
* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the distribution.
* <p>
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
* IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
* OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
* OF SUCH DAMAGE.
*/
public abstract class TwoFingerGestureDetector extends BaseGestureDetector {
private static final String TAG = TwoFingerGestureDetector.class.getSimpleName();
private final float mEdgeSlop;
protected float mPrevFingerDiffX;
protected float mPrevFingerDiffY;
protected float mCurrFingerDiffX;
protected float mCurrFingerDiffY;
private float mRightSlopEdge;
private float mBottomSlopEdge;
private float mCurrLen;
private float mPrevLen;
public TwoFingerGestureDetector(Context context) {
super(context);
ViewConfiguration config = ViewConfiguration.get(context);
mEdgeSlop = config.getScaledEdgeSlop();
}
@Override
protected abstract void handleStartProgressEvent(int actionCode, MotionEvent event);
@Override
protected abstract void handleInProgressEvent(int actionCode, MotionEvent event);
protected void updateStateByEvent(MotionEvent curr) {
super.updateStateByEvent(curr);
final MotionEvent prev = mPrevEvent;
mCurrLen = -1;
mPrevLen = -1;
// Previous
final float px0 = prev.getX(0);
final float py0 = prev.getY(0);
final float px1 = prev.getX(1);
final float py1 = prev.getY(1);
final float pvx = px1 - px0;
final float pvy = py1 - py0;
mPrevFingerDiffX = pvx;
mPrevFingerDiffY = pvy;
// Current
final float cx0 = curr.getX(0);
final float cy0 = curr.getY(0);
final float cx1 = curr.getX(1);
final float cy1 = curr.getY(1);
final float cvx = cx1 - cx0;
final float cvy = cy1 - cy0;
mCurrFingerDiffX = cvx;
mCurrFingerDiffY = cvy;
}
/**
* Return the current distance between the two pointers forming the
* gesture in progress.
*
* @return Distance between pointers in pixels.
*/
public float getCurrentSpan() {
if (mCurrLen == -1) {
final float cvx = mCurrFingerDiffX;
final float cvy = mCurrFingerDiffY;
mCurrLen = (float) Math.sqrt(cvx * cvx + cvy * cvy);
}
return mCurrLen;
}
/**
* Return the previous distance between the two pointers forming the
* gesture in progress.
*
* @return Previous distance between pointers in pixels.
*/
public float getPreviousSpan() {
if (mPrevLen == -1) {
final float pvx = mPrevFingerDiffX;
final float pvy = mPrevFingerDiffY;
mPrevLen = (float) Math.sqrt(pvx * pvx + pvy * pvy);
}
return mPrevLen;
}
/**
* Check if we have a sloppy gesture. Sloppy gestures can happen if the edge
* of the user's hand is touching the screen, for example.
*
* @param event
* @return
*/
protected boolean isSloppyGesture(MotionEvent event) {
// As orientation can change, query the metrics in touch down
DisplayMetrics metrics = mContext.getResources().getDisplayMetrics();
mRightSlopEdge = metrics.widthPixels - mEdgeSlop;
mBottomSlopEdge = metrics.heightPixels - mEdgeSlop;
final float edgeSlop = mEdgeSlop;
final float rightSlop = mRightSlopEdge;
final float bottomSlop = mBottomSlopEdge;
final float x0 = event.getRawX();
final float y0 = event.getRawY();
final float x1 = getRawX(event, 1);
final float y1 = getRawY(event, 1);
Log.d(TAG,
String.format("x0: %f, y0: %f, x1: %f, y1: %f, EdgeSlop: %f, RightSlop: %f, BottomSlop: %f",
x0, y0, x1, y1, edgeSlop, rightSlop, bottomSlop));
boolean p0sloppy = x0 < edgeSlop || y0 < edgeSlop
|| x0 > rightSlop || y0 > bottomSlop;
boolean p1sloppy = x1 < edgeSlop || y1 < edgeSlop
|| x1 > rightSlop || y1 > bottomSlop;
if (p0sloppy && p1sloppy) {
return true;
} else if (p0sloppy) {
return true;
} else if (p1sloppy) {
return true;
}
return false;
}
/**
* MotionEvent has no getRawX(int) method; simulate it pending future API approval.
*
* @param event
* @param pointerIndex
* @return
*/
protected static float getRawX(MotionEvent event, int pointerIndex) {
float offset = event.getX() - event.getRawX();
if (pointerIndex < event.getPointerCount()) {
return event.getX(pointerIndex) + offset;
}
return 0f;
}
/**
* MotionEvent has no getRawY(int) method; simulate it pending future API approval.
*
* @param event
* @param pointerIndex
* @return
*/
protected static float getRawY(MotionEvent event, int pointerIndex) {
float offset = Math.abs(event.getY() - event.getRawY());
if (pointerIndex < event.getPointerCount()) {
return event.getY(pointerIndex) + offset;
}
return 0f;
}
}

View File

@@ -1,83 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.viewmodel;
public class Font {
/**
* color value (ex: 0xFF00FF)
*/
private int color;
/**
* name of the font
*/
private String typeface;
/**
* size of the font, relative to parent
*/
private float size;
public Font() {
}
public void increaseSize(float diff) {
if (size + diff <= Limits.MAX_FONT_SIZE) {
size = size + diff;
}
}
public void decreaseSize(float diff) {
if (size - diff >= Limits.MIN_FONT_SIZE) {
size = size - diff;
}
}
public int getColor() {
return color;
}
public void setColor(int color) {
this.color = color;
}
public String getTypeface() {
return typeface;
}
public void setTypeface(String typeface) {
this.typeface = typeface;
}
public float getSize() {
return size;
}
public void setSize(float size) {
this.size = size;
}
private interface Limits {
float MIN_FONT_SIZE = 0.01F;
float MAX_FONT_SIZE = 0.46F;
}
}

View File

@@ -1,141 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.viewmodel;
import android.support.annotation.FloatRange;
import org.thoughtcrime.securesms.logging.Log;
public class Layer {
/**
* rotation relative to the layer center, in degrees
*/
@FloatRange(from = 0.0F, to = 360.0F)
private float rotationInDegrees;
private float scale;
/**
* top left X coordinate, relative to parent canvas
*/
private float x;
/**
* top left Y coordinate, relative to parent canvas
*/
private float y;
/**
* is layer flipped horizontally (by X-coordinate)
*/
private boolean isFlipped;
public Layer() {
reset();
}
protected void reset() {
this.rotationInDegrees = 0.0F;
this.scale = 1.0F;
this.isFlipped = false;
this.x = 0.0F;
this.y = 0.0F;
}
public void postScale(float scaleDiff) {
Log.i("Layer", "ScaleDiff: " + scaleDiff);
float newVal = scale + scaleDiff;
if (newVal >= getMinScale() && newVal <= getMaxScale()) {
scale = newVal;
}
}
protected float getMaxScale() {
return Limits.MAX_SCALE;
}
protected float getMinScale() {
return Limits.MIN_SCALE;
}
public void postRotate(float rotationInDegreesDiff) {
this.rotationInDegrees += rotationInDegreesDiff;
this.rotationInDegrees %= 360.0F;
}
public void postTranslate(float dx, float dy) {
this.x += dx;
this.y += dy;
}
public void flip() {
this.isFlipped = !isFlipped;
}
public float initialScale() {
return Limits.INITIAL_ENTITY_SCALE;
}
public float getRotationInDegrees() {
return rotationInDegrees;
}
public void setRotationInDegrees(@FloatRange(from = 0.0, to = 360.0) float rotationInDegrees) {
this.rotationInDegrees = rotationInDegrees;
}
public float getScale() {
return scale;
}
public void setScale(float scale) {
this.scale = scale;
}
public float getX() {
return x;
}
public void setX(float x) {
this.x = x;
}
public float getY() {
return y;
}
public void setY(float y) {
this.y = y;
}
public boolean isFlipped() {
return isFlipped;
}
public void setFlipped(boolean flipped) {
isFlipped = flipped;
}
interface Limits {
float MIN_SCALE = 0.06F;
float MAX_SCALE = 4.0F;
float INITIAL_ENTITY_SCALE = 0.4F;
}
}

View File

@@ -1,94 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.viewmodel;
public class TextLayer extends Layer {
private String text;
private Font font;
public TextLayer() {
}
@Override
protected void reset() {
super.reset();
this.text = "";
this.font = new Font();
}
@Override
protected float getMaxScale() {
return Limits.MAX_SCALE;
}
@Override
protected float getMinScale() {
return Limits.MIN_SCALE;
}
@Override
public float initialScale() {
return Limits.INITIAL_SCALE;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Font getFont() {
return font;
}
public void setFont(Font font) {
this.font = font;
}
@Override
public void postScale(float scaleDiff) {
if (scaleDiff > 0) font.increaseSize(scaleDiff);
else if (scaleDiff < 0) font.decreaseSize(Math.abs(scaleDiff));
}
public interface Limits {
/**
* limit text size to view bounds
* so that users don't put small font size and scale it 100+ times
*/
float MAX_SCALE = 1.0F;
float MIN_SCALE = 0.2F;
float MIN_BITMAP_HEIGHT = 0.13F;
float FONT_SIZE_STEP = 0.008F;
float INITIAL_FONT_SIZE = 0.1F;
int INITIAL_FONT_COLOR = 0xff000000;
float INITIAL_SCALE = 0.8F; // set the same to avoid text scaling
}
}

View File

@@ -1,881 +0,0 @@
/**
* CanvasView.java
*
* Copyright (c) 2014 Tomohiro IKEDA (Korilakkuma)
* Released under the MIT license
*/
package org.thoughtcrime.securesms.scribbles.widget;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* This class defines fields and methods for drawing.
*/
public class CanvasView extends View {
private static final String TAG = CanvasView.class.getSimpleName();
public static final int DEFAULT_STROKE_WIDTH = 15;
// Enumeration for Mode
public enum Mode {
DRAW,
TEXT,
ERASER;
}
// Enumeration for Drawer
public enum Drawer {
PEN,
LINE,
RECTANGLE,
CIRCLE,
ELLIPSE,
QUADRATIC_BEZIER,
QUBIC_BEZIER;
}
private int initialWidth = 0;
private int initialHeight = 0;
private int canvasWidth = 1;
private int canvasHeight = 1;
private Bitmap bitmap = null;
private List<Path> pathLists = new ArrayList<Path>();
private List<Paint> paintLists = new ArrayList<Paint>();
// for Eraser
// private int baseColor = Color.WHITE;
private int baseColor = Color.TRANSPARENT;
// for Undo, Redo
private int historyPointer = 0;
// Flags
private Mode mode = Mode.DRAW;
private Drawer drawer = Drawer.PEN;
private boolean isDown = false;
// for Paint
private Paint.Style paintStyle = Paint.Style.STROKE;
private int paintStrokeColor = Color.BLACK;
private int paintFillColor = Color.BLACK;
private float paintStrokeWidth = DEFAULT_STROKE_WIDTH;
private int opacity = 255;
private float blur = 0F;
private Paint.Cap lineCap = Paint.Cap.ROUND;
// for Drawer
private float startX = 0F;
private float startY = 0F;
private float controlX = 0F;
private float controlY = 0F;
private boolean active = false;
/**
* Copy Constructor
*
* @param context
* @param attrs
* @param defStyle
*/
public CanvasView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.setup();
}
/**
* Copy Constructor
*
* @param context
* @param attrs
*/
public CanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
this.setup();
}
/**
* Copy Constructor
*
* @param context
*/
public CanvasView(Context context) {
super(context);
}
private void setup() {
this.pathLists.add(new Path());
this.paintLists.add(this.createPaint());
this.historyPointer++;
}
/**
* This method creates the instance of Paint.
* In addition, this method sets styles for Paint.
*
* @return paint This is returned as the instance of Paint
*/
private Paint createPaint() {
Paint paint = new Paint();
paint.setAntiAlias(true);
paint.setStyle(this.paintStyle);
paint.setStrokeWidth(this.paintStrokeWidth);
paint.setStrokeCap(this.lineCap);
paint.setStrokeJoin(Paint.Join.ROUND); // fixed
if (this.mode == Mode.ERASER) {
// Eraser
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
paint.setARGB(0, 0, 0, 0);
// paint.setColor(this.baseColor);
// paint.setShadowLayer(this.blur, 0F, 0F, this.baseColor);
} else {
// Otherwise
paint.setColor(this.paintStrokeColor);
paint.setShadowLayer(this.blur, 0F, 0F, this.paintStrokeColor);
paint.setAlpha(this.opacity);
}
return paint;
}
/**
* This method initialize Path.
* Namely, this method creates the instance of Path,
* and moves current position.
*
* @param event This is argument of onTouchEvent method
* @return path This is returned as the instance of Path
*/
private Path createPath(MotionEvent event) {
Path path = new Path();
// Save for ACTION_MOVE
this.startX = scaleX(event.getX());
this.startY = scaleY(event.getY());
path.moveTo(this.startX, this.startY);
return path;
}
/**
* This method updates the lists for the instance of Path and Paint.
* "Undo" and "Redo" are enabled by this method.
*
* @param path the instance of Path
*/
private void updateHistory(Path path) {
if (this.historyPointer == this.pathLists.size()) {
this.pathLists.add(path);
this.paintLists.add(this.createPaint());
this.historyPointer++;
} else {
// On the way of Undo or Redo
this.pathLists.set(this.historyPointer, path);
this.paintLists.set(this.historyPointer, this.createPaint());
this.historyPointer++;
for (int i = this.historyPointer, size = this.paintLists.size(); i < size; i++) {
this.pathLists.remove(this.historyPointer);
this.paintLists.remove(this.historyPointer);
}
}
}
/**
* This method gets the instance of Path that pointer indicates.
*
* @return the instance of Path
*/
private Path getCurrentPath() {
return this.pathLists.get(this.historyPointer - 1);
}
/**
* This method defines processes on MotionEvent.ACTION_DOWN
*
* @param event This is argument of onTouchEvent method
*/
private void onActionDown(MotionEvent event) {
switch (this.mode) {
case DRAW :
case ERASER :
if ((this.drawer != Drawer.QUADRATIC_BEZIER) && (this.drawer != Drawer.QUBIC_BEZIER)) {
// Oherwise
this.updateHistory(this.createPath(event));
this.isDown = true;
} else {
// Bezier
if ((this.startX == 0F) && (this.startY == 0F)) {
// The 1st tap
this.updateHistory(this.createPath(event));
} else {
// The 2nd tap
this.controlX = event.getX();
this.controlY = event.getY();
this.isDown = true;
}
}
break;
case TEXT :
this.startX = event.getX();
this.startY = event.getY();
break;
default :
break;
}
}
/**
* This method defines processes on MotionEvent.ACTION_MOVE
*
* @param event This is argument of onTouchEvent method
*/
private void onActionMove(MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (this.mode) {
case DRAW :
case ERASER :
if ((this.drawer != Drawer.QUADRATIC_BEZIER) && (this.drawer != Drawer.QUBIC_BEZIER)) {
if (!isDown) {
return;
}
Path path = this.getCurrentPath();
switch (this.drawer) {
case PEN :
for (int i = 0; i < event.getHistorySize(); i++) {
path.lineTo(scaleX(event.getHistoricalX(i)), scaleY(event.getHistoricalY(i)));
}
break;
case LINE :
path.reset();
path.moveTo(this.startX, this.startY);
path.lineTo(x, y);
break;
case RECTANGLE :
path.reset();
path.addRect(this.startX, this.startY, x, y, Path.Direction.CCW);
break;
case CIRCLE :
double distanceX = Math.abs((double)(this.startX - x));
double distanceY = Math.abs((double)(this.startX - y));
double radius = Math.sqrt(Math.pow(distanceX, 2.0) + Math.pow(distanceY, 2.0));
path.reset();
path.addCircle(this.startX, this.startY, (float)radius, Path.Direction.CCW);
break;
case ELLIPSE :
RectF rect = new RectF(this.startX, this.startY, x, y);
path.reset();
path.addOval(rect, Path.Direction.CCW);
break;
default :
break;
}
} else {
if (!isDown) {
return;
}
Path path = this.getCurrentPath();
path.reset();
path.moveTo(scaleX(this.startX), scaleY(this.startY));
path.quadTo(this.controlX, this.controlY, x, y);
}
break;
case TEXT :
this.startX = x;
this.startY = y;
break;
default :
break;
}
}
/**
* This method defines processes on MotionEvent.ACTION_DOWN
*
* @param event This is argument of onTouchEvent method
*/
private void onActionUp(MotionEvent event) {
if (isDown) {
this.startX = 0F;
this.startY = 0F;
this.isDown = false;
}
}
public SavedState saveState() {
return new SavedState(pathLists, paintLists, historyPointer, initialWidth, initialHeight, canvasWidth, canvasHeight);
}
public void restoreState(@NonNull SavedState state) {
this.pathLists.clear();
this.pathLists.addAll(state.getPaths());
this.paintLists.clear();
this.paintLists.addAll(state.getPaints());
this.historyPointer = state.getHistoryPointer();
this.initialWidth = state.getInitialWidth();
this.initialHeight = state.getInitialHeight();
postInvalidate();
}
public void setActive(boolean active) {
this.active = active;
}
/**
* This method updates the instance of Canvas (View)
*
* @param canvas the new instance of Canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawColor(this.baseColor);
render(canvas);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
this.canvasWidth = w;
this.canvasHeight = h;
if (initialWidth == 0) {
initialWidth = canvasWidth;
}
if (initialHeight == 0) {
initialHeight = canvasHeight;
}
}
public void render(Canvas canvas) {
render(canvas, initialWidth, initialHeight, canvasWidth, canvasHeight, pathLists, paintLists, historyPointer);
}
public static void render(Canvas canvas, int initialWidth, int initialHeight, int canvasWidth, int canvasHeight, List<Path> pathLists, List<Paint> paintLists, int historyPointer) {
float scaleX = 1f;
float scaleY = 1f;
if (initialWidth > 0) {
scaleX *= (float) canvasWidth / initialWidth;
}
if (initialHeight > 0) {
scaleY *= (float) canvasHeight / initialHeight;
}
scaleX *= (float) canvas.getWidth() / canvasWidth;
scaleY *= (float) canvas.getHeight() / canvasHeight;
Matrix matrix = new Matrix();
matrix.setScale(scaleX, scaleY);
for (int i = 0; i < historyPointer; i++) {
Path path = pathLists.get(i);
Paint paint = paintLists.get(i);
Path scaledPath = new Path();
path.transform(matrix, scaledPath);
Paint scaledPaint = new Paint(paint);
scaledPaint.setStrokeWidth(scaledPaint.getStrokeWidth() * scaleX);
canvas.drawPath(scaledPath, scaledPaint);
}
}
/**
* This method set event listener for drawing.
*
* @param event the instance of MotionEvent
* @return
*/
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!active) return false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.onActionDown(event);
break;
case MotionEvent.ACTION_MOVE :
this.onActionMove(event);
break;
case MotionEvent.ACTION_UP :
this.onActionUp(event);
break;
default :
break;
}
// Re draw
this.invalidate();
return true;
}
/**
* This method is getter for mode.
*
* @return
*/
public Mode getMode() {
return this.mode;
}
/**
* This method is setter for mode.
*
* @param mode
*/
public void setMode(Mode mode) {
this.mode = mode;
}
/**
* This method is getter for drawer.
*
* @return
*/
public Drawer getDrawer() {
return this.drawer;
}
/**
* This method is setter for drawer.
*
* @param drawer
*/
public void setDrawer(Drawer drawer) {
this.drawer = drawer;
}
/**
* This method draws canvas again for Undo.
*
* @return If Undo is enabled, this is returned as true. Otherwise, this is returned as false.
*/
public boolean undo() {
if (this.historyPointer > 1) {
this.historyPointer--;
this.invalidate();
return true;
} else {
return false;
}
}
/**
* This method draws canvas again for Redo.
*
* @return If Redo is enabled, this is returned as true. Otherwise, this is returned as false.
*/
public boolean redo() {
if (this.historyPointer < this.pathLists.size()) {
this.historyPointer++;
this.invalidate();
return true;
} else {
return false;
}
}
/**
* This method initializes canvas.
*
* @return
*/
public void clear() {
Path path = new Path();
path.moveTo(0F, 0F);
path.addRect(0F, 0F, 1000F, 1000F, Path.Direction.CCW);
path.close();
Paint paint = new Paint();
paint.setColor(Color.WHITE);
paint.setStyle(Paint.Style.FILL);
if (this.historyPointer == this.pathLists.size()) {
this.pathLists.add(path);
this.paintLists.add(paint);
this.historyPointer++;
} else {
// On the way of Undo or Redo
this.pathLists.set(this.historyPointer, path);
this.paintLists.set(this.historyPointer, paint);
this.historyPointer++;
for (int i = this.historyPointer, size = this.paintLists.size(); i < size; i++) {
this.pathLists.remove(this.historyPointer);
this.paintLists.remove(this.historyPointer);
}
}
// Clear
this.invalidate();
}
/**
* This method is getter for canvas background color
*
* @return
*/
public int getBaseColor() {
return this.baseColor;
}
/**
* This method is setter for canvas background color
*
* @param color
*/
public void setBaseColor(int color) {
this.baseColor = color;
}
/**
* This method is getter for stroke or fill.
*
* @return
*/
public Paint.Style getPaintStyle() {
return this.paintStyle;
}
/**
* This method is setter for stroke or fill.
*
* @param style
*/
public void setPaintStyle(Paint.Style style) {
this.paintStyle = style;
}
/**
* This method is getter for stroke color.
*
* @return
*/
public int getPaintStrokeColor() {
return this.paintStrokeColor;
}
/**
* This method is setter for stroke color.
*
* @param color
*/
public void setPaintStrokeColor(int color) {
this.paintStrokeColor = color;
}
/**
* This method is getter for fill color.
* But, current Android API cannot set fill color (?).
*
* @return
*/
public int getPaintFillColor() {
return this.paintFillColor;
};
/**
* This method is setter for fill color.
* But, current Android API cannot set fill color (?).
*
* @param color
*/
public void setPaintFillColor(int color) {
this.paintFillColor = color;
}
/**
* This method is getter for stroke width.
*
* @return
*/
public float getPaintStrokeWidth() {
return this.paintStrokeWidth;
}
/**
* This method is setter for stroke width.
*
* @param width
*/
public void setPaintStrokeWidth(float width) {
if (width >= 0) {
this.paintStrokeWidth = width;
} else {
this.paintStrokeWidth = 3F;
}
}
/**
* This method is getter for alpha.
*
* @return
*/
public int getOpacity() {
return this.opacity;
}
/**
* This method is setter for alpha.
* The 1st argument must be between 0 and 255.
*
* @param opacity
*/
public void setOpacity(int opacity) {
if ((opacity >= 0) && (opacity <= 255)) {
this.opacity = opacity;
} else {
this.opacity= 255;
}
}
/**
* This method is getter for amount of blur.
*
* @return
*/
public float getBlur() {
return this.blur;
}
/**
* This method is setter for amount of blur.
* The 1st argument is greater than or equal to 0.0.
*
* @param blur
*/
public void setBlur(float blur) {
if (blur >= 0) {
this.blur = blur;
} else {
this.blur = 0F;
}
}
/**
* This method is getter for line cap.
*
* @return
*/
public Paint.Cap getLineCap() {
return this.lineCap;
}
/**
* This method is setter for line cap.
*
* @param cap
*/
public void setLineCap(Paint.Cap cap) {
this.lineCap = cap;
}
/**
* This method gets current canvas as bitmap.
*
* @return This is returned as bitmap.
*/
public Bitmap getBitmap() {
this.setDrawingCacheEnabled(false);
this.setDrawingCacheEnabled(true);
return Bitmap.createBitmap(this.getDrawingCache());
}
/**
* This method gets current canvas as scaled bitmap.
*
* @return This is returned as scaled bitmap.
*/
public Bitmap getScaleBitmap(int w, int h) {
this.setDrawingCacheEnabled(false);
this.setDrawingCacheEnabled(true);
return Bitmap.createScaledBitmap(this.getDrawingCache(), w, h, true);
}
/**
* This method draws the designated bitmap to canvas.
*
* @param bitmap
*/
public void drawBitmap(Bitmap bitmap) {
this.bitmap = bitmap;
this.invalidate();
}
/**
* This method draws the designated byte array of bitmap to canvas.
*
* @param byteArray This is returned as byte array of bitmap.
*/
public void drawBitmap(byte[] byteArray) {
this.drawBitmap(BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length));
}
/**
* This static method gets the designated bitmap as byte array.
*
* @param bitmap
* @param format
* @param quality
* @return This is returned as byte array of bitmap.
*/
public static byte[] getBitmapAsByteArray(Bitmap bitmap, CompressFormat format, int quality) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
bitmap.compress(format, quality, byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
}
/**
* This method gets the bitmap as byte array.
*
* @param format
* @param quality
* @return This is returned as byte array of bitmap.
*/
public byte[] getBitmapAsByteArray(CompressFormat format, int quality) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
this.getBitmap().compress(format, quality, byteArrayOutputStream);
return byteArrayOutputStream.toByteArray();
}
/**
* This method gets the bitmap as byte array.
* Bitmap format is PNG, and quality is 100.
*
* @return This is returned as byte array of bitmap.
*/
public byte[] getBitmapAsByteArray() {
return this.getBitmapAsByteArray(CompressFormat.PNG, 100);
}
public @NonNull Set<Integer> getUniqueColors() {
Set<Integer> colors = new LinkedHashSet<>();
for (int i = 1; i < paintLists.size() && i < historyPointer; i++) {
int color = paintLists.get(i).getColor();
colors.add(Color.rgb(Color.red(color), Color.green(color), Color.blue(color)));
}
return colors;
}
private float scaleX(float x) {
return ((float) initialWidth / canvasWidth) * x;
}
private float scaleY(float y) {
return ((float) initialWidth / canvasWidth) * y;
}
static class SavedState {
private final List<Path> paths;
private final List<Paint> paints;
private final int historyPointer;
private final int initialWidth;
private final int initialHeight;
private final int canvasWidth;
private final int canvasHeight;
SavedState(List<Path> paths, List<Paint> paints, int historyPointer, int initialWidth, int initialHeight, int canvasWidth, int canvasHeight) {
this.paths = new ArrayList<>(paths);
this.paints = new ArrayList<>(paints);
this.historyPointer = historyPointer;
this.initialWidth = initialWidth;
this.initialHeight = initialHeight;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
}
List<Path> getPaths() {
return paths;
}
List<Paint> getPaints() {
return paints;
}
int getHistoryPointer() {
return historyPointer;
}
int getInitialWidth() {
return initialWidth;
}
int getInitialHeight() {
return initialHeight;
}
int getCanvasWidth() {
return canvasWidth;
}
int getCanvasHeight() {
return canvasHeight;
}
boolean isEmpty() {
return paths.size() <= 1;
}
}
}

View File

@@ -1,526 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.widget;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.GestureDetectorCompat;
import android.support.v4.view.ViewCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.scribbles.multitouch.MoveGestureDetector;
import org.thoughtcrime.securesms.scribbles.multitouch.RotateGestureDetector;
import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
public class MotionView extends FrameLayout implements TextWatcher {
private static final String TAG = MotionView.class.getSimpleName();
public interface Constants {
float SELECTED_LAYER_ALPHA = 0.15F;
}
public interface MotionViewCallback {
void onEntitySelected(@Nullable MotionEntity entity);
void onEntityDoubleTap(@NonNull MotionEntity entity);
}
// layers
private final List<MotionEntity> entities = new ArrayList<>();
@Nullable
private MotionEntity selectedEntity;
private Paint selectedLayerPaint;
// callback
@Nullable
private MotionViewCallback motionViewCallback;
private EditText editText;
// gesture detection
private ScaleGestureDetector scaleGestureDetector;
private RotateGestureDetector rotateGestureDetector;
private MoveGestureDetector moveGestureDetector;
private GestureDetectorCompat gestureDetectorCompat;
// constructors
public MotionView(Context context) {
super(context);
init(context, null);
}
public MotionView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public MotionView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
@SuppressWarnings("unused")
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public MotionView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context, attrs);
}
private void init(@NonNull Context context, @Nullable AttributeSet attrs) {
// I fucking love Android
setWillNotDraw(false);
selectedLayerPaint = new Paint();
selectedLayerPaint.setAlpha((int) (255 * Constants.SELECTED_LAYER_ALPHA));
selectedLayerPaint.setAntiAlias(true);
this.editText = new EditText(context, attrs);
ViewCompat.setAlpha(this.editText, 0);
this.editText.setLayoutParams(new LayoutParams(1, 1, Gravity.TOP | Gravity.LEFT));
this.editText.setClickable(false);
this.editText.setBackgroundColor(Color.TRANSPARENT);
this.editText.setTextSize(TypedValue.COMPLEX_UNIT_SP, 1);
this.editText.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
this.editText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
this.addView(editText);
this.editText.clearFocus();
this.editText.addTextChangedListener(this);
this.editText.setId(R.id.motion_view_edittext);
// init listeners
this.scaleGestureDetector = new ScaleGestureDetector(context, new ScaleListener());
this.rotateGestureDetector = new RotateGestureDetector(context, new RotateListener());
this.moveGestureDetector = new MoveGestureDetector(context, new MoveListener());
this.gestureDetectorCompat = new GestureDetectorCompat(context, new TapsListener());
setOnTouchListener(onTouchListener);
updateUI();
}
public SavedState saveState() {
return new SavedState(entities);
}
public void restoreState(@NonNull SavedState savedState) {
this.entities.clear();
this.entities.addAll(savedState.getEntities());
postInvalidate();
}
public void startEditing(TextEntity entity) {
editText.setFocusableInTouchMode(true);
editText.setFocusable(true);
editText.requestFocus();
editText.setText(entity.getLayer().getText());
Selection.setSelection(editText.getText(), editText.length());
InputMethodManager ims = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
ims.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT);
}
public MotionEntity getSelectedEntity() {
return selectedEntity;
}
public List<MotionEntity> getEntities() {
return entities;
}
public void setMotionViewCallback(@Nullable MotionViewCallback callback) {
this.motionViewCallback = callback;
}
public void addEntity(@Nullable MotionEntity entity) {
if (entity != null) {
entities.add(entity);
selectEntity(entity, false);
}
}
public void addEntityAndPosition(@Nullable MotionEntity entity) {
if (entity != null) {
initEntityBorder(entity);
initialTranslateAndScale(entity);
entities.add(entity);
selectEntity(entity, true);
}
}
public @NonNull Set<Integer> getUniqueColors() {
Set<Integer> colors = new LinkedHashSet<>();
for (MotionEntity entity : entities) {
if (entity instanceof TextEntity) {
colors.add(((TextEntity) entity).getLayer().getFont().getColor());
}
}
return colors;
}
private void initEntityBorder(@NonNull MotionEntity entity ) {
// init stroke
int strokeSize = getResources().getDimensionPixelSize(R.dimen.scribble_stroke_size);
Paint borderPaint = new Paint();
borderPaint.setStrokeWidth(strokeSize);
borderPaint.setAntiAlias(true);
borderPaint.setColor(getContext().getResources().getColor(R.color.sticker_selected_color));
entity.setBorderPaint(borderPaint);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
// dispatch draw is called after child views is drawn.
// the idea that is we draw background stickers, than child views (if any), and than selected item
// to draw on top of child views - do it in dispatchDraw(Canvas)
// to draw below that - do it in onDraw(Canvas)
if (selectedEntity != null) {
selectedEntity.draw(canvas, selectedLayerPaint);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
render(canvas, entities);
}
public void render(Canvas canvas) {
unselectEntity();
draw(canvas);
}
public static void render(Canvas canvas, List<MotionEntity> entities) {
for (int i = 0; i < entities.size(); i++) {
entities.get(i).draw(canvas, null);
}
}
/**
* as a side effect - the method deselects Entity (if any selected)
* @return bitmap with all the Entities at their current positions
*/
public Bitmap getThumbnailImage() {
selectEntity(null, false);
Bitmap bmp = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
// IMPORTANT: always create white background, cos if the image is saved in JPEG format,
// which doesn't have transparent pixels, the background will be black
bmp.eraseColor(Color.WHITE);
Canvas canvas = new Canvas(bmp);
render(canvas, entities);
return bmp;
}
private void updateUI() {
invalidate();
}
private void handleTranslate(PointF delta) {
if (selectedEntity != null) {
float newCenterX = selectedEntity.absoluteCenterX() + delta.x;
float newCenterY = selectedEntity.absoluteCenterY() + delta.y;
// limit entity center to screen bounds
boolean needUpdateUI = false;
if (newCenterX >= 0 && newCenterX <= getWidth()) {
selectedEntity.getLayer().postTranslate(delta.x / getWidth(), 0.0F);
needUpdateUI = true;
}
if (newCenterY >= 0 && newCenterY <= getHeight()) {
selectedEntity.getLayer().postTranslate(0.0F, delta.y / getHeight());
needUpdateUI = true;
}
if (needUpdateUI) {
updateUI();
}
}
}
private void initialTranslateAndScale(@NonNull MotionEntity entity) {
entity.moveToCanvasCenter();
entity.getLayer().setScale(entity.getLayer().initialScale());
}
private void selectEntity(@Nullable MotionEntity entity, boolean updateCallback) {
if (selectedEntity != null && entity != selectedEntity) {
selectedEntity.setIsSelected(false);
if (selectedEntity instanceof TextEntity) {
if (TextUtils.isEmpty(((TextEntity) selectedEntity).getLayer().getText())) {
deletedSelectedEntity();
} else {
editText.clearComposingText();
editText.clearFocus();
}
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editText.getWindowToken(), 0);
}
}
if (entity != null) {
entity.setIsSelected(true);
}
selectedEntity = entity;
invalidate();
if (updateCallback && motionViewCallback != null) {
motionViewCallback.onEntitySelected(entity);
}
}
public void unselectEntity() {
if (selectedEntity != null) {
selectEntity(null, false);
}
}
@Nullable
private MotionEntity findEntityAtPoint(float x, float y) {
MotionEntity selected = null;
PointF p = new PointF(x, y);
for (int i = entities.size() - 1; i >= 0; i--) {
if (entities.get(i).pointInLayerRect(p)) {
selected = entities.get(i);
break;
}
}
return selected;
}
private void updateSelectionOnTap(MotionEvent e) {
MotionEntity entity = findEntityAtPoint(e.getX(), e.getY());
selectEntity(entity, true);
}
private void updateOnLongPress(MotionEvent e) {
// if layer is currently selected and point inside layer - move it to front
if (selectedEntity != null) {
PointF p = new PointF(e.getX(), e.getY());
if (selectedEntity.pointInLayerRect(p)) {
bringLayerToFront(selectedEntity);
}
}
}
private void bringLayerToFront(@NonNull MotionEntity entity) {
// removing and adding brings layer to front
if (entities.remove(entity)) {
entities.add(entity);
invalidate();
}
}
private void moveEntityToBack(@Nullable MotionEntity entity) {
if (entity == null) {
return;
}
if (entities.remove(entity)) {
entities.add(0, entity);
invalidate();
}
}
public void flipSelectedEntity() {
if (selectedEntity == null) {
return;
}
selectedEntity.getLayer().flip();
invalidate();
}
public void moveSelectedBack() {
moveEntityToBack(selectedEntity);
}
public void deletedSelectedEntity() {
if (selectedEntity == null) {
return;
}
if (entities.remove(selectedEntity)) {
selectedEntity.release();
selectedEntity = null;
invalidate();
}
}
// memory
public void release() {
for (MotionEntity entity : entities) {
entity.release();
}
}
// gesture detectors
private final View.OnTouchListener onTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
if (scaleGestureDetector != null) {
scaleGestureDetector.onTouchEvent(event);
rotateGestureDetector.onTouchEvent(event);
moveGestureDetector.onTouchEvent(event);
gestureDetectorCompat.onTouchEvent(event);
}
return true;
}
};
private class TapsListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDoubleTap(MotionEvent e) {
if (motionViewCallback != null && selectedEntity != null) {
motionViewCallback.onEntityDoubleTap(selectedEntity);
}
return true;
}
@Override
public void onLongPress(MotionEvent e) {
updateOnLongPress(e);
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
updateSelectionOnTap(e);
return true;
}
@Override
public boolean onDown(MotionEvent e) {
updateSelectionOnTap(e);
return false;
}
}
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
@Override
public boolean onScale(ScaleGestureDetector detector) {
if (selectedEntity != null) {
float scaleFactorDiff = detector.getScaleFactor();
Log.d(TAG, "ScaleFactorDiff: " + scaleFactorDiff);
selectedEntity.getLayer().postScale(scaleFactorDiff - 1.0F);
selectedEntity.updateEntity();
updateUI();
}
return true;
}
}
private class RotateListener extends RotateGestureDetector.SimpleOnRotateGestureListener {
@Override
public boolean onRotate(RotateGestureDetector detector) {
if (selectedEntity != null) {
selectedEntity.getLayer().postRotate(-detector.getRotationDegreesDelta());
updateUI();
}
return true;
}
}
private class MoveListener extends MoveGestureDetector.SimpleOnMoveGestureListener {
@Override
public boolean onMove(MoveGestureDetector detector) {
handleTranslate(detector.getFocusDelta());
return true;
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
String text = s.toString();
MotionEntity entity = getSelectedEntity();
if (entity != null && entity instanceof TextEntity) {
TextEntity textEntity = (TextEntity)entity;
if (!textEntity.getLayer().getText().equals(text)) {
textEntity.getLayer().setText(text);
textEntity.updateEntity();
MotionView.this.invalidate();
}
}
}
static class SavedState {
private final List<MotionEntity> entities;
SavedState(List<MotionEntity> entities) {
this.entities = new ArrayList<>(entities);
Stream.of(entities).forEach(e -> e.setIsSelected(false));
}
List<MotionEntity> getEntities() {
return entities;
}
boolean isEmpty() {
return entities.isEmpty();
}
}
}

View File

@@ -1,270 +0,0 @@
/*
* Copyright (C) 2016 Open Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.scribbles.widget;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.FrameLayout;
import android.widget.ImageView;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.request.target.Target;
import com.bumptech.glide.request.transition.Transition;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.util.LinkedHashSet;
import java.util.Set;
public class ScribbleView extends FrameLayout {
private static final String TAG = ScribbleView.class.getSimpleName();
public static final int DEFAULT_BRUSH_WIDTH = CanvasView.DEFAULT_STROKE_WIDTH;
private ImageView imageView;
private MotionView motionView;
private CanvasView canvasView;
private @Nullable Uri imageUri;
public ScribbleView(Context context) {
super(context);
initialize(context);
}
public ScribbleView(Context context, AttributeSet attrs) {
super(context, attrs);
initialize(context);
}
public ScribbleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initialize(context);
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public ScribbleView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initialize(context);
}
public void setImage(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
this.imageUri = uri;
glideRequests.load(new DecryptableUri(uri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.transition(DrawableTransitionOptions.withCrossFade())
.fitCenter()
.into(imageView);
}
public @NonNull ListenableFuture<Bitmap> getRenderedImage(@NonNull GlideRequests glideRequests) {
return renderImage(getContext(), imageUri, saveState(), glideRequests);
}
public static @NonNull ListenableFuture<Bitmap> renderImage(@NonNull Context context,
@Nullable Uri imageUri,
@NonNull SavedState savedState,
@NonNull GlideRequests glideRequests)
{
final SettableFuture<Bitmap> future = new SettableFuture<>();
final boolean isLowMemory = Util.isLowMemory(context);
if (imageUri == null) {
future.setException(new IllegalStateException("No image URI."));
return future;
}
int width = Target.SIZE_ORIGINAL;
int height = Target.SIZE_ORIGINAL;
if (isLowMemory) {
width = 768;
height = 768;
}
glideRequests.asBitmap()
.load(new DecryptableUri(imageUri))
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.override(width, height)
.into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition<? super Bitmap> transition) {
Canvas canvas = new Canvas(bitmap);
MotionView.render(canvas, savedState.getMotionState().getEntities());
CanvasView.render(canvas,
savedState.getCanvasState().getInitialWidth(),
savedState.getCanvasState().getInitialHeight(),
savedState.getCanvasState().getCanvasWidth(),
savedState.getCanvasState().getCanvasHeight(),
savedState.getCanvasState().getPaths(),
savedState.getCanvasState().getPaints(),
savedState.getCanvasState().getHistoryPointer());
future.set(bitmap);
}
@Override
public void onLoadFailed(@Nullable Drawable errorDrawable) {
future.setException(new Throwable("Failed to load image."));
}
});
return future;
}
public SavedState saveState() {
return new SavedState(canvasView.saveState(), motionView.saveState());
}
public void restoreState(@NonNull SavedState state) {
canvasView.restoreState(state.getCanvasState());
motionView.restoreState(state.getMotionState());
}
private void initialize(@NonNull Context context) {
inflate(context, R.layout.scribble_view, this);
this.imageView = findViewById(R.id.image_view);
this.motionView = findViewById(R.id.motion_view);
this.canvasView = findViewById(R.id.canvas_view);
}
public void setMotionViewCallback(MotionView.MotionViewCallback callback) {
this.motionView.setMotionViewCallback(callback);
}
@SuppressLint("ClickableViewAccessibility")
public void setDrawingChangedListener(@Nullable DrawingChangedListener listener) {
this.canvasView.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
if (listener != null) {
listener.onDrawingChanged();
}
}
return false;
});
}
public void setDrawingMode(boolean enabled) {
this.canvasView.setActive(enabled);
if (enabled) this.motionView.unselectEntity();
}
public void setDrawingBrushColor(int color) {
this.canvasView.setPaintFillColor(color);
this.canvasView.setPaintStrokeColor(color);
this.canvasView.setOpacity(Color.alpha(color));
}
public void setDrawingBrushWidth(int width) {
this.canvasView.setPaintStrokeWidth(width);
}
public void addEntityAndPosition(MotionEntity entity) {
this.motionView.addEntityAndPosition(entity);
}
public MotionEntity getSelectedEntity() {
return this.motionView.getSelectedEntity();
}
public void deleteSelected() {
this.motionView.deletedSelectedEntity();
}
public void clearSelection() {
this.motionView.unselectEntity();
}
public void undoDrawing() {
this.canvasView.undo();
}
public void startEditing(TextEntity entity) {
this.motionView.startEditing(entity);
}
public @NonNull Set<Integer> getUniqueColors() {
Set<Integer> colors = new LinkedHashSet<>();
colors.addAll(motionView.getUniqueColors());
colors.addAll(canvasView.getUniqueColors());
return colors;
}
@Override
public void onMeasure(int width, int height) {
super.onMeasure(width, height);
setMeasuredDimension(imageView.getMeasuredWidth(), imageView.getMeasuredHeight());
canvasView.measure(MeasureSpec.makeMeasureSpec(imageView.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(imageView.getMeasuredHeight(), MeasureSpec.EXACTLY));
motionView.measure(MeasureSpec.makeMeasureSpec(imageView.getMeasuredWidth(), MeasureSpec.EXACTLY),
MeasureSpec.makeMeasureSpec(imageView.getMeasuredHeight(), MeasureSpec.EXACTLY));
}
public interface DrawingChangedListener {
void onDrawingChanged();
}
public static class SavedState {
private final CanvasView.SavedState canvasState;
private final MotionView.SavedState motionState;
SavedState(@NonNull CanvasView.SavedState canvasState, @NonNull MotionView.SavedState motionState) {
this.canvasState = canvasState;
this.motionState = motionState;
}
CanvasView.SavedState getCanvasState() {
return canvasState;
}
MotionView.SavedState getMotionState() {
return motionState;
}
public boolean isEmpty() {
return canvasState.isEmpty() && motionState.isEmpty();
}
}
}

View File

@@ -172,6 +172,8 @@ public class VerticalSlideColorPicker extends View {
viewWidth = w;
viewHeight = h;
if (viewWidth <= 0 || viewHeight <= 0) return;
int barWidth = (int) (viewWidth * INDICATOR_TO_BAR_WIDTH_RATIO);
centerX = viewWidth / 2;

View File

@@ -1,83 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.widget.entity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
public class ImageEntity extends MotionEntity {
@NonNull
private final Bitmap bitmap;
public ImageEntity(@NonNull Layer layer,
@NonNull Bitmap bitmap,
@IntRange(from = 1) int canvasWidth,
@IntRange(from = 1) int canvasHeight) {
super(layer, canvasWidth, canvasHeight);
this.bitmap = bitmap;
float width = bitmap.getWidth();
float height = bitmap.getHeight();
float widthAspect = 1.0F * canvasWidth / width;
float heightAspect = 1.0F * canvasHeight / height;
// fit the smallest size
holyScale = Math.min(widthAspect, heightAspect);
// initial position of the entity
srcPoints[0] = 0; srcPoints[1] = 0;
srcPoints[2] = width; srcPoints[3] = 0;
srcPoints[4] = width; srcPoints[5] = height;
srcPoints[6] = 0; srcPoints[7] = height;
srcPoints[8] = 0; srcPoints[8] = 0;
}
@Override
public void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) {
canvas.drawBitmap(bitmap, matrix, drawingPaint);
}
@Override
public int getWidth() {
return bitmap.getWidth();
}
@Override
public int getHeight() {
return bitmap.getHeight();
}
@Override
public void release() {
if (!bitmap.isRecycled()) {
bitmap.recycle();
}
}
}

View File

@@ -1,290 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.widget.entity;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.util.MathUtils;
import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
@SuppressWarnings({"WeakerAccess"})
public abstract class MotionEntity {
/**
* data
*/
@NonNull
protected final Layer layer;
/**
* transformation matrix for the entity
*/
protected final Matrix matrix = new Matrix();
/**
* true - entity is selected and need to draw it's border
* false - not selected, no need to draw it's border
*/
private boolean isSelected;
/**
* maximum scale of the initial image, so that
* the entity still fits within the parent canvas
*/
protected float holyScale;
/**
* width of canvas the entity is drawn in
*/
@IntRange(from = 0)
protected int canvasWidth;
/**
* height of canvas the entity is drawn in
*/
@IntRange(from = 0)
protected int canvasHeight;
/**
* Destination points of the entity
* 5 points. Size of array - 10; Starting upper left corner, clockwise
* last point is the same as first to close the circle
* NOTE: saved as a field variable in order to avoid creating array in draw()-like methods
*/
private final float[] destPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0
/**
* Initial points of the entity
* @see #destPoints
*/
protected final float[] srcPoints = new float[10]; // x0, y0, x1, y1, x2, y2, x3, y3, x0, y0
@NonNull
private Paint borderPaint = new Paint();
public MotionEntity(@NonNull Layer layer,
@IntRange(from = 1) int canvasWidth,
@IntRange(from = 1) int canvasHeight) {
this.layer = layer;
this.canvasWidth = canvasWidth;
this.canvasHeight = canvasHeight;
}
private boolean isSelected() {
return isSelected;
}
public void setIsSelected(boolean isSelected) {
this.isSelected = isSelected;
}
/**
* S - scale matrix, R - rotate matrix, T - translate matrix,
* L - result transformation matrix
* <p>
* The correct order of applying transformations is : L = S * R * T
* <p>
* See more info: <a href="http://gamedev.stackexchange.com/questions/29260/transform-matrix-multiplication-order">Game Dev: Transform Matrix multiplication order</a>
* <p>
* Preconcat works like M` = M * S, so we apply preScale -> preRotate -> preTranslate
* the result will be the same: L = S * R * T
* <p>
* NOTE: postconcat (postScale, etc.) works the other way : M` = S * M, in order to use it
* we'd need to reverse the order of applying
* transformations : post holy scale -> postTranslate -> postRotate -> postScale
*/
protected void updateMatrix() {
// init matrix to E - identity matrix
matrix.reset();
float widthAspect = 1.0F * canvasWidth / getWidth();
float heightAspect = 1.0F * canvasHeight / getHeight();
// fit the smallest size
holyScale = Math.min(widthAspect, heightAspect);
float topLeftX = layer.getX() * canvasWidth;
float topLeftY = layer.getY() * canvasHeight;
float centerX = topLeftX + getWidth() * holyScale * 0.5F;
float centerY = topLeftY + getHeight() * holyScale * 0.5F;
// calculate params
float rotationInDegree = layer.getRotationInDegrees();
float scaleX = layer.getScale();
float scaleY = layer.getScale();
if (layer.isFlipped()) {
// flip (by X-coordinate) if needed
rotationInDegree *= -1.0F;
scaleX *= -1.0F;
}
// applying transformations : L = S * R * T
// scale
matrix.preScale(scaleX, scaleY, centerX, centerY);
// rotate
matrix.preRotate(rotationInDegree, centerX, centerY);
// translate
matrix.preTranslate(topLeftX, topLeftY);
// applying holy scale - S`, the result will be : L = S * R * T * S`
matrix.preScale(holyScale, holyScale);
}
public float absoluteCenterX() {
float topLeftX = layer.getX() * canvasWidth;
return topLeftX + getWidth() * holyScale * 0.5F;
}
public float absoluteCenterY() {
float topLeftY = layer.getY() * canvasHeight;
return topLeftY + getHeight() * holyScale * 0.5F;
}
public PointF absoluteCenter() {
float topLeftX = layer.getX() * canvasWidth;
float topLeftY = layer.getY() * canvasHeight;
float centerX = topLeftX + getWidth() * holyScale * 0.5F;
float centerY = topLeftY + getHeight() * holyScale * 0.5F;
return new PointF(centerX, centerY);
}
public void moveToCanvasCenter() {
moveCenterTo(new PointF(canvasWidth * 0.5F, canvasHeight * 0.5F));
}
public void moveCenterTo(PointF moveToCenter) {
PointF currentCenter = absoluteCenter();
layer.postTranslate(1.0F * (moveToCenter.x - currentCenter.x) / canvasWidth,
1.0F * (moveToCenter.y - currentCenter.y) / canvasHeight);
}
private final PointF pA = new PointF();
private final PointF pB = new PointF();
private final PointF pC = new PointF();
private final PointF pD = new PointF();
/**
* For more info:
* <a href="http://math.stackexchange.com/questions/190111/how-to-check-if-a-point-is-inside-a-rectangle">StackOverflow: How to check point is in rectangle</a>
* <p>NOTE: it's easier to apply the same transformation matrix (calculated before) to the original source points, rather than
* calculate the result points ourselves
* @param point point
* @return true if point (x, y) is inside the triangle
*/
public boolean pointInLayerRect(PointF point) {
updateMatrix();
// map rect vertices
matrix.mapPoints(destPoints, srcPoints);
pA.x = destPoints[0];
pA.y = destPoints[1];
pB.x = destPoints[2];
pB.y = destPoints[3];
pC.x = destPoints[4];
pC.y = destPoints[5];
pD.x = destPoints[6];
pD.y = destPoints[7];
return MathUtils.pointInTriangle(point, pA, pB, pC) || MathUtils.pointInTriangle(point, pA, pD, pC);
}
/**
* http://judepereira.com/blog/calculate-the-real-scale-factor-and-the-angle-of-rotation-from-an-android-matrix/
*
* @param canvas Canvas to draw
* @param drawingPaint Paint to use during drawing
*/
public final void draw(@NonNull Canvas canvas, @Nullable Paint drawingPaint) {
this.canvasWidth = canvas.getWidth();
this.canvasHeight = canvas.getHeight();
updateMatrix();
canvas.save();
drawContent(canvas, drawingPaint);
if (isSelected()) {
// get alpha from drawingPaint
int storedAlpha = borderPaint.getAlpha();
if (drawingPaint != null) {
borderPaint.setAlpha(drawingPaint.getAlpha());
}
drawSelectedBg(canvas);
// restore border alpha
borderPaint.setAlpha(storedAlpha);
}
canvas.restore();
}
private void drawSelectedBg(Canvas canvas) {
matrix.mapPoints(destPoints, srcPoints);
//noinspection Range
canvas.drawLines(destPoints, 0, 8, borderPaint);
//noinspection Range
canvas.drawLines(destPoints, 2, 8, borderPaint);
}
@NonNull
public Layer getLayer() {
return layer;
}
public void setBorderPaint(@NonNull Paint borderPaint) {
this.borderPaint = borderPaint;
}
protected abstract void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint);
public abstract int getWidth();
public abstract int getHeight();
public void release() {
// free resources here
}
public void updateEntity() {}
@Override
protected void finalize() throws Throwable {
try {
release();
} finally {
//noinspection ThrowFromFinallyBlock
super.finalize();
}
}
}

View File

@@ -1,189 +0,0 @@
/**
* Copyright (c) 2016 UPTech
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package org.thoughtcrime.securesms.scribbles.widget.entity;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PointF;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Layout;
import android.text.StaticLayout;
import android.text.TextPaint;
import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
public class TextEntity extends MotionEntity {
private final TextPaint textPaint;
@Nullable
private Bitmap bitmap;
public TextEntity(@NonNull TextLayer textLayer,
@IntRange(from = 1) int canvasWidth,
@IntRange(from = 1) int canvasHeight)
{
super(textLayer, canvasWidth, canvasHeight);
this.textPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
updateEntity(false);
}
private void updateEntity(boolean moveToPreviousCenter) {
// save previous center
PointF oldCenter = absoluteCenter();
Bitmap newBmp = createBitmap(getLayer(), bitmap);
// recycle previous bitmap (if not reused) as soon as possible
if (bitmap != null && bitmap != newBmp && !bitmap.isRecycled()) {
bitmap.recycle();
}
this.bitmap = newBmp;
float width = bitmap.getWidth();
float height = bitmap.getHeight();
@SuppressWarnings("UnnecessaryLocalVariable")
float widthAspect = 1.0F * canvasWidth / width;
// for text we always match text width with parent width
this.holyScale = widthAspect;
// initial position of the entity
srcPoints[0] = 0;
srcPoints[1] = 0;
srcPoints[2] = width;
srcPoints[3] = 0;
srcPoints[4] = width;
srcPoints[5] = height;
srcPoints[6] = 0;
srcPoints[7] = height;
srcPoints[8] = 0;
srcPoints[8] = 0;
if (moveToPreviousCenter) {
// move to previous center
moveCenterTo(oldCenter);
}
}
/**
* If reuseBmp is not null, and size of the new bitmap matches the size of the reuseBmp,
* new bitmap won't be created, reuseBmp it will be reused instead
*
* @param textLayer text to draw
* @param reuseBmp the bitmap that will be reused
* @return bitmap with the text
*/
@NonNull
private Bitmap createBitmap(@NonNull TextLayer textLayer, @Nullable Bitmap reuseBmp) {
int boundsWidth = canvasWidth;
// init params - size, color, typeface
textPaint.setStyle(Paint.Style.FILL);
textPaint.setTextSize(textLayer.getFont().getSize() * canvasWidth);
textPaint.setColor(textLayer.getFont().getColor());
// textPaint.setTypeface(fontProvider.getTypeface(textLayer.getFont().getTypeface()));
// drawing text guide : http://ivankocijan.xyz/android-drawing-multiline-text-on-canvas/
// Static layout which will be drawn on canvas
StaticLayout sl = new StaticLayout(
textLayer.getText(), // - text which will be drawn
textPaint,
boundsWidth, // - width of the layout
Layout.Alignment.ALIGN_CENTER, // - layout alignment
1, // 1 - text spacing multiply
1, // 1 - text spacing add
true); // true - include padding
// calculate height for the entity, min - Limits.MIN_BITMAP_HEIGHT
int boundsHeight = sl.getHeight();
// create bitmap not smaller than TextLayer.Limits.MIN_BITMAP_HEIGHT
int bmpHeight = (int) (canvasHeight * Math.max(TextLayer.Limits.MIN_BITMAP_HEIGHT,
1.0F * boundsHeight / canvasHeight));
// create bitmap where text will be drawn
Bitmap bmp;
if (reuseBmp != null && reuseBmp.getWidth() == boundsWidth
&& reuseBmp.getHeight() == bmpHeight) {
// if previous bitmap exists, and it's width/height is the same - reuse it
bmp = reuseBmp;
bmp.eraseColor(Color.TRANSPARENT); // erase color when reusing
} else {
bmp = Bitmap.createBitmap(boundsWidth, bmpHeight, Bitmap.Config.ARGB_8888);
}
Canvas canvas = new Canvas(bmp);
canvas.save();
// move text to center if bitmap is bigger that text
if (boundsHeight < bmpHeight) {
//calculate Y coordinate - In this case we want to draw the text in the
//center of the canvas so we move Y coordinate to center.
float textYCoordinate = (bmpHeight - boundsHeight) / 2;
canvas.translate(0, textYCoordinate);
}
//draws static layout on canvas
sl.draw(canvas);
canvas.restore();
return bmp;
}
@Override
@NonNull
public TextLayer getLayer() {
return (TextLayer) layer;
}
@Override
protected void drawContent(@NonNull Canvas canvas, @Nullable Paint drawingPaint) {
if (bitmap != null) {
canvas.drawBitmap(bitmap, matrix, drawingPaint);
}
}
@Override
public int getWidth() {
return bitmap != null ? bitmap.getWidth() : 0;
}
@Override
public int getHeight() {
return bitmap != null ? bitmap.getHeight() : 0;
}
@Override
public void updateEntity() {
updateEntity(true);
}
}