mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-20 16:49:40 +01:00
Add support for time stickers in image editor.
This commit is contained in:
@@ -59,6 +59,10 @@ 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.stickers.AnalogClockStickerRenderer;
|
||||
import org.thoughtcrime.securesms.scribbles.stickers.DigitalClockStickerRenderer;
|
||||
import org.thoughtcrime.securesms.scribbles.stickers.FeatureSticker;
|
||||
import org.thoughtcrime.securesms.scribbles.stickers.TappableRenderer;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.ParcelUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
@@ -413,10 +417,25 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (resultCode == RESULT_OK && requestCode == SELECT_STICKER_REQUEST_CODE && data != null) {
|
||||
final Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
UriGlideRenderer renderer = new UriGlideRenderer(uri, true, imageMaxWidth, imageMaxHeight);
|
||||
EditorElement element = new EditorElement(renderer, EditorModel.Z_STICKERS);
|
||||
Renderer renderer = null;
|
||||
if (data.hasExtra(ImageEditorStickerSelectActivity.EXTRA_FEATURE_STICKER)) {
|
||||
FeatureSticker sticker = FeatureSticker.fromType(data.getStringExtra(ImageEditorStickerSelectActivity.EXTRA_FEATURE_STICKER));
|
||||
switch (sticker) {
|
||||
case DIGITAL_CLOCK:
|
||||
renderer = new DigitalClockStickerRenderer(System.currentTimeMillis());
|
||||
break;
|
||||
case ANALOG_CLOCK:
|
||||
renderer = new AnalogClockStickerRenderer(System.currentTimeMillis());
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
final Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
renderer = new UriGlideRenderer(uri, true, imageMaxWidth, imageMaxHeight);
|
||||
}
|
||||
}
|
||||
if (renderer != null) {
|
||||
EditorElement element = new EditorElement(renderer, EditorModel.Z_STICKERS);
|
||||
imageEditorView.getModel().addElementCentered(element, 0.4f);
|
||||
setCurrentSelection(element);
|
||||
hasMadeAnEditThisSession = true;
|
||||
@@ -1002,6 +1021,9 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
|
||||
if (editorElement.getRenderer() instanceof MultiLineTextRenderer) {
|
||||
setTextElement(editorElement, (ColorableRenderer) editorElement.getRenderer(), imageEditorView.isTextEditing());
|
||||
} else {
|
||||
if (editorElement.getRenderer() instanceof TappableRenderer) {
|
||||
((TappableRenderer) editorElement.getRenderer()).onTapped();
|
||||
}
|
||||
imageEditorHud.setMode(ImageEditorHudV2.Mode.MOVE_STICKER);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -9,7 +9,6 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
@@ -17,14 +16,17 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
|
||||
import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment;
|
||||
import org.thoughtcrime.securesms.keyboard.sticker.StickerSearchDialogFragment;
|
||||
import org.thoughtcrime.securesms.scribbles.stickers.FeatureSticker;
|
||||
import org.thoughtcrime.securesms.scribbles.stickers.ScribbleStickersFragment;
|
||||
import org.thoughtcrime.securesms.stickers.StickerEventListener;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public final class ImageEditorStickerSelectActivity extends AppCompatActivity implements StickerEventListener, MediaKeyboard.MediaKeyboardListener, StickerKeyboardPageFragment.Callback {
|
||||
public final class ImageEditorStickerSelectActivity extends AppCompatActivity implements StickerEventListener, MediaKeyboard.MediaKeyboardListener, StickerKeyboardPageFragment.Callback, ScribbleStickersFragment.Callback {
|
||||
|
||||
public static final String EXTRA_FEATURE_STICKER = "imageEditor.featureSticker";
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
@@ -36,12 +38,6 @@ public final class ImageEditorStickerSelectActivity extends AppCompatActivity im
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.scribble_select_new_sticker_activity);
|
||||
|
||||
KeyboardPagerViewModel keyboardPagerViewModel = new ViewModelProvider(this).get(KeyboardPagerViewModel.class);
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.STICKER);
|
||||
|
||||
MediaKeyboard mediaKeyboard = findViewById(R.id.emoji_drawer);
|
||||
mediaKeyboard.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -87,4 +83,14 @@ public final class ImageEditorStickerSelectActivity extends AppCompatActivity im
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFeatureSticker(FeatureSticker featureSticker) {
|
||||
Intent intent = new Intent();
|
||||
intent.putExtra(EXTRA_FEATURE_STICKER, featureSticker.getType());
|
||||
setResult(RESULT_OK, intent);
|
||||
|
||||
ViewUtil.hideKeyboard(this, findViewById(android.R.id.content));
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.SystemClock
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
/**
|
||||
* Animatable drawable of an analog clock. You can set a time, or start the animation to animate
|
||||
* the current time.
|
||||
*/
|
||||
class AnalogClockStickerDrawable(val context: Context) : Drawable(), Animatable {
|
||||
|
||||
private var clockFace: Drawable = AppCompatResources.getDrawable(context, R.drawable.clock_face_1)!!
|
||||
private var minuteHand: Drawable = AppCompatResources.getDrawable(context, R.drawable.clock_minute_hand_1)!!
|
||||
private var hourHand: Drawable = AppCompatResources.getDrawable(context, R.drawable.clock_hour_hand_1)!!
|
||||
private var clockCenter: Drawable? = null
|
||||
|
||||
/** Percentage of hour hand height that should shoot past the center point **/
|
||||
private var hourOffset = 0.28f
|
||||
|
||||
/** Percentage of minute hand height that should shoot past the center point **/
|
||||
private var minuteOffset = 0.2f
|
||||
|
||||
private var animating = false
|
||||
private var displayStyle = Style.STANDARD
|
||||
|
||||
private var time: Long? = null
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
clockFace.draw(canvas)
|
||||
|
||||
val now = time?.toLocalDateTime() ?: LocalDateTime.now()
|
||||
val hourDeg = computeHourRotationDeg(now)
|
||||
val minuteDeg = computeMinuteRotationDeg(now)
|
||||
|
||||
canvas.save()
|
||||
canvas.rotate(hourDeg, bounds.exactCenterX(), bounds.exactCenterY())
|
||||
hourHand.draw(canvas)
|
||||
canvas.restore()
|
||||
|
||||
canvas.save()
|
||||
canvas.rotate(minuteDeg, bounds.exactCenterX(), bounds.exactCenterY())
|
||||
minuteHand.draw(canvas)
|
||||
canvas.restore()
|
||||
|
||||
if (animating) {
|
||||
scheduleSelf(this::invalidateSelf, SystemClock.uptimeMillis() + 1000)
|
||||
}
|
||||
|
||||
clockCenter?.draw(canvas)
|
||||
}
|
||||
|
||||
fun nextFace() {
|
||||
setStyle(displayStyle.next())
|
||||
}
|
||||
|
||||
fun setStyle(style: Style) {
|
||||
displayStyle = style
|
||||
when (displayStyle) {
|
||||
Style.STANDARD -> clockFace1()
|
||||
Style.BLOCKY -> clockFace2()
|
||||
Style.LIGHT -> clockFace3()
|
||||
Style.GREEN -> clockFace4()
|
||||
}
|
||||
onBoundsChange(bounds)
|
||||
}
|
||||
|
||||
fun getStyle(): Style {
|
||||
return displayStyle
|
||||
}
|
||||
|
||||
fun setTime(newTime: Long?) {
|
||||
time = newTime
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
private fun clockFace1() {
|
||||
clockFace = AppCompatResources.getDrawable(context, R.drawable.clock_face_1)!!
|
||||
minuteHand = AppCompatResources.getDrawable(context, R.drawable.clock_minute_hand_1)!!
|
||||
hourHand = AppCompatResources.getDrawable(context, R.drawable.clock_hour_hand_1)!!
|
||||
clockCenter = null
|
||||
|
||||
hourOffset = 0.28f
|
||||
minuteOffset = 0.2f
|
||||
}
|
||||
|
||||
private fun clockFace2() {
|
||||
clockFace = AppCompatResources.getDrawable(context, R.drawable.clock_face_2)!!
|
||||
minuteHand = AppCompatResources.getDrawable(context, R.drawable.clock_minute_hand_2)!!
|
||||
hourHand = AppCompatResources.getDrawable(context, R.drawable.clock_hour_hand_2)!!
|
||||
clockCenter = null
|
||||
|
||||
hourOffset = 0.238f
|
||||
minuteOffset = 0.1623f
|
||||
}
|
||||
|
||||
private fun clockFace3() {
|
||||
clockFace = AppCompatResources.getDrawable(context, R.drawable.clock_face_3)!!
|
||||
minuteHand = AppCompatResources.getDrawable(context, R.drawable.clock_minute_hand_3)!!
|
||||
hourHand = AppCompatResources.getDrawable(context, R.drawable.clock_hour_hand_3)!!
|
||||
clockCenter = null
|
||||
|
||||
hourOffset = 0f
|
||||
minuteOffset = 0f
|
||||
}
|
||||
|
||||
private fun clockFace4() {
|
||||
clockFace = AppCompatResources.getDrawable(context, R.drawable.clock_face_4)!!
|
||||
minuteHand = AppCompatResources.getDrawable(context, R.drawable.clock_minute_hand_4)!!
|
||||
hourHand = AppCompatResources.getDrawable(context, R.drawable.clock_hour_hand_4)!!
|
||||
clockCenter = AppCompatResources.getDrawable(context, R.drawable.clock_center_cover_4)
|
||||
|
||||
hourOffset = 0f
|
||||
minuteOffset = 0f
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
val dimen = min(bounds.width(), bounds.height())
|
||||
val scale: Float = dimen.toFloat() / clockFace.intrinsicWidth.toFloat()
|
||||
val centerX = bounds.centerX()
|
||||
val centerY = bounds.centerY()
|
||||
|
||||
val hourW = (hourHand.intrinsicWidth * scale).roundToInt()
|
||||
val hourH = (hourHand.intrinsicHeight * scale).roundToInt()
|
||||
|
||||
val minuteW = (minuteHand.intrinsicWidth * scale).roundToInt()
|
||||
val minuteH = (minuteHand.intrinsicHeight * scale).roundToInt()
|
||||
|
||||
if (bounds.width() > bounds.height()) {
|
||||
val diff = (bounds.width() - bounds.height()) / 2
|
||||
clockFace.setBounds(bounds.left + diff, bounds.top, bounds.right - diff, bounds.bottom)
|
||||
} else {
|
||||
val diff = (bounds.height() - bounds.width()) / 2
|
||||
clockFace.setBounds(bounds.left, bounds.top - diff, bounds.right, bounds.bottom + diff)
|
||||
}
|
||||
val hourVertical = (hourH * hourOffset).roundToInt()
|
||||
val minuteVertical = (minuteH * minuteOffset).roundToInt()
|
||||
hourHand.setBounds(centerX - hourW / 2, (centerY - hourH + hourVertical), centerX + hourW / 2, centerY + hourVertical)
|
||||
minuteHand.setBounds(centerX - minuteW / 2, (centerY - minuteH + minuteVertical), centerX + minuteW / 2, centerY + minuteVertical)
|
||||
|
||||
val centerVal = clockCenter
|
||||
if (centerVal != null) {
|
||||
val centerW = (centerVal.intrinsicWidth * scale).roundToInt()
|
||||
val centerH = (centerVal.intrinsicHeight * scale).roundToInt()
|
||||
|
||||
centerVal.setBounds(centerX - centerW / 2, centerY - centerH / 2, centerX + centerW / 2, centerY + centerH / 2)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getIntrinsicWidth(): Int {
|
||||
return clockFace.intrinsicWidth
|
||||
}
|
||||
|
||||
override fun getIntrinsicHeight(): Int {
|
||||
return clockFace.intrinsicHeight
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
animating = true
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
animating = false
|
||||
unscheduleSelf(this::invalidateSelf)
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return animating
|
||||
}
|
||||
|
||||
private fun computeHourRotationDeg(localDateTime: LocalDateTime): Float {
|
||||
val hour = localDateTime.hour % 12
|
||||
val minute = localDateTime.minute
|
||||
val seconds = localDateTime.second
|
||||
|
||||
return 360f * (hour + (minute / 60f) + (seconds / 3600f)) / 12f
|
||||
}
|
||||
|
||||
private fun computeMinuteRotationDeg(localDateTime: LocalDateTime): Float {
|
||||
val minute = localDateTime.minute
|
||||
val seconds = localDateTime.second
|
||||
|
||||
return 360f * (minute + (seconds / 60f)) / 60f
|
||||
}
|
||||
|
||||
enum class Style(val type: Int) {
|
||||
STANDARD(0),
|
||||
BLOCKY(1),
|
||||
LIGHT(2),
|
||||
GREEN(3);
|
||||
|
||||
fun next(): Style {
|
||||
val values = Style.values()
|
||||
|
||||
return values[(values.indexOf(this) + 1) % values.size]
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromType(type: Int) = Style.values().first { it.type == type }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.signal.imageeditor.core.Bounds
|
||||
import org.signal.imageeditor.core.RendererContext
|
||||
import org.signal.imageeditor.core.SelectableRenderer
|
||||
import org.signal.imageeditor.core.renderers.InvalidateableRenderer
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
/**
|
||||
* Analog clock sticker renderer for the image editor.
|
||||
*/
|
||||
class AnalogClockStickerRenderer
|
||||
@JvmOverloads constructor(
|
||||
val time: Long,
|
||||
val style: AnalogClockStickerDrawable.Style = AnalogClockStickerDrawable.Style.STANDARD
|
||||
) : InvalidateableRenderer(), SelectableRenderer, TappableRenderer {
|
||||
|
||||
private val clockStickerDrawable = AnalogClockStickerDrawable(ApplicationDependencies.getApplication())
|
||||
private val insetBounds = Rect(
|
||||
Bounds.FULL_BOUNDS.left.toInt(),
|
||||
Bounds.FULL_BOUNDS.top.toInt(),
|
||||
Bounds.FULL_BOUNDS.right.toInt(),
|
||||
Bounds.FULL_BOUNDS.bottom.toInt()
|
||||
).apply { inset(261, 261) }
|
||||
|
||||
init {
|
||||
clockStickerDrawable.bounds = insetBounds
|
||||
clockStickerDrawable.setTime(time)
|
||||
clockStickerDrawable.setStyle(style)
|
||||
}
|
||||
|
||||
override fun onTapped() {
|
||||
clockStickerDrawable.nextFace()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onSelected(selected: Boolean) {
|
||||
}
|
||||
|
||||
override fun getSelectionBounds(bounds: RectF) {
|
||||
bounds.set(Bounds.FULL_BOUNDS)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeLong(time)
|
||||
dest.writeInt(clockStickerDrawable.getStyle().type)
|
||||
}
|
||||
|
||||
override fun render(rendererContext: RendererContext) {
|
||||
clockStickerDrawable.draw(rendererContext.canvas)
|
||||
}
|
||||
|
||||
override fun hitTest(x: Float, y: Float): Boolean {
|
||||
return Bounds.FULL_BOUNDS.contains(x, y)
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<AnalogClockStickerRenderer> {
|
||||
override fun createFromParcel(parcel: Parcel): AnalogClockStickerRenderer {
|
||||
return AnalogClockStickerRenderer(parcel.readLong(), AnalogClockStickerDrawable.Style.fromType(parcel.readInt()))
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<AnalogClockStickerRenderer?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.ColorFilter
|
||||
import android.graphics.Paint
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.Rect
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.SystemClock
|
||||
import android.text.TextPaint
|
||||
import android.text.format.DateFormat
|
||||
import org.thoughtcrime.securesms.util.toLocalDateTime
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Animatable drawable of a digital clock. You can set a time, or start the animation to animate
|
||||
* the current time. Supports 12/24 hr time.
|
||||
*/
|
||||
class DigitalClockStickerDrawable(
|
||||
val context: Context,
|
||||
private var displayStyle: Style = Style.LIGHT_NO_BG
|
||||
) :
|
||||
Drawable(), Animatable {
|
||||
|
||||
companion object {
|
||||
private const val BG_PADDING = 40f
|
||||
private const val BG_CORNER_RADIUS = 40f
|
||||
private const val AM_PM_SPACING = 7f
|
||||
private const val LIGHT_BG_COLOR = 0x66FFFFFF
|
||||
private const val DARK_BG_COLOR = 0x66000000
|
||||
private const val RED_TEXT_COLOR = 0xFFFF4747.toInt()
|
||||
private const val TIME_TEXT_SIZE = 204f
|
||||
private const val AM_PM_TEXT_SIZE = 50f
|
||||
|
||||
/** Box dimensions that wrap the sticker. Dimensions are relative to this value from designs. */
|
||||
private const val STICKER_BOX_SIZE = 512f
|
||||
|
||||
/** Additional scaling factor as sticker is still small within the box */
|
||||
private const val STICKER_SCALING_ADJUSTMENT = 1.2f
|
||||
}
|
||||
|
||||
private val ampmTypeface = Typeface.createFromAsset(context.assets, "fonts/Inter-Medium.otf")
|
||||
private val digitTypeface = Typeface.createFromAsset(context.assets, "fonts/Hatsuishi-Regular.otf")
|
||||
|
||||
private var wrapped = false
|
||||
private var animating = false
|
||||
private var scale = 1f
|
||||
|
||||
private var time: Long? = null
|
||||
|
||||
private val digitPaint = TextPaint().apply {
|
||||
this.typeface = digitTypeface
|
||||
this.textSize = 204f
|
||||
this.textAlign = Paint.Align.LEFT
|
||||
this.color = Color.WHITE
|
||||
}
|
||||
|
||||
private val ampmPaint = TextPaint().apply {
|
||||
this.typeface = ampmTypeface
|
||||
this.textSize = 50f
|
||||
this.textAlign = Paint.Align.LEFT
|
||||
this.color = Color.WHITE
|
||||
}
|
||||
|
||||
private val bgPaint = Paint().apply {
|
||||
color = Color.WHITE
|
||||
}
|
||||
|
||||
init {
|
||||
setStyle(displayStyle)
|
||||
}
|
||||
|
||||
override fun draw(canvas: Canvas) {
|
||||
val centerX = bounds.exactCenterX()
|
||||
val centerY = bounds.exactCenterY()
|
||||
|
||||
val timeMetrics = digitPaint.fontMetrics
|
||||
val now = time?.toLocalDateTime() ?: LocalDateTime.now()
|
||||
val is24Hours = DateFormat.is24HourFormat(context)
|
||||
val timeHeight = timeMetrics.bottom + timeMetrics.top + timeMetrics.leading
|
||||
val baseline = centerY - timeHeight / 2f
|
||||
if (is24Hours) {
|
||||
digitPaint.textAlign = Paint.Align.CENTER
|
||||
val timeStr = getHoursString(now)
|
||||
val width = digitPaint.measureText(timeStr)
|
||||
if (wrapped) {
|
||||
val bgCornerRadius = getBgCornerRadius()
|
||||
val bgPadding = getBgPadding()
|
||||
canvas.drawRoundRect(
|
||||
centerX - width / 2f - bgPadding,
|
||||
baseline + timeMetrics.top - bgPadding,
|
||||
centerX + width / 2f + bgPadding,
|
||||
baseline + timeMetrics.bottom + bgPadding,
|
||||
bgCornerRadius,
|
||||
bgCornerRadius,
|
||||
bgPaint
|
||||
)
|
||||
}
|
||||
canvas.drawText(timeStr, centerX, baseline, digitPaint)
|
||||
} else {
|
||||
digitPaint.textAlign = Paint.Align.LEFT
|
||||
val timeStr = getHoursString(now)
|
||||
val timeWidth = digitPaint.measureText(timeStr)
|
||||
val amPmStr = getAmPmString(now)
|
||||
val amPmWidth = ampmPaint.measureText(amPmStr)
|
||||
val ampmSpacing = AM_PM_SPACING * scale
|
||||
val totalWidth = timeWidth + amPmWidth + ampmSpacing
|
||||
|
||||
if (wrapped) {
|
||||
val bgPadding = getBgPadding()
|
||||
val bgCornerRadius = getBgCornerRadius()
|
||||
canvas.drawRoundRect(
|
||||
centerX - totalWidth / 2f - bgPadding,
|
||||
baseline + timeMetrics.top - bgPadding,
|
||||
centerX + totalWidth / 2f + bgPadding,
|
||||
baseline + timeMetrics.bottom + bgPadding,
|
||||
bgCornerRadius,
|
||||
bgCornerRadius,
|
||||
bgPaint
|
||||
)
|
||||
}
|
||||
|
||||
canvas.drawText(timeStr, centerX - totalWidth / 2f, baseline, digitPaint)
|
||||
canvas.drawText(amPmStr, centerX + ampmSpacing + timeWidth - (totalWidth / 2f), baseline, ampmPaint)
|
||||
}
|
||||
|
||||
if (animating) {
|
||||
scheduleSelf(this::invalidateSelf, SystemClock.uptimeMillis() + 1000)
|
||||
}
|
||||
}
|
||||
|
||||
fun nextStyle() {
|
||||
setStyle(displayStyle.next())
|
||||
}
|
||||
|
||||
fun setStyle(style: Style) {
|
||||
displayStyle = style
|
||||
when (style) {
|
||||
Style.LIGHT_NO_BG -> styleWhiteTextNoBg()
|
||||
Style.DARK_NO_BG -> styleBlackTextNoBg()
|
||||
Style.LIGHT -> styleLightWithBg()
|
||||
Style.DARK -> styleDarkWithBg()
|
||||
Style.DARK_WITH_RED_TEXT -> styleDarkWithRedText()
|
||||
}
|
||||
onBoundsChange(bounds)
|
||||
}
|
||||
|
||||
fun getStyle(): Style {
|
||||
return displayStyle
|
||||
}
|
||||
|
||||
private fun styleWhiteTextNoBg() {
|
||||
digitPaint.color = Color.WHITE
|
||||
ampmPaint.color = Color.WHITE
|
||||
wrapped = false
|
||||
}
|
||||
|
||||
private fun styleBlackTextNoBg() {
|
||||
digitPaint.color = Color.BLACK
|
||||
ampmPaint.color = Color.BLACK
|
||||
wrapped = false
|
||||
}
|
||||
|
||||
private fun styleLightWithBg() {
|
||||
digitPaint.color = Color.WHITE
|
||||
ampmPaint.color = Color.WHITE
|
||||
bgPaint.color = LIGHT_BG_COLOR
|
||||
wrapped = true
|
||||
}
|
||||
|
||||
private fun styleDarkWithBg() {
|
||||
digitPaint.color = Color.WHITE
|
||||
ampmPaint.color = Color.WHITE
|
||||
bgPaint.color = DARK_BG_COLOR
|
||||
wrapped = true
|
||||
}
|
||||
|
||||
private fun styleDarkWithRedText() {
|
||||
digitPaint.color = RED_TEXT_COLOR
|
||||
ampmPaint.color = RED_TEXT_COLOR
|
||||
bgPaint.color = DARK_BG_COLOR
|
||||
wrapped = true
|
||||
}
|
||||
|
||||
private fun getBgPadding(): Float {
|
||||
return BG_PADDING * scale
|
||||
}
|
||||
|
||||
private fun getBgCornerRadius(): Float {
|
||||
return BG_CORNER_RADIUS * scale
|
||||
}
|
||||
|
||||
private fun getAmPmString(time: LocalDateTime): String {
|
||||
return DateTimeFormatter.ofPattern("a", Locale.getDefault()).format(time)
|
||||
}
|
||||
|
||||
private fun getHoursString(time: LocalDateTime): String {
|
||||
return if (!DateFormat.is24HourFormat(context)) {
|
||||
DateTimeFormatter.ofPattern("h:mm", Locale.getDefault()).format(time)
|
||||
} else {
|
||||
DateTimeFormatter.ofPattern("H:mm", Locale.getDefault()).format(time)
|
||||
}
|
||||
}
|
||||
|
||||
fun setTime(newTime: Long?) {
|
||||
time = newTime
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun setAlpha(alpha: Int) = Unit
|
||||
override fun setColorFilter(colorFilter: ColorFilter?) = Unit
|
||||
|
||||
override fun getOpacity(): Int {
|
||||
return PixelFormat.TRANSLUCENT
|
||||
}
|
||||
|
||||
override fun onBoundsChange(bounds: Rect) {
|
||||
val dimension = min(bounds.width(), bounds.height())
|
||||
scale = (dimension / STICKER_BOX_SIZE) * STICKER_SCALING_ADJUSTMENT
|
||||
digitPaint.textSize = scale * TIME_TEXT_SIZE
|
||||
ampmPaint.textSize = scale * AM_PM_TEXT_SIZE
|
||||
}
|
||||
|
||||
override fun start() {
|
||||
animating = true
|
||||
invalidateSelf()
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
animating = false
|
||||
unscheduleSelf(this::invalidateSelf)
|
||||
}
|
||||
|
||||
override fun isRunning(): Boolean {
|
||||
return animating
|
||||
}
|
||||
|
||||
enum class Style(val type: Int) {
|
||||
LIGHT_NO_BG(0),
|
||||
DARK_NO_BG(1),
|
||||
LIGHT(2),
|
||||
DARK(3),
|
||||
DARK_WITH_RED_TEXT(4);
|
||||
|
||||
fun next(): Style {
|
||||
val values = Style.values()
|
||||
|
||||
return values[(values.indexOf(this) + 1) % values.size]
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun fromType(type: Int) = Style.values().first { it.type == type }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import android.graphics.Rect
|
||||
import android.graphics.RectF
|
||||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
import org.signal.imageeditor.core.Bounds
|
||||
import org.signal.imageeditor.core.RendererContext
|
||||
import org.signal.imageeditor.core.SelectableRenderer
|
||||
import org.signal.imageeditor.core.renderers.InvalidateableRenderer
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
/**
|
||||
* Analog clock sticker renderer for the image editor.
|
||||
*/
|
||||
class DigitalClockStickerRenderer
|
||||
@JvmOverloads constructor(
|
||||
val time: Long,
|
||||
val style: DigitalClockStickerDrawable.Style = DigitalClockStickerDrawable.Style.LIGHT_NO_BG
|
||||
) : InvalidateableRenderer(), SelectableRenderer, TappableRenderer {
|
||||
|
||||
private val clockStickerDrawable = DigitalClockStickerDrawable(ApplicationDependencies.getApplication())
|
||||
private val insetBounds = Rect(
|
||||
Bounds.FULL_BOUNDS.left.toInt(),
|
||||
Bounds.FULL_BOUNDS.top.toInt(),
|
||||
Bounds.FULL_BOUNDS.right.toInt(),
|
||||
Bounds.FULL_BOUNDS.bottom.toInt()
|
||||
).apply { inset(261, 261) }
|
||||
|
||||
init {
|
||||
clockStickerDrawable.bounds = insetBounds
|
||||
clockStickerDrawable.setTime(time)
|
||||
clockStickerDrawable.setStyle(style)
|
||||
}
|
||||
|
||||
override fun onTapped() {
|
||||
clockStickerDrawable.nextStyle()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onSelected(selected: Boolean) {
|
||||
}
|
||||
|
||||
override fun getSelectionBounds(bounds: RectF) {
|
||||
bounds.set(Bounds.FULL_BOUNDS)
|
||||
}
|
||||
|
||||
override fun describeContents(): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun writeToParcel(dest: Parcel, flags: Int) {
|
||||
dest.writeLong(time)
|
||||
dest.writeInt(clockStickerDrawable.getStyle().type)
|
||||
}
|
||||
|
||||
override fun render(rendererContext: RendererContext) {
|
||||
clockStickerDrawable.draw(rendererContext.canvas)
|
||||
}
|
||||
|
||||
override fun hitTest(x: Float, y: Float): Boolean {
|
||||
return Bounds.FULL_BOUNDS.contains(x, y)
|
||||
}
|
||||
|
||||
companion object CREATOR : Parcelable.Creator<DigitalClockStickerRenderer> {
|
||||
override fun createFromParcel(parcel: Parcel): DigitalClockStickerRenderer {
|
||||
return DigitalClockStickerRenderer(
|
||||
parcel.readLong(),
|
||||
DigitalClockStickerDrawable.Style.fromType(parcel.readInt())
|
||||
)
|
||||
}
|
||||
|
||||
override fun newArray(size: Int): Array<DigitalClockStickerRenderer?> {
|
||||
return arrayOfNulls(size)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
/**
|
||||
* Types of feature rich stickers for the image editor
|
||||
*/
|
||||
enum class FeatureSticker(val type: String) {
|
||||
DIGITAL_CLOCK("digital_clock"),
|
||||
ANALOG_CLOCK("analog_clock") ;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun fromType(type: String) = FeatureSticker.values().first { it.type == type }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Animatable
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.keyboard.sticker.KeyboardStickerListAdapter
|
||||
import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment
|
||||
import org.thoughtcrime.securesms.util.Throttler
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList
|
||||
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder
|
||||
import org.thoughtcrime.securesms.util.fragments.findListener
|
||||
|
||||
/**
|
||||
* Sticker chooser fragment for the image editor. Implement the Callback for
|
||||
* both feature stickers, and regular stickers from StickerKeyboardPageFragment
|
||||
*/
|
||||
class ScribbleStickersFragment : StickerKeyboardPageFragment() {
|
||||
|
||||
interface Callback {
|
||||
fun onFeatureSticker(sticker: FeatureSticker)
|
||||
}
|
||||
|
||||
private val stickerThrottler: Throttler = Throttler(100)
|
||||
|
||||
private val featureStickerList: MappingModelList = MappingModelList(
|
||||
listOf(
|
||||
FeatureHeader(R.string.ScribbleStickersFragment__featured_stickers),
|
||||
FeatureStickerModel(FeatureSticker.ANALOG_CLOCK),
|
||||
FeatureStickerModel(FeatureSticker.DIGITAL_CLOCK)
|
||||
)
|
||||
)
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
stickerListAdapter.registerFactory(FeatureStickerModel::class.java, LayoutFactory(::FeatureStickerViewHolder, R.layout.sticker_keyboard_page_list_item))
|
||||
stickerListAdapter.registerFactory(FeatureHeader::class.java, LayoutFactory(::HeaderViewHolder, R.layout.sticker_grid_header))
|
||||
}
|
||||
|
||||
override fun updateStickerList(stickers: MappingModelList) {
|
||||
stickers.addAll(0, featureStickerList)
|
||||
super.updateStickerList(stickers)
|
||||
}
|
||||
|
||||
override fun scrollOnLoad() {
|
||||
}
|
||||
|
||||
private fun onStickerClick(featureSticker: FeatureSticker) {
|
||||
stickerThrottler.publish { findListener<Callback>()?.onFeatureSticker(featureSticker) }
|
||||
}
|
||||
|
||||
data class FeatureStickerModel(val featureSticker: FeatureSticker) : MappingModel<FeatureStickerModel> {
|
||||
|
||||
override fun areItemsTheSame(newItem: FeatureStickerModel): Boolean {
|
||||
return featureSticker == newItem.featureSticker
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: FeatureStickerModel): Boolean {
|
||||
return areItemsTheSame(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FeatureStickerViewHolder(itemView: View) : MappingViewHolder<FeatureStickerModel>(itemView) {
|
||||
|
||||
private val image: ImageView = findViewById(R.id.sticker_keyboard_page_image)
|
||||
|
||||
override fun bind(model: FeatureStickerModel) {
|
||||
when (model.featureSticker) {
|
||||
FeatureSticker.ANALOG_CLOCK -> bindAnalogClock()
|
||||
FeatureSticker.DIGITAL_CLOCK -> bindDigitalClock()
|
||||
}
|
||||
image.setOnClickListener {
|
||||
onStickerClick(model.featureSticker)
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindAnalogClock() {
|
||||
val clockDrawable = AnalogClockStickerDrawable(image.context)
|
||||
clockDrawable.start()
|
||||
image.setImageDrawable(clockDrawable)
|
||||
}
|
||||
|
||||
private fun bindDigitalClock() {
|
||||
val clockDrawable = DigitalClockStickerDrawable(image.context)
|
||||
clockDrawable.start()
|
||||
image.setImageDrawable(clockDrawable)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
(image.drawable as? Animatable)?.start()
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
(image.drawable as? Animatable)?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
data class FeatureHeader(private val titleResource: Int?) : MappingModel<FeatureHeader>, KeyboardStickerListAdapter.Header {
|
||||
fun getTitle(context: Context): String {
|
||||
return context.resources.getString(titleResource ?: R.string.StickerManagementAdapter_untitled)
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(newItem: FeatureHeader): Boolean {
|
||||
return titleResource == newItem.titleResource
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: FeatureHeader): Boolean {
|
||||
return areItemsTheSame(newItem)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class HeaderViewHolder(itemView: View) : MappingViewHolder<FeatureHeader>(itemView) {
|
||||
|
||||
private val title: TextView = findViewById(R.id.sticker_grid_header_title)
|
||||
|
||||
override fun bind(model: FeatureHeader) {
|
||||
title.text = model.getTitle(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package org.thoughtcrime.securesms.scribbles.stickers
|
||||
|
||||
import org.signal.imageeditor.core.Renderer
|
||||
|
||||
/**
|
||||
* A renderer that can handle a tap event
|
||||
*/
|
||||
interface TappableRenderer : Renderer {
|
||||
fun onTapped()
|
||||
}
|
||||
Reference in New Issue
Block a user