Refresh media selection and sending flow with a shiny new UX.

This commit is contained in:
Alex Hart
2021-09-02 17:04:43 -03:00
committed by Greyson Parrelli
parent a940487611
commit 664d6475d9
195 changed files with 7075 additions and 4812 deletions

View File

@@ -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
}
}
}

View File

@@ -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 {

View File

@@ -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() {
}
};
}

View File

@@ -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()
}
}

View File

@@ -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")
}
}
}
}

View File

@@ -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);
}

View File

@@ -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));
}
}
}
}

View File

@@ -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);
}
}