From 28f27915c5fbad460a187014d84950627691d1d1 Mon Sep 17 00:00:00 2001 From: Clark Date: Thu, 30 Mar 2023 11:26:56 -0400 Subject: [PATCH] Add support for time stickers in image editor. --- .../main/assets/fonts/Hatsuishi-Regular.otf | Bin 0 -> 2700 bytes .../sticker/KeyboardStickerListAdapter.kt | 3 +- .../sticker/StickerKeyboardPageFragment.kt | 18 +- .../scribbles/ImageEditorFragment.java | 30 +- .../ImageEditorStickerSelectActivity.java | 24 +- .../stickers/AnalogClockStickerDrawable.kt | 222 +++++++++++++++ .../stickers/AnalogClockStickerRenderer.kt | 74 +++++ .../stickers/DigitalClockStickerDrawable.kt | 262 ++++++++++++++++++ .../stickers/DigitalClockStickerRenderer.kt | 77 +++++ .../scribbles/stickers/FeatureSticker.kt | 14 + .../stickers/ScribbleStickersFragment.kt | 126 +++++++++ .../scribbles/stickers/TappableRenderer.kt | 10 + .../res/drawable/clock_center_cover_4.xml | 15 + app/src/main/res/drawable/clock_face_1.xml | 64 +++++ app/src/main/res/drawable/clock_face_2.xml | 49 ++++ app/src/main/res/drawable/clock_face_3.xml | 58 ++++ app/src/main/res/drawable/clock_face_4.xml | 121 ++++++++ .../main/res/drawable/clock_hour_hand_1.xml | 9 + .../main/res/drawable/clock_hour_hand_2.xml | 14 + .../main/res/drawable/clock_hour_hand_3.xml | 9 + .../main/res/drawable/clock_hour_hand_4.xml | 9 + .../main/res/drawable/clock_minute_hand_1.xml | 11 + .../main/res/drawable/clock_minute_hand_2.xml | 14 + .../main/res/drawable/clock_minute_hand_3.xml | 11 + .../main/res/drawable/clock_minute_hand_4.xml | 11 + .../main/res/drawable/ic_location_sticker.xml | 13 + .../scribble_select_new_sticker_activity.xml | 4 +- app/src/main/res/values/strings.xml | 3 + .../renderers/InvalidateableRenderer.java | 2 +- 29 files changed, 1254 insertions(+), 23 deletions(-) create mode 100644 app/src/main/assets/fonts/Hatsuishi-Regular.otf create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerDrawable.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerRenderer.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerDrawable.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerRenderer.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/FeatureSticker.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/ScribbleStickersFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/TappableRenderer.kt create mode 100644 app/src/main/res/drawable/clock_center_cover_4.xml create mode 100644 app/src/main/res/drawable/clock_face_1.xml create mode 100644 app/src/main/res/drawable/clock_face_2.xml create mode 100644 app/src/main/res/drawable/clock_face_3.xml create mode 100644 app/src/main/res/drawable/clock_face_4.xml create mode 100644 app/src/main/res/drawable/clock_hour_hand_1.xml create mode 100644 app/src/main/res/drawable/clock_hour_hand_2.xml create mode 100644 app/src/main/res/drawable/clock_hour_hand_3.xml create mode 100644 app/src/main/res/drawable/clock_hour_hand_4.xml create mode 100644 app/src/main/res/drawable/clock_minute_hand_1.xml create mode 100644 app/src/main/res/drawable/clock_minute_hand_2.xml create mode 100644 app/src/main/res/drawable/clock_minute_hand_3.xml create mode 100644 app/src/main/res/drawable/clock_minute_hand_4.xml create mode 100644 app/src/main/res/drawable/ic_location_sticker.xml diff --git a/app/src/main/assets/fonts/Hatsuishi-Regular.otf b/app/src/main/assets/fonts/Hatsuishi-Regular.otf new file mode 100644 index 0000000000000000000000000000000000000000..dd49000cf827ca90e34567aadcd14cc40949703b GIT binary patch literal 2700 zcmb_de`s4(6h8O8ymakK+U7MPtku^Ne;_WVq3zmLVRmDyR##cqb%?meEX_;TG+pz$ zc9h_RiA9U<4@F`Jt;}trOW0(aV-&P1vO!rkTZ$9baX!CQL6(*XtEpkJ-(245Z-}H=>HV%pHThYy~@{H#!>zZXQPS=X&C-M`+D3v z)sQwSYr+kb|H9c6@{SIIja&io!8+g#`QgW_f1>>b0O5_na70UA01w7LisuRd+S*Ve zPB>4z_wDz4${zj=MDaRm;Ja#gH}|ap#`=VA+>0QP7RO$6I|BEzBMc|phjUYAPdanv z_AmjpIBNvT6tTqX=_b>=25MQK$-oTB7aLfZ4`WEMMtIruY0rGwv*2M5l9ja7eu>=G z-n|_@#_TqV3SwNsT(F`U1a?E9$|$CJIegA>85CzNFt;_hLxwm+sD=tITfh!3E=yR$ zOtQNQAGHMklZ8k!q(F>+BmePK;gTK!(_AhdxOG>E`3 z1aVby^}r7BLjX1Xcs2xeSVXt^Yk@9khi=#oEd~76ukiB}uu<}Cb|KafA_pN11K8%J zXe#P7@R=tEa%V2IaYHk52qC|H=ucL?_#MTmAb%G$;B3UT0qq(K@W|)dY2s)YNr6_3 zLscnMo#nN`he?B8mx!~HI&FR}fNup#@Ma45Q;<$GY)q$7N3xsb^v~&U($~^o@SBK7 zI4BB^^f`L-`GbK)z*o)W{HBuno>Up6*0EHTkh;@S1vOQ`s#sr@&@Jg=nO=i3A4%xrn)|MlZ0U7|H5w8DX7pRIC?Y zusmRyv3w|BAI$F8I9S`T-}z`JCgF8UXnWL(3`BXb9Ppew?t!-F>aObk4B}} zjbn~@T+%lc$@M#DQDFp%^mD2sk&yI2Q8d=oB^h`&VZ3O3=|E>R?2D_aX?8j4h{ecE zP4orH%;;}%=;PK{tZP;>nylz+JCXX3=@*ltepCO|u_WTDpp-C#iG+L}xyg%&_q?&) zv4nmsD(P?U#|*M$QL&r=@|G7e@N)#ez~SczeqnX^vjo4u;m;EM0>bMr83$J6yMHCT zK84w1+o)-$f7gE@zS*uMR7pRVD3I+S!JX!Cg9LZFFx>L&GARF8-a|(FN(Q~FmiOP} zx2(U}+c}>paWR#+f|t02N^Hjxa|&{0whVGB6@qBgP~NRP?^??H)52Z89J^iJQzz-d zQNO1%WO|6qG$+$TWTp%E`*xT3KPI_()ozG*Xhq4e!M{ZdMQ*R5V; zJFzOo+NF(gQe!?Td?EAhf7|hS7M~Zd9d}GkPEJjYO^%F=jg5>d q=EbXeD*xPQu;lxss%>{oZ%CIGzf!t3<*d|w4*XkA{~2x#bN4q6-8PW` literal 0 HcmV?d00001 diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt index 2032fdfb8e..364a359833 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/KeyboardStickerListAdapter.kt @@ -62,7 +62,7 @@ class KeyboardStickerListAdapter( } } - data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel, HasPackId { + data class StickerHeader(override val packId: String, private val title: String?, private val titleResource: Int?) : MappingModel, 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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt index 9f62203e71..a4223803f8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt @@ -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> = 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) diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java index 17e1974674..d47c9f537b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorFragment.java @@ -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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorStickerSelectActivity.java b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorStickerSelectActivity.java index fa283071b9..85347e5b98 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorStickerSelectActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/ImageEditorStickerSelectActivity.java @@ -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(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerDrawable.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerDrawable.kt new file mode 100644 index 0000000000..8937a311b9 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerDrawable.kt @@ -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 } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerRenderer.kt new file mode 100644 index 0000000000..28c9c2e697 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/AnalogClockStickerRenderer.kt @@ -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 { + override fun createFromParcel(parcel: Parcel): AnalogClockStickerRenderer { + return AnalogClockStickerRenderer(parcel.readLong(), AnalogClockStickerDrawable.Style.fromType(parcel.readInt())) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerDrawable.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerDrawable.kt new file mode 100644 index 0000000000..233b630dd4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerDrawable.kt @@ -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 } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerRenderer.kt new file mode 100644 index 0000000000..ea7b86cd33 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/DigitalClockStickerRenderer.kt @@ -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 { + override fun createFromParcel(parcel: Parcel): DigitalClockStickerRenderer { + return DigitalClockStickerRenderer( + parcel.readLong(), + DigitalClockStickerDrawable.Style.fromType(parcel.readInt()) + ) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/FeatureSticker.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/FeatureSticker.kt new file mode 100644 index 0000000000..b077467055 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/FeatureSticker.kt @@ -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 } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/ScribbleStickersFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/ScribbleStickersFragment.kt new file mode 100644 index 0000000000..2f6f009c08 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/ScribbleStickersFragment.kt @@ -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()?.onFeatureSticker(featureSticker) } + } + + data class FeatureStickerModel(val featureSticker: FeatureSticker) : MappingModel { + + 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(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, 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(itemView) { + + private val title: TextView = findViewById(R.id.sticker_grid_header_title) + + override fun bind(model: FeatureHeader) { + title.text = model.getTitle(context) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/TappableRenderer.kt b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/TappableRenderer.kt new file mode 100644 index 0000000000..7d03ce384c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/scribbles/stickers/TappableRenderer.kt @@ -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() +} diff --git a/app/src/main/res/drawable/clock_center_cover_4.xml b/app/src/main/res/drawable/clock_center_cover_4.xml new file mode 100644 index 0000000000..eb66c875f5 --- /dev/null +++ b/app/src/main/res/drawable/clock_center_cover_4.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/clock_face_1.xml b/app/src/main/res/drawable/clock_face_1.xml new file mode 100644 index 0000000000..cc5fc88e4c --- /dev/null +++ b/app/src/main/res/drawable/clock_face_1.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/clock_face_2.xml b/app/src/main/res/drawable/clock_face_2.xml new file mode 100644 index 0000000000..a84c8e9aec --- /dev/null +++ b/app/src/main/res/drawable/clock_face_2.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/clock_face_3.xml b/app/src/main/res/drawable/clock_face_3.xml new file mode 100644 index 0000000000..466630bf3a --- /dev/null +++ b/app/src/main/res/drawable/clock_face_3.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/clock_face_4.xml b/app/src/main/res/drawable/clock_face_4.xml new file mode 100644 index 0000000000..61bcf788e3 --- /dev/null +++ b/app/src/main/res/drawable/clock_face_4.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/clock_hour_hand_1.xml b/app/src/main/res/drawable/clock_hour_hand_1.xml new file mode 100644 index 0000000000..a2cc20e66b --- /dev/null +++ b/app/src/main/res/drawable/clock_hour_hand_1.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/clock_hour_hand_2.xml b/app/src/main/res/drawable/clock_hour_hand_2.xml new file mode 100644 index 0000000000..7fd583af93 --- /dev/null +++ b/app/src/main/res/drawable/clock_hour_hand_2.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/drawable/clock_hour_hand_3.xml b/app/src/main/res/drawable/clock_hour_hand_3.xml new file mode 100644 index 0000000000..7b7a1d7153 --- /dev/null +++ b/app/src/main/res/drawable/clock_hour_hand_3.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/clock_hour_hand_4.xml b/app/src/main/res/drawable/clock_hour_hand_4.xml new file mode 100644 index 0000000000..1ea5806d48 --- /dev/null +++ b/app/src/main/res/drawable/clock_hour_hand_4.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/clock_minute_hand_1.xml b/app/src/main/res/drawable/clock_minute_hand_1.xml new file mode 100644 index 0000000000..d90c3ba5bf --- /dev/null +++ b/app/src/main/res/drawable/clock_minute_hand_1.xml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/clock_minute_hand_2.xml b/app/src/main/res/drawable/clock_minute_hand_2.xml new file mode 100644 index 0000000000..43959a3a4d --- /dev/null +++ b/app/src/main/res/drawable/clock_minute_hand_2.xml @@ -0,0 +1,14 @@ + + + diff --git a/app/src/main/res/drawable/clock_minute_hand_3.xml b/app/src/main/res/drawable/clock_minute_hand_3.xml new file mode 100644 index 0000000000..24955fe41e --- /dev/null +++ b/app/src/main/res/drawable/clock_minute_hand_3.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/clock_minute_hand_4.xml b/app/src/main/res/drawable/clock_minute_hand_4.xml new file mode 100644 index 0000000000..d66a6784d5 --- /dev/null +++ b/app/src/main/res/drawable/clock_minute_hand_4.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_sticker.xml b/app/src/main/res/drawable/ic_location_sticker.xml new file mode 100644 index 0000000000..b920d656f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_location_sticker.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/layout/scribble_select_new_sticker_activity.xml b/app/src/main/res/layout/scribble_select_new_sticker_activity.xml index 133514f87d..acf3ef046f 100644 --- a/app/src/main/res/layout/scribble_select_new_sticker_activity.xml +++ b/app/src/main/res/layout/scribble_select_new_sticker_activity.xml @@ -1,10 +1,12 @@ - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2d472f94fc..0ebc67c1ec 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4478,6 +4478,9 @@ Toggle between marker and highlighter Toggle between text styles + + Featured + Send Tap to remove diff --git a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/InvalidateableRenderer.java b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/InvalidateableRenderer.java index 72b9a7ca11..45e05de868 100644 --- a/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/InvalidateableRenderer.java +++ b/image-editor/lib/src/main/java/org/signal/imageeditor/core/renderers/InvalidateableRenderer.java @@ -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 invalidate = new WeakReference<>(null);