mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 16:49:40 +01:00
@@ -95,6 +95,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
private int imageMaxWidth;
|
||||
|
||||
private final ThrottledDebouncer deleteFadeDebouncer = new ThrottledDebouncer(500);
|
||||
private float initialDialImageDegrees;
|
||||
private float initialDialScale;
|
||||
private float minDialScaleDown;
|
||||
|
||||
public static class Data {
|
||||
private final Bundle bundle;
|
||||
@@ -133,7 +136,6 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
private boolean hasMadeAnEditThisSession;
|
||||
private boolean wasInTrashHitZone;
|
||||
|
||||
|
||||
public static ImageEditorFragment newInstanceForAvatarCapture(@NonNull Uri imageUri) {
|
||||
ImageEditorFragment fragment = newInstance(imageUri);
|
||||
fragment.requireArguments().putString(KEY_MODE, Mode.AVATAR_CAPTURE.code);
|
||||
@@ -422,6 +424,8 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
.setVisible(mode == ImageEditorHudV2.Mode.DELETE)
|
||||
.persist();
|
||||
|
||||
updateHudDialRotation();
|
||||
|
||||
switch (mode) {
|
||||
case CROP: {
|
||||
imageEditorView.getModel().startCrop();
|
||||
@@ -561,6 +565,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
@Override
|
||||
public void onClearAll() {
|
||||
imageEditorView.getModel().clearUndoStack();
|
||||
updateHudDialRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -586,6 +591,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
public void onUndo() {
|
||||
imageEditorView.getModel().undo();
|
||||
imageEditorHud.setBlurFacesToggleEnabled(imageEditorView.getModel().hasFaceRenderer());
|
||||
updateHudDialRotation();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -641,6 +647,32 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
controller.onDoneEditing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialRotationGestureStarted() {
|
||||
float localScaleX = imageEditorView.getModel().getMainImage().getLocalScaleX();
|
||||
minDialScaleDown = initialDialScale / localScaleX;
|
||||
imageEditorView.getModel().pushUndoPoint();
|
||||
imageEditorView.getModel().updateUndoRedoAvailabilityState();
|
||||
initialDialImageDegrees = (float) Math.toDegrees(imageEditorView.getModel().getMainImage().getLocalRotationAngle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialRotationGestureFinished() {
|
||||
imageEditorView.getModel().getMainImage().commitEditorMatrix();
|
||||
imageEditorView.getModel().postEdit(true);
|
||||
imageEditorView.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDialRotationChanged(float degrees) {
|
||||
imageEditorView.setMainImageEditorMatrixRotation(degrees - initialDialImageDegrees, minDialScaleDown);
|
||||
}
|
||||
|
||||
private void updateHudDialRotation() {
|
||||
imageEditorHud.setDialRotation(getRotationDegreesRounded(imageEditorView.getModel().getMainImage()));
|
||||
initialDialScale = imageEditorView.getModel().getMainImage().getLocalScaleX();
|
||||
}
|
||||
|
||||
private ResizeAnimation resizeAnimation;
|
||||
|
||||
private void scaleViewPortForDrawing(int orientation) {
|
||||
@@ -738,6 +770,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
private void onDrawingChanged(boolean stillTouching, boolean isUserEdit) {
|
||||
if (isUserEdit) {
|
||||
hasMadeAnEditThisSession = true;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -832,10 +865,18 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
}
|
||||
};
|
||||
|
||||
public float getRotationDegreesRounded(@Nullable EditorElement editorElement) {
|
||||
if (editorElement == null) {
|
||||
return 0f;
|
||||
}
|
||||
return Math.round(Math.toDegrees(editorElement.getLocalRotationAngle()));
|
||||
}
|
||||
|
||||
private final ImageEditorView.DragListener dragListener = new ImageEditorView.DragListener() {
|
||||
@Override
|
||||
public void onDragStarted(@Nullable EditorElement editorElement) {
|
||||
if (imageEditorHud.getMode() == ImageEditorHudV2.Mode.CROP) {
|
||||
updateHudDialRotation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -855,6 +896,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
@Override
|
||||
public void onDragMoved(@Nullable EditorElement editorElement, boolean isInTrashHitZone) {
|
||||
if (imageEditorHud.getMode() == ImageEditorHudV2.Mode.CROP || editorElement == null) {
|
||||
updateHudDialRotation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -882,6 +924,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
wasInTrashHitZone = false;
|
||||
imageEditorHud.animate().alpha(1f);
|
||||
if (imageEditorHud.getMode() == ImageEditorHudV2.Mode.CROP) {
|
||||
updateHudDialRotation();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -961,6 +1004,7 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
if (editorElement != null && editorElement.getRenderer() instanceof SelectableRenderer) {
|
||||
((SelectableRenderer) editorElement.getRenderer()).onSelected(selected);
|
||||
}
|
||||
imageEditorView.getModel().setSelected(selected ? editorElement : null);
|
||||
}
|
||||
|
||||
private final OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(false) {
|
||||
|
||||
@@ -65,16 +65,18 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
private val bottomGuideline: Guideline = findViewById(R.id.image_editor_bottom_guide)
|
||||
private val brushPreview: BrushWidthPreviewView = findViewById(R.id.image_editor_hud_brush_preview)
|
||||
private val textStyleToggle: ImageView = findViewById(R.id.image_editor_hud_text_style_button)
|
||||
private val rotationDial: RotationDialView = findViewById(R.id.image_editor_hud_crop_rotation_dial)
|
||||
|
||||
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 cropTools: Set<View> = setOf(rotationDial)
|
||||
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 allModeTools: Set<View> = drawTools + blurTools + drawButtonRow + cropButtonRow + textStyleToggle
|
||||
private val allModeTools: Set<View> = drawTools + blurTools + drawButtonRow + cropButtonRow + textStyleToggle + cropTools
|
||||
|
||||
private val viewsToSlide: Set<View> = drawButtonRow + cropButtonRow
|
||||
|
||||
@@ -150,6 +152,24 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
blurToggle.setOnCheckedChangeListener { _, enabled -> listener?.onBlurFacesToggled(enabled) }
|
||||
|
||||
setupWidthSeekBar()
|
||||
|
||||
rotationDial.listener = object : RotationDialView.Listener {
|
||||
override fun onDegreeChanged(degrees: Float) {
|
||||
listener?.onDialRotationChanged(degrees)
|
||||
}
|
||||
|
||||
override fun onGestureStart() {
|
||||
listener?.onDialRotationGestureStarted()
|
||||
}
|
||||
|
||||
override fun onGestureEnd() {
|
||||
listener?.onDialRotationGestureFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setDialRotation(degrees: Float) {
|
||||
rotationDial.setDegrees(degrees)
|
||||
}
|
||||
|
||||
fun setBottomOfImageEditorView(bottom: Int) {
|
||||
@@ -326,7 +346,7 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
|
||||
private fun presentModeCrop() {
|
||||
animateModeChange(
|
||||
inSet = cropButtonRow - if (isAvatarEdit) setOf(cropAspectLockButton) else setOf(),
|
||||
inSet = cropTools + cropButtonRow - if (isAvatarEdit) setOf(cropAspectLockButton) else setOf(),
|
||||
outSet = allModeTools
|
||||
)
|
||||
animateInUndoTools()
|
||||
@@ -523,6 +543,9 @@ class ImageEditorHudV2 @JvmOverloads constructor(
|
||||
fun onRotate90AntiClockwise()
|
||||
fun onCropAspectLock()
|
||||
fun onTextStyleToggle()
|
||||
fun onDialRotationGestureStarted()
|
||||
fun onDialRotationChanged(degrees: Float)
|
||||
fun onDialRotationGestureFinished()
|
||||
val isCropAspectLocked: Boolean
|
||||
|
||||
fun onRequestFullScreen(fullScreen: Boolean, hideKeyboard: Boolean)
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package org.thoughtcrime.securesms.scribbles
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.graphics.Typeface
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.Dimension
|
||||
import androidx.annotation.Px
|
||||
import org.thoughtcrime.securesms.util.ViewUtil
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.ceil
|
||||
import kotlin.math.floor
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class RotationDialView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val canvasBounds = Rect()
|
||||
private val centerMostIndicatorRect = RectF()
|
||||
private val indicatorRect = RectF()
|
||||
private val dimensions = Dimensions()
|
||||
|
||||
private var snapDegrees: Float = 0f
|
||||
private var degrees: Float = 0f
|
||||
private var isInGesture: Boolean = false
|
||||
|
||||
private val gestureDetector: GestureDetector = GestureDetector(context, GestureListener())
|
||||
|
||||
var listener: Listener? = null
|
||||
|
||||
private val textPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
textSize = ViewUtil.spToPx(15f).toFloat()
|
||||
typeface = Typeface.DEFAULT
|
||||
color = Colors.textColor
|
||||
style = Paint.Style.FILL
|
||||
textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
private val angleIndicatorPaint = Paint().apply {
|
||||
isAntiAlias = true
|
||||
color = Color.WHITE
|
||||
style = Paint.Style.FILL
|
||||
}
|
||||
|
||||
fun setDegrees(degrees: Float) {
|
||||
if (degrees != this.degrees) {
|
||||
this.degrees = degrees
|
||||
this.snapDegrees = calculateSnapDegrees()
|
||||
|
||||
if (isInGesture) {
|
||||
listener?.onDegreeChanged(snapDegrees)
|
||||
}
|
||||
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
if (event.actionIndex != 0) return false
|
||||
|
||||
isInGesture = gestureDetector.onTouchEvent(event)
|
||||
|
||||
when (event.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> listener?.onGestureStart()
|
||||
MotionEvent.ACTION_UP -> listener?.onGestureEnd()
|
||||
}
|
||||
|
||||
return isInGesture
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
if (isInEditMode) {
|
||||
canvas.drawColor(Color.BLACK)
|
||||
}
|
||||
|
||||
canvas.getClipBounds(canvasBounds)
|
||||
|
||||
val dialDegrees = getDialDegrees(snapDegrees)
|
||||
val bottom = canvasBounds.bottom
|
||||
val approximateCenterDegree = dialDegrees.roundToInt()
|
||||
var currentDegree = approximateCenterDegree
|
||||
val fractionalOffset = dialDegrees - approximateCenterDegree
|
||||
val dialOffset = dimensions.spaceBetweenAngleIndicators * fractionalOffset
|
||||
|
||||
val centerX = width / 2f
|
||||
centerMostIndicatorRect.set(
|
||||
centerX - dimensions.angleIndicatorWidth / 2f,
|
||||
bottom.toFloat() - dimensions.majorAngleIndicatorHeight,
|
||||
centerX + dimensions.angleIndicatorWidth / 2f,
|
||||
bottom.toFloat()
|
||||
)
|
||||
centerMostIndicatorRect.offset(-dialOffset, 0f)
|
||||
|
||||
indicatorRect.set(centerMostIndicatorRect)
|
||||
|
||||
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
||||
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
||||
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
||||
indicatorRect.offset(dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
||||
currentDegree += 1
|
||||
|
||||
while (indicatorRect.left < width && currentDegree <= ceil(MAX_DEGREES)) {
|
||||
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
||||
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
||||
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
||||
indicatorRect.offset(dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
||||
currentDegree += 1
|
||||
}
|
||||
|
||||
currentDegree = approximateCenterDegree
|
||||
indicatorRect.set(centerMostIndicatorRect)
|
||||
indicatorRect.offset(-dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
||||
currentDegree -= 1
|
||||
|
||||
while (indicatorRect.left >= 0 && currentDegree >= floor(MIN_DEGRESS)) {
|
||||
angleIndicatorPaint.color = Colors.colorForOtherDegree(currentDegree)
|
||||
indicatorRect.top = bottom.toFloat() - dimensions.getHeightForDegree(currentDegree)
|
||||
canvas.drawRect(indicatorRect, angleIndicatorPaint)
|
||||
indicatorRect.offset(-dimensions.spaceBetweenAngleIndicators.toFloat(), 0f)
|
||||
currentDegree -= 1
|
||||
}
|
||||
|
||||
centerMostIndicatorRect.offset(dialOffset, 0f)
|
||||
angleIndicatorPaint.color = Colors.colorForCenterDegree(approximateCenterDegree)
|
||||
canvas.drawRect(centerMostIndicatorRect, angleIndicatorPaint)
|
||||
|
||||
drawText(canvas)
|
||||
}
|
||||
|
||||
private fun drawText(canvas: Canvas) {
|
||||
val approximateDegrees = getDialDegrees(snapDegrees).roundToInt()
|
||||
canvas.drawText(
|
||||
"$approximateDegrees",
|
||||
width / 2f,
|
||||
canvasBounds.bottom - textPaint.descent() - dimensions.majorAngleIndicatorHeight - dimensions.textPaddingBottom,
|
||||
textPaint
|
||||
)
|
||||
}
|
||||
|
||||
private fun getDialDegrees(degrees: Float): Float {
|
||||
val alpha: Float = degrees % 360f
|
||||
|
||||
if (alpha % 90 == 0f) {
|
||||
return 0f
|
||||
}
|
||||
|
||||
val beta: Float = floor(alpha / 90f)
|
||||
val offset: Float = alpha - beta * 90f
|
||||
|
||||
return if (offset > 45f) {
|
||||
offset - 90f
|
||||
} else {
|
||||
offset
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateSnapDegrees(): Float {
|
||||
return if (isInGesture) {
|
||||
val dialDegrees = getDialDegrees(degrees)
|
||||
if (dialDegrees.roundToInt() == 0) {
|
||||
degrees - dialDegrees
|
||||
} else {
|
||||
degrees
|
||||
}
|
||||
} else {
|
||||
degrees
|
||||
}
|
||||
}
|
||||
|
||||
private inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean {
|
||||
val degreeIncrement: Float = distanceX / dimensions.spaceBetweenAngleIndicators
|
||||
val prevDialDegrees = getDialDegrees(degrees)
|
||||
val newDialDegrees = getDialDegrees(degrees + degreeIncrement)
|
||||
|
||||
val offEndOfMax = prevDialDegrees >= MAX_DEGREES / 2f && newDialDegrees <= MIN_DEGRESS / 2f
|
||||
val offEndOfMin = newDialDegrees >= MAX_DEGREES / 2f && prevDialDegrees <= MIN_DEGRESS / 2f
|
||||
|
||||
if (prevDialDegrees.roundToInt() != newDialDegrees.roundToInt() && isHapticFeedbackEnabled) {
|
||||
performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
|
||||
}
|
||||
|
||||
when {
|
||||
offEndOfMax -> {
|
||||
val newIncrement = MAX_DEGREES - prevDialDegrees
|
||||
setDegrees(degrees + newIncrement)
|
||||
}
|
||||
offEndOfMin -> {
|
||||
val newIncrement = MAX_DEGREES - abs(prevDialDegrees)
|
||||
setDegrees(degrees - newIncrement)
|
||||
}
|
||||
else -> {
|
||||
setDegrees(degrees + degreeIncrement)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class Dimensions {
|
||||
|
||||
@Px
|
||||
val spaceBetweenAngleIndicators: Int = ViewUtil.dpToPx(Dimensions.spaceBetweenAngleIndicators)
|
||||
|
||||
@Px
|
||||
val angleIndicatorWidth: Int = ViewUtil.dpToPx(Dimensions.angleIndicatorWidth)
|
||||
|
||||
@Px
|
||||
val minorAngleIndicatorHeight: Int = ViewUtil.dpToPx(Dimensions.minorAngleIndicatorHeight)
|
||||
|
||||
@Px
|
||||
val majorAngleIndicatorHeight: Int = ViewUtil.dpToPx(Dimensions.majorAngleIndicatorHeight)
|
||||
|
||||
@Px
|
||||
val textPaddingBottom: Int = ViewUtil.dpToPx(Dimensions.textPaddingBottom)
|
||||
|
||||
fun getHeightForDegree(degree: Int): Int {
|
||||
return if (degree == 0) {
|
||||
majorAngleIndicatorHeight
|
||||
} else {
|
||||
minorAngleIndicatorHeight
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val spaceBetweenAngleIndicators: Int = 12
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val angleIndicatorWidth: Int = 1
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val minorAngleIndicatorHeight: Int = 12
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val majorAngleIndicatorHeight: Int = 32
|
||||
|
||||
@Dimension(unit = Dimension.DP)
|
||||
private val textPaddingBottom: Int = 8
|
||||
}
|
||||
}
|
||||
|
||||
private object Colors {
|
||||
@ColorInt
|
||||
val textColor: Int = Color.WHITE
|
||||
|
||||
@ColorInt
|
||||
val majorAngleIndicatorColor: Int = 0xFF62E87A.toInt()
|
||||
|
||||
@ColorInt
|
||||
val modFiveIndicatorColor: Int = Color.WHITE
|
||||
|
||||
@ColorInt
|
||||
val minorAngleIndicatorColor: Int = 0x80FFFFFF.toInt()
|
||||
|
||||
fun colorForCenterDegree(degree: Int) = if (degree == 0) modFiveIndicatorColor else majorAngleIndicatorColor
|
||||
|
||||
fun colorForOtherDegree(degree: Int): Int {
|
||||
return when {
|
||||
degree % 5 == 0 -> modFiveIndicatorColor
|
||||
else -> minorAngleIndicatorColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_DEGREES: Float = 44.99999f
|
||||
private const val MIN_DEGRESS: Float = -44.99999f
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun onDegreeChanged(degrees: Float)
|
||||
fun onGestureStart()
|
||||
fun onGestureEnd()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user