mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-19 16:19:33 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,450 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Paint;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.ImageEditorView;
|
||||
import org.thoughtcrime.securesms.imageeditor.Renderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorElement;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
public final class ImageEditorFragment extends Fragment implements ImageEditorHud.EventListener,
|
||||
VerticalSlideColorPicker.OnColorChangeListener,
|
||||
MediaSendPageFragment {
|
||||
|
||||
private static final String TAG = Log.tag(ImageEditorFragment.class);
|
||||
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
|
||||
private static final int SELECT_OLD_STICKER_REQUEST_CODE = 123;
|
||||
private static final int SELECT_NEW_STICKER_REQUEST_CODE = 124;
|
||||
|
||||
private EditorModel restoredModel;
|
||||
|
||||
@Nullable private EditorElement currentSelection;
|
||||
private int imageMaxHeight;
|
||||
private int imageMaxWidth;
|
||||
private ImageEditorFragmentViewModel viewModel;
|
||||
|
||||
public static class Data {
|
||||
private final Bundle bundle;
|
||||
|
||||
Data(Bundle bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
public Data() {
|
||||
this(new Bundle());
|
||||
}
|
||||
|
||||
void writeModel(@NonNull EditorModel model) {
|
||||
byte[] bytes = ParcelUtil.serialize(model);
|
||||
bundle.putByteArray("MODEL", bytes);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public EditorModel readModel() {
|
||||
byte[] bytes = bundle.getByteArray("MODEL");
|
||||
if (bytes == null) {
|
||||
return null;
|
||||
}
|
||||
return ParcelUtil.deserialize(bytes, EditorModel.CREATOR);
|
||||
}
|
||||
}
|
||||
|
||||
private Uri imageUri;
|
||||
private Controller controller;
|
||||
private ImageEditorHud imageEditorHud;
|
||||
private ImageEditorView imageEditorView;
|
||||
|
||||
public static ImageEditorFragment newInstance(@NonNull Uri imageUri) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_IMAGE_URI, imageUri);
|
||||
|
||||
ImageEditorFragment fragment = new ImageEditorFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.setUri(imageUri);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement Controller interface.");
|
||||
}
|
||||
controller = (Controller) getActivity();
|
||||
Bundle arguments = getArguments();
|
||||
if (arguments != null) {
|
||||
imageUri = arguments.getParcelable(KEY_IMAGE_URI);
|
||||
}
|
||||
|
||||
if (imageUri == null) {
|
||||
throw new AssertionError("No KEY_IMAGE_URI supplied");
|
||||
}
|
||||
|
||||
MediaConstraints mediaConstraints = new PushMediaConstraints();
|
||||
|
||||
imageMaxWidth = mediaConstraints.getImageMaxWidth(requireContext());
|
||||
imageMaxHeight = mediaConstraints.getImageMaxHeight(requireContext());
|
||||
|
||||
StickerSearchRepository repository = new StickerSearchRepository(requireContext());
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new ImageEditorFragmentViewModel.Factory(requireActivity().getApplication(), repository))
|
||||
.get(ImageEditorFragmentViewModel.class);
|
||||
|
||||
viewModel.getStickersAvailability().observe(this, isAvailable -> imageEditorHud.setStickersAvailable(isAvailable));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.image_editor_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
||||
imageEditorView = view.findViewById(R.id.image_editor_view);
|
||||
|
||||
imageEditorHud.setEventListener(this);
|
||||
|
||||
imageEditorView.setTapListener(selectionListener);
|
||||
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
|
||||
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
|
||||
|
||||
EditorModel editorModel = null;
|
||||
|
||||
if (restoredModel != null) {
|
||||
editorModel = restoredModel;
|
||||
restoredModel = null;
|
||||
}
|
||||
|
||||
if (editorModel == null) {
|
||||
editorModel = new EditorModel();
|
||||
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
|
||||
image.getFlags().setSelectable(false).persist();
|
||||
editorModel.addElement(image);
|
||||
}
|
||||
|
||||
imageEditorView.setModel(editorModel);
|
||||
|
||||
refreshUniqueColors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUri(@NonNull Uri uri) {
|
||||
this.imageUri = uri;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Uri getUri() {
|
||||
return imageUri;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View getPlaybackControls() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object saveState() {
|
||||
Data data = new Data();
|
||||
data.writeModel(imageEditorView.getModel());
|
||||
return data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(@NonNull Object state) {
|
||||
if (state instanceof Data) {
|
||||
|
||||
Data data = (Data) state;
|
||||
EditorModel model = data.readModel();
|
||||
|
||||
if (model != null) {
|
||||
if (imageEditorView != null) {
|
||||
imageEditorView.setModel(model);
|
||||
refreshUniqueColors();
|
||||
} else {
|
||||
this.restoredModel = model;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void notifyHidden() {
|
||||
}
|
||||
|
||||
private void changeEntityColor(int selectedColor) {
|
||||
if (currentSelection != null) {
|
||||
Renderer renderer = currentSelection.getRenderer();
|
||||
if (renderer instanceof ColorableRenderer) {
|
||||
((ColorableRenderer) renderer).setColor(selectedColor);
|
||||
refreshUniqueColors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startTextEntityEditing(@NonNull EditorElement textElement, boolean selectAll) {
|
||||
imageEditorView.startTextEditing(textElement, TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()), selectAll);
|
||||
}
|
||||
|
||||
protected void addText() {
|
||||
String initialText = "";
|
||||
int color = imageEditorHud.getActiveColor();
|
||||
MultiLineTextRenderer renderer = new MultiLineTextRenderer(initialText, color);
|
||||
EditorElement element = new EditorElement(renderer);
|
||||
|
||||
imageEditorView.getModel().addElementCentered(element, 1);
|
||||
imageEditorView.invalidate();
|
||||
|
||||
currentSelection = element;
|
||||
|
||||
startTextEntityEditing(element, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK && requestCode == SELECT_NEW_STICKER_REQUEST_CODE && data != null) {
|
||||
final Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
UriGlideRenderer renderer = new UriGlideRenderer(uri, true, imageMaxWidth, imageMaxHeight);
|
||||
EditorElement element = new EditorElement(renderer);
|
||||
imageEditorView.getModel().addElementCentered(element, 0.2f);
|
||||
currentSelection = element;
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
|
||||
}
|
||||
} else if (resultCode == RESULT_OK && requestCode == SELECT_OLD_STICKER_REQUEST_CODE && data != null) {
|
||||
final Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
UriGlideRenderer renderer = new UriGlideRenderer(uri, false, imageMaxWidth, imageMaxHeight);
|
||||
EditorElement element = new EditorElement(renderer);
|
||||
imageEditorView.getModel().addElementCentered(element, 0.2f);
|
||||
currentSelection = element;
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
|
||||
}
|
||||
} else {
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
|
||||
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
||||
imageEditorView.doneTextEditing();
|
||||
|
||||
controller.onTouchEventsNeeded(mode != ImageEditorHud.Mode.NONE);
|
||||
|
||||
switch (mode) {
|
||||
case CROP: {
|
||||
imageEditorView.getModel().startCrop();
|
||||
break;
|
||||
}
|
||||
|
||||
case DRAW: {
|
||||
imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND);
|
||||
break;
|
||||
}
|
||||
|
||||
case HIGHLIGHT: {
|
||||
imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE);
|
||||
break;
|
||||
}
|
||||
|
||||
case TEXT: {
|
||||
addText();
|
||||
break;
|
||||
}
|
||||
|
||||
case INSERT_STICKER: {
|
||||
Intent intent = new Intent(getContext(), ImageEditorStickerSelectActivity.class);
|
||||
startActivityForResult(intent, SELECT_NEW_STICKER_REQUEST_CODE);
|
||||
break;
|
||||
}
|
||||
|
||||
case MOVE_DELETE:
|
||||
break;
|
||||
|
||||
case NONE: {
|
||||
imageEditorView.getModel().doneCrop();
|
||||
currentSelection = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorChange(int color) {
|
||||
imageEditorView.setDrawingBrushColor(color);
|
||||
changeEntityColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUndo() {
|
||||
imageEditorView.getModel().undo();
|
||||
refreshUniqueColors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
imageEditorView.deleteElement(currentSelection);
|
||||
refreshUniqueColors();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSave() {
|
||||
SaveAttachmentTask.showWarningDialog(requireContext(), (dialogInterface, i) -> {
|
||||
Permissions.with(this)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(requireContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
SimpleTask.run(() -> {
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
Bitmap image = imageEditorView.getModel().render(requireContext());
|
||||
|
||||
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
return BlobProvider.getInstance()
|
||||
.forData(outputStream.toByteArray())
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleUseInMemory();
|
||||
|
||||
}, uri -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext());
|
||||
SaveAttachmentTask.Attachment attachment = new SaveAttachmentTask.Attachment(uri, MediaUtil.IMAGE_JPEG, System.currentTimeMillis(), null);
|
||||
saveTask.executeOnExecutor(SignalExecutors.BOUNDED, attachment);
|
||||
});
|
||||
})
|
||||
.execute();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlipHorizontal() {
|
||||
imageEditorView.getModel().flipHorizontal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotate90AntiClockwise() {
|
||||
imageEditorView.getModel().rotate90anticlockwise();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCropAspectLock(boolean locked) {
|
||||
imageEditorView.getModel().setCropAspectLock(locked);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCropAspectLocked() {
|
||||
return imageEditorView.getModel().isCropAspectLocked();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
|
||||
controller.onRequestFullScreen(fullScreen, hideKeyboard);
|
||||
}
|
||||
|
||||
private void refreshUniqueColors() {
|
||||
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
||||
}
|
||||
|
||||
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
|
||||
imageEditorHud.setUndoAvailability(undoAvailable);
|
||||
}
|
||||
|
||||
private final ImageEditorView.TapListener selectionListener = new ImageEditorView.TapListener() {
|
||||
|
||||
@Override
|
||||
public void onEntityDown(@Nullable EditorElement editorElement) {
|
||||
if (editorElement != null) {
|
||||
controller.onTouchEventsNeeded(true);
|
||||
} else {
|
||||
currentSelection = null;
|
||||
controller.onTouchEventsNeeded(false);
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.NONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
|
||||
currentSelection = editorElement;
|
||||
if (currentSelection != null) {
|
||||
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
|
||||
} else {
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
|
||||
currentSelection = editorElement;
|
||||
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true);
|
||||
}
|
||||
}
|
||||
|
||||
private void setTextElement(@NonNull EditorElement editorElement,
|
||||
@NonNull ColorableRenderer colorableRenderer,
|
||||
boolean startEditing)
|
||||
{
|
||||
int color = colorableRenderer.getColor();
|
||||
imageEditorHud.enterMode(ImageEditorHud.Mode.TEXT);
|
||||
imageEditorHud.setActiveColor(color);
|
||||
if (startEditing) {
|
||||
startTextEntityEditing(editorElement, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public interface Controller {
|
||||
void onTouchEventsNeeded(boolean needed);
|
||||
|
||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.app.Application;
|
||||
import android.database.ContentObserver;
|
||||
import android.os.Handler;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
|
||||
import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
|
||||
import org.thoughtcrime.securesms.util.Throttler;
|
||||
|
||||
public final class ImageEditorFragmentViewModel extends ViewModel {
|
||||
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
private final MutableLiveData<Boolean> stickersAvailable;
|
||||
private final Throttler availabilityThrottler;
|
||||
private final ContentObserver packObserver;
|
||||
|
||||
private ImageEditorFragmentViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.stickersAvailable = new MutableLiveData<>();
|
||||
this.availabilityThrottler = new Throttler(500);
|
||||
this.packObserver = new ContentObserver(new Handler()) {
|
||||
@Override
|
||||
public void onChange(boolean selfChange) {
|
||||
availabilityThrottler.publish(() -> repository.getStickerFeatureAvailability(stickersAvailable::postValue));
|
||||
}
|
||||
};
|
||||
|
||||
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getStickersAvailability() {
|
||||
repository.getStickerFeatureAvailability(stickersAvailable::postValue);
|
||||
return stickersAvailable;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
application.getContentResolver().unregisterContentObserver(packObserver);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
private final Application application;
|
||||
private final StickerSearchRepository repository;
|
||||
|
||||
public Factory(@NonNull Application application, @NonNull StickerSearchRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ImageEditorFragmentViewModel(application, repository));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* The HUD (heads-up display) that contains all of the tools for interacting with
|
||||
* {@link org.thoughtcrime.securesms.imageeditor.ImageEditorView}
|
||||
*/
|
||||
public final class ImageEditorHud extends LinearLayout {
|
||||
|
||||
private View cropButton;
|
||||
private View cropFlipButton;
|
||||
private View cropRotateButton;
|
||||
private ImageView cropAspectLock;
|
||||
private View drawButton;
|
||||
private View highlightButton;
|
||||
private View textButton;
|
||||
private View stickerButton;
|
||||
private View undoButton;
|
||||
private View saveButton;
|
||||
private View deleteButton;
|
||||
private View confirmButton;
|
||||
private VerticalSlideColorPicker colorPicker;
|
||||
private RecyclerView colorPalette;
|
||||
|
||||
@NonNull
|
||||
private EventListener eventListener = NULL_EVENT_LISTENER;
|
||||
@Nullable
|
||||
private ColorPaletteAdapter colorPaletteAdapter;
|
||||
|
||||
private final Map<Mode, Set<View>> visibilityModeMap = new HashMap<>();
|
||||
private final Set<View> allViews = new HashSet<>();
|
||||
|
||||
private Mode currentMode;
|
||||
private boolean undoAvailable;
|
||||
|
||||
public ImageEditorHud(@NonNull Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public ImageEditorHud(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
inflate(getContext(), R.layout.image_editor_hud, this);
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
cropButton = findViewById(R.id.scribble_crop_button);
|
||||
cropFlipButton = findViewById(R.id.scribble_crop_flip);
|
||||
cropRotateButton = findViewById(R.id.scribble_crop_rotate);
|
||||
cropAspectLock = findViewById(R.id.scribble_crop_aspect_lock);
|
||||
colorPalette = findViewById(R.id.scribble_color_palette);
|
||||
drawButton = findViewById(R.id.scribble_draw_button);
|
||||
highlightButton = findViewById(R.id.scribble_highlight_button);
|
||||
textButton = findViewById(R.id.scribble_text_button);
|
||||
stickerButton = findViewById(R.id.scribble_sticker_button);
|
||||
undoButton = findViewById(R.id.scribble_undo_button);
|
||||
saveButton = findViewById(R.id.scribble_save_button);
|
||||
deleteButton = findViewById(R.id.scribble_delete_button);
|
||||
confirmButton = findViewById(R.id.scribble_confirm_button);
|
||||
colorPicker = findViewById(R.id.scribble_color_picker);
|
||||
|
||||
cropAspectLock.setOnClickListener(v -> {
|
||||
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
|
||||
updateCropAspectLockImage(eventListener.isCropAspectLocked());
|
||||
});
|
||||
|
||||
initializeViews();
|
||||
initializeVisibilityMap();
|
||||
setMode(Mode.NONE);
|
||||
}
|
||||
|
||||
private void updateCropAspectLockImage(boolean cropAspectLocked) {
|
||||
cropAspectLock.setImageDrawable(getResources().getDrawable(cropAspectLocked ? R.drawable.ic_crop_lock_32 : R.drawable.ic_crop_unlock_32));
|
||||
}
|
||||
|
||||
private void initializeVisibilityMap() {
|
||||
setStickersAvailable(false);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.HIGHLIGHT, confirmButton, undoButton, colorPicker, colorPalette);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.TEXT, confirmButton, deleteButton, colorPicker, colorPalette);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.MOVE_DELETE, confirmButton, deleteButton);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.INSERT_STICKER, confirmButton);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.CROP, confirmButton, cropFlipButton, cropRotateButton, cropAspectLock, undoButton);
|
||||
|
||||
for (Set<View> views : visibilityModeMap.values()) {
|
||||
allViews.addAll(views);
|
||||
}
|
||||
|
||||
allViews.add(stickerButton);
|
||||
}
|
||||
|
||||
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
|
||||
visibilityModeMap.put(mode, new HashSet<>(Arrays.asList(views)));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void setStickersAvailable(boolean stickersAvailable) {
|
||||
if (stickersAvailable) {
|
||||
setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, stickerButton, cropButton, undoButton, saveButton);
|
||||
} else {
|
||||
setVisibleViewsWhenInMode(Mode.NONE, drawButton, highlightButton, textButton, cropButton, undoButton, saveButton);
|
||||
}
|
||||
updateButtonVisibility(currentMode);
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
undoButton.setOnClickListener(v -> eventListener.onUndo());
|
||||
|
||||
deleteButton.setOnClickListener(v -> {
|
||||
eventListener.onDelete();
|
||||
setMode(Mode.NONE);
|
||||
});
|
||||
|
||||
cropButton.setOnClickListener(v -> setMode(Mode.CROP));
|
||||
cropFlipButton.setOnClickListener(v -> eventListener.onFlipHorizontal());
|
||||
cropRotateButton.setOnClickListener(v -> eventListener.onRotate90AntiClockwise());
|
||||
|
||||
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
|
||||
|
||||
colorPaletteAdapter = new ColorPaletteAdapter();
|
||||
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
|
||||
|
||||
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
colorPalette.setAdapter(colorPaletteAdapter);
|
||||
|
||||
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
|
||||
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
|
||||
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
|
||||
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
|
||||
saveButton.setOnClickListener(v -> eventListener.onSave());
|
||||
}
|
||||
|
||||
public void setColorPalette(@NonNull Set<Integer> colors) {
|
||||
if (colorPaletteAdapter != null) {
|
||||
colorPaletteAdapter.setColors(colors);
|
||||
}
|
||||
}
|
||||
|
||||
public int getActiveColor() {
|
||||
return colorPicker.getActiveColor();
|
||||
}
|
||||
|
||||
public void setActiveColor(int color) {
|
||||
colorPicker.setActiveColor(color);
|
||||
}
|
||||
|
||||
public void setEventListener(@Nullable EventListener eventListener) {
|
||||
this.eventListener = eventListener != null ? eventListener : NULL_EVENT_LISTENER;
|
||||
}
|
||||
|
||||
public void enterMode(@NonNull Mode mode) {
|
||||
setMode(mode, false);
|
||||
}
|
||||
|
||||
public void setMode(@NonNull Mode mode) {
|
||||
setMode(mode, true);
|
||||
}
|
||||
|
||||
private void setMode(@NonNull Mode mode, boolean notify) {
|
||||
this.currentMode = mode;
|
||||
updateButtonVisibility(mode);
|
||||
|
||||
switch (mode) {
|
||||
case CROP: presentModeCrop(); break;
|
||||
case DRAW: presentModeDraw(); break;
|
||||
case HIGHLIGHT: presentModeHighlight(); break;
|
||||
case TEXT: presentModeText(); break;
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
eventListener.onModeStarted(mode);
|
||||
}
|
||||
eventListener.onRequestFullScreen(mode != Mode.NONE, mode != Mode.TEXT);
|
||||
}
|
||||
|
||||
private void updateButtonVisibility(@NonNull Mode mode) {
|
||||
Set<View> visibleButtons = visibilityModeMap.get(mode);
|
||||
for (View button : allViews) {
|
||||
button.setVisibility(buttonIsVisible(visibleButtons, button) ? VISIBLE : GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean buttonIsVisible(@Nullable Set<View> visibleButtons, @NonNull View button) {
|
||||
return visibleButtons != null &&
|
||||
visibleButtons.contains(button) &&
|
||||
(button != undoButton || undoAvailable);
|
||||
}
|
||||
|
||||
private void presentModeCrop() {
|
||||
updateCropAspectLockImage(eventListener.isCropAspectLocked());
|
||||
}
|
||||
|
||||
private void presentModeDraw() {
|
||||
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
|
||||
colorPicker.setActiveColor(Color.RED);
|
||||
}
|
||||
|
||||
private void presentModeHighlight() {
|
||||
colorPicker.setOnColorChangeListener(highlightOnColorChangeListener);
|
||||
colorPicker.setActiveColor(Color.YELLOW);
|
||||
}
|
||||
|
||||
private void presentModeText() {
|
||||
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
|
||||
colorPicker.setActiveColor(Color.WHITE);
|
||||
}
|
||||
|
||||
private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = selectedColor -> eventListener.onColorChange(selectedColor);
|
||||
|
||||
private final VerticalSlideColorPicker.OnColorChangeListener highlightOnColorChangeListener = selectedColor -> eventListener.onColorChange(replaceAlphaWith128(selectedColor));
|
||||
|
||||
private static int replaceAlphaWith128(int color) {
|
||||
return color & ~0xff000000 | 0x80000000;
|
||||
}
|
||||
|
||||
public void setUndoAvailability(boolean undoAvailable) {
|
||||
this.undoAvailable = undoAvailable;
|
||||
|
||||
undoButton.setVisibility(buttonIsVisible(visibilityModeMap.get(currentMode), undoButton) ? VISIBLE : GONE);
|
||||
}
|
||||
|
||||
public enum Mode {
|
||||
NONE,
|
||||
CROP,
|
||||
TEXT,
|
||||
DRAW,
|
||||
HIGHLIGHT,
|
||||
MOVE_DELETE,
|
||||
INSERT_STICKER,
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onModeStarted(@NonNull Mode mode);
|
||||
void onColorChange(int color);
|
||||
void onUndo();
|
||||
void onDelete();
|
||||
void onSave();
|
||||
void onFlipHorizontal();
|
||||
void onRotate90AntiClockwise();
|
||||
void onCropAspectLock(boolean locked);
|
||||
boolean isCropAspectLocked();
|
||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||
}
|
||||
|
||||
private static final EventListener NULL_EVENT_LISTENER = new EventListener() {
|
||||
|
||||
@Override
|
||||
public void onModeStarted(@NonNull Mode mode) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onColorChange(int color) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUndo() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSave() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFlipHorizontal() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotate90AntiClockwise() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCropAspectLock(boolean locked) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCropAspectLocked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard) {
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
public final class ImageEditorStickerSelectActivity extends FragmentActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
setContentView(R.layout.scribble_select_new_sticker_activity);
|
||||
|
||||
MediaKeyboard mediaKeyboard = findViewById(R.id.emoji_drawer);
|
||||
|
||||
mediaKeyboard.setProviders(0, new StickerKeyboardProvider(this, new StickerKeyboardProvider.StickerEventListener() {
|
||||
@Override
|
||||
public void onStickerSelected(@NonNull StickerRecord sticker) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(sticker.getUri());
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() ->
|
||||
DatabaseFactory.getStickerDatabase(getApplicationContext())
|
||||
.updateStickerLastUsedTime(sticker.getRowId(), System.currentTimeMillis())
|
||||
);
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStickerManagementClicked() {
|
||||
startActivity(StickerManagementActivity.getIntent(ImageEditorStickerSelectActivity.this));
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
mediaKeyboard.setKeyboardListener(new MediaKeyboard.MediaKeyboardListener() {
|
||||
@Override
|
||||
public void onShown() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHidden() {
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
|
||||
}
|
||||
});
|
||||
|
||||
mediaKeyboard.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
import org.thoughtcrime.securesms.imageeditor.Bounds;
|
||||
import org.thoughtcrime.securesms.imageeditor.Renderer;
|
||||
import org.thoughtcrime.securesms.imageeditor.RendererContext;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Uses Glide to load an image and implements a {@link Renderer}.
|
||||
*
|
||||
* The image can be encrypted.
|
||||
*/
|
||||
final class UriGlideRenderer implements Renderer {
|
||||
|
||||
private static final int PREVIEW_DIMENSION_LIMIT = 2048;
|
||||
|
||||
private final Uri imageUri;
|
||||
private final Paint paint = new Paint();
|
||||
private final Matrix imageProjectionMatrix = new Matrix();
|
||||
private final Matrix temp = new Matrix();
|
||||
private final boolean decryptable;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
|
||||
@Nullable
|
||||
private Bitmap bitmap;
|
||||
|
||||
UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight) {
|
||||
this.imageUri = imageUri;
|
||||
this.decryptable = decryptable;
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setFilterBitmap(true);
|
||||
paint.setDither(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render(@NonNull RendererContext rendererContext) {
|
||||
if (getBitmap() == null) {
|
||||
if (rendererContext.isBlockingLoad()) {
|
||||
try {
|
||||
Bitmap bitmap = getBitmapGlideRequest(rendererContext.context, false).submit().get();
|
||||
setBitmap(rendererContext, bitmap);
|
||||
} catch (ExecutionException | InterruptedException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
} else {
|
||||
getBitmapGlideRequest(rendererContext.context, true).into(new CustomTarget<Bitmap>() {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
|
||||
setBitmap(rendererContext, resource);
|
||||
|
||||
rendererContext.invalidate.onInvalidate(UriGlideRenderer.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
bitmap = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
final Bitmap bitmap = getBitmap();
|
||||
if (bitmap != null) {
|
||||
rendererContext.save();
|
||||
|
||||
rendererContext.canvasMatrix.concat(imageProjectionMatrix);
|
||||
|
||||
// Units are image level pixels at this point.
|
||||
|
||||
int alpha = paint.getAlpha();
|
||||
paint.setAlpha(rendererContext.getAlpha(alpha));
|
||||
|
||||
rendererContext.canvas.drawBitmap(bitmap, 0, 0, paint);
|
||||
|
||||
paint.setAlpha(alpha);
|
||||
|
||||
rendererContext.restore();
|
||||
} else if (rendererContext.isBlockingLoad()) {
|
||||
// If failed to load, we draw a black out, in case image was sticker positioned to cover private info.
|
||||
rendererContext.canvas.drawRect(Bounds.FULL_BOUNDS, paint);
|
||||
}
|
||||
}
|
||||
|
||||
private GlideRequest<Bitmap> getBitmapGlideRequest(@NonNull Context context, boolean preview) {
|
||||
int width = this.maxWidth;
|
||||
int height = this.maxHeight;
|
||||
|
||||
if (preview) {
|
||||
width = Math.min(width, PREVIEW_DIMENSION_LIMIT);
|
||||
height = Math.min(height, PREVIEW_DIMENSION_LIMIT);
|
||||
}
|
||||
|
||||
return GlideApp.with(context)
|
||||
.asBitmap()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.override(width, height)
|
||||
.centerInside()
|
||||
.load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hitTest(float x, float y) {
|
||||
return pixelAlphaNotZero(x, y);
|
||||
}
|
||||
|
||||
private boolean pixelAlphaNotZero(float x, float y) {
|
||||
Bitmap bitmap = getBitmap();
|
||||
|
||||
if (bitmap == null) return false;
|
||||
|
||||
imageProjectionMatrix.invert(temp);
|
||||
|
||||
float[] onBmp = new float[2];
|
||||
temp.mapPoints(onBmp, new float[]{ x, y });
|
||||
|
||||
int xInt = (int) onBmp[0];
|
||||
int yInt = (int) onBmp[1];
|
||||
|
||||
if (xInt >= 0 && xInt < bitmap.getWidth() && yInt >= 0 && yInt < bitmap.getHeight()) {
|
||||
return (bitmap.getPixel(xInt, yInt) & 0xff000000) != 0;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Always use this getter, as Bitmap is kept in Glide's LRUCache, so it could have been recycled
|
||||
* by Glide. If it has, or was never set, this method returns null.
|
||||
*/
|
||||
private @Nullable Bitmap getBitmap() {
|
||||
if (bitmap != null && bitmap.isRecycled()) {
|
||||
bitmap = null;
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
private void setBitmap(@NonNull RendererContext rendererContext, @Nullable Bitmap bitmap) {
|
||||
this.bitmap = bitmap;
|
||||
if (bitmap != null) {
|
||||
RectF from = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||
imageProjectionMatrix.setRectToRect(from, Bounds.FULL_BOUNDS, Matrix.ScaleToFit.CENTER);
|
||||
rendererContext.rendererReady.onReady(UriGlideRenderer.this, cropMatrix(bitmap), new Point(bitmap.getWidth(), bitmap.getHeight()));
|
||||
}
|
||||
}
|
||||
|
||||
private static Matrix cropMatrix(Bitmap bitmap) {
|
||||
Matrix matrix = new Matrix();
|
||||
if (bitmap.getWidth() > bitmap.getHeight()) {
|
||||
matrix.preScale(1, ((float) bitmap.getHeight()) / bitmap.getWidth());
|
||||
} else {
|
||||
matrix.preScale(((float) bitmap.getWidth()) / bitmap.getHeight(), 1);
|
||||
}
|
||||
return matrix;
|
||||
}
|
||||
|
||||
public static final Creator<UriGlideRenderer> CREATOR = new Creator<UriGlideRenderer>() {
|
||||
@Override
|
||||
public UriGlideRenderer createFromParcel(Parcel in) {
|
||||
return new UriGlideRenderer(Uri.parse(in.readString()),
|
||||
in.readInt() == 1,
|
||||
in.readInt(),
|
||||
in.readInt()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriGlideRenderer[] newArray(int size) {
|
||||
return new UriGlideRenderer[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeString(imageUri.toString());
|
||||
dest.writeInt(decryptable ? 1 : 0);
|
||||
dest.writeInt(maxWidth);
|
||||
dest.writeInt(maxHeight);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package org.thoughtcrime.securesms.scribbles.widget;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class ColorPaletteAdapter extends RecyclerView.Adapter<ColorPaletteAdapter.ColorViewHolder> {
|
||||
|
||||
private final List<Integer> colors = new ArrayList<>();
|
||||
|
||||
private EventListener eventListener;
|
||||
|
||||
@Override
|
||||
public @NonNull ColorViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ColorViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.item_color, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ColorViewHolder holder, int position) {
|
||||
holder.bind(colors.get(position), eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return colors.size();
|
||||
}
|
||||
|
||||
public void setColors(@NonNull Collection<Integer> colors) {
|
||||
this.colors.clear();
|
||||
this.colors.addAll(colors);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setEventListener(@Nullable EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onColorSelected(int color);
|
||||
}
|
||||
|
||||
static class ColorViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
ImageView foreground;
|
||||
|
||||
ColorViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
foreground = itemView.findViewById(R.id.palette_item_foreground);
|
||||
}
|
||||
|
||||
void bind(int color, @Nullable EventListener eventListener) {
|
||||
foreground.setColorFilter(color, PorterDuff.Mode.SRC_IN);
|
||||
|
||||
if (eventListener != null) {
|
||||
itemView.setOnClickListener(v -> eventListener.onColorSelected(color));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
/**
|
||||
* 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 static final float INDICATOR_TO_BAR_WIDTH_RATIO = 0.5f;
|
||||
|
||||
private Paint paint;
|
||||
private Paint strokePaint;
|
||||
private Paint indicatorStrokePaint;
|
||||
private Paint indicatorFillPaint;
|
||||
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 float indicatorRadius;
|
||||
private int[] colors;
|
||||
|
||||
private int touchY;
|
||||
private int activeColor;
|
||||
|
||||
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);
|
||||
|
||||
indicatorStrokePaint = new Paint(strokePaint);
|
||||
indicatorStrokePaint.setStrokeWidth(borderWidth / 2);
|
||||
|
||||
indicatorFillPaint = new Paint();
|
||||
indicatorFillPaint.setStyle(Paint.Style.FILL);
|
||||
indicatorFillPaint.setAntiAlias(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
path.addCircle(centerX, borderWidth + colorPickerRadius + indicatorRadius, colorPickerRadius, Path.Direction.CW);
|
||||
path.addRect(colorPickerBody, Path.Direction.CW);
|
||||
path.addCircle(centerX, viewHeight - (borderWidth + colorPickerRadius + indicatorRadius), colorPickerRadius, Path.Direction.CW);
|
||||
|
||||
bitmapCanvas.drawColor(Color.TRANSPARENT);
|
||||
|
||||
bitmapCanvas.drawPath(path, strokePaint);
|
||||
bitmapCanvas.drawPath(path, paint);
|
||||
|
||||
canvas.drawBitmap(bitmap, 0, 0, null);
|
||||
|
||||
touchY = Math.max((int) colorPickerBody.top, touchY);
|
||||
|
||||
indicatorFillPaint.setColor(activeColor);
|
||||
canvas.drawCircle(centerX, touchY, indicatorRadius, indicatorFillPaint);
|
||||
canvas.drawCircle(centerX, touchY, indicatorRadius, indicatorStrokePaint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent event) {
|
||||
touchY = (int) Math.min(event.getY(), colorPickerBody.bottom);
|
||||
touchY = (int) Math.max(colorPickerBody.top, touchY);
|
||||
|
||||
activeColor = bitmap.getPixel(viewWidth/2, touchY);
|
||||
|
||||
if (onColorChangeListener != null) {
|
||||
onColorChangeListener.onColorChange(activeColor);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
viewWidth = w;
|
||||
viewHeight = h;
|
||||
|
||||
if (viewWidth <= 0 || viewHeight <= 0) return;
|
||||
|
||||
int barWidth = (int) (viewWidth * INDICATOR_TO_BAR_WIDTH_RATIO);
|
||||
|
||||
centerX = viewWidth / 2;
|
||||
indicatorRadius = (viewWidth / 2) - borderWidth;
|
||||
colorPickerRadius = (barWidth / 2) - borderWidth;
|
||||
|
||||
colorPickerBody = new RectF(centerX - colorPickerRadius,
|
||||
borderWidth + colorPickerRadius + indicatorRadius,
|
||||
centerX + colorPickerRadius,
|
||||
viewHeight - (borderWidth + colorPickerRadius + indicatorRadius));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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 setActiveColor(int color) {
|
||||
activeColor = color;
|
||||
|
||||
if (colorPickerBody != null) {
|
||||
touchY = (int) colorPickerBody.top;
|
||||
}
|
||||
|
||||
if (onColorChangeListener != null) {
|
||||
onColorChangeListener.onColorChange(color);
|
||||
}
|
||||
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public int getActiveColor() {
|
||||
return activeColor;
|
||||
}
|
||||
|
||||
public void setOnColorChangeListener(OnColorChangeListener onColorChangeListener) {
|
||||
this.onColorChangeListener = onColorChangeListener;
|
||||
}
|
||||
|
||||
public interface OnColorChangeListener {
|
||||
void onColorChange(int selectedColor);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user