mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
0999359454
A variety of improvements to the image editor, such as: - New, fullscreen styling - Smoother lines - Better text and sticker handling - Improved color picker with a history pallette - New highlighter tool
497 lines
15 KiB
Java
497 lines
15 KiB
Java
/**
|
|
* 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.Log;
|
|
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.InputMethodManager;
|
|
import android.widget.EditText;
|
|
import android.widget.FrameLayout;
|
|
|
|
import org.thoughtcrime.securesms.R;
|
|
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.addView(editText);
|
|
this.editText.clearFocus();
|
|
this.editText.addTextChangedListener(this);
|
|
|
|
// 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 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);
|
|
drawAllEntities(canvas);
|
|
}
|
|
|
|
public void render(Canvas canvas) {
|
|
unselectEntity();
|
|
draw(canvas);
|
|
}
|
|
|
|
/**
|
|
* draws all entities on the canvas
|
|
* @param canvas Canvas where to draw all entities
|
|
*/
|
|
private void drawAllEntities(Canvas canvas) {
|
|
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);
|
|
drawAllEntities(canvas);
|
|
|
|
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.w(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();
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|