mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Refresh media selection and sending flow with a shiny new UX.
This commit is contained in:
committed by
Greyson Parrelli
parent
a940487611
commit
664d6475d9
@@ -0,0 +1,199 @@
|
||||
package org.thoughtcrime.securesms.scribbles
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Path
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.PointF
|
||||
import android.graphics.RectF
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.widget.SeekBar
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.customizeOnDraw
|
||||
|
||||
/**
|
||||
* One stop shop to turn an AppCompatSeekBar into an HSV Color Slider.
|
||||
*/
|
||||
object HSVColorSlider {
|
||||
|
||||
private const val MAX_SEEK_DIVISIONS = 1023
|
||||
private const val MAX_HUE = 360
|
||||
|
||||
private val colors: IntArray = (0..MAX_SEEK_DIVISIONS).map { hue ->
|
||||
ColorUtils.HSLToColor(
|
||||
floatArrayOf(
|
||||
hue.toHue(MAX_SEEK_DIVISIONS),
|
||||
1f,
|
||||
calculateLightness(hue.toFloat(), 0.4f)
|
||||
)
|
||||
)
|
||||
}.toIntArray()
|
||||
|
||||
fun AppCompatSeekBar.getColor(): Int {
|
||||
return colors[progress]
|
||||
}
|
||||
|
||||
fun AppCompatSeekBar.setColor(color: Int) {
|
||||
val index = colors.indexOf(color)
|
||||
progress = if (index >= 0) {
|
||||
index
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
fun AppCompatSeekBar.setUpForColor(
|
||||
@ColorInt thumbBorderColor: Int,
|
||||
onColorChanged: (Int) -> Unit,
|
||||
onDragStart: () -> Unit,
|
||||
onDragEnd: () -> Unit
|
||||
) {
|
||||
max = MAX_SEEK_DIVISIONS
|
||||
thumb = createThumbDrawable(thumbBorderColor)
|
||||
progressDrawable = createColorProgressDrawable()
|
||||
setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
val color = colors[progress]
|
||||
(thumb as ThumbDrawable).setColor(color)
|
||||
onColorChanged(color)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {
|
||||
onDragStart()
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {
|
||||
onDragEnd()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
progress = 0
|
||||
(thumb as ThumbDrawable).setColor(colors[progress])
|
||||
}
|
||||
|
||||
fun createThumbDrawable(@ColorInt borderColor: Int): Drawable {
|
||||
return ThumbDrawable(borderColor)
|
||||
}
|
||||
|
||||
private fun createColorProgressDrawable(): Drawable {
|
||||
return GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, colors).forSeekBar()
|
||||
}
|
||||
|
||||
private fun calculateLightness(hue: Float, valueFor60To80: Float = 0.3f): Float {
|
||||
|
||||
val point1 = PointF()
|
||||
val point2 = PointF()
|
||||
|
||||
if (hue >= 0f && hue < 60f) {
|
||||
point1.set(0f, 0.45f)
|
||||
point2.set(60f, valueFor60To80)
|
||||
} else if (hue >= 60f && hue < 180f) {
|
||||
return valueFor60To80
|
||||
} else if (hue >= 180f && hue < 240f) {
|
||||
point1.set(180f, valueFor60To80)
|
||||
point2.set(240f, 0.5f)
|
||||
} else if (hue >= 240f && hue < 300f) {
|
||||
point1.set(240f, 0.5f)
|
||||
point2.set(300f, 0.4f)
|
||||
} else if (hue >= 300f && hue < 360f) {
|
||||
point1.set(300f, 0.4f)
|
||||
point2.set(360f, 0.45f)
|
||||
} else {
|
||||
return 0.45f
|
||||
}
|
||||
|
||||
return interpolate(point1, point2, hue)
|
||||
}
|
||||
|
||||
private fun interpolate(point1: PointF, point2: PointF, x: Float): Float {
|
||||
return ((point1.y * (point2.x - x)) + (point2.y * (x - point1.x))) / (point2.x - point1.x)
|
||||
}
|
||||
|
||||
private fun Number.toHue(max: Number): Float {
|
||||
return Util.clamp(toFloat() * (MAX_HUE / max.toFloat()), 0f, MAX_HUE.toFloat())
|
||||
}
|
||||
|
||||
private fun Drawable.forSeekBar(): Drawable {
|
||||
val height: Int = ViewUtil.dpToPx(1)
|
||||
val radii: FloatArray = (1..8).map { 50f }.toFloatArray()
|
||||
val bounds = RectF()
|
||||
val clipPath = Path()
|
||||
|
||||
return customizeOnDraw { wrapped, canvas ->
|
||||
canvas.save()
|
||||
bounds.set(this.bounds)
|
||||
bounds.inset(0f, (height / 2f) + 1)
|
||||
|
||||
clipPath.rewind()
|
||||
clipPath.addRoundRect(bounds, radii, Path.Direction.CW)
|
||||
|
||||
canvas.clipPath(clipPath)
|
||||
wrapped.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
|
||||
private class ThumbDrawable(@ColorInt borderColor: Int) : Drawable() {
|
||||
|
||||
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = borderColor
|
||||
}
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||
color = Color.TRANSPARENT
|
||||
}
|
||||
|
||||
private val borderWidth: Int = ViewUtil.dpToPx(THUMB_MARGIN)
|
||||
private val thumbInnerSize: Int = ViewUtil.dpToPx(THUMB_INNER_SIZE)
|
||||
private val innerRadius: Float = thumbInnerSize / 2f
|
||||
private val thumbSize: Float = (thumbInnerSize + borderWidth).toFloat()
|
||||
private val thumbRadius: Float = thumbSize / 2f
|
||||
|
||||
override fun getIntrinsicHeight(): Int = ViewUtil.dpToPx(48)
|
||||
|
||||
override fun getIntrinsicWidth(): Int = ViewUtil.dpToPx(48)
|
||||
|
||||
fun setColor(@ColorInt color: Int) {
|
||||
paint.color = color
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
canvas.drawCircle(
|
||||
(bounds.width() / 2f) + bounds.left,
|
||||
(bounds.height() / 2f) + bounds.top,
|
||||
thumbRadius,
|
||||
borderPaint
|
||||
)
|
||||
canvas.drawCircle(
|
||||
(bounds.width() / 2f) + bounds.left,
|
||||
(bounds.height() / 2f) + bounds.top,
|
||||
innerRadius,
|
||||
paint
|
||||
)
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int = PixelFormat.TRANSPARENT
|
||||
|
||||
companion object {
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val THUMB_INNER_SIZE = 4
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val THUMB_MARGIN = 24
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
@@ -14,15 +16,23 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.engine.GlideException;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.Target;
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.ResizeAnimation;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.imageeditor.Bounds;
|
||||
import org.thoughtcrime.securesms.imageeditor.ColorableRenderer;
|
||||
@@ -39,12 +49,12 @@ import org.thoughtcrime.securesms.mms.PushMediaConstraints;
|
||||
import org.thoughtcrime.securesms.mms.SentMediaQuality;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
@@ -54,16 +64,15 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
public final class ImageEditorFragment extends Fragment implements ImageEditorHud.EventListener,
|
||||
VerticalSlideColorPicker.OnColorChangeListener,
|
||||
MediaSendPageFragment {
|
||||
public final class ImageEditorFragment extends Fragment implements ImageEditorHudV2.EventListener,
|
||||
MediaSendPageFragment,
|
||||
TextEntryDialogFragment.Controller
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(ImageEditorFragment.class);
|
||||
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
private static final String KEY_MODE = "mode";
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
private static final String KEY_MODE = "mode";
|
||||
|
||||
private static final int SELECT_STICKER_REQUEST_CODE = 124;
|
||||
|
||||
@@ -72,13 +81,13 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
private Pair<Uri, FaceDetectionResult> cachedFaceDetection;
|
||||
|
||||
@Nullable private EditorElement currentSelection;
|
||||
private int imageMaxHeight;
|
||||
private int imageMaxWidth;
|
||||
private int imageMaxHeight;
|
||||
private int imageMaxWidth;
|
||||
|
||||
public static class Data {
|
||||
private final Bundle bundle;
|
||||
|
||||
Data(Bundle bundle) {
|
||||
public Data(Bundle bundle) {
|
||||
this.bundle = bundle;
|
||||
}
|
||||
|
||||
@@ -99,12 +108,17 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
return ParcelUtil.deserialize(bytes, EditorModel.CREATOR);
|
||||
}
|
||||
|
||||
public @NonNull Bundle getBundle() {
|
||||
return bundle;
|
||||
}
|
||||
}
|
||||
|
||||
private Uri imageUri;
|
||||
private Controller controller;
|
||||
private ImageEditorHud imageEditorHud;
|
||||
private ImageEditorHudV2 imageEditorHud;
|
||||
private ImageEditorView imageEditorView;
|
||||
private boolean hasMadeAnEditThisSession;
|
||||
|
||||
public static ImageEditorFragment newInstanceForAvatarCapture(@NonNull Uri imageUri) {
|
||||
ImageEditorFragment fragment = newInstance(imageUri);
|
||||
@@ -129,6 +143,15 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
return fragment;
|
||||
}
|
||||
|
||||
public void setMode(ImageEditorHudV2.Mode mode) {
|
||||
ImageEditorHudV2.Mode currentMode = imageEditorHud.getMode();
|
||||
if (currentMode == mode) {
|
||||
return;
|
||||
}
|
||||
|
||||
imageEditorHud.setMode(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -139,7 +162,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
} else if (getActivity() instanceof Controller) {
|
||||
controller = (Controller) getActivity();
|
||||
} else {
|
||||
throw new IllegalStateException("Parent activity must implement Controller interface.");
|
||||
throw new IllegalStateException("Parent must implement Controller interface.");
|
||||
}
|
||||
|
||||
Bundle arguments = getArguments();
|
||||
@@ -172,16 +195,21 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
imageEditorHud = view.findViewById(R.id.scribble_hud);
|
||||
imageEditorView = view.findViewById(R.id.image_editor_view);
|
||||
|
||||
int width = getResources().getDisplayMetrics().widthPixels;
|
||||
imageEditorView.setMinimumHeight((int) ((16 / 9f) * width));
|
||||
imageEditorView.requestLayout();
|
||||
|
||||
imageEditorHud.setEventListener(this);
|
||||
|
||||
imageEditorView.setDrawListener(drawListener);
|
||||
imageEditorView.setTapListener(selectionListener);
|
||||
imageEditorView.setDrawingChangedListener(this::refreshUniqueColors);
|
||||
imageEditorView.setDrawingChangedListener(stillTouching -> onDrawingChanged(stillTouching, true));
|
||||
imageEditorView.setUndoRedoStackListener(this::onUndoRedoAvailabilityChanged);
|
||||
|
||||
EditorModel editorModel = null;
|
||||
|
||||
if (restoredModel != null) {
|
||||
editorModel = restoredModel;
|
||||
editorModel = restoredModel;
|
||||
restoredModel = null;
|
||||
}
|
||||
|
||||
@@ -198,9 +226,11 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
break;
|
||||
}
|
||||
|
||||
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight));
|
||||
EditorElement image = new EditorElement(new UriGlideRenderer(imageUri, true, imageMaxWidth, imageMaxHeight, UriGlideRenderer.STRONG_BLUR, mainImageRequestListener));
|
||||
image.getFlags().setSelectable(false).persist();
|
||||
editorModel.addElement(image);
|
||||
} else {
|
||||
controller.onMainImageLoaded();
|
||||
}
|
||||
|
||||
if (mode == Mode.AVATAR_CAPTURE || mode == Mode.AVATAR_EDIT) {
|
||||
@@ -208,7 +238,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
|
||||
if (mode == Mode.AVATAR_CAPTURE) {
|
||||
imageEditorHud.enterMode(ImageEditorHud.Mode.CROP);
|
||||
imageEditorHud.enterMode(ImageEditorHudV2.Mode.CROP);
|
||||
}
|
||||
|
||||
imageEditorView.setModel(editorModel);
|
||||
@@ -218,7 +248,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
SignalStore.tooltips().markBlurHudIconTooltipSeen();
|
||||
}
|
||||
|
||||
refreshUniqueColors();
|
||||
onDrawingChanged(false, false);
|
||||
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(), onBackPressedCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -255,7 +287,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
if (model != null) {
|
||||
if (imageEditorView != null) {
|
||||
imageEditorView.setModel(model);
|
||||
refreshUniqueColors();
|
||||
onDrawingChanged(false, false);
|
||||
} else {
|
||||
this.restoredModel = model;
|
||||
}
|
||||
@@ -274,13 +306,36 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
Renderer renderer = currentSelection.getRenderer();
|
||||
if (renderer instanceof ColorableRenderer) {
|
||||
((ColorableRenderer) renderer).setColor(selectedColor);
|
||||
refreshUniqueColors();
|
||||
onDrawingChanged(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startTextEntityEditing(@NonNull EditorElement textElement, boolean selectAll) {
|
||||
imageEditorView.startTextEditing(textElement, TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()), selectAll);
|
||||
imageEditorView.startTextEditing(textElement);
|
||||
|
||||
TextEntryDialogFragment.Companion.show(
|
||||
getChildFragmentManager(),
|
||||
textElement,
|
||||
TextSecurePreferences.isIncognitoKeyboardEnabled(requireContext()),
|
||||
selectAll,
|
||||
imageEditorHud.getColorIndex()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void zoomToFitText(@NonNull EditorElement editorElement, @NonNull MultiLineTextRenderer textRenderer) {
|
||||
imageEditorView.zoomToFitText(editorElement, textRenderer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextEntryDialogDismissed(boolean hasText) {
|
||||
imageEditorView.doneTextEditing();
|
||||
|
||||
if (!hasText) {
|
||||
onUndo();
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.DRAW);
|
||||
}
|
||||
}
|
||||
|
||||
protected void addText() {
|
||||
@@ -307,19 +362,32 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
EditorElement element = new EditorElement(renderer, EditorModel.Z_STICKERS);
|
||||
imageEditorView.getModel().addElementCentered(element, 0.2f);
|
||||
currentSelection = element;
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.MOVE_DELETE);
|
||||
hasMadeAnEditThisSession = true;
|
||||
}
|
||||
} else {
|
||||
imageEditorHud.setMode(ImageEditorHud.Mode.NONE);
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.DRAW);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onModeStarted(@NonNull ImageEditorHud.Mode mode) {
|
||||
public void onModeStarted(@NonNull ImageEditorHudV2.Mode mode, @NonNull ImageEditorHudV2.Mode previousMode) {
|
||||
onBackPressedCallback.setEnabled(shouldHandleOnBackPressed(mode));
|
||||
|
||||
imageEditorView.setMode(ImageEditorView.Mode.MoveAndResize);
|
||||
imageEditorView.doneTextEditing();
|
||||
|
||||
controller.onTouchEventsNeeded(mode != ImageEditorHud.Mode.NONE);
|
||||
controller.onTouchEventsNeeded(mode != ImageEditorHudV2.Mode.NONE);
|
||||
|
||||
boolean shouldScaleViewPortForCurrentMode = shouldScaleViewPort(mode);
|
||||
boolean shouldScaleViewPortForPreviousMode = shouldScaleViewPort(previousMode);
|
||||
|
||||
if (shouldScaleViewPortForCurrentMode != shouldScaleViewPortForPreviousMode) {
|
||||
if (shouldScaleViewPortForCurrentMode) {
|
||||
scaleViewPortForDrawing();
|
||||
} else {
|
||||
restoreViewPortScaling();
|
||||
}
|
||||
}
|
||||
|
||||
switch (mode) {
|
||||
case CROP: {
|
||||
@@ -327,18 +395,14 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
break;
|
||||
}
|
||||
|
||||
case DRAW: {
|
||||
imageEditorView.startDrawing(0.01f, Paint.Cap.ROUND, false);
|
||||
break;
|
||||
}
|
||||
|
||||
case DRAW:
|
||||
case HIGHLIGHT: {
|
||||
imageEditorView.startDrawing(0.03f, Paint.Cap.SQUARE, false);
|
||||
onBrushWidthChange(imageEditorHud.getActiveBrushWidth());
|
||||
break;
|
||||
}
|
||||
|
||||
case BLUR: {
|
||||
imageEditorView.startDrawing(0.052f, Paint.Cap.ROUND, true);
|
||||
onBrushWidthChange(imageEditorHud.getActiveBrushWidth());
|
||||
imageEditorHud.setBlurFacesToggleEnabled(imageEditorView.getModel().hasFaceRenderer());
|
||||
break;
|
||||
}
|
||||
@@ -360,6 +424,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
case NONE: {
|
||||
imageEditorView.getModel().doneCrop();
|
||||
currentSelection = null;
|
||||
hasMadeAnEditThisSession = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -371,6 +436,23 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
changeEntityColor(color);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextColorChange(int colorIndex) {
|
||||
imageEditorHud.setColorIndex(colorIndex);
|
||||
onColorChange(imageEditorHud.getActiveColor());
|
||||
}
|
||||
|
||||
private static final float MINIMUM_DRAW_WIDTH = 0.01f;
|
||||
private static final float MAXIMUM_DRAW_WIDTH = 0.05f;
|
||||
|
||||
@Override
|
||||
public void onBrushWidthChange(int widthPercentage) {
|
||||
ImageEditorHudV2.Mode mode = imageEditorHud.getMode();
|
||||
|
||||
float interpolatedWidth = MINIMUM_DRAW_WIDTH + (MAXIMUM_DRAW_WIDTH - MINIMUM_DRAW_WIDTH) * (widthPercentage / 100f);
|
||||
imageEditorView.startDrawing(interpolatedWidth, mode == ImageEditorHudV2.Mode.HIGHLIGHT ? Paint.Cap.SQUARE : Paint.Cap.ROUND, mode == ImageEditorHudV2.Mode.BLUR);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBlurFacesToggled(boolean enabled) {
|
||||
EditorModel model = imageEditorView.getModel();
|
||||
@@ -407,7 +489,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
if (bitmap != null) {
|
||||
FaceDetector detector = new AndroidFaceDetector();
|
||||
|
||||
Point size = model.getOutputSizeMaxWidth(1000);
|
||||
Point size = model.getOutputSizeMaxWidth(1000);
|
||||
Bitmap render = model.render(ApplicationDependencies.getApplication(), size);
|
||||
try {
|
||||
return new FaceDetectionResult(detector.detect(render), new Point(render.getWidth(), render.getHeight()), inverseCropPosition);
|
||||
@@ -427,17 +509,40 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onClearAll() {
|
||||
imageEditorView.getModel().clearUndoStack();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCancel() {
|
||||
if (hasMadeAnEditThisSession) {
|
||||
new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.MediaReviewImagePageFragment__discard_changes)
|
||||
.setMessage(R.string.MediaReviewImagePageFragment__youll_lose_any_changes)
|
||||
.setPositiveButton(R.string.MediaReviewImagePageFragment__discard, (d, w) -> {
|
||||
d.dismiss();
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.NONE);
|
||||
controller.onCancelEditing();
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, (d, w) -> d.dismiss())
|
||||
.show();
|
||||
} else {
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.NONE);
|
||||
controller.onCancelEditing();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUndo() {
|
||||
imageEditorView.getModel().undo();
|
||||
refreshUniqueColors();
|
||||
imageEditorHud.setBlurFacesToggleEnabled(imageEditorView.getModel().hasFaceRenderer());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete() {
|
||||
imageEditorView.deleteElement(currentSelection);
|
||||
refreshUniqueColors();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -469,8 +574,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCropAspectLock(boolean locked) {
|
||||
imageEditorView.getModel().setCropAspectLock(locked);
|
||||
public void onCropAspectLock() {
|
||||
imageEditorView.getModel().setCropAspectLock(!imageEditorView.getModel().isCropAspectLocked());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -488,6 +593,42 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
controller.onDoneEditing();
|
||||
}
|
||||
|
||||
private ResizeAnimation resizeAnimation;
|
||||
|
||||
private void scaleViewPortForDrawing() {
|
||||
if (resizeAnimation != null) {
|
||||
resizeAnimation.cancel();
|
||||
}
|
||||
|
||||
float aspectRatio = 9 / 16f;
|
||||
int targetWidth = requireView().getMeasuredWidth() - ViewUtil.dpToPx(32);
|
||||
int targetHeight = (int) ((1 / aspectRatio) * targetWidth);
|
||||
|
||||
if (targetWidth < requireView().getMeasuredWidth()) {
|
||||
resizeAnimation = new ResizeAnimation(imageEditorView, targetWidth, targetHeight);
|
||||
resizeAnimation.setDuration(250);
|
||||
imageEditorView.startAnimation(resizeAnimation);
|
||||
}
|
||||
}
|
||||
|
||||
private void restoreViewPortScaling() {
|
||||
if (resizeAnimation != null) {
|
||||
resizeAnimation.cancel();
|
||||
}
|
||||
|
||||
float aspectRatio = 9 / 16f;
|
||||
int targetWidth = requireView().getMeasuredWidth();
|
||||
int targetHeight = (int) ((1 / aspectRatio) * targetWidth);
|
||||
|
||||
resizeAnimation = new ResizeAnimation(imageEditorView, targetWidth, targetHeight);
|
||||
resizeAnimation.setDuration(250);
|
||||
imageEditorView.startAnimation(resizeAnimation);
|
||||
}
|
||||
|
||||
private static boolean shouldScaleViewPort(@NonNull ImageEditorHudV2.Mode mode) {
|
||||
return mode != ImageEditorHudV2.Mode.NONE;
|
||||
}
|
||||
|
||||
private void performSaveToDisk() {
|
||||
SimpleTask.run(this::renderToSingleUseBlob, uri -> {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(requireContext());
|
||||
@@ -510,8 +651,14 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
.createForSingleUseInMemory();
|
||||
}
|
||||
|
||||
private void refreshUniqueColors() {
|
||||
imageEditorHud.setColorPalette(imageEditorView.getModel().getUniqueColorsIgnoringAlpha());
|
||||
private void onDrawingChanged(boolean stillTouching, boolean isUserEdit) {
|
||||
if (isUserEdit) {
|
||||
hasMadeAnEditThisSession = true;
|
||||
}
|
||||
|
||||
if (!stillTouching && shouldExitModeOnChange(imageEditorHud.getMode())) {
|
||||
onPopEditorMode();
|
||||
}
|
||||
}
|
||||
|
||||
private void onUndoRedoAvailabilityChanged(boolean undoAvailable, boolean redoAvailable) {
|
||||
@@ -531,9 +678,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
Matrix faceMatrix = new Matrix();
|
||||
|
||||
for (FaceDetector.Face face : faces) {
|
||||
Renderer faceBlurRenderer = new FaceBlurRenderer();
|
||||
EditorElement element = new EditorElement(faceBlurRenderer, EditorModel.Z_MASK);
|
||||
Matrix localMatrix = element.getLocalMatrix();
|
||||
Renderer faceBlurRenderer = new FaceBlurRenderer();
|
||||
EditorElement element = new EditorElement(faceBlurRenderer, EditorModel.Z_MASK);
|
||||
Matrix localMatrix = element.getLocalMatrix();
|
||||
|
||||
faceMatrix.setRectToRect(Bounds.FULL_BOUNDS, face.getBounds(), Matrix.ScaleToFit.FILL);
|
||||
|
||||
@@ -541,8 +688,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
localMatrix.preConcat(faceMatrix);
|
||||
|
||||
element.getFlags().setEditable(false)
|
||||
.setSelectable(false)
|
||||
.persist();
|
||||
.setSelectable(false)
|
||||
.persist();
|
||||
|
||||
imageEditorView.getModel().addElementWithoutPushUndo(element);
|
||||
}
|
||||
@@ -552,51 +699,126 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
cachedFaceDetection = new Pair<>(getUri(), result);
|
||||
}
|
||||
|
||||
private boolean shouldHandleOnBackPressed(ImageEditorHudV2.Mode mode) {
|
||||
return mode == ImageEditorHudV2.Mode.CROP ||
|
||||
mode == ImageEditorHudV2.Mode.DRAW ||
|
||||
mode == ImageEditorHudV2.Mode.HIGHLIGHT ||
|
||||
mode == ImageEditorHudV2.Mode.BLUR ||
|
||||
mode == ImageEditorHudV2.Mode.TEXT ||
|
||||
mode == ImageEditorHudV2.Mode.MOVE_DELETE ||
|
||||
mode == ImageEditorHudV2.Mode.INSERT_STICKER;
|
||||
}
|
||||
|
||||
private boolean shouldExitModeOnChange(ImageEditorHudV2.Mode mode) {
|
||||
return mode == ImageEditorHudV2.Mode.MOVE_DELETE || mode == ImageEditorHudV2.Mode.INSERT_STICKER;
|
||||
}
|
||||
|
||||
private void onPopEditorMode() {
|
||||
currentSelection = null;
|
||||
|
||||
switch (imageEditorHud.getMode()) {
|
||||
case NONE:
|
||||
return;
|
||||
case CROP:
|
||||
case DRAW:
|
||||
case HIGHLIGHT:
|
||||
case BLUR:
|
||||
onCancel();
|
||||
break;
|
||||
case INSERT_STICKER:
|
||||
case TEXT:
|
||||
controller.onTouchEventsNeeded(true);
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.DRAW);
|
||||
break;
|
||||
case MOVE_DELETE:
|
||||
onDone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private final RequestListener<Bitmap> mainImageRequestListener = new RequestListener<Bitmap>() {
|
||||
@Override public boolean onLoadFailed(@Nullable GlideException e, Object model, Target<Bitmap> target, boolean isFirstResource) {
|
||||
controller.onMainImageFailedToLoad();
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, DataSource dataSource, boolean isFirstResource) {
|
||||
controller.onMainImageLoaded();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
private final ImageEditorView.DrawListener drawListener = new ImageEditorView.DrawListener() {
|
||||
@Override
|
||||
public void onDrawStarted() {
|
||||
imageEditorHud.animate().alpha(0f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDrawEnded() {
|
||||
imageEditorHud.animate().alpha(1f);
|
||||
}
|
||||
};
|
||||
|
||||
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 onEntityDown(@Nullable EditorElement editorElement) {
|
||||
if (editorElement != null) {
|
||||
controller.onTouchEventsNeeded(true);
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
boolean isMoveableElement = editorElement.getZOrder() == EditorModel.Z_STICKERS ||
|
||||
editorElement.getZOrder() == EditorModel.Z_TEXT;
|
||||
|
||||
@Override
|
||||
public void onEntityDoubleTap(@NonNull EditorElement editorElement) {
|
||||
currentSelection = editorElement;
|
||||
boolean notInsertSticker = imageEditorHud.getMode() != ImageEditorHudV2.Mode.INSERT_STICKER;
|
||||
|
||||
if (isMoveableElement && notInsertSticker) {
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.MOVE_DELETE);
|
||||
}
|
||||
} else {
|
||||
onPopEditorMode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntitySingleTap(@Nullable EditorElement editorElement) {
|
||||
currentSelection = editorElement;
|
||||
if (currentSelection != null) {
|
||||
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), true);
|
||||
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
|
||||
} else {
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.MOVE_DELETE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@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(ImageEditorHudV2.Mode.TEXT);
|
||||
imageEditorHud.setActiveColor(color);
|
||||
if (startEditing) {
|
||||
startTextEntityEditing(editorElement, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
onPopEditorMode();
|
||||
}
|
||||
};
|
||||
|
||||
public interface Controller {
|
||||
void onTouchEventsNeeded(boolean needed);
|
||||
@@ -604,6 +826,12 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||
|
||||
void onDoneEditing();
|
||||
|
||||
void onCancelEditing();
|
||||
|
||||
void onMainImageLoaded();
|
||||
|
||||
void onMainImageFailedToLoad();
|
||||
}
|
||||
|
||||
private static class FaceDetectionResult {
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
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 android.widget.Switch;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
|
||||
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 blurButton;
|
||||
private View textButton;
|
||||
private View stickerButton;
|
||||
private View undoButton;
|
||||
private View saveButton;
|
||||
private View deleteButton;
|
||||
private View confirmButton;
|
||||
private View doneButton;
|
||||
private View blurToggleHud;
|
||||
private Switch blurToggle;
|
||||
private View blurToast;
|
||||
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 final Debouncer toastDebouncer = new Debouncer(3000);
|
||||
|
||||
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);
|
||||
blurButton = findViewById(R.id.scribble_blur_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);
|
||||
doneButton = findViewById(R.id.scribble_done_button);
|
||||
blurToggleHud = findViewById(R.id.scribble_blur_toggle_hud);
|
||||
blurToggle = findViewById(R.id.scribble_blur_toggle);
|
||||
blurToast = findViewById(R.id.scribble_blur_toast);
|
||||
|
||||
cropAspectLock.setOnClickListener(v -> {
|
||||
eventListener.onCropAspectLock(!eventListener.isCropAspectLocked());
|
||||
updateCropAspectLockImage(eventListener.isCropAspectLocked());
|
||||
});
|
||||
|
||||
initializeViews();
|
||||
initializeVisibilityMap();
|
||||
setMode(Mode.NONE);
|
||||
}
|
||||
|
||||
private void updateCropAspectLockImage(boolean cropAspectLocked) {
|
||||
cropAspectLock.setImageDrawable(getResources().getDrawable(cropAspectLocked ? R.drawable.ic_crop_lock_32 : R.drawable.ic_crop_unlock_32));
|
||||
}
|
||||
|
||||
private void initializeVisibilityMap() {
|
||||
setVisibleViewsWhenInMode(Mode.NONE, drawButton, blurButton, textButton, stickerButton, cropButton, undoButton, saveButton);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.DRAW, confirmButton, undoButton, colorPicker, colorPalette, highlightButton);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.HIGHLIGHT, confirmButton, undoButton, colorPicker, colorPalette, drawButton);
|
||||
|
||||
setVisibleViewsWhenInMode(Mode.BLUR, confirmButton, undoButton, blurToggleHud);
|
||||
|
||||
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);
|
||||
allViews.add(doneButton);
|
||||
}
|
||||
|
||||
private void setVisibleViewsWhenInMode(Mode mode, View... views) {
|
||||
visibilityModeMap.put(mode, new HashSet<>(Arrays.asList(views)));
|
||||
}
|
||||
|
||||
private void initializeViews() {
|
||||
undoButton.setOnClickListener(v -> eventListener.onUndo());
|
||||
|
||||
deleteButton.setOnClickListener(v -> {
|
||||
eventListener.onDelete();
|
||||
setMode(Mode.NONE);
|
||||
});
|
||||
|
||||
cropButton.setOnClickListener(v -> setMode(Mode.CROP));
|
||||
cropFlipButton.setOnClickListener(v -> eventListener.onFlipHorizontal());
|
||||
cropRotateButton.setOnClickListener(v -> eventListener.onRotate90AntiClockwise());
|
||||
|
||||
confirmButton.setOnClickListener(v -> setMode(Mode.NONE));
|
||||
|
||||
colorPaletteAdapter = new ColorPaletteAdapter();
|
||||
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
|
||||
|
||||
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
colorPalette.setAdapter(colorPaletteAdapter);
|
||||
|
||||
drawButton.setOnClickListener(v -> setMode(Mode.DRAW));
|
||||
blurButton.setOnClickListener(v -> setMode(Mode.BLUR));
|
||||
highlightButton.setOnClickListener(v -> setMode(Mode.HIGHLIGHT));
|
||||
textButton.setOnClickListener(v -> setMode(Mode.TEXT));
|
||||
stickerButton.setOnClickListener(v -> setMode(Mode.INSERT_STICKER));
|
||||
saveButton.setOnClickListener(v -> eventListener.onSave());
|
||||
doneButton.setOnClickListener(v -> eventListener.onDone());
|
||||
blurToggle.setOnCheckedChangeListener((button, enabled) -> eventListener.onBlurFacesToggled(enabled));
|
||||
}
|
||||
|
||||
public void setUpForAvatarEditing() {
|
||||
visibilityModeMap.get(Mode.NONE).add(doneButton);
|
||||
visibilityModeMap.get(Mode.NONE).remove(saveButton);
|
||||
visibilityModeMap.get(Mode.CROP).remove(cropAspectLock);
|
||||
|
||||
if (currentMode == Mode.NONE) {
|
||||
doneButton.setVisibility(View.VISIBLE);
|
||||
saveButton.setVisibility(View.GONE);
|
||||
} else if (currentMode == Mode.CROP) {
|
||||
cropAspectLock.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
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 setBlurFacesToggleEnabled(boolean enabled) {
|
||||
blurToggle.setOnCheckedChangeListener(null);
|
||||
blurToggle.setChecked(enabled);
|
||||
blurToggle.setOnCheckedChangeListener((button, value) -> eventListener.onBlurFacesToggled(value));
|
||||
}
|
||||
|
||||
public void showBlurHudTooltip() {
|
||||
TooltipPopup.forTarget(blurButton)
|
||||
.setText(R.string.ImageEditorHud_new_blur_faces_or_draw_anywhere_to_blur)
|
||||
.setBackgroundTint(ContextCompat.getColor(getContext(), R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(getContext(), R.color.core_white))
|
||||
.show(TooltipPopup.POSITION_BELOW);
|
||||
}
|
||||
|
||||
public void showBlurToast() {
|
||||
blurToast.clearAnimation();
|
||||
blurToast.setVisibility(View.VISIBLE);
|
||||
toastDebouncer.publish(() -> blurToast.setVisibility(GONE));
|
||||
}
|
||||
|
||||
public void hideBlurToast() {
|
||||
blurToast.clearAnimation();
|
||||
blurToast.setVisibility(View.GONE);
|
||||
toastDebouncer.clear();
|
||||
}
|
||||
|
||||
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 NONE: presentModeNone(); break;
|
||||
case CROP: presentModeCrop(); break;
|
||||
case DRAW: presentModeDraw(); break;
|
||||
case BLUR: presentModeBlur(); 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 presentModeNone() {
|
||||
blurToast.setVisibility(GONE);
|
||||
}
|
||||
|
||||
private void presentModeCrop() {
|
||||
updateCropAspectLockImage(eventListener.isCropAspectLocked());
|
||||
}
|
||||
|
||||
private void presentModeDraw() {
|
||||
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
|
||||
colorPicker.setActiveColor(Color.RED);
|
||||
}
|
||||
|
||||
private void presentModeBlur() {
|
||||
colorPicker.setOnColorChangeListener(standardOnColorChangeListener);
|
||||
colorPicker.setActiveColor(Color.BLACK);
|
||||
}
|
||||
|
||||
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(withHighlighterAlpha(selectedColor));
|
||||
|
||||
private static int withHighlighterAlpha(int color) {
|
||||
return color & ~0xff000000 | 0x60000000;
|
||||
}
|
||||
|
||||
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,
|
||||
BLUR,
|
||||
MOVE_DELETE,
|
||||
INSERT_STICKER,
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onModeStarted(@NonNull Mode mode);
|
||||
void onColorChange(int color);
|
||||
void onBlurFacesToggled(boolean enabled);
|
||||
void onUndo();
|
||||
void onDelete();
|
||||
void onSave();
|
||||
void onFlipHorizontal();
|
||||
void onRotate90AntiClockwise();
|
||||
void onCropAspectLock(boolean locked);
|
||||
boolean isCropAspectLocked();
|
||||
void onRequestFullScreen(boolean fullScreen, boolean hideKeyboard);
|
||||
void onDone();
|
||||
}
|
||||
|
||||
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 onBlurFacesToggled(boolean enabled) {
|
||||
}
|
||||
|
||||
@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) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDone() {
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,486 @@
|
||||
package org.thoughtcrime.securesms.scribbles
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import android.widget.SeekBar
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.isVisible
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import com.google.android.material.switchmaterial.SwitchMaterial
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.TooltipPopup
|
||||
import org.thoughtcrime.securesms.scribbles.HSVColorSlider.getColor
|
||||
import org.thoughtcrime.securesms.scribbles.HSVColorSlider.setColor
|
||||
import org.thoughtcrime.securesms.scribbles.HSVColorSlider.setUpForColor
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.ThrottledDebouncer
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import org.thoughtcrime.securesms.util.setListeners
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
private var listener: EventListener? = null
|
||||
private var currentMode: Mode = Mode.NONE
|
||||
private var undoAvailability: Boolean = false
|
||||
private var isAvatarEdit: Boolean = false
|
||||
|
||||
init {
|
||||
inflate(context, R.layout.v2_media_image_editor_hud, this)
|
||||
}
|
||||
|
||||
private val undoButton: View = findViewById(R.id.image_editor_hud_undo)
|
||||
private val clearAllButton: View = findViewById(R.id.image_editor_hud_clear_all)
|
||||
private val cancelButton: View = findViewById(R.id.image_editor_hud_cancel_button)
|
||||
private val drawButton: View = findViewById(R.id.image_editor_hud_draw_button)
|
||||
private val textButton: View = findViewById(R.id.image_editor_hud_text_button)
|
||||
private val stickerButton: View = findViewById(R.id.image_editor_hud_sticker_button)
|
||||
private val blurButton: View = findViewById(R.id.image_editor_hud_blur_button)
|
||||
private val doneButton: View = findViewById(R.id.image_editor_hud_done_button)
|
||||
private val drawSeekBar: AppCompatSeekBar = findViewById(R.id.image_editor_hud_draw_color_bar)
|
||||
private val brushToggle: ImageView = findViewById(R.id.image_editor_hud_draw_brush)
|
||||
private val widthSeekBar: AppCompatSeekBar = findViewById(R.id.image_editor_hud_draw_width_bar)
|
||||
private val cropRotateButton: View = findViewById(R.id.image_editor_hud_rotate_button)
|
||||
private val cropFlipButton: View = findViewById(R.id.image_editor_hud_flip_button)
|
||||
private val cropAspectLockButton: ImageView = findViewById(R.id.image_editor_hud_aspect_lock_button)
|
||||
private val blurToggleContainer: View = findViewById(R.id.image_editor_hud_blur_toggle_container)
|
||||
private val blurToggle: SwitchMaterial = findViewById(R.id.image_editor_hud_blur_toggle)
|
||||
private val blurToast: View = findViewById(R.id.image_editor_hud_blur_toast)
|
||||
private val blurHelpText: View = findViewById(R.id.image_editor_hud_blur_help_text)
|
||||
private val colorIndicator: ImageView = findViewById(R.id.image_editor_hud_color_indicator)
|
||||
|
||||
private val selectableSet: Set<View> = setOf(drawButton, textButton, stickerButton, blurButton)
|
||||
|
||||
private val undoTools: Set<View> = setOf(undoButton, clearAllButton)
|
||||
private val drawTools: Set<View> = setOf(brushToggle, drawSeekBar, widthSeekBar)
|
||||
private val blurTools: Set<View> = setOf(blurToggleContainer, blurHelpText, widthSeekBar)
|
||||
private val drawButtonRow: Set<View> = setOf(cancelButton, doneButton, drawButton, textButton, stickerButton, blurButton)
|
||||
private val cropButtonRow: Set<View> = setOf(cancelButton, doneButton, cropRotateButton, cropFlipButton, cropAspectLockButton)
|
||||
|
||||
private val viewsToSlide: Set<View> = drawButtonRow + cropButtonRow
|
||||
|
||||
private val modeChangeAnimationThrottler = ThrottledDebouncer(ANIMATION_DURATION)
|
||||
private val undoToolsAnimationThrottler = ThrottledDebouncer(ANIMATION_DURATION)
|
||||
|
||||
private val toastDebouncer = Debouncer(3000)
|
||||
private var colorIndicatorAlphaAnimator: Animator? = null
|
||||
|
||||
init {
|
||||
initializeViews()
|
||||
setMode(currentMode)
|
||||
}
|
||||
|
||||
private fun initializeViews() {
|
||||
undoButton.setOnClickListener { listener?.onUndo() }
|
||||
clearAllButton.setOnClickListener { listener?.onClearAll() }
|
||||
cancelButton.setOnClickListener { listener?.onCancel() }
|
||||
|
||||
drawButton.setOnClickListener { setMode(Mode.DRAW) }
|
||||
blurButton.setOnClickListener { setMode(Mode.BLUR) }
|
||||
textButton.setOnClickListener { setMode(Mode.TEXT) }
|
||||
stickerButton.setOnClickListener { setMode(Mode.INSERT_STICKER) }
|
||||
brushToggle.setOnClickListener {
|
||||
if (currentMode == Mode.DRAW) {
|
||||
setMode(Mode.HIGHLIGHT)
|
||||
} else {
|
||||
setMode(Mode.DRAW)
|
||||
}
|
||||
}
|
||||
|
||||
doneButton.setOnClickListener {
|
||||
if (isAvatarEdit && currentMode == Mode.CROP) {
|
||||
setMode(Mode.NONE)
|
||||
} else {
|
||||
listener?.onDone()
|
||||
}
|
||||
}
|
||||
|
||||
drawSeekBar.setUpForColor(
|
||||
thumbBorderColor = Color.WHITE,
|
||||
onColorChanged = {
|
||||
updateColorIndicator()
|
||||
listener?.onColorChange(getActiveColor())
|
||||
},
|
||||
onDragStart = {
|
||||
colorIndicatorAlphaAnimator?.end()
|
||||
colorIndicatorAlphaAnimator = ObjectAnimator.ofFloat(colorIndicator, "alpha", colorIndicator.alpha, 1f)
|
||||
colorIndicatorAlphaAnimator?.duration = 150L
|
||||
colorIndicatorAlphaAnimator?.start()
|
||||
},
|
||||
onDragEnd = {
|
||||
colorIndicatorAlphaAnimator?.end()
|
||||
colorIndicatorAlphaAnimator = ObjectAnimator.ofFloat(colorIndicator, "alpha", colorIndicator.alpha, 0f)
|
||||
colorIndicatorAlphaAnimator?.duration = 150L
|
||||
colorIndicatorAlphaAnimator?.start()
|
||||
}
|
||||
)
|
||||
|
||||
cropFlipButton.setOnClickListener { listener?.onFlipHorizontal() }
|
||||
cropRotateButton.setOnClickListener { listener?.onRotate90AntiClockwise() }
|
||||
|
||||
cropAspectLockButton.setOnClickListener {
|
||||
listener?.onCropAspectLock()
|
||||
if (listener?.isCropAspectLocked == true) {
|
||||
cropAspectLockButton.setImageResource(R.drawable.ic_crop_lock_24)
|
||||
} else {
|
||||
cropAspectLockButton.setImageResource(R.drawable.ic_crop_unlock_24)
|
||||
}
|
||||
}
|
||||
|
||||
blurToggle.setOnCheckedChangeListener { _, enabled -> listener?.onBlurFacesToggled(enabled) }
|
||||
|
||||
setupWidthSeekBar()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private fun setupWidthSeekBar() {
|
||||
widthSeekBar.thumb = HSVColorSlider.createThumbDrawable(Color.WHITE)
|
||||
widthSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
listener?.onBrushWidthChange(progress)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) = Unit
|
||||
})
|
||||
|
||||
widthSeekBar.setOnTouchListener { v, event ->
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
v?.animate()
|
||||
?.setDuration(ANIMATION_DURATION)
|
||||
?.setInterpolator(DecelerateInterpolator())
|
||||
?.translationX(ViewUtil.dpToPx(36).toFloat())
|
||||
} else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) {
|
||||
v?.animate()
|
||||
?.setDuration(ANIMATION_DURATION)
|
||||
?.setInterpolator(DecelerateInterpolator())
|
||||
?.translationX(0f)
|
||||
}
|
||||
|
||||
v.onTouchEvent(event)
|
||||
}
|
||||
|
||||
widthSeekBar.progress = 20
|
||||
}
|
||||
|
||||
fun setUpForAvatarEditing() {
|
||||
isAvatarEdit = true
|
||||
}
|
||||
|
||||
fun setColorPalette(colors: Set<Int>) {
|
||||
}
|
||||
|
||||
fun getActiveColor(): Int {
|
||||
return if (currentMode == Mode.HIGHLIGHT) {
|
||||
withHighlighterAlpha(drawSeekBar.getColor())
|
||||
} else {
|
||||
drawSeekBar.getColor()
|
||||
}
|
||||
}
|
||||
|
||||
fun getColorIndex(): Int {
|
||||
return drawSeekBar.progress
|
||||
}
|
||||
|
||||
fun setColorIndex(index: Int) {
|
||||
drawSeekBar.progress = index
|
||||
}
|
||||
|
||||
fun setActiveColor(color: Int) {
|
||||
drawSeekBar.setColor(color or 0xFF000000.toInt())
|
||||
updateColorIndicator()
|
||||
}
|
||||
|
||||
fun getActiveBrushWidth(): Int {
|
||||
return widthSeekBar.progress
|
||||
}
|
||||
|
||||
fun setBlurFacesToggleEnabled(enabled: Boolean) {
|
||||
blurToggle.setOnCheckedChangeListener(null)
|
||||
blurToggle.isChecked = enabled
|
||||
blurToggle.setOnCheckedChangeListener { _, value -> listener?.onBlurFacesToggled(value) }
|
||||
}
|
||||
|
||||
fun showBlurHudTooltip() {
|
||||
TooltipPopup.forTarget(blurButton)
|
||||
.setText(R.string.ImageEditorHud_new_blur_faces_or_draw_anywhere_to_blur)
|
||||
.setBackgroundTint(ContextCompat.getColor(context, R.color.core_ultramarine))
|
||||
.setTextColor(ContextCompat.getColor(context, R.color.core_white))
|
||||
.show(TooltipPopup.POSITION_BELOW)
|
||||
}
|
||||
|
||||
fun showBlurToast() {
|
||||
blurToast.clearAnimation()
|
||||
blurToast.visible = true
|
||||
toastDebouncer.publish { blurToast.visible = false }
|
||||
}
|
||||
|
||||
fun hideBlurToast() {
|
||||
blurToast.clearAnimation()
|
||||
blurToast.visible = false
|
||||
toastDebouncer.clear()
|
||||
}
|
||||
|
||||
fun setEventListener(eventListener: EventListener?) {
|
||||
listener = eventListener
|
||||
}
|
||||
|
||||
fun enterMode(mode: Mode) {
|
||||
setMode(mode, false)
|
||||
}
|
||||
|
||||
fun setMode(mode: Mode) {
|
||||
setMode(mode, true)
|
||||
}
|
||||
|
||||
fun getMode(): Mode = currentMode
|
||||
|
||||
fun setUndoAvailability(undoAvailability: Boolean) {
|
||||
this.undoAvailability = undoAvailability
|
||||
|
||||
if (currentMode != Mode.NONE) {
|
||||
if (undoAvailability) {
|
||||
animateInUndoTools()
|
||||
} else {
|
||||
animateOutUndoTools()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setMode(mode: Mode, notify: Boolean) {
|
||||
val previousMode: Mode = currentMode
|
||||
currentMode = mode
|
||||
// updateVisibilities
|
||||
clearSelection()
|
||||
|
||||
when (mode) {
|
||||
Mode.NONE -> presentModeNone()
|
||||
Mode.CROP -> presentModeCrop()
|
||||
Mode.TEXT -> presentModeText()
|
||||
Mode.DRAW -> presentModeDraw()
|
||||
Mode.BLUR -> presentModeBlur()
|
||||
Mode.HIGHLIGHT -> presentModeHighlight()
|
||||
Mode.INSERT_STICKER -> presentModeMoveDelete()
|
||||
Mode.MOVE_DELETE -> presentModeMoveDelete()
|
||||
}
|
||||
|
||||
if (notify) {
|
||||
listener?.onModeStarted(mode, previousMode)
|
||||
}
|
||||
|
||||
listener?.onRequestFullScreen(mode != Mode.NONE, mode != Mode.TEXT)
|
||||
}
|
||||
|
||||
private fun presentModeNone() {
|
||||
if (isAvatarEdit) {
|
||||
animateViewSetChange(
|
||||
inSet = drawButtonRow,
|
||||
outSet = cropButtonRow + blurTools + drawTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
} else {
|
||||
animateViewSetChange(
|
||||
inSet = setOf(),
|
||||
outSet = drawButtonRow + cropButtonRow + blurTools + drawTools
|
||||
)
|
||||
animateOutUndoTools()
|
||||
}
|
||||
}
|
||||
|
||||
private fun presentModeCrop() {
|
||||
animateViewSetChange(
|
||||
inSet = cropButtonRow - if (isAvatarEdit) setOf(cropAspectLockButton) else setOf(),
|
||||
outSet = drawButtonRow + blurTools + drawTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
}
|
||||
|
||||
private fun presentModeDraw() {
|
||||
drawButton.isSelected = true
|
||||
brushToggle.setImageResource(R.drawable.ic_draw_white_24)
|
||||
listener?.onColorChange(getActiveColor())
|
||||
updateColorIndicator()
|
||||
animateViewSetChange(
|
||||
inSet = drawButtonRow + drawTools,
|
||||
outSet = cropButtonRow + blurTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
}
|
||||
|
||||
private fun presentModeHighlight() {
|
||||
drawButton.isSelected = true
|
||||
brushToggle.setImageResource(R.drawable.ic_marker_24)
|
||||
listener?.onColorChange(getActiveColor())
|
||||
updateColorIndicator()
|
||||
animateViewSetChange(
|
||||
inSet = drawButtonRow + drawTools,
|
||||
outSet = cropButtonRow + blurTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
}
|
||||
|
||||
private fun presentModeBlur() {
|
||||
blurButton.isSelected = true
|
||||
animateViewSetChange(
|
||||
inSet = drawButtonRow + blurTools,
|
||||
outSet = drawTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
}
|
||||
|
||||
private fun presentModeText() {
|
||||
textButton.isSelected = true
|
||||
animateViewSetChange(
|
||||
inSet = setOf(drawSeekBar),
|
||||
outSet = drawTools + blurTools + drawButtonRow + cropButtonRow
|
||||
)
|
||||
animateOutUndoTools()
|
||||
}
|
||||
|
||||
private fun presentModeMoveDelete() {
|
||||
animateViewSetChange(
|
||||
outSet = drawTools + blurTools + drawButtonRow + cropButtonRow
|
||||
)
|
||||
}
|
||||
|
||||
private fun clearSelection() {
|
||||
selectableSet.forEach { it.isSelected = false }
|
||||
}
|
||||
|
||||
private fun updateColorIndicator() {
|
||||
colorIndicator.drawable.colorFilter = SimpleColorFilter(drawSeekBar.getColor())
|
||||
colorIndicator.translationX = (drawSeekBar.thumb.bounds.left.toFloat() + ViewUtil.dpToPx(16))
|
||||
}
|
||||
|
||||
private fun animateViewSetChange(
|
||||
inSet: Set<View> = setOf(),
|
||||
outSet: Set<View> = setOf(),
|
||||
throttledDebouncer: ThrottledDebouncer = modeChangeAnimationThrottler
|
||||
) {
|
||||
val actualOutSet = outSet - inSet
|
||||
|
||||
throttledDebouncer.publish {
|
||||
animateInViewSet(inSet)
|
||||
animateOutViewSet(actualOutSet)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateInViewSet(viewSet: Set<View>) {
|
||||
viewSet.forEach { view ->
|
||||
if (!view.isVisible) {
|
||||
view.animation = getInAnimation(view)
|
||||
view.animation.duration = ANIMATION_DURATION
|
||||
view.visibility = VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateOutViewSet(viewSet: Set<View>) {
|
||||
viewSet.forEach { view ->
|
||||
if (view.isVisible) {
|
||||
val animation: Animation = getOutAnimation(view)
|
||||
animation.duration = ANIMATION_DURATION
|
||||
animation.setListeners(
|
||||
onAnimationEnd = {
|
||||
view.visibility = GONE
|
||||
}
|
||||
)
|
||||
|
||||
view.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getInAnimation(view: View): Animation {
|
||||
return if (viewsToSlide.contains(view)) {
|
||||
AnimationUtils.loadAnimation(context, R.anim.slide_from_bottom)
|
||||
} else {
|
||||
AnimationUtils.loadAnimation(context, R.anim.fade_in)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOutAnimation(view: View): Animation {
|
||||
return if (viewsToSlide.contains(view)) {
|
||||
AnimationUtils.loadAnimation(context, R.anim.slide_to_bottom)
|
||||
} else {
|
||||
AnimationUtils.loadAnimation(context, R.anim.fade_out)
|
||||
}
|
||||
}
|
||||
|
||||
private fun animateInUndoTools() {
|
||||
animateViewSetChange(
|
||||
inSet = undoToolsIfAvailable(),
|
||||
throttledDebouncer = undoToolsAnimationThrottler
|
||||
)
|
||||
}
|
||||
|
||||
private fun animateOutUndoTools() {
|
||||
animateViewSetChange(
|
||||
outSet = undoTools,
|
||||
throttledDebouncer = undoToolsAnimationThrottler
|
||||
)
|
||||
}
|
||||
|
||||
private fun undoToolsIfAvailable(): Set<View> {
|
||||
return if (undoAvailability) {
|
||||
undoTools
|
||||
} else {
|
||||
setOf()
|
||||
}
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
NONE,
|
||||
CROP,
|
||||
TEXT,
|
||||
DRAW,
|
||||
HIGHLIGHT,
|
||||
BLUR,
|
||||
MOVE_DELETE,
|
||||
INSERT_STICKER
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val ANIMATION_DURATION = 250L
|
||||
|
||||
private fun withHighlighterAlpha(color: Int): Int {
|
||||
return color and 0xFF000000.toInt().inv() or 0x60000000
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
fun onModeStarted(mode: Mode, previousMode: Mode)
|
||||
fun onColorChange(color: Int)
|
||||
fun onBrushWidthChange(@IntRange(from = 0, to = 100) widthPercentage: Int)
|
||||
fun onBlurFacesToggled(enabled: Boolean)
|
||||
fun onUndo()
|
||||
fun onClearAll()
|
||||
fun onDelete()
|
||||
fun onSave()
|
||||
fun onFlipHorizontal()
|
||||
fun onRotate90AntiClockwise()
|
||||
fun onCropAspectLock()
|
||||
val isCropAspectLocked: Boolean
|
||||
|
||||
fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean)
|
||||
fun onDone()
|
||||
fun onCancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package org.thoughtcrime.securesms.scribbles
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.widget.AppCompatSeekBar
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import com.airbnb.lottie.SimpleColorFilter
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.KeyboardEntryDialogFragment
|
||||
import org.thoughtcrime.securesms.imageeditor.HiddenEditText
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorElement
|
||||
import org.thoughtcrime.securesms.imageeditor.renderers.MultiLineTextRenderer
|
||||
import org.thoughtcrime.securesms.keyboard.findListener
|
||||
import org.thoughtcrime.securesms.scribbles.HSVColorSlider.getColor
|
||||
import org.thoughtcrime.securesms.scribbles.HSVColorSlider.setUpForColor
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
|
||||
class TextEntryDialogFragment : KeyboardEntryDialogFragment(R.layout.v2_media_image_editor_text_entry_fragment) {
|
||||
|
||||
private lateinit var hiddenTextEntry: HiddenEditText
|
||||
private lateinit var controller: Controller
|
||||
|
||||
private var colorIndicatorAlphaAnimator: Animator? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
controller = requireNotNull(findListener())
|
||||
|
||||
hiddenTextEntry = HiddenEditText(requireContext())
|
||||
(view as ViewGroup).addView(hiddenTextEntry)
|
||||
|
||||
view.setOnClickListener {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
val element: EditorElement = requireNotNull(requireArguments().getParcelable("element"))
|
||||
val incognito = requireArguments().getBoolean("incognito")
|
||||
val selectAll = requireArguments().getBoolean("selectAll")
|
||||
|
||||
hiddenTextEntry.setCurrentTextEditorElement(element)
|
||||
hiddenTextEntry.setIncognitoKeyboardEnabled(incognito)
|
||||
|
||||
if (selectAll) {
|
||||
hiddenTextEntry.selectAll()
|
||||
}
|
||||
|
||||
hiddenTextEntry.setOnEditOrSelectionChange { editorElement, textRenderer ->
|
||||
controller.zoomToFitText(editorElement, textRenderer)
|
||||
}
|
||||
|
||||
hiddenTextEntry.setOnEndEdit {
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
ViewUtil.focusAndShowKeyboard(hiddenTextEntry)
|
||||
|
||||
val slider: AppCompatSeekBar = view.findViewById(R.id.image_editor_hud_draw_color_bar)
|
||||
val colorIndicator: ImageView = view.findViewById(R.id.image_editor_hud_color_indicator)
|
||||
slider.setUpForColor(
|
||||
Color.WHITE,
|
||||
{
|
||||
colorIndicator.drawable.colorFilter = SimpleColorFilter(slider.getColor())
|
||||
colorIndicator.translationX = (slider.thumb.bounds.left.toFloat() + ViewUtil.dpToPx(16))
|
||||
controller.onTextColorChange(slider.progress)
|
||||
},
|
||||
{
|
||||
colorIndicatorAlphaAnimator?.end()
|
||||
colorIndicatorAlphaAnimator = ObjectAnimator.ofFloat(colorIndicator, "alpha", colorIndicator.alpha, 1f)
|
||||
colorIndicatorAlphaAnimator?.duration = 150L
|
||||
colorIndicatorAlphaAnimator?.start()
|
||||
},
|
||||
{
|
||||
colorIndicatorAlphaAnimator?.end()
|
||||
colorIndicatorAlphaAnimator = ObjectAnimator.ofFloat(colorIndicator, "alpha", colorIndicator.alpha, 0f)
|
||||
colorIndicatorAlphaAnimator?.duration = 150L
|
||||
colorIndicatorAlphaAnimator?.start()
|
||||
}
|
||||
)
|
||||
|
||||
slider.progress = requireArguments().getInt("color_index")
|
||||
}
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
controller.onTextEntryDialogDismissed(!hiddenTextEntry.text.isNullOrEmpty())
|
||||
}
|
||||
|
||||
interface Controller {
|
||||
fun onTextEntryDialogDismissed(hasText: Boolean)
|
||||
fun zoomToFitText(editorElement: EditorElement, textRenderer: MultiLineTextRenderer)
|
||||
fun onTextColorChange(colorIndex: Int)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun show(
|
||||
fragmentManager: FragmentManager,
|
||||
editorElement: EditorElement,
|
||||
isIncognitoEnabled: Boolean,
|
||||
selectAll: Boolean,
|
||||
colorIndex: Int
|
||||
) {
|
||||
val args = Bundle().apply {
|
||||
putParcelable("element", editorElement)
|
||||
putBoolean("incognito", isIncognitoEnabled)
|
||||
putBoolean("selectAll", selectAll)
|
||||
putInt("color_index", colorIndex)
|
||||
}
|
||||
|
||||
TextEntryDialogFragment().apply {
|
||||
arguments = args
|
||||
show(fragmentManager, "text_entry")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.RequestListener;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
|
||||
@@ -51,15 +52,16 @@ public final class UriGlideRenderer implements Renderer {
|
||||
public static final float WEAK_BLUR = 3f;
|
||||
public static final float STRONG_BLUR = 25f;
|
||||
|
||||
private final Uri imageUri;
|
||||
private final Paint paint = new Paint();
|
||||
private final Matrix imageProjectionMatrix = new Matrix();
|
||||
private final Matrix temp = new Matrix();
|
||||
private final Matrix blurScaleMatrix = new Matrix();
|
||||
private final boolean decryptable;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
private final float blurRadius;
|
||||
private final Uri imageUri;
|
||||
private final Paint paint = new Paint();
|
||||
private final Matrix imageProjectionMatrix = new Matrix();
|
||||
private final Matrix temp = new Matrix();
|
||||
private final Matrix blurScaleMatrix = new Matrix();
|
||||
private final boolean decryptable;
|
||||
private final int maxWidth;
|
||||
private final int maxHeight;
|
||||
private final float blurRadius;
|
||||
private final RequestListener<Bitmap> bitmapRequestListener;
|
||||
|
||||
@Nullable private Bitmap bitmap;
|
||||
@Nullable private Bitmap blurredBitmap;
|
||||
@@ -70,11 +72,16 @@ public final class UriGlideRenderer implements Renderer {
|
||||
}
|
||||
|
||||
public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius) {
|
||||
this.imageUri = imageUri;
|
||||
this.decryptable = decryptable;
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
this.blurRadius = blurRadius;
|
||||
this(imageUri, decryptable, maxWidth, maxHeight, blurRadius, null);
|
||||
}
|
||||
|
||||
public UriGlideRenderer(@NonNull Uri imageUri, boolean decryptable, int maxWidth, int maxHeight, float blurRadius, @Nullable RequestListener<Bitmap> bitmapRequestListener) {
|
||||
this.imageUri = imageUri;
|
||||
this.decryptable = decryptable;
|
||||
this.maxWidth = maxWidth;
|
||||
this.maxHeight = maxHeight;
|
||||
this.blurRadius = blurRadius;
|
||||
this.bitmapRequestListener = bitmapRequestListener;
|
||||
paint.setAntiAlias(true);
|
||||
paint.setFilterBitmap(true);
|
||||
paint.setDither(true);
|
||||
@@ -186,6 +193,7 @@ public final class UriGlideRenderer implements Renderer {
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.override(width, height)
|
||||
.centerInside()
|
||||
.addListener(bitmapRequestListener)
|
||||
.load(decryptable ? new DecryptableStreamUriLoader.DecryptableUri(imageUri) : imageUri);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
package org.thoughtcrime.securesms.scribbles.widget;
|
||||
|
||||
import android.graphics.PorterDuff;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,239 +0,0 @@
|
||||
/**
|
||||
* 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