mirror of
https://github.com/oxen-io/session-android.git
synced 2025-10-25 13:58:40 +00:00
Support for stickers and scribbles
// FREEBIE
This commit is contained in:
278
src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java
Normal file
278
src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java
Normal file
@@ -0,0 +1,278 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
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.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
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.concurrent.ListenableFuture;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import ws.com.google.android.mms.ContentType;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class ScribbleActivity extends PassphraseRequiredActionBarActivity implements ScribbleToolbar.ScribbleToolbarListener, VerticalSlideColorPicker.OnColorChangeListener {
|
||||
|
||||
private static final String TAG = ScribbleActivity.class.getName();
|
||||
|
||||
public static final int SELECT_STICKER_REQUEST_CODE = 123;
|
||||
public static final int SCRIBBLE_REQUEST_CODE = 31424;
|
||||
|
||||
private VerticalSlideColorPicker colorPicker;
|
||||
private ScribbleToolbar toolbar;
|
||||
private ScribbleView scribbleView;
|
||||
private MasterSecret masterSecret;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, @NonNull MasterSecret masterSecret) {
|
||||
setContentView(R.layout.scribble_activity);
|
||||
|
||||
this.masterSecret = masterSecret;
|
||||
this.scribbleView = (ScribbleView) findViewById(R.id.scribble_view);
|
||||
this.toolbar = (ScribbleToolbar) findViewById(R.id.toolbar);
|
||||
this.colorPicker = (VerticalSlideColorPicker) findViewById(R.id.scribble_color_picker);
|
||||
|
||||
this.toolbar.setListener(this);
|
||||
this.toolbar.setToolColor(Color.RED);
|
||||
|
||||
scribbleView.setMotionViewCallback(motionViewCallback);
|
||||
scribbleView.setDrawingMode(false);
|
||||
scribbleView.setImage(getIntent().getData(), masterSecret);
|
||||
|
||||
colorPicker.setOnColorChangeListener(this);
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
|
||||
getSupportActionBar().setTitle(null);
|
||||
}
|
||||
|
||||
private void addSticker(final Bitmap pica) {
|
||||
scribbleView.post(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// move text sticker up so that its not hidden under keyboard
|
||||
PointF center = textEntity.absoluteCenter();
|
||||
center.y = center.y * 0.5F;
|
||||
textEntity.moveCenterTo(center);
|
||||
|
||||
// redraw
|
||||
scribbleView.invalidate();
|
||||
|
||||
startTextEntityEditing();
|
||||
changeTextEntityColor(toolbar.getToolColor());
|
||||
}
|
||||
|
||||
private TextLayer createTextLayer() {
|
||||
TextLayer textLayer = new TextLayer();
|
||||
Font font = new Font();
|
||||
|
||||
font.setColor(TextLayer.Limits.INITIAL_FONT_COLOR);
|
||||
font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE);
|
||||
|
||||
textLayer.setFont(font);
|
||||
|
||||
return textLayer;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK) {
|
||||
if (requestCode == SELECT_STICKER_REQUEST_CODE) {
|
||||
if (data != null) {
|
||||
toolbar.setStickerSelected(true);
|
||||
final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE);
|
||||
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected @Nullable
|
||||
Bitmap doInBackground(Void... params) {
|
||||
try {
|
||||
return BitmapFactory.decodeStream(getAssets().open(stickerFile));
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable Bitmap bitmap) {
|
||||
addSticker(bitmap);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBrushSelected(boolean enabled) {
|
||||
scribbleView.setDrawingMode(enabled);
|
||||
colorPicker.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPaintUndo() {
|
||||
scribbleView.undoDrawing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextSelected(boolean enabled) {
|
||||
if (enabled) {
|
||||
addTextSticker();
|
||||
scribbleView.setDrawingMode(false);
|
||||
colorPicker.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
scribbleView.clearSelection();
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerSelected(boolean enabled) {
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
|
||||
if (!enabled) {
|
||||
scribbleView.clearSelection();
|
||||
} else {
|
||||
scribbleView.setDrawingMode(false);
|
||||
Intent intent = new Intent(this, StickerSelectActivity.class);
|
||||
startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onDeleteSelected() {
|
||||
scribbleView.deleteSelected();
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSave() {
|
||||
ListenableFuture<Bitmap> future = scribbleView.getRenderedImage();
|
||||
|
||||
future.addListener(new ListenableFuture.Listener<Bitmap>() {
|
||||
@Override
|
||||
public void onSuccess(Bitmap result) {
|
||||
PersistentBlobProvider provider = PersistentBlobProvider.getInstance(ScribbleActivity.this);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
result.compress(Bitmap.CompressFormat.JPEG, 80, baos);
|
||||
|
||||
byte[] data = baos.toByteArray();
|
||||
baos = null;
|
||||
result = null;
|
||||
|
||||
Uri uri = provider.create(masterSecret, data, ContentType.IMAGE_JPEG);
|
||||
Intent intent = new Intent();
|
||||
intent.setData(uri);
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() {
|
||||
@Override
|
||||
public void onEntitySelected(@Nullable MotionEntity entity) {
|
||||
if (entity == null) {
|
||||
toolbar.setNoneSelected();
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
} else if (entity instanceof TextEntity) {
|
||||
toolbar.setTextSelected(true);
|
||||
colorPicker.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
toolbar.setStickerSelected(true);
|
||||
colorPicker.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityDoubleTap(@NonNull MotionEntity entity) {
|
||||
startTextEntityEditing();
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onColorChange(int color) {
|
||||
if (color == 0) color = Color.RED;
|
||||
|
||||
toolbar.setToolColor(color);
|
||||
scribbleView.setDrawingBrushColor(color);
|
||||
|
||||
changeTextEntityColor(color);
|
||||
}
|
||||
}
|
||||
240
src/org/thoughtcrime/securesms/scribbles/ScribbleToolbar.java
Normal file
240
src/org/thoughtcrime/securesms/scribbles/ScribbleToolbar.java
Normal file
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import android.animation.LayoutTransition;
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class ScribbleToolbar extends Toolbar implements View.OnClickListener {
|
||||
|
||||
private enum Selected {
|
||||
NONE,
|
||||
STICKER,
|
||||
TEXT,
|
||||
BRUSH
|
||||
}
|
||||
|
||||
private int foregroundSelectedTint;
|
||||
private int foregroundUnselectedTint;
|
||||
|
||||
private LinearLayout toolsView;
|
||||
|
||||
private ImageView saveView;
|
||||
private ImageView brushView;
|
||||
private ImageView textView;
|
||||
private ImageView stickerView;
|
||||
|
||||
private ImageView separatorView;
|
||||
|
||||
private ImageView undoView;
|
||||
private ImageView deleteView;
|
||||
|
||||
private Drawable background;
|
||||
|
||||
@Nullable
|
||||
private ScribbleToolbarListener listener;
|
||||
|
||||
private int toolColor = Color.RED;
|
||||
private Selected selected = Selected.NONE;
|
||||
|
||||
public ScribbleToolbar(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScribbleToolbar(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public ScribbleToolbar(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
inflate(context, R.layout.scribble_toolbar, this);
|
||||
|
||||
this.toolsView = (LinearLayout) findViewById(R.id.tools_view);
|
||||
this.brushView = (ImageView) findViewById(R.id.brush_button);
|
||||
this.textView = (ImageView) findViewById(R.id.text_button);
|
||||
this.stickerView = (ImageView) findViewById(R.id.sticker_button);
|
||||
this.separatorView = (ImageView) findViewById(R.id.separator);
|
||||
this.saveView = (ImageView) findViewById(R.id.save);
|
||||
|
||||
this.undoView = (ImageView) findViewById(R.id.undo);
|
||||
this.deleteView = (ImageView) findViewById(R.id.delete);
|
||||
|
||||
this.background = getResources().getDrawable(R.drawable.circle_tintable);
|
||||
this.foregroundSelectedTint = getResources().getColor(R.color.white);
|
||||
this.foregroundUnselectedTint = getResources().getColor(R.color.grey_800);
|
||||
|
||||
this.undoView.setOnClickListener(this);
|
||||
this.brushView.setOnClickListener(this);
|
||||
this.textView.setOnClickListener(this);
|
||||
this.stickerView.setOnClickListener(this);
|
||||
this.separatorView.setOnClickListener(this);
|
||||
this.deleteView.setOnClickListener(this);
|
||||
this.saveView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
public void setListener(@Nullable ScribbleToolbarListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void setToolColor(int toolColor) {
|
||||
this.toolColor = toolColor;
|
||||
this.background.setColorFilter(new PorterDuffColorFilter(toolColor, PorterDuff.Mode.MULTIPLY));
|
||||
}
|
||||
|
||||
public int getToolColor() {
|
||||
return this.toolColor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
this.toolsView.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
|
||||
|
||||
if (v == this.brushView) {
|
||||
boolean enabled = selected != Selected.BRUSH;
|
||||
setBrushSelected(enabled);
|
||||
if (listener != null) listener.onBrushSelected(enabled);
|
||||
} else if (v == this.stickerView) {
|
||||
setNoneSelected();
|
||||
if (listener != null) listener.onStickerSelected(true);
|
||||
} else if (v == this.textView) {
|
||||
boolean enabled = selected != Selected.TEXT;
|
||||
setTextSelected(enabled);
|
||||
if (listener != null) listener.onTextSelected(enabled);
|
||||
} else if (v == this.deleteView) {
|
||||
setNoneSelected();
|
||||
if (listener != null) listener.onDeleteSelected();
|
||||
} else if (v == this.undoView) {
|
||||
if (listener != null) listener.onPaintUndo();
|
||||
} else if (v == this.saveView) {
|
||||
if (listener != null) listener.onSave();
|
||||
}
|
||||
}
|
||||
|
||||
private void setBrushSelected(boolean enabled) {
|
||||
if (enabled) {
|
||||
|
||||
this.textView.setBackground(null);
|
||||
this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.brushView.setBackground(background);
|
||||
this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundSelectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.stickerView.setBackground(null);
|
||||
this.stickerView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.separatorView.setVisibility(View.VISIBLE);
|
||||
this.undoView.setVisibility(View.VISIBLE);
|
||||
this.deleteView.setVisibility(View.GONE);
|
||||
|
||||
this.selected = Selected.BRUSH;
|
||||
} else {
|
||||
this.brushView.setBackground(null);
|
||||
this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
this.separatorView.setVisibility(View.GONE);
|
||||
this.undoView.setVisibility(View.GONE);
|
||||
|
||||
this.selected = Selected.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setTextSelected(boolean enabled) {
|
||||
if (enabled) {
|
||||
this.brushView.setBackground(null);
|
||||
this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.textView.setBackground(background);
|
||||
this.textView.setColorFilter(new PorterDuffColorFilter(foregroundSelectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.stickerView.setBackground(null);
|
||||
this.stickerView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.separatorView.setVisibility(View.VISIBLE);
|
||||
this.undoView.setVisibility(View.GONE);
|
||||
this.deleteView.setVisibility(View.VISIBLE);
|
||||
|
||||
this.selected = Selected.TEXT;
|
||||
} else {
|
||||
this.textView.setBackground(null);
|
||||
this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.separatorView.setVisibility(View.GONE);
|
||||
this.deleteView.setVisibility(View.GONE);
|
||||
|
||||
this.selected = Selected.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setStickerSelected(boolean enabled) {
|
||||
if (enabled) {
|
||||
this.brushView.setBackground(null);
|
||||
this.brushView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.textView.setBackground(null);
|
||||
this.textView.setColorFilter(new PorterDuffColorFilter(foregroundUnselectedTint, PorterDuff.Mode.MULTIPLY));
|
||||
|
||||
this.separatorView.setVisibility(View.VISIBLE);
|
||||
this.undoView.setVisibility(View.GONE);
|
||||
this.deleteView.setVisibility(View.VISIBLE);
|
||||
|
||||
this.selected = Selected.STICKER;
|
||||
} else {
|
||||
this.separatorView.setVisibility(View.GONE);
|
||||
this.deleteView.setVisibility(View.GONE);
|
||||
|
||||
this.selected = Selected.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
public void setNoneSelected() {
|
||||
setBrushSelected(false);
|
||||
setStickerSelected(false);
|
||||
setTextSelected(false);
|
||||
|
||||
this.selected = Selected.NONE;
|
||||
}
|
||||
|
||||
public interface ScribbleToolbarListener {
|
||||
public void onBrushSelected(boolean enabled);
|
||||
public void onPaintUndo();
|
||||
public void onTextSelected(boolean enabled);
|
||||
public void onStickerSelected(boolean enabled);
|
||||
public void onDeleteSelected();
|
||||
public void onSave();
|
||||
}
|
||||
}
|
||||
56
src/org/thoughtcrime/securesms/scribbles/StickerLoader.java
Normal file
56
src/org/thoughtcrime/securesms/scribbles/StickerLoader.java
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class StickerLoader extends AsyncLoader<String[]> {
|
||||
|
||||
private static final String TAG = StickerLoader.class.getName();
|
||||
|
||||
private final String assetDirectory;
|
||||
|
||||
StickerLoader(Context context, String assetDirectory) {
|
||||
super(context);
|
||||
this.assetDirectory = assetDirectory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull
|
||||
String[] loadInBackground() {
|
||||
try {
|
||||
String[] files = getContext().getAssets().list(assetDirectory);
|
||||
|
||||
for (int i=0;i<files.length;i++) {
|
||||
files[i] = assetDirectory + "/" + files[i];
|
||||
}
|
||||
|
||||
return files;
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return new String[0];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentActivity;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.style.ImageSpan;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import org.thoughtcrime.securesms.BaseActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class StickerSelectActivity extends FragmentActivity implements StickerSelectFragment.StickerSelectionListener {
|
||||
|
||||
private static final String TAG = StickerSelectActivity.class.getName();
|
||||
|
||||
public static final String EXTRA_STICKER_FILE = "extra_sticker_file";
|
||||
|
||||
private static final int[] TAB_TITLES = new int[] {
|
||||
R.drawable.ic_tag_faces_white_24dp,
|
||||
R.drawable.ic_work_white_24dp,
|
||||
R.drawable.ic_pets_white_24dp,
|
||||
R.drawable.ic_local_dining_white_24dp,
|
||||
R.drawable.ic_wb_sunny_white_24dp
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scribble_select_sticker_activity);
|
||||
|
||||
ViewPager viewPager = (ViewPager) findViewById(R.id.pager);
|
||||
viewPager.setAdapter(new StickerPagerAdapter(getSupportFragmentManager(), this));
|
||||
|
||||
TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
|
||||
for (int i=0;i<tabLayout.getTabCount();i++) {
|
||||
tabLayout.getTabAt(i).setIcon(TAB_TITLES[i]);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerSelected(String name) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EXTRA_STICKER_FILE, name);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
}
|
||||
|
||||
class StickerPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final Fragment[] fragments;
|
||||
|
||||
StickerPagerAdapter(FragmentManager fm, StickerSelectFragment.StickerSelectionListener listener) {
|
||||
super(fm);
|
||||
|
||||
this.fragments = new Fragment[] {
|
||||
StickerSelectFragment.newInstance("stickers/emoticons"),
|
||||
StickerSelectFragment.newInstance("stickers/clothes"),
|
||||
StickerSelectFragment.newInstance("stickers/animals"),
|
||||
StickerSelectFragment.newInstance("stickers/food"),
|
||||
StickerSelectFragment.newInstance("stickers/weather"),
|
||||
};
|
||||
|
||||
for (Fragment fragment : fragments) {
|
||||
((StickerSelectFragment)fragment).setListener(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
return fragments[position];
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return fragments.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
import android.content.Context;
|
||||
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.support.v4.app.LoaderManager;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class StickerSelectFragment extends Fragment implements LoaderManager.LoaderCallbacks<String[]> {
|
||||
|
||||
private RecyclerView recyclerView;
|
||||
private String assetDirectory;
|
||||
private StickerSelectionListener listener;
|
||||
|
||||
public static StickerSelectFragment newInstance(String assetDirectory) {
|
||||
StickerSelectFragment fragment = new StickerSelectFragment();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString("assetDirectory", assetDirectory);
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
@Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState)
|
||||
{
|
||||
View view = inflater.inflate(R.layout.scribble_select_sticker_fragment, container, false);
|
||||
this.recyclerView = (RecyclerView)view.findViewById(R.id.stickers_recycler_view);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Bundle bundle) {
|
||||
super.onActivityCreated(bundle);
|
||||
|
||||
this.assetDirectory = getArguments().getString("assetDirectory");
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
this.recyclerView.setLayoutManager(new GridLayoutManager(getActivity(), 3));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Loader<String[]> onCreateLoader(int id, Bundle args) {
|
||||
return new StickerLoader(getActivity(), assetDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(Loader<String[]> loader, String[] data) {
|
||||
recyclerView.setAdapter(new StickersAdapter(getActivity(), data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(Loader<String[]> loader) {
|
||||
recyclerView.setAdapter(null);
|
||||
}
|
||||
|
||||
public void setListener(StickerSelectionListener listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
class StickersAdapter extends RecyclerView.Adapter<StickersAdapter.StickerViewHolder> {
|
||||
|
||||
private final Context context;
|
||||
private final String[] stickerFiles;
|
||||
private final LayoutInflater layoutInflater;
|
||||
|
||||
StickersAdapter(@NonNull Context context, @NonNull String[] stickerFiles) {
|
||||
this.context = context;
|
||||
this.stickerFiles = stickerFiles;
|
||||
this.layoutInflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickerViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
return new StickerViewHolder(layoutInflater.inflate(R.layout.scribble_sticker_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(StickerViewHolder holder, int position) {
|
||||
holder.fileName = stickerFiles[position];
|
||||
|
||||
Glide.with(context)
|
||||
.load(Uri.parse("file:///android_asset/" + holder.fileName))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(holder.image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return stickerFiles.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(StickerViewHolder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
Glide.clear(holder.image);
|
||||
}
|
||||
|
||||
private void onStickerSelected(String fileName) {
|
||||
if (listener != null) listener.onStickerSelected(fileName);
|
||||
}
|
||||
|
||||
class StickerViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private String fileName;
|
||||
private ImageView image;
|
||||
|
||||
StickerViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
image = (ImageView) itemView.findViewById(R.id.sticker_image);
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
int pos = getAdapterPosition();
|
||||
if (pos >= 0) {
|
||||
onStickerSelected(fileName);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface StickerSelectionListener {
|
||||
public void onStickerSelected(String name);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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.getName();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package org.thoughtcrime.securesms.scribbles.multitouch;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewConfiguration;
|
||||
|
||||
/**
|
||||
* @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 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.w("TwoFinger",
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
83
src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java
Normal file
83
src/org/thoughtcrime/securesms/scribbles/viewmodel/Font.java
Normal file
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
141
src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java
Normal file
141
src/org/thoughtcrime/securesms/scribbles/viewmodel/Layer.java
Normal file
@@ -0,0 +1,141 @@
|
||||
/**
|
||||
* 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 android.util.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.w("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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/**
|
||||
* 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.util.Log;
|
||||
|
||||
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.075F;
|
||||
int INITIAL_FONT_COLOR = 0xff000000;
|
||||
|
||||
float INITIAL_SCALE = 0.8F; // set the same to avoid text scaling
|
||||
}
|
||||
}
|
||||
901
src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java
Normal file
901
src/org/thoughtcrime/securesms/scribbles/widget/CanvasView.java
Normal file
@@ -0,0 +1,901 @@
|
||||
/**
|
||||
* 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.graphics.Typeface;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* This class defines fields and methods for drawing.
|
||||
*/
|
||||
public class CanvasView extends View {
|
||||
|
||||
// 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 Context context = null;
|
||||
private Canvas canvas = null;
|
||||
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 = 15F;
|
||||
private int opacity = 255;
|
||||
private float blur = 0F;
|
||||
private Paint.Cap lineCap = Paint.Cap.ROUND;
|
||||
|
||||
// for Text
|
||||
private String text = "";
|
||||
private Typeface fontFamily = Typeface.DEFAULT;
|
||||
private float fontSize = 32F;
|
||||
private Paint.Align textAlign = Paint.Align.RIGHT; // fixed
|
||||
private Paint textPaint = new Paint();
|
||||
private float textX = 0F;
|
||||
private float textY = 0F;
|
||||
|
||||
// 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(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Constructor
|
||||
*
|
||||
* @param context
|
||||
* @param attrs
|
||||
*/
|
||||
public CanvasView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
this.setup(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy Constructor
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
public CanvasView(Context context) {
|
||||
super(context);
|
||||
this.setup(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Common initialization.
|
||||
*
|
||||
* @param context
|
||||
*/
|
||||
private void setup(Context context) {
|
||||
this.context = context;
|
||||
|
||||
this.pathLists.add(new Path());
|
||||
this.paintLists.add(this.createPaint());
|
||||
this.historyPointer++;
|
||||
|
||||
this.textPaint.setARGB(0, 255, 255, 255);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.MITER); // fixed
|
||||
|
||||
// for Text
|
||||
if (this.mode == Mode.TEXT) {
|
||||
paint.setTypeface(this.fontFamily);
|
||||
paint.setTextSize(this.fontSize);
|
||||
paint.setTextAlign(this.textAlign);
|
||||
paint.setStrokeWidth(0F);
|
||||
}
|
||||
|
||||
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 = event.getX();
|
||||
this.startY = 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 draws text.
|
||||
*
|
||||
* @param canvas the instance of Canvas
|
||||
*/
|
||||
private void drawText(Canvas canvas) {
|
||||
if (this.text.length() <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.mode == Mode.TEXT) {
|
||||
this.textX = this.startX;
|
||||
this.textY = this.startY;
|
||||
|
||||
this.textPaint = this.createPaint();
|
||||
}
|
||||
|
||||
float textX = this.textX;
|
||||
float textY = this.textY;
|
||||
|
||||
Paint paintForMeasureText = new Paint();
|
||||
|
||||
// Line break automatically
|
||||
float textLength = paintForMeasureText.measureText(this.text);
|
||||
float lengthOfChar = textLength / (float)this.text.length();
|
||||
float restWidth = this.canvas.getWidth() - textX; // text-align : right
|
||||
int numChars = (lengthOfChar <= 0) ? 1 : (int) Math.floor((double)(restWidth / lengthOfChar)); // The number of characters at 1 line
|
||||
int modNumChars = (numChars < 1) ? 1 : numChars;
|
||||
float y = textY;
|
||||
|
||||
for (int i = 0, len = this.text.length(); i < len; i += modNumChars) {
|
||||
String substring = "";
|
||||
|
||||
if ((i + modNumChars) < len) {
|
||||
substring = this.text.substring(i, (i + modNumChars));
|
||||
} else {
|
||||
substring = this.text.substring(i, len);
|
||||
}
|
||||
|
||||
y += this.fontSize;
|
||||
|
||||
canvas.drawText(substring, textX, y, this.textPaint);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 :
|
||||
path.lineTo(x, y);
|
||||
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(this.startX, 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 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);
|
||||
|
||||
// Before "drawPath"
|
||||
canvas.drawColor(this.baseColor);
|
||||
|
||||
if (this.bitmap != null) {
|
||||
canvas.drawBitmap(this.bitmap, 0F, 0F, new Paint());
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.historyPointer; i++) {
|
||||
Path path = this.pathLists.get(i);
|
||||
Paint paint = this.paintLists.get(i);
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
|
||||
this.drawText(canvas);
|
||||
|
||||
this.canvas = canvas;
|
||||
}
|
||||
|
||||
public void render(Canvas canvas) {
|
||||
if (this.canvas == null) return;
|
||||
|
||||
float scaleX = 1.0F * canvas.getWidth() / this.canvas.getWidth();
|
||||
float scaleY = 1.0F * canvas.getHeight() / this.canvas.getHeight();
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setScale(scaleX, scaleY);
|
||||
|
||||
for (int i = 0; i < this.historyPointer; i++) {
|
||||
Path path = this.pathLists.get(i);
|
||||
Paint paint = this.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);
|
||||
}
|
||||
}
|
||||
|
||||
this.text = "";
|
||||
|
||||
// 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 drawn text.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public String getText() {
|
||||
return this.text;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is setter for drawn text.
|
||||
*
|
||||
* @param text
|
||||
*/
|
||||
public void setText(String text) {
|
||||
this.text = text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 is getter for font size,
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public float getFontSize() {
|
||||
return this.fontSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is setter for font size.
|
||||
* The 1st argument is greater than or equal to 0.0.
|
||||
*
|
||||
* @param size
|
||||
*/
|
||||
public void setFontSize(float size) {
|
||||
if (size >= 0F) {
|
||||
this.fontSize = size;
|
||||
} else {
|
||||
this.fontSize = 32F;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is getter for font-family.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
public Typeface getFontFamily() {
|
||||
return this.fontFamily;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is setter for font-family.
|
||||
*
|
||||
* @param face
|
||||
*/
|
||||
public void setFontFamily(Typeface face) {
|
||||
this.fontFamily = face;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
473
src/org/thoughtcrime/securesms/scribbles/widget/MotionView.java
Normal file
473
src/org/thoughtcrime/securesms/scribbles/widget/MotionView.java
Normal file
@@ -0,0 +1,473 @@
|
||||
/**
|
||||
* 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.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.List;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
selectedEntity.setIsSelected(false);
|
||||
|
||||
if (selectedEntity instanceof TextEntity) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
/**
|
||||
* 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.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Log;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
public class ScribbleView extends FrameLayout {
|
||||
|
||||
private static final String TAG = ScribbleView.class.getSimpleName();
|
||||
|
||||
private ImageView imageView;
|
||||
private MotionView motionView;
|
||||
private CanvasView canvasView;
|
||||
|
||||
private @Nullable Uri imageUri;
|
||||
private @Nullable MasterSecret masterSecret;
|
||||
|
||||
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 Uri uri, @NonNull MasterSecret masterSecret) {
|
||||
this.imageUri = uri;
|
||||
this.masterSecret = masterSecret;
|
||||
|
||||
Glide.with(getContext())
|
||||
.load(new DecryptableUri(masterSecret, uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.into(imageView);
|
||||
}
|
||||
|
||||
public @NonNull ListenableFuture<Bitmap> getRenderedImage() {
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
final Context context = getContext();
|
||||
|
||||
if (imageUri == null || masterSecret == null) {
|
||||
future.set(null);
|
||||
return future;
|
||||
}
|
||||
|
||||
new AsyncTask<Void, Void, Bitmap>() {
|
||||
@Override
|
||||
protected @Nullable Bitmap doInBackground(Void... params) {
|
||||
try {
|
||||
return Glide.with(context)
|
||||
.load(new DecryptableUri(masterSecret, imageUri))
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.into(-1, -1)
|
||||
.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.w(TAG, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(@Nullable Bitmap bitmap) {
|
||||
if (bitmap == null) {
|
||||
future.set(null);
|
||||
return;
|
||||
}
|
||||
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
motionView.render(canvas);
|
||||
canvasView.render(canvas);
|
||||
future.set(bitmap);
|
||||
}
|
||||
}.execute();
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
private void initialize(@NonNull Context context) {
|
||||
inflate(context, R.layout.scribble_view, this);
|
||||
|
||||
this.imageView = (ImageView) findViewById(R.id.image_view);
|
||||
this.motionView = (MotionView) findViewById(R.id.motion_view);
|
||||
this.canvasView = (CanvasView) findViewById(R.id.canvas_view);
|
||||
}
|
||||
|
||||
public void setMotionViewCallback(MotionView.MotionViewCallback callback) {
|
||||
this.motionView.setMotionViewCallback(callback);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/**
|
||||
* Copyright (c) 2016 Mark Charles
|
||||
* Copyright (c) 2016 Open Whisper Systems
|
||||
*
|
||||
* 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.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.Shader;
|
||||
import android.os.Build;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
public class VerticalSlideColorPicker extends View {
|
||||
|
||||
private Paint paint;
|
||||
private Paint strokePaint;
|
||||
private Path path;
|
||||
private Bitmap bitmap;
|
||||
private Canvas bitmapCanvas;
|
||||
|
||||
private int viewWidth;
|
||||
private int viewHeight;
|
||||
private int centerX;
|
||||
private float colorPickerRadius;
|
||||
private RectF colorPickerBody;
|
||||
|
||||
private OnColorChangeListener onColorChangeListener;
|
||||
|
||||
private int borderColor;
|
||||
private float borderWidth;
|
||||
private int[] colors;
|
||||
|
||||
public VerticalSlideColorPicker(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public VerticalSlideColorPicker(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
|
||||
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.VerticalSlideColorPicker, 0, 0);
|
||||
|
||||
try {
|
||||
int colorsResourceId = a.getResourceId(R.styleable.VerticalSlideColorPicker_pickerColors, R.array.scribble_colors);
|
||||
|
||||
colors = a.getResources().getIntArray(colorsResourceId);
|
||||
borderColor = a.getColor(R.styleable.VerticalSlideColorPicker_pickerBorderColor, Color.WHITE);
|
||||
borderWidth = a.getDimension(R.styleable.VerticalSlideColorPicker_pickerBorderWidth, 10f);
|
||||
|
||||
} finally {
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
|
||||
public VerticalSlideColorPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
|
||||
super(context, attrs, defStyleAttr, defStyleRes);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
setWillNotDraw(false);
|
||||
|
||||
paint = new Paint();
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
paint.setAntiAlias(true);
|
||||
|
||||
path = new Path();
|
||||
|
||||
strokePaint = new Paint();
|
||||
strokePaint.setStyle(Paint.Style.STROKE);
|
||||
strokePaint.setColor(borderColor);
|
||||
strokePaint.setAntiAlias(true);
|
||||
strokePaint.setStrokeWidth(borderWidth);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
path.addCircle(centerX, borderWidth + colorPickerRadius, colorPickerRadius, Path.Direction.CW);
|
||||
path.addRect(colorPickerBody, Path.Direction.CW);
|
||||
path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius), colorPickerRadius, Path.Direction.CW);
|
||||
|
||||
bitmapCanvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
bitmapCanvas.drawPath(path, strokePaint);
|
||||
bitmapCanvas.drawPath(path, paint);
|
||||
|
||||
canvas.drawBitmap(bitmap, 0, 0, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
|
||||
float yPos = Math.min(event.getY(), colorPickerBody.bottom);
|
||||
yPos = Math.max(colorPickerBody.top, yPos);
|
||||
|
||||
int selectedColor = bitmap.getPixel(viewWidth/2, (int) yPos);
|
||||
|
||||
if (onColorChangeListener != null) {
|
||||
onColorChangeListener.onColorChange(selectedColor);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
viewWidth = w;
|
||||
viewHeight = h;
|
||||
|
||||
centerX = viewWidth / 2;
|
||||
colorPickerRadius = (viewWidth / 2) - borderWidth;
|
||||
|
||||
colorPickerBody = new RectF(centerX - colorPickerRadius, borderWidth + colorPickerRadius, centerX + colorPickerRadius, viewHeight - (borderWidth + colorPickerRadius));
|
||||
|
||||
LinearGradient gradient = new LinearGradient(0, colorPickerBody.top, 0, colorPickerBody.bottom, colors, null, Shader.TileMode.CLAMP);
|
||||
paint.setShader(gradient);
|
||||
|
||||
if (bitmap != null) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
|
||||
bitmap = Bitmap.createBitmap(viewWidth, viewHeight, Bitmap.Config.ARGB_8888);
|
||||
bitmapCanvas = new Canvas(bitmap);
|
||||
|
||||
resetToDefault();
|
||||
}
|
||||
|
||||
public void setBorderColor(int borderColor) {
|
||||
this.borderColor = borderColor;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setBorderWidth(float borderWidth) {
|
||||
this.borderWidth = borderWidth;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setColors(int[] colors) {
|
||||
this.colors = colors;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void resetToDefault() {
|
||||
if (onColorChangeListener != null) {
|
||||
onColorChangeListener.onColorChange(Color.RED);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public void setOnColorChangeListener(OnColorChangeListener onColorChangeListener) {
|
||||
this.onColorChangeListener = onColorChangeListener;
|
||||
}
|
||||
|
||||
public interface OnColorChangeListener {
|
||||
|
||||
void onColorChange(int selectedColor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user