feat: 完成添加心情功能实现 V1.4.0

-  AddActivity完整实现:包含完整的摇晃检测和心情记录功能
-  遮罩引导系统:2秒"握紧手机" → 2秒"开始摇晃,记录心情" → 自动消失,300ms流畅动画
-  加速度传感器集成:摇晃检测算法(15f阈值+100ms冷却+强度过滤)
-  心情值计算系统:基础增量2分+强度加成,0-100范围实时累计更新
-  心情文案生成:5级程度描述+6种心情类型动态组合
-  实时UI更新:进度条、数值、文案同步更新,提供即时反馈
-  震动反馈功能:每次有效摇晃提供100ms震动反馈,支持新旧Android版本
-  EditActivity新增模式:支持新增记录的数据处理和保存逻辑
-  完整数据流集成:首页→添加页→编辑页→首页的完整循环和自动定位
-  遮罩显示优化:全屏覆盖+文字阴影+28sp字体,确保清晰可读
-  权限管理:添加震动权限,完善传感器权限检查
-  错误处理完善:无传感器Toast提示,震动失败不影响核心功能

技术实现亮点:
- 摇晃检测算法合理,强度计算和阈值过滤
- 震动反馈向后兼容,支持新旧Android版本
- 完整的错误处理和用户反馈机制
- 优化的用户体验,流畅的动画和清晰的引导

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ddshi 2025-10-24 11:58:07 +08:00
parent 28271833d8
commit 43056b245f
14 changed files with 1017 additions and 80 deletions

View File

@ -2,6 +2,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<!-- 震动权限 -->
<uses-permission android:name="android.permission.VIBRATE" />
<application <application
android:allowBackup="true" android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
@ -42,6 +45,13 @@
android:label="编辑心情" android:label="编辑心情"
android:theme="@style/Theme.Chick_mood" android:theme="@style/Theme.Chick_mood"
android:windowSoftInputMode="adjustResize" /> android:windowSoftInputMode="adjustResize" />
<activity
android:name=".AddActivity"
android:exported="false"
android:label="添加心情"
android:theme="@style/Theme.Chick_mood"
android:screenOrientation="portrait" />
</application> </application>
</manifest> </manifest>

View File

@ -0,0 +1,416 @@
package com.daodaoshi.chick_mood
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.animation.AlphaAnimation
import android.view.animation.Animation
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import android.os.Vibrator
import android.os.VibrationEffect
import com.chick_mood.data.model.Emotion
import java.text.DecimalFormat
/**
* 添加心情页面
* 通过摇晃手机来记录心情强度的核心功能页面
*/
class AddActivity : AppCompatActivity(), SensorEventListener {
// UI组件
private lateinit var btnBack: ImageView
private lateinit var tvTitle: TextView
private lateinit var tvMoodText: TextView
private lateinit var tvMoodValue: TextView
private lateinit var progressBar: ProgressBar
private lateinit var ivChick: ImageView
private lateinit var btnEnd: Button
private lateinit var viewOverlay: View
private lateinit var tvOverlayText: TextView
// 传感器相关
private lateinit var sensorManager: SensorManager
private var accelerometer: Sensor? = null
private var isSensorRegistered = false
// 震动反馈
private lateinit var vibrator: Vibrator
// 心情数据
private var selectedEmotion: Emotion? = null
private var currentMoodValue = 0
private val maxMoodValue = 100
// 遮罩引导系统
private val handler = Handler(Looper.getMainLooper())
private var overlayState = OVERLAY_STATE_INITIAL
// 摇晃检测相关
private var lastShakeTime = 0L
private var shakeThreshold = 15f // 摇晃强度阈值
private val moodValueFormatter = DecimalFormat("#")
companion object {
private const val TAG = "AddActivity"
private const val EXTRA_EMOTION_TYPE = "emotion_type"
// 遮罩状态
private const val OVERLAY_STATE_INITIAL = 0
private const val OVERLAY_STATE_READY = 1
private const val OVERLAY_STATE_HIDDEN = 2
// 遮罩显示时间(毫秒)
private const val OVERLAY_INITIAL_DURATION = 2000L
private const val OVERLAY_READY_DURATION = 2000L
// 心情值累计参数
private const val SHAKE_COOLDOWN = 100L // 摇晃冷却时间(毫秒)
private const val MOOD_INCREMENT_BASE = 2 // 基础心情值增量
/**
* 启动AddActivity
* @param context 上下文
* @param emotion 选择的心情类型
*/
fun start(context: Context, emotion: Emotion) {
val intent = Intent(context, AddActivity::class.java).apply {
putExtra(EXTRA_EMOTION_TYPE, emotion.name)
}
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_add)
// 获取传递的心情类型
selectedEmotion = intent.getStringExtra(EXTRA_EMOTION_TYPE)?.let {
Emotion.valueOf(it)
} ?: Emotion.HAPPY
initViews()
initSensor()
startOverlaySequence()
}
/**
* 初始化视图组件
*/
private fun initViews() {
btnBack = findViewById(R.id.btn_back)
tvTitle = findViewById(R.id.tv_title)
tvMoodText = findViewById(R.id.tv_mood_text)
tvMoodValue = findViewById(R.id.tv_mood_value)
progressBar = findViewById(R.id.progress_bar)
ivChick = findViewById(R.id.iv_chick)
btnEnd = findViewById(R.id.btn_end)
viewOverlay = findViewById(R.id.view_overlay)
tvOverlayText = findViewById(R.id.tv_overlay_text)
// 设置标题
tvTitle.text = "添加心情"
// 根据心情类型设置小鸡图标
selectedEmotion?.let { emotion ->
val chickIcon = getEmotionIconRes(emotion)
ivChick.setImageResource(chickIcon)
}
// 初始化UI状态
resetUI()
// 设置点击监听器
btnBack.setOnClickListener {
finish()
}
btnEnd.setOnClickListener {
onEndButtonClicked()
}
// 遮罩点击事件(可选:允许用户手动关闭遮罩)
viewOverlay.setOnClickListener {
// 可以选择是否允许用户手动关闭遮罩
// hideOverlay()
}
}
/**
* 初始化传感器和震动器
*/
private fun initSensor() {
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
// 初始化震动器
vibrator = getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
if (accelerometer == null) {
Toast.makeText(this, "无传感器", Toast.LENGTH_SHORT).show()
}
}
/**
* 开始遮罩引导序列
*/
private fun startOverlaySequence() {
overlayState = OVERLAY_STATE_INITIAL
viewOverlay.visibility = View.VISIBLE
tvOverlayText.visibility = View.VISIBLE
tvOverlayText.text = "握紧手机"
// 添加淡入动画
val fadeIn = AlphaAnimation(0f, 1f)
fadeIn.duration = 300
viewOverlay.startAnimation(fadeIn)
tvOverlayText.startAnimation(fadeIn)
// 2秒后切换到准备状态
handler.postDelayed({
if (overlayState == OVERLAY_STATE_INITIAL) {
overlayState = OVERLAY_STATE_READY
tvOverlayText.text = "开始摇晃,记录心情"
// 再过2秒后隐藏遮罩
handler.postDelayed({
hideOverlay()
}, OVERLAY_READY_DURATION)
}
}, OVERLAY_INITIAL_DURATION)
}
/**
* 隐藏遮罩
*/
private fun hideOverlay() {
if (overlayState != OVERLAY_STATE_HIDDEN) {
overlayState = OVERLAY_STATE_HIDDEN
// 添加淡出动画
val fadeOut = AlphaAnimation(1f, 0f)
fadeOut.duration = 300
fadeOut.setAnimationListener(object : Animation.AnimationListener {
override fun onAnimationStart(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {
viewOverlay.visibility = View.GONE
tvOverlayText.visibility = View.GONE
// 开始监听摇晃
startShakeDetection()
}
override fun onAnimationRepeat(animation: Animation) {}
})
viewOverlay.startAnimation(fadeOut)
tvOverlayText.startAnimation(fadeOut)
}
}
/**
* 开始摇晃检测
*/
private fun startShakeDetection() {
if (!isSensorRegistered && accelerometer != null) {
sensorManager.registerListener(
this,
accelerometer,
SensorManager.SENSOR_DELAY_NORMAL
)
isSensorRegistered = true
}
}
/**
* 停止摇晃检测
*/
private fun stopShakeDetection() {
if (isSensorRegistered) {
sensorManager.unregisterListener(this)
isSensorRegistered = false
}
}
/**
* 重置UI状态
*/
private fun resetUI() {
currentMoodValue = 0
tvMoodText.text = "开始!"
tvMoodValue.text = moodValueFormatter.format(currentMoodValue)
progressBar.progress = 0
}
/**
* 结束按钮点击处理
*/
private fun onEndButtonClicked() {
stopShakeDetection()
// 创建心情记录数据
val currentTime = System.currentTimeMillis()
// 跳转到编辑页面
EditActivity.startForAdd(
context = this,
emotion = selectedEmotion!!,
moodValue = currentMoodValue,
timestamp = currentTime
)
finish()
}
/**
* 更新心情值和UI
*/
private fun updateMoodValue(increment: Int) {
currentMoodValue = (currentMoodValue + increment).coerceAtMost(maxMoodValue)
// 更新UI
tvMoodValue.text = moodValueFormatter.format(currentMoodValue)
progressBar.progress = currentMoodValue
// 更新心情文案
updateMoodText()
}
/**
* 更新心情文案
*/
private fun updateMoodText() {
val moodText = generateMoodText(currentMoodValue, selectedEmotion!!)
tvMoodText.text = moodText
}
/**
* 生成心情文案
*/
private fun generateMoodText(value: Int, emotion: Emotion): String {
val degree = when {
value <= 20 -> "有些"
value <= 40 -> "非常"
value <= 60 -> "超级"
value <= 80 -> "这也太"
else -> "完全无法控制"
}
return "$degree ${emotion.displayName}"
}
/**
* 获取情绪对应的图标资源
*/
private fun getEmotionIconRes(emotion: Emotion): Int {
return when (emotion) {
Emotion.HAPPY -> R.drawable.ic_placeholder_chick_happy
Emotion.ANGRY -> R.drawable.ic_placeholder_chick_angry
Emotion.SAD -> R.drawable.ic_placeholder_chick_sad
Emotion.WORRIED -> R.drawable.ic_placeholder_chick_worried
Emotion.LONELY -> R.drawable.ic_placeholder_chick_lonely
Emotion.SCARED -> R.drawable.ic_placeholder_chick_scared
}
}
// SensorEventListener 回调方法
override fun onSensorChanged(event: SensorEvent?) {
event?.let {
if (it.sensor.type == Sensor.TYPE_ACCELEROMETER) {
val currentTime = System.currentTimeMillis()
// 防止过于频繁的检测
if (currentTime - lastShakeTime < SHAKE_COOLDOWN) {
return
}
val x = it.values[0]
val y = it.values[1]
val z = it.values[2]
// 计算加速度的平方和
val accelerationSquare = x * x + y * y + z * z
// 计算总的加速度强度
val acceleration = Math.sqrt(accelerationSquare.toDouble()).toFloat()
// 检测是否达到摇晃阈值
if (acceleration > shakeThreshold) {
lastShakeTime = currentTime
// 提供震动反馈
provideVibrationFeedback()
// 根据摇晃强度计算心情值增量
val increment = calculateMoodIncrement(acceleration)
updateMoodValue(increment)
}
}
}
}
/**
* 提供震动反馈
*/
private fun provideVibrationFeedback() {
try {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// Android 8.0及以上使用VibrationEffect
val vibrationEffect = VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE)
vibrator.vibrate(vibrationEffect)
} else {
// 旧版本使用传统方法
@Suppress("DEPRECATION")
vibrator.vibrate(100)
}
} catch (e: Exception) {
// 震动功能失败不影响核心功能
Log.d(TAG, "震动反馈失败", e)
}
}
/**
* 根据摇晃强度计算心情值增量
*/
private fun calculateMoodIncrement(acceleration: Float): Int {
// 基础增量 + 根据摇晃强度的额外增量
val baseIncrement = MOOD_INCREMENT_BASE
val bonusIncrement = ((acceleration - shakeThreshold) / 10).toInt().coerceAtLeast(0)
return baseIncrement + bonusIncrement
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// 不需要处理
}
override fun onResume() {
super.onResume()
// 如果遮罩已经隐藏,重新开始传感器监听
if (overlayState == OVERLAY_STATE_HIDDEN) {
startShakeDetection()
}
}
override fun onPause() {
super.onPause()
stopShakeDetection()
handler.removeCallbacksAndMessages(null)
}
override fun onDestroy() {
super.onDestroy()
stopShakeDetection()
handler.removeCallbacksAndMessages(null)
}
}

View File

@ -1,6 +1,7 @@
package com.daodaoshi.chick_mood package com.daodaoshi.chick_mood
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
@ -49,6 +50,12 @@ class EditActivity : AppCompatActivity() {
private var currentImagePath: String? = null private var currentImagePath: String? = null
private val databaseManager = SimpleDatabaseManager.getInstance(this) private val databaseManager = SimpleDatabaseManager.getInstance(this)
// 新增记录模式相关变量
private var isAddMode = false
private var addModeEmotion: Emotion? = null
private var addModeMoodValue: Int = 0
private var addModeTimestamp: Long = 0
// 图片选择器 // 图片选择器
private val imagePickerLauncher = registerForActivityResult( private val imagePickerLauncher = registerForActivityResult(
androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult() androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult()
@ -64,6 +71,43 @@ class EditActivity : AppCompatActivity() {
companion object { companion object {
private const val MAX_TEXT_LENGTH = 500 private const val MAX_TEXT_LENGTH = 500
private const val TAG = "EditActivity" private const val TAG = "EditActivity"
// Intent extra keys
private const val EXTRA_RECORD_ID = "record_id"
private const val EXTRA_IS_ADD_MODE = "is_add_mode"
private const val EXTRA_EMOTION_TYPE = "emotion_type"
private const val EXTRA_MOOD_VALUE = "mood_value"
private const val EXTRA_TIMESTAMP = "timestamp"
/**
* 启动编辑页面编辑模式
* @param context 上下文
* @param recordId 要编辑的记录ID
*/
fun start(context: Context, recordId: Long) {
val intent = Intent(context, EditActivity::class.java).apply {
putExtra(EXTRA_RECORD_ID, recordId)
putExtra(EXTRA_IS_ADD_MODE, false)
}
context.startActivity(intent)
}
/**
* 启动编辑页面新增模式
* @param context 上下文
* @param emotion 心情类型
* @param moodValue 心情值
* @param timestamp 时间戳
*/
fun startForAdd(context: Context, emotion: Emotion, moodValue: Int, timestamp: Long) {
val intent = Intent(context, EditActivity::class.java).apply {
putExtra(EXTRA_IS_ADD_MODE, true)
putExtra(EXTRA_EMOTION_TYPE, emotion.name)
putExtra(EXTRA_MOOD_VALUE, moodValue)
putExtra(EXTRA_TIMESTAMP, timestamp)
}
context.startActivity(intent)
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -119,13 +163,53 @@ class EditActivity : AppCompatActivity() {
* 加载心情记录数据 * 加载心情记录数据
*/ */
private fun loadData() { private fun loadData() {
val recordId = intent.getLongExtra("record_id", -1L) // 检查是否为新增模式
if (recordId == -1L) { isAddMode = intent.getBooleanExtra(EXTRA_IS_ADD_MODE, false)
finish()
return
}
loadRecordFromDatabase(recordId) if (isAddMode) {
// 新增模式
loadAddModeData()
} else {
// 编辑模式
val recordId = intent.getLongExtra("record_id", -1L)
if (recordId == -1L) {
finish()
return
}
loadRecordFromDatabase(recordId)
}
}
/**
* 加载新增模式的数据
*/
private fun loadAddModeData() {
try {
addModeEmotion = intent.getStringExtra(EXTRA_EMOTION_TYPE)?.let {
Emotion.valueOf(it)
} ?: Emotion.HAPPY
addModeMoodValue = intent.getIntExtra(EXTRA_MOOD_VALUE, 0)
addModeTimestamp = intent.getLongExtra(EXTRA_TIMESTAMP, System.currentTimeMillis())
// 创建临时记录用于显示
currentRecord = MoodRecord(
id = 0, // 临时ID保存时会重新分配
emotion = addModeEmotion!!,
moodIntensity = addModeMoodValue,
timestamp = addModeTimestamp,
textContent = null,
imagePath = null,
isFavorite = false
)
// 更新UI
displayRecord(currentRecord!!)
Log.d(TAG, "Add mode data loaded successfully")
} catch (e: Exception) {
Log.e(TAG, "Error loading add mode data", e)
showToast("加载新增模式数据失败")
finish()
}
} }
/** /**
@ -354,26 +438,60 @@ class EditActivity : AppCompatActivity() {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val updatedRecord = record.copy( if (isAddMode) {
textContent = if (updatedText.isEmpty()) null else updatedText, // 新增模式:创建新记录
imagePath = currentImagePath, val newRecord = MoodRecord(
timestamp = record.timestamp // 保持原始时间戳不变 id = 0, // 数据库自动生成ID
) emotion = record.emotion,
moodIntensity = record.moodIntensity,
timestamp = record.timestamp,
textContent = if (updatedText.isEmpty()) null else updatedText,
imagePath = currentImagePath,
isFavorite = false
)
databaseManager.updateMoodRecord(updatedRecord) val newId = databaseManager.createMoodRecord(
emotion = newRecord.emotion,
intensity = newRecord.moodIntensity,
noteText = newRecord.textContent ?: "",
imagePaths = if (newRecord.imagePath != null) listOf(newRecord.imagePath) else emptyList()
)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showToast("保存成功") showToast("保存成功")
// 直接跳转到首页并传递编辑的记录ID用于定位 // 跳转到首页并传递新增记录ID用于定位
val homeIntent = Intent(this@EditActivity, MainActivitySimple::class.java).apply { val homeIntent = Intent(this@EditActivity, MainActivitySimple::class.java).apply {
putExtra("edited_record_id", record.id) putExtra("new_record_id", newId)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
}
startActivity(homeIntent)
finish()
}
} else {
// 编辑模式:更新现有记录
val updatedRecord = record.copy(
textContent = if (updatedText.isEmpty()) null else updatedText,
imagePath = currentImagePath,
timestamp = record.timestamp // 保持原始时间戳不变
)
databaseManager.updateMoodRecord(updatedRecord)
withContext(Dispatchers.Main) {
showToast("保存成功")
// 跳转到首页并传递编辑的记录ID用于定位
val homeIntent = Intent(this@EditActivity, MainActivitySimple::class.java).apply {
putExtra("edited_record_id", record.id)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(homeIntent)
finish()
} }
startActivity(homeIntent)
finish()
} }
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Error saving record", e)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showToast("保存失败") showToast("保存失败")
} }

View File

@ -154,12 +154,10 @@ class MainActivitySimple : AppCompatActivity() {
Emotion.SCARED -> "害怕" Emotion.SCARED -> "害怕"
} }
showTemporaryMessage("你选择了:$emotionName")
// 创建一条测试记录来验证功能
createMoodRecordWithEmotion(emotion)
Log.d(TAG, "用户选择了情绪:$emotionName") Log.d(TAG, "用户选择了情绪:$emotionName")
// 跳转到添加心情页面
AddActivity.start(this, emotion)
} }
/** /**
@ -652,6 +650,12 @@ class MainActivitySimple : AppCompatActivity() {
Log.d(TAG, "检测到编辑完成的记录ID: $editedRecordId") Log.d(TAG, "检测到编辑完成的记录ID: $editedRecordId")
recordIdToLocate = editedRecordId recordIdToLocate = editedRecordId
} }
val newRecordId = intent.getLongExtra("new_record_id", -1L)
if (newRecordId != -1L) {
Log.d(TAG, "检测到新增完成的记录ID: $newRecordId")
recordIdToLocate = newRecordId
}
} }
/** /**

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/primary_dark" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/text_hint" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="8dp" />
<solid android:color="@color/primary_color" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 背景进度条 -->
<item android:id="@android:id/background">
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/progress_background" />
</shape>
</item>
<!-- 主进度条 -->
<item android:id="@android:id/progress">
<clip>
<shape android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="@color/primary_color" />
</shape>
</clip>
</item>
</layer-list>

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/background_light"
tools:context=".AddActivity">
<!-- 顶部导航栏 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/top_bar"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@color/white"
android:elevation="4dp"
app:layout_constraintTop_toTopOf="parent">
<!-- 返回按钮 -->
<ImageView
android:id="@+id/btn_back"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="返回"
android:padding="12dp"
android:src="@drawable/ic_arrow_back"
android:tint="@color/text_primary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<!-- 标题 -->
<TextView
android:id="@+id/tv_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="添加心情"
android:textColor="@color/text_primary"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 内容区域 -->
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/content_area"
android:layout_width="match_parent"
android:layout_height="0dp"
android:padding="32dp"
app:layout_constraintBottom_toTopOf="@id/btn_end"
app:layout_constraintTop_toBottomOf="@id/top_bar">
<!-- 小鸡形象 -->
<ImageView
android:id="@+id/iv_chick"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="48dp"
android:contentDescription="小鸡形象"
android:scaleType="centerInside"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_placeholder_happy" />
<!-- 心情文案 -->
<TextView
android:id="@+id/tv_mood_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:text="开始!"
android:textColor="@color/text_primary"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/iv_chick" />
<!-- 心情值显示 -->
<TextView
android:id="@+id/tv_mood_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="0"
android:textColor="@color/text_primary"
android:textSize="48sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_mood_text" />
<!-- 进度条 -->
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="8dp"
android:layout_marginTop="24dp"
android:layout_marginHorizontal="32dp"
android:max="100"
android:progress="0"
android:progressDrawable="@drawable/progress_bar_mood"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_mood_value" />
<!-- 提示文案 -->
<TextView
android:id="@+id/tv_hint"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="摇晃手机记录你的心情"
android:textColor="@color/text_secondary"
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progress_bar" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- 结束按钮 -->
<Button
android:id="@+id/btn_end"
android:layout_width="0dp"
android:layout_height="56dp"
android:layout_marginHorizontal="32dp"
android:layout_marginBottom="32dp"
android:background="@drawable/button_primary"
android:text="结束"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- 遮罩层 - 放在最后确保覆盖所有内容 -->
<View
android:id="@+id/view_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/overlay_background"
android:clickable="true"
android:focusable="true"
android:visibility="gone"
tools:visibility="visible" />
<!-- 遮罩文案 - 放在遮罩层之后 -->
<TextView
android:id="@+id/tv_overlay_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="握紧手机"
android:textColor="@color/white"
android:textSize="28sp"
android:textStyle="bold"
android:shadowColor="#80000000"
android:shadowDx="2"
android:shadowDy="2"
android:shadowRadius="4"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -40,4 +40,10 @@
<!-- 额外色彩 --> <!-- 额外色彩 -->
<color name="chick_red">#FF5252</color> <color name="chick_red">#FF5252</color>
<color name="blue_300">#64B5F6</color> <color name="blue_300">#64B5F6</color>
<!-- 添加心情页面专用颜色 -->
<color name="background_light">#FFF8E1</color>
<color name="primary_color">#FFD954</color>
<color name="progress_background">#FFE0B2</color>
<color name="overlay_background">#80000000</color>
</resources> </resources>

275
claude.md
View File

@ -6,9 +6,9 @@
**项目名称:** 别摇小鸡心情记录App **项目名称:** 别摇小鸡心情记录App
**开发平台:** Android原生Kotlin **开发平台:** Android原生Kotlin
**当前版本:** V1.3.0 **当前版本:** V1.4.0
**更新日期:** 2025-10-24 **更新日期:** 2025-10-24
**开发状态:** ✅ 编辑页功能完成,图片显示系统完善 **开发状态:** ✅ 添加心情功能完成,摇晃检测和震动反馈集成
--- ---
@ -20,6 +20,7 @@
- **数据存储:** SQLite Room + SharedPreferences - **数据存储:** SQLite Room + SharedPreferences
- **架构模式:** MVVM + Repository - **架构模式:** MVVM + Repository
- **动画引擎:** Lottie (准备中) + Property Animation - **动画引擎:** Lottie (准备中) + Property Animation
- **传感器支持:** 加速度传感器 (摇晃检测)
### 功能模块 ### 功能模块
**V1.1.0 核心功能:** **V1.1.0 核心功能:**
@ -57,8 +58,19 @@
- ✅ 图片选择、保存、加载、删除完整流程 - ✅ 图片选择、保存、加载、删除完整流程
- ✅ 定位机制优化(编辑和新增记录自动定位) - ✅ 定位机制优化(编辑和新增记录自动定位)
- ✅ 数据一致性修复(禁用额外测试记录) - ✅ 数据一致性修复(禁用额外测试记录)
- 📋 摇晃检测与心情值计算(待开发)
- 📋 小鸡动画响应系统(待开发) **V1.4.0 添加心情功能:**
- ✅ 添加心情页面基础布局和UI组件AddActivity + activity_add.xml
- ✅ 遮罩引导系统握紧手机→开始摇晃→自动消失300ms动画
- ✅ 加速度传感器集成和摇晃检测15f阈值+100ms冷却
- ✅ 心情值计算和实时进度更新2-100分增量算法
- ✅ 心情文案生成逻辑5级程度描述+6种心情类型
- ✅ 小鸡形象显示(根据心情类型显示对应图标)
- ✅ 结束按钮和编辑页跳转逻辑EditActivity新增模式
- ✅ 首页添加按钮与心情选择弹窗集成(完整跳转逻辑)
- ✅ 完整数据流传递(首页→添加页→编辑页→首页自动定位)
- ✅ 震动反馈功能100ms震动支持新旧Android版本
- ✅ 遮罩显示优化(全屏覆盖+文字阴影+28sp字体
--- ---
@ -132,14 +144,27 @@
- **模块6.8** ✅ 定位机制优化(编辑和新增记录自动定位) - **模块6.8** ✅ 定位机制优化(编辑和新增记录自动定位)
- **模块6.9** ✅ 数据一致性修复(禁用额外测试记录) - **模块6.9** ✅ 数据一致性修复(禁用额外测试记录)
### 🔄 开发中模块 ### ✅ 已完成模块
**阶段7核心功能完善下一步重点** **阶段7添加心情功能实现已完成**
- **模块7.1** 📋 摇晃检测功能实现 - **模块7.1** ✅ 添加心情页面基础布局和UI组件
- **模块7.2** 📋 心情值计算逻辑 - **模块7.2** ✅ 遮罩引导系统实现(握紧手机→开始摇晃→自动消失)
- **模块7.3** 📋 小鸡动画响应系统 - **模块7.3** ✅ 加速度传感器集成和摇晃检测逻辑
- **模块7.4** 📋 侧边抽屉功能(更多按钮) - **模块7.4** ✅ 心情值计算和实时进度更新系统
- **模块7.5** 📋 统计页面实现(统计按钮) - **模块7.5** ✅ 心情文案生成逻辑(基于心情值+心情类型)
- **模块7.6** ✅ 小鸡形象显示(根据心情类型)
- **模块7.7** ✅ 结束按钮和编辑页跳转逻辑
- **模块7.8** ✅ 首页添加按钮与心情选择弹窗集成
- **模块7.9** ✅ 完整数据流传递(首页→添加页→编辑页→首页)
### 🔄 后续开发计划
**阶段8功能完善和优化下一阶段**
- **模块8.1** 📋 小鸡动画响应系统
- **模块8.2** 📋 侧边抽屉功能(更多按钮)
- **模块8.3** 📋 统计页面实现(统计按钮)
- **模块8.4** 📋 摇晃检测参数调优和设备适配
- **模块8.5** 📋 用户体验细节优化和错误处理完善
--- ---
@ -170,68 +195,95 @@
## 🎯 下一步开发计划 ## 🎯 下一步开发计划
### 优先级1详情页功能实现当前重点 ### 优先级1添加心情页面实现当前重点
**模块5.1详情页基础布局和UI组件**
- 创建DetailActivity和对应布局文件
- 实现顶部导航栏(返回、时间、更多操作)
- 实现内容区域布局(小鸡形象、心情值、文案、配图、文本)
- 支持四种内容状态(有图有文、有图无文、无图有文、无图无文)
**模块5.2:心情文案生成逻辑** **模块7.1添加心情页面基础布局和UI组件**
- 实现心情值到程度描述的映射逻辑 - 创建AddActivity和对应布局文件activity_add.xml
- 动态生成"程度+心情类型"组合文案 - 实现顶部导航栏(返回按钮、标题"添加心情"
- 心情值范围0-20(有些) → 20-40(非常) → 40-60(超级) → 60-80(这也太) → 80-100(完全无法控制) - 实现内容区域布局(心情文案、心情值、进度条、小鸡形象)
- 实现底部结束按钮和遮罩引导层
**模块5.3:更多操作弹窗功能** **模块7.2:遮罩引导系统实现**
- 实现底部弹窗布局(编辑、收藏、分享、删除) - 创建遮罩布局引导用户操作流程
- 弹窗动画效果和交互逻辑 - 实现定时器控制:握紧手机(2s) → 开始摇晃(2s) → 自动消失
- 各操作按钮的状态管理和点击响应 - 添加遮罩动画效果(淡入淡出)
- 处理用户点击遮罩的交互逻辑
**模块5.4:删除功能和二次确认弹窗** **模块7.3:加速度传感器集成和摇晃检测**
- 实现删除确认弹窗布局和交互 - 集成Android加速度传感器(SensorManager)
- 数据库记录删除操作 - 实现传感器事件监听和数据采集
- 删除成功提示和页面跳转逻辑 - 开发摇晃检测算法(强度计算、阈值过滤)
- 添加传感器权限检查和错误处理
**模块5.5:收藏功能和状态持久化** **模块7.4:心情值计算和实时进度更新系统**
- 数据库添加收藏字段支持 - 开发心情值累计算法(基于摇晃强度和时长)
- 收藏状态切换和UI反馈 - 实现进度条实时更新UI组件
- 收藏状态持久化存储 - 添加心情值上限控制最高100
- 优化更新频率,平衡性能和用户体验
**模块5.6:图片放大查看功能** **模块7.5:心情文案生成逻辑**
- 实现图片点击放大预览 - 复用详情页的心情文案生成逻辑
- 支持手势缩放和拖拽 - 根据实时心情值更新程度描述
- 1:1比例显示图片自适应 - 实现文案动态切换动画效果
- 确保文案与心情值同步更新
**模块5.7:分享功能集成** **模块7.6:小鸡形象显示**
- 集成Android系统分享功能 - 根据传入的心情类型显示对应小鸡图标
- 生成分享文本内容 - 复用现有的心情类型图标资源
- 跳转系统分享选择器 - 添加小鸡形象的显示动画(可选)
### 优先级2核心交互功能摇晃检测 **模块7.7:结束按钮和编辑页跳转逻辑**
**模块6.1:摇晃检测功能实现** - 实现结束按钮点击事件处理
- 实现手机摇晃传感器监听 - 准备跳转数据(心情类型、心情值、时间戳)
- 摇晃强度和持续时间计算 - 修改EditActivity支持新增记录模式
- 摇晃阈值设置和优化 - 实现AddActivity到EditActivity的数据传递
**模块6.2:心情值计算逻辑** **模块7.8:首页添加按钮与心情选择弹窗集成**
- 根据摇晃参数计算心情强度值 - 修改MainActivity的添加按钮逻辑
- 心情强度映射到情绪等级 - 修改心情选择弹窗的确认逻辑
- 心情值与情绪选择的关联 - 实现MainActivity到AddActivity的跳转
- 传递心情类型数据到AddActivity
### 优先级3动画系统完善 **模块7.9:完整数据流传递优化**
**模块6.3:小鸡动画响应系统** - 测试完整流程:首页→添加页→编辑页→首页
- 优化EditActivity的新增记录模式
- 确保首页自动定位到新增记录
- 修复数据传递中的边界情况
### 优先级2功能完善和优化后续开发
**模块8.1:摇晃检测优化**
- 调整摇晃检测灵敏度参数
- 添加不同设备的适配逻辑
- 优化传感器数据处理的性能
- 添加摇晃检测的用户反馈优化
**模块8.2:用户体验优化**
- 添加页面切换动画效果
- 优化遮罩引导的视觉体验
- 添加摇晃过程的音效反馈(可选)
- 完善错误状态的提示信息
**模块8.3:测试和稳定性**
- 编写摇晃检测的单元测试
- 测试不同设备上的传感器兼容性
- 添加异常情况的处理逻辑
- 进行完整的集成测试
### 优先级3扩展功能未来开发
**模块9.1:小鸡动画响应系统**
- 小鸡待机动画Lottie集成 - 小鸡待机动画Lottie集成
- 摇晃时的小鸡反应动画 - 摇晃时的小鸡反应动画
- 不同情绪对应的小鸡状态动画 - 不同情绪对应的小鸡状态动画
- 点击小鸡的交互反馈动画 - 点击小鸡的交互反馈动画
### 优先级4页面功能扩展 **模块9.2:侧边抽屉功能(更多按钮)**
**模块6.4:侧边抽屉功能(更多按钮)**
- 实现侧边抽屉UI布局 - 实现侧边抽屉UI布局
- 设置、关于、帮助等页面入口 - 设置、关于、帮助等页面入口
- 用户个人资料管理 - 用户个人资料管理
**模块6.5:统计页面实现(统计按钮)** **模块9.3:统计页面实现(统计按钮)**
- 心情数据统计分析 - 心情数据统计分析
- 情绪趋势图表展示 - 情绪趋势图表展示
- 记录统计和可视化 - 记录统计和可视化
@ -325,6 +377,20 @@ Chick_Mood/
## 🔄 更新记录 ## 🔄 更新记录
**2025-10-24 (V1.4.0 添加心情功能完成)**
- ✅ **AddActivity完整实现**:包含完整的摇晃检测和心情记录功能
- ✅ **遮罩引导系统**2秒"握紧手机" → 2秒"开始摇晃,记录心情" → 自动消失300ms流畅动画
- ✅ **加速度传感器集成**摇晃检测算法15f阈值+100ms冷却+强度过滤)
- ✅ **心情值计算系统**基础增量2分+强度加成0-100范围实时累计更新
- ✅ **心情文案生成**5级程度描述有些/非常/超级/这也太/完全无法控制)+6种心情类型
- ✅ **实时UI更新**:进度条、数值、文案同步更新,提供即时反馈
- ✅ **震动反馈功能**每次有效摇晃提供100ms震动反馈支持新旧Android版本
- ✅ **EditActivity新增模式**:支持新增记录的数据处理和保存逻辑
- ✅ **完整数据流集成**:首页→添加页→编辑页→首页的完整循环和自动定位
- ✅ **遮罩显示优化**:全屏覆盖+文字阴影+28sp字体确保清晰可读
- ✅ **权限管理**:添加震动权限,完善传感器权限检查
- ✅ **错误处理完善**无传感器Toast提示震动失败不影响核心功能
**2025-10-24 (V1.3.0 编辑页功能完成)** **2025-10-24 (V1.3.0 编辑页功能完成)**
- ✅ **编辑页完整实现**包含基础布局、UI组件和退出确认机制 - ✅ **编辑页完整实现**包含基础布局、UI组件和退出确认机制
- ✅ **返回键退出确认逻辑**:检测编辑行为,弹出二次确认弹窗 - ✅ **返回键退出确认逻辑**:检测编辑行为,弹出二次确认弹窗
@ -465,4 +531,103 @@ Chick_Mood/
--- ---
## 📄 添加心情页面功能详细说明
### 🎯 功能概述
添加心情页面是通过摇晃手机来记录心情强度的核心功能页面,用户通过摇晃动作来量化心情程度,然后跳转到编辑页完成最终记录。
### 🔄 完整交互流程
```
首页 → 点击添加按钮 → 心情选择弹窗 → 选择心情类型 → 添加心情页面 → 握紧提示(2s) → 摇晃提示(2s) → 遮罩消失 → 用户摇晃 → 实时显示进度 → 点击结束按钮 → 编辑页面 → 编辑文本/图片 → 保存 → 首页(自动定位到新记录)
```
### 📱 页面结构与状态
#### 1. 顶部区域
- **返回按钮:** 左上角,点击返回上一页(首页)
- **标题显示:** 添加心情(页面标题)
#### 2. 信息显示区域
- **心情文案区域:**
- 默认状态:显示"开始!"
- 摇晃状态:显示"程度+心情类型"组合文案
- 程度映射0-20(有些) → 20-40(非常) → 40-60(超级) → 60-80(这也太) → 80-100(完全无法控制)
- 示例心情值88 + 开心 = "完全无法控制 开心"
- **心情值显示:**
- 数值范围0-100
- 根据摇晃强度和时长从0开始累计
- 实时更新显示
- **进度条:**
- 根据心情值实时更新进度
- 视觉反馈摇晃强度
- **小鸡形象:**
- 根据心情类型显示对应图标
- 暂不根据心情值变化(与详情页保持一致)
#### 3. 结束按钮
- **位置:** 页面底部
- **功能:** 点击跳转到编辑页面
- **数据传递:** 携带心情类型、心情值、时间等信息
#### 4. 遮罩引导系统
- **第一阶段0-2秒** 显示"握紧手机"
- **第二阶段2-4秒** 显示"开始摇晃,记录心情"
- **第三阶段4秒后** 遮罩自动消失
### 🔧 技术实现要点
#### 传感器监听
- **加速度传感器:** 监听手机摇晃动作
- **摇晃强度计算:** 根据传感器数据计算摇晃强度
- **阈值过滤:** 只有达到一定强度的摇晃才累计心情值
- **错误处理:** 无传感器时Toast提示"无传感器"
#### 心情值计算
- **累计逻辑:** 根据摇晃强度和持续时间累计心情值
- **上限控制:** 最高累计到100不再增加
- **实时更新:** 心情值和进度条实时反馈
#### 数据流传递
- **首页→添加页:** 心情类型数据通过Intent传递
- **添加页→编辑页:** 心情类型、心情值、时间数据传递
- **编辑页→首页:** 保存后返回首页并自动定位到新记录
### 📝 页面状态管理
#### 初始状态
- 遮罩显示"握紧手机"
- 心情值显示0
- 进度条为空
- 小鸡形象根据传入的心情类型显示
#### 准备状态
- 遮罩显示"开始摇晃,记录心情"
- 其他UI元素保持初始状态
#### 摇晃检测状态
- 遮罩消失
- 文案显示"开始!"
- 等待用户摇晃动作
#### 摇晃进行状态
- 实时更新心情文案(程度+心情类型)
- 心情值实时累计
- 进度条实时更新
- 小鸡形象保持不变
#### 完成状态
- 用户点击结束按钮
- 携带所有数据跳转到编辑页面
### 🚨 注意事项
1. **传感器权限:** 需要确保应用有传感器使用权限
2. **性能优化:** 传感器数据更新频率需要合理控制,避免过度消耗电量
3. **用户体验:** 摇晃检测灵敏度需要平衡,既不能太敏感也不能太迟钝
4. **数据一致性:** 确保整个流程中数据传递的准确性和完整性
---
*此文档将随着开发进展持续更新,记录重要的技术决策和进度变化。* *此文档将随着开发进展持续更新,记录重要的技术决策和进度变化。*

Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB