Add support for time stickers in image editor.

This commit is contained in:
Clark 2023-03-30 11:26:56 -04:00 committed by Alex Hart
parent 08ebca501b
commit 28f27915c5
29 changed files with 1254 additions and 23 deletions

Binary file not shown.

View file

@ -62,7 +62,7 @@ class KeyboardStickerListAdapter(
}
}
data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel<StickerHeader>, HasPackId {
data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel<StickerHeader>, HasPackId, Header {
fun getTitle(context: Context): String {
return title ?: context.resources.getString(titleResource ?: R.string.StickerManagementAdapter_untitled)
}
@ -85,6 +85,7 @@ class KeyboardStickerListAdapter(
}
}
interface Header
interface HasPackId {
val packId: String
}

View file

@ -31,7 +31,7 @@ import java.util.Optional
import kotlin.math.abs
import kotlin.math.max
class StickerKeyboardPageFragment :
open class StickerKeyboardPageFragment :
LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment),
KeyboardStickerListAdapter.EventListener,
StickerRolloverTouchListener.RolloverEventListener,
@ -39,9 +39,9 @@ class StickerKeyboardPageFragment :
DatabaseObserver.Observer,
View.OnLayoutChangeListener {
private lateinit var stickerList: RecyclerView
private lateinit var stickerListAdapter: KeyboardStickerListAdapter
private lateinit var layoutManager: GridLayoutManager
protected lateinit var stickerList: RecyclerView
protected lateinit var stickerListAdapter: KeyboardStickerListAdapter
protected lateinit var layoutManager: GridLayoutManager
private lateinit var listTouchListener: StickerRolloverTouchListener
private lateinit var stickerPacksRecycler: RecyclerView
private lateinit var appBarLayout: AppBarLayout
@ -63,7 +63,7 @@ class StickerKeyboardPageFragment :
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int): Int {
val model: Optional<MappingModel<*>> = stickerListAdapter.getModel(position)
if (model.isPresent && model.get() is KeyboardStickerListAdapter.StickerHeader) {
if (model.isPresent && model.get() is KeyboardStickerListAdapter.Header) {
return spanCount
}
return 1
@ -124,15 +124,19 @@ class StickerKeyboardPageFragment :
viewModel.refreshStickers()
}
private fun updateStickerList(stickers: MappingModelList) {
open fun updateStickerList(stickers: MappingModelList) {
if (firstLoad) {
stickerListAdapter.submitList(stickers) { layoutManager.scrollToPositionWithOffset(1, 0) }
stickerListAdapter.submitList(stickers, this::scrollOnLoad)
firstLoad = false
} else {
stickerListAdapter.submitList(stickers)
}
}
open fun scrollOnLoad() {
layoutManager.scrollToPositionWithOffset(1, 0)
}
private fun onTabSelected(stickerPack: KeyboardStickerPackListAdapter.StickerPack) {
scrollTo(stickerPack.packRecord.packId)
viewModel.selectPack(stickerPack.packRecord.packId)

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="42dp"
android:height="42dp"
android:viewportWidth="42"
android:viewportHeight="42">
<path
android:pathData="M21,21m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
android:fillColor="#fff"/>
<path
android:pathData="M21,21m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
android:fillColor="#fff"/>
<path
android:pathData="M21,21m-21,0a21,21 0,1 1,42 0a21,21 0,1 1,-42 0"
android:fillColor="#d1ffc1"/>
</vector>

View file

@ -0,0 +1,64 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600"
tools:ignore="VectorRaster">
<path
android:pathData="M600,300C600,465.68 465.68,600 300,600C134.32,600 0,465.68 0,300C0,134.32 134.32,0 300,0C465.68,0 600,134.32 600,300Z"
android:strokeAlpha="0.8"
android:fillColor="#000000"
android:fillAlpha="0.8"/>
<path
android:pathData="M310,295.31C310,300.84 305.52,305.31 300,305.31C294.48,305.31 290,300.84 290,295.31C290,289.79 294.48,285.31 300,285.31C305.52,285.31 310,289.79 310,295.31Z"
android:fillColor="#FF0000"/>
<path
android:pathData="M320,300C320,311.05 311.05,320 300,320C288.95,320 280,311.05 280,300C280,288.95 288.95,280 300,280C311.05,280 320,288.95 320,300Z"
android:fillColor="#FFBE20"/>
<path
android:pathData="M283.96,83.41V28.94H278.47C277.3,34.5 273.07,37.17 268.52,37.17H267.19V43.2H269.3C271.89,43.2 275.73,42.26 277.45,40.15V83.41H283.96Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M326.03,83.41V77.45H302.75C308.63,71.73 313.25,67.03 317.17,62.09C322.73,55.43 325.4,48.92 325.4,42.5C325.4,32.86 319.99,27.76 310.43,27.76C300.71,27.76 294.83,33.25 294.83,42.42C294.83,43.99 294.99,45.48 295.3,46.89L302.36,47.75C301.97,45.95 301.81,44.22 301.81,42.58C301.81,36.78 304.87,33.48 310.12,33.48C315.29,33.48 318.19,36.7 318.19,42.81C318.19,48.38 316.15,53.71 311.37,59.66C306.43,65.62 300.63,71.42 294.29,77.84V83.41H326.03Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M432.31,117.64V62.72H426.78C425.6,68.33 421.33,71.02 416.75,71.02H415.4V77.1H417.54C420.14,77.1 424.01,76.15 425.75,74.02V117.64H432.31Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M528.69,206.37V200.36H505.23C511.15,194.6 515.81,189.85 519.76,184.88C525.37,178.16 528.06,171.6 528.06,165.12C528.06,155.41 522.61,150.27 512.97,150.27C503.17,150.27 497.24,155.8 497.24,165.04C497.24,166.63 497.4,168.13 497.72,169.55L504.83,170.42C504.43,168.6 504.28,166.86 504.28,165.2C504.28,159.36 507.36,156.04 512.65,156.04C517.87,156.04 520.79,159.28 520.79,165.44C520.79,171.05 518.73,176.42 513.92,182.43C508.94,188.43 503.09,194.28 496.69,200.76V206.37H528.69Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M525.9,427.65H518.4V391.47H510.65L490.5,428.21V432.95H511.99V446.38H518.4V432.95H525.9V427.65ZM511.99,427.65H496.75C498.17,425.91 499.27,423.94 500.7,421.41L508.68,406.48C510.18,403.55 511.13,401.42 511.99,398.66H512.15C511.99,401.58 511.99,403.63 511.99,407.27V427.65Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M416.19,531.2C426.54,531.2 432.54,523.85 432.54,511.68C432.54,499.75 427.57,493.03 418.72,493.03C413.5,493.03 409.63,495.33 407.42,499.67L408.92,481.02H430.17V475.1H403.47L401.18,506.15L407.66,506.86C408.6,501.8 411.84,498.64 416.66,498.64C422.51,498.64 425.51,503.39 425.51,512.08C425.51,520.85 422.27,525.51 416.35,525.51C411.05,525.51 408.05,521.4 407.42,515.39L400.39,516.18C401.1,525.27 406.95,531.2 416.19,531.2Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M299.39,525.51C293.86,525.51 289.83,528.11 287.61,532.54C287.77,518.48 291.17,511.6 297.97,511.6C302.86,511.6 305.39,514.68 306.02,521.08L313.06,519.9C312.11,510.65 306.89,506.07 298.05,506.07C286.98,506.07 280.82,514.21 280.82,534.91C280.82,555.37 286.35,563.35 297.81,563.35C307.68,563.35 313.69,556.16 313.69,544.08C313.69,532.38 308.39,525.51 299.39,525.51ZM297.81,557.74C291.88,557.74 288.17,553 287.69,540.68C289.2,534.67 292.43,531.04 297.65,531.04C303.34,531.04 306.66,535.86 306.66,544.16C306.66,553.16 303.34,557.74 297.81,557.74Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M174.49,527.7V523.35C174.57,507.39 179.62,491.36 190.29,477.77V472.79H159.71V478.95H183.18C172.99,493.57 167.46,508.82 167.3,525.41V527.7H174.49Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M96.07,417.68C102.16,415.54 105.01,411.59 105.01,405.11C105.01,395.93 99,390.95 89.43,390.95C80.02,390.95 73.77,396.17 73.77,405.18C73.77,412.46 76.94,416.17 82.71,418.7C75.99,420.84 72.51,425.43 72.51,432.78C72.51,442.5 78.83,448.28 89.67,448.28C100.03,448.28 106.51,442.9 106.51,432.46C106.51,424.32 102.87,420.37 96.07,417.68ZM89.35,396.01C94.96,396.01 98.29,398.94 98.29,405.11C98.29,411.35 96.39,413.72 90.69,415.86L88.64,415.15C83.42,413.33 80.57,411.11 80.57,405.03C80.57,398.94 83.9,396.01 89.35,396.01ZM89.67,442.9C83.18,442.9 79.39,439.1 79.39,432.62C79.39,425.9 82,422.74 88.16,420.6L90.93,421.55C96.62,423.53 99.63,426.22 99.63,432.7C99.63,439.89 95.99,442.9 89.67,442.9Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M71.09,211.05V156.14H65.56C64.37,161.75 60.11,164.44 55.52,164.44H54.18V170.52H56.31C58.92,170.52 62.79,169.57 64.53,167.44V211.05H71.09Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M100.78,212.24C112.23,212.24 118.71,204.97 118.71,183.56C118.71,162.15 112.23,154.96 100.78,154.96C89.32,154.96 82.84,162.15 82.84,183.56C82.84,204.97 89.32,212.24 100.78,212.24ZM100.78,206.63C93.59,206.63 89.8,201.26 89.8,183.56C89.8,165.86 93.59,160.57 100.78,160.57C107.97,160.57 111.76,165.86 111.76,183.56C111.76,201.26 107.97,206.63 100.78,206.63Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M159.35,119.95V65.03H153.82C152.63,70.64 148.36,73.33 143.78,73.33H142.44V79.41H144.57C147.18,79.41 151.05,78.47 152.79,76.33V119.95H159.35Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M186.51,119.95V65.03H180.98C179.79,70.64 175.52,73.33 170.94,73.33H169.6V79.41H171.73C174.34,79.41 178.21,78.47 179.95,76.33V119.95H186.51Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M544.27,327.82C554.3,327.82 560.31,322.21 560.31,311.86C560.31,304.04 556.75,298.9 550.2,297.8C555.8,296.38 559.2,291.24 559.2,284.52C559.2,275.28 553.67,270.54 544.27,270.54C533.84,270.54 528.39,277.02 528.31,287.61L535.1,288.63C535.18,280.02 537.95,276.23 544.11,276.23C549.17,276.23 552.17,279.07 552.17,285C552.17,291.55 549.09,295.27 543.32,295.27H540.79V300.96H543.56C549.88,300.96 553.28,304.75 553.28,311.94C553.28,318.81 550.12,322.13 544.27,322.13C538.34,322.13 534.79,318.02 534.71,309.81L527.76,310.6C527.91,321.97 533.92,327.82 544.27,327.82Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M50.58,270.54C40.54,270.54 34.62,277.57 34.62,288.55C34.62,300.09 40.3,306.33 49.31,306.33C54.45,306.33 58.71,303.96 60.93,299.38C60.93,317.39 57.69,322.29 50.73,322.29C45.52,322.29 42.75,318.73 42.12,312.41L35.17,313.76C36.27,322.92 41.57,327.82 50.58,327.82C61.4,327.82 67.25,321.11 67.25,298.51C67.25,276.54 61.32,270.54 50.58,270.54ZM50.73,300.8C45.04,300.8 41.57,296.3 41.57,288.39C41.57,280.49 44.89,276.07 50.58,276.07C57.21,276.07 60.14,279.7 60.77,290.21C59.66,296.93 55.79,300.8 50.73,300.8Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,49 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600"
tools:ignore="VectorRaster">
<path
android:fillColor="#FF000000"
android:pathData="M300,300m-300,0a300,300 0,1 1,600 0a300,300 0,1 1,-600 0"
android:strokeAlpha="0.5"
android:fillAlpha="0.5"/>
<path
android:pathData="M291,42h20v64h-20z"
android:fillColor="#fff"/>
<path
android:pathData="M291,494h20v64h-20z"
android:fillColor="#fff"/>
<path
android:pathData="M389.33,126.99l32,-55.43l17.32,10l-32,55.43z"
android:fillColor="#fff"/>
<path
android:pathData="M163.34,518.44l32,-55.43l17.32,10l-32,55.43z"
android:fillColor="#fff"/>
<path
android:pathData="M464.01,194.34l55.43,-32l10,17.32l-55.43,32z"
android:fillColor="#fff"/>
<path
android:pathData="M72.56,420.34l55.43,-32l10,17.32l-55.43,32z"
android:fillColor="#fff"/>
<path
android:pathData="M464.01,405.66l10,-17.32l55.43,32l-10,17.32z"
android:fillColor="#fff"/>
<path
android:pathData="M72.56,179.66l10,-17.32l55.43,32l-10,17.32z"
android:fillColor="#fff"/>
<path
android:pathData="M389.34,473.01l17.32,-10l32,55.43l-17.32,10z"
android:fillColor="#fff"/>
<path
android:pathData="M163.34,81.56l17.32,-10l32,55.43l-17.32,10z"
android:fillColor="#fff"/>
<path
android:pathData="M42,290h64v20h-64z"
android:fillColor="#fff"/>
<path
android:pathData="M494.53,290h64v20h-64z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,58 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600"
tools:ignore="VectorRaster">
<path
android:pathData="M300,300m-300,0a300,300 0,1 1,600 0a300,300 0,1 1,-600 0"
android:strokeAlpha="0.3"
android:fillColor="#fff"
android:fillAlpha="0.3"/>
<path
android:pathData="m524.43,171h0c2.76,4.78 1.12,10.9 -3.66,13.66l-17.32,10c-4.78,2.76 -10.9,1.12 -13.66,-3.66h0c-2.76,-4.78 -1.12,-10.9 3.66,-13.66l17.32,-10c4.78,-2.76 10.9,-1.12 13.66,3.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m112.21,409h0c2.76,4.78 1.12,10.9 -3.66,13.66l-17.32,10c-4.78,2.76 -10.9,1.12 -13.66,-3.66h0c-2.76,-4.78 -1.12,-10.9 3.66,-13.66l17.32,-10c4.78,-2.76 10.9,-1.12 13.66,3.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m430,76.57h0c4.78,2.76 6.42,8.88 3.66,13.66l-10,17.32c-2.76,4.78 -8.88,6.42 -13.66,3.66h0c-4.78,-2.76 -6.42,-8.88 -3.66,-13.66l10,-17.32c2.76,-4.78 8.88,-6.42 13.66,-3.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m192,488.79h0c4.78,2.76 6.42,8.88 3.66,13.66l-10,17.32c-2.76,4.78 -8.88,6.42 -13.66,3.66h0c-4.78,-2.76 -6.42,-8.88 -3.66,-13.66l10,-17.32c2.76,-4.78 8.88,-6.42 13.66,-3.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m172,76.57h0c4.78,-2.76 10.9,-1.12 13.66,3.66l10,17.32c2.76,4.78 1.12,10.9 -3.66,13.66h0c-4.78,2.76 -10.9,1.12 -13.66,-3.66l-10,-17.32c-2.76,-4.78 -1.12,-10.9 3.66,-13.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m410,488.79h0c4.78,-2.76 10.9,-1.12 13.66,3.66l10,17.32c2.76,4.78 1.12,10.9 -3.66,13.66h0c-4.78,2.76 -10.9,1.12 -13.66,-3.66l-10,-17.32c-2.76,-4.78 -1.12,-10.9 3.66,-13.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m77.57,171h0c2.76,-4.78 8.88,-6.42 13.66,-3.66l17.32,10c4.78,2.76 6.42,8.88 3.66,13.66h0c-2.76,4.78 -8.88,6.42 -13.66,3.66l-17.32,-10c-4.78,-2.76 -6.42,-8.88 -3.66,-13.66Z"
android:fillColor="#fff"/>
<path
android:pathData="m489.79,409h0c2.76,-4.78 8.88,-6.42 13.66,-3.66l17.32,10c4.78,2.76 6.42,8.88 3.66,13.66h0c-2.76,4.78 -8.88,6.42 -13.66,3.66l-17.32,-10c-4.78,-2.76 -6.42,-8.88 -3.66,-13.66Z"
android:fillColor="#fff"/>
<path
android:pathData="M300,300m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
android:fillColor="#ff0000"/>
<path
android:pathData="M300,300m-22,0a22,22 0,1 1,44 0a22,22 0,1 1,-44 0"
android:fillColor="#fff"/>
<path
android:pathData="m285.47,80.64V26.17h-7.29c-1.49,5.49 -5.49,8.15 -10.42,8.15h-1.33v8.15h1.96c2.9,0 6.51,-0.86 8.23,-2.82v40.99h8.86Z"
android:fillColor="#fff"/>
<path
android:pathData="m328.61,80.64v-7.52h-22.34c5.96,-5.41 10.35,-9.64 13.95,-14.11 5.33,-6.27 7.92,-12.38 7.92,-18.73 0,-10.27 -5.8,-15.28 -16.22,-15.28s-16.77,5.72 -16.77,15.13c0,1.49 0.16,2.9 0.47,4.31l9.25,1.02c-0.31,-1.72 -0.47,-3.45 -0.47,-5.02 0,-5.17 2.59,-8.15 7.13,-8.15 4.47,0 6.97,2.82 6.97,8.31 0,5.41 -1.88,10.27 -6.43,15.75 -4.47,5.33 -10.11,10.74 -17.24,17.32v6.97h33.78Z"
android:fillColor="#fff"/>
<path
android:pathData="m300.28,536.57c-5.45,0 -9.24,2.29 -11.46,6.24 0.16,-12.88 3.16,-18.41 9.09,-18.41 4.27,0 6.48,2.77 7.11,8.45l9.09,-1.42c-0.95,-9.4 -6.4,-14.06 -16.04,-14.06 -11.61,0 -18.25,8.14 -18.25,28.76 0,20.31 6.01,28.52 18.09,28.52 10.43,0 16.83,-7.43 16.83,-19.67 0,-11.93 -5.61,-18.41 -14.46,-18.41ZM297.75,567.54c-5.29,0 -8.53,-4.43 -8.93,-16.04 1.5,-4.98 4.42,-7.9 8.85,-7.9 4.98,0 7.9,4.11 7.9,11.69 0,8.22 -2.92,12.25 -7.82,12.25Z"
android:fillColor="#fff"/>
<path
android:pathData="m553.66,329.74c10.75,0 17.15,-5.85 17.15,-16.2 0,-7.59 -3.56,-12.56 -10.35,-13.67 5.85,-1.42 9.4,-6.56 9.4,-13.12 0,-9.4 -5.93,-14.3 -16.12,-14.3 -10.98,0 -17.07,6.32 -17.15,17.22l9.09,1.26c0.08,-7.98 2.53,-11.3 7.82,-11.3 4.43,0 6.95,2.45 6.95,7.9 0,5.85 -2.77,9.24 -7.98,9.24h-2.45v6.95h2.61c5.77,0 8.77,3.24 8.77,9.72 0,6.01 -2.77,9.01 -7.74,9.01 -5.14,0 -8.22,-3.71 -8.22,-11.3l-9.32,1.03c0.16,11.61 6.56,17.54 17.54,17.54Z"
android:fillColor="#fff"/>
<path
android:pathData="m45.99,272.46c-10.74,0 -16.99,7.19 -16.99,18.33 0,11.38 5.77,17.78 15.01,17.78 4.82,0 8.93,-1.98 11.14,-6.01 -0.16,15.88 -3,20.07 -8.93,20.07 -4.5,0 -7.03,-3.24 -7.59,-8.85l-9.01,1.58c0.95,9.24 6.64,14.38 16.28,14.38 11.69,0 17.86,-6.72 17.86,-29.15 0,-22.04 -6.16,-28.13 -17.78,-28.13ZM46.15,301.62c-4.98,0 -8.06,-4.11 -8.06,-11.06 0,-7.27 2.92,-11.14 7.98,-11.14 5.93,0 8.53,3.32 9.01,13.75 -1.26,5.45 -4.66,8.45 -8.93,8.45Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,121 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="600dp"
android:height="600dp"
android:viewportWidth="600"
android:viewportHeight="600"
tools:ignore="VectorRaster">
<path
android:fillColor="#FF000000"
android:pathData="M300,300m-300,0a300,300 0,1 1,600 0a300,300 0,1 1,-600 0"
android:strokeAlpha="0.8"
android:fillAlpha="0.8"/>
<path
android:pathData="M288,488L312,488A2,2 0,0 1,314 490L314,566A2,2 0,0 1,312 568L288,568A2,2 0,0 1,286 566L286,490A2,2 0,0 1,288 488z"
android:fillColor="#fff"/>
<path
android:pathData="M288,488L312,488A2,2 0,0 1,314 490L314,566A2,2 0,0 1,312 568L288,568A2,2 0,0 1,286 566L286,490A2,2 0,0 1,288 488z"
android:fillColor="#fff"/>
<path
android:pathData="M288,488L312,488A2,2 0,0 1,314 490L314,566A2,2 0,0 1,312 568L288,568A2,2 0,0 1,286 566L286,490A2,2 0,0 1,288 488z"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M34,286L110,286A2,2 0,0 1,112 288L112,312A2,2 0,0 1,110 314L34,314A2,2 0,0 1,32 312L32,288A2,2 0,0 1,34 286z"
android:fillColor="#fff"/>
<path
android:pathData="M34,286L110,286A2,2 0,0 1,112 288L112,312A2,2 0,0 1,110 314L34,314A2,2 0,0 1,32 312L32,288A2,2 0,0 1,34 286z"
android:fillColor="#fff"/>
<path
android:pathData="M34,286L110,286A2,2 0,0 1,112 288L112,312A2,2 0,0 1,110 314L34,314A2,2 0,0 1,32 312L32,288A2,2 0,0 1,34 286z"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M490,286L566,286A2,2 0,0 1,568 288L568,312A2,2 0,0 1,566 314L490,314A2,2 0,0 1,488 312L488,288A2,2 0,0 1,490 286z"
android:fillColor="#fff"/>
<path
android:pathData="M490,286L566,286A2,2 0,0 1,568 288L568,312A2,2 0,0 1,566 314L490,314A2,2 0,0 1,488 312L488,288A2,2 0,0 1,490 286z"
android:fillColor="#fff"/>
<path
android:pathData="M490,286L566,286A2,2 0,0 1,568 288L568,312A2,2 0,0 1,566 314L490,314A2,2 0,0 1,488 312L488,288A2,2 0,0 1,490 286z"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M424.05,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M424.05,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M424.05,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M178.03,513.06m-11.94,20.68a23.88,23.88 102.59,1 1,23.88 -41.35a23.88,23.88 102.59,1 1,-23.88 41.35"
android:fillColor="#fff"/>
<path
android:pathData="M178.03,513.06m-11.94,20.68a23.88,23.88 102.59,1 1,23.88 -41.35a23.88,23.88 102.59,1 1,-23.88 41.35"
android:fillColor="#fff"/>
<path
android:pathData="M178.03,513.06m-11.94,20.68a23.88,23.88 102.59,1 1,23.88 -41.35a23.88,23.88 102.59,1 1,-23.88 41.35"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M514.1,176.98m-22.32,8.47a23.88,23.88 127.36,1 1,44.64 -16.94a23.88,23.88 127.36,1 1,-44.64 16.94"
android:fillColor="#fff"/>
<path
android:pathData="M514.1,176.98m-22.32,8.47a23.88,23.88 127.36,1 1,44.64 -16.94a23.88,23.88 127.36,1 1,-44.64 16.94"
android:fillColor="#fff"/>
<path
android:pathData="M514.1,176.98m-22.32,8.47a23.88,23.88 127.36,1 1,44.64 -16.94a23.88,23.88 127.36,1 1,-44.64 16.94"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M87.98,423.01m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M87.98,423.01m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M87.98,423.01m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M514.09,423m-15.1,18.5a23.88,23.88 76.42,1 1,30.19 -36.99a23.88,23.88 76.42,1 1,-30.19 36.99"
android:fillColor="#fff"/>
<path
android:pathData="M514.09,423m-15.1,18.5a23.88,23.88 76.42,1 1,30.19 -36.99a23.88,23.88 76.42,1 1,-30.19 36.99"
android:fillColor="#fff"/>
<path
android:pathData="M514.09,423m-15.1,18.5a23.88,23.88 76.42,1 1,30.19 -36.99a23.88,23.88 76.42,1 1,-30.19 36.99"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M87.98,176.99m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M87.98,176.99m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M87.98,176.99m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M424.05,513.06m-20.68,11.94a23.88,23.88 77.26,1 1,41.35 -23.88a23.88,23.88 77.26,1 1,-41.35 23.88"
android:fillColor="#fff"/>
<path
android:pathData="M424.05,513.06m-20.68,11.94a23.88,23.88 77.26,1 1,41.35 -23.88a23.88,23.88 77.26,1 1,-41.35 23.88"
android:fillColor="#fff"/>
<path
android:pathData="M424.05,513.06m-20.68,11.94a23.88,23.88 77.26,1 1,41.35 -23.88a23.88,23.88 77.26,1 1,-41.35 23.88"
android:fillColor="#d1ffc1"/>
<path
android:pathData="M178.03,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M178.03,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#fff"/>
<path
android:pathData="M178.03,86.94m-23.88,0a23.88,23.88 0,1 1,47.75 0a23.88,23.88 0,1 1,-47.75 0"
android:fillColor="#d1ffc1"/>
<path
android:pathData="m301.83,115.87c-0.7,1.59 -2.95,1.59 -3.66,0l-32.44,-73.31c-0.58,-1.32 0.38,-2.81 1.83,-2.81h64.87c1.45,0 2.41,1.49 1.83,2.81l-32.44,73.31Z"
android:fillColor="#fff"/>
<path
android:pathData="m301.83,115.87c-0.7,1.59 -2.95,1.59 -3.66,0l-32.44,-73.31c-0.58,-1.32 0.38,-2.81 1.83,-2.81h64.87c1.45,0 2.41,1.49 1.83,2.81l-32.44,73.31Z"
android:fillColor="#fff"/>
<path
android:pathData="m301.83,115.87c-0.7,1.59 -2.95,1.59 -3.66,0l-32.44,-73.31c-0.58,-1.32 0.38,-2.81 1.83,-2.81h64.87c1.45,0 2.41,1.49 1.83,2.81l-32.44,73.31Z"
android:fillColor="#d1ffc1"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="200dp"
android:viewportWidth="14"
android:viewportHeight="200">
<path
android:pathData="M7,0L7,0A7,7 0,0 1,14 7L14,193A7,7 0,0 1,7 200L7,200A7,7 0,0 1,0 193L0,7A7,7 0,0 1,7 0z"
android:fillColor="#FFBE20"/>
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="40dp"
android:height="210dp"
android:viewportWidth="40"
android:viewportHeight="210"
tools:ignore="VectorRaster">
<path
android:pathData="m5,0L0,210h40L35,0L5,0ZM20,176c8.84,0 16,-7.16 16,-16s-7.16,-16 -16,-16 -16,7.16 -16,16 7.16,16 16,16Z"
android:strokeAlpha="0.98"
android:fillColor="#fff"
android:fillType="evenOdd"
android:fillAlpha="0.98"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="149dp"
android:viewportWidth="20"
android:viewportHeight="149">
<path
android:pathData="m10,149C4.48,149 0,144.52 0,139V10C0,4.48 4.48,0 10,0h0c5.52,0 10,4.48 10,10v129c0,5.52 -4.48,10 -10,10h0Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="138dp"
android:viewportWidth="28"
android:viewportHeight="138">
<path
android:pathData="m27.65,18.84c0.23,0.33 0.35,0.73 0.35,1.13v118.03H0V19.97c0,-0.41 0.12,-0.8 0.35,-1.13L12.35,1.4c0.8,-1.15 2.5,-1.15 3.3,0l12,17.44Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:aapt="http://schemas.android.com/aapt"
xmlns:tools="http://schemas.android.com/tools"
android:viewportWidth="10"
android:viewportHeight="280"
android:width="10dp"
android:height="280dp"
tools:ignore="VectorRaster">
<path
android:pathData="M5 280C2.239 280 0 278.01 0 275.556V4.444C0 1.99 2.239 0 5 0h0c2.761 0 5 1.99 5 4.444v271.112c0 2.454 -2.239 4.444 -5 4.444h0Z"
android:fillColor="#FFBE20" />
</vector>

View file

@ -0,0 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="40dp"
android:height="308dp"
android:viewportWidth="40"
android:viewportHeight="308"
tools:ignore="VectorRaster">
<path
android:pathData="m5.76,0L0,308h40L34.24,0L5.76,0ZM20,273.94c8.84,0 16,-7.16 16,-16s-7.16,-16 -16,-16 -16,7.16 -16,16 7.16,16 16,16Z"
android:strokeAlpha="0.98"
android:fillColor="#fff"
android:fillType="evenOdd"
android:fillAlpha="0.98"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="20dp"
android:height="229dp"
android:viewportWidth="20"
android:viewportHeight="229"
tools:ignore="VectorRaster">
<path
android:pathData="m10,229C4.48,229 0,224.52 0,219V10C0,4.48 4.48,0 10,0h0c5.52,0 10,4.48 10,10v209c0,5.52 -4.48,10 -10,10h0Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="28dp"
android:height="267dp"
android:viewportWidth="28"
android:viewportHeight="267"
tools:ignore="VectorRaster">
<path
android:pathData="m27.67,19.52c0.22,0.33 0.34,0.71 0.34,1.11v246.37H0V20.63c0,-0.4 0.12,-0.78 0.34,-1.11L12.34,1.5c0.79,-1.19 2.54,-1.19 3.33,0l12,18.02Z"
android:fillColor="#fff"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="72dp"
android:height="36dp"
android:viewportWidth="72"
android:viewportHeight="36">
<path
android:pathData="M5.275,0L66.725,0A5.275,5.275 0,0 1,72 5.275L72,30.725A5.275,5.275 0,0 1,66.725 36L5.275,36A5.275,5.275 0,0 1,0 30.725L0,5.275A5.275,5.275 0,0 1,5.275 0z"
android:fillColor="#ffffff"/>
<path
android:pathData="M15.385,29.886C18.277,28.09 24.717,21.282 24.717,16.007C24.717,10.48 20.365,6 14.996,6C9.628,6 5.275,10.48 5.275,16.007C5.275,21.282 11.715,28.09 14.607,29.886C14.849,30.036 15.144,30.036 15.385,29.886ZM14.996,19.646C16.948,19.646 18.531,18.017 18.531,16.007C18.531,13.997 16.948,12.368 14.996,12.368C13.043,12.368 11.461,13.997 11.461,16.007C11.461,18.017 13.043,19.646 14.996,19.646Z"
android:fillColor="#F44336"
android:fillType="evenOdd"/>
</vector>

View file

@ -1,10 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.emoji.MediaKeyboard xmlns:android="http://schemas.android.com/apk/res/android"
<fragment
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/emoji_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:name="org.thoughtcrime.securesms.scribbles.stickers.ScribbleStickersFragment"
android:background="@color/core_grey_90"
app:tabs_gravity="top" />

View file

@ -4478,6 +4478,9 @@
<string name="ImageEditorHud__toggle_between_marker_and_highlighter">Toggle between marker and highlighter</string>
<string name="ImageEditorHud__toggle_between_text_styles">Toggle between text styles</string>
<!-- Header for section of featured stickers (location/time stickers) -->
<string name="ScribbleStickersFragment__featured_stickers">Featured</string>
<string name="MediaCountIndicatorButton__send">Send</string>
<string name="MediaReviewSelectedItem__tap_to_remove">Tap to remove</string>

View file

@ -10,7 +10,7 @@ import java.lang.ref.WeakReference;
/**
* Maintains a weak reference to the an invalidate callback allowing future invalidation without memory leak risk.
*/
abstract class InvalidateableRenderer implements Renderer {
public abstract class InvalidateableRenderer implements Renderer {
private WeakReference<RendererContext.Invalidate> invalidate = new WeakReference<>(null);