diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f07c92..1e41bc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ -Last updated: 2023-11-25 +Last updated: 2024-03-25 + +- v3.0.2-alpha + - Use single animator to help avoid data races/bugs + +- v3.0.1-alpha + - Use LruCache instead of BitmapCache + +- v3.0.0-alpha + - Dynamic layouts - v2.12.1 - Remove heartrate complication prefix style diff --git a/app/build.gradle b/app/build.gradle index 818d6dd..2a02e41 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -27,8 +27,8 @@ android { applicationId "dev.rdnt.m8face" minSdk 28 targetSdk 33 - versionCode 56 - versionName '2.12.1' + versionCode 62 + versionName '3.0.5' } buildFeatures { @@ -51,6 +51,7 @@ android { minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' versionNameSuffix '-preview' + signingConfig signingConfigs.debug } release { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ef19fa..20859cf 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - - + xmlns:tools="http://schemas.android.com/tools"> - + - - - - - - + + + + + + - + - + - - - - - + + + + + - - - + + + - + - - + + - + - - + + - - - - - + + + + + diff --git a/app/src/main/java/dev/rdnt/m8face/BitmapCache.kt b/app/src/main/java/dev/rdnt/m8face/BitmapCache.kt new file mode 100644 index 0000000..624f911 --- /dev/null +++ b/app/src/main/java/dev/rdnt/m8face/BitmapCache.kt @@ -0,0 +1,68 @@ +package dev.rdnt.m8face + +import android.graphics.Bitmap +import android.util.Log + +//const val HOURS_BITMAP_KEY = "hours" +//const val MINUTES_BITMAP_KEY = "minutes" +//const val SECONDS_BITMAP_KEY = "seconds" +//const val AMPM_BITMAP_KEY = "ampm" +// +//const val TIME_BITMAP_KEY = "time" +//const val AUX_BITMAP_KEY = "aux" + +class BitmapCacheOld { + private val entries: MutableMap = mutableMapOf() + + override fun toString(): String { + //loop entries + var result = "" + entries.forEach { (k, v) -> result += "{$k: $v} " } + return "(entries= $result)" + } + + fun get(k: String, h: String): Bitmap? { + return entries[k]?.get(h) + } + + fun set(k: String, h: String, b: Bitmap?) { + val entry = entries.getOrPut(k) { BitmapCacheEntry() } + entry.set(h, b) + } + + fun renders(k: String): Int { + return entries[k]?.renders ?: 1 + } + + fun loads(k: String): Int { + return entries[k]?.loads ?: 1 + } +} + +class BitmapCacheEntry { + var renders: Int = 1 + var loads: Int = 1 + private var hash: String = "" + private var bitmap: Bitmap? = null + + override fun toString(): String { + return "(renders=$renders, loads=$loads)" + } + + fun get(h: String): Bitmap? { + loads++ + + if (bitmap != null && h == hash) { + return bitmap + } + + return null + } + + fun set(h: String, b: Bitmap?) { + renders++ + + hash = h + bitmap = b + } +} diff --git a/app/src/main/java/dev/rdnt/m8face/WatchCanvasRenderer.kt b/app/src/main/java/dev/rdnt/m8face/WatchCanvasRenderer.kt index 3099c7e..a136fe2 100644 --- a/app/src/main/java/dev/rdnt/m8face/WatchCanvasRenderer.kt +++ b/app/src/main/java/dev/rdnt/m8face/WatchCanvasRenderer.kt @@ -16,22 +16,33 @@ package dev.rdnt.m8face import android.animation.AnimatorSet -import android.animation.Keyframe import android.animation.ObjectAnimator -import android.animation.PropertyValuesHolder import android.content.Context -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Rect +import android.graphics.RectF import android.util.FloatProperty +import android.util.Log +import android.util.LruCache import android.view.SurfaceHolder import android.view.animation.AnimationUtils import androidx.annotation.Keep -import androidx.core.content.ContextCompat.getDrawable +import androidx.compose.runtime.saveable.autoSaver +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.core.animation.doOnEnd +import androidx.core.animation.doOnStart import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.set import androidx.core.graphics.withRotation import androidx.core.graphics.withScale +import androidx.core.graphics.withTranslation import androidx.wear.watchface.ComplicationSlotsManager -import androidx.wear.watchface.DrawMode import androidx.wear.watchface.Renderer import androidx.wear.watchface.WatchState import androidx.wear.watchface.style.CurrentUserStyleRepository @@ -40,24 +51,34 @@ import androidx.wear.watchface.style.UserStyleSetting import androidx.wear.watchface.style.WatchFaceLayer import dev.rdnt.m8face.data.watchface.AmbientStyle import dev.rdnt.m8face.data.watchface.ColorStyle +import dev.rdnt.m8face.data.watchface.LayoutStyle import dev.rdnt.m8face.data.watchface.SecondsStyle import dev.rdnt.m8face.data.watchface.WatchFaceColorPalette.Companion.convertToWatchFaceColorPalette import dev.rdnt.m8face.data.watchface.WatchFaceData import dev.rdnt.m8face.utils.AMBIENT_STYLE_SETTING -import dev.rdnt.m8face.utils.BIG_AMBIENT_SETTING import dev.rdnt.m8face.utils.COLOR_STYLE_SETTING import dev.rdnt.m8face.utils.HorizontalComplication +import dev.rdnt.m8face.utils.HorizontalTextComplication +import dev.rdnt.m8face.utils.LAYOUT_STYLE_SETTING import dev.rdnt.m8face.utils.MILITARY_TIME_SETTING import dev.rdnt.m8face.utils.SECONDS_STYLE_SETTING import dev.rdnt.m8face.utils.VerticalComplication import java.time.Duration +import java.time.Instant import java.time.ZonedDateTime +import java.time.temporal.ChronoUnit +import kotlin.math.min import kotlin.math.pow import kotlin.math.sqrt -import kotlinx.coroutines.* +import kotlin.system.measureNanoTime +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch -// Default for how long each frame is displayed at expected frame rate. -private const val DEFAULT_INTERACTIVE_DRAW_MODE_UPDATE_DELAY_MILLIS: Long = 16 +private const val debug = false +private const val debugTiming = false /** * Renders watch face via data in Room database. Also, updates watch face state based on setting @@ -76,7 +97,7 @@ class WatchCanvasRenderer( currentUserStyleRepository, watchState, canvasType, - DEFAULT_INTERACTIVE_DRAW_MODE_UPDATE_DELAY_MILLIS, + 60000, clearWithBackgroundTintBeforeRenderingHighlightLayer = false ) { class AnalogSharedAssets : SharedAssets { @@ -84,11 +105,12 @@ class WatchCanvasRenderer( } } - private val scope: CoroutineScope = - CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + // https://stackoverflow.com/a/32948752 + private val ambientTransitionMs = context.resources.getInteger(android.R.integer.config_longAnimTime).toLong() - private val coroutineScope: CoroutineScope = - CoroutineScope(Dispatchers.Main.immediate) + private val scope: CoroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) + + private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main.immediate) // Represents all data needed to render the watch face. All value defaults are constants. Only // three values are changeable by the user (color scheme, ticks being rendered, and length of @@ -99,7 +121,7 @@ class WatchCanvasRenderer( ambientStyle = AmbientStyle.OUTLINE, secondsStyle = SecondsStyle.NONE, militaryTime = true, - bigAmbient = false, + layoutStyle = LayoutStyle.INFO1, ) // Converts resource ids into Colors and ComplicationDrawable. @@ -127,15 +149,18 @@ class WatchCanvasRenderer( } private val hourPaint = Paint().apply { + this.isSubpixelText = true isAntiAlias = true // make sure text is not anti-aliased even with this on typeface = context.resources.getFont(R.font.m8stealth57) - textSize = 112f / 14f * 14f // TODO: 98f/112f +// textSize = 112f / 14f * 14f // TODO: 98f/112f + textSize = 8f color = watchFaceColors.primaryColor } private val ambientHourPaint = Paint().apply { isAntiAlias = true - val big = watchFaceData.bigAmbient +// val big = watchFaceData.bigAmbient + var big = false if (watchFaceData.ambientStyle.id == AmbientStyle.OUTLINE.id) { typeface = context.resources.getFont(R.font.m8stealth57thin) @@ -166,13 +191,32 @@ class WatchCanvasRenderer( private val minutePaint = Paint().apply { isAntiAlias = true // make sure text is not anti-aliased even with this on typeface = context.resources.getFont(R.font.m8stealth57) - textSize = 112f / 14f * 14f // TODO: 98f/112F +// textSize = 112f / 14f * 14f // TODO: 98f/112F + textSize = 8f color = watchFaceColors.secondaryColor } + private val secondPaint = Paint().apply { + isAntiAlias = true // make sure text is not anti-aliased even with this on + typeface = context.resources.getFont(R.font.m8stealth57) + color = watchFaceColors.tertiaryColor + textSize = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> 56f + else -> 48f + } + } + + private val ampmPaint = Paint().apply { + isAntiAlias = true // make sure text is not anti-aliased even with this on + typeface = context.resources.getFont(R.font.m8stealth57) + textSize = 112f / 14f * 5f + color = watchFaceColors.tertiaryColor + } + private val ambientMinutePaint = Paint().apply { isAntiAlias = true - val big = watchFaceData.bigAmbient +// val big = watchFaceData.bigAmbient + var big = false if (watchFaceData.ambientStyle.id == AmbientStyle.OUTLINE.id) { typeface = context.resources.getFont(R.font.m8stealth57thin) @@ -220,90 +264,75 @@ class WatchCanvasRenderer( private var is24Format: Boolean = watchFaceData.militaryTime - private val ambientTransitionMs = 1000L private var drawProperties = DrawProperties() + private var isHeadless = false + private var isAmbient = false - private val ambientExitAnimator = - AnimatorSet().apply { - val linearOutSlow = - AnimationUtils.loadInterpolator( - context, - android.R.interpolator.linear_out_slow_in - ) - play( - ObjectAnimator.ofFloat( - drawProperties, - DrawProperties.TIME_SCALE, - drawProperties.timeScale, - 1.0f - ).apply { - duration = ambientTransitionMs - interpolator = linearOutSlow - setAutoCancel(false) - }, + private val ambientAnimator = + ObjectAnimator.ofFloat(drawProperties, DrawProperties.TIME_SCALE, 0f, 1f).apply { + interpolator = AnimationUtils.loadInterpolator( + context, + android.R.interpolator.accelerate_decelerate ) + duration = ambientTransitionMs + setAutoCancel(true) } - private val ambientEnterAnimator = - AnimatorSet().apply { - val linearOutSlow = - AnimationUtils.loadInterpolator( - context, - android.R.interpolator.linear_out_slow_in - ) - - val keyframes = arrayOf( - Keyframe.ofFloat(0f, drawProperties.timeScale), - Keyframe.ofFloat(0.9f, 0f), - Keyframe.ofFloat(1f, 0f) - ) + private lateinit var state: UserStyle - val propertyValuesHolder = PropertyValuesHolder.ofKeyframe( - DrawProperties.TIME_SCALE, - *keyframes - ) + private var bitmapsInitialized: Boolean = false + private var bitmapsScale: Float = 0f - play( - ObjectAnimator.ofPropertyValuesHolder(drawProperties, propertyValuesHolder).apply { - duration = ambientTransitionMs * 5 / 9 - interpolator = linearOutSlow - setAutoCancel(false) - }, - ) - } + override suspend fun init() { + super.init() + } init { scope.launch { currentUserStyleRepository.userStyle.collect { userStyle -> updateWatchFaceData(userStyle) + state = userStyle } } coroutineScope.launch { - watchState.isAmbient.collect { isAmbient -> - if (isAmbient!!) { // you call this readable? come on - ambientExitAnimator.cancel() - drawProperties.timeScale = 0f + watchState.isAmbient.collect { ambient -> + isHeadless = watchState.isHeadless + isAmbient = ambient!! + + if (!watchState.isHeadless) { + if (isAmbient) { + ambientAnimator.removeAllListeners() + ambientAnimator.cancel() + drawProperties.timeScale = 0f + interactiveDrawModeUpdateDelayMillis = interactiveFrameDelay + } else { + interactiveDrawModeUpdateDelayMillis = 16 + + ambientAnimator.removeAllListeners() + ambientAnimator.cancel() + ambientAnimator.doOnEnd { interactiveDrawModeUpdateDelayMillis = interactiveFrameDelay } + ambientAnimator.start() + } } else { - ambientExitAnimator.setupStartValues() - ambientExitAnimator.start() + drawProperties.timeScale = 1f } } } } - override suspend fun createSharedAssets(): AnalogSharedAssets { - return AnalogSharedAssets() + private val interactiveFrameDelay: Long + get() = when (watchFaceData.secondsStyle.id) { + SecondsStyle.NONE.id -> if (shouldDrawSeconds) 1000 else 60000 + SecondsStyle.DASHES.id -> 16 + SecondsStyle.DOTS.id -> 16 + else -> 1000 } - private fun updateRefreshRate() { - interactiveDrawModeUpdateDelayMillis = when { - animating() -> 16 // 60 fps cause animating - watchFaceData.secondsStyle.id == SecondsStyle.NONE.id -> 60000 // update once a second - watchFaceData.secondsStyle.id == SecondsStyle.DASHES.id -> 16 // 60 fps - watchFaceData.secondsStyle.id == SecondsStyle.DOTS.id -> 16 // 60 fps - else -> 60000 // safe default - } + private val memoryCache: LruCache = LruCache(485) + + override suspend fun createSharedAssets(): AnalogSharedAssets { + return AnalogSharedAssets() } /* @@ -318,9 +347,19 @@ class WatchCanvasRenderer( // Loops through user style and applies new values to watchFaceData. for (options in userStyle) { when (options.key.id.toString()) { + LAYOUT_STYLE_SETTING -> { + val listOption = + options.value as UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption + + newWatchFaceData = newWatchFaceData.copy( + layoutStyle = LayoutStyle.getLayoutStyleConfig( + listOption.id.toString() + ), + ) + } + COLOR_STYLE_SETTING -> { - val listOption = options.value as - UserStyleSetting.ListUserStyleSetting.ListOption + val listOption = options.value as UserStyleSetting.ListUserStyleSetting.ListOption newWatchFaceData = newWatchFaceData.copy( colorStyle = ColorStyle.getColorStyleConfig( @@ -330,8 +369,7 @@ class WatchCanvasRenderer( } AMBIENT_STYLE_SETTING -> { - val listOption = options.value as - UserStyleSetting.ListUserStyleSetting.ListOption + val listOption = options.value as UserStyleSetting.ListUserStyleSetting.ListOption newWatchFaceData = newWatchFaceData.copy( ambientStyle = AmbientStyle.getAmbientStyleConfig( @@ -341,8 +379,7 @@ class WatchCanvasRenderer( } SECONDS_STYLE_SETTING -> { - val listOption = options.value as - UserStyleSetting.ListUserStyleSetting.ListOption + val listOption = options.value as UserStyleSetting.ListUserStyleSetting.ListOption newWatchFaceData = newWatchFaceData.copy( secondsStyle = SecondsStyle.getSecondsStyleConfig( @@ -352,22 +389,12 @@ class WatchCanvasRenderer( } MILITARY_TIME_SETTING -> { - val booleanValue = options.value as - UserStyleSetting.BooleanUserStyleSetting.BooleanOption + val booleanValue = options.value as UserStyleSetting.BooleanUserStyleSetting.BooleanOption newWatchFaceData = newWatchFaceData.copy( militaryTime = booleanValue.value, ) } - - BIG_AMBIENT_SETTING -> { - val booleanValue = options.value as - UserStyleSetting.BooleanUserStyleSetting.BooleanOption - - newWatchFaceData = newWatchFaceData.copy( - bigAmbient = booleanValue.value, - ) - } } } @@ -390,6 +417,14 @@ class WatchCanvasRenderer( minutePaint.color = watchFaceColors.secondaryColor ambientMinutePaint.color = watchFaceColors.secondaryColor + secondPaint.color = watchFaceColors.tertiaryColor + secondPaint.textSize = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> 56f + else -> 48f + } + + ampmPaint.color = watchFaceColors.tertiaryColor + batteryPaint.color = watchFaceColors.tertiaryColor batteryIconPaint.color = watchFaceColors.tertiaryColor @@ -397,7 +432,11 @@ class WatchCanvasRenderer( ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thin) ambientHourPaint.textSize = 8F - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thinbig) +// ambientHourPaint.textSize = 8F +// } + if (false) { ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thinbig) ambientHourPaint.textSize = 8F } @@ -405,34 +444,52 @@ class WatchCanvasRenderer( ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thin) ambientMinutePaint.textSize = 8F - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thinbig) +// ambientMinutePaint.textSize = 8F +// } + if (false) { ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thinbig) ambientMinutePaint.textSize = 8F } } else if (watchFaceData.ambientStyle.id == AmbientStyle.BOLD_OUTLINE.id) { ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thick) ambientHourPaint.textSize = 8F - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thickbig) +// ambientHourPaint.textSize = 8F +// } + if (false) { ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57thickbig) ambientHourPaint.textSize = 8F } ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thick) ambientMinutePaint.textSize = 8F - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thickbig) +// ambientMinutePaint.textSize = 8F +// } + if (false) { ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57thickbig) ambientMinutePaint.textSize = 8F } } else if (watchFaceData.ambientStyle.id == AmbientStyle.FILLED.id) { ambientHourPaint.typeface = context.resources.getFont(R.font.m8stealth57) ambientHourPaint.textSize = 112F / 14f * 16f - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientHourPaint.textSize = 112F / 14f * 18f +// } + if (false) { ambientHourPaint.textSize = 112F / 14f * 18f } ambientMinutePaint.typeface = context.resources.getFont(R.font.m8stealth57) ambientMinutePaint.textSize = 112F / 14f * 16f - if (watchFaceData.bigAmbient) { +// if (watchFaceData.bigAmbient) { +// ambientMinutePaint.textSize = 112F / 14f * 18f +// } + if (false) { ambientMinutePaint.textSize = 112F / 14f * 18f } @@ -440,6 +497,10 @@ class WatchCanvasRenderer( is24Format = watchFaceData.militaryTime + if (!ambientAnimator.isRunning) { + interactiveDrawModeUpdateDelayMillis = interactiveFrameDelay + } + // TODO: update colors for all elements here // Applies the user chosen complication color scheme changes. ComplicationDrawables for @@ -454,12 +515,341 @@ class WatchCanvasRenderer( is HorizontalComplication -> (complication.renderer as HorizontalComplication).tertiaryColor = watchFaceColors.tertiaryColor + is HorizontalTextComplication -> (complication.renderer as HorizontalTextComplication).tertiaryColor = + watchFaceColors.tertiaryColor + else -> {} } } } } + private fun preloadBitmaps(bounds: Rect, scale: Float) { + Log.d("WatchCanvasRenderer", "preloadBitmaps($scale)") + bitmapsScale = scale + + val compBmp = Bitmap.createBitmap( + bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888 + ) + memoryCache.put("comp", compBmp) + + val secsBmp = Bitmap.createBitmap( + bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888 + ) + memoryCache.put("secs", secsBmp) + + val canvas = Canvas() + preloadHourBitmaps(canvas, scale) + preloadHourBitmapsOutline(canvas, scale) + preloadHourBitmapsOutlineBig(canvas, scale) + preloadHourBitmapsBoldOutline(canvas, scale) + preloadHourBitmapsBoldOutlineBig(canvas, scale) + preloadMinuteBitmaps(canvas, scale) + preloadMinuteBitmapsOutline(canvas, scale) + preloadMinuteBitmapsOutlineBig(canvas, scale) + preloadMinuteBitmapsBoldOutline(canvas, scale) + preloadMinuteBitmapsBoldOutlineBig(canvas, scale) + preloadSecondBitmaps(canvas, scale) + preloadAmPmBitmaps(canvas, scale) + } + + private val hourPaintNormal2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.primaryColor + + typeface = context.resources.getFont(R.font.m8stealth57) + textSize = 144f + } + + private val hourPaint2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.primaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thin) + textSize = 8f + } + + private val hourPaintBig2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.primaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thinbig) + textSize = 8f + } + + private val hourPaintBold2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.primaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thick) + textSize = 8f + } + + private val hourPaintBoldBig2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.primaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thickbig) + textSize = 8f + } + + private val minutePaintNormal2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.secondaryColor + + typeface = context.resources.getFont(R.font.m8stealth57) + textSize = 144f + } + + private val minutePaint2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.secondaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thin) + textSize = 8f + } + + private val minutePaintBig2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.secondaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thinbig) + textSize = 8f + } + + private val minutePaintBold2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.secondaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thick) + textSize = 8f + } + + private val minutePaintBoldBig2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.secondaryColor + + typeface = context.resources.getFont(R.font.m8stealth57thickbig) + textSize = 8f + } + + private val secondPaintNormal2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.tertiaryColor + + typeface = context.resources.getFont(R.font.m8stealth57) + textSize = 56f + } + + private val ampmPaint2: Paint + get() = Paint(hourPaint).apply { + isAntiAlias = false + + color = watchFaceColors.tertiaryColor + + typeface = context.resources.getFont(R.font.m8stealth57) + textSize = 40f + } + + private fun preloadBitmap(tmpCanvas: Canvas, time: String, paint: Paint): Bitmap { + var textBounds = Rect() + paint.getTextBounds(time, 0, time.length, textBounds) + textBounds = Rect(0, 0, textBounds.width(), textBounds.height()) + + val bmp = Bitmap.createBitmap( + textBounds.width(), textBounds.height(), Bitmap.Config.ARGB_8888 + ) + + tmpCanvas.setBitmap(bmp) + + tmpCanvas.drawText( + time, + 0f, + textBounds.height().toFloat(), + paint, + ) + + tmpCanvas.setBitmap(null) + + return bmp.asShared() + } + + private fun preloadHourBitmaps(canvas: Canvas, scale: Float) { + for (i in 0..23) { + val hour = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, hour, hourPaintNormal2.apply { + textSize *= scale + }) + + val cacheKey = "hour_normal_$hour" + + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadHourBitmapsOutline(canvas: Canvas, scale: Float) { + for (i in 0..23) { + val hour = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, hour, Paint(hourPaint2).apply { + textSize *= scale + }) + + val cacheKey = "hour_outline_$hour" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadHourBitmapsOutlineBig(canvas: Canvas, scale: Float) { + for (i in 0..23) { + val hour = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, hour, Paint(hourPaintBig2).apply { + textSize *= scale + }) + + val cacheKey = "hour_big_outline_$hour" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadHourBitmapsBoldOutline(canvas: Canvas, scale: Float) { + for (i in 0..23) { + val hour = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, hour, Paint(hourPaintBold2).apply { + textSize *= scale + }) + + val cacheKey = "hour_bold_outline_$hour" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadHourBitmapsBoldOutlineBig(canvas: Canvas, scale: Float) { + for (i in 0..23) { + val hour = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, hour, Paint(hourPaintBoldBig2).apply { + textSize *= scale + }) + + val cacheKey = "hour_big_bold_outline_$hour" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadMinuteBitmaps(canvas: Canvas, scale: Float) { + for (i in 0..59) { + val minute = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, minute, minutePaintNormal2.apply { + textSize *= scale + }) + + val cacheKey = "minute_normal_$minute" + + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadMinuteBitmapsOutline(canvas: Canvas, scale: Float) { + for (i in 0..59) { + val minute = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, minute, minutePaint2.apply { + textSize *= scale + }) + + val cacheKey = "minute_outline_$minute" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadMinuteBitmapsOutlineBig(canvas: Canvas, scale: Float) { + for (i in 0..59) { + val minute = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, minute, minutePaintBig2.apply { + textSize *= scale + }) + + val cacheKey = "minute_big_outline_$minute" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadMinuteBitmapsBoldOutline(canvas: Canvas, scale: Float) { + for (i in 0..59) { + val minute = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, minute, minutePaintBold2.apply { + textSize *= scale + }) + + val cacheKey = "minute_bold_outline_$minute" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadMinuteBitmapsBoldOutlineBig(canvas: Canvas, scale: Float) { + for (i in 0..59) { + val minute = i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, minute, minutePaintBoldBig2.apply { + textSize *= scale + }) + + val cacheKey = "minute_big_bold_outline_$minute" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadSecondBitmaps(canvas: Canvas, scale: Float) { + for (i in 0..60) { + val second = if (i == 60) "M8" else i.toString().padStart(2, '0') + + val bmp = preloadBitmap(canvas, second, secondPaintNormal2.apply { + textSize *= scale + }) + val cacheKey = "second_normal_$second" + memoryCache.put(cacheKey, bmp) + } + } + + private fun preloadAmPmBitmaps(canvas: Canvas, scale: Float) { + for (text in arrayOf("AM", "PM")) { + val bmp = preloadBitmap(canvas, text, ampmPaint2.apply { + textSize *= scale + }) + val cacheKey = "ampm_$text" + memoryCache.put(cacheKey, bmp) + } + } + override fun onDestroy() { // Log.d(TAG, "onDestroy()") scope.cancel("WatchCanvasRenderer scope clear() request") @@ -467,10 +857,7 @@ class WatchCanvasRenderer( } override fun renderHighlightLayer( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - sharedAssets: AnalogSharedAssets + canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime, sharedAssets: AnalogSharedAssets ) { canvas.drawColor(renderParameters.highlightLayer!!.backgroundTint) @@ -481,199 +868,378 @@ class WatchCanvasRenderer( } } - override fun render( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - sharedAssets: AnalogSharedAssets, - ) { - updateRefreshRate() - - canvas.drawColor(Color.parseColor("#ff000000")) + private val scale + get() = if (!isHeadless) drawProperties.timeScale else 1f - if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) { - var hour: Int - if (is24Format) { - hour = zonedDateTime.hour - } else { - hour = zonedDateTime.hour % 12 - if (hour == 0) { - hour = 12 - } - } - - if (drawProperties.timeScale == 0f) { - var hourOffsetX = 0f - var hourOffsetY = 0f - var minuteOffsetX = 0f - var minuteOffsetY = 0f + fun interpolate(start: Float, end: Float): Float { + return start + easeInOutCubic(scale) * (end - start) + } - when (watchFaceData.ambientStyle.id) { - AmbientStyle.OUTLINE.id -> { - if (watchFaceData.bigAmbient) { - hourOffsetX = -99f - hourOffsetY = -9f - minuteOffsetX = -99f - minuteOffsetY = 135f + val timeScale: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.INFO1.id, LayoutStyle.INFO2.id, LayoutStyle.INFO3.id, LayoutStyle.INFO4.id, LayoutStyle.SPORT.id -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.OUTLINE, AmbientStyle.BOLD_OUTLINE -> { + if (isAmbient) { + 18f / 18f } else { - hourOffsetX = -88f - hourOffsetY = -8f - minuteOffsetX = -88f - minuteOffsetY = 120f + interpolate(16f / 18f, 14f / 18f) } } - - AmbientStyle.BOLD_OUTLINE.id -> { - if (watchFaceData.bigAmbient) { - hourOffsetX = -99f - hourOffsetY = -9f - minuteOffsetX = -99f - minuteOffsetY = 135f + AmbientStyle.BIG_OUTLINE, AmbientStyle.BIG_BOLD_OUTLINE, AmbientStyle.BIG_FILLED -> { + if (isAmbient) { + 18f / 18f } else { - hourOffsetX = -88f - hourOffsetY = -8f - minuteOffsetX = -88f - minuteOffsetY = 120f + interpolate(18f / 18f, 14f / 18f) } } + AmbientStyle.FILLED -> { + if (isAmbient) { + 16f / 18f + } else { + interpolate(16f / 18f, 14f / 18f) + } + } + AmbientStyle.DETAILED -> 14f / 18f + } + } - AmbientStyle.FILLED.id -> { - if (watchFaceData.bigAmbient) { - hourOffsetX = -99f - hourOffsetY = -9f - minuteOffsetX = -99f - minuteOffsetY = 135f + LayoutStyle.FOCUS.id -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.OUTLINE, AmbientStyle.BOLD_OUTLINE -> { + if (isAmbient) { + 18f / 18f } else { - hourOffsetX = -88f - hourOffsetY = -8f - minuteOffsetX = -88f - minuteOffsetY = 120f + 16f / 18f } } + AmbientStyle.BIG_OUTLINE, AmbientStyle.BIG_BOLD_OUTLINE, AmbientStyle.BIG_FILLED -> { + if (isAmbient) { + 18f / 18f + } else { + interpolate(18f / 18f, 16f / 18f) + } + } + AmbientStyle.FILLED-> 16f / 18f + AmbientStyle.DETAILED-> 16f / 18f } + } - drawTime(canvas, bounds, hour, ambientHourPaint, hourOffsetX, hourOffsetY, 0f) - drawTime( - canvas, - bounds, - zonedDateTime.minute, - ambientMinutePaint, - minuteOffsetX, - minuteOffsetY, - 0f - ) - } else { - when (watchFaceData.secondsStyle.id) { - SecondsStyle.NONE.id -> { + else -> 18f / 18f + } + val complicationsScale: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.FOCUS.id -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.BIG_OUTLINE, AmbientStyle.BIG_BOLD_OUTLINE, AmbientStyle.BIG_FILLED -> { + interpolate(18f / 16f, 16f / 16f) +// interpolate(17f / 16f, 16f / 16f) } - SecondsStyle.DASHES.id -> { - drawDashes(canvas, bounds, zonedDateTime) + else -> 16f / 16f + } + } + + else -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.BIG_OUTLINE, AmbientStyle.BIG_BOLD_OUTLINE, AmbientStyle.BIG_FILLED -> { + interpolate(18f / 14f, 14f / 14f) +// interpolate(16f / 14f, 14f / 14f) } - SecondsStyle.DOTS.id -> { - drawDots(canvas, bounds, zonedDateTime) + AmbientStyle.DETAILED -> 14f / 14f + + else -> { + interpolate(16f / 14f, 14f / 14f) +// interpolate(15f / 14f, 14f / 14f) } } + } + } - val scaleOffset = if (this.watchFaceData.bigAmbient) { - 18f / 14f - 1f + val hourOffsetY: Float + get() = when (watchFaceData.ambientStyle) { + AmbientStyle.OUTLINE, AmbientStyle.BOLD_OUTLINE -> { + if (isAmbient) { + -64f } else { - 16f / 14f - 1f + -72f } + } - drawTime(canvas, bounds, hour, hourPaint, -77f, -7f, scaleOffset) // Rect(0, 0, 152, 14)) - drawTime( - canvas, - bounds, - zonedDateTime.minute, - minutePaint, - -77f, - 105f, - scaleOffset - )//Rect(0, 0, 152, -210)) + else -> -72f + } + + val minuteOffsetY: Float + get() = when (watchFaceData.ambientStyle) { + AmbientStyle.OUTLINE, AmbientStyle.BOLD_OUTLINE -> { + if (isAmbient) { + 64f + } else { + 72f + } } + else -> 72f } - if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS) && - drawProperties.timeScale != 0f - ) { - drawComplications(canvas, zonedDateTime) + val shouldDrawSeconds: Boolean + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.INFO1.id, LayoutStyle.INFO3.id, LayoutStyle.SPORT.id -> true + else -> false } - } - override fun shouldAnimate(): Boolean { - return ambientEnterAnimator.isRunning || super.shouldAnimate() - } + val secondsOffsetX: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> 94f + else -> 129f + } - private fun animating(): Boolean { - return ambientEnterAnimator.isRunning || ambientExitAnimator.isRunning - } + val secondsOffsetY: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> -32f + else -> 0f + } - // ----- All drawing functions ----- - private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) { - val opacity = this.easeInOutCirc(drawProperties.timeScale) + val shouldDrawAmPm: Boolean + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> true + else -> false + } - for ((_, complication) in complicationSlotsManager.complicationSlots) { - if (complication.enabled) { - when (complication.renderer) { - is VerticalComplication -> (complication.renderer as VerticalComplication).opacity = - opacity + val timeTextStyle: String + get() = when(isAmbient) { + true -> { + when (watchFaceData.ambientStyle.id) { + AmbientStyle.OUTLINE.id -> "outline" + AmbientStyle.BIG_OUTLINE.id -> "big_outline" + AmbientStyle.BOLD_OUTLINE.id -> "bold_outline" + AmbientStyle.BIG_BOLD_OUTLINE.id -> "big_bold_outline" + else -> "normal" + } + } + false -> "normal" + } - is HorizontalComplication -> (complication.renderer as HorizontalComplication).opacity = - opacity + val timeOffsetX: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.DETAILED -> -45f + else -> interpolate(0f, -45f) + } + } + else -> 0f + } - else -> {} + val complicationsOffsetX: Float + get() = when (watchFaceData.layoutStyle.id) { + LayoutStyle.SPORT.id -> { + when(watchFaceData.ambientStyle) { + AmbientStyle.DETAILED -> 0f + else -> interpolate(35f, 0f) } + } + else -> 0f + } - complication.render(canvas, zonedDateTime, renderParameters) + fun getHour(zonedDateTime: ZonedDateTime): Int { + if (is24Format) { + return zonedDateTime.hour + } else { + val hour = zonedDateTime.hour % 12 + if (hour == 0) { + return 12 } + return hour } } - private fun drawTime( + fun getAmPm(zonedDateTime: ZonedDateTime): String { + return if (zonedDateTime.hour < 12) { + "am" + } else { + "pm" + } + } + + override fun render( canvas: Canvas, bounds: Rect, - time: Int, - paint: Paint, - offsetX: Float, - offsetY: Float, - scaleOffset: Float + zonedDateTime: ZonedDateTime, + sharedAssets: AnalogSharedAssets, ) { - // dx:70 dy:98 - val p = Paint(paint) - p.textSize = p.textSize / 384F * bounds.width() + canvas.drawColor(Color.parseColor("#ff000000")) - var scale = 1f +// if (!this::state.isInitialized) { +// return +// } - p.isAntiAlias = true + // all calculations are done with 384x384 resolution in mind + val renderScale = min(bounds.width(), bounds.height()).toFloat() / 384.0f - scale += (1f - this.easeInOutCirc(drawProperties.timeScale)) * scaleOffset + if (!bitmapsInitialized || bitmapsScale != renderScale) { + preloadBitmaps(bounds, renderScale) + bitmapsInitialized = true + } - canvas.withScale(scale, scale, bounds.exactCenterX(), bounds.exactCenterY()) { - canvas.drawText( - time.toString().padStart(2, '0'), - bounds.exactCenterX() + offsetX / 384F * bounds.width().toFloat(), - bounds.exactCenterY() + offsetY / 384F * bounds.height().toFloat(), - p - ) + val took = measureNanoTime { + canvas.withScale( + timeScale, + timeScale, + bounds.exactCenterX(), + bounds.exactCenterY() + ) { + + canvas.withTranslation(timeOffsetX * renderScale, 0f) { + if (renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE)) { + + val opacity = interpolate(1f, 1f) + + val hourText = getHour(zonedDateTime).toString().padStart(2, '0') + val hourBmp = memoryCache.get("hour_${timeTextStyle}_$hourText") + + canvas.drawBitmap( + hourBmp, + bounds.exactCenterX() - hourBmp.width / 2f, + bounds.exactCenterY() - hourBmp.height / 2f + hourOffsetY * renderScale, + Paint().apply { alpha = (opacity * 255).toInt() }, + ) + + val minuteText = zonedDateTime.minute.toString().padStart(2, '0') + val minuteBmp = memoryCache.get("minute_${timeTextStyle}_$minuteText") + + canvas.drawBitmap( + minuteBmp, + bounds.exactCenterX() - minuteBmp.width / 2f, + bounds.exactCenterY() - minuteBmp.height / 2f + minuteOffsetY * renderScale, + Paint().apply { alpha = (opacity * 255).toInt() }, + ) + } + } + } + + val shouldDrawComplications = renderParameters.watchFaceLayers.contains(WatchFaceLayer.COMPLICATIONS) && + (!isAmbient || watchFaceData.ambientStyle == AmbientStyle.DETAILED) + val shouldDrawSecondsAmPm = renderParameters.watchFaceLayers.contains(WatchFaceLayer.BASE) + + if (shouldDrawComplications || shouldDrawSecondsAmPm) { + val compBmp = memoryCache.get("comp") + compBmp.eraseColor(Color.TRANSPARENT) + val compCanvas = Canvas(compBmp) + + val opacity = if (watchFaceData.ambientStyle == AmbientStyle.DETAILED) interpolate(1f, 1f) else interpolate(0f, 1f) + + canvas.withScale(complicationsScale, complicationsScale, bounds.exactCenterX(), bounds.exactCenterY()) { + canvas.withTranslation(complicationsOffsetX, 0f) { + + if (shouldDrawSecondsAmPm) { + if (shouldDrawSeconds) { + val secondText = if (watchFaceData.ambientStyle == AmbientStyle.DETAILED && isAmbient) "M8" else zonedDateTime.second.toString().padStart(2, '0') + val cacheKey = "second_normal_$secondText" + + val secondBmp = memoryCache.get(cacheKey) + + val offsetX = secondsOffsetX * renderScale + val offsetY = secondsOffsetY * renderScale + + compCanvas.drawBitmap( + secondBmp, + bounds.exactCenterX() - secondBmp.width / 2 + offsetX, + bounds.exactCenterY() - secondBmp.height / 2 + offsetY, + Paint(), + ) + + } + + if (shouldDrawAmPm) { + val ampmText = getAmPm(zonedDateTime).uppercase() + val cacheKey = "ampm_$ampmText" + + val ampmBmp = memoryCache.get(cacheKey) + + val offsetX = 83f * renderScale + val offsetY = 24f * renderScale + + compCanvas.drawBitmap( + ampmBmp, + bounds.exactCenterX() - ampmBmp.width / 2 + offsetX, + bounds.exactCenterY() - ampmBmp.height / 2 + offsetY, + Paint(), + ) + + } + } + + if (shouldDrawComplications) { + drawComplications(compCanvas, zonedDateTime) + } + + canvas.drawBitmap( + compBmp, + bounds.left.toFloat(), + bounds.top.toFloat(), + Paint().apply { alpha = (opacity * 255).toInt() }, + ) + + } + } + } + + if (watchFaceData.secondsStyle != SecondsStyle.NONE) { + val secsBmp = memoryCache.get("secs") + secsBmp.eraseColor(Color.TRANSPARENT) + val secsCanvas = Canvas(secsBmp) + + val opacity = if (watchFaceData.ambientStyle == AmbientStyle.DETAILED) interpolate(1f, 1f) else interpolate(0f, 1f) + + when (watchFaceData.secondsStyle.id) { + SecondsStyle.DASHES.id -> { + drawDashes(secsCanvas, bounds, zonedDateTime) + } + + SecondsStyle.DOTS.id -> { + drawDots(secsCanvas, bounds, zonedDateTime) + } + } + + canvas.drawBitmap( + secsBmp, + bounds.left.toFloat(), + bounds.top.toFloat(), + Paint().apply { alpha = (opacity * 255).toInt() }, + ) + } + } + + if (debugTiming) { + Log.d("WatchCanvasRenderer.timing", "${took.toFloat() / 1000000.0}ms") + } + } + + override fun shouldAnimate(): Boolean { + return super.shouldAnimate() || ambientAnimator.isRunning + } + + // ----- All drawing functions ----- + private fun drawComplications(canvas: Canvas, zonedDateTime: ZonedDateTime) { + for ((_, complication) in complicationSlotsManager.complicationSlots) { + if (complication.enabled) { + complication.render(canvas, zonedDateTime, renderParameters) + } } } private fun drawDashes( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime + canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime ) { // Retrieve current time to calculate location/rotation of watch arms. val nanoOfDate = zonedDateTime.toLocalTime().toNanoOfDay() val secondsPerSecondHandRotation = Duration.ofMinutes(1).seconds val secondsRotation = - (nanoOfDate.toFloat() / 1000000000F).rem(secondsPerSecondHandRotation) * 360.0f / - secondsPerSecondHandRotation + (nanoOfDate.toFloat() / 1000000000F).rem(secondsPerSecondHandRotation) * 360.0f / secondsPerSecondHandRotation val secondHandSize = 12f * this.easeInOutCirc(this.drawProperties.timeScale) @@ -681,19 +1247,19 @@ class WatchCanvasRenderer( secondHandPaint.alpha = (this.easeInOutCirc(this.drawProperties.timeScale) * secondHandPaint.alpha).toInt() - val transitionOffset: Float = this.easeInOutCirc(1 - this.drawProperties.timeScale) * 16f - - canvas.withRotation(secondsRotation, bounds.exactCenterX(), bounds.exactCenterY()) { - canvas.drawRect( - RectF( - (bounds.width() / 2f - 1.25f / 384F * bounds.width()), - transitionOffset / 384F * bounds.width(), - (bounds.width() / 2f + 1.25f / 384F * bounds.width()), - (secondHandSize + transitionOffset * 2) / 384F * bounds.width(), - ), - secondHandPaint, - ) - } + val transitionOffset: Float = this.easeInOutCirc(1 - this.drawProperties.timeScale) * -16f * 0 + +// canvas.withRotation(secondsRotation, bounds.exactCenterX(), bounds.exactCenterY()) { +// canvas.drawRect( +// RectF( +// (bounds.width() / 2f - 1.25f / 384F * bounds.width()), +// transitionOffset / 384F * bounds.width(), +// (bounds.width() / 2f + 1.25f / 384F * bounds.width()), +// (secondHandSize + transitionOffset * 2) / 384F * bounds.width(), +// ), +// secondHandPaint, +// ) +// } val maxAlpha = 255f @@ -709,18 +1275,18 @@ class WatchCanvasRenderer( if (i.mod(90f) == 0f) { // cardinals color = watchFaceColors.primaryColor - maxSize = 18f - weight = 1.75f + maxSize = 12f + weight = 2f minAlpha = maxAlpha / 4f } else if (i.mod(30f) == 0f) { // intermediates color = watchFaceColors.secondaryColor - maxSize = 14f - weight = 1.75f + maxSize = 10f + weight = 2f minAlpha = maxAlpha / 4f } else { color = watchFaceColors.tertiaryColor - maxSize = 10f - weight = 1.5f + maxSize = 8f + weight = 1.75f minAlpha = maxAlpha / 8f } @@ -791,8 +1357,7 @@ class WatchCanvasRenderer( transitionOffset / 384F * bounds.width(), bounds.exactCenterX() + weight / 384F * bounds.width(), (size + transitionOffset * 2) / 384F * bounds.width(), - ), - outerElementPaint + ), outerElementPaint ) } @@ -801,17 +1366,14 @@ class WatchCanvasRenderer( } private fun drawDots( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime + canvas: Canvas, bounds: Rect, zonedDateTime: ZonedDateTime ) { // Retrieve current time to calculate location/rotation of watch arms. val nanoOfDate = zonedDateTime.toLocalTime().toNanoOfDay() val secondsPerSecondHandRotation = Duration.ofMinutes(1).seconds val secondsRotation = - (nanoOfDate.toFloat() / 1000000000F).rem(secondsPerSecondHandRotation) * 360.0f / - secondsPerSecondHandRotation + (nanoOfDate.toFloat() / 1000000000F).rem(secondsPerSecondHandRotation) * 360.0f / secondsPerSecondHandRotation val div = secondsRotation.div(6f).toInt() val mod = secondsRotation.mod(6f) @@ -820,20 +1382,20 @@ class WatchCanvasRenderer( var centerY = 8f * this.easeInOutCirc(this.drawProperties.timeScale) val secondHandSize = 3.5f * this.easeInOutCirc(this.drawProperties.timeScale) - val transitionOffset = this.easeInOutCirc(1 - this.drawProperties.timeScale) * 24f + val transitionOffset = this.easeInOutCirc(1 - this.drawProperties.timeScale) * 24f * 0 val secondHandPaint = Paint(this.secondHandPaint) secondHandPaint.alpha = (this.easeInOutCirc(this.drawProperties.timeScale) * secondHandPaint.alpha).toInt() - canvas.withRotation(rotation, bounds.exactCenterX(), bounds.exactCenterY()) { - canvas.drawCircle( - bounds.centerX().toFloat(), - (centerY + transitionOffset), - secondHandSize / 384F * bounds.width(), - secondHandPaint - ) - } +// canvas.withRotation(rotation, bounds.exactCenterX(), bounds.exactCenterY()) { +// canvas.drawCircle( +// bounds.centerX().toFloat(), +// (centerY + transitionOffset), +// secondHandSize / 384F * bounds.width(), +// secondHandPaint +// ) +// } val maxAlpha = 255f @@ -950,20 +1512,30 @@ class WatchCanvasRenderer( } } + private fun easeInOutCubic(x: Float): Float { + return if (x < 0.5f) { + 4 * x * x * x + } else { + 1 - (-2f * x + 2f).pow(3f) / 2 + } + } + private class DrawProperties( - var timeScale: Float = 0f + var timeScale: Float = 1f ) { companion object { - val TIME_SCALE = - object : FloatProperty("timeScale") { - override fun setValue(obj: DrawProperties, value: Float) { - obj.timeScale = value + val TIME_SCALE = object : FloatProperty("timeScale") { + override fun setValue(obj: DrawProperties, value: Float) { + if (value.isNaN()) { + throw IllegalArgumentException("timeScale cannot be NaN") } + obj.timeScale = value + } - override fun get(obj: DrawProperties): Float { - return obj.timeScale - } + override fun get(obj: DrawProperties): Float { + return obj.timeScale } + } } } @@ -971,3 +1543,4 @@ class WatchCanvasRenderer( private const val TAG = "WatchCanvasRenderer" } } + diff --git a/app/src/main/java/dev/rdnt/m8face/data/watchface/AmbientStyle.kt b/app/src/main/java/dev/rdnt/m8face/data/watchface/AmbientStyle.kt index 2f269bc..011ac68 100644 --- a/app/src/main/java/dev/rdnt/m8face/data/watchface/AmbientStyle.kt +++ b/app/src/main/java/dev/rdnt/m8face/data/watchface/AmbientStyle.kt @@ -31,25 +31,56 @@ enum class AmbientStyle( OUTLINE( id = "outline", nameResourceId = R.string.outline_ambient_style_name, - iconResourceId = R.drawable.outline_style_icon, +// iconResourceId = R.drawable.outline_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon + ), + BIG_OUTLINE( + id = "big_outline", + nameResourceId = R.string.big_outline_ambient_style_name, // TODO +// iconResourceId = R.drawable.outline_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon ), BOLD_OUTLINE( id = "bold_outline", nameResourceId = R.string.bold_outline_ambient_style_name, - iconResourceId = R.drawable.bold_outline_style_icon, +// iconResourceId = R.drawable.bold_outline_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon + ), + BIG_BOLD_OUTLINE( + id = "big_bold_outline", + nameResourceId = R.string.big_bold_outline_ambient_style_name, // TODO +// iconResourceId = R.drawable.bold_outline_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon ), FILLED( id = "filled", nameResourceId = R.string.filled_ambient_style_name, - iconResourceId = R.drawable.filled_style_icon, +// iconResourceId = R.drawable.filled_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon + ), + BIG_FILLED( + id = "big_filled", + nameResourceId = R.string.big_filled_ambient_style_name, +// iconResourceId = R.drawable.filled_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon + ), + DETAILED( + id = "detailed", + nameResourceId = R.string.detailed_ambient_style_name, // TODO +// iconResourceId = R.drawable.filled_style_icon, + iconResourceId = R.drawable.mauve_style_icon, // TODO @rdnt fix icon ); companion object { fun getAmbientStyleConfig(id: String): AmbientStyle { return when (id) { OUTLINE.id -> OUTLINE + BIG_OUTLINE.id -> BIG_OUTLINE BOLD_OUTLINE.id -> BOLD_OUTLINE + BIG_BOLD_OUTLINE.id -> BIG_BOLD_OUTLINE FILLED.id -> FILLED + BIG_FILLED.id -> BIG_FILLED + DETAILED.id -> DETAILED else -> OUTLINE } } diff --git a/app/src/main/java/dev/rdnt/m8face/data/watchface/LayoutStyle.kt b/app/src/main/java/dev/rdnt/m8face/data/watchface/LayoutStyle.kt new file mode 100644 index 0000000..ee07967 --- /dev/null +++ b/app/src/main/java/dev/rdnt/m8face/data/watchface/LayoutStyle.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package dev.rdnt.m8face.data.watchface + +import android.content.Context +import android.graphics.drawable.Icon +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.wear.watchface.style.UserStyleSetting +import androidx.wear.watchface.style.UserStyleSetting.ListUserStyleSetting +import dev.rdnt.m8face.R + +enum class LayoutStyle( + val id: String, + @StringRes val nameResourceId: Int, + @DrawableRes val iconResourceId: Int, +) { + // TODO: @rdnt use SVGs or compress the pngs to conserve wire memory for both + // these and other styles (so that we don't exceed transfer limitations of + // watchface config over BLE) + INFO1( + id = "info1", + nameResourceId = R.string.info1_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ), + INFO2( + id = "info2", + nameResourceId = R.string.info2_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ), + INFO3( + id = "info3", + nameResourceId = R.string.info3_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ), + INFO4( + id = "info4", + nameResourceId = R.string.info4_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ), + SPORT( + id = "sport", + nameResourceId = R.string.sport_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ), + FOCUS( + id = "focus", + nameResourceId = R.string.focus_layout_style_name, + iconResourceId = R.drawable.steel_style_icon, // TODO @rdnt fix icon + ); + + companion object { + fun getLayoutStyleConfig(id: String): LayoutStyle { + return when (id) { + INFO1.id -> INFO1 + INFO2.id -> INFO2 + INFO3.id -> INFO3 + INFO4.id -> INFO4 + SPORT.id -> SPORT + FOCUS.id -> FOCUS + else -> INFO1 + } + } + } +} diff --git a/app/src/main/java/dev/rdnt/m8face/data/watchface/WatchFaceData.kt b/app/src/main/java/dev/rdnt/m8face/data/watchface/WatchFaceData.kt index 62efb91..bc761aa 100644 --- a/app/src/main/java/dev/rdnt/m8face/data/watchface/WatchFaceData.kt +++ b/app/src/main/java/dev/rdnt/m8face/data/watchface/WatchFaceData.kt @@ -19,9 +19,9 @@ package dev.rdnt.m8face.data.watchface * Represents all data needed to render an analog watch face. */ data class WatchFaceData( + val layoutStyle: LayoutStyle = LayoutStyle.INFO1, val colorStyle: ColorStyle = ColorStyle.LAST_DANCE, val ambientStyle: AmbientStyle = AmbientStyle.OUTLINE, val secondsStyle: SecondsStyle = SecondsStyle.NONE, val militaryTime: Boolean = true, - val bigAmbient: Boolean = true, ) diff --git a/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigActivity.kt b/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigActivity.kt index 0a03f78..bd5e941 100644 --- a/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigActivity.kt +++ b/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigActivity.kt @@ -22,6 +22,7 @@ package dev.rdnt.m8face.editor +import android.graphics.RectF import android.os.Bundle import android.util.Log import android.view.HapticFeedbackConstants.KEYBOARD_TAP @@ -29,15 +30,43 @@ import android.view.HapticFeedbackConstants.LONG_PRESS import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.core.LinearEasing -import androidx.compose.foundation.* -import androidx.compose.foundation.gestures.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.focusable +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.rememberScrollableState +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.PagerState +import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -49,8 +78,6 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color.Companion.Black -import androidx.compose.ui.graphics.Color.Companion.Blue -import androidx.compose.ui.graphics.Color.Companion.Red import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.graphics.Color.Companion.White import androidx.compose.ui.graphics.ColorFilter @@ -73,17 +100,24 @@ import androidx.compose.ui.zIndex import androidx.core.graphics.ColorUtils import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope -import androidx.wear.compose.material.* -import androidx.wear.compose.material.ButtonDefaults.outlinedButtonBorder -import androidx.wear.compose.foundation.lazy.ScalingLazyListState +import androidx.wear.compose.foundation.lazy.AutoCenteringParams import androidx.wear.compose.foundation.lazy.ScalingLazyColumn import androidx.wear.compose.foundation.lazy.ScalingLazyColumnDefaults import androidx.wear.compose.foundation.lazy.ScalingLazyListAnchorType -import androidx.wear.compose.foundation.lazy.AutoCenteringParams -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.PagerDefaults -import androidx.compose.foundation.pager.PagerState -import androidx.compose.foundation.pager.rememberPagerState +import androidx.wear.compose.foundation.lazy.ScalingLazyListState +import androidx.wear.compose.material.ButtonDefaults.outlinedButtonBorder +import androidx.wear.compose.material.HorizontalPageIndicator +import androidx.wear.compose.material.OutlinedButton +import androidx.wear.compose.material.PageIndicatorState +import androidx.wear.compose.material.PositionIndicator +import androidx.wear.compose.material.PositionIndicatorAlignment +import androidx.wear.compose.material.PositionIndicatorState +import androidx.wear.compose.material.PositionIndicatorVisibility +import androidx.wear.compose.material.Scaffold +import androidx.wear.compose.material.Switch +import androidx.wear.compose.material.Text +import androidx.wear.compose.material.ToggleChip +import androidx.wear.compose.material.ToggleChipDefaults import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.rotaryinput.rotaryWithSnap import com.google.android.horologist.compose.rotaryinput.toRotaryScrollAdapter @@ -91,19 +125,10 @@ import dev.chrisbanes.snapper.ExperimentalSnapperApi import dev.rdnt.m8face.R import dev.rdnt.m8face.data.watchface.AmbientStyle import dev.rdnt.m8face.data.watchface.ColorStyle +import dev.rdnt.m8face.data.watchface.LayoutStyle import dev.rdnt.m8face.data.watchface.SecondsStyle import dev.rdnt.m8face.theme.WearAppTheme -import dev.rdnt.m8face.utils.BOTTOM_COMPLICATION_ID -import dev.rdnt.m8face.utils.HORIZONTAL_COMPLICATION_HEIGHT -import dev.rdnt.m8face.utils.HORIZONTAL_COMPLICATION_LEFT_BOUND -import dev.rdnt.m8face.utils.HORIZONTAL_COMPLICATION_OFFSET -import dev.rdnt.m8face.utils.HORIZONTAL_COMPLICATION_RIGHT_BOUND -import dev.rdnt.m8face.utils.LEFT_COMPLICATION_ID -import dev.rdnt.m8face.utils.RIGHT_COMPLICATION_ID -import dev.rdnt.m8face.utils.TOP_COMPLICATION_ID -import dev.rdnt.m8face.utils.VERTICAL_COMPLICATION_OFFSET -import dev.rdnt.m8face.utils.VERTICAL_COMPLICATION_TOP_BOUND -import dev.rdnt.m8face.utils.VERTICAL_COMPLICATION_WIDTH +import dev.rdnt.m8face.utils.* import kotlinx.coroutines.launch @Preview() @@ -152,9 +177,9 @@ fun ScrollableColumn( } Scaffold( - Modifier - .onPreRotaryScrollEvent { false } - .fillMaxSize(), + Modifier + .onPreRotaryScrollEvent { false } + .fillMaxSize(), positionIndicator = { PositionIndicator( state = state, @@ -171,69 +196,69 @@ fun ScrollableColumn( Box( modifier = Modifier - .fillMaxSize() - .scrollable( - orientation = Orientation.Vertical, - state = rememberScrollableState { delta -> - val value = position.value - delta - - - if (value < 0f) { - if (position.value != 0f) { - view.performHapticFeedback(LONG_PRESS) - } - - position.value = 0f - } else if (value > maxValue) { - if (position.value != maxValue - 1) { - view.performHapticFeedback(LONG_PRESS) + .fillMaxSize() + .scrollable( + orientation = Orientation.Vertical, + state = rememberScrollableState { delta -> + val value = position.value - delta + + + if (value < 0f) { + if (position.value != 0f) { + view.performHapticFeedback(LONG_PRESS) + } + + position.value = 0f + } else if (value > maxValue) { + if (position.value != maxValue - 1) { + view.performHapticFeedback(LONG_PRESS) + } + + position.value = maxValue - 1 + } else { + position.value = value + val selected = (value / threshold) + .toInt() + .coerceAtMost(items - 1) + if (selected != selectedItem.value) { + selectedItem.value = selected + view.performHapticFeedback(KEYBOARD_TAP) + } + } + + delta } - - position.value = maxValue - 1 - } else { - position.value = value - val selected = (value / threshold) - .toInt() - .coerceAtMost(items - 1) - if (selected != selectedItem.value) { - selectedItem.value = selected - view.performHapticFeedback(KEYBOARD_TAP) + ) + .onRotaryScrollEvent { + val value = position.value + it.verticalScrollPixels + + if (value < 0f) { + if (position.value != 0f) { + view.performHapticFeedback(LONG_PRESS) + } + + position.value = 0f + } else if (value > maxValue) { + if (position.value != maxValue - 1) { + view.performHapticFeedback(LONG_PRESS) + } + + position.value = maxValue - 1 + } else { + position.value = value + val selected = (value / threshold) + .toInt() + .coerceAtMost(items - 1) + if (selected != selectedItem.value) { + selectedItem.value = selected + view.performHapticFeedback(KEYBOARD_TAP) + } } - } - delta - } - ) - .onRotaryScrollEvent { - val value = position.value + it.verticalScrollPixels - - if (value < 0f) { - if (position.value != 0f) { - view.performHapticFeedback(LONG_PRESS) - } - - position.value = 0f - } else if (value > maxValue) { - if (position.value != maxValue - 1) { - view.performHapticFeedback(LONG_PRESS) - } - - position.value = maxValue - 1 - } else { - position.value = value - val selected = (value / threshold) - .toInt() - .coerceAtMost(items - 1) - if (selected != selectedItem.value) { - selectedItem.value = selected - view.performHapticFeedback(KEYBOARD_TAP) - } + true } - - true - } - .focusRequester(focusRequester) - .focusable(), + .focusRequester(focusRequester) + .focusable(), ) } @@ -251,6 +276,10 @@ class WatchFaceConfigActivity : ComponentActivity() { ) } + private val layoutStyles by lazy { + enumValues() + } + private val colorStyles by lazy { enumValues() } @@ -268,6 +297,7 @@ class WatchFaceConfigActivity : ComponentActivity() { lifecycleScope.launch { stateHolder + layoutStyles colorStyles ambientStyles secondsStyles @@ -276,6 +306,7 @@ class WatchFaceConfigActivity : ComponentActivity() { setContent { WatchfaceConfigApp( stateHolder, + layoutStyles, colorStyles, ambientStyles, secondsStyles, @@ -288,6 +319,7 @@ class WatchFaceConfigActivity : ComponentActivity() { @Composable fun WatchfaceConfigApp( stateHolder: WatchFaceConfigStateHolder, + layoutStyles: Array, colorStyles: Array, ambientStyles: Array, secondsStyles: Array, @@ -306,6 +338,8 @@ fun WatchfaceConfigApp( when (val state = uiState) { is WatchFaceConfigStateHolder.EditWatchFaceUiState.Success -> { val bitmap = state.userStylesAndPreview.previewImage.asImageBitmap() + val layoutIndex = + layoutStyles.indexOfFirst { it.id == state.userStylesAndPreview.layoutStyleId } val colorIndex = colorStyles.indexOfFirst { it.id == state.userStylesAndPreview.colorStyleId } val ambientStyleIndex = @@ -313,35 +347,35 @@ fun WatchfaceConfigApp( val secondsStyleIndex = secondsStyles.indexOfFirst { it.id == state.userStylesAndPreview.secondsStyleId } val militaryTimeEnabled = state.userStylesAndPreview.militaryTime - val bigAmbientEnabled = state.userStylesAndPreview.bigAmbient Box( - Modifier - .fillMaxSize() - .zIndex(1f) - .background(Black) + Modifier + .fillMaxSize() + .zIndex(1f) + .background(Black) ) { ConfigScaffold( stateHolder, + layoutStyles, colorStyles, ambientStyles, secondsStyles, bitmap, + layoutIndex, colorIndex, ambientStyleIndex, secondsStyleIndex, militaryTimeEnabled, - bigAmbientEnabled, ) } } else -> { Box( - Modifier - .fillMaxSize() - .zIndex(2f) - .background(Black) + Modifier + .fillMaxSize() + .zIndex(2f) + .background(Black) ) { SplashScreen(screenIsRound) } @@ -355,22 +389,25 @@ fun WatchfaceConfigApp( @Composable fun ConfigScaffold( stateHolder: WatchFaceConfigStateHolder, + layoutStyles: Array, colorStyles: Array, ambientStyles: Array, secondsStyles: Array, bitmap: ImageBitmap, + layoutIndex: Int, colorIndex: Int, ambientStyleIndex: Int, secondsStyleIndex: Int, militaryTimeEnabled: Boolean, - bigAmbientEnabled: Boolean, ) { Log.d( "Editor", - "ConfigScaffold($colorIndex, $ambientStyleIndex, $militaryTimeEnabled, $bigAmbientEnabled)" + "ConfigScaffold($layoutIndex, $colorIndex, $ambientStyleIndex, $militaryTimeEnabled)" ) - val pagerState = rememberPagerState { 5 } + val pagerState = rememberPagerState( + pageCount = { 6 } + ) Scaffold( positionIndicator = { @@ -387,113 +424,116 @@ fun ConfigScaffold( HorizontalPageIndicator( pageIndicatorState = pageIndicatorState, - Modifier - .padding(4.dp) - .zIndex(4f) + Modifier + .padding(4.dp) + .zIndex(4f) ) }, modifier = Modifier - .onPreRotaryScrollEvent { false } - .fillMaxSize() - .zIndex(1f) + .onPreRotaryScrollEvent { false } + .fillMaxSize() + .zIndex(1f) ) { val focusRequester0 = remember { FocusRequester() } val focusRequester1 = remember { FocusRequester() } val focusRequester2 = remember { FocusRequester() } + val focusRequester3 = remember { FocusRequester() } HorizontalPager( flingBehavior = PagerDefaults.flingBehavior(state = pagerState), state = pagerState, modifier = Modifier - .onPreRotaryScrollEvent { pagerState.currentPage != 0 && pagerState.currentPage != 1 && pagerState.currentPage != 2 } - .zIndex(3f), // don't ask + .onPreRotaryScrollEvent { pagerState.currentPage != 0 && pagerState.currentPage != 1 && pagerState.currentPage != 2 && pagerState.currentPage != 3 } + .zIndex(3f), // don't ask key = { it } ) { page -> when (page) { - 0 -> ColorStyleSelect(focusRequester0, stateHolder, colorStyles, colorIndex) - 1 -> SecondsStyleSelect(focusRequester1, stateHolder, secondsStyles, secondsStyleIndex) - 2 -> AmbientStyleSelect(focusRequester2, stateHolder, ambientStyles, ambientStyleIndex) - 3 -> ComplicationPicker(stateHolder) - 4 -> Options(stateHolder, militaryTimeEnabled, bigAmbientEnabled) + 0 -> LayoutStyleSelect(focusRequester0, stateHolder, layoutStyles, layoutIndex) + 1 -> ColorStyleSelect(focusRequester1, stateHolder, colorStyles, colorIndex) + 2 -> SecondsStyleSelect(focusRequester2, stateHolder, secondsStyles, secondsStyleIndex) + 3 -> AmbientStyleSelect(focusRequester3, stateHolder, ambientStyles, ambientStyleIndex) + 4 -> ComplicationPicker(stateHolder, layoutIndex) + 5 -> Options(stateHolder, militaryTimeEnabled) } } - if (pagerState.currentPage == 2) { // special background for third page (ambient style) + if (pagerState.currentPage == 3) { // special background for third page (ambient style) var id = 0 val current = ambientStyles.indexOfFirst { it.id == (stateHolder.uiState.value as WatchFaceConfigStateHolder.EditWatchFaceUiState.Success).userStylesAndPreview.ambientStyleId } - if (current == 0) { - if (!militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_outline - } else if (militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_outline_military - } else if (!militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_outline_big - } else if (militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_outline_military_big - } - } else if (current == 1) { - if (!militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_bold - } else if (militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_bold_military - } else if (!militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_bold_big - } else if (militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_bold_military_big - } - } else if (current == 2) { - if (!militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_filled - } else if (militaryTimeEnabled && !bigAmbientEnabled) { - id = R.drawable.preview_ambient_filled_military - } else if (!militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_filled_big - } else if (militaryTimeEnabled && bigAmbientEnabled) { - id = R.drawable.preview_ambient_filled_military_big - } - } - - Image( - painterResource(id = id), - contentDescription = "Preview", - colorFilter = ColorFilter.tint( - colorResource(id = colorStyles[colorIndex].primaryColorId), - BlendMode.Darken - ), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .zIndex(1f) - .clip(TopHalfRectShape) - .scale(1f) - ) - Image( - painterResource(id = id), - contentDescription = "Preview", - colorFilter = ColorFilter.tint( - colorResource(id = colorStyles[colorIndex].secondaryColorId), - BlendMode.Darken - ), - contentScale = ContentScale.Crop, - modifier = Modifier - .fillMaxSize() - .zIndex(1f) - .clip(BottomHalfRectShape) - .scale(1f) - ) + // TODO: @rdnt fix + Preview(bitmap) +// if (current == 0) { +// if (!militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_outline +// } else if (militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_outline_military +// } else if (!militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_outline_big +// } else if (militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_outline_military_big +// } +// } else if (current == 1) { +// if (!militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_bold +// } else if (militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_bold_military +// } else if (!militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_bold_big +// } else if (militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_bold_military_big +// } +// } else if (current == 2) { +// if (!militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_filled +// } else if (militaryTimeEnabled && !bigAmbientEnabled) { +// id = R.drawable.preview_ambient_filled_military +// } else if (!militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_filled_big +// } else if (militaryTimeEnabled && bigAmbientEnabled) { +// id = R.drawable.preview_ambient_filled_military_big +// } +// } +// Image( +// painterResource(id = id), +// contentDescription = "Preview", +// colorFilter = ColorFilter.tint( +// colorResource(id = colorStyles[colorIndex].primaryColorId), +// BlendMode.Darken +// ), +// contentScale = ContentScale.Crop, +// modifier = Modifier +// .fillMaxSize() +// .zIndex(1f) +// .clip(TopHalfRectShape) +// .scale(1f) +// ) +// Image( +// painterResource(id = id), +// contentDescription = "Preview", +// colorFilter = ColorFilter.tint( +// colorResource(id = colorStyles[colorIndex].secondaryColorId), +// BlendMode.Darken +// ), +// contentScale = ContentScale.Crop, +// modifier = Modifier +// .fillMaxSize() +// .zIndex(1f) +// .clip(BottomHalfRectShape) +// .scale(1f) +// ) } else { Preview(bitmap) } - Overlay(pagerState) +// Overlay(pagerState) // TODO fix LaunchedEffect(pagerState.currentPage) { Log.d("Editor", "LaunchedEffect(${pagerState.currentPage})") @@ -504,6 +544,8 @@ fun ConfigScaffold( focusRequester1.requestFocus() } else if (pagerState.currentPage == 2) { focusRequester2.requestFocus() + } else if (pagerState.currentPage == 3) { + focusRequester3.requestFocus() } } } @@ -522,18 +564,18 @@ fun Overlay( } Box( modifier = Modifier - .fillMaxSize() - .alpha(opacity * 0.75f) - .background( - brush = Brush.verticalGradient( - startY = 192f, - colors = listOf( - Color(0x00000000), - Color(0xFF000000), - ) + .fillMaxSize() + .alpha(opacity * 0.75f) + .background( + brush = Brush.verticalGradient( + startY = 192f, + colors = listOf( + Color(0x00000000), + Color(0xFF000000), + ) + ) ) - ) - .zIndex(2f), + .zIndex(2f), ) } @@ -562,63 +604,63 @@ fun Overlay( ) Box( - Modifier - .zIndex(2f) - .fillMaxSize() - .border(10.dp, Color(ringColor), CircleShape) + Modifier + .zIndex(2f) + .fillMaxSize() + .border(10.dp, Color(ringColor), CircleShape) ) Column( - Modifier - .fillMaxSize() - .padding(10.dp) - .clip(CircleShape) - .zIndex(2f) + Modifier + .fillMaxSize() + .padding(10.dp) + .clip(CircleShape) + .zIndex(2f) ) { Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_OFFSET + HORIZONTAL_COMPLICATION_HEIGHT) - .fillMaxWidth() - .alpha(opacity2 * 0.75f) - .background(Black) + Modifier + .weight(HORIZONTAL_COMPLICATION_OFFSET + HORIZONTAL_COMPLICATION_HEIGHT) + .fillMaxWidth() + .alpha(opacity2 * 0.75f) + .background(Black) ) Row( - Modifier - .weight(1f - HORIZONTAL_COMPLICATION_OFFSET * 2 - HORIZONTAL_COMPLICATION_HEIGHT * 2 + extend) - .fillMaxWidth() + Modifier + .weight(1f - HORIZONTAL_COMPLICATION_OFFSET * 2 - HORIZONTAL_COMPLICATION_HEIGHT * 2 + extend) + .fillMaxWidth() ) { Box( - Modifier - .weight(VERTICAL_COMPLICATION_OFFSET + VERTICAL_COMPLICATION_WIDTH) - .fillMaxHeight() - .alpha(opacity2 * 0.75f) - .background(Black) + Modifier + .weight(VERTICAL_COMPLICATION_OFFSET + VERTICAL_COMPLICATION_WIDTH) + .fillMaxHeight() + .alpha(opacity2 * 0.75f) + .background(Black) ) Box( - Modifier - .weight(1f - VERTICAL_COMPLICATION_OFFSET * 2 - VERTICAL_COMPLICATION_WIDTH * 2 + extend) - .fillMaxHeight() - .alpha(opacity * 0.75f) - .background(Black) + Modifier + .weight(1f - VERTICAL_COMPLICATION_OFFSET * 2 - VERTICAL_COMPLICATION_WIDTH * 2 + extend) + .fillMaxHeight() + .alpha(opacity * 0.75f) + .background(Black) ) Box( - Modifier - .weight(VERTICAL_COMPLICATION_OFFSET + VERTICAL_COMPLICATION_WIDTH) - .fillMaxHeight() - .alpha(opacity2 * 0.75f) - .background(Black) + Modifier + .weight(VERTICAL_COMPLICATION_OFFSET + VERTICAL_COMPLICATION_WIDTH) + .fillMaxHeight() + .alpha(opacity2 * 0.75f) + .background(Black) ) } Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_OFFSET + HORIZONTAL_COMPLICATION_HEIGHT) - .fillMaxWidth() - .alpha(opacity2 * 0.75f) - .background(Black) + Modifier + .weight(HORIZONTAL_COMPLICATION_OFFSET + HORIZONTAL_COMPLICATION_HEIGHT) + .fillMaxWidth() + .alpha(opacity2 * 0.75f) + .background(Black) ) } } @@ -628,7 +670,6 @@ fun Overlay( fun Options( stateHolder: WatchFaceConfigStateHolder, militaryTime: Boolean, - bigAmbient: Boolean, ) { Box( Modifier @@ -641,9 +682,9 @@ fun Options( ) { ToggleChip( modifier = Modifier - .fillMaxWidth() - .padding(12.dp, 0.dp) - .height((40.dp)), + .fillMaxWidth() + .padding(12.dp, 0.dp) + .height((40.dp)), checked = militaryTime, colors = ToggleChipDefaults.toggleChipColors( checkedStartBackgroundColor = Transparent, @@ -663,7 +704,7 @@ fun Options( label = { Text( text = "Military Time", - fontSize = 14.sp, + fontSize = 12.sp, fontWeight = Medium, fontFamily = Default, color = White @@ -671,37 +712,6 @@ fun Options( }, ) - ToggleChip( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp, 0.dp) - .height((40.dp)), - checked = bigAmbient, - colors = ToggleChipDefaults.toggleChipColors( - checkedStartBackgroundColor = Transparent, - checkedEndBackgroundColor = Transparent, - uncheckedStartBackgroundColor = Transparent, - uncheckedEndBackgroundColor = Transparent, - ), - onCheckedChange = { - stateHolder.setBigAmbient(it) - }, - toggleControl = { - Switch( - modifier = Modifier.padding(0.dp), - checked = bigAmbient, - ) - }, - label = { - Text( - text = "Big Ambient", - fontSize = 14.sp, - fontWeight = Medium, - fontFamily = Default, - color = White - ) - }, - ) } } } @@ -750,8 +760,8 @@ fun Preview( bitmap = bitmap, contentDescription = "Preview", modifier = Modifier - .fillMaxSize() - .zIndex(1f) + .fillMaxSize() + .zIndex(1f) ) } @@ -788,6 +798,41 @@ fun ColorStyleSelect( } +} + +@Composable +fun LayoutStyleSelect( + focusRequester: FocusRequester, + stateHolder: WatchFaceConfigStateHolder, + layoutStyles: Array, + layoutIndex: Int, +) { + Log.d("Editor", "LayoutStyleSelect($layoutIndex)") + + val layoutIdsSize = remember { layoutStyles.size } + + Box( + Modifier + .fillMaxSize() + ) { + ScrollableColumn( + focusRequester, + layoutIdsSize, + 100f, + layoutIndex, + ) { itemIndex -> + val current = + layoutStyles.indexOfFirst { it.id == (stateHolder.uiState.value as WatchFaceConfigStateHolder.EditWatchFaceUiState.Success).userStylesAndPreview.colorStyleId } + if (current != itemIndex) { + Log.d("Editor", "setLayoutStyle(${layoutStyles[itemIndex].id})") + stateHolder.setLayoutStyle(layoutStyles[itemIndex].id) + } + } + + ColorName(stringResource(layoutStyles[layoutIndex].nameResourceId)) + } + + } @Composable @@ -910,10 +955,10 @@ fun ColorPicker( alignment = Alignment.CenterVertically ), modifier = Modifier - .fillMaxSize() - .focusable() - .focusRequester(focusRequester) - .rotaryWithSnap(adapter, focusRequester), + .fillMaxSize() + .focusable() + .focusRequester(focusRequester) + .rotaryWithSnap(adapter, focusRequester), state = state, autoCentering = AutoCenteringParams(itemIndex = 0, itemOffset = 0), scalingParams = ScalingLazyColumnDefaults.scalingParams( @@ -939,136 +984,387 @@ fun ColorPicker( @Composable fun ComplicationPicker( stateHolder: WatchFaceConfigStateHolder, + layoutIndex: Int, ) { Log.d("Editor", "ComplicationPicker()") - Box ( + Box( Modifier .fillMaxSize() ) { - Column( - Modifier - .fillMaxSize() - ) { - Row( - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_OFFSET, true) - ) {} - Row( - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_HEIGHT, true) - ) { - Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() + + when (layoutIndex) { + 0, 1, 2, 3 -> { + ComplicationButton( + stateHolder, + HOUR_COMPLICATION_ID, + RectF( + HOUR_COMPLICATION_LEFT_BOUND + 6f / 384f, + HOUR_COMPLICATION_TOP_BOUND + 6f / 384f, + HOUR_COMPLICATION_RIGHT_BOUND - 6f / 384f, + HOUR_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), ) - OutlinedButton( - onClick = { stateHolder.setComplication(TOP_COMPLICATION_ID) }, - border = outlinedButtonBorder( - Color(0xFF5c6063), - borderWidth = 2.dp + ComplicationButton( + stateHolder, + MINUTE_COMPLICATION_ID, + RectF( + MINUTE_COMPLICATION_LEFT_BOUND + 6f / 384f, + MINUTE_COMPLICATION_TOP_BOUND + 6f / 384f, + MINUTE_COMPLICATION_RIGHT_BOUND - 6f / 384f, + MINUTE_COMPLICATION_BOTTOM_BOUND - 6f / 384f, ), - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_RIGHT_BOUND - HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() - ) {} - Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() ) } - Box( - modifier = Modifier - .weight(1f - HORIZONTAL_COMPLICATION_OFFSET * 2 - HORIZONTAL_COMPLICATION_HEIGHT * 2, true) - ) - Row( - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_HEIGHT, true) - ) { - Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() + 4 -> { + ComplicationButton( + stateHolder, + HOUR_COMPLICATION_ID, + RectF( + HOUR_SPORT_COMPLICATION_LEFT_BOUND + 6f / 384f, + HOUR_SPORT_COMPLICATION_TOP_BOUND + 6f / 384f, + HOUR_SPORT_COMPLICATION_RIGHT_BOUND - 6f / 384f, + HOUR_SPORT_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), ) - OutlinedButton( - onClick = { stateHolder.setComplication(BOTTOM_COMPLICATION_ID) }, - border = outlinedButtonBorder( - Color(0xFF5c6063), - borderWidth = 2.dp + ComplicationButton( + stateHolder, + MINUTE_COMPLICATION_ID, + RectF( + MINUTE_SPORT_COMPLICATION_LEFT_BOUND, + MINUTE_SPORT_COMPLICATION_TOP_BOUND + 6f / 384f, + MINUTE_SPORT_COMPLICATION_RIGHT_BOUND - 6f / 384f, + MINUTE_SPORT_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), + ) + } + 5 -> { + ComplicationButton( + stateHolder, + HOUR_COMPLICATION_ID, + RectF( + HOUR_FOCUS_COMPLICATION_LEFT_BOUND + 6f / 384f, + HOUR_FOCUS_COMPLICATION_TOP_BOUND + 6f / 384f, + HOUR_FOCUS_COMPLICATION_RIGHT_BOUND - 6f / 384f, + HOUR_FOCUS_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), + ) + ComplicationButton( + stateHolder, + MINUTE_COMPLICATION_ID, + RectF( + MINUTE_FOCUS_COMPLICATION_LEFT_BOUND + 6f / 384f, + MINUTE_FOCUS_COMPLICATION_TOP_BOUND + 6f / 384f, + MINUTE_FOCUS_COMPLICATION_RIGHT_BOUND - 6f / 384f, + MINUTE_FOCUS_COMPLICATION_BOTTOM_BOUND - 6f / 384f, ), - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_RIGHT_BOUND - HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() - ) {} - Box( - Modifier - .weight(HORIZONTAL_COMPLICATION_LEFT_BOUND, true) - .fillMaxHeight() ) } - Row( - modifier = Modifier - .weight(HORIZONTAL_COMPLICATION_OFFSET, true) - ) {} } - Column( - Modifier - .fillMaxSize() - ) { - Box( - modifier = Modifier - .weight(VERTICAL_COMPLICATION_TOP_BOUND, true) - ) + when (layoutIndex) { + 0 -> { + ComplicationButton( + stateHolder, + TOP_COMPLICATION_ID, + RectF( + TOP_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_TOP_BOUND, + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, + ), + ) - Row( - modifier = Modifier - .weight(1f - VERTICAL_COMPLICATION_TOP_BOUND * 2, true) - ) { - Box( - Modifier - .weight(VERTICAL_COMPLICATION_OFFSET, true) - .fillMaxHeight() + ComplicationButton( + stateHolder, + BOTTOM_COMPLICATION_ID, + RectF( + BOTTOM_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_TOP_BOUND, + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, + ), ) - OutlinedButton( - onClick = { stateHolder.setComplication(LEFT_COMPLICATION_ID) }, - border = outlinedButtonBorder( - Color(0xFF5c6063), - borderWidth = 2.dp + + ComplicationButton( + stateHolder, + LEFT_COMPLICATION_ID, + RectF( + LEFT_COMPLICATION_LEFT_BOUND, + VERTICAL_COMPLICATION_TOP_BOUND, + LEFT_COMPLICATION_RIGHT_BOUND, + VERTICAL_COMPLICATION_BOTTOM_BOUND, + ), + ) + } + 1 -> { + ComplicationButton( + stateHolder, + TOP_COMPLICATION_ID, + RectF( + TOP_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_TOP_BOUND, + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, ), - modifier = Modifier - .weight(VERTICAL_COMPLICATION_WIDTH, true) - .fillMaxHeight(), - ) {} - Box( - Modifier - .weight(1f - VERTICAL_COMPLICATION_WIDTH * 2 - VERTICAL_COMPLICATION_OFFSET * 2, true) - .fillMaxHeight() ) - OutlinedButton( - onClick = { stateHolder.setComplication(RIGHT_COMPLICATION_ID) }, + + ComplicationButton( + stateHolder, + BOTTOM_COMPLICATION_ID, + RectF( + BOTTOM_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_TOP_BOUND, + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + LEFT_COMPLICATION_ID, + RectF( + LEFT_COMPLICATION_LEFT_BOUND, + VERTICAL_COMPLICATION_TOP_BOUND, + LEFT_COMPLICATION_RIGHT_BOUND, + VERTICAL_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + RIGHT_COMPLICATION_ID, + RectF( + RIGHT_COMPLICATION_LEFT_BOUND, + VERTICAL_COMPLICATION_TOP_BOUND, + RIGHT_COMPLICATION_RIGHT_BOUND, + VERTICAL_COMPLICATION_BOTTOM_BOUND, + ), + ) + } + + 2 -> { + ComplicationButton( + stateHolder, + TOP_COMPLICATION_ID, + RectF( + TOP_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_TOP_BOUND, + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + BOTTOM_COMPLICATION_ID, + RectF( + BOTTOM_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_TOP_BOUND, + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, + RectF( + TOP_LEFT_COMPLICATION_LEFT_BOUND - 3f / 384f, + TOP_LEFT_COMPLICATION_TOP_BOUND - 6f / 384f, + TOP_LEFT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + TOP_LEFT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, + RectF( + BOTTOM_LEFT_COMPLICATION_LEFT_BOUND - 3f / 384f, + BOTTOM_LEFT_COMPLICATION_TOP_BOUND - 6f / 384f, + BOTTOM_LEFT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + BOTTOM_LEFT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + } + + 3-> { + ComplicationButton( + stateHolder, + TOP_COMPLICATION_ID, + RectF( + TOP_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_TOP_BOUND, + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + BOTTOM_COMPLICATION_ID, + RectF( + BOTTOM_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_TOP_BOUND, + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, + RectF( + TOP_LEFT_COMPLICATION_LEFT_BOUND - 3f / 384f, + TOP_LEFT_COMPLICATION_TOP_BOUND - 6f / 384f, + TOP_LEFT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + TOP_LEFT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, + RectF( + BOTTOM_LEFT_COMPLICATION_LEFT_BOUND - 3f / 384f, + BOTTOM_LEFT_COMPLICATION_TOP_BOUND - 6f / 384f, + BOTTOM_LEFT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + BOTTOM_LEFT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID, + RectF( + TOP_RIGHT_COMPLICATION_LEFT_BOUND - 3f / 384f, + TOP_RIGHT_COMPLICATION_TOP_BOUND - 6f / 384f, + TOP_RIGHT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + TOP_RIGHT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + + ComplicationButton( + stateHolder, + COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID, + RectF( + BOTTOM_RIGHT_COMPLICATION_LEFT_BOUND - 3f / 384f, + BOTTOM_RIGHT_COMPLICATION_TOP_BOUND - 6f / 384f, + BOTTOM_RIGHT_COMPLICATION_RIGHT_BOUND + 3f / 384f, + BOTTOM_RIGHT_COMPLICATION_BOTTOM_BOUND + 6f / 384f, + ), + ) + } + + 4 -> { + ComplicationButton( + stateHolder, + TOP_COMPLICATION_ID, + RectF( + TOP_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_TOP_BOUND, + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + BOTTOM_COMPLICATION_ID, + RectF( + BOTTOM_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_TOP_BOUND, + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, + ), + ) + + ComplicationButton( + stateHolder, + RIGHT_TEXT_COMPLICATION_ID, + RectF( + RIGHT_TEXT_COMPLICATION_LEFT_BOUND, + RIGHT_TEXT_COMPLICATION_TOP_BOUND, + RIGHT_TEXT_COMPLICATION_RIGHT_BOUND, + RIGHT_TEXT_COMPLICATION_BOTTOM_BOUND, + ), + ) + } + + 5 -> { + ComplicationButton( + stateHolder, + FOCUS_LEFT_ICON_COMPLICATION_ID, + RectF( + LEFT_ICON_COMPLICATION_LEFT_BOUND - 9f / 384f, + LEFT_ICON_COMPLICATION_TOP_BOUND + 6f / 384f, + LEFT_ICON_COMPLICATION_RIGHT_BOUND + 9f / 384f, + LEFT_ICON_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), + ) + + ComplicationButton( + stateHolder, + FOCUS_RIGHT_ICON_COMPLICATION_ID, + RectF( + RIGHT_ICON_COMPLICATION_LEFT_BOUND - 9f / 384f, + RIGHT_ICON_COMPLICATION_TOP_BOUND + 6f / 384f, + RIGHT_ICON_COMPLICATION_RIGHT_BOUND + 9f / 384f, + RIGHT_ICON_COMPLICATION_BOTTOM_BOUND - 6f / 384f, + ), + ) + } + } + + + } +} + +@Composable +fun ComplicationButton( + stateHolder: WatchFaceConfigStateHolder, + id: Int, + bounds: RectF, +// left: Float, top: Float, right: Float, bottom: Float +) { +// var left= bounds.left - 6f / 384f +// var right= bounds.right + 6f / 384f +// var top= bounds.top - 6f / 384f +// var bottom= bounds.bottom + 6f / 384f + + var left= bounds.left + var right= bounds.right + var top= bounds.top + var bottom= bounds.bottom + +// Log.d("Editor", "ComplicationButton(${left}, ${top}, ${right}, ${bottom})") + + Row(Modifier.fillMaxSize()) { + Box(Modifier.weight(left, true)) + Column( + Modifier + .fillMaxSize() + .weight(right - left, true) + ) { + Box(Modifier.weight(top, true)) + + OutlinedButton( + onClick = { stateHolder.setComplication(id) }, border = outlinedButtonBorder( Color(0xFF5c6063), borderWidth = 2.dp ), +// shape = RoundedCornerShape(16.dp), modifier = Modifier - .weight(VERTICAL_COMPLICATION_WIDTH, true) - .fillMaxHeight(), + .weight(bottom - top, true) +// .background(Color.Blue) + .fillMaxSize(), ) {} - Box( - Modifier - .weight(VERTICAL_COMPLICATION_OFFSET, true) - .fillMaxHeight() - ) - } - Box( - modifier = Modifier - .weight(VERTICAL_COMPLICATION_TOP_BOUND, true) - ) + Box(Modifier.weight(1f - bottom, true)) } + Box(Modifier.weight(1f - right, true)) } } @@ -1087,11 +1383,11 @@ fun ColorName(name: String) { text = name, textAlign = TextAlign.Center, modifier = Modifier - .weight(4f) - .height(20.dp) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFF202124)) - .padding(12.dp, 1.dp), + .weight(4f) + .height(20.dp) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFF202124)) + .padding(12.dp, 1.dp), fontSize = 12.sp, fontWeight = Medium, fontFamily = Default, @@ -1126,10 +1422,10 @@ fun Label(label: String) { text = label, textAlign = TextAlign.Center, modifier = Modifier - .weight(4f) - .clip(RoundedCornerShape(10.dp)) - .background(Color(0xFF000000)) - .padding(12.dp, 1.dp), + .weight(4f) + .clip(RoundedCornerShape(10.dp)) + .background(Color(0xFF000000)) + .padding(12.dp, 1.dp), fontSize = 12.sp, fontWeight = Medium, fontFamily = Default, @@ -1147,10 +1443,10 @@ fun ColorItem() { Box( modifier = Modifier - .padding(0.dp, 0.dp) - .height(48.dp) - .width(0.dp) - .alpha(0.0F) + .padding(0.dp, 0.dp) + .height(48.dp) + .width(0.dp) + .alpha(0.0F) ) } @@ -1160,12 +1456,12 @@ fun DebugColorItem(colorId: Int?) { Box( modifier = Modifier - .padding(0.dp, 0.dp) - .height(48.dp) - .fillMaxWidth() - .alpha(0.0F) + .padding(0.dp, 0.dp) + .height(48.dp) + .fillMaxWidth() + .alpha(0.0F) // .alpha(0.2F) - .background(colorId?.let { colorResource(colorId) } ?: Black) + .background(colorId?.let { colorResource(colorId) } ?: Black) ) } diff --git a/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigStateHolder.kt b/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigStateHolder.kt index 580185d..31ec2f7 100644 --- a/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigStateHolder.kt +++ b/app/src/main/java/dev/rdnt/m8face/editor/WatchFaceConfigStateHolder.kt @@ -60,11 +60,11 @@ class WatchFaceConfigStateHolder( private lateinit var editorSession: EditorSession // Keys from Watch Face Data Structure + private lateinit var layoutStyleKey: UserStyleSetting.ComplicationSlotsUserStyleSetting private lateinit var colorStyleKey: UserStyleSetting.ListUserStyleSetting private lateinit var ambientStyleKey: UserStyleSetting.ListUserStyleSetting private lateinit var secondsStyleKey: UserStyleSetting.ListUserStyleSetting private lateinit var militaryTimeKey: UserStyleSetting.BooleanUserStyleSetting - private lateinit var bigAmbientKey: UserStyleSetting.BooleanUserStyleSetting val uiState: StateFlow = flow { @@ -99,6 +99,10 @@ class WatchFaceConfigStateHolder( // Loops through user styles and retrieves user editable styles. for (setting in userStyleSchema.userStyleSettings) { when (setting.id.toString()) { + LAYOUT_STYLE_SETTING -> { + layoutStyleKey = setting as UserStyleSetting.ComplicationSlotsUserStyleSetting + } + COLOR_STYLE_SETTING -> { colorStyleKey = setting as UserStyleSetting.ListUserStyleSetting } @@ -114,10 +118,6 @@ class WatchFaceConfigStateHolder( MILITARY_TIME_SETTING -> { militaryTimeKey = setting as UserStyleSetting.BooleanUserStyleSetting } - - BIG_AMBIENT_SETTING -> { - bigAmbientKey = setting as UserStyleSetting.BooleanUserStyleSetting - } } } } @@ -132,6 +132,8 @@ class WatchFaceConfigStateHolder( Log.d(TAG, "createWatchFacePreview()") + // actual watch uses this date and not 09:30:36. changed to 22 instead of 10 + // for military time to be visible val instant = LocalDateTime.parse("2020-10-10T22:09:36") .atZone(ZoneId.of("UTC")) .toInstant() @@ -145,6 +147,9 @@ class WatchFaceConfigStateHolder( complicationsPreviewData ) + val layoutStyle = + userStyle[layoutStyleKey] as UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption + val colorStyle = userStyle[colorStyleKey] as UserStyleSetting.ListUserStyleSetting.ListOption @@ -157,26 +162,23 @@ class WatchFaceConfigStateHolder( val militaryTime = userStyle[militaryTimeKey] as UserStyleSetting.BooleanUserStyleSetting.BooleanOption - val bigAmbient = - userStyle[bigAmbientKey] as UserStyleSetting.BooleanUserStyleSetting.BooleanOption - return UserStylesAndPreview( + layoutStyleId = layoutStyle.id.toString(), colorStyleId = colorStyle.id.toString(), ambientStyleId = ambientStyle.id.toString(), secondsStyleId = secondsStyle.id.toString(), militaryTime = militaryTime.value, - bigAmbient = bigAmbient.value, previewImage = bitmap, ) } - fun setComplication(complicationLocation: Int) { + fun setComplication(complicationId: Int) { if (launchInProgress) { return } launchInProgress = true - val complicationSlotId = when (complicationLocation) { + val complicationSlotId = when (complicationId) { LEFT_COMPLICATION_ID -> { LEFT_COMPLICATION_ID } @@ -193,6 +195,42 @@ class WatchFaceConfigStateHolder( BOTTOM_COMPLICATION_ID } + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID -> { + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID + } + + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID -> { + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID + } + + COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID -> { + COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID + } + + COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID -> { + COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID + } + + FOCUS_LEFT_ICON_COMPLICATION_ID -> { + FOCUS_LEFT_ICON_COMPLICATION_ID + } + + FOCUS_RIGHT_ICON_COMPLICATION_ID -> { + FOCUS_RIGHT_ICON_COMPLICATION_ID + } + + RIGHT_TEXT_COMPLICATION_ID -> { + RIGHT_TEXT_COMPLICATION_ID + } + + HOUR_COMPLICATION_ID -> { + HOUR_COMPLICATION_ID + } + + MINUTE_COMPLICATION_ID -> { + MINUTE_COMPLICATION_ID + } + else -> { launchInProgress = false return @@ -208,6 +246,28 @@ class WatchFaceConfigStateHolder( ) } + fun setLayoutStyle(layoutStyleId: String) { + val userStyleSettingList = editorSession.userStyleSchema.userStyleSettings + // TODO @rdnt editorSession.userStyleSchema.rootUserStyleSettings + + // Loops over all UserStyleSettings (basically the keys in the map) to find the setting for + // the color style (which contains all the possible options for that style setting). + for (userStyleSetting in userStyleSettingList) { + if (userStyleSetting.id == UserStyleSetting.Id(LAYOUT_STYLE_SETTING)) { + val layoutUserStyleSetting = + userStyleSetting as UserStyleSetting.ComplicationSlotsUserStyleSetting + + // Loops over the UserStyleSetting.Option colors (all possible values for the key) + // to find the matching option, and if it exists, sets it as the color style. + for (layoutOptions in layoutUserStyleSetting.options) { + if (layoutOptions.id.toString() == layoutStyleId) { + setUserStyleOption(layoutStyleKey, layoutOptions) + } + } + } + } + } + fun setColorStyle(colorStyleId: String) { val userStyleSettingList = editorSession.userStyleSchema.userStyleSettings @@ -278,13 +338,6 @@ class WatchFaceConfigStateHolder( ) } - fun setBigAmbient(enabled: Boolean) { - setUserStyleOption( - bigAmbientKey, - UserStyleSetting.BooleanUserStyleSetting.BooleanOption.from(enabled) - ) - } - // Saves User Style Option change back to the back to the EditorSession. // Note: The UI widgets in the Activity that can trigger this method (through the 'set' methods) // will only be enabled after the EditorSession has been initialized. @@ -311,12 +364,12 @@ class WatchFaceConfigStateHolder( } data class UserStylesAndPreview( + val layoutStyleId: String, val colorStyleId: String, val ambientStyleId: String, val secondsStyleId: String, val militaryTime: Boolean, - val bigAmbient: Boolean, - val previewImage: Bitmap + val previewImage: Bitmap, ) companion object { diff --git a/app/src/main/java/dev/rdnt/m8face/utils/ComplicationUtils.kt b/app/src/main/java/dev/rdnt/m8face/utils/ComplicationUtils.kt index af24df4..bf097f5 100644 --- a/app/src/main/java/dev/rdnt/m8face/utils/ComplicationUtils.kt +++ b/app/src/main/java/dev/rdnt/m8face/utils/ComplicationUtils.kt @@ -19,33 +19,20 @@ import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color -import android.graphics.Paint -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter import android.graphics.Rect import android.graphics.RectF import android.util.Log -import androidx.annotation.Keep -import androidx.core.content.ContextCompat -import androidx.core.graphics.drawable.toBitmap -import androidx.wear.watchface.CanvasComplication +import android.util.LruCache import androidx.wear.watchface.CanvasComplicationFactory import androidx.wear.watchface.ComplicationSlot import androidx.wear.watchface.ComplicationSlotsManager -import androidx.wear.watchface.RenderParameters import androidx.wear.watchface.complications.ComplicationSlotBounds import androidx.wear.watchface.complications.DefaultComplicationDataSourcePolicy import androidx.wear.watchface.complications.SystemDataSources import androidx.wear.watchface.complications.data.ComplicationData import androidx.wear.watchface.complications.data.ComplicationType -import androidx.wear.watchface.complications.data.MonochromaticImageComplicationData -import androidx.wear.watchface.complications.data.NoDataComplicationData -import androidx.wear.watchface.complications.data.ShortTextComplicationData -import androidx.wear.watchface.complications.data.SmallImageComplicationData import androidx.wear.watchface.style.CurrentUserStyleRepository import dev.rdnt.m8face.R -import java.time.Instant -import java.time.ZonedDateTime // Information needed for complications. // Creates bounds for the locations of both right and left complications. (This is the @@ -55,37 +42,45 @@ const val VERTICAL_COMPLICATION_TOP_BOUND = 0.328125f // 126px / 384 const val VERTICAL_COMPLICATION_BOTTOM_BOUND = 1f - VERTICAL_COMPLICATION_TOP_BOUND // offset: 18px, width: 78px (canvas 384x384) -const val VERTICAL_COMPLICATION_OFFSET = 24f / 384f +const val VERTICAL_COMPLICATION_OFFSET = 21f / 384f const val VERTICAL_COMPLICATION_WIDTH = 78f / 384f //const val VERTICAL_COMPLICATION_HEIGHT = VERTICAL_COMPLICATION_BOTTOM_BOUND - VERTICAL_COMPLICATION_TOP_BOUND // 0.03125 -private const val LEFT_COMPLICATION_LEFT_BOUND = VERTICAL_COMPLICATION_OFFSET -private const val LEFT_COMPLICATION_RIGHT_BOUND = + const val LEFT_COMPLICATION_LEFT_BOUND = VERTICAL_COMPLICATION_OFFSET + const val LEFT_COMPLICATION_RIGHT_BOUND = VERTICAL_COMPLICATION_OFFSET + VERTICAL_COMPLICATION_WIDTH +const val LEFT_COMPLICATION_TOP_BOUND = VERTICAL_COMPLICATION_TOP_BOUND +const val LEFT_COMPLICATION_BOTTOM_BOUND = VERTICAL_COMPLICATION_BOTTOM_BOUND -private const val RIGHT_COMPLICATION_LEFT_BOUND = + const val RIGHT_COMPLICATION_LEFT_BOUND = 1f - VERTICAL_COMPLICATION_OFFSET - VERTICAL_COMPLICATION_WIDTH -private const val RIGHT_COMPLICATION_RIGHT_BOUND = 1f - VERTICAL_COMPLICATION_OFFSET + const val RIGHT_COMPLICATION_RIGHT_BOUND = 1f - VERTICAL_COMPLICATION_OFFSET +const val RIGHT_COMPLICATION_TOP_BOUND = VERTICAL_COMPLICATION_TOP_BOUND +const val RIGHT_COMPLICATION_BOTTOM_BOUND = VERTICAL_COMPLICATION_BOTTOM_BOUND // Both left and right complications use the same top and bottom bounds. -const val HORIZONTAL_COMPLICATION_LEFT_BOUND = 102f / 384f +const val HORIZONTAL_COMPLICATION_LEFT_BOUND = 99f / 384f const val HORIZONTAL_COMPLICATION_RIGHT_BOUND = 1f - HORIZONTAL_COMPLICATION_LEFT_BOUND // offset: 18px, height: 51px (canvas 384x384) -const val HORIZONTAL_COMPLICATION_OFFSET = 24f / 384f +const val HORIZONTAL_COMPLICATION_OFFSET = 27f / 384f const val HORIZONTAL_COMPLICATION_HEIGHT = 48f / 384f //const val HORIZONTAL_COMPLICATION_WIDTH = HORIZONTAL_COMPLICATION_RIGHT_BOUND - HORIZONTAL_COMPLICATION_LEFT_BOUND -private const val TOP_COMPLICATION_TOP_BOUND = HORIZONTAL_COMPLICATION_OFFSET -private const val TOP_COMPLICATION_BOTTOM_BOUND = + const val TOP_COMPLICATION_TOP_BOUND = HORIZONTAL_COMPLICATION_OFFSET + const val TOP_COMPLICATION_BOTTOM_BOUND = HORIZONTAL_COMPLICATION_OFFSET + HORIZONTAL_COMPLICATION_HEIGHT +const val TOP_COMPLICATION_LEFT_BOUND = HORIZONTAL_COMPLICATION_LEFT_BOUND +const val TOP_COMPLICATION_RIGHT_BOUND = HORIZONTAL_COMPLICATION_RIGHT_BOUND -private const val BOTTOM_COMPLICATION_TOP_BOUND = + const val BOTTOM_COMPLICATION_TOP_BOUND = 1f - HORIZONTAL_COMPLICATION_OFFSET - HORIZONTAL_COMPLICATION_HEIGHT -private const val BOTTOM_COMPLICATION_BOTTOM_BOUND = 1f - HORIZONTAL_COMPLICATION_OFFSET + const val BOTTOM_COMPLICATION_BOTTOM_BOUND = 1f - HORIZONTAL_COMPLICATION_OFFSET +const val BOTTOM_COMPLICATION_LEFT_BOUND = HORIZONTAL_COMPLICATION_LEFT_BOUND +const val BOTTOM_COMPLICATION_RIGHT_BOUND = HORIZONTAL_COMPLICATION_RIGHT_BOUND // Unique IDs for each complication. The settings activity that supports allowing users // to select their complication data provider requires numbers to be >= 0. @@ -93,285 +88,788 @@ internal const val LEFT_COMPLICATION_ID = 100 internal const val RIGHT_COMPLICATION_ID = 101 internal const val TOP_COMPLICATION_ID = 102 internal const val BOTTOM_COMPLICATION_ID = 103 +internal const val HOUR_COMPLICATION_ID = 104 +internal const val MINUTE_COMPLICATION_ID = 105 + +internal const val COMPLICATIONS_TOP_LEFT_COMPLICATION_ID = 106 +internal const val COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID = 107 +internal const val COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID = 108 +internal const val COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID = 109 +internal const val COMPLICATIONS_TOP_COMPLICATION_ID = 110 +internal const val COMPLICATIONS_BOTTOM_COMPLICATION_ID = 111 +internal const val COMPLICATIONS_HOUR_COMPLICATION_ID = 112 +internal const val COMPLICATIONS_MINUTE_COMPLICATION_ID = 113 + +internal const val FOCUS_LEFT_ICON_COMPLICATION_ID = 114 +internal const val FOCUS_RIGHT_ICON_COMPLICATION_ID = 115 +internal const val FOCUS_HOUR_COMPLICATION_ID = 116 +internal const val FOCUS_MINUTE_COMPLICATION_ID = 117 + +internal const val RIGHT_TEXT_COMPLICATION_ID = 118 +internal const val SPORT_HOUR_COMPLICATION_ID = 119 +internal const val SPORT_MINUTE_COMPLICATION_ID = 120 + +const val TOP_LEFT_COMPLICATION_LEFT_BOUND = 33f / 384f +const val TOP_LEFT_COMPLICATION_TOP_BOUND = 93f / 384f +const val TOP_LEFT_COMPLICATION_RIGHT_BOUND = 33f / 384f + 60f / 384f +const val TOP_LEFT_COMPLICATION_BOTTOM_BOUND = 93f / 384f + 90f / 384f + +const val BOTTOM_LEFT_COMPLICATION_LEFT_BOUND = 33f / 384f +const val BOTTOM_LEFT_COMPLICATION_TOP_BOUND = 201f / 384f +const val BOTTOM_LEFT_COMPLICATION_RIGHT_BOUND = 33f / 384f + 60f / 384f +const val BOTTOM_LEFT_COMPLICATION_BOTTOM_BOUND = 201f / 384f + 90f / 384f + +const val TOP_RIGHT_COMPLICATION_LEFT_BOUND = 285f / 384f +const val TOP_RIGHT_COMPLICATION_TOP_BOUND = 93f / 384f +const val TOP_RIGHT_COMPLICATION_RIGHT_BOUND = 285f / 384f + 60f / 384f +const val TOP_RIGHT_COMPLICATION_BOTTOM_BOUND = 93f / 384f + 90f / 384f + +const val BOTTOM_RIGHT_COMPLICATION_LEFT_BOUND = 285f / 384f +const val BOTTOM_RIGHT_COMPLICATION_TOP_BOUND = 201f / 384f +const val BOTTOM_RIGHT_COMPLICATION_RIGHT_BOUND = 285f / 384f + 60f / 384f +const val BOTTOM_RIGHT_COMPLICATION_BOTTOM_BOUND = 201f / 384f + 90f / 384f + +const val LEFT_ICON_COMPLICATION_LEFT_BOUND = 24f / 384f +const val LEFT_ICON_COMPLICATION_TOP_BOUND = 126f / 384f +const val LEFT_ICON_COMPLICATION_RIGHT_BOUND = 24f / 384f + 54f / 384f +const val LEFT_ICON_COMPLICATION_BOTTOM_BOUND = 126f / 384f + 132f / 384f + +const val RIGHT_ICON_COMPLICATION_LEFT_BOUND = 306f / 384f +const val RIGHT_ICON_COMPLICATION_TOP_BOUND = 126f / 384f +const val RIGHT_ICON_COMPLICATION_RIGHT_BOUND = 306f / 384f + 54f / 384f +const val RIGHT_ICON_COMPLICATION_BOTTOM_BOUND = 126f / 384f + 132f / 384f + +const val RIGHT_TEXT_COMPLICATION_LEFT_BOUND = 249f / 384f - 14f / 384f +const val RIGHT_TEXT_COMPLICATION_TOP_BOUND = 246f / 384f - 14f / 384f + 2f / 384f +const val RIGHT_TEXT_COMPLICATION_RIGHT_BOUND = 249f / 384f + 82f / 384f + 14f / 384f +const val RIGHT_TEXT_COMPLICATION_BOTTOM_BOUND = 246f / 384f + 14f / 384f + 14f / 384f + 2f / 384f + +const val HOUR_COMPLICATION_LEFT_BOUND = 114f / 384f +const val HOUR_COMPLICATION_TOP_BOUND = 87f / 384f +const val HOUR_COMPLICATION_RIGHT_BOUND = 114f / 384f + 156f / 384f +const val HOUR_COMPLICATION_BOTTOM_BOUND = 87f / 384f + 99f / 384f + +const val HOUR_SPORT_COMPLICATION_LEFT_BOUND = 81f / 384f +const val HOUR_SPORT_COMPLICATION_TOP_BOUND = 87f / 384f +const val HOUR_SPORT_COMPLICATION_RIGHT_BOUND = 81f / 384f + 156f / 384f +const val HOUR_SPORT_COMPLICATION_BOTTOM_BOUND = 87f / 384f + 102f / 384f + +const val HOUR_FOCUS_COMPLICATION_LEFT_BOUND = 93f / 384f +const val HOUR_FOCUS_COMPLICATION_TOP_BOUND = 57f / 384f +const val HOUR_FOCUS_COMPLICATION_RIGHT_BOUND = 93f / 384f + 198f / 384f +const val HOUR_FOCUS_COMPLICATION_BOTTOM_BOUND = 57f / 384f + 126f / 384f + +const val MINUTE_COMPLICATION_LEFT_BOUND = 114f / 384f +const val MINUTE_COMPLICATION_TOP_BOUND = 198f / 384f +const val MINUTE_COMPLICATION_RIGHT_BOUND = 114f / 384f + 156f / 384f +const val MINUTE_COMPLICATION_BOTTOM_BOUND = 198f / 384f + 99f / 384f + +const val MINUTE_SPORT_COMPLICATION_LEFT_BOUND = 81f / 384f +const val MINUTE_SPORT_COMPLICATION_TOP_BOUND = 198f / 384f +const val MINUTE_SPORT_COMPLICATION_RIGHT_BOUND = 81f / 384f + 156f / 384f +const val MINUTE_SPORT_COMPLICATION_BOTTOM_BOUND = 198f / 384f + 102f / 384f + +const val MINUTE_FOCUS_COMPLICATION_LEFT_BOUND = 93f / 384f +const val MINUTE_FOCUS_COMPLICATION_TOP_BOUND = 201f / 384f +const val MINUTE_FOCUS_COMPLICATION_RIGHT_BOUND = 93f / 384f + 198f / 384f +const val MINUTE_FOCUS_COMPLICATION_BOTTOM_BOUND = 201f / 384f + 126f / 384f + +///** +// * Represents the unique id associated with a complication and the complication types it supports. +// */ +//sealed class ComplicationConfigDelete( +// val id: Int, +// val supportedTypes: List, +// val renderBounds: RectF = RectF(), +// val slotBounds: RectF = RectF(), +// val nameResourceId: Int?, +//) { +// object Hour : ComplicationConfigDelete( +// HOUR_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// slotBounds = RectF( +// HOUR_COMPLICATION_LEFT_BOUND, +// HOUR_COMPLICATION_TOP_BOUND, +// HOUR_COMPLICATION_RIGHT_BOUND, +// HOUR_COMPLICATION_BOTTOM_BOUND, +// ), +// renderBounds = RectF( +// HOUR_COMPLICATION_LEFT_BOUND, +// HOUR_COMPLICATION_TOP_BOUND, +// HOUR_COMPLICATION_RIGHT_BOUND, +// HOUR_COMPLICATION_BOTTOM_BOUND, +// ), +// nameResourceId = R.string.hour_complication_name, +// ) +// +// object Minute : ComplicationConfigDelete( +// MINUTE_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// slotBounds = RectF( +// MINUTE_COMPLICATION_LEFT_BOUND, +// MINUTE_COMPLICATION_TOP_BOUND, +// MINUTE_COMPLICATION_RIGHT_BOUND, +// MINUTE_COMPLICATION_BOTTOM_BOUND, +// ), +// renderBounds = RectF( +// MINUTE_COMPLICATION_LEFT_BOUND, +// MINUTE_COMPLICATION_TOP_BOUND, +// MINUTE_COMPLICATION_RIGHT_BOUND, +// MINUTE_COMPLICATION_BOTTOM_BOUND, +// ), +// nameResourceId = R.string.minute_complication_name, +// ) +// +// object Left : ComplicationConfigDelete( +// LEFT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// LEFT_COMPLICATION_LEFT_BOUND, +// LEFT_COMPLICATION_TOP_BOUND, +// LEFT_COMPLICATION_RIGHT_BOUND, +// LEFT_COMPLICATION_BOTTOM_BOUND, +// ), +// slotBounds = RectF( +// 0f / 384f, +// 87f / 384f, +// 0f / 384f + 108f / 384f, +// 87f / 384f + 210f / 384f, +// ), +// nameResourceId = R.string.left_complication_name, +// ) +// +// object Right : ComplicationConfigDelete( +// RIGHT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// RIGHT_COMPLICATION_LEFT_BOUND, +// RIGHT_COMPLICATION_TOP_BOUND, +// RIGHT_COMPLICATION_RIGHT_BOUND, +// RIGHT_COMPLICATION_BOTTOM_BOUND, +// ), +// slotBounds = RectF( +// 276f / 384f, +// 87f / 384f, +// 276f / 384f + 108f / 384f, +// 87f / 384f + 210f / 384f +// ), +// nameResourceId = R.string.right_complication_name, +// ) +// +// object Top : ComplicationConfigDelete( +// TOP_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ), +// renderBounds = RectF( +// TOP_COMPLICATION_LEFT_BOUND, +// TOP_COMPLICATION_TOP_BOUND, +// TOP_COMPLICATION_RIGHT_BOUND, +// TOP_COMPLICATION_BOTTOM_BOUND, +// ), +// slotBounds = RectF( +// 0f / 384f, +// 0f / 384f, +// 0f / 384f + 384f / 384f, +// 0f / 384f + 81f / 384f, +// ), +// nameResourceId = R.string.top_complication_name, +// ) +// +// object Bottom : ComplicationConfigDelete( +// BOTTOM_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ), +// renderBounds = RectF( +// BOTTOM_COMPLICATION_LEFT_BOUND, +// BOTTOM_COMPLICATION_TOP_BOUND, +// BOTTOM_COMPLICATION_RIGHT_BOUND, +// BOTTOM_COMPLICATION_BOTTOM_BOUND, +// ), +// slotBounds = RectF( +// 0f / 384f, +// 303f / 384f, +// 0f / 384f + 384f / 384f, +// 303f / 384f + 81f / 384f, +// ), +// nameResourceId = R.string.bottom_complication_name, +// ) +// +// // ========================================================================== +// +// object TopLeft : ComplicationConfigDelete( +// COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// TOP_LEFT_COMPLICATION_LEFT_BOUND, +// TOP_LEFT_COMPLICATION_TOP_BOUND-6f/384f, +// TOP_LEFT_COMPLICATION_RIGHT_BOUND, +// TOP_LEFT_COMPLICATION_BOTTOM_BOUND+6f/384f, +// ), +// slotBounds = RectF( +// 0f / 384f, +// 87f / 384f, +// 0f / 384f + 108f / 384f, +// 87f / 384f + 102f / 384f, +// ), +// nameResourceId = R.string.top_left_complication_name, +// ) +// +// object BottomLeft : ComplicationConfigDelete( +// COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// BOTTOM_LEFT_COMPLICATION_LEFT_BOUND, +// BOTTOM_LEFT_COMPLICATION_TOP_BOUND-6f/384f, +// BOTTOM_LEFT_COMPLICATION_RIGHT_BOUND, +// BOTTOM_LEFT_COMPLICATION_BOTTOM_BOUND+6f/384f, +// ), +// slotBounds = RectF( +// 0f / 384f, +// 195f / 384f, +// 0f / 384f + 108f / 384f, +// 195f / 384f + 102f / 384f, +// ), +// nameResourceId = R.string.bottom_left_complication_name, +// ) +// +// object TopRight : ComplicationConfigDelete( +// COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// TOP_RIGHT_COMPLICATION_LEFT_BOUND, +// TOP_RIGHT_COMPLICATION_TOP_BOUND-6f/384f, +// TOP_RIGHT_COMPLICATION_RIGHT_BOUND, +// TOP_RIGHT_COMPLICATION_BOTTOM_BOUND+6f/384f, +// ), +// slotBounds = RectF( +// 276f / 384f, +// 87f / 384f, +// 276f / 384f + 108f / 384f, +// 87f / 384f + 102f / 384f, +// ), +// nameResourceId = R.string.top_right_complication_name, +// ) +// +// object BottomRight : ComplicationConfigDelete( +// COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// BOTTOM_RIGHT_COMPLICATION_LEFT_BOUND, +// BOTTOM_RIGHT_COMPLICATION_TOP_BOUND-6f/384f, +// BOTTOM_RIGHT_COMPLICATION_RIGHT_BOUND, +// BOTTOM_RIGHT_COMPLICATION_BOTTOM_BOUND+6f/384f, +// ), +// slotBounds = RectF( +// 276f / 384f, +// 195f / 384f, +// 276f / 384f + 108f / 384f, +// 195f / 384f + 102f / 384f, +// ), +// nameResourceId = R.string.bottom_right_complication_name, +// ) +// +//// object ComplicationsTop : ComplicationConfigDelete( +//// COMPLICATIONS_TOP_COMPLICATION_ID, listOf( +//// ComplicationType.SHORT_TEXT, +//// ), +//// renderBounds = RectF( +//// TOP_COMPLICATION_LEFT_BOUND, +//// TOP_COMPLICATION_TOP_BOUND, +//// TOP_COMPLICATION_RIGHT_BOUND, +//// TOP_COMPLICATION_BOTTOM_BOUND, +//// ), +//// slotBounds = RectF( +//// 0f / 384f, +//// 0f / 384f, +//// 0f / 384f + 384f / 384f, +//// 0f / 384f + 81f / 384f, +//// ), +//// ) +// +//// object ComplicationsBottom : ComplicationConfigDelete( +//// COMPLICATIONS_BOTTOM_COMPLICATION_ID, listOf( +//// ComplicationType.SHORT_TEXT, +//// ), +//// renderBounds = RectF( +//// BOTTOM_COMPLICATION_LEFT_BOUND, +//// BOTTOM_COMPLICATION_TOP_BOUND, +//// BOTTOM_COMPLICATION_RIGHT_BOUND, +//// BOTTOM_COMPLICATION_BOTTOM_BOUND, +//// ), +//// slotBounds = RectF( +//// 0f / 384f, +//// 303f / 384f, +//// 0f / 384f + 384f / 384f, +//// 303f / 384f + 81f / 384f, +//// ), +//// ) +// +// // ========================================================================== +// +// object LeftIcon : ComplicationConfigDelete( +// FOCUS_LEFT_ICON_COMPLICATION_ID, listOf( +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// LEFT_ICON_COMPLICATION_LEFT_BOUND, +// LEFT_ICON_COMPLICATION_TOP_BOUND, +// LEFT_ICON_COMPLICATION_RIGHT_BOUND, +// LEFT_ICON_COMPLICATION_BOTTOM_BOUND +// ), +// slotBounds = RectF( +// 0f / 384f, +// 0f / 384f, +// 0f / 384f + 93f / 384f, +// 0f / 384f + 384f / 384f +// ), +// nameResourceId = R.string.left_complication_name, +// ) +// +// object RightIcon : ComplicationConfigDelete( +// FOCUS_RIGHT_ICON_COMPLICATION_ID, listOf( +// ComplicationType.MONOCHROMATIC_IMAGE, +// ComplicationType.SMALL_IMAGE +// ), +// renderBounds = RectF( +// RIGHT_ICON_COMPLICATION_LEFT_BOUND, +// RIGHT_ICON_COMPLICATION_TOP_BOUND, +// RIGHT_ICON_COMPLICATION_RIGHT_BOUND, +// RIGHT_ICON_COMPLICATION_BOTTOM_BOUND +// ), +// slotBounds = RectF( +// 291f / 384f, +// 0f / 384f, +// 291f / 384f + 93f / 384f, +// 0f / 384f + 384f / 384f +// ), +// nameResourceId = R.string.right_complication_name, +// ) +// +// object Text : ComplicationConfigDelete( +// RIGHT_TEXT_COMPLICATION_ID, listOf( +// ComplicationType.SHORT_TEXT, +// ), +// renderBounds = RectF( +// RIGHT_TEXT_COMPLICATION_LEFT_BOUND, +// RIGHT_TEXT_COMPLICATION_TOP_BOUND, +// RIGHT_TEXT_COMPLICATION_RIGHT_BOUND, +// RIGHT_TEXT_COMPLICATION_BOTTOM_BOUND +// ), +// slotBounds = RectF( +// RIGHT_TEXT_COMPLICATION_LEFT_BOUND, +// RIGHT_TEXT_COMPLICATION_TOP_BOUND, +// RIGHT_TEXT_COMPLICATION_RIGHT_BOUND, +// RIGHT_TEXT_COMPLICATION_BOTTOM_BOUND +// ), +// nameResourceId = R.string.right_complication_name, +// ) +//} -/** - * Represents the unique id associated with a complication and the complication types it supports. - */ -sealed class ComplicationConfig(val id: Int, val supportedTypes: List) { - object Left : ComplicationConfig( - LEFT_COMPLICATION_ID, - listOf( +// Utility function that initializes default complication slots (left and right). +fun createComplicationSlotManager( + context: Context, + currentUserStyleRepository: CurrentUserStyleRepository, +): ComplicationSlotsManager { + val verticalComplicationFactory = createVerticalComplicationFactory(context) + val horizontalComplicationFactory = createHorizontalComplicationFactory(context) + val invisibleComplicationFactory = createInvisibleComplicationFactory(context) + val horizontalTextComplicationFactory = createHorizontalTextComplicationFactory(context) + + val hourComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = HOUR_COMPLICATION_ID, + canvasComplicationFactory = invisibleComplicationFactory, + supportedTypes = listOf( ComplicationType.SHORT_TEXT, -// ComplicationType.RANGED_VALUE, ComplicationType.MONOCHROMATIC_IMAGE, ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + HOUR_COMPLICATION_LEFT_BOUND, + HOUR_COMPLICATION_TOP_BOUND, + HOUR_COMPLICATION_RIGHT_BOUND, + HOUR_COMPLICATION_BOTTOM_BOUND, + ), ) - ) - - object Right : ComplicationConfig( - RIGHT_COMPLICATION_ID, - listOf( + ).setNameResourceId(R.string.hour_complication_name) + .setScreenReaderNameResourceId(R.string.hour_complication_name) + .setEnabled(false) + .build() + + val minuteComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = MINUTE_COMPLICATION_ID, + canvasComplicationFactory = invisibleComplicationFactory, + supportedTypes = listOf( ComplicationType.SHORT_TEXT, -// ComplicationType.RANGED_VALUE, ComplicationType.MONOCHROMATIC_IMAGE, ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + MINUTE_COMPLICATION_LEFT_BOUND, + MINUTE_COMPLICATION_TOP_BOUND, + MINUTE_COMPLICATION_RIGHT_BOUND, + MINUTE_COMPLICATION_BOTTOM_BOUND, + ) ) - ) - - object Top : ComplicationConfig( - TOP_COMPLICATION_ID, - listOf( - ComplicationType.SHORT_TEXT, - ) - ) - - object Bottom : ComplicationConfig( - BOTTOM_COMPLICATION_ID, - listOf( + ).setNameResourceId(R.string.minute_complication_name) + .setScreenReaderNameResourceId(R.string.minute_complication_name) + .setEnabled(false) + .build() + + val leftComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = LEFT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( ComplicationType.SHORT_TEXT, - ) - ) -} - -// Utility function that initializes default complication slots (left and right). -fun createComplicationSlotManager( - context: Context, - currentUserStyleRepository: CurrentUserStyleRepository, -): ComplicationSlotsManager { - - val customLeftComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( - id = ComplicationConfig.Left.id, - canvasComplicationFactory = createVerticalComplicationFactory(context), - supportedTypes = ComplicationConfig.Left.supportedTypes, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( -// SystemDataSources.DATA_SOURCE_DAY_OF_WEEK, -// ComplicationType.SHORT_TEXT - SystemDataSources.NO_DATA_SOURCE, - ComplicationType.SHORT_TEXT + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT ), bounds = ComplicationSlotBounds( RectF( LEFT_COMPLICATION_LEFT_BOUND, - VERTICAL_COMPLICATION_TOP_BOUND, + LEFT_COMPLICATION_TOP_BOUND, LEFT_COMPLICATION_RIGHT_BOUND, - VERTICAL_COMPLICATION_BOTTOM_BOUND + LEFT_COMPLICATION_BOTTOM_BOUND, ) ) ).setNameResourceId(R.string.left_complication_name) - .setScreenReaderNameResourceId(R.string.left_complication_name).build() - - val customRightComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( - id = ComplicationConfig.Right.id, - canvasComplicationFactory = createVerticalComplicationFactory(context), - supportedTypes = ComplicationConfig.Right.supportedTypes, + .setScreenReaderNameResourceId(R.string.left_complication_name) + .setEnabled(false) + .build() + + val rightComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = RIGHT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( -// SystemDataSources.DATA_SOURCE_DAY_OF_WEEK, -// ComplicationType.SHORT_TEXT - SystemDataSources.NO_DATA_SOURCE, - ComplicationType.SHORT_TEXT + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT ), bounds = ComplicationSlotBounds( RectF( RIGHT_COMPLICATION_LEFT_BOUND, - VERTICAL_COMPLICATION_TOP_BOUND, + RIGHT_COMPLICATION_TOP_BOUND, RIGHT_COMPLICATION_RIGHT_BOUND, - VERTICAL_COMPLICATION_BOTTOM_BOUND + RIGHT_COMPLICATION_BOTTOM_BOUND, ) ) ).setNameResourceId(R.string.right_complication_name) - .setScreenReaderNameResourceId(R.string.right_complication_name).build() - - val customTopComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( - id = ComplicationConfig.Top.id, - canvasComplicationFactory = createHorizontalComplicationFactory(context), - supportedTypes = ComplicationConfig.Top.supportedTypes, + .setScreenReaderNameResourceId(R.string.right_complication_name) + .setEnabled(false) + .build() + + val topComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = TOP_COMPLICATION_ID, + canvasComplicationFactory = horizontalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ), defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( - SystemDataSources.DATA_SOURCE_DATE, - ComplicationType.SHORT_TEXT -// SystemDataSources.NO_DATA_SOURCE, -// ComplicationType.SHORT_TEXT + SystemDataSources.DATA_SOURCE_DATE, ComplicationType.SHORT_TEXT ), bounds = ComplicationSlotBounds( RectF( - HORIZONTAL_COMPLICATION_LEFT_BOUND, + TOP_COMPLICATION_LEFT_BOUND, TOP_COMPLICATION_TOP_BOUND, - HORIZONTAL_COMPLICATION_RIGHT_BOUND, - TOP_COMPLICATION_BOTTOM_BOUND + TOP_COMPLICATION_RIGHT_BOUND, + TOP_COMPLICATION_BOTTOM_BOUND, ) ) ).setNameResourceId(R.string.top_complication_name) - .setScreenReaderNameResourceId(R.string.top_complication_name).build() - - val customBottomComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( - id = ComplicationConfig.Bottom.id, - canvasComplicationFactory = createHorizontalComplicationFactory(context), - supportedTypes = ComplicationConfig.Bottom.supportedTypes, + .setScreenReaderNameResourceId(R.string.top_complication_name) + .setEnabled(false) + .build() + + val bottomComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = BOTTOM_COMPLICATION_ID, + canvasComplicationFactory = horizontalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ), defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( - SystemDataSources.DATA_SOURCE_WATCH_BATTERY, - ComplicationType.SHORT_TEXT + SystemDataSources.DATA_SOURCE_WATCH_BATTERY, ComplicationType.SHORT_TEXT ), bounds = ComplicationSlotBounds( RectF( - HORIZONTAL_COMPLICATION_LEFT_BOUND, + BOTTOM_COMPLICATION_LEFT_BOUND, BOTTOM_COMPLICATION_TOP_BOUND, - HORIZONTAL_COMPLICATION_RIGHT_BOUND, - BOTTOM_COMPLICATION_BOTTOM_BOUND + BOTTOM_COMPLICATION_RIGHT_BOUND, + BOTTOM_COMPLICATION_BOTTOM_BOUND, ) ) ).setNameResourceId(R.string.bottom_complication_name) - .setScreenReaderNameResourceId(R.string.bottom_complication_name).build() + .setScreenReaderNameResourceId(R.string.bottom_complication_name) + .setEnabled(false) + .build() + + val topLeftComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + TOP_LEFT_COMPLICATION_LEFT_BOUND, + TOP_LEFT_COMPLICATION_TOP_BOUND-6f/384f, + TOP_LEFT_COMPLICATION_RIGHT_BOUND, + TOP_LEFT_COMPLICATION_BOTTOM_BOUND+6f/384f, + ) + ) + ).setNameResourceId(R.string.top_left_complication_name) + .setScreenReaderNameResourceId(R.string.top_left_complication_name) + .setEnabled(false) + .build() + + val bottomLeftComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + BOTTOM_LEFT_COMPLICATION_LEFT_BOUND, + BOTTOM_LEFT_COMPLICATION_TOP_BOUND-6f/384f, + BOTTOM_LEFT_COMPLICATION_RIGHT_BOUND, + BOTTOM_LEFT_COMPLICATION_BOTTOM_BOUND+6f/384f, + ) + ) + ).setNameResourceId(R.string.bottom_left_complication_name) + .setScreenReaderNameResourceId(R.string.bottom_left_complication_name) + .setEnabled(false) + .build() + + val topRightComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + TOP_RIGHT_COMPLICATION_LEFT_BOUND, + TOP_RIGHT_COMPLICATION_TOP_BOUND-6f/384f, + TOP_RIGHT_COMPLICATION_RIGHT_BOUND, + TOP_RIGHT_COMPLICATION_BOTTOM_BOUND+6f/384f, + ) + ) + ).setNameResourceId(R.string.top_right_complication_name) + .setScreenReaderNameResourceId(R.string.top_right_complication_name) + .setEnabled(false) + .build() + + val bottomRightComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + BOTTOM_RIGHT_COMPLICATION_LEFT_BOUND, + BOTTOM_RIGHT_COMPLICATION_TOP_BOUND-6f/384f, + BOTTOM_RIGHT_COMPLICATION_RIGHT_BOUND, + BOTTOM_RIGHT_COMPLICATION_BOTTOM_BOUND+6f/384f, + ) + ) + ).setNameResourceId(R.string.bottom_right_complication_name) + .setScreenReaderNameResourceId(R.string.bottom_right_complication_name) + .setEnabled(false) + .build() + + val leftIconComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = FOCUS_LEFT_ICON_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.MONOCHROMATIC_IMAGE + ), + bounds = ComplicationSlotBounds( + RectF( + LEFT_ICON_COMPLICATION_LEFT_BOUND, + LEFT_ICON_COMPLICATION_TOP_BOUND, + LEFT_ICON_COMPLICATION_RIGHT_BOUND, + LEFT_ICON_COMPLICATION_BOTTOM_BOUND + ) + ) + ).setNameResourceId(R.string.left_complication_name) + .setScreenReaderNameResourceId(R.string.left_complication_name) + .setEnabled(false) + .build() + + val rightIconComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = FOCUS_RIGHT_ICON_COMPLICATION_ID, + canvasComplicationFactory = verticalComplicationFactory, + supportedTypes = listOf( + ComplicationType.MONOCHROMATIC_IMAGE, + ComplicationType.SMALL_IMAGE + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.MONOCHROMATIC_IMAGE + ), + bounds = ComplicationSlotBounds( + RectF( + RIGHT_ICON_COMPLICATION_LEFT_BOUND, + RIGHT_ICON_COMPLICATION_TOP_BOUND, + RIGHT_ICON_COMPLICATION_RIGHT_BOUND, + RIGHT_ICON_COMPLICATION_BOTTOM_BOUND + ) + ) + ).setNameResourceId(R.string.right_complication_name) + .setScreenReaderNameResourceId(R.string.right_complication_name) + .setEnabled(false) + .build() + + val textComplication = ComplicationSlot.createRoundRectComplicationSlotBuilder( + id = RIGHT_TEXT_COMPLICATION_ID, + canvasComplicationFactory = horizontalTextComplicationFactory, + supportedTypes = listOf( + ComplicationType.SHORT_TEXT, + ), + defaultDataSourcePolicy = DefaultComplicationDataSourcePolicy( + SystemDataSources.NO_DATA_SOURCE, ComplicationType.SHORT_TEXT + ), + bounds = ComplicationSlotBounds( + RectF( + RIGHT_TEXT_COMPLICATION_LEFT_BOUND, + RIGHT_TEXT_COMPLICATION_TOP_BOUND, + RIGHT_TEXT_COMPLICATION_RIGHT_BOUND, + RIGHT_TEXT_COMPLICATION_BOTTOM_BOUND + ) + ) + ).setNameResourceId(R.string.right_complication_name) + .setScreenReaderNameResourceId(R.string.right_complication_name) + .setEnabled(false) + .build() return ComplicationSlotsManager( listOf( - customLeftComplication, - customRightComplication, - customTopComplication, - customBottomComplication - ), - currentUserStyleRepository - ) -} + hourComplication, + minuteComplication, -class RectangleCanvasComplication(private val context: Context) : CanvasComplication { - override fun render( - canvas: Canvas, - bounds: Rect, - zonedDateTime: ZonedDateTime, - renderParameters: RenderParameters, - slotId: Int - ) { - val start = System.currentTimeMillis() - Log.d("RectangleCanvasComplication", "render($slotId, ${_data.type}) -- start: ${start}ms") - - val dataSource = _data.dataSource - val isBattery = - dataSource?.className == "com.google.android.clockwork.sysui.experiences.complications.providers.BatteryProviderService" - - // debug - val dp = Paint() - dp.color = Color.parseColor("#444444") -// canvas.drawRect(bounds, dp) - - var text: String - var title: String? = null - var icon: Bitmap? = null - var iconRect = Rect(0, 0, 32, 32) - - when (_data.type) { - ComplicationType.SHORT_TEXT -> { - val dat = _data as ShortTextComplicationData - text = dat.text.getTextAt(context.resources, Instant.now()).toString() - - if (dat.monochromaticImage != null) { - val drawable = dat.monochromaticImage!!.image.loadDrawable(context) - if (drawable != null) { - icon = drawable.toBitmap(32, 32) - } - } - - if (dat.title != null) { - title = dat.title!!.getTextAt(context.resources, Instant.now()).toString() - } - - } - - else -> { - Log.d("TIME", "start: ${start}ms, elapsed: ${System.currentTimeMillis() - start}ms") - return - } - } + leftComplication, + rightComplication, - if (isBattery) { - val drawable = ContextCompat.getDrawable(context, R.drawable.battery_icon)!! - icon = drawable.toBitmap(30, 15) - iconRect = Rect(-1, 0, 29, 15) - } + topComplication, + bottomComplication, -// text = "1234567" + topLeftComplication, + bottomLeftComplication, + topRightComplication, + bottomRightComplication, - val tp = Paint() - tp.isAntiAlias = true - tp.textSize = 24F / 384F * canvas.width - tp.typeface = context.resources.getFont(R.font.m8stealth57) - tp.textAlign = Paint.Align.CENTER - tp.color = Color.parseColor("#8888bb") + leftIconComplication, + rightIconComplication, - var offsetX = - iconRect.width() / 2f + 6f // half icon width to the right plus 3f to the right for some spacing - val offsetY = 10.5f + textComplication, + ), currentUserStyleRepository + ) +} - var prefixLen = 0 - if (isBattery) { - prefixLen = 3 - text.length - text = text.padStart(3, ' ') - } +class ComplicationRenderer { + val bmpCache = LruCache(1) + val complCache = LruCache(1) - val width = 15f * text.length + 3f * (text.length - 1) + fun reset() { + complCache.evictAll() + } - if (title != null) { - offsetX = 0f - text = "$title $text" + inline fun render( + bounds: Rect, + data: ComplicationData, + renderer: (canvas: Canvas, bounds: Rect, data: T) -> Unit + ): Bitmap { + val cacheKey = "${bounds.hashCode()},${data.hashCode()}" + + val cached = complCache.get(cacheKey) + if (cached != null) { + return cached } - tp.color = Color.parseColor("#8888bb") - - canvas.drawText( - text.uppercase(), - bounds.exactCenterX() + offsetX / 384F * canvas.width.toFloat(), - bounds.exactCenterY() + offsetY / 384F * canvas.height.toFloat(), - tp - ) - - if (isBattery) { - val prefix = "".padStart(prefixLen, '0') + " ".repeat(3 - prefixLen) - - tp.color = Color.parseColor("#343434") + val bmpKey = "${bounds.hashCode()}" - canvas.drawText( - prefix, - bounds.exactCenterX() + offsetX / 384F * canvas.width.toFloat(), - bounds.exactCenterY() + offsetY / 384F * canvas.height.toFloat(), - tp + var bitmap = bmpCache.get(bmpKey) + if (bitmap != null) { + bitmap.eraseColor(Color.TRANSPARENT) + } else { + bitmap = Bitmap.createBitmap( + bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888 ) + bmpCache.put(bmpKey, bitmap) } - if (icon != null && title == null) { + val bitmapCanvas = Canvas(bitmap) + val rect = Rect(0, 0, bitmap.width, bitmap.height) - val srcRect = iconRect - val dstRect = RectF( - bounds.exactCenterX() - iconRect.width() / 2f - width / 2 - 6f, - bounds.exactCenterY() - iconRect.height() / 2f, - bounds.exactCenterX() + iconRect.width() / 2f - width / 2 - 6f, - bounds.exactCenterY() + iconRect.height() / 2f, - ) - - val iconPaint = Paint() - iconPaint.isAntiAlias = false - iconPaint.colorFilter = - PorterDuffColorFilter(Color.parseColor("#8888bb"), PorterDuff.Mode.SRC_IN) - canvas.drawBitmap(icon, srcRect, dstRect, iconPaint) + (data as? T)?.let { + renderer(bitmapCanvas, rect, it) } - Log.d("TIME", "start: ${start}ms, elapsed: ${System.currentTimeMillis() - start}ms") - } + complCache.put(cacheKey, bitmap) - override fun drawHighlight( - canvas: Canvas, - bounds: Rect, - boundsType: Int, - zonedDateTime: ZonedDateTime, - color: Int - ) { + return bitmap } - private var _data: ComplicationData = NoDataComplicationData() - - override fun getData(): ComplicationData = _data - - override fun loadData( - complicationData: ComplicationData, - loadDrawablesAsynchronous: Boolean - ) { - _data = complicationData - } } diff --git a/app/src/main/java/dev/rdnt/m8face/utils/HorizontalComplication.kt b/app/src/main/java/dev/rdnt/m8face/utils/HorizontalComplication.kt index a4d35b8..5b68862 100644 --- a/app/src/main/java/dev/rdnt/m8face/utils/HorizontalComplication.kt +++ b/app/src/main/java/dev/rdnt/m8face/utils/HorizontalComplication.kt @@ -1,21 +1,34 @@ package dev.rdnt.m8face.utils import android.content.Context -import android.graphics.* +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect +import android.graphics.RectF import android.util.Log -import androidx.core.content.ContextCompat -import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toBitmap -import androidx.core.graphics.withRotation import androidx.wear.watchface.CanvasComplication import androidx.wear.watchface.CanvasComplicationFactory import androidx.wear.watchface.RenderParameters -import androidx.wear.watchface.complications.data.* +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.ShortTextComplicationData import dev.rdnt.m8face.R import java.time.Instant import java.time.ZonedDateTime class HorizontalComplication(private val context: Context) : CanvasComplication { + private val renderer = ComplicationRenderer() + + init { + Log.d("HorizontalComplication", "Constructor ran") + } + var tertiaryColor: Int = Color.parseColor("#8888bb") set(tertiaryColor) { field = tertiaryColor @@ -24,21 +37,7 @@ class HorizontalComplication(private val context: Context) : CanvasComplication iconPaint.colorFilter = PorterDuffColorFilter(tertiaryColor, PorterDuff.Mode.SRC_IN) prefixPaint.color = tertiaryColor prefixPaint.alpha = 100 - } - - var opacity: Float = 1f - set(opacity) { - field = opacity - - val color = ColorUtils.blendARGB(Color.TRANSPARENT, tertiaryColor, opacity) - - textPaint.color = color - titlePaint.color = color - - iconPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) - - prefixPaint.color = color - prefixPaint.alpha = 100 + renderer.reset() } private val textPaint = Paint().apply { @@ -76,116 +75,73 @@ class HorizontalComplication(private val context: Context) : CanvasComplication ) { if (bounds.isEmpty) return - when (data.type) { + val data = if (data.type == ComplicationType.NO_DATA) { + val placeholder = (data as NoDataComplicationData).placeholder + placeholder ?: data + } else { + data + } + + val bitmap = when (data.type) { ComplicationType.SHORT_TEXT -> { - renderShortTextComplication(canvas, bounds, data as ShortTextComplicationData) + renderer.render(bounds, data, ::renderShortTextComplication) } else -> return } + + canvas.drawBitmap( + bitmap, + bounds.left.toFloat(), + bounds.top.toFloat(), + Paint(), + ) } private fun renderShortTextComplication( canvas: Canvas, bounds: Rect, - data: ShortTextComplicationData, + complData: ComplicationData ) { + val data = complData as ShortTextComplicationData + val now = Instant.now() var text = data.text.getTextAt(context.resources, now).toString().uppercase() - if (text == "--") { - return - } - - val isBattery = - data.dataSource?.className == "com.google.android.clockwork.sysui.experiences.complications.providers.BatteryProviderService" - val threeDigit = isBattery + val title = data.title?.getTextAt(context.resources, now)?.toString()?.uppercase() + if (title != null) { + text = "$text $title" + } - var title: String? = null var icon: Bitmap? = null var iconBounds = Rect() + if (title == null) { + val bmpSize = (bounds.width().coerceAtMost(bounds.height()).toFloat() * 0.55f).toInt() - if (isBattery) { - val drawable = ContextCompat.getDrawable(context, R.drawable.battery_icon_32)!! - icon = drawable.toBitmap( - (32f / 48f * bounds.height()).toInt(), - (32f / 48f * bounds.height()).toInt() - ) - iconBounds = - Rect(0, 0, (32f / 48f * bounds.height()).toInt(), (32f / 48f * bounds.height()).toInt()) - } else if (data.monochromaticImage != null) { - val drawable = data.monochromaticImage!!.image.loadDrawable(context) - if (drawable != null) { - val size = (bounds.width().coerceAtMost(bounds.height()).toFloat() * 0.55f).toInt() - - icon = drawable.toBitmap(size, size) - iconBounds = Rect(0, 0, size, size) - } - } - - var prefixLen = 0 - - if (threeDigit) { - prefixLen = 3 - text.length - text = text.padStart(3, ' ') - } - - if (data.title != null && !data.title!!.isPlaceholder()) { - title = data.title!!.getTextAt(context.resources, now).toString().uppercase() + icon = data.monochromaticImage?.image?.loadDrawable(context)?.toBitmap(bmpSize, bmpSize) + iconBounds = Rect(0, 0, bmpSize, bmpSize) } - textPaint.textSize = 24F / 48f * bounds.height() + textPaint.textSize = 24F / 186f * canvas.width val textBounds = Rect() - - if (threeDigit) { - textPaint.getTextBounds("000", 0, 3, textBounds) - } else { - textPaint.getTextBounds(text, 0, text.length, textBounds) - } - - val titleBounds = Rect() - - if (title != null) { - titlePaint.textSize = textPaint.textSize - titlePaint.getTextBounds(title, 0, title.length, titleBounds) - } + textPaint.getTextBounds(text, 0, text.length, textBounds) var iconOffsetX = 0f - var titleOffsetX = 0f var textOffsetX = 0f - if (title != null) { - val width = titleBounds.width() + textBounds.width() - - titleOffsetX = (width - titleBounds.width()).toFloat() / 2f - textOffsetX = (width - textBounds.width()).toFloat() / 2f - - titleOffsetX += 6f / 156f * bounds.width() - textOffsetX += 6f / 156f * bounds.width() - } else if (icon != null) { + if (icon != null) { val width = iconBounds.width() + textBounds.width() iconOffsetX = (width - iconBounds.width()).toFloat() / 2f textOffsetX = (width - textBounds.width()).toFloat() / 2f - iconOffsetX += 9f / 156f * bounds.width() - textOffsetX += 9f / 156f * bounds.width() - - if (isBattery) { - iconOffsetX = iconOffsetX.toInt().toFloat() - } + iconOffsetX += 9f / 186f * canvas.width + textOffsetX += 9f / 186f * canvas.width } - if (title != null) { - canvas.drawText( - title, - bounds.exactCenterX() - titleBounds.width() / 2 - titleOffsetX, - bounds.exactCenterY() + titleBounds.height() / 2, - titlePaint - ) - } else if (icon != null) { + if (title == null && icon != null) { val dstRect = RectF( bounds.exactCenterX() - iconBounds.width() / 2f - iconOffsetX, bounds.exactCenterY() - iconBounds.height() / 2f, @@ -196,22 +152,10 @@ class HorizontalComplication(private val context: Context) : CanvasComplication canvas.drawBitmap(icon, iconBounds, dstRect, iconPaint) } - if (prefixLen > 0) { - val prefix = "".padStart(prefixLen, '0') - prefixPaint.textSize = textPaint.textSize - - canvas.drawText( - prefix, - bounds.exactCenterX() - textBounds.width() / 2 + textOffsetX, - bounds.exactCenterY() + textBounds.height() / 2, - prefixPaint - ) - } - canvas.drawText( text, bounds.exactCenterX() - textBounds.width() / 2 + textOffsetX, - bounds.exactCenterY() + textBounds.height() / 2, + bounds.exactCenterY() + textPaint.fontSpacing / 2, textPaint ) } diff --git a/app/src/main/java/dev/rdnt/m8face/utils/HorizontalTextComplication.kt b/app/src/main/java/dev/rdnt/m8face/utils/HorizontalTextComplication.kt new file mode 100644 index 0000000..cc24026 --- /dev/null +++ b/app/src/main/java/dev/rdnt/m8face/utils/HorizontalTextComplication.kt @@ -0,0 +1,185 @@ +package dev.rdnt.m8face.utils + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.Rect +import android.graphics.RectF +import android.util.Log +import android.util.LruCache +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.toRectF +import androidx.core.graphics.withScale +import androidx.core.graphics.withTranslation +import androidx.wear.watchface.CanvasComplication +import androidx.wear.watchface.CanvasComplicationFactory +import androidx.wear.watchface.RenderParameters +import androidx.wear.watchface.complications.data.ComplicationData +import androidx.wear.watchface.complications.data.ComplicationType +import androidx.wear.watchface.complications.data.NoDataComplicationData +import androidx.wear.watchface.complications.data.ShortTextComplicationData +import dev.rdnt.m8face.BitmapCacheEntry +import dev.rdnt.m8face.R +import java.time.Instant +import java.time.ZonedDateTime + +private const val debug = false + +class HorizontalTextComplication(private val context: Context) : CanvasComplication { + private val memoryCache = LruCache(1) + + var tertiaryColor: Int = Color.parseColor("#8888bb") + set(tertiaryColor) { + field = tertiaryColor + textPaint.color = tertiaryColor + } + + private val textPaint = Paint().apply { + isAntiAlias = true + typeface = context.resources.getFont(R.font.m8stealth57) + textAlign = Paint.Align.LEFT + color = tertiaryColor + textSize = 112f / 14f / 7f * 14f + } + + override fun render( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime, + renderParameters: RenderParameters, + slotId: Int + ) { + if (bounds.isEmpty) return + + if (memoryCache.get("") == null) { + Log.d("@@@", "create bitmap") + val bitmap = Bitmap.createBitmap( + bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888 + ) + memoryCache.put("", bitmap) + } + + return + + val bitmap = when (data.type) { + ComplicationType.SHORT_TEXT -> { + drawShortTextComplication(bounds, data as ShortTextComplicationData) + } + + ComplicationType.NO_DATA -> { + val placeholder = (data as NoDataComplicationData).placeholder + if (placeholder != null && placeholder.type == ComplicationType.SHORT_TEXT) { + drawShortTextComplication(bounds, placeholder as ShortTextComplicationData) + } else { + return + } + } + + else -> return + } + +// renderDebug(canvas, bounds.toRectF()) + + canvas.drawBitmap( + bitmap, + bounds.left.toFloat(), + bounds.top.toFloat(), + Paint(), + ) + } + + private fun drawShortTextComplication( + bounds: Rect, + data: ShortTextComplicationData + ): Bitmap { + val cached = memoryCache.get("") + return cached + cached.eraseColor(Color.TRANSPARENT) + val bitmap = cached + +// val bitmap = Bitmap.createBitmap( +// bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888 +// ) + val bitmapCanvas = Canvas(bitmap) + + val rect = Rect(0, 0, bitmap.width, bitmap.height) + + renderShortTextComplication(bitmapCanvas, rect, data) + + memoryCache.put("", bitmap) + + return bitmap + } + + private fun renderShortTextComplication( + canvas: Canvas, + bounds: Rect, + data: ShortTextComplicationData, + ) { + val now = Instant.now() + + val text = data.text.getTextAt(context.resources, now).toString().uppercase() + + val textBounds = Rect() + textPaint.getTextBounds(text, 0, text.length, textBounds) + + canvas.drawText( + text, + bounds.left.toFloat() + 14f, + bounds.exactCenterY() + textBounds.height() / 2, + textPaint, + ) + } + + private fun renderDebug(canvas: Canvas, bounds: RectF) { + if (debug) { + canvas.drawRect(bounds, Paint().apply { + this.color = ColorUtils.blendARGB(Color.TRANSPARENT, Color.parseColor("#aa02d7f2"), 1f) + style = Paint.Style.STROKE + strokeWidth = 2f + }) + val p2 = Paint() + p2.color = ColorUtils.blendARGB(Color.TRANSPARENT, Color.parseColor("#aa02d7f2"), 1f) + p2.typeface = context.resources.getFont(R.font.m8stealth57) + p2.textSize = 8f +// canvas.drawText( +// "r ${bitmapCache.loads} w ${bitmapCache.renders}", +// bounds.left + 3f, +// bounds.bottom - 3f, +// p2, +// ) + } + } + + override fun drawHighlight( + canvas: Canvas, + bounds: Rect, + boundsType: Int, + zonedDateTime: ZonedDateTime, + color: Int + ) { + // Rendering of highlights + } + + private var data: ComplicationData = NoDataComplicationData() + + override fun getData(): ComplicationData = data + + override fun loadData( + complicationData: ComplicationData, + loadDrawablesAsynchronous: Boolean + ) { + data = complicationData +// memoryCache.remove("") + } +} + +fun createHorizontalTextComplicationFactory(context: Context) = CanvasComplicationFactory { _, _ -> + HorizontalTextComplication(context) +} diff --git a/app/src/main/java/dev/rdnt/m8face/utils/InvisibleComplication.kt b/app/src/main/java/dev/rdnt/m8face/utils/InvisibleComplication.kt new file mode 100644 index 0000000..5a54a34 --- /dev/null +++ b/app/src/main/java/dev/rdnt/m8face/utils/InvisibleComplication.kt @@ -0,0 +1,58 @@ +package dev.rdnt.m8face.utils + +import android.content.Context +import android.graphics.* +import android.util.Log +import androidx.core.content.ContextCompat +import androidx.core.graphics.ColorUtils +import androidx.core.graphics.drawable.toBitmap +import androidx.core.graphics.withRotation +import androidx.wear.watchface.CanvasComplication +import androidx.wear.watchface.CanvasComplicationFactory +import androidx.wear.watchface.RenderParameters +import androidx.wear.watchface.complications.data.* +import dev.rdnt.m8face.R +import java.time.Instant +import java.time.ZonedDateTime + +class InvisibleComplication(private val context: Context) : CanvasComplication { + override fun render( + canvas: Canvas, + bounds: Rect, + zonedDateTime: ZonedDateTime, + renderParameters: RenderParameters, + slotId: Int + ) { + if (bounds.isEmpty) return + +// // DEBUG +// canvas.drawRect(bounds, Paint().apply { +// color = Color.parseColor("#22ffffff") +// }) + } + + override fun drawHighlight( + canvas: Canvas, + bounds: Rect, + boundsType: Int, + zonedDateTime: ZonedDateTime, + color: Int + ) { + // Rendering of highlights + } + + private var data: ComplicationData = NoDataComplicationData() + + override fun getData(): ComplicationData = data + + override fun loadData( + complicationData: ComplicationData, + loadDrawablesAsynchronous: Boolean + ) { + data = complicationData + } +} + +fun createInvisibleComplicationFactory(context: Context) = CanvasComplicationFactory { _, _ -> + InvisibleComplication(context) +} diff --git a/app/src/main/java/dev/rdnt/m8face/utils/UserStyleSchemaUtils.kt b/app/src/main/java/dev/rdnt/m8face/utils/UserStyleSchemaUtils.kt index 01770a5..9ed8649 100644 --- a/app/src/main/java/dev/rdnt/m8face/utils/UserStyleSchemaUtils.kt +++ b/app/src/main/java/dev/rdnt/m8face/utils/UserStyleSchemaUtils.kt @@ -16,26 +16,33 @@ package dev.rdnt.m8face.utils import android.content.Context +import android.graphics.RectF +import android.graphics.drawable.Icon import android.text.format.DateFormat +import androidx.wear.watchface.complications.ComplicationSlotBounds +import androidx.wear.watchface.complications.data.ComplicationType import androidx.wear.watchface.style.UserStyleSchema import androidx.wear.watchface.style.UserStyleSetting +import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting +import androidx.wear.watchface.style.UserStyleSetting.ComplicationSlotsUserStyleSetting.ComplicationSlotsOption import androidx.wear.watchface.style.WatchFaceLayer import dev.rdnt.m8face.R import dev.rdnt.m8face.data.watchface.AmbientStyle import dev.rdnt.m8face.data.watchface.AmbientStyle.Companion.ambientStyleToListOption import dev.rdnt.m8face.data.watchface.ColorStyle import dev.rdnt.m8face.data.watchface.ColorStyle.Companion.colorStyleToListOption +import dev.rdnt.m8face.data.watchface.LayoutStyle import dev.rdnt.m8face.data.watchface.SecondsStyle import dev.rdnt.m8face.data.watchface.SecondsStyle.Companion.secondsStyleToListOption // Keys to matched content in the user style settings. We listen for changes to these // values in the renderer and if new, we will update the database and update the watch face // being rendered. +const val LAYOUT_STYLE_SETTING = "layout_style_setting" const val COLOR_STYLE_SETTING = "color_style_setting" const val AMBIENT_STYLE_SETTING = "ambient_style_setting" const val SECONDS_STYLE_SETTING = "seconds_style_setting" const val MILITARY_TIME_SETTING = "military_time_setting" -const val BIG_AMBIENT_SETTING = "big_ambient_setting" /* * Creates user styles in the settings activity associated with the watch face, so users can @@ -45,6 +52,281 @@ const val BIG_AMBIENT_SETTING = "big_ambient_setting" fun createUserStyleSchema(context: Context): UserStyleSchema { // 1. Allows user to change the color styles of the watch face (if any are available). + // TODO: @rdnt fix icons, fix name resource IDs and screen reader name resource IDs + + val info1 = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.INFO1.id), + resources = context.resources, + displayNameResourceId = R.string.info1_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + TOP_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + BOTTOM_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val info2 = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.INFO2.id), + resources = context.resources, + displayNameResourceId = R.string.info2_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + RIGHT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + TOP_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + BOTTOM_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val info3 = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.INFO3.id), + resources = context.resources, + displayNameResourceId = R.string.info3_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + TOP_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + BOTTOM_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val info4 = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.INFO4.id), + resources = context.resources, + displayNameResourceId = R.string.info4_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + TOP_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + BOTTOM_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_TOP_LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_BOTTOM_LEFT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_TOP_RIGHT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + COMPLICATIONS_BOTTOM_RIGHT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val sport = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.SPORT.id), + resources = context.resources, + displayNameResourceId = R.string.sport_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, + complicationSlotBounds = ComplicationSlotBounds(RectF( + HOUR_SPORT_COMPLICATION_LEFT_BOUND, + HOUR_SPORT_COMPLICATION_TOP_BOUND, + HOUR_SPORT_COMPLICATION_RIGHT_BOUND, + HOUR_SPORT_COMPLICATION_BOTTOM_BOUND, + )), +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, + complicationSlotBounds = ComplicationSlotBounds(RectF( + MINUTE_SPORT_COMPLICATION_LEFT_BOUND, + MINUTE_SPORT_COMPLICATION_TOP_BOUND, + MINUTE_SPORT_COMPLICATION_RIGHT_BOUND, + MINUTE_SPORT_COMPLICATION_BOTTOM_BOUND, + )), +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + TOP_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + BOTTOM_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + RIGHT_TEXT_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val focus = ComplicationSlotsOption( + id = UserStyleSetting.Option.Id(LayoutStyle.FOCUS.id), + resources = context.resources, + displayNameResourceId = R.string.focus_layout_style_name, + icon = Icon.createWithResource(context, R.drawable.aqua_style_icon), +// screenReaderNameResourceId = R.string.sport_layout_style_name, + complicationSlotOverlays = listOf( + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + HOUR_COMPLICATION_ID, + enabled = true, + complicationSlotBounds = ComplicationSlotBounds(RectF( + HOUR_FOCUS_COMPLICATION_LEFT_BOUND, + HOUR_FOCUS_COMPLICATION_TOP_BOUND, + HOUR_FOCUS_COMPLICATION_RIGHT_BOUND, + HOUR_FOCUS_COMPLICATION_BOTTOM_BOUND, + )), +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + MINUTE_COMPLICATION_ID, + enabled = true, + complicationSlotBounds = ComplicationSlotBounds(RectF( + MINUTE_FOCUS_COMPLICATION_LEFT_BOUND, + MINUTE_FOCUS_COMPLICATION_TOP_BOUND, + MINUTE_FOCUS_COMPLICATION_RIGHT_BOUND, + MINUTE_FOCUS_COMPLICATION_BOTTOM_BOUND, + )), +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + FOCUS_LEFT_ICON_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ComplicationSlotsUserStyleSetting.ComplicationSlotOverlay( + FOCUS_RIGHT_ICON_COMPLICATION_ID, + enabled = true, +// nameResourceId = R.string.minute_complication_name, + ), + ) + ) + + val layoutStyleSetting = + ComplicationSlotsUserStyleSetting( + id = UserStyleSetting.Id(LAYOUT_STYLE_SETTING), + resources = context.resources, + displayNameResourceId = R.string.layout_style_setting, + descriptionResourceId = R.string.layout_style_setting_description, + icon = Icon.createWithResource(context, R.drawable.mauve_style_icon), // TODO: @rdnt fix icon + complicationConfig = listOf( + info1, + info2, + info3, + info4, + sport, + focus, + ), + listOf(WatchFaceLayer.COMPLICATIONS), + defaultOption = info2, + ) + val colorStyleSetting = UserStyleSetting.ListUserStyleSetting( UserStyleSetting.Id(COLOR_STYLE_SETTING), @@ -92,24 +374,14 @@ fun createUserStyleSchema(context: Context): UserStyleSchema { DateFormat.is24HourFormat(context), // default ) - val bigAmbientSetting = UserStyleSetting.BooleanUserStyleSetting( - UserStyleSetting.Id(BIG_AMBIENT_SETTING), - context.resources, - R.string.big_ambient_setting, - R.string.big_ambient_setting_description, - null, - listOf(WatchFaceLayer.BASE), - false, - ) - // 4. Create style settings to hold all options. return UserStyleSchema( listOf( + layoutStyleSetting, colorStyleSetting, ambientStyleSetting, secondsStyleSetting, militaryTimeSetting, - bigAmbientSetting, ) ) } diff --git a/app/src/main/java/dev/rdnt/m8face/utils/VerticalComplication.kt b/app/src/main/java/dev/rdnt/m8face/utils/VerticalComplication.kt index f0075b5..0e5449b 100644 --- a/app/src/main/java/dev/rdnt/m8face/utils/VerticalComplication.kt +++ b/app/src/main/java/dev/rdnt/m8face/utils/VerticalComplication.kt @@ -2,9 +2,7 @@ package dev.rdnt.m8face.utils import android.content.Context import android.graphics.* -import android.util.Log import androidx.core.content.ContextCompat -import androidx.core.graphics.ColorUtils import androidx.core.graphics.drawable.toBitmap import androidx.wear.watchface.CanvasComplication import androidx.wear.watchface.CanvasComplicationFactory @@ -15,6 +13,8 @@ import java.time.Instant import java.time.ZonedDateTime class VerticalComplication(private val context: Context) : CanvasComplication { + private val renderer = ComplicationRenderer() + var tertiaryColor: Int = Color.parseColor("#8888bb") set(tertiaryColor) { field = tertiaryColor @@ -23,25 +23,13 @@ class VerticalComplication(private val context: Context) : CanvasComplication { iconPaint.colorFilter = PorterDuffColorFilter(tertiaryColor, PorterDuff.Mode.SRC_IN) prefixPaint.color = tertiaryColor prefixPaint.alpha = 100 - } - - var opacity: Float = 1f - set(opacity) { - field = opacity - - val color = ColorUtils.blendARGB(Color.TRANSPARENT, tertiaryColor, opacity) - textPaint.color = color - titlePaint.color = color - - iconPaint.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_IN) - imagePaint.alpha = (opacity * 255).toInt() - - prefixPaint.color = color - prefixPaint.alpha = 100 + renderer.reset() } private val textPaint = Paint().apply { isAntiAlias = true + isDither = true + isFilterBitmap = true typeface = context.resources.getFont(R.font.m8stealth57) textAlign = Paint.Align.LEFT color = tertiaryColor @@ -77,25 +65,35 @@ class VerticalComplication(private val context: Context) : CanvasComplication { ) { if (bounds.isEmpty) return - when (data.type) { + val data = if (data.type == ComplicationType.NO_DATA) { + val placeholder = (data as NoDataComplicationData).placeholder + placeholder ?: data + } else { + data + } + + val bitmap = when (data.type) { ComplicationType.SHORT_TEXT -> { - renderShortTextComplication(canvas, bounds, data as ShortTextComplicationData) + renderer.render(bounds, data, ::renderShortTextComplication) } ComplicationType.MONOCHROMATIC_IMAGE -> { - renderMonochromaticImageComplication( - canvas, - bounds, - data as MonochromaticImageComplicationData - ) + renderer.render(bounds, data, ::renderMonochromaticImageComplication) } ComplicationType.SMALL_IMAGE -> { - renderSmallImageComplication(canvas, bounds, data as SmallImageComplicationData) + renderer.render(bounds, data, ::renderSmallImageComplication) } else -> return } + + canvas.drawBitmap( + bitmap, + bounds.left.toFloat(), + bounds.top.toFloat(), + Paint(), + ) } private fun renderShortTextComplication( @@ -122,11 +120,11 @@ class VerticalComplication(private val context: Context) : CanvasComplication { if (isBattery) { val drawable = ContextCompat.getDrawable(context, R.drawable.battery_icon_32)!! icon = drawable.toBitmap( - (32f / 78f * bounds.width()).toInt(), - (32f / 78f * bounds.width()).toInt() + (32f).toInt(), + (32f).toInt() ) iconBounds = - Rect(0, 0, (32f / 78f * bounds.width()).toInt(), (32f / 78f * bounds.width()).toInt()) + Rect(0, 0, (32f).toInt(), (32f).toInt()) } else if (data.monochromaticImage != null) { val drawable = data.monochromaticImage!!.image.loadDrawable(context) if (drawable != null) { @@ -149,11 +147,11 @@ class VerticalComplication(private val context: Context) : CanvasComplication { } if (text.length <= 3) { - textPaint.textSize = 24F / 78F * bounds.width() + textPaint.textSize = 24F } else if (text.length <= 6) { - textPaint.textSize = 16F / 78F * bounds.width() + textPaint.textSize = 16F } else { - textPaint.textSize = 12F / 78F * bounds.width() + textPaint.textSize = 12F } val textBounds = Rect() @@ -168,11 +166,11 @@ class VerticalComplication(private val context: Context) : CanvasComplication { if (title != null) { if (title.length <= 3) { - titlePaint.textSize = 24F / 78F * bounds.width() + titlePaint.textSize = 24F } else if (title.length <= 6) { - titlePaint.textSize = 16F / 78F * bounds.width() + titlePaint.textSize = 16F } else { - titlePaint.textSize = 12F / 78F * bounds.width() + titlePaint.textSize = 12F } titlePaint.getTextBounds(title, 0, title.length, titleBounds) @@ -188,22 +186,28 @@ class VerticalComplication(private val context: Context) : CanvasComplication { iconOffsetY = (height - iconBounds.height()).toFloat() / 2f textOffsetY = (height - textBounds.height()).toFloat() / 2f - iconOffsetY += 9f / 132f * bounds.height() + iconOffsetY += 6f if (isBattery) { iconOffsetY = iconOffsetY.toInt().toFloat() } - textOffsetY += 9f / 132f * bounds.height() + textOffsetY += 6f } else if (title != null) { val height = titleBounds.height() + textBounds.height() titleOffsetY = (height - titleBounds.height()).toFloat() / 2f textOffsetY = (height - textBounds.height()).toFloat() / 2f - titleOffsetY += 9f / 132f * bounds.height() - textOffsetY += 9f / 132f * bounds.height() + titleOffsetY += 6f + textOffsetY += 6f } + + + + + + if (icon != null) { val dstRect = RectF( bounds.exactCenterX() - iconBounds.width() / 2, @@ -240,6 +244,7 @@ class VerticalComplication(private val context: Context) : CanvasComplication { bounds.exactCenterY() + textBounds.height() / 2 + textOffsetY, textPaint ) + } private fun renderMonochromaticImageComplication( @@ -252,7 +257,7 @@ class VerticalComplication(private val context: Context) : CanvasComplication { val drawable = data.monochromaticImage.image.loadDrawable(context) ?: return - val size = (bounds.width().coerceAtMost(bounds.height()).toFloat() * 0.8f).toInt() + val size = (bounds.width().coerceAtMost(bounds.height()).toFloat() * 0.75f).toInt() icon = drawable.toBitmap(size, size) iconBounds = Rect(0, 0, size, size) diff --git a/app/src/main/res/drawable-nodpi/bold_outline_style_icon.png b/app/src/main/res/drawable-nodpi/bold_outline_style_icon.png index d3ba120..b9c4542 100644 Binary files a/app/src/main/res/drawable-nodpi/bold_outline_style_icon.png and b/app/src/main/res/drawable-nodpi/bold_outline_style_icon.png differ diff --git a/app/src/main/res/drawable-nodpi/watch_preview.png b/app/src/main/res/drawable-nodpi/watch_preview.png index 9ed6b23..2ba7c71 100644 Binary files a/app/src/main/res/drawable-nodpi/watch_preview.png and b/app/src/main/res/drawable-nodpi/watch_preview.png differ diff --git a/app/src/main/res/drawable-nodpi/watch_preview_subtle.png b/app/src/main/res/drawable-nodpi/watch_preview_subtle.png index 98018a4..42d2e99 100644 Binary files a/app/src/main/res/drawable-nodpi/watch_preview_subtle.png and b/app/src/main/res/drawable-nodpi/watch_preview_subtle.png differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fed0f35..d9f9aa1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -14,16 +14,18 @@ limitations under the License. --> - m8face + M8 - m8face + M8 + Layout Style Color Ambient Style Seconds Indicator + Layout Style Color scheme Ambient mode style Seconds indicator style @@ -75,20 +77,41 @@ Snow Onyx - Outline - Bold - Filled + Outline I + Outline II + Bold I + Bold II + Filled I + Filled II + Detailed None Dashes Dots + Hours + Minutes + Left Right + Top Bottom + Top Left + Bottom Left + Top Right + Bottom Right + + + Info I + Info II + Info III + Info IV + Focus + Sport + @@ -126,12 +149,16 @@ Hour Pips Military Time Big Ambient + Detailed Ambient + Debug Whether to draw or not Whether to use military instead of standard time Use big time on ambient mode + Show complications on ambient mode + >.> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 651071e..94174ba 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,12 +1,11 @@ [versions] accompanist-pager = "0.32.0" -activity-compose = "1.8.0" -android-gradle-plugin = "8.1.2" -androidx-activity = "1.8.0" -androidx-lifecycle = "2.6.2" -androidx-wear-watchface = "1.1.1" -compiler = "1.5.3" -compose-material = "1.2.0" +activity-compose = "1.8.2" +android-gradle-plugin = "8.3.1" +androidx-activity = "1.8.2" +androidx-lifecycle = "2.7.0" +androidx-wear-watchface = "1.2.1" +compiler = "1.5.11" horologist-compose-layout = "0.5.7" ktlint = "0.46.1" org-jetbrains-kotlin = "1.9.10" @@ -15,14 +14,14 @@ org-jetbrains-kotlinx = "1.7.3" [libraries] accompanist-pager = { module = "com.google.accompanist:accompanist-pager", version.ref = "accompanist-pager" } accompanist-pager-indicators = { module = "com.google.accompanist:accompanist-pager-indicators", version.ref = "accompanist-pager" } -android-material = "com.google.android.material:material:1.10.0" +android-material = "com.google.android.material:material:1.11.0" androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activity-compose" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-compose-material-iconsExtended = { module = "androidx.compose.material:material-icons-extended" } androidx-activity-ktx = { module = "androidx.activity:activity-ktx", version.ref = "androidx-activity" } androidx-compiler = { module = "androidx.compose.compiler:compiler", version.ref = "compiler" } -androidx-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version = "1.3.0-alpha07" } -androidx-compose-material = { module = "androidx.wear.compose:compose-material", version = "1.3.0-alpha07" } +androidx-compose-foundation = { module = "androidx.wear.compose:compose-foundation", version = "1.3.0" } +androidx-compose-material = { module = "androidx.wear.compose:compose-material", version = "1.3.0" } androidx-core-ktx = "androidx.core:core-ktx:1.12.0" androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx-lifecycle" } androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2848974..309b4e1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.0-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle index 4b87ba7..f299f1c 100644 --- a/settings.gradle +++ b/settings.gradle @@ -33,4 +33,4 @@ dependencyResolutionManagement { } include ":app" -rootProject.name = "m8face" +rootProject.name = "m8"