diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db8769d7..d8205d30f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add 'Animator duration scale' alert - Update App theme colors - Update App monochrome logo +- Update Android release timeline dialog - Fix Android U platlogo - Project modularization - Upgrade project dependencies diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index a1ec9b688..c9ae08a1b 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -6,6 +6,7 @@ - 添加 ‘Animator 时长缩放’ 检查 - 更新 App 主题色 - 更新 App 单色图标 +- 更新 Android 发布时间线弹框 - 修复 Android U 彩蛋图标 - 项目模块化 - 升级项目依赖项 diff --git a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItems.kt b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItems.kt index 384477915..abce22afc 100644 --- a/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItems.kt +++ b/app/src/main/java/com/dede/android_eggs/views/main/compose/EasterEggItems.kt @@ -39,6 +39,7 @@ import com.dede.android_eggs.views.main.util.EasterEggShortcutsHelp import com.dede.android_eggs.views.main.util.EggActionHelp import com.dede.basic.provider.BaseEasterEgg import com.dede.basic.provider.EasterEgg +import com.dede.basic.utils.AppLocaleDateFormatter @Composable @Preview @@ -55,7 +56,7 @@ fun EasterEggHighestItem( EasterEggHelp.ApiLevelFormatter.create(egg.apiLevelRange).format(context) } val dateFormat = remember(egg, context.resources.configuration) { - EasterEggHelp.DateFormatter.getInstance("MM yyyy") + AppLocaleDateFormatter.getInstance("MM yyyy") } Card( diff --git a/app/src/main/java/com/dede/android_eggs/views/main/util/EasterEggHelp.kt b/app/src/main/java/com/dede/android_eggs/views/main/util/EasterEggHelp.kt index 79f8102e6..8699d84ff 100644 --- a/app/src/main/java/com/dede/android_eggs/views/main/util/EasterEggHelp.kt +++ b/app/src/main/java/com/dede/android_eggs/views/main/util/EasterEggHelp.kt @@ -1,11 +1,9 @@ package com.dede.android_eggs.views.main.util import android.content.Context -import android.icu.text.SimpleDateFormat import android.os.Build import android.util.SparseArray import androidx.annotation.StringRes -import androidx.appcompat.app.AppCompatDelegate import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalInspectionMode import com.dede.android_eggs.R @@ -13,9 +11,6 @@ import com.dede.android_eggs.inject.EasterEggModules import com.dede.basic.provider.EasterEgg import com.dede.basic.provider.EasterEggProvider import dagger.Module -import java.text.Format -import java.util.Date -import java.util.Locale object EasterEggHelp { @@ -39,34 +34,6 @@ object EasterEggHelp { return EasterEggModules.providePureEasterEggList(baseEasterEggs) } - class DateFormatter private constructor(pattern: String, locale: Locale) { - - companion object { - fun getInstance(pattern: String): DateFormatter { - return DateFormatter(pattern, getApplicationLocale()) - } - - private fun getApplicationLocale(): Locale { - val locales = AppCompatDelegate.getApplicationLocales() - return if (locales.isEmpty) { - Locale.getDefault() - } else { - locales.get(0) ?: Locale.getDefault() - } - } - } - - private val format: Format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - SimpleDateFormat(pattern, locale) - } else { - java.text.SimpleDateFormat(pattern, locale) - } - - fun format(date: Date): String { - return format.format(date) - } - } - class VersionFormatter private constructor( @StringRes val nicknameRes: Int, private vararg val versionNames: CharSequence, diff --git a/app/src/main/java/com/dede/android_eggs/views/timeline/TimelineEventHelp.kt b/app/src/main/java/com/dede/android_eggs/views/timeline/TimelineEventHelp.kt index f175bca44..71d9a0fe5 100644 --- a/app/src/main/java/com/dede/android_eggs/views/timeline/TimelineEventHelp.kt +++ b/app/src/main/java/com/dede/android_eggs/views/timeline/TimelineEventHelp.kt @@ -5,8 +5,8 @@ import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle -import com.dede.android_eggs.views.main.util.EasterEggHelp import com.dede.basic.provider.TimelineEvent +import com.dede.basic.utils.AppLocaleDateFormatter import java.util.Calendar import java.util.TimeZone @@ -37,14 +37,14 @@ object TimelineEventHelp { get() { val calendar = Calendar.getInstance(TimeZone.getDefault()) calendar.set(Calendar.YEAR, year) - return EasterEggHelp.DateFormatter.getInstance("yyyy").format(calendar.time) + return AppLocaleDateFormatter.getInstance("yyyy").format(calendar.time) } val TimelineEvent.localMonth: String get() { val calendar = Calendar.getInstance(TimeZone.getDefault()) calendar.set(Calendar.MONTH, month) - return EasterEggHelp.DateFormatter.getInstance("MMMM").format(calendar.time) + return AppLocaleDateFormatter.getInstance("MMMM").format(calendar.time) } val TimelineEvent.eventAnnotatedString: AnnotatedString diff --git a/basic/src/main/java/com/dede/basic/utils/AppLocaleDateFormatter.kt b/basic/src/main/java/com/dede/basic/utils/AppLocaleDateFormatter.kt new file mode 100644 index 000000000..62a0920da --- /dev/null +++ b/basic/src/main/java/com/dede/basic/utils/AppLocaleDateFormatter.kt @@ -0,0 +1,49 @@ +package com.dede.basic.utils + +import android.icu.text.SimpleDateFormat +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import java.text.DateFormat +import java.text.FieldPosition +import java.text.Format +import java.text.ParsePosition +import java.util.Date +import java.util.Locale + + +class AppLocaleDateFormatter private constructor(pattern: String, locale: Locale) : DateFormat() { + + companion object { + fun getInstance(pattern: String): AppLocaleDateFormatter { + return AppLocaleDateFormatter(pattern, getApplicationLocale()) + } + + private fun getApplicationLocale(): Locale { + val locales = AppCompatDelegate.getApplicationLocales() + return if (locales.isEmpty) { + Locale.getDefault() + } else { + locales.get(0) ?: Locale.getDefault() + } + } + } + + private val format: Format = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + SimpleDateFormat(pattern, locale) + } else { + java.text.SimpleDateFormat(pattern, locale) + } + + override fun format( + date: Date, + toAppendTo: StringBuffer, + fieldPosition: FieldPosition + ): StringBuffer { + return format.format(date, toAppendTo, fieldPosition) + } + + override fun parse(source: String, pos: ParsePosition): Date? { + return format.parseObject(source, pos) as? Date + } + +} diff --git a/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextEasterEgg.kt b/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextEasterEgg.kt index 7f571914f..d4441e88d 100644 --- a/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextEasterEgg.kt +++ b/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextEasterEgg.kt @@ -24,7 +24,7 @@ import javax.inject.Singleton object AndroidNextEasterEgg : EasterEggProvider { const val RELEASE_YEAR = 2025 - private const val RELEASE_MONTH = Calendar.SEPTEMBER + const val RELEASE_MONTH = Calendar.MAY // private const val NEXT_API = Build.VERSION_CODES.CUR_DEVELOPMENT// android next private const val NEXT_API = 36// android 16 @@ -41,16 +41,6 @@ object AndroidNextEasterEgg : EasterEggProvider { @DrawableRes private val PLATLOGO_RES = R.drawable.ic_android_16_platlogo - fun getTimelineMessage(context: Context): String { - val calendar = Calendar.getInstance() - val year = calendar.get(Calendar.YEAR) - return if (year > RELEASE_YEAR) { - context.getString(R.string.summary_android_release_pushed) - } else { - context.getString(R.string.summary_android_waiting) - } - } - @Provides @IntoSet @Singleton diff --git a/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextTimelineDialog.kt b/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextTimelineDialog.kt index 0cdc53d2c..e67c83a99 100644 --- a/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextTimelineDialog.kt +++ b/eggs/AndroidNext/src/main/java/com/android_next/egg/AndroidNextTimelineDialog.kt @@ -5,7 +5,6 @@ import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -16,9 +15,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -28,9 +25,14 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorMatrix @@ -38,14 +40,18 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.drawText +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import com.dede.android_eggs.util.CustomTabsBrowser import com.dede.basic.requireDrawable import com.google.accompanist.drawablepainter.rememberDrawablePainter import kotlinx.coroutines.launch import java.util.Calendar -import kotlin.math.max import kotlin.math.min internal var androidNextDialogVisible by mutableStateOf(false) @@ -82,7 +88,7 @@ fun AndroidNextTimelineDialog( text = { Column { Text( - text = AndroidNextEasterEgg.getTimelineMessage(context), + text = getTimelineMessage(context), style = MaterialTheme.typography.bodyMedium ) Spacer(modifier = Modifier.height(12.dp)) @@ -109,40 +115,45 @@ fun AndroidNextTimelineDialog( ) } +private val offsetXPercentArr = floatArrayOf( + 49 / 789f, + 153 / 789f, + 257 / 789f, + 361 / 789f, + 465 / 789f, + 569 / 789f, + 717 / 789f +) + +private const val offsetYPercent = 135 / 180f + @Composable private fun AndroidReleaseTimeline() { - val calendar = Calendar.getInstance() - val year = calendar.get(Calendar.YEAR) - val month = calendar.get(Calendar.MONTH)// [0, 11] - // Month Progress Calender.MONTH - // Feb 0 1 - // ... - // Jul 5 6 - // Aug - 7 - val offsetXArr = intArrayOf(20, 111, 202, 294, 386, 478, 584) - val nextReleaseYear = AndroidNextEasterEgg.RELEASE_YEAR - val offsetXIndex = - if (year < nextReleaseYear || (year == nextReleaseYear && month < Calendar.FEBRUARY)) { + val nowDate = Calendar.getInstance().setDateZero() + val releaseDate = remember { getReleaseCalendar() } + + val offsetXIndex = remember(nowDate, releaseDate) { + val diffMonth = getDateDiffMonth(start = nowDate, end = releaseDate) + if (diffMonth > MONTH_CYCLE) { // No preview -1 - } else if (year == nextReleaseYear && month in Calendar.FEBRUARY..Calendar.JULY) { + } else if (diffMonth < MONTH_CYCLE) { // Preview - month - 1 + MONTH_CYCLE - diffMonth - 1 } else { // Final release 6 } + } val isFinalRelease = offsetXIndex == 6 - val hasPreview = offsetXIndex != -1 - val offsetX = offsetXArr[min(offsetXArr.size - 1, max(offsetXIndex, 0))] val scrollState = rememberScrollState() - if (hasPreview) { + if (offsetXIndex != -1) { LaunchedEffect(offsetXIndex, isFinalRelease) { val value = if (isFinalRelease) { scrollState.maxValue } else { - scrollState.maxValue / (offsetXArr.size) * (offsetXIndex) + scrollState.maxValue / offsetXPercentArr.size * offsetXIndex } launch { scrollState.animateScrollTo(value) @@ -156,48 +167,86 @@ private fun AndroidReleaseTimeline() { .height(160.dp) .aspectRatio(789f / 180) ) { - if (hasPreview) { - Box( - modifier = Modifier - .padding(top = 103.dp, start = offsetX.dp) - ) { - val shape = RoundedCornerShape( - topStartPercent = 50, topEndPercent = 50, - bottomEndPercent = 50, bottomStartPercent = 50 - ) - if (isFinalRelease) { - // Final release - Box( - modifier = Modifier - .width(106.dp) - .height(34.dp) - .background(Color(0xFF3DDC84), shape) - ) - } else { - // Preview - Box( - modifier = Modifier - .width(52.dp) - .height(34.dp) - .background(Color(0xFFF86734), shape) - ) - } - } - } + val context = LocalContext.current val matrix = ColorMatrix() - if (isSystemNightMode(LocalContext.current)) { + if (isSystemNightMode(context)) { // Increase the overall brightness and more blue brightness matrix.setToScale(1.3f, 1.5f, 2f, 1f) } + + val timelineMonths = remember { getReleaseCycleMonths(context) } + val textMeasurer = rememberTextMeasurer(cacheSize = timelineMonths.size) Image( painter = painterResource(id = R.drawable.timeline_bg), contentDescription = null, - colorFilter = ColorFilter.colorMatrix(matrix) + colorFilter = ColorFilter.colorMatrix(matrix), + modifier = Modifier.drawWithCache { + onDrawWithContent { + for ((index, month) in timelineMonths.withIndex()) { + val isLastMonth = index == timelineMonths.size - 1 + + val textLayout = textMeasurer.measure( + text = month, + style = TextStyle( + fontSize = 14.sp, + fontWeight = if (isLastMonth) FontWeight.Bold else FontWeight.Medium + ), + ) + + val offsetX = size.width * offsetXPercentArr[index] + val offsetY = size.height * offsetYPercent + + if (index == offsetXIndex) { + val rectSize = Size( + width = textLayout.size.width + textLayout.size.height * 1.3f, + height = textLayout.size.height * 1.6f + ) + val radius = min(rectSize.height, rectSize.width) / 2f + drawRoundRect( + color = if (isLastMonth) + Color(0xFF3DDC84) + else + Color(0xFFF86734), + topLeft = Offset( + x = offsetX - rectSize.width / 2f, + y = offsetY - rectSize.height / 2f + ), + size = rectSize, + cornerRadius = CornerRadius(radius, radius) + ) + } + + drawText( + textLayoutResult = textLayout, + color = if (isLastMonth) + Color(0xFF188038) + else + Color.Black, + topLeft = Offset( + x = offsetX - textLayout.size.width / 2f, + y = offsetY - textLayout.size.height / 2f, + ), + ) + } + + drawContent() + } + } ) } } } +private fun getTimelineMessage(context: Context): String { + val nowDate = Calendar.getInstance().setDateZero() + val releaseDate = getReleaseCalendar() + return if (nowDate.after(releaseDate)) { + context.getString(R.string.summary_android_release_pushed) + } else { + context.getString(R.string.summary_android_waiting) + } +} + private fun isSystemNightMode(context: Context): Boolean { return (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES diff --git a/eggs/AndroidNext/src/main/java/com/android_next/egg/Utiles.kt b/eggs/AndroidNext/src/main/java/com/android_next/egg/Utiles.kt new file mode 100644 index 000000000..868c5278d --- /dev/null +++ b/eggs/AndroidNext/src/main/java/com/android_next/egg/Utiles.kt @@ -0,0 +1,58 @@ +package com.android_next.egg + +import android.content.Context +import com.dede.basic.utils.AppLocaleDateFormatter +import java.util.Calendar + +internal const val MONTH_CYCLE = 7 +internal val MONTHS = intArrayOf( + Calendar.JANUARY, + Calendar.FEBRUARY, + Calendar.MARCH, + Calendar.APRIL, + Calendar.MAY, + Calendar.JUNE, + Calendar.JULY, + Calendar.AUGUST, + Calendar.SEPTEMBER, + Calendar.OCTOBER, + Calendar.NOVEMBER, + Calendar.DECEMBER, +) + +internal fun Calendar.setDateZero(): Calendar { + clear(Calendar.HOUR_OF_DAY) + clear(Calendar.MINUTE) + clear(Calendar.SECOND) + clear(Calendar.MILLISECOND) + return this +} + +internal fun getReleaseCalendar(): Calendar { + val calendar = Calendar.getInstance() + calendar.set(AndroidNextEasterEgg.RELEASE_YEAR, AndroidNextEasterEgg.RELEASE_MONTH, 1) + calendar.setDateZero() + return calendar +} + +internal fun getReleaseCycleMonths(context: Context): List { + val calendar = getReleaseCalendar() + val list = ArrayList() + // add final release label + list.add(context.getString(R.string.label_timeline_final_release)) + val format = AppLocaleDateFormatter.getInstance("MMM") + var c = 1 + do { + calendar.add(Calendar.MONTH, -1) + list.add(format.format(calendar.time)) + c++ + } while (c < MONTH_CYCLE) + list.reverse() + return list +} + +internal fun getDateDiffMonth(start: Calendar, end: Calendar): Int { + val yearDiff = end[Calendar.YEAR] - start[Calendar.YEAR] + val monthDiff = end[Calendar.MONTH] - start[Calendar.MONTH] + return yearDiff * 12 + monthDiff +} diff --git a/eggs/AndroidNext/src/main/res/drawable-anydpi/timeline_bg.xml b/eggs/AndroidNext/src/main/res/drawable-anydpi/timeline_bg.xml index c2c3e737b..f56ce567d 100644 --- a/eggs/AndroidNext/src/main/res/drawable-anydpi/timeline_bg.xml +++ b/eggs/AndroidNext/src/main/res/drawable-anydpi/timeline_bg.xml @@ -17,37 +17,37 @@ android:fillColor="#1769E0"/> + android:fillColor="#54585D"/> + android:fillColor="#54585D"/> - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/eggs/AndroidNext/src/main/res/drawable-zh-anydpi/timeline_bg.xml b/eggs/AndroidNext/src/main/res/drawable-zh-anydpi/timeline_bg.xml index ef2fc8044..e6848c845 100644 --- a/eggs/AndroidNext/src/main/res/drawable-zh-anydpi/timeline_bg.xml +++ b/eggs/AndroidNext/src/main/res/drawable-zh-anydpi/timeline_bg.xml @@ -17,37 +17,37 @@ android:fillColor="#4285F4"/> + android:fillColor="#54585D"/> + android:fillColor="#54585D"/> - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/eggs/AndroidNext/src/main/res/values-zh/strings.xml b/eggs/AndroidNext/src/main/res/values-zh/strings.xml index 6c9f0b036..5024fb937 100644 --- a/eggs/AndroidNext/src/main/res/values-zh/strings.xml +++ b/eggs/AndroidNext/src/main/res/values-zh/strings.xml @@ -4,4 +4,5 @@ 最终版本已经发布 时间表、里程碑和更新 版本 + 最终发布 diff --git a/eggs/AndroidNext/src/main/res/values/strings.xml b/eggs/AndroidNext/src/main/res/values/strings.xml index 96748a891..894ee78a6 100644 --- a/eggs/AndroidNext/src/main/res/values/strings.xml +++ b/eggs/AndroidNext/src/main/res/values/strings.xml @@ -4,4 +4,5 @@ Timeline, milestones, and updates Releases + Final Release diff --git a/fastlane/metadata/android/en-US/changelogs/53.txt b/fastlane/metadata/android/en-US/changelogs/53.txt index aa6e18218..319e5eb6c 100644 --- a/fastlane/metadata/android/en-US/changelogs/53.txt +++ b/fastlane/metadata/android/en-US/changelogs/53.txt @@ -2,6 +2,7 @@ - Add 'Animator duration scale' alert - Update App theme colors - Update App monochrome logo +- Update Android release timeline dialog - Fix Android U platlogo - Project modularization - Upgrade project dependencies \ No newline at end of file diff --git a/fastlane/metadata/android/zh-CN/changelogs/53.txt b/fastlane/metadata/android/zh-CN/changelogs/53.txt index 0c50270fc..5dc8635c9 100644 --- a/fastlane/metadata/android/zh-CN/changelogs/53.txt +++ b/fastlane/metadata/android/zh-CN/changelogs/53.txt @@ -2,6 +2,7 @@ - 添加 ‘Animator 时长缩放’ 检查 - 更新 App 主题色 - 更新 App 单色图标 +- 更新 Android 发布时间线弹框 - 修复 Android U 彩蛋图标 - 项目模块化 - 升级项目依赖项 \ No newline at end of file