parent
ccedbc10df
commit
41c896ed7d
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,2 @@
|
||||
#Thu Jun 05 00:28:15 CST 2025
|
||||
java.home=/Applications/Android Studio.app/Contents/jbr/Contents/Home
|
Binary file not shown.
@ -1,17 +1,77 @@
|
||||
package org.tensorflow.lite.examples.poseestimation
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.fragment.app.Fragment
|
||||
import org.tensorflow.lite.examples.poseestimation.R
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
|
||||
|
||||
class DataFragment : Fragment() {
|
||||
|
||||
private lateinit var recyclerView: RecyclerView
|
||||
private lateinit var adapter: VideoAnalysisAdapter
|
||||
private var currentUsername: String? = null
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
android.util.Log.d("DataFragment", "onCreateView called")
|
||||
return inflater.inflate(R.layout.fragment_data, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
android.util.Log.d("DataFragment", "onViewCreated called")
|
||||
|
||||
// 获取当前登录用户名
|
||||
val prefs = requireContext().getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
|
||||
currentUsername = prefs.getString("username", null)?.trim()
|
||||
android.util.Log.d("DataFragment", "Current Username: $currentUsername")
|
||||
|
||||
if (currentUsername == null) {
|
||||
Toast.makeText(requireContext(), "未登录,无法查看数据", Toast.LENGTH_SHORT).show()
|
||||
android.util.Log.w("DataFragment", "Current username is null, cannot load data.")
|
||||
return
|
||||
}
|
||||
|
||||
recyclerView = view.findViewById(R.id.recycler_view_video_analysis_results)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
|
||||
loadVideoAnalysisResults()
|
||||
}
|
||||
|
||||
private fun loadVideoAnalysisResults() {
|
||||
android.util.Log.d("DataFragment", "loadVideoAnalysisResults called for username: $currentUsername")
|
||||
currentUsername?.let { username ->
|
||||
lifecycleScope.launch {
|
||||
val db = AppDatabase.getDatabase(requireContext())
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
db.videoAnalysisResultDao().getVideoAnalysisResultsByUsername(username).firstOrNull()
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (results != null && results.isNotEmpty()) {
|
||||
android.util.Log.d("DataFragment", "Loaded ${results.size} video analysis results.")
|
||||
adapter = VideoAnalysisAdapter(results)
|
||||
recyclerView.adapter = adapter
|
||||
} else {
|
||||
// 没有数据,可以显示一个提示
|
||||
Toast.makeText(requireContext(), "暂无训练记录", Toast.LENGTH_SHORT).show()
|
||||
android.util.Log.d("DataFragment", "No video analysis results found for username: $username")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,18 +1,23 @@
|
||||
package org.tensorflow.lite.examples.poseestimation
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
|
||||
class OnboardingAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
class OnboardingAdapter(activity: FragmentActivity, private val username: String?) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = 3
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
return when (position) {
|
||||
val fragment = when (position) {
|
||||
0 -> Onboarding1Fragment()
|
||||
1 -> Onboarding2Fragment()
|
||||
2 -> Onboarding3Fragment()
|
||||
else -> Onboarding1Fragment()
|
||||
}
|
||||
fragment.arguments = Bundle().apply {
|
||||
putString("username", username)
|
||||
}
|
||||
return fragment
|
||||
}
|
||||
}
|
@ -0,0 +1,286 @@
|
||||
package org.tensorflow.lite.examples.poseestimation
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.provider.OpenableColumns
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.delay
|
||||
import org.tensorflow.lite.examples.poseestimation.data.AppDatabase
|
||||
import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
|
||||
import org.tensorflow.lite.examples.poseestimation.ml.MoveNet
|
||||
import org.tensorflow.lite.examples.poseestimation.ml.PoseDetector
|
||||
import org.tensorflow.lite.examples.poseestimation.ml.ModelType
|
||||
import org.tensorflow.lite.examples.poseestimation.data.Device
|
||||
import org.tensorflow.lite.examples.poseestimation.data.Person
|
||||
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.DeadliftEvaluator
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.SquatEvaluator
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.ExerciseEvaluator
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.PlankEvaluator
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.PullUpEvaluator
|
||||
import org.tensorflow.lite.examples.poseestimation.evaluator.PushUpEvaluator
|
||||
|
||||
class VideoAnalysisActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var tvExerciseName: TextView
|
||||
private lateinit var btnSelectVideo: Button
|
||||
private lateinit var tvAnalysisStatus: TextView
|
||||
private lateinit var progressBarAnalysis: ProgressBar
|
||||
|
||||
private var currentExerciseType: String? = null
|
||||
private var currentUsername: String? = null
|
||||
|
||||
private lateinit var poseDetector: PoseDetector
|
||||
|
||||
// 用于存储复制到内部存储后的视频URI
|
||||
private var internalVideoUri: Uri? = null
|
||||
|
||||
// Launcher for selecting video from local storage
|
||||
private val selectVideoLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val videoUri: Uri? = result.data?.data
|
||||
videoUri?.let {
|
||||
// 将视频复制到内部存储
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val copiedUri = copyVideoToInternalStorage(it)
|
||||
withContext(Dispatchers.Main) {
|
||||
if (copiedUri != null) {
|
||||
internalVideoUri = copiedUri
|
||||
android.util.Log.d("VideoAnalysisActivity", "视频已复制到内部存储: $internalVideoUri")
|
||||
tvAnalysisStatus.text = "已选择视频: ${getFileName(it)},开始分析..."
|
||||
tvAnalysisStatus.visibility = View.VISIBLE
|
||||
progressBarAnalysis.visibility = View.VISIBLE
|
||||
progressBarAnalysis.progress = 0
|
||||
startVideoAnalysis(internalVideoUri!!, currentExerciseType)
|
||||
} else {
|
||||
Toast.makeText(this@VideoAnalysisActivity, "视频复制失败,请重试", Toast.LENGTH_SHORT).show()
|
||||
tvAnalysisStatus.text = "视频复制失败"
|
||||
tvAnalysisStatus.visibility = View.VISIBLE
|
||||
progressBarAnalysis.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
} ?: run {
|
||||
Toast.makeText(this, "未选择视频", Toast.LENGTH_SHORT).show()
|
||||
tvAnalysisStatus.text = "未选择视频"
|
||||
tvAnalysisStatus.visibility = View.VISIBLE
|
||||
progressBarAnalysis.visibility = View.GONE
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, "取消选择视频", Toast.LENGTH_SHORT).show()
|
||||
tvAnalysisStatus.text = "取消选择视频"
|
||||
tvAnalysisStatus.visibility = View.VISIBLE
|
||||
progressBarAnalysis.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_video_analysis)
|
||||
|
||||
// 获取当前登录用户名 (假设用SharedPreferences存储)
|
||||
val prefs = getSharedPreferences("user_prefs", Context.MODE_PRIVATE)
|
||||
currentUsername = prefs.getString("username", null)?.trim()
|
||||
|
||||
if (currentUsername == null) {
|
||||
Toast.makeText(this, "未登录,请重新登录", Toast.LENGTH_SHORT).show()
|
||||
startActivity(Intent(this, LoginActivity::class.java))
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
// Get exercise name from intent
|
||||
currentExerciseType = intent.getStringExtra("current_exercise")
|
||||
|
||||
// Initialize views
|
||||
tvExerciseName = findViewById(R.id.tv_exercise_name)
|
||||
btnSelectVideo = findViewById(R.id.btn_select_video)
|
||||
tvAnalysisStatus = findViewById(R.id.tv_analysis_status)
|
||||
progressBarAnalysis = findViewById(R.id.progress_bar_analysis)
|
||||
|
||||
// Initialize PoseDetector (using MoveNet Lightning by default)
|
||||
poseDetector = MoveNet.create(this, Device.CPU, ModelType.Lightning)
|
||||
|
||||
// Set exercise name
|
||||
tvExerciseName.text = currentExerciseType ?: "未知动作"
|
||||
|
||||
// Set click listener for video selection button
|
||||
btnSelectVideo.setOnClickListener {
|
||||
openVideoPicker()
|
||||
}
|
||||
}
|
||||
|
||||
// Override onDestroy to close the poseDetector
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
poseDetector.close()
|
||||
}
|
||||
|
||||
private fun openVideoPicker() {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.type = "video/*"
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
selectVideoLauncher.launch(Intent.createChooser(intent, "选择视频"))
|
||||
}
|
||||
|
||||
// Helper function to get file name from Uri
|
||||
private fun getFileName(uri: Uri): String {
|
||||
var result: String? = null
|
||||
if (uri.scheme == "content") {
|
||||
val cursor = contentResolver.query(uri, null, null, null, null)
|
||||
cursor?.use {
|
||||
if (it.moveToFirst()) {
|
||||
val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (nameIndex != -1) {
|
||||
result = it.getString(nameIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == null) {
|
||||
result = uri.path
|
||||
val cut = result?.lastIndexOf('/')
|
||||
if (cut != -1) {
|
||||
result = result?.substring(cut!! + 1)
|
||||
}
|
||||
}
|
||||
return result ?: "未知文件"
|
||||
}
|
||||
|
||||
// 将视频复制到应用内部存储并返回新URI
|
||||
private fun copyVideoToInternalStorage(uri: Uri): Uri? {
|
||||
return try {
|
||||
val inputStream = contentResolver.openInputStream(uri)
|
||||
val outputFileName = "video_${System.currentTimeMillis()}.mp4"
|
||||
val outputFile = java.io.File(filesDir, outputFileName)
|
||||
val outputStream = outputFile.outputStream()
|
||||
|
||||
inputStream?.copyTo(outputStream)
|
||||
inputStream?.close()
|
||||
outputStream.close()
|
||||
android.util.Log.d("VideoAnalysisActivity", "视频已复制到: ${outputFile.absolutePath}")
|
||||
Uri.fromFile(outputFile)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("VideoAnalysisActivity", "复制视频到内部存储出错: ${e.message}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder for video analysis logic
|
||||
private fun startVideoAnalysis(videoUri: Uri, exerciseType: String?) {
|
||||
val db = AppDatabase.getDatabase(applicationContext)
|
||||
val videoAnalysisResultDao = db.videoAnalysisResultDao()
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
tvAnalysisStatus.text = "正在初始化姿态识别模型..."
|
||||
btnSelectVideo.isEnabled = false // Disable button during analysis
|
||||
}
|
||||
|
||||
val retriever = MediaMetadataRetriever()
|
||||
retriever.setDataSource(applicationContext, videoUri)
|
||||
val durationMs = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0
|
||||
|
||||
// Process frames at a reasonable interval, e.g., 10 frames per second for analysis
|
||||
val frameRate = 10 // frames per second
|
||||
val frameIntervalUs = 1_000_000L / frameRate // interval in microseconds
|
||||
|
||||
var currentFrameTimeUs = 0L
|
||||
|
||||
// Initialize evaluator based on exercise type
|
||||
val evaluator: ExerciseEvaluator? = when (exerciseType) {
|
||||
"硬拉" -> DeadliftEvaluator()
|
||||
"深蹲" -> SquatEvaluator()
|
||||
"平板支撑" -> PlankEvaluator()
|
||||
"引体向上" -> PullUpEvaluator()
|
||||
"俯卧撑" -> PushUpEvaluator()
|
||||
// TODO: Add evaluators for other exercises
|
||||
else -> null
|
||||
}
|
||||
|
||||
while (currentFrameTimeUs <= durationMs * 1000) { // durationMs is in milliseconds, getFrameAtTime expects microseconds
|
||||
val bitmap = retriever.getFrameAtTime(currentFrameTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
||||
|
||||
bitmap?.let { frameBitmap ->
|
||||
val persons = poseDetector.estimatePoses(frameBitmap)
|
||||
val person = persons.firstOrNull()
|
||||
|
||||
person?.let { p ->
|
||||
if (p.score > 0.3) { // Only consider poses with reasonable confidence
|
||||
// Pass keypoints to the evaluator
|
||||
evaluator?.evaluateFrame(p.keyPoints)
|
||||
}
|
||||
}
|
||||
frameBitmap.recycle() // Recycle bitmap to free memory
|
||||
}
|
||||
|
||||
val progress = ((currentFrameTimeUs.toFloat() / (durationMs * 1000)) * 100).toInt()
|
||||
withContext(Dispatchers.Main) {
|
||||
progressBarAnalysis.progress = progress
|
||||
tvAnalysisStatus.text = "正在分析视频帧: ${currentFrameTimeUs / 1000}ms / ${durationMs}ms"
|
||||
}
|
||||
|
||||
currentFrameTimeUs += frameIntervalUs
|
||||
}
|
||||
retriever.release()
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
tvAnalysisStatus.text = "姿态识别完成,正在评估..."
|
||||
}
|
||||
|
||||
// --- Pose Evaluation and Scoring ---
|
||||
|
||||
val finalScore: Float
|
||||
val evaluation: String
|
||||
|
||||
if (evaluator != null) {
|
||||
finalScore = evaluator.getFinalScore()
|
||||
evaluation = evaluator.getFinalEvaluation(finalScore)
|
||||
} else {
|
||||
finalScore = 0f
|
||||
evaluation = "暂不支持该动作的详细评估。"
|
||||
}
|
||||
|
||||
// Store result in database
|
||||
val result = VideoAnalysisResult(
|
||||
username = currentUsername ?: "未知用户",
|
||||
exerciseType = exerciseType ?: "未知运动",
|
||||
videoUri = videoUri.toString(), // 使用已经复制到内部存储的URI
|
||||
score = finalScore,
|
||||
evaluation = evaluation,
|
||||
timestamp = System.currentTimeMillis()
|
||||
)
|
||||
|
||||
videoAnalysisResultDao.insertVideoAnalysisResult(result)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
tvAnalysisStatus.text = "分析完成!正在显示结果..."
|
||||
progressBarAnalysis.visibility = View.GONE
|
||||
btnSelectVideo.isEnabled = true // Re-enable button
|
||||
|
||||
val intent = Intent(this@VideoAnalysisActivity, VideoAnalysisResultActivity::class.java).apply {
|
||||
putExtra("exercise_name", currentExerciseType)
|
||||
putExtra("score", finalScore)
|
||||
putExtra("evaluation", evaluation)
|
||||
}
|
||||
startActivity(intent)
|
||||
finish() // Finish this activity so user can't go back to it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
package org.tensorflow.lite.examples.poseestimation
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import androidx.core.content.FileProvider
|
||||
import org.tensorflow.lite.examples.poseestimation.data.VideoAnalysisResult
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
class VideoAnalysisAdapter(private val results: List<VideoAnalysisResult>) : RecyclerView.Adapter<VideoAnalysisAdapter.VideoAnalysisViewHolder>() {
|
||||
|
||||
class VideoAnalysisViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val tvExerciseType: TextView = itemView.findViewById(R.id.tv_exercise_type)
|
||||
val tvScore: TextView = itemView.findViewById(R.id.tv_score)
|
||||
val tvEvaluation: TextView = itemView.findViewById(R.id.tv_evaluation)
|
||||
val videoThumbnail: ImageView = itemView.findViewById(R.id.video_thumbnail)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoAnalysisViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_video_analysis_card, parent, false)
|
||||
return VideoAnalysisViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: VideoAnalysisViewHolder, position: Int) {
|
||||
val result = results[position]
|
||||
holder.tvExerciseType.text = "运动类型:${result.exerciseType}"
|
||||
holder.tvScore.text = "${result.score.toInt()} 分"
|
||||
holder.tvEvaluation.text = "评价:${result.evaluation}"
|
||||
|
||||
Glide.with(holder.itemView.context)
|
||||
.load(Uri.parse(result.videoUri))
|
||||
.placeholder(R.drawable.placeholder_image)
|
||||
.error(R.drawable.placeholder_image)
|
||||
.into(holder.videoThumbnail)
|
||||
|
||||
android.util.Log.d("VideoAnalysisAdapter", "Loading video thumbnail for URI: ${result.videoUri}")
|
||||
|
||||
holder.videoThumbnail.setOnClickListener {
|
||||
try {
|
||||
val videoFile = File(Uri.parse(result.videoUri).path)
|
||||
val context = holder.itemView.context
|
||||
|
||||
val contentUri: Uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
videoFile
|
||||
)
|
||||
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(contentUri, "video/*")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.e("VideoAnalysisAdapter", "无法播放视频: ${result.videoUri}, 错误: ${e.message}", e)
|
||||
Toast.makeText(holder.itemView.context, "无法播放视频,请检查文件或权限", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = results.size
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package org.tensorflow.lite.examples.poseestimation
|
||||
|
||||
import android.os.Bundle
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
||||
class VideoAnalysisResultActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_video_analysis_result)
|
||||
|
||||
val tvExerciseName = findViewById<TextView>(R.id.tv_result_exercise_name)
|
||||
val tvScore = findViewById<TextView>(R.id.tv_result_score)
|
||||
val tvEvaluation = findViewById<TextView>(R.id.tv_result_evaluation)
|
||||
|
||||
// 从 Intent 中获取数据
|
||||
val exerciseName = intent.getStringExtra("exercise_name")
|
||||
val score = intent.getFloatExtra("score", 0f)
|
||||
val evaluation = intent.getStringExtra("evaluation")
|
||||
|
||||
// 设置数据显示
|
||||
tvExerciseName.text = exerciseName ?: "未知动作"
|
||||
tvScore.text = String.format("%.1f", score) // 格式化得分,保留一位小数
|
||||
tvEvaluation.text = evaluation ?: "暂无评价"
|
||||
|
||||
// 设置标题 (可选)
|
||||
supportActionBar?.title = "分析结果"
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package org.tensorflow.lite.examples.poseestimation.data
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "video_analysis_results")
|
||||
data class VideoAnalysisResult(
|
||||
@PrimaryKey(autoGenerate = true) val id: Long = 0,
|
||||
val username: String,
|
||||
val exerciseType: String,
|
||||
val videoUri: String,
|
||||
val score: Float, // Simplified score for now
|
||||
val evaluation: String,
|
||||
val timestamp: Long // Timestamp for when the analysis was performed
|
||||
)
|
@ -0,0 +1,19 @@
|
||||
package org.tensorflow.lite.examples.poseestimation.data
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface VideoAnalysisResultDao {
|
||||
@Insert
|
||||
fun insertVideoAnalysisResult(result: VideoAnalysisResult): Long
|
||||
|
||||
@Query("SELECT * FROM video_analysis_results WHERE username = :username ORDER BY timestamp DESC")
|
||||
fun getVideoAnalysisResultsByUsername(username: String): Flow<List<VideoAnalysisResult>>
|
||||
|
||||
// Optional: Get results for a specific exercise type
|
||||
@Query("SELECT * FROM video_analysis_results WHERE username = :username AND exerciseType = :exerciseType ORDER BY timestamp DESC")
|
||||
fun getVideoAnalysisResultsByUsernameAndExerciseType(username: String, exerciseType: String): Flow<List<VideoAnalysisResult>>
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package org.tensorflow.lite.examples.poseestimation.evaluator
|
||||
|
||||
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
|
||||
|
||||
interface ExerciseEvaluator {
|
||||
fun evaluateFrame(keyPoints: List<KeyPoint>)
|
||||
fun getFinalScore(): Float
|
||||
fun getFinalEvaluation(finalScore: Float): String
|
||||
}
|
@ -0,0 +1,237 @@
|
||||
package org.tensorflow.lite.examples.poseestimation.evaluator
|
||||
|
||||
import org.tensorflow.lite.examples.poseestimation.data.KeyPoint
|
||||
import org.tensorflow.lite.examples.poseestimation.data.BodyPart
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
// 定义俯卧撑动作的阶段
|
||||
enum class PushUpState {
|
||||
START, // 起始姿态(手臂伸直)
|
||||
DESCENT, // 下降过程
|
||||
BOTTOM, // 底部姿态(胸部接近地面)
|
||||
ASCENT, // 上推过程
|
||||
LOCKOUT // 恢复到起始姿态
|
||||
}
|
||||
|
||||
class PushUpEvaluator : ExerciseEvaluator {
|
||||
|
||||
private var currentState: PushUpState = PushUpState.START
|
||||
private var totalScore: Float = 0f
|
||||
private var frameCount: Int = 0
|
||||
private var evaluationMessages: MutableList<EvaluationMessage> = mutableListOf()
|
||||
private var repCount: Int = 0
|
||||
private var isRepCounting: Boolean = false
|
||||
|
||||
// 俯卧撑关键角度阈值 (示例值,需要根据实际模型和标准进行调整)
|
||||
private val ELBOW_ANGLE_START_MIN = 170f // 起始姿态(手臂伸直,放宽要求)
|
||||
private val ELBOW_ANGLE_BOTTOM_MAX = 100f // 底部姿态(手肘弯曲程度,放宽要求)
|
||||
private val HIP_SHOULDER_ANKLE_MIN = 160f // 身体直线(髋-肩-踝)
|
||||
private val HIP_SHOULDER_ANKLE_MAX = 185f // 允许略微的弧度
|
||||
|
||||
// Helper function to calculate angle between three keypoints (A-B-C)
|
||||
// B is the vertex of the angle
|
||||
private fun calculateAngle(A: KeyPoint, B: KeyPoint, C: KeyPoint): Float {
|
||||
return B.abttPoints(A, B, C)
|
||||
}
|
||||
|
||||
override fun evaluateFrame(keyPoints: List<KeyPoint>) {
|
||||
if (keyPoints.isEmpty()) {
|
||||
evaluationMessages.add(EvaluationMessage("未检测到关键点,无法评估。", true))
|
||||
return
|
||||
}
|
||||
|
||||
frameCount++
|
||||
|
||||
// 获取俯卧撑所需关键点
|
||||
val leftShoulder = keyPoints.find { it.bodyPart == BodyPart.LEFT_SHOULDER }
|
||||
val rightShoulder = keyPoints.find { it.bodyPart == BodyPart.RIGHT_SHOULDER }
|
||||
val leftElbow = keyPoints.find { it.bodyPart == BodyPart.LEFT_ELBOW }
|
||||
val rightElbow = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ELBOW }
|
||||
val leftWrist = keyPoints.find { it.bodyPart == BodyPart.LEFT_WRIST }
|
||||
val rightWrist = keyPoints.find { it.bodyPart == BodyPart.RIGHT_WRIST }
|
||||
val leftHip = keyPoints.find { it.bodyPart == BodyPart.LEFT_HIP }
|
||||
val rightHip = keyPoints.find { it.bodyPart == BodyPart.RIGHT_HIP }
|
||||
val leftAnkle = keyPoints.find { it.bodyPart == BodyPart.LEFT_ANKLE }
|
||||
val rightAnkle = keyPoints.find { it.bodyPart == BodyPart.RIGHT_ANKLE }
|
||||
|
||||
// 确保所有关键点都存在
|
||||
if (leftShoulder == null || rightShoulder == null || leftElbow == null || rightElbow == null ||
|
||||
leftWrist == null || rightWrist == null || leftHip == null || rightHip == null ||
|
||||
leftAnkle == null || rightAnkle == null) {
|
||||
evaluationMessages.add(EvaluationMessage("关键点缺失,评估可能不准确。", true))
|
||||
return
|
||||
}
|
||||
|
||||
// 计算左右平均关键点,提高稳定性
|
||||
val midShoulder = KeyPoint(
|
||||
BodyPart.NOSE,
|
||||
android.graphics.PointF(
|
||||
(leftShoulder.coordinate.x + rightShoulder.coordinate.x) / 2,
|
||||
(leftShoulder.coordinate.y + rightShoulder.coordinate.y) / 2
|
||||
),
|
||||
(leftShoulder.score + rightShoulder.score) / 2
|
||||
)
|
||||
val midElbow = KeyPoint(
|
||||
BodyPart.NOSE,
|
||||
android.graphics.PointF(
|
||||
(leftElbow.coordinate.x + rightElbow.coordinate.x) / 2,
|
||||
(leftElbow.coordinate.y + rightElbow.coordinate.y) / 2
|
||||
),
|
||||
(leftElbow.score + rightElbow.score) / 2
|
||||
)
|
||||
val midWrist = KeyPoint(
|
||||
BodyPart.NOSE,
|
||||
android.graphics.PointF(
|
||||
(leftWrist.coordinate.x + rightWrist.coordinate.x) / 2,
|
||||
(leftWrist.coordinate.y + rightWrist.coordinate.y) / 2
|
||||
),
|
||||
(leftWrist.score + rightWrist.score) / 2
|
||||
)
|
||||
val midHip = KeyPoint(
|
||||
BodyPart.NOSE,
|
||||
android.graphics.PointF(
|
||||
(leftHip.coordinate.x + rightHip.coordinate.x) / 2,
|
||||
(leftHip.coordinate.y + rightHip.coordinate.y) / 2
|
||||
),
|
||||
(leftHip.score + rightHip.score) / 2
|
||||
)
|
||||
val midAnkle = KeyPoint(
|
||||
BodyPart.NOSE,
|
||||
android.graphics.PointF(
|
||||
(leftAnkle.coordinate.x + rightAnkle.coordinate.x) / 2,
|
||||
(leftAnkle.coordinate.y + rightAnkle.coordinate.y) / 2
|
||||
),
|
||||
(leftAnkle.score + rightAnkle.score) / 2
|
||||
)
|
||||
|
||||
// 计算核心角度
|
||||
val elbowAngle = calculateAngle(midShoulder, midElbow, midWrist) // 肩-肘-腕
|
||||
val bodyLineAngle = calculateAngle(midShoulder, midHip, midAnkle) // 肩-髋-踝
|
||||
|
||||
var frameScore = 0f
|
||||
var frameEvaluation = ""
|
||||
var isBodyStraight = true
|
||||
|
||||
// 检查身体是否呈直线
|
||||
if (bodyLineAngle !in HIP_SHOULDER_ANKLE_MIN..HIP_SHOULDER_ANKLE_MAX) {
|
||||
isBodyStraight = false
|
||||
if (midHip.coordinate.y < midShoulder.coordinate.y) { // 臀部过高
|
||||
evaluationMessages.add(EvaluationMessage("臀部抬得太高了,尝试放低臀部,保持身体呈一条直线。", true))
|
||||
} else if (midHip.coordinate.y > midAnkle.coordinate.y) { // 臀部下沉
|
||||
evaluationMessages.add(EvaluationMessage("臀部下沉了,收紧核心,将臀部向上抬起,保持身体的平直。", true))
|
||||
} else {
|
||||
evaluationMessages.add(EvaluationMessage("身体不够平直,请调整髋部位置。", true))
|
||||
}
|
||||
}
|
||||
|
||||
when (currentState) {
|
||||
PushUpState.START -> {
|
||||
// 评估起始姿态 (手臂伸直,身体呈直线)
|
||||
if (elbowAngle > ELBOW_ANGLE_START_MIN - 10 && isBodyStraight) { // 稍微放宽手臂伸直要求
|
||||
frameEvaluation = "起始姿态良好,手臂基本伸直,身体呈一条直线。"
|
||||
isRepCounting = false
|
||||
currentState = PushUpState.DESCENT
|
||||
frameScore = 200f // 提高分数
|
||||
} else {
|
||||
if (elbowAngle < ELBOW_ANGLE_START_MIN - 10) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
|
||||
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("身体未保持一条直线,请调整核心,不要塌腰或撅臀。", true))
|
||||
frameEvaluation = "请调整至正确起始姿态。"
|
||||
}
|
||||
}
|
||||
PushUpState.DESCENT -> {
|
||||
// 评估下降过程
|
||||
if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) { // 手肘开始弯曲
|
||||
if (isBodyStraight) {
|
||||
frameEvaluation = "下降得很稳,保持身体平直。"
|
||||
currentState = PushUpState.BOTTOM
|
||||
frameScore = 400f // 提高分数
|
||||
} else {
|
||||
evaluationMessages.add(EvaluationMessage("下降时身体不够平直,请调整核心,保持身体直线。", true))
|
||||
frameEvaluation = "下降中,注意保持身体直线。"
|
||||
}
|
||||
} else {
|
||||
frameEvaluation = "继续下降,感受胸部拉伸。"
|
||||
}
|
||||
}
|
||||
PushUpState.BOTTOM -> {
|
||||
// 评估底部姿态 (胸部接近地面,手肘弯曲到位,身体呈直线)
|
||||
if (elbowAngle < ELBOW_ANGLE_BOTTOM_MAX + 10 && isBodyStraight) { // 放宽手肘弯曲
|
||||
frameEvaluation = "底部姿态完美,胸部接近地面!"
|
||||
currentState = PushUpState.ASCENT
|
||||
frameScore = 600f // 提高分数
|
||||
} else {
|
||||
if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 10) evaluationMessages.add(EvaluationMessage("下降不够深,请尝试让胸部更接近地面。", true))
|
||||
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("底部姿态身体不直,请调整。", true))
|
||||
frameEvaluation = "底部姿态可改进。"
|
||||
}
|
||||
}
|
||||
PushUpState.ASCENT -> {
|
||||
// 评估上推过程
|
||||
if (elbowAngle > ELBOW_ANGLE_BOTTOM_MAX + 15) { // 手肘开始伸直
|
||||
if (isBodyStraight) {
|
||||
frameEvaluation = "上推有力,身体保持平直。"
|
||||
currentState = PushUpState.LOCKOUT
|
||||
frameScore = 400f // 提高分数
|
||||
} else {
|
||||
evaluationMessages.add(EvaluationMessage("上推时身体不够平直,请调整核心,保持身体直线。", true))
|
||||
frameEvaluation = "上推中,注意保持身体直线。"
|
||||
}
|
||||
} else {
|
||||
frameEvaluation = "继续上推,感受胸部和手臂发力。"
|
||||
}
|
||||
}
|
||||
PushUpState.LOCKOUT -> {
|
||||
// 评估完全推起(锁定)姿态
|
||||
if (elbowAngle > ELBOW_ANGLE_START_MIN - 15 && isBodyStraight) { // 稍微放宽手臂伸直要求
|
||||
frameEvaluation = "完美完成一次俯卧撑!手臂基本伸直,身体呈一条直线。"
|
||||
if (!isRepCounting) {
|
||||
repCount++
|
||||
isRepCounting = true
|
||||
evaluationMessages.add(EvaluationMessage("恭喜你,又完成了一次俯卧撑!目前累计完成了 $repCount 次,继续保持!"))
|
||||
}
|
||||
currentState = PushUpState.START // 完成一次后回到起始状态,准备下一次
|
||||
frameScore = 200f // 提高分数
|
||||
} else {
|
||||
if (elbowAngle < ELBOW_ANGLE_START_MIN - 15) evaluationMessages.add(EvaluationMessage("手臂未完全伸直,请确保回到起始位置。", true))
|
||||
if (!isBodyStraight) evaluationMessages.add(EvaluationMessage("锁定姿态身体不直,请调整。", true))
|
||||
frameEvaluation = "请完成锁定:手臂基本伸直,身体保持平直。"
|
||||
}
|
||||
}
|
||||
}
|
||||
totalScore += frameScore
|
||||
evaluationMessages.add(EvaluationMessage(frameEvaluation))
|
||||
}
|
||||
|
||||
override fun getFinalScore(): Float {
|
||||
val rawScore = if (frameCount > 0) totalScore / frameCount else 0f
|
||||
val roundedScore = (rawScore / 10.0f).roundToInt() * 10.0f
|
||||
return roundedScore.coerceIn(0f, 100f)
|
||||
}
|
||||
|
||||
override fun getFinalEvaluation(finalScore: Float): String {
|
||||
val uniqueMessages = evaluationMessages.map { it.message }.distinct().joinToString("\n")
|
||||
val errors = evaluationMessages.filter { it.isError }.map { it.message }.distinct()
|
||||
|
||||
val overallEvaluationBuilder = StringBuilder()
|
||||
|
||||
if (errors.isEmpty()) {
|
||||
when (finalScore.toInt()) {
|
||||
in 90..100 -> overallEvaluationBuilder.append("太棒了!你的俯卧撑动作几乎完美无瑕,姿态标准,力量十足!继续保持!")
|
||||
in 70..89 -> overallEvaluationBuilder.append("非常不错的俯卧撑!动作基本流畅,姿态也比较到位,再稍加注意细节就能更完美!")
|
||||
in 50..69 -> overallEvaluationBuilder.append("俯卧撑动作有进步空间哦!虽然有些地方做得不错,但还需要多练习,让姿态更稳定、发力更集中。")
|
||||
in 30..49 -> overallEvaluationBuilder.append("本次俯卧撑需要更多练习。动作中存在一些明显的姿态问题,这会影响训练效果和安全性。")
|
||||
else -> overallEvaluationBuilder.append("俯卧撑动作仍需大量改进。请务必仔细对照标准,从基础开始练习,避免受伤。")
|
||||
}
|
||||
} else {
|
||||
overallEvaluationBuilder.append("本次俯卧撑分析完成!发现了一些可以改进的地方:\n")
|
||||
overallEvaluationBuilder.append(errors.joinToString("\n"))
|
||||
overallEvaluationBuilder.append("\n总次数:$repCount")
|
||||
}
|
||||
|
||||
overallEvaluationBuilder.append("\n\n以下是本次训练的详细分析过程,希望能帮助你更好地理解和改进:\n")
|
||||
overallEvaluationBuilder.append(uniqueMessages)
|
||||
|
||||
return overallEvaluationBuilder.toString()
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#1C1C1E"
|
||||
tools:context=".VideoAnalysisActivity">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_exercise_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="32dp"
|
||||
android:text="动作名称"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/btn_select_video"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:text="选择本地视频"
|
||||
android:textColor="#FFFFFF"
|
||||
android:backgroundTint="#A020F0"
|
||||
android:textSize="18sp"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="24dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_analysis_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/btn_select_video"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginTop="24dp"
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="16sp"
|
||||
android:text="请选择视频进行分析..."
|
||||
android:visibility="gone"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar_analysis"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/tv_analysis_status"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:progressTint="#A020F0"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="#1C1C1E"
|
||||
tools:context=".VideoAnalysisResultActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_result_exercise_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="动作名称"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_result_score_label"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="最终得分: "
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="18sp"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_result_score"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="N/A"
|
||||
android:textColor="#A020F0"
|
||||
android:textSize="36sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginBottom="24dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_result_evaluation_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="详细评价: "
|
||||
android:textColor="#CCCCCC"
|
||||
android:textSize="18sp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_result_evaluation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="分析中..."
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:lineSpacingExtra="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
@ -1,14 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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="#1C1C1E">
|
||||
android:background="#1C1C1E"
|
||||
tools:context=".DataFragment">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="数据页面"
|
||||
android:textColor="#FFF"
|
||||
android:textSize="24sp"
|
||||
android:layout_gravity="center"/>
|
||||
</FrameLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view_video_analysis_results"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp"
|
||||
android:clipToPadding="false"
|
||||
android:background="#1C1C1E"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -0,0 +1,71 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="120dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="4dp"
|
||||
app:cardBackgroundColor="#222222">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/video_thumbnail"
|
||||
android:layout_width="100dp"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="centerCrop"
|
||||
android:src="@drawable/placeholder_image"
|
||||
android:background="#000000"
|
||||
android:contentDescription="视频缩略图"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_exercise_type"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="运动类型:硬拉"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_evaluation"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:maxLines="3"
|
||||
android:ellipsize="end"
|
||||
android:text="评价:动作完成度很高,但下放深度不足。请注意控制节奏。"
|
||||
android:textColor="#AAAAAA"
|
||||
android:textSize="13sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_score"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="85 分"
|
||||
android:textColor="#FFBB86FC"
|
||||
android:textSize="28sp"
|
||||
android:textStyle="bold"
|
||||
android:gravity="center_vertical"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<files-path name="my_videos" path="."/>
|
||||
</paths>
|
Loading…
Reference in new issue