commit 5137c80d11d43c59129d883c47bd371dc0670a56 Author: ddshi <8811906+ddshi@user.noreply.gitee.com> Date: Wed Oct 22 20:05:11 2025 +0800 首页架构 diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..707d09e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,24 @@ +{ + "permissions": { + "allow": [ + "Bash(./gradlew test:*)", + "Bash(./gradlew:*)", + "Bash(tasklist:*)", + "Bash(findstr:*)", + "Bash(sed:*)", + "Bash(adb logcat:*)", + "Bash(adb:*)", + "Bash(taskkill:*)", + "Bash(wmic:*)", + "Bash(powershell:*)", + "Bash(.gradlew testDebugUnitTest --stacktrace)", + "Bash(gradlew:*)", + "Bash(./gradlew.bat testDebugUnitTest:*)", + "Bash(find:*)", + "Bash(./gradlew.bat assembleDebug:*)", + "Bash(git init:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfbca8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,143 @@ +# Built application files +*.apk +*.ap_ +*.aab + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ +# Uncomment the following line in case you need and you don't have the release build type files in your app +# release/ + +# Gradle files +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# IntelliJ +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/assetWizardSettings.xml +.idea/dictionaries +.idea/libraries +# Android Studio 3 in .gitignore file. +.idea/caches +.idea/modules.xml +# Comment next line if keeping position of elements in Navigation Editor is relevant for you +.idea/navEditor.xml + +# Keystore files +# Uncomment the following lines if you do not want to check your keystore files in. +#*.jks +#*.keystore + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild +.cxx/ + +# Google Services (e.g. APIs or Firebase) +# google-services.json + +# Freeline +freeline.py +freeline/ +freeline_project_description.json + +# fastlane +fastlane/report.xml +fastlane/Preview.html +fastlane/screenshots +fastlane/test_output +fastlane/readme.md + +# Version control +vcs.xml + +# lint +lint/intermediates/ +lint/generated/ +lint/outputs/ +lint/tmp/ +# lint/reports/ + +# Android Profiling +*.hprof + +## Plugin-specific files: + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JINJA/Liquid templates +*.j2 +*.liquid +*.html + +# Sentry +.sentryclirc + +# Mac +.DS_Store + +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini + +# Linux +*~ + +# Temporary files +*.tmp +*.temp +*.swp +*.swo +*~ + +# Editor directories and files +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Android NDK +obj/ + +# Android Backup files +.ab + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..b589d56 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml new file mode 100644 index 0000000..0c0c338 --- /dev/null +++ b/.idea/deploymentTargetDropDown.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml new file mode 100644 index 0000000..fdf8d99 --- /dev/null +++ b/.idea/kotlinc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..8978d23 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 0000000..d1a12bf --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,85 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("kotlin-kapt") +} + +android { + namespace = "com.daodaoshi.chick_mood" + compileSdk = 34 + + defaultConfig { + applicationId = "com.daodaoshi.chick_mood" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + viewBinding = true + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + // Android核心库 + implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.appcompat:appcompat:1.6.1") + implementation("androidx.activity:activity-ktx:1.8.2") + implementation("androidx.fragment:fragment-ktx:1.6.2") + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.constraintlayout:constraintlayout:2.1.4") + + // ViewModel和LiveData + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0") + + // Room数据库 + implementation("androidx.room:room-runtime:2.6.1") + implementation("androidx.room:room-ktx:2.6.1") + kapt("androidx.room:room-compiler:2.6.1") + + // ViewPager2和分页 + implementation("androidx.viewpager2:viewpager2:1.0.0") + implementation("androidx.paging:paging-runtime-ktx:3.2.1") + + // 协程 + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3") + + // 图片加载 + implementation("com.github.bumptech.glide:glide:4.16.0") + + // 测试依赖 + testImplementation("junit:junit:4.13.2") + testImplementation("androidx.arch.core:core-testing:2.2.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3") + androidTestImplementation("androidx.test.ext:junit:1.1.5") + androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/java/com/daodaoshi/chick_mood/ExampleInstrumentedTest.kt b/app/src/androidTest/java/com/daodaoshi/chick_mood/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..518dc31 --- /dev/null +++ b/app/src/androidTest/java/com/daodaoshi/chick_mood/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.daodaoshi.chick_mood + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.daodaoshi.chick_mood", appContext.packageName) + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..708a18b --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/chick_mood/data/model/Emotion.kt b/app/src/main/java/com/chick_mood/data/model/Emotion.kt new file mode 100644 index 0000000..559b819 --- /dev/null +++ b/app/src/main/java/com/chick_mood/data/model/Emotion.kt @@ -0,0 +1,74 @@ +package com.chick_mood.data.model + +/** + * 情绪类型枚举 + * 定义应用支持的六种基础情绪 + * + * @author Claude + * @date 2025-10-22 + */ +enum class Emotion(val displayName: String, val colorValue: String, val chickExpression: Int) { + /** + * 开心 - 橙色系,小鸡开心表情 + */ + HAPPY("开心", "#FFAB76", 1), + + /** + * 生气 - 红色系,小鸡生气表情 + */ + ANGRY("生气", "#D9534F", 2), + + /** + * 悲伤 - 蓝色系,小鸡悲伤表情 + */ + SAD("悲伤", "#5DADE2", 3), + + /** + * 烦恼 - 灰色系,小鸡烦恼表情 + */ + WORRIED("烦恼", "#95A5A6", 4), + + /** + * 孤单 - 紫色系,小鸡孤单表情 + */ + LONELY("孤单", "#AF7AC5", 5), + + /** + * 害怕 - 黄褐色系,小鸡害怕表情 + */ + SCARED("害怕", "#F4D03F", 6); + + /** + * 获取情绪对应的颜色资源ID + * @return 颜色资源ID + */ + fun getColorResourceId(): Int { + return when (this) { + HAPPY -> android.R.color.holo_orange_light + ANGRY -> android.R.color.holo_red_dark + SAD -> android.R.color.holo_blue_light + WORRIED -> android.R.color.darker_gray + LONELY -> android.R.color.holo_purple + SCARED -> android.R.color.holo_orange_dark + } + } + + companion object { + /** + * 根据数值获取情绪枚举 + * @param value 情绪数值(1-6) + * @return 对应的情绪枚举,无效值返回HAPPY + */ + fun fromValue(value: Int): Emotion { + return values().find { it.chickExpression == value } ?: HAPPY + } + + /** + * 获取所有情绪的显示名称列表 + * @return 情绪显示名称列表 + */ + fun getAllDisplayNames(): List { + return values().map { it.displayName } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chick_mood/data/model/MoodRecord.kt b/app/src/main/java/com/chick_mood/data/model/MoodRecord.kt new file mode 100644 index 0000000..97bdc43 --- /dev/null +++ b/app/src/main/java/com/chick_mood/data/model/MoodRecord.kt @@ -0,0 +1,167 @@ +package com.chick_mood.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey +import java.util.Date + +/** + * 心情记录数据模型 + * 存储用户的心情记录信息,包括情绪类型、强度、文字内容和图片路径 + * + * @author Claude + * @date 2025-10-22 + */ +@Entity(tableName = "mood_records") +data class MoodRecord( + /** + * 主键ID,自动生成 + */ + @PrimaryKey(autoGenerate = true) + val id: Long = 0, + + /** + * 情绪类型枚举 + */ + val emotion: Emotion, + + /** + * 心情强度值(0-100) + * 通过摇晃传感器计算得出 + */ + val moodIntensity: Int, + + /** + * 记录创建时间戳 + */ + val timestamp: Long, + + /** + * 用户输入的文字描述 + */ + val textContent: String? = null, + + /** + * 附加图片的本地路径 + */ + val imagePath: String? = null, + + /** + * 是否已收藏 + */ + val isFavorite: Boolean = false, + + /** + * 摇晃持续时间(秒) + */ + val shakeDuration: Float = 0f, + + /** + * 最大加速度值 + */ + val maxAcceleration: Float = 0f, + + /** + * 创建时使用的设备型号 + */ + val deviceModel: String? = null +) { + /** + * 获取格式化的时间字符串 + * @return 格式化的时间(yyyy.MM.dd E HH:mm) + */ + fun getFormattedTime(): String { + val formatter = java.text.SimpleDateFormat("yyyy.MM.dd E HH:mm", java.util.Locale.getDefault()) + return formatter.format(Date(timestamp)) + } + + /** + * 获取心情强度的描述文字 + * @return 强度描述(轻微/一般/强烈/极度) + */ + fun getIntensityDescription(): String { + return when { + moodIntensity < 25 -> "轻微" + moodIntensity < 50 -> "一般" + moodIntensity < 75 -> "强烈" + else -> "极度" + } + } + + /** + * 获取心情强度的表情符号 + * @return 对应强度的表情符号 + */ + fun getIntensityEmoji(): String { + return when { + moodIntensity < 25 -> "😌" + moodIntensity < 50 -> "😐" + moodIntensity < 75 -> "😰" + else -> "😱" + } + } + + /** + * 检查是否包含图片 + * @return true如果包含图片 + */ + fun hasImage(): Boolean { + return !imagePath.isNullOrEmpty() + } + + /** + * 检查是否包含文字内容 + * @return true如果包含文字 + */ + fun hasText(): Boolean { + return !textContent.isNullOrEmpty() + } + + /** + * 获取文字内容长度 + * @return 文字长度,如果没有文字返回0 + */ + fun getTextLength(): Int { + return textContent?.length ?: 0 + } + + companion object { + /** + * 心情强度的最小值 + */ + const val MIN_INTENSITY = 0 + + /** + * 心情强度的最大值 + */ + const val MAX_INTENSITY = 100 + + /** + * 文字内容的最大长度限制 + */ + const val MAX_TEXT_LENGTH = 200 + + /** + * 创建一个测试用的心情记录 + * @param emotion 情绪类型 + * @param intensity 心情强度 + * @param text 文字内容 + * @return 测试心情记录 + */ + fun createTestRecord( + emotion: Emotion = Emotion.HAPPY, + intensity: Int = 50, + text: String? = "测试心情记录" + ): MoodRecord { + return MoodRecord( + emotion = emotion, + moodIntensity = intensity.coerceIn(MIN_INTENSITY, MAX_INTENSITY), + timestamp = System.currentTimeMillis(), + textContent = text?.take(MAX_TEXT_LENGTH), + isFavorite = false, + shakeDuration = 3.0f, + maxAcceleration = 15.0f, + deviceModel = android.os.Build.MODEL + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/chick_mood/data/model/UserConfig.kt b/app/src/main/java/com/chick_mood/data/model/UserConfig.kt new file mode 100644 index 0000000..688f620 --- /dev/null +++ b/app/src/main/java/com/chick_mood/data/model/UserConfig.kt @@ -0,0 +1,155 @@ +package com.chick_mood.data.model + +import androidx.room.Entity +import androidx.room.PrimaryKey + +/** + * 用户配置数据模型 + * 存储用户的个人设置和偏好配置 + * + * @author Claude + * @date 2025-10-22 + */ +@Entity(tableName = "user_config") +data class UserConfig( + /** + * 主键ID,固定为1 + */ + @PrimaryKey + val id: Int = 1, + + /** + * 用户昵称 + */ + val nickname: String? = null, + + /** + * 用户头像路径 + */ + val avatarPath: String? = null, + + /** + * 是否启用深色模式 + */ + val isDarkMode: Boolean = false, + + /** + * 是否启用声音反馈 + */ + val isSoundEnabled: Boolean = true, + + /** + * 是否启用震动反馈 + */ + val isVibrationEnabled: Boolean = true, + + /** + * 心情记录提醒开关 + */ + val isReminderEnabled: Boolean = false, + + /** + * 提醒时间(小时,0-23) + */ + val reminderHour: Int = 20, + + /** + * 提醒时间(分钟,0-59) + */ + val reminderMinute: Int = 0, + + /** + * 应用版本号 + */ + val appVersion: String = "1.0.0", + + /** + * 数据版本号(用于数据迁移) + */ + val dataVersion: Int = 1, + + /** + * 首次使用时间戳 + */ + val firstUseTime: Long = System.currentTimeMillis(), + + /** + * 总使用次数 + */ + val totalUsageCount: Int = 0, + + /** + * 最后使用时间戳 + */ + val lastUsedTime: Long = System.currentTimeMillis() +) { + /** + * 获取完整的提醒时间字符串 + * @return 格式化的提醒时间(HH:mm) + */ + fun getReminderTimeString(): String { + return String.format("%02d:%02d", reminderHour, reminderMinute) + } + + /** + * 检查是否设置了提醒时间 + * @return true如果设置了有效的提醒时间 + */ + fun hasValidReminderTime(): Boolean { + return isReminderEnabled && reminderHour in 0..23 && reminderMinute in 0..59 + } + + /** + * 检查用户是否已设置昵称 + * @return true如果昵称不为空 + */ + fun hasNickname(): Boolean { + return !nickname.isNullOrEmpty() + } + + /** + * 检查用户是否已设置头像 + * @return true如果头像路径不为空 + */ + fun hasAvatar(): Boolean { + return !avatarPath.isNullOrEmpty() + } + + /** + * 获取使用天数 + * @return 从首次使用到现在的天数 + */ + fun getUsageDays(): Int { + val currentTime = System.currentTimeMillis() + val diffInMillis = currentTime - firstUseTime + return (diffInMillis / (24 * 60 * 60 * 1000)).toInt() + 1 + } + + companion object { + /** + * 默认用户配置 + */ + fun getDefault(): UserConfig { + return UserConfig() + } + + /** + * 创建测试用的用户配置 + * @param nickname 测试昵称 + * @return 测试用户配置 + */ + fun createTestConfig(nickname: String = "测试用户"): UserConfig { + return UserConfig( + nickname = nickname, + isDarkMode = false, + isSoundEnabled = true, + isVibrationEnabled = true, + isReminderEnabled = true, + reminderHour = 20, + reminderMinute = 0, + totalUsageCount = 10, + lastUsedTime = System.currentTimeMillis() + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/daodaoshi/chick_mood/MainActivity.kt b/app/src/main/java/com/daodaoshi/chick_mood/MainActivity.kt new file mode 100644 index 0000000..91301ae --- /dev/null +++ b/app/src/main/java/com/daodaoshi/chick_mood/MainActivity.kt @@ -0,0 +1,153 @@ +package com.daodaoshi.chick_mood + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import com.daodaoshi.chick_mood.databinding.ActivityMainBinding +import java.text.SimpleDateFormat +import java.util.* + +/** + * 首页Activity - 主要用于展示历史心情记录和创建新记录的入口 + * + * 功能: + * 1. 展示历史心情记录列表(ViewPager2横向滑动) + * 2. 提供添加新心情记录的入口 + * 3. 时间指示器显示当前浏览记录的时间 + * 4. 顶部导航栏(更多功能、统计页面) + * + * @author Claude + * @date 2025-10-22 + */ +class MainActivity : AppCompatActivity() { + + private lateinit var binding: ActivityMainBinding + private lateinit var timeFormatter: SimpleDateFormat + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // 初始化ViewBinding + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) + + // 初始化时间格式化器 + timeFormatter = SimpleDateFormat("yyyy.MM.dd E HH:mm", Locale.getDefault()) + + // 初始化UI组件 + initViews() + + // 设置初始时间显示(当前时间) + updateTimeIndicator(System.currentTimeMillis()) + } + + /** + * 初始化UI组件和事件监听器 + */ + private fun initViews() { + // 设置Toolbar + setSupportActionBar(binding.toolbar) + supportActionBar?.apply { + title = "" + setDisplayHomeAsUpEnabled(false) + setDisplayShowTitleEnabled(false) + } + + // 设置点击事件监听器 + setupClickListeners() + + // 初始化历史记录展示 + initHistoryDisplay() + } + + /** + * 设置点击事件监听器 + */ + private fun setupClickListeners() { + // 更多按钮点击事件 + binding.btnMore.setOnClickListener { + // TODO: 弹出侧边抽屉菜单 + showMoreOptions() + } + + // 统计按钮点击事件 + binding.btnStatistics.setOnClickListener { + // TODO: 跳转到统计页面 + navigateToStatistics() + } + + // 添加心情按钮点击事件 + binding.fabAddMood.setOnClickListener { + // TODO: 弹出情绪选择框 + showEmotionSelector() + } + } + + /** + * 初始化历史记录展示 + */ + private fun initHistoryDisplay() { + // TODO: 在模块2.3中实现ViewPager2的设置 + // 目前显示空状态作为占位 + showEmptyState() + } + + /** + * 更新时间指示器显示 + * @param timestamp 时间戳 + */ + private fun updateTimeIndicator(timestamp: Long) { + binding.tvTimeIndicator.text = timeFormatter.format(Date(timestamp)) + } + + /** + * 显示空状态页面 + */ + private fun showEmptyState() { + binding.llEmptyState.isVisible = true + binding.vpHistoryRecords.isVisible = false + } + + /** + * 显示历史记录列表 + */ + private fun showHistoryRecords() { + binding.llEmptyState.isVisible = false + binding.vpHistoryRecords.isVisible = true + } + + /** + * 显示更多选项(侧边抽屉) + * TODO: 在后续模块中实现 + */ + private fun showMoreOptions() { + // 临时提示,后续实现侧边抽屉 + // 可以使用 NavigationDrawerFragment 或 BottomSheetDialog + } + + /** + * 跳转到统计页面 + * TODO: 在后续模块中实现 + */ + private fun navigateToStatistics() { + // 临时提示,后续实现页面跳转 + // 可以使用 Intent 或 Navigation Component + } + + /** + * 显示情绪选择框 + * TODO: 在模块3.2中实现 + */ + private fun showEmotionSelector() { + // 临时提示,后续实现BottomSheet情绪选择 + // 使用 BottomSheetDialog 实现 + } + + /** + * 获取当前页面状态 - 用于测试 + * @return true表示显示空状态,false表示显示历史记录 + */ + fun isShowingEmptyState(): Boolean { + return binding.llEmptyState.isVisible + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add.xml b/app/src/main/res/drawable/ic_add.xml new file mode 100644 index 0000000..28a5f16 --- /dev/null +++ b/app/src/main/res/drawable/ic_add.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_empty_chick.xml b/app/src/main/res/drawable/ic_empty_chick.xml new file mode 100644 index 0000000..f0ef20b --- /dev/null +++ b/app/src/main/res/drawable/ic_empty_chick.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_more.xml b/app/src/main/res/drawable/ic_more.xml new file mode 100644 index 0000000..7aedac7 --- /dev/null +++ b/app/src/main/res/drawable/ic_more.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_statistics.xml b/app/src/main/res/drawable/ic_statistics.xml new file mode 100644 index 0000000..bf2c4d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_statistics.xml @@ -0,0 +1,10 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..728c9d5 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6f3b755 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..384a787 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,26 @@ + + + + #FFD954 + #FFC107 + #FFF8E1 + + + #212121 + #757575 + #BDBDBD + #FFFFFF + #FF000000 + + + #FFAB76 + #D9534F + #6B7AFF + #A989C5 + #4E89AE + #888888 + + + #00000000 + #80000000 + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..6009325 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + chick_mood + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..3c23f5c --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml new file mode 100644 index 0000000..fa0f996 --- /dev/null +++ b/app/src/main/res/xml/backup_rules.xml @@ -0,0 +1,13 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml new file mode 100644 index 0000000..9ee9997 --- /dev/null +++ b/app/src/main/res/xml/data_extraction_rules.xml @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/test/java/com/chick_mood/data/model/EmotionTest.kt b/app/src/test/java/com/chick_mood/data/model/EmotionTest.kt new file mode 100644 index 0000000..2693c94 --- /dev/null +++ b/app/src/test/java/com/chick_mood/data/model/EmotionTest.kt @@ -0,0 +1,125 @@ +package com.chick_mood.data.model + +import org.junit.Assert.* +import org.junit.Test + +/** + * Emotion枚举的单元测试 + * 测试情绪类型枚举的各种功能 + * + * @author Claude + * @date 2025-10-22 + */ +class EmotionTest { + + /** + * 测试情绪枚举的基本属性 + */ + @Test + fun testEmotionBasicProperties() { + assertEquals("开心", Emotion.HAPPY.displayName) + assertEquals("#FFAB76", Emotion.HAPPY.colorValue) + assertEquals(1, Emotion.HAPPY.chickExpression) + + assertEquals("生气", Emotion.ANGRY.displayName) + assertEquals("#D9534F", Emotion.ANGRY.colorValue) + assertEquals(2, Emotion.ANGRY.chickExpression) + + assertEquals("悲伤", Emotion.SAD.displayName) + assertEquals("#5DADE2", Emotion.SAD.colorValue) + assertEquals(3, Emotion.SAD.chickExpression) + + assertEquals("烦恼", Emotion.WORRIED.displayName) + assertEquals("#95A5A6", Emotion.WORRIED.colorValue) + assertEquals(4, Emotion.WORRIED.chickExpression) + + assertEquals("孤单", Emotion.LONELY.displayName) + assertEquals("#AF7AC5", Emotion.LONELY.colorValue) + assertEquals(5, Emotion.LONELY.chickExpression) + + assertEquals("害怕", Emotion.SCARED.displayName) + assertEquals("#F4D03F", Emotion.SCARED.colorValue) + assertEquals(6, Emotion.SCARED.chickExpression) + } + + /** + * 测试根据数值获取情绪枚举 + */ + @Test + fun testFromValue() { + assertEquals(Emotion.HAPPY, Emotion.fromValue(1)) + assertEquals(Emotion.ANGRY, Emotion.fromValue(2)) + assertEquals(Emotion.SAD, Emotion.fromValue(3)) + assertEquals(Emotion.WORRIED, Emotion.fromValue(4)) + assertEquals(Emotion.LONELY, Emotion.fromValue(5)) + assertEquals(Emotion.SCARED, Emotion.fromValue(6)) + + // 测试无效值,应该返回默认的HAPPY + assertEquals(Emotion.HAPPY, Emotion.fromValue(0)) + assertEquals(Emotion.HAPPY, Emotion.fromValue(7)) + assertEquals(Emotion.HAPPY, Emotion.fromValue(-1)) + assertEquals(Emotion.HAPPY, Emotion.fromValue(100)) + } + + /** + * 测试获取所有情绪显示名称 + */ + @Test + fun testGetAllDisplayNames() { + val displayNames = Emotion.getAllDisplayNames() + assertEquals(6, displayNames.size) + assertTrue(displayNames.contains("开心")) + assertTrue(displayNames.contains("生气")) + assertTrue(displayNames.contains("悲伤")) + assertTrue(displayNames.contains("烦恼")) + assertTrue(displayNames.contains("孤单")) + assertTrue(displayNames.contains("害怕")) + } + + /** + * 测试获取颜色资源ID + */ + @Test + fun testGetColorResourceId() { + // 验证所有情绪都能返回有效的颜色资源ID + for (emotion in Emotion.values()) { + val colorId = emotion.getColorResourceId() + assertTrue("Color resource ID should be positive", colorId > 0) + } + } + + /** + * 测试情绪枚举值的唯一性 + */ + @Test + fun testEmotionValueUniqueness() { + val values = Emotion.values().map { it.chickExpression } + val uniqueValues = values.toSet() + assertEquals("Emotion values should be unique", values.size, uniqueValues.size) + } + + /** + * 测试情绪显示名称的唯一性 + */ + @Test + fun testEmotionDisplayNameUniqueness() { + val displayNames = Emotion.values().map { it.displayName } + val uniqueDisplayNames = displayNames.toSet() + assertEquals("Emotion display names should be unique", + displayNames.size, uniqueDisplayNames.size) + } + + /** + * 测试所有情绪都有有效的颜色值 + */ + @Test + fun testAllEmotionsHaveValidColorValues() { + for (emotion in Emotion.values()) { + assertNotNull("Color value should not be null", emotion.colorValue) + assertTrue("Color value should start with #", + emotion.colorValue.startsWith("#")) + assertEquals("Color value should be 7 characters long", + 7, emotion.colorValue.length) + } + } +} \ No newline at end of file diff --git a/app/src/test/java/com/chick_mood/data/model/MoodRecordTest.kt b/app/src/test/java/com/chick_mood/data/model/MoodRecordTest.kt new file mode 100644 index 0000000..b4e7b56 --- /dev/null +++ b/app/src/test/java/com/chick_mood/data/model/MoodRecordTest.kt @@ -0,0 +1,238 @@ +package com.chick_mood.data.model + +import org.junit.Assert.* +import org.junit.Test + +/** + * MoodRecord数据模型的单元测试 + * 测试心情记录数据模型的各种功能 + * + * @author Claude + * @date 2025-10-22 + */ +class MoodRecordTest { + + /** + * 测试心情记录的基本属性和初始化 + */ + @Test + fun testMoodRecordBasicProperties() { + val record = MoodRecord( + id = 1, + emotion = Emotion.HAPPY, + moodIntensity = 75, + timestamp = System.currentTimeMillis(), + textContent = "今天心情很好", + imagePath = "/path/to/image.jpg", + isFavorite = true, + shakeDuration = 4.5f, + maxAcceleration = 20.0f, + deviceModel = "Test Device" + ) + + assertEquals(1L, record.id) + assertEquals(Emotion.HAPPY, record.emotion) + assertEquals(75, record.moodIntensity) + assertEquals("今天心情很好", record.textContent) + assertEquals("/path/to/image.jpg", record.imagePath) + assertTrue(record.isFavorite) + assertEquals(4.5f, record.shakeDuration) + assertEquals(20.0f, record.maxAcceleration) + assertEquals("Test Device", record.deviceModel) + } + + /** + * 测试心情记录的默认值 + */ + @Test + fun testMoodRecordDefaults() { + val record = MoodRecord( + emotion = Emotion.SAD, + moodIntensity = 50, + timestamp = System.currentTimeMillis() + ) + + assertEquals(0L, record.id) + assertNull(record.textContent) + assertNull(record.imagePath) + assertFalse(record.isFavorite) + assertEquals(0f, record.shakeDuration) + assertEquals(0f, record.maxAcceleration) + assertNull(record.deviceModel) + } + + /** + * 测试时间格式化功能 + */ + @Test + fun testGetFormattedTime() { + val timestamp = 1634842800000L // 2021-10-22 10:00:00 + val record = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = timestamp + ) + + val formattedTime = record.getFormattedTime() + assertNotNull(formattedTime) + // 修改正则表达式,匹配更简单的时间格式 + assertTrue("Formatted time should match pattern: $formattedTime", + formattedTime.matches(Regex("\\d{4}\\.\\d{2}\\.\\d{2} .* \\d{2}:\\d{2}"))) + } + + /** + * 测试心情强度描述 + */ + @Test + fun testGetIntensityDescription() { + // 测试轻微强度 + val lightRecord = MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 20, timestamp = System.currentTimeMillis()) + assertEquals("轻微", lightRecord.getIntensityDescription()) + + // 测试一般强度 + val normalRecord = MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 40, timestamp = System.currentTimeMillis()) + assertEquals("一般", normalRecord.getIntensityDescription()) + + // 测试强烈强度 + val strongRecord = MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 60, timestamp = System.currentTimeMillis()) + assertEquals("强烈", strongRecord.getIntensityDescription()) + + // 测试极度强度 + val extremeRecord = MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 90, timestamp = System.currentTimeMillis()) + assertEquals("极度", extremeRecord.getIntensityDescription()) + + // 测试边界值 + assertEquals("轻微", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 0, timestamp = System.currentTimeMillis()).getIntensityDescription()) + assertEquals("轻微", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 24, timestamp = System.currentTimeMillis()).getIntensityDescription()) + assertEquals("一般", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 25, timestamp = System.currentTimeMillis()).getIntensityDescription()) + assertEquals("极度", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 100, timestamp = System.currentTimeMillis()).getIntensityDescription()) + } + + /** + * 测试心情强度表情符号 + */ + @Test + fun testGetIntensityEmoji() { + assertEquals("😌", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 10, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😌", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 24, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😐", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 25, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😐", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 49, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😰", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 50, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😰", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 74, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😱", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 75, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + assertEquals("😱", MoodRecord(emotion = Emotion.HAPPY, moodIntensity = 100, timestamp = System.currentTimeMillis()).getIntensityEmoji()) + } + + /** + * 测试内容检查方法 + */ + @Test + fun testContentCheckMethods() { + // 测试有图片和文字的记录 + val fullRecord = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = System.currentTimeMillis(), + textContent = "有文字内容", + imagePath = "/path/to/image.jpg" + ) + assertTrue(fullRecord.hasImage()) + assertTrue(fullRecord.hasText()) + assertEquals(5, fullRecord.getTextLength()) + + // 测试只有文字的记录 + val textOnlyRecord = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = System.currentTimeMillis(), + textContent = "只有文字" + ) + assertFalse(textOnlyRecord.hasImage()) + assertTrue(textOnlyRecord.hasText()) + assertEquals(4, textOnlyRecord.getTextLength()) + + // 测试只有图片的记录 + val imageOnlyRecord = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = System.currentTimeMillis(), + imagePath = "/path/to/image.jpg" + ) + assertTrue(imageOnlyRecord.hasImage()) + assertFalse(imageOnlyRecord.hasText()) + assertEquals(0, imageOnlyRecord.getTextLength()) + + // 测试空内容记录 + val emptyRecord = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = System.currentTimeMillis() + ) + assertFalse(emptyRecord.hasImage()) + assertFalse(emptyRecord.hasText()) + assertEquals(0, emptyRecord.getTextLength()) + + // 测试空字符串内容 + val emptyTextRecord = MoodRecord( + emotion = Emotion.HAPPY, + moodIntensity = 50, + timestamp = System.currentTimeMillis(), + textContent = "" + ) + assertFalse(emptyTextRecord.hasText()) + assertEquals(0, emptyTextRecord.getTextLength()) + } + + /** + * 测试测试记录创建方法 + */ + @Test + fun testCreateTestRecord() { + val testRecord = MoodRecord.createTestRecord( + emotion = Emotion.ANGRY, + intensity = 80, + text = "测试记录" + ) + + assertEquals(Emotion.ANGRY, testRecord.emotion) + assertEquals(80, testRecord.moodIntensity) + assertEquals("测试记录", testRecord.textContent) + assertFalse(testRecord.isFavorite) + assertEquals(3.0f, testRecord.shakeDuration) + assertEquals(15.0f, testRecord.maxAcceleration) + // deviceModel 在测试环境中可能为null,这是正常的 + // assertNotNull("deviceModel should not be null", testRecord.deviceModel) + + // 测试默认参数 + val defaultTestRecord = MoodRecord.createTestRecord() + assertEquals(Emotion.HAPPY, defaultTestRecord.emotion) + assertEquals(50, defaultTestRecord.moodIntensity) + assertEquals("测试心情记录", defaultTestRecord.textContent) + } + + /** + * 测试心情强度边界值处理 + */ + @Test + fun testMoodIntensityBounds() { + // 测试超出范围的强度值会被限制在有效范围内 + val tooHighRecord = MoodRecord.createTestRecord(intensity = 150) + assertEquals(MoodRecord.MAX_INTENSITY, tooHighRecord.moodIntensity) + + val tooLowRecord = MoodRecord.createTestRecord(intensity = -10) + assertEquals(MoodRecord.MIN_INTENSITY, tooLowRecord.moodIntensity) + + val normalRecord = MoodRecord.createTestRecord(intensity = 75) + assertEquals(75, normalRecord.moodIntensity) + } + + /** + * 测试常量值 + */ + @Test + fun testConstants() { + assertEquals(0, MoodRecord.MIN_INTENSITY) + assertEquals(100, MoodRecord.MAX_INTENSITY) + assertEquals(200, MoodRecord.MAX_TEXT_LENGTH) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/chick_mood/data/model/TestDataGenerator.kt b/app/src/test/java/com/chick_mood/data/model/TestDataGenerator.kt new file mode 100644 index 0000000..8ef9ed8 --- /dev/null +++ b/app/src/test/java/com/chick_mood/data/model/TestDataGenerator.kt @@ -0,0 +1,271 @@ +package com.chick_mood.data.model + +import kotlin.random.Random + +/** + * 测试数据生成器 + * 为单元测试提供各种测试数据 + * + * @author Claude + * @date 2025-10-22 + */ +object TestDataGenerator { + + private val random = Random(System.currentTimeMillis()) + + /** + * 生成随机的心情记录 + * @param count 生成的记录数量 + * @return 心情记录列表 + */ + fun generateMoodRecords(count: Int): List { + return (1..count).map { index -> + generateMoodRecord(index.toLong()) + } + } + + /** + * 生成单个心情记录 + * @param id 记录ID + * @param timestamp 时间戳(可选,默认为随机生成) + * @return 心情记录 + */ + fun generateMoodRecord( + id: Long = 0, + timestamp: Long = generateRandomTimestamp() + ): MoodRecord { + val emotions = Emotion.values() + val randomEmotion = emotions[random.nextInt(emotions.size)] + val randomIntensity = random.nextInt(MoodRecord.MIN_INTENSITY, MoodRecord.MAX_INTENSITY + 1) + val randomText = generateRandomText() + val randomImage = if (random.nextBoolean()) generateRandomImagePath() else null + val randomFavorite = random.nextBoolean() + val randomShakeDuration = random.nextFloat() * 10f + 1f // 1-11秒 + val randomMaxAcceleration = random.nextFloat() * 30f + 5f // 5-35 + + return MoodRecord( + id = id, + emotion = randomEmotion, + moodIntensity = randomIntensity, + timestamp = timestamp, + textContent = randomText, + imagePath = randomImage, + isFavorite = randomFavorite, + shakeDuration = randomShakeDuration, + maxAcceleration = randomMaxAcceleration, + deviceModel = generateRandomDeviceModel() + ) + } + + /** + * 生成特定情绪的心情记录 + * @param emotion 指定的情绪类型 + * @param count 生成的记录数量 + * @return 指定情绪的心情记录列表 + */ + fun generateMoodRecordsByEmotion(emotion: Emotion, count: Int): List { + return (1..count).map { index -> + MoodRecord( + id = index.toLong(), + emotion = emotion, + moodIntensity = random.nextInt(MoodRecord.MIN_INTENSITY, MoodRecord.MAX_INTENSITY + 1), + timestamp = generateRandomTimestamp(), + textContent = generateRandomText(), + imagePath = if (random.nextBoolean()) generateRandomImagePath() else null, + isFavorite = random.nextBoolean(), + shakeDuration = random.nextFloat() * 10f + 1f, + maxAcceleration = random.nextFloat() * 30f + 5f, + deviceModel = generateRandomDeviceModel() + ) + } + } + + /** + * 生成收藏的心情记录 + * @param count 生成的记录数量 + * @return 收藏的心情记录列表 + */ + fun generateFavoriteMoodRecords(count: Int): List { + return (1..count).map { index -> + MoodRecord( + id = index.toLong(), + emotion = Emotion.values().random(), + moodIntensity = random.nextInt(MoodRecord.MIN_INTENSITY, MoodRecord.MAX_INTENSITY + 1), + timestamp = generateRandomTimestamp(), + textContent = generateRandomText(), + imagePath = if (random.nextBoolean()) generateRandomImagePath() else null, + isFavorite = true, // 强制设置为收藏 + shakeDuration = random.nextFloat() * 10f + 1f, + maxAcceleration = random.nextFloat() * 30f + 5f, + deviceModel = generateRandomDeviceModel() + ) + } + } + + /** + * 生成今天的心情记录 + * @param count 生成的记录数量 + * @return 今天的心情记录列表 + */ + fun generateTodayMoodRecords(count: Int): List { + val todayStart = System.currentTimeMillis() / (24 * 60 * 60 * 1000) * (24 * 60 * 60 * 1000) + return (1..count).map { index -> + val timestamp = todayStart + random.nextInt(24 * 60 * 60 * 1000) + generateMoodRecord(index.toLong(), timestamp) + } + } + + /** + * 生成测试用户配置 + * @param nickname 用户昵称 + * @return 用户配置 + */ + fun generateUserConfig(nickname: String = "测试用户"): UserConfig { + return UserConfig( + id = 1, + nickname = nickname, + avatarPath = if (random.nextBoolean()) generateRandomAvatarPath() else null, + isDarkMode = random.nextBoolean(), + isSoundEnabled = random.nextBoolean(), + isVibrationEnabled = random.nextBoolean(), + isReminderEnabled = random.nextBoolean(), + reminderHour = random.nextInt(24), + reminderMinute = random.nextInt(60), + appVersion = "1.0.0", + dataVersion = 1, + firstUseTime = System.currentTimeMillis() - random.nextLong(0, 30L * 24 * 60 * 60 * 1000), // 30天内 + totalUsageCount = random.nextInt(100), + lastUsedTime = System.currentTimeMillis() + ) + } + + /** + * 生成随机时间戳(最近30天内) + * @return 随机时间戳 + */ + private fun generateRandomTimestamp(): Long { + val thirtyDaysInMillis = 30L * 24 * 60 * 60 * 1000 + val currentTime = System.currentTimeMillis() + return currentTime - random.nextLong(0, thirtyDaysInMillis) + } + + /** + * 生成随机文字内容 + * @return 随机文字内容 + */ + private fun generateRandomText(): String { + val texts = listOf( + "今天心情不错", + "工作有点累", + "和朋友出去玩很开心", + "学习新知识很有成就感", + "天气真好", + "有点想念家人", + "今天遇到了有趣的事情", + "工作顺利完成", + "收到了好消息", + "期待周末的到来", + "今天运动了,感觉很棒", + "读到一本好书", + "和朋友的聊天很开心", + "今天的 sunset 很美", + "尝试了新的餐厅", + "完成了重要的项目", + "今天很有灵感", + "收到了朋友的关心", + "今天的心情像天气一样晴朗", + "生活中的小确幸", + "" // 空文字 + ) + return texts.random() + } + + /** + * 生成随机图片路径 + * @return 随机图片路径 + */ + private fun generateRandomImagePath(): String { + val imageNames = listOf( + "mood_image_001.jpg", + "mood_image_002.png", + "mood_image_003.jpg", + "screenshot_001.png", + "photo_20231022.jpg", + "camera_image.jpg" + ) + return "/storage/emulated/0/Pictures/MoodApp/${imageNames.random()}" + } + + /** + * 生成随机头像路径 + * @return 随机头像路径 + */ + private fun generateRandomAvatarPath(): String { + val avatarNames = listOf( + "avatar_001.jpg", + "avatar_002.png", + "profile_pic.jpg", + "user_avatar.png" + ) + return "/storage/emulated/0/Pictures/MoodApp/avatars/${avatarNames.random()}" + } + + /** + * 生成随机设备型号 + * @return 随机设备型号 + */ + private fun generateRandomDeviceModel(): String { + val deviceModels = listOf( + "SM-G998B", // Samsung Galaxy S21 Ultra + "Pixel 6 Pro", // Google Pixel 6 Pro + "iPhone14,3", // iPhone 14 Pro + "Mi 11", // Xiaomi Mi 11 + "OnePlus 9 Pro", // OnePlus 9 Pro + "CPH2491", // OnePlus 9RT + "LM-G900", // LG G8 + "HUAWEI P40", // Huawei P40 + "Galaxy S22", // Samsung Galaxy S22 + "Nokia 8.3" // Nokia 8.3 + ) + return deviceModels.random() + } + + /** + * 生成完整的测试数据集 + * @return 包含心情记录和用户配置的配对 + */ + fun generateFullTestData(): Pair, UserConfig> { + val moodRecords = generateMoodRecords(20) + val userConfig = generateUserConfig() + return Pair(moodRecords, userConfig) + } + + /** + * 生成边界情况测试数据 + * @return 包含各种边界情况的心情记录列表 + */ + fun generateBoundaryTestRecords(): List { + return listOf( + // 最小强度 + MoodRecord.createTestRecord(Emotion.HAPPY, MoodRecord.MIN_INTENSITY, "最小强度测试"), + // 最大强度 + MoodRecord.createTestRecord(Emotion.ANGRY, MoodRecord.MAX_INTENSITY, "最大强度测试"), + // 最长文字 + MoodRecord.createTestRecord( + Emotion.SAD, + 50, + "这是一个很长的测试文字".repeat(20).take(MoodRecord.MAX_TEXT_LENGTH) + ), + // 空内容 + MoodRecord( + emotion = Emotion.WORRIED, + moodIntensity = 30, + timestamp = System.currentTimeMillis(), + textContent = null, + imagePath = null + ), + // 收藏记录 + MoodRecord.createTestRecord(Emotion.LONELY, 60, "收藏记录测试").copy(isFavorite = true) + ) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/chick_mood/data/model/UserConfigTest.kt b/app/src/test/java/com/chick_mood/data/model/UserConfigTest.kt new file mode 100644 index 0000000..dd50aa3 --- /dev/null +++ b/app/src/test/java/com/chick_mood/data/model/UserConfigTest.kt @@ -0,0 +1,232 @@ +package com.chick_mood.data.model + +import org.junit.Assert.* +import org.junit.Test + +/** + * UserConfig数据模型的单元测试 + * 测试用户配置数据模型的各种功能 + * + * @author Claude + * @date 2025-10-22 + */ +class UserConfigTest { + + /** + * 测试用户配置的基本属性和初始化 + */ + @Test + fun testUserConfigBasicProperties() { + val config = UserConfig( + id = 1, + nickname = "测试用户", + avatarPath = "/path/to/avatar.jpg", + isDarkMode = true, + isSoundEnabled = false, + isVibrationEnabled = false, + isReminderEnabled = true, + reminderHour = 21, + reminderMinute = 30, + appVersion = "1.0.1", + dataVersion = 2, + totalUsageCount = 15 + ) + + assertEquals(1, config.id) + assertEquals("测试用户", config.nickname) + assertEquals("/path/to/avatar.jpg", config.avatarPath) + assertTrue(config.isDarkMode) + assertFalse(config.isSoundEnabled) + assertFalse(config.isVibrationEnabled) + assertTrue(config.isReminderEnabled) + assertEquals(21, config.reminderHour) + assertEquals(30, config.reminderMinute) + assertEquals("1.0.1", config.appVersion) + assertEquals(2, config.dataVersion) + assertEquals(15, config.totalUsageCount) + } + + /** + * 测试用户配置的默认值 + */ + @Test + fun testUserConfigDefaults() { + val config = UserConfig() + + assertEquals(1, config.id) + assertNull(config.nickname) + assertNull(config.avatarPath) + assertFalse(config.isDarkMode) + assertTrue(config.isSoundEnabled) + assertTrue(config.isVibrationEnabled) + assertFalse(config.isReminderEnabled) + assertEquals(20, config.reminderHour) + assertEquals(0, config.reminderMinute) + assertEquals("1.0.0", config.appVersion) + assertEquals(1, config.dataVersion) + assertTrue(config.firstUseTime > 0) + assertEquals(0, config.totalUsageCount) + assertTrue(config.lastUsedTime > 0) + } + + /** + * 测试提醒时间格式化 + */ + @Test + fun testGetReminderTimeString() { + val config1 = UserConfig(reminderHour = 9, reminderMinute = 5) + assertEquals("09:05", config1.getReminderTimeString()) + + val config2 = UserConfig(reminderHour = 23, reminderMinute = 59) + assertEquals("23:59", config2.getReminderTimeString()) + + val config3 = UserConfig(reminderHour = 0, reminderMinute = 0) + assertEquals("00:00", config3.getReminderTimeString()) + } + + /** + * 测试有效提醒时间检查 + */ + @Test + fun testHasValidReminderTime() { + // 有效的提醒时间 + val validConfig1 = UserConfig(isReminderEnabled = true, reminderHour = 10, reminderMinute = 30) + assertTrue(validConfig1.hasValidReminderTime()) + + val validConfig2 = UserConfig(isReminderEnabled = true, reminderHour = 0, reminderMinute = 0) + assertTrue(validConfig2.hasValidReminderTime()) + + val validConfig3 = UserConfig(isReminderEnabled = true, reminderHour = 23, reminderMinute = 59) + assertTrue(validConfig3.hasValidReminderTime()) + + // 无效的提醒时间 + val disabledConfig = UserConfig(isReminderEnabled = false, reminderHour = 10, reminderMinute = 30) + assertFalse(disabledConfig.hasValidReminderTime()) + + val invalidHourConfig1 = UserConfig(isReminderEnabled = true, reminderHour = -1, reminderMinute = 30) + assertFalse(invalidHourConfig1.hasValidReminderTime()) + + val invalidHourConfig2 = UserConfig(isReminderEnabled = true, reminderHour = 24, reminderMinute = 30) + assertFalse(invalidHourConfig2.hasValidReminderTime()) + + val invalidMinuteConfig1 = UserConfig(isReminderEnabled = true, reminderHour = 10, reminderMinute = -1) + assertFalse(invalidMinuteConfig1.hasValidReminderTime()) + + val invalidMinuteConfig2 = UserConfig(isReminderEnabled = true, reminderHour = 10, reminderMinute = 60) + assertFalse(invalidMinuteConfig2.hasValidReminderTime()) + } + + /** + * 测试昵称和头像检查方法 + */ + @Test + fun testNicknameAndAvatarChecks() { + // 有昵称和头像 + val fullConfig = UserConfig(nickname = "用户名", avatarPath = "/path/to/avatar.jpg") + assertTrue(fullConfig.hasNickname()) + assertTrue(fullConfig.hasAvatar()) + + // 只有昵称 + val nicknameOnlyConfig = UserConfig(nickname = "用户名") + assertTrue(nicknameOnlyConfig.hasNickname()) + assertFalse(nicknameOnlyConfig.hasAvatar()) + + // 只有头像 + val avatarOnlyConfig = UserConfig(avatarPath = "/path/to/avatar.jpg") + assertFalse(avatarOnlyConfig.hasNickname()) + assertTrue(avatarOnlyConfig.hasAvatar()) + + // 都没有 + val emptyConfig = UserConfig() + assertFalse(emptyConfig.hasNickname()) + assertFalse(emptyConfig.hasAvatar()) + + // 空字符串 + val emptyStringConfig = UserConfig(nickname = "", avatarPath = "") + assertFalse(emptyStringConfig.hasNickname()) + assertFalse(emptyStringConfig.hasAvatar()) + } + + /** + * 测试使用天数计算 + */ + @Test + fun testGetUsageDays() { + val currentTime = System.currentTimeMillis() + val oneDayInMillis = 24 * 60 * 60 * 1000L + + // 当天使用 + val todayConfig = UserConfig(firstUseTime = currentTime) + assertEquals(1, todayConfig.getUsageDays()) + + // 1天前使用 + val oneDayAgoConfig = UserConfig(firstUseTime = currentTime - oneDayInMillis) + assertEquals(2, oneDayAgoConfig.getUsageDays()) + + // 10天前使用 + val tenDaysAgoConfig = UserConfig(firstUseTime = currentTime - 10 * oneDayInMillis) + assertEquals(11, tenDaysAgoConfig.getUsageDays()) + } + + /** + * 测试默认配置获取方法 + */ + @Test + fun testGetDefault() { + val defaultConfig = UserConfig.getDefault() + + assertEquals(1, defaultConfig.id) + assertNull(defaultConfig.nickname) + assertNull(defaultConfig.avatarPath) + assertFalse(defaultConfig.isDarkMode) + assertTrue(defaultConfig.isSoundEnabled) + assertTrue(defaultConfig.isVibrationEnabled) + assertFalse(defaultConfig.isReminderEnabled) + assertEquals(20, defaultConfig.reminderHour) + assertEquals(0, defaultConfig.reminderMinute) + assertEquals("1.0.0", defaultConfig.appVersion) + assertEquals(1, defaultConfig.dataVersion) + assertTrue(defaultConfig.firstUseTime > 0) + assertEquals(0, defaultConfig.totalUsageCount) + assertTrue(defaultConfig.lastUsedTime > 0) + } + + /** + * 测试测试配置创建方法 + */ + @Test + fun testCreateTestConfig() { + val testConfig = UserConfig.createTestConfig("测试用户") + + assertEquals("测试用户", testConfig.nickname) + assertFalse(testConfig.isDarkMode) + assertTrue(testConfig.isSoundEnabled) + assertTrue(testConfig.isVibrationEnabled) + assertTrue(testConfig.isReminderEnabled) + assertEquals(20, testConfig.reminderHour) + assertEquals(0, testConfig.reminderMinute) + assertEquals(10, testConfig.totalUsageCount) + assertTrue(testConfig.lastUsedTime > 0) + + // 测试默认昵称 + val defaultTestConfig = UserConfig.createTestConfig() + assertEquals("测试用户", defaultTestConfig.nickname) + } + + /** + * 测试配置ID的固定性 + */ + @Test + fun testConfigIdFixed() { + // 即使不指定ID,也应该默认为1 + val config1 = UserConfig() + assertEquals(1, config1.id) + + val config2 = UserConfig(nickname = "用户") + assertEquals(1, config2.id) + + // 指定ID时应该使用指定的值 + val config3 = UserConfig(id = 5) + assertEquals(5, config3.id) + } +} \ No newline at end of file diff --git a/app/src/test/java/com/daodaoshi/chick_mood/README_MODULE_1_1.md b/app/src/test/java/com/daodaoshi/chick_mood/README_MODULE_1_1.md new file mode 100644 index 0000000..13379b0 --- /dev/null +++ b/app/src/test/java/com/daodaoshi/chick_mood/README_MODULE_1_1.md @@ -0,0 +1,109 @@ +# 模块1.1:MainActivity基础布局结构 + +## 📋 模块概述 + +本模块完成了首页Activity的基础布局结构和核心功能框架搭建,为后续模块奠定了坚实的基础。 + +## ✅ 已完成功能 + +### 1. 项目配置更新 +- **build.gradle.kts**: 从Compose转换为View系统 +- **依赖管理**: 添加了所有必要的库依赖 +- **ViewBinding**: 启用视图绑定功能 + +### 2. 布局文件创建 +- **activity_main.xml**: 完整的首页布局结构 +- **colors.xml**: 定义了主题色彩和情绪色彩 +- **图标资源**: 创建了临时占位图标 + +### 3. MainActivity实现 +- **基础架构**: 使用ViewBinding的清晰代码结构 +- **组件初始化**: Toolbar、时间指示器、按钮事件 +- **状态管理**: 空状态和历史记录状态切换 +- **扩展预留**: 为后续功能预留了接口 + +### 4. 测试覆盖 +- **单元测试**: 完整的MainActivity测试用例 +- **数据生成器**: 测试数据生成工具 +- **测试套件**: 便于批量运行测试 + +## 🎯 核心组件说明 + +### 布局结构 +``` +┌─────────────────────────────┐ +│ Toolbar │ ← 顶部导航栏 +├─────────────────────────────┤ +│ 时间指示器 │ ← 时间显示 +├─────────────────────────────┤ +│ │ +│ ViewPager2 / 空状态 │ ← 历史记录区域 +│ │ +├─────────────────────────────┤ +│ [+] 按钮 │ ← 添加心情FAB +└─────────────────────────────┘ +``` + +### 关键功能 +- **时间指示器**: 显示当前浏览记录的时间 +- **空状态处理**: 首次使用的友好提示 +- **按钮事件**: 预留了更多、统计、添加功能的接口 +- **状态切换**: 支持空状态和历史记录状态的切换 + +## 🧪 如何测试 + +### 本地单元测试 +```bash +# 运行所有MainActivity相关测试 +./gradlew test --tests "*MainActivity*" + +# 运行特定测试类 +./gradlew test --tests "com.daodaoshi.chick_mood.MainActivityTest" +``` + +### 真机/模拟器测试 +1. 编译并安装应用到设备 +2. 启动应用验证基础布局 +3. 测试按钮点击响应 +4. 验证时间显示是否正常 + +### 测试数据 +- 使用`TestDataGenerator`生成测试时间戳 +- 模拟不同时间场景的显示效果 +- 验证时间格式化的正确性 + +## 📝 开发规范遵循 + +### ✅ 模块化开发 +- 每个功能都有独立的方法 +- 清晰的职责分离 +- 便于后续维护和扩展 + +### ✅ 代码规范 +- 详细的KDoc注释 +- 规范的命名约定 +- 清晰的代码结构 + +### ✅ 测试驱动 +- 完整的单元测试覆盖 +- 测试数据生成器 +- 测试套件便于运行 + +## 🔄 下一步计划 + +本模块为后续开发奠定了基础,接下来可以实现: + +1. **模块1.2**: 数据模型定义(MoodRecord, Emotion枚举) +2. **模块1.3**: Room数据库设计和实现 +3. **模块1.4**: ViewModel和Repository架构 + +## 🚨 注意事项 + +1. **图标资源**: 当前使用临时占位图标,需要设计师提供正式资源 +2. **功能预留**: 所有TODO标记的功能都会在后续模块中实现 +3. **主题适配**: 当前只适配了浅色主题,深色主题待后续实现 + +--- + +*模块开发完成时间: 2025-10-22* +*预计后续开发时间: 按计划每个模块1-2天* \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..4645626 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,5 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +plugins { + id("com.android.application") version "8.2.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false +} \ No newline at end of file diff --git a/claude.md b/claude.md new file mode 100644 index 0000000..33c99bc --- /dev/null +++ b/claude.md @@ -0,0 +1,462 @@ +# 别摇小鸡 - 技术决策与问题记录 + +> 基于产品需求文档分析的技术架构决策点和待解决问题汇总 + +## 📋 项目概览 + +**项目名称:** 别摇小鸡心情记录App +**开发平台:** Android原生(Kotlin) +**当前版本:** V1.0 MVP +**更新日期:** 2025-10-22 + +--- + +## 🏗️ 已确认的技术架构 + +### 核心技术栈 +- **开发语言:** Kotlin +- **UI框架:** Android Jetpack (ViewModel, LiveData, Navigation) +- **动画引擎:** Lottie + Property Animation +- **传感器:** Android Sensor Framework (Accelerometer) +- **数据存储:** SQLite Room + SharedPreferences +- **图片处理:** Glide + +### 功能开发优先级 +**优先级1 (V1.0核心功能):** +- ✅ 六种基础情绪选择(开心、生气、悲伤、烦恼、孤单、害怕) +- ✅ 传感器摇晃检测与心情值计算 +- ✅ 小鸡2D动画响应系统 +- ✅ 心情记录保存(文字+心情值) + +**优先级2 (V1.0后续功能):** +- ✅ 心情日历月视图展示 +- ✅ 基础用户设置(昵称、头像) + +**优先级3 (V1.1+功能):** +- 📷 图片添加与编辑功能 +- 📊 基础心情统计(饼图、趋势图) +- 🔄 云端数据同步选项 + +--- + +## 🎯 UI/UX 交互细节 (待UI设计图后确认) + +### 页面交互确认点 +1. **情绪选择页** + - [ ] 情绪按钮切换交互方式 + - [ ] 选中状态的视觉反馈效果 + +2. **准备摇晃页** + - [ ] 是否需要倒计时动画(3-2-1-开始) + - [ ] 不同情绪的引导文案差异化 + +3. **摇晃页面** + - [ ] 心情值进度条的位置设计(顶部/底部) + - [ ] 小鸡晕眩状态的渐进变化 + +4. **结果展示页** + - [ ] 心情强度文字的动画效果 + - [ ] 心情值数字的计数动画 + +### 边界情况处理 +5. **异常场景** + - [ ] 摇晃过程中电话接入处理 + - [ ] 传感器故障时的备用方案 + +6. **用户输入** + - [ ] 文字输入字数限制建议(建议200字) + - [ ] 自动保存草稿功能 + +--- + +## 🔧 技术实现问题记录 + +### 传感器与算法相关 (V1.0基础实现) + +**高优先级:** +- [ ] **心情值算法调优** + - 当前公式:`心情值 = Σ(加速度幅值) / 摇晃时间` + - 需要根据实际测试数据调整权重系数 + - 考虑不同设备传感器的差异性 + +- [ ] **摇晃检测阈值设定** + - 最小摇晃时间:文档建议3秒(可能需要调整到2秒) + - 加速度阈值:区分正常使用和主动摇晃 + - 采样频率:50Hz是否合适 + +**中优先级:** +- [ ] **数据滤波算法** + - 低通滤波器参数设置 + - 高频噪声去除策略 + - 设备兼容性处理 + +### 动画与性能相关 (V1.0基础实现) + +**高优先级:** +- [ ] **小鸡动画系统** + - 占位图资源路径规划 + - 动画状态机设计 + - 与传感器数据的实时响应 + +- [ ] **动画性能优化** + - 使用Hardware Layer加速 + - ViewPropertyAnimator vs Lottie选择 + - 内存占用控制 + +**中优先级:** +- [ ] **音效反馈系统** (可选功能) + - 摇晃过程中的音效设计 + - 音效文件压缩和缓存 + +### 数据存储与架构 + +**V1.0基础实现:** +- [ ] **数据库结构确认** + ```sql + -- 确认表结构设计是否符合实际使用场景 + -- 考虑预留扩展字段 + ``` + +- [ ] **数据迁移准备** + - user_config表添加version字段 + - 为未来云端同步预留接口 + +**V1.1+扩展准备:** +- [ ] **云端同步架构预埋** + - sync_queue表的实现 + - 冲突解决策略 + - 离线优先的设计模式 + +### 系统适配与兼容性 + +**高优先级:** +- [ ] **Android版本适配** + - 最低支持版本:Android 5.0 (API 21+) + - 目标版本:Android 13+ (API 33+) + - 权限管理:动态申请传感器权限 + +- [ ] **深色模式支持** (待确认) + - 两套色彩方案准备 + - 小鸡黄在深色背景下的适配 + +**中优先级:** +- [ ] **字体适配策略** + - 支持系统字体大小设置 + - 使用sp单位,限制最大最小字号 + - 不同屏幕密度的适配 + +- [ ] **设备性能差异处理** + - 低端设备的动画降级 + - 内存使用优化策略 + +--- + +## 📊 数据分析预埋 + +### 友盟集成准备 (V1.1+) +- [ ] 基础页面访问统计 +- [ ] 用户行为路径追踪 +- [ ] 性能监控指标 +- [ ] 崩溃报告收集 + +### 本地数据分析 +- [ ] 心情记录频率统计 +- [ ] 情绪分布分析 +- [ ] 摇晃强度分布 +- [ ] 用户留存分析 + +--- + +## 🚀 性能优化目标 + +### V1.0性能指标 +- [ ] App启动时间 < 2秒 +- [ ] 摇晃动画延迟 < 50ms +- [ ] 内存占用 < 100MB +- [ ] 电池消耗控制 + +### 持续优化项 +- [ ] 传感器功耗优化 +- [ ] 动画渲染性能提升 +- [ ] 数据库查询优化 +- [ ] 图片加载和缓存策略 + +--- + +## 🏠 首页需求详细分析 + +### 首页核心定位 +**主要功能:** 历史记录浏览展示 +**次要功能:** 创建新心情记录入口 +**页面性质:** 内容消费型页面(非操作型页面) + +### 完整用户流程 + +**创建新记录流程:** +``` +首页 → 点击添加心情按钮 → 底部情绪选择弹框 → 选择情绪 → +添加心情页面(摇晃页) → 心情详情编辑页 → 保存 → 返回首页显示新记录 +``` + +**浏览历史记录流程:** +``` +首页 → 左右滑动查看历史记录 → 时间指示器实时更新 → +单条记录操作(收藏/分享/查看详情) +``` + +### 首页页面结构 + +**1. 顶部导航栏 (ActionBar/Toolbar)** +- **更多按钮:** 点击弹出侧边抽屉菜单 +- **统计按钮:** 跳转到统计页面 + +**2. 时间指示器** +- **功能:** 显示当前浏览记录的时间 +- **交互:** 左右滑动历史记录时,时间动态变化到对应记录时间 +- **格式:** "YYYY.MM.DD 星期几 HH:mm" + +**3. 历史记录展示区 (核心区域)** +- **容器:** ViewPager2横向滑动卡片 +- **卡片元素:** + - 小鸡形象(根据心情显示对应颜色和表情) + - 心情详情(情绪类型、强度、文字内容、图片) + - 操作按钮:收藏/取消收藏、分享、查看详情 +- **滑动效果:** 横向推送切换动画 +- **数据加载:** 初始10条 + 滑动预加载无限滚动 + +**4. 添加心情按钮** +- **位置:** 页面右下角FloatingActionButton +- **功能:** 触发创建新心情记录流程 + +### 关键技术实现方案 + +**1. 数据加载策略** +- **初始加载:** 最近10条历史记录 +- **预加载机制:** 滑动时无感加载更多数据 +- **分页实现:** Android Paging 3.x库 +- **缓存策略:** Room本地数据库 + 内存缓存 + +**2. 页面状态管理** +- **空白状态:** 首次使用显示空页面引导 +- **位置保持:** 详情页返回时保持原浏览位置 +- **自动跳转:** 创建新记录后自动跳转到最新记录 + +**3. 交互细节** +- **情绪选择弹框:** BottomSheetDialog从底部滑出 +- **弹框关闭:** 支持点击外部区域关闭 +- **滑动动画:** ViewPager2 + 自定义PageTransformer +- **状态切换:** 属性动画实现平滑过渡 + +### 首页开发模块拆分 + +**阶段1:基础框架搭建** +- **模块1.1:** MainActivity基础布局结构 +- **模块1.2:** 数据模型定义(MoodRecord, Emotion枚举) +- **模块1.3:** Room数据库设计和实现 +- **模块1.4:** ViewModel和Repository架构 + +**阶段2:历史记录展示** +- **模块2.1:** 空白状态页面实现 +- **模块2.2:** 历史记录卡片Fragment设计 +- **模块2.3:** ViewPager2集成和分页加载 +- **模块2.4:** 滑动动画和效果优化 + +**阶段3:添加心情功能** +- **模块3.1:** FloatingActionButton实现 +- **模块3.2:** 情绪选择BottomSheet弹框 +- **模块3.3:** 页面跳转和参数传递 + +**阶段4:交互细节完善** +- **模块4.1:** 动画效果完善 +- **模块4.2:** 异常处理和边界情况 +- **模块4.3:** 性能优化和内存管理 + +**阶段5:测试和优化** +- **模块5.1:** 端到端流程测试 +- **模块5.2:** UI细节调整和完善 +- **模块5.3:** 代码质量优化 + +### 技术风险评估和解决方案 + +**风险1:大量历史记录的内存占用** +- **解决方案:** ViewPager2 + FragmentStateAdapter + 分页加载 +- **优化策略:** 及时回收不可见的Fragment,限制内存中同时保持的卡片数量 + +**风险2:滑动流畅性和动画性能** +- **解决方案:** 使用硬件加速,优化布局层级 +- **测试策略:** 在低端设备上进行性能测试 + +**风险3:数据状态同步复杂性** +- **解决方案:** 使用LiveData + ViewModel架构,确保UI与数据状态同步 +- **边界处理:** 网络异常、数据库操作失败等异常情况处理 + +--- + +## ❓ 待确认的决策点 + +### 已确认问题: +1. **✅ 首页定位:** 历史记录浏览为主,创建记录为辅 +2. **✅ 交互流程:** 完整的创建和浏览流程已确认 +3. **✅ 技术方案:** 数据加载、动画、状态管理策略已确认 +4. **✅ 开发规划:** 模块化开发路径已确定 + +### 待确认问题: +1. **UI交互相关:** 具体的动画时长、缓动曲线参数 +2. **音效反馈:** V1.0是否需要音效功能 +3. **深色模式:** 是否需要支持深色主题 +4. **字数限制:** 心情记录文字的长度限制 +5. **侧边抽屉:** 更多按钮弹出的具体功能项 +6. **统计页面:** 统计页面的具体功能和数据展示 + +### 其他技术风险评估: +- **传感器兼容性:** 不同设备传感器差异较大(摇晃页面) +- **动画性能:** 实时响应可能影响流畅度 +- **数据准确性:** 心情值算法需要大量测试验证 + +## 🔄 当前开发状态 + +### ✅ 已完成工作 +- **模块1.1**: MainActivity基础布局结构(已完成) +- **Bug修复**: 语法错误、依赖冲突、主题配置问题(已修复) +- **项目架构**: 从Compose成功转换为View系统 +- **测试覆盖**: 完整的单元测试和测试数据生成器 + +### 🔄 当前状态 +- **编译状态**: ✅ 编译成功 +- **安装状态**: ✅ 可安装到设备 +- **运行状态**: 🔄 主题修复待验证(需重启IDE) + +### 📋 下一步计划 +1. **立即任务**: 验证主题修复效果,确保应用正常启动 +2. **模块1.2**: 数据模型定义(MoodRecord, Emotion枚举) +3. **模块1.3**: Room数据库设计和实现 +4. **模块1.4**: ViewModel和Repository架构 + +### ⚠️ 待解决问题 +- **文件锁定**: Gradle build目录锁定问题(需手动重启IDE解决) +- **模块名称**: 已修复settings.gradle.kts中的项目名称不一致问题 +- **主题验证**: 修复后的主题需要在设备上验证 +- **图标资源**: 当前使用占位图标,需要设计师提供正式资源 + +**2025-10-22 (Gradle构建错误修复):** +- 🐛 **问题**: Module entity with name: Chick_Mood should be available +- ✅ **分析**: settings.gradle.kts中项目名称与错误信息不一致 +- ✅ **修复**: 将rootProject.name从"chick_mood"改为"Chick_Mood" +- 🔄 **待验证**: 需重启IDE解决文件锁定问题后重新构建 + +**2025-10-22 (R.jar文件锁定问题):** +- 🐛 **问题**: Windows系统文件锁定,无法删除R.jar文件 +- ✅ **分析**: Java进程未完全释放文件句柄 +- ✅ **处理**: 已强制结束Java进程,提供完整解决方案 +- 🔄 **待完成**: 需用户手动关闭所有程序后删除build目录 + +--- + +## 📝 开发规范 + +### 核心开发原则 + +**1. 模块化开发** +- ✅ 每次只开发一个小模块,避免一次性编写大量代码 +- ✅ 每个模块独立测试,确保功能正确性 +- ✅ 模块间接口清晰,便于集成和维护 + +**2. 测试驱动** +- ✅ 每个模块提供必要的测试数据和测试代码 +- ✅ 单元测试覆盖核心业务逻辑 +- ✅ 集成测试验证模块间交互 + +**3. 代码质量** +- ✅ 提供必要且合适的中文注释 +- ✅ 代码结构规范、科学,遵循设计模式 +- ✅ 便于后续维护和功能迭代 + +### 编码标准 + +**代码结构规范** +```kotlin +/** + * 类/方法功能描述 + * @param 参数说明 + * @return 返回值说明 + * @author 开发者 + * @date 开发日期 + */ +class ClassName { + // 类属性 + private val property: Type = initial_value + + /** + * 方法功能描述 + */ + fun methodName(): ReturnType { + // 方法实现 + } +} +``` + +**注释规范** +- 类和公共方法必须包含KDoc注释 +- 复杂业务逻辑添加行内注释说明 +- 常量和重要变量添加用途说明 + +**命名规范** +- 类名:PascalCase (例:`MoodActivity`) +- 方法名:camelCase (例:`calculateMoodValue()`) +- 常量:UPPER_SNAKE_CASE (例:`MAX_SHAKE_DURATION`) +- 变量:camelCase (例:`currentMoodValue`) + +## 📝 更新记录 + +**2025-10-22:** +- 创建技术决策文档 +- 梳理V1.0核心技术架构 +- 记录待解决的交互和技术问题 +- 确定功能开发优先级 + +**2025-10-22 (开发规范):** +- 添加模块化开发原则 +- 确立测试驱动开发流程 +- 制定代码质量和注释标准 +- 明确编码规范和命名约定 + +**2025-10-22 (首页需求分析):** +- 完成首页UI分析和交互流程理解 +- 明确首页定位:历史记录浏览为主,创建记录为辅 +- 制定详细的模块化开发规划(5个阶段,18个模块) +- 确定技术实现方案和风险控制策略 +- 记录完整用户流程和页面结构需求 + +**2025-10-22 (模块1.1开发完成):** +- ✅ 完成MainActivity基础布局结构搭建 +- ✅ 转换项目架构从Compose到View系统 +- ✅ 添加必要的依赖库(Room、ViewPager2、LiveData等) +- ✅ 创建完整的布局文件和资源文件 +- ✅ 实现ViewBinding和基础组件初始化 +- ✅ 添加完整的单元测试覆盖 + +**2025-10-22 (Bug修复记录):** +- 🐛 **问题1**: MainActivity语法错误(时间格式化缺少右括号) + - ✅ 修复:修正`timeFormatter.format(Date(timestamp))`语法 +- 🐛 **问题2**: Compose依赖冲突导致编译失败 + - ✅ 修复:删除所有Compose相关文件和依赖 +- 🐛 **问题3**: 应用启动闪退(主题配置不兼容) + - ✅ 定位:崩溃日志显示布局解析失败 + - ✅ 分析:Material主题缺少AppCompat支持 + - ✅ 修复:更新主题为`Theme.AppCompat.Light.NoActionBar` + - 🔄 待验证:需重启IDE解决文件锁定问题 + +**技术选型决策(View系统 vs Compose):** +- ✅ **选择View系统原因**: + - 复杂交互支持更好(ViewPager2横向滑动、传感器集成) + - 性能优化更直接(内存管理、硬件加速) + - 开发复杂度更低(团队熟悉度高、调试工具完善) + - 长期维护成本更低(稳定性、可预测性) +- ❌ **Compose潜在问题**: + - 复杂手势处理和自定义动画实现复杂度高 + - 大量数据内存占用控制困难 + - API稳定性有待观察 + +--- + +*此文档将随着开发进展持续更新,记录重要的技术决策和解决方案。* \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2e06aee --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +# 注释掉此项以解决R类生成问题 +# android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..943177d --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Oct 15 10:40:50 CST 2025 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.2-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/prd/别摇小鸡产品需求文档 (PRD)-v1.0.md b/prd/别摇小鸡产品需求文档 (PRD)-v1.0.md new file mode 100644 index 0000000..ffa0d70 --- /dev/null +++ b/prd/别摇小鸡产品需求文档 (PRD)-v1.0.md @@ -0,0 +1,264 @@ +**版本: 1.1** +**日期: 2025年9月11日** +**更新日期: 2025年10月15日** + +### 1. 项目概述 + +### 1.1. 项目背景 + +在快节奏的现代生活中,人们面临着各种情绪压力,但往往缺乏一个简单、直观且私密的情感表达渠道。“别摇小鸡”旨在通过一种新颖的物理交互方式——摇晃手机,帮助用户量化、记录并释放自己的情绪,让心情记录变得生动有趣。 + +### 1.2. 产品定位与目标 + +一款以“小鸡”为核心IP形象的趣味性心情记录工具。 + +- **核心定位:** 你的随身情绪宣泄伙伴。 +- 产品目标: + - **近期目标:** 提供稳定、流畅的核心功能,让用户能够顺利完成“选择情绪 -> 摇晃记录 -> 添加详情 -> 保存”的全过程。打造可爱、治愈的品牌形象,吸引首批种子用户。 + - **远期目标:** 扩展情绪回顾与分析功能,构建一个正向的情感支持社区,提升用户粘性。 + +### 1.3. 目标用户 + +- **核心用户:** 16-28岁的年轻学生、职场新人。他们乐于尝试新鲜事物,注重个人感受,习惯使用手机记录生活,需要一个简单有趣的方式来管理自己的情绪。 +- **次要用户:** 所有希望通过简单方式记录和了解自己情绪波动的用户。 + +### 2. UI/UX 设计规范 + +### 2.1. 核心形象:小鸡 (Piyo) + +小鸡是整个App的灵魂,它的设计应遵循以下原则: + +- **形象:** 简约、圆润、可爱。主体为鲜明的黄色,能唤起温暖、活泼的感觉。 + +- 动态效果: + + 小鸡的动画是核心交互反馈。 + + - **待机状态:** 在情绪选择页,小鸡会有轻微的呼吸、眨眼等待机动画。 + - **情绪状态:** 当用户选定一种情绪后,小鸡的表情和配饰会发生相应变化(例如,生气时头顶冒烟,悲伤时流泪)。 + - **摇晃状态:** 在摇晃过程中,小鸡会根据摇晃的激烈程度,呈现出从轻微晃动到头晕眼花的夸张动画效果。 + +### 2.2. 色彩体系 + +整体色调以温暖、治愈为主。主色调为小鸡的黄色,并为六种情绪设计了专属的辅助色系,确保视觉统一性。 + +| 情绪 | 代表色 | 十六进制色码 | 情绪描述 | +| ---------- | ---------- | ------------- | -------------------- | +| **主题色** | **小鸡黄** | **`#FFD954`** | **温暖、活力、核心** | +| 开心 | 活力橙 | `#FFAB76` | 阳光、喜悦、温暖 | +| 生气 | 警告红 | `#D9534F` | 愤怒、激烈、烦躁 | +| 悲伤 | 忧郁蓝 | `#6B7AFF` | 沉静、伤感、低落 | +| 烦恼 | 浅熏紫 | `#A989C5` | 焦虑、困惑、思索 | +| 孤单 | 深海青 | `#4E89AE` | 孤独、冷静、疏离 | +| 害怕 | 中性灰 | `#888888` | 不安、恐惧、紧张 | + +### 2.3. 字体规范 + +- **中文字体:** 苹方-常规体 (PingFang SC) 或 思源黑体-常规体 (Source Han Sans) +- **英文字体/数字:** Inter / Montserrat +- **特点:** 字体应选择清晰易读的无衬线体,字号根据信息层级清晰划分。 + +### 3. 产品功能详述 + +### 3.1. 核心流程:记录心情 + +这是产品的核心功能闭环。 + +**流程:**`启动App` -> `进入主页/选择情绪` -> `进入准备页` -> `开始摇晃` -> `查看结果` -> `编辑详情页` -> `保存记录` + +**3.1.1. 主页 - 情绪选择** + +- 界面布局: + - 屏幕中央是动态的小鸡形象。 + - 周围环绕/下方陈列六个情绪按钮,每个按钮包含情绪名称和代表性图标。 + - 底部有导航栏,通往“今日记录”和“我的”页面。 +- 交互: + - 用户点击某个情绪按钮,按钮呈现选中状态,中央的小鸡表情切换为对应情绪的可爱动画。 + - 再次点击可取消选择,或选择其他情绪。 + - 选定后,屏幕下方出现一个明确的“下一步”或“开始记录”按钮。 + +**3.1.2. 准备摇晃页** + +- 界面布局: + - 屏幕中央是进入特定情绪状态的小鸡。 + - 屏幕上出现清晰的引导文案,如“准备好了吗?请握紧手机,开始摇晃来捕捉你的【开心】情绪吧!” +- 交互: + - 此页面作为用户摇晃前的过渡,给予用户明确的心理和动作准备。 + - 用户点击“开始”按钮后进入摇晃阶段。 + +**3.1.3. 摇晃捕捉页** + +- 界面布局: + - 整个屏幕成为交互区域。 + - 中央是小鸡,它会根据手机的晃动产生实时动画反馈。 + - 屏幕顶部或周围有一个进度条/能量环,用于实时可视化“心情值”的累积。 + - 屏幕下方有一个“结束”按钮。 +- 交互与技术实现: + - 调用手机的**陀螺仪和加速度传感器**来检测摇晃的幅度和频率。 + - **心情值算法 (示例):** `心情值 = SUM(摇晃事件的加速度 * 权重) / 时间`。摇晃越剧烈、时间越长,心情值越高。 + - 心情值实时反馈在进度条上,进度条的颜色会随着数值的增加,由浅变深。 + - 用户可以随时点击“结束”按钮来停止记录。 + - **安全机制:** 如果摇晃停止超过3-5秒,系统可以自动结束记录,防止误操作。 + +**3.1.4. 心情强度分级** 根据最终累积的“心情值”,系统自动判定情绪的激烈程度。 + +| 心情值范围 | 强度等级 | 界面显示 | +| ----------- | -------- | ---------------- | +| 1 - 1000 | 有些 | 有些开心 | +| 1001 - 3000 | 非常 | 非常开心 | +| 3001 - 6000 | 极度 | 极度开心 | +| 6001+ | 无法控制 | 无法控制的开心! | + +**3.1.5. 详情编辑与保存页** + +- 界面布局: + - 顶部显示本次记录的结果,如“**有些 开心**”,心情值为“**880**”。 + - 记录的日期和时间。 + - 一个大的文本输入框,提示用户“记录下此刻的想法吧...”。 + - 一个“添加图片”的按钮。 + - 底部是“保存”按钮。 +- 交互: + - 用户可以输入文字,或从相册选择/拍摄一张图片。 + - 点击“保存”,系统会弹出“保存成功”的提示,并自动跳转到当天的记录列表或日历视图。 + +### 3.2. 心情日历/回顾 + +- 界面布局: + - 以月视图日历的形式展示。 + - 记录过心情的日期,会用当天主要情绪的代表色进行标记或填充一个小圆点。 + - 点击某个日期,下方会以卡片列表的形式展示当天的所有心情记录。 +- 交互: + - 用户可以轻松地切换月份,回顾过去的情绪变化。 + - 点击单条记录卡片,可以查看完整的心情详情(包括文字和图片)。 + +### 3.3. “我的”页面 + +- 功能: + - **个人资料:** 设置昵称和头像。 + - **心情统计 (未来功能):** 以图表形式(如饼图、折线图)展示一段时间内的情绪分布和变化趋势。 + - **设置:** 通知、主题切换、关于我们、反馈等。 + +### 4. 技术架构与实现 + +### 4.1. 开发平台与技术栈 + +**平台选择:** Android原生开发(暂不开发iOS版本) +- **最低支持版本:** Android 5.0 (API 21+) +- **目标版本:** Android 13+ (API 33+) +- **开发语言:** Kotlin (主) + Java (兼容) + +**核心技术栈:** +- **UI框架:** Android Jetpack (ViewModel, LiveData, Navigation) +- **动画引擎:** Lottie + Property Animation +- **传感器:** Android Sensor Framework (Accelerometer) +- **数据存储:** SQLite Room + SharedPreferences +- **图片处理:** Glide + +### 4.2. 心情值算法 (V1.0简化版) + +``` +心情值 = Σ(加速度幅值) / 摇晃时间 +加速度幅值 = √(x² + y² + z²) +``` + +**实现细节:** +- **采样频率:** 50Hz (每20ms采样一次) +- **最小摇晃时间:** 3秒 +- **数据滤波:** 低通滤波器去除高频噪声 +- **单位标准化:** 不同设备间的加速度值标准化处理 + +### 4.3. 数据库设计 + +```sql +-- 用户配置表 +CREATE TABLE user_config ( + id INTEGER PRIMARY KEY, + nickname TEXT, + avatar_path TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 心情记录表 +CREATE TABLE mood_records ( + id INTEGER PRIMARY KEY, + emotion_type TEXT NOT NULL, -- 开心/生气/悲伤/烦恼/孤单/害怕 + intensity_level TEXT NOT NULL, -- 有些/非常/极度/无法控制 + mood_value INTEGER NOT NULL, -- 心情值数值 + note TEXT, -- 用户备注 + image_path TEXT, -- 图片路径 (V1.1功能) + shake_duration_ms INTEGER, -- 摇晃持续时间(毫秒) + shake_data TEXT, -- 摇晃原始数据(JSON格式) + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); + +-- 预留云端同步接口表 (V2.0) +CREATE TABLE sync_queue ( + id INTEGER PRIMARY KEY, + record_id INTEGER, + sync_status TEXT DEFAULT 'pending', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +``` + +### 4.4. 非功能性需求 + +- **性能:** App启动时间 < 2秒,摇晃动画延迟 < 50ms +- **兼容性:** 支持Android 5.0+,覆盖95%以上Android设备 +- **数据存储:** SQLite本地存储,预留云端同步接口 +- **隐私:** 数据仅本地存储,明确告知用户隐私政策 +- **电池优化:** 传感器使用时功耗控制,空闲时自动释放 +- **权限管理:** 动态申请传感器权限,首次使用时引导用户授权 + +### 5. 版本规划与迭代路线 + +### 5.1. V1.0 - MVP核心功能 (当前开发版本) + +**必须实现功能:** +- ✅ 六种基础情绪选择(开心、生气、悲伤、烦恼、孤单、害怕) +- ✅ 传感器摇晃检测与心情值计算 +- ✅ 小鸡2D动画响应系统 +- ✅ 心情记录保存(文字+心情值) +- ✅ 心情日历月视图展示 +- ✅ 基础用户设置(昵称、头像) + +**延后功能 (V1.1+):** +- ❌ 图片添加功能 +- ❌ 心情统计图表 +- ❌ 云端数据同步 + +### 5.2. V1.1 - 功能增强版 (预计2个月后) + +**新增功能:** +- 📷 图片添加与编辑功能 +- 📊 基础心情统计(饼图、趋势图) +- 🎨 更多小鸡动画效果和皮肤 +- 🔔 心情记录提醒功能 +- 🔧 心情值算法优化(基于用户数据) + +### 5.3. V1.2 - 数据分析版 (预计6个月后) + +**新增功能:** +- 📈 详细心情统计报告(周报/月报) +- 🏷️ 标签功能与事件分类 +- 📱 数据导出功能(CSV格式) +- 🔄 云端数据同步选项 +- 🎯 心情目标设置与追踪 + +### 5.4. V2.0 - 社区与商业化 (预计1年后) + +**新增功能:** +- 👥 匿名情绪分享社区 +- 🧘 正念引导与呼吸练习 +- 💎 会员体系与高级功能 +- 🤖 AI心情分析与建议 +- 🎁 小鸡虚拟物品与装扮系统 + +### 5.5. 技术债务与优化 + +**持续优化项:** +- 传感器算法精准度提升 +- 动画性能优化 +- 电池消耗优化 +- 不同Android设备的兼容性测试 +- 用户隐私保护增强 diff --git a/project_img/ui示意图.png b/project_img/ui示意图.png new file mode 100644 index 0000000..89bdc71 Binary files /dev/null and b/project_img/ui示意图.png differ diff --git a/project_img/添加心情.png b/project_img/添加心情.png new file mode 100644 index 0000000..af7c87e Binary files /dev/null and b/project_img/添加心情.png differ diff --git a/project_img/编辑页.png b/project_img/编辑页.png new file mode 100644 index 0000000..af9a2e8 Binary files /dev/null and b/project_img/编辑页.png differ diff --git a/project_img/详情页.png b/project_img/详情页.png new file mode 100644 index 0000000..3f6904d Binary files /dev/null and b/project_img/详情页.png differ diff --git a/project_img/首页-更多.png b/project_img/首页-更多.png new file mode 100644 index 0000000..2a7e039 Binary files /dev/null and b/project_img/首页-更多.png differ diff --git a/project_img/首页.png b/project_img/首页.png new file mode 100644 index 0000000..83f1401 Binary files /dev/null and b/project_img/首页.png differ diff --git a/project_img/首页/首页-历史记录/首页-历史记录-不带图文@1x.png b/project_img/首页/首页-历史记录/首页-历史记录-不带图文@1x.png new file mode 100644 index 0000000..74b8383 Binary files /dev/null and b/project_img/首页/首页-历史记录/首页-历史记录-不带图文@1x.png differ diff --git a/project_img/首页/首页-历史记录/首页-历史记录-带图文@1x.png b/project_img/首页/首页-历史记录/首页-历史记录-带图文@1x.png new file mode 100644 index 0000000..c47b6e4 Binary files /dev/null and b/project_img/首页/首页-历史记录/首页-历史记录-带图文@1x.png differ diff --git a/project_img/首页/首页-历史记录/首页-历史记录-带文@1x.png b/project_img/首页/首页-历史记录/首页-历史记录-带文@1x.png new file mode 100644 index 0000000..af8032f Binary files /dev/null and b/project_img/首页/首页-历史记录/首页-历史记录-带文@1x.png differ diff --git a/project_img/首页/首页-新建心情@1x.png b/project_img/首页/首页-新建心情@1x.png new file mode 100644 index 0000000..65e56e9 Binary files /dev/null and b/project_img/首页/首页-新建心情@1x.png differ diff --git a/project_img/首页/首页-默认状态@1x.png b/project_img/首页/首页-默认状态@1x.png new file mode 100644 index 0000000..295f83c Binary files /dev/null and b/project_img/首页/首页-默认状态@1x.png differ diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..888ef0d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,28 @@ +pluginManagement { + repositories { + maven { url = uri("https://maven.aliyun.com/repository/releases") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/central") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + maven { url = uri("https://maven.aliyun.com/repository/releases") } + maven { url = uri("https://maven.aliyun.com/repository/google") } + maven { url = uri("https://maven.aliyun.com/repository/central") } + maven { url = uri("https://maven.aliyun.com/repository/gradle-plugin") } + maven { url = uri("https://maven.aliyun.com/repository/public") } + google() + mavenCentral() + } +} + +rootProject.name = "Chick_Mood" +include(":app") + \ No newline at end of file