| | |
| | | package com.example.firstapp.ui.dashboard |
| | | |
| | | import com.example.firstapp.R |
| | | import android.graphics.Color |
| | | import android.os.Bundle |
| | | import android.view.Gravity |
| | | import android.view.LayoutInflater |
| | | import android.view.View |
| | | import android.view.ViewGroup |
| | | import android.webkit.WebView |
| | | import android.widget.GridLayout |
| | | import android.widget.HorizontalScrollView |
| | | import android.widget.LinearLayout |
| | | import android.widget.TextView |
| | | import android.widget.Toast |
| | | import androidx.cardview.widget.CardView |
| | | import androidx.fragment.app.Fragment |
| | | import androidx.fragment.app.viewModels |
| | | import com.example.firstapp.databinding.FragmentDashboardBinding |
| | | import com.google.android.material.tabs.TabLayout |
| | | import androidx.lifecycle.lifecycleScope |
| | | import androidx.recyclerview.widget.LinearLayoutManager |
| | | import com.example.firstapp.R |
| | | import com.example.firstapp.adapter.PackageAdapter |
| | | import com.example.firstapp.database.response.UserInfo |
| | | import com.example.firstapp.database.service.RetrofitClient |
| | | import com.example.firstapp.databinding.FragmentDashboardBinding |
| | | import com.example.firstapp.model.DailyStat |
| | | import com.example.firstapp.utils.PreferencesManager |
| | | import com.github.mikephil.charting.charts.BarChart |
| | | import com.github.mikephil.charting.charts.PieChart |
| | | import com.github.mikephil.charting.components.Legend |
| | | import com.github.mikephil.charting.components.XAxis |
| | | import com.github.mikephil.charting.data.* |
| | | import com.github.mikephil.charting.formatter.ValueFormatter |
| | | import java.util.* |
| | | import java.text.SimpleDateFormat |
| | | import android.graphics.Color |
| | | import android.view.Gravity |
| | | import android.widget.GridLayout |
| | | import android.widget.LinearLayout |
| | | import android.widget.Toast |
| | | import androidx.cardview.widget.CardView |
| | | import androidx.lifecycle.lifecycleScope |
| | | import com.example.firstapp.database.response.UserInfo |
| | | import com.example.firstapp.database.service.RetrofitClient |
| | | import com.example.firstapp.model.DailyStat |
| | | import com.example.firstapp.utils.PreferencesManager |
| | | import com.google.android.material.tabs.TabLayout |
| | | import kotlinx.coroutines.launch |
| | | import java.text.SimpleDateFormat |
| | | import java.util.* |
| | | |
| | | |
| | | class DashboardFragment : Fragment() { |
| | | |
| | |
| | | } |
| | | private val viewModel: DashboardViewModel by viewModels() |
| | | |
| | | |
| | | override fun onCreateView( |
| | | inflater: LayoutInflater, |
| | | container: ViewGroup?, |
| | |
| | | |
| | | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
| | | super.onViewCreated(view, savedInstanceState) |
| | | |
| | | // val webView: WebView = binding.layoutWeekStats.webView |
| | | // val webSettings = webView.settings |
| | | // webSettings.javaScriptEnabled = true // 启用 JavaScript |
| | | // |
| | | // |
| | | // // 加载本地的 HTML 文件 |
| | | // webView.loadUrl("file:///android_asset/calendar-heatmap.html") |
| | | |
| | | //渲染包裹列表 |
| | | setupRecyclerView() |
| | |
| | | heatmapView.visibility = View.GONE |
| | | } |
| | | |
| | | |
| | | |
| | | // 用于创建图例 |
| | | private fun createLegend(): LinearLayout { |
| | | val legendLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.HORIZONTAL |
| | | gravity = Gravity.END or Gravity.CENTER_VERTICAL |
| | | setPadding(8, 16, 8, 0) |
| | | } |
| | | |
| | | val labelLow = TextView(context).apply { |
| | | text = "少" |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | legendLayout.addView(labelLow) |
| | | |
| | | val legendLevels = listOf(0, 5, 10, 15, 20) |
| | | legendLevels.forEach { level -> |
| | | val colorBox = View(context).apply { |
| | | setBackgroundColor(getHeatmapColor(level)) |
| | | val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | layoutParams = LinearLayout.LayoutParams(size, size).apply { |
| | | marginEnd = 4 |
| | | } |
| | | } |
| | | |
| | | val label = TextView(context).apply { |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | |
| | | legendLayout.addView(colorBox) |
| | | legendLayout.addView(label) |
| | | } |
| | | |
| | | val labelHigh = TextView(context).apply { |
| | | text = "多" |
| | | textSize = 10f |
| | | setPadding(8, 0, 0, 0) |
| | | } |
| | | legendLayout.addView(labelHigh) |
| | | |
| | | return legendLayout |
| | | } |
| | | |
| | | private fun updateHeatmapData() { |
| | | viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats -> |
| | | if (stats.isEmpty()) return@observe |
| | | |
| | | val heatmapMatrix = Array(7) { IntArray(52) } |
| | | stats.forEach { stat -> |
| | | val week = stat.weekOfYear - 1 |
| | | val dayOfWeek = stat.dayOfWeek - 1 |
| | | if (week in 0..51 && dayOfWeek in 0..6) { |
| | | heatmapMatrix[dayOfWeek][week] = stat.count |
| | | } |
| | | } |
| | | |
| | | binding.layoutWeekStats.heatmapYearly.apply { |
| | | removeAllViews() |
| | | |
| | | // 外层父布局:水平 LinearLayout,左固定周标签,右为横向滚动热力图 |
| | | val outerLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.HORIZONTAL |
| | | } |
| | | |
| | | // 左侧星期标签列 |
| | | val dayLabelLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.VERTICAL |
| | | val dayLabels = arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日") |
| | | // 顶部空白占位 |
| | | addView(TextView(context).apply { |
| | | text = "" |
| | | textSize = 20f |
| | | height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | }) |
| | | dayLabels.forEach { label -> |
| | | val textView = TextView(context).apply { |
| | | text = label |
| | | textSize = 10f |
| | | height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | setPadding(4, 0, 4, 0) |
| | | } |
| | | addView(textView) |
| | | } |
| | | } |
| | | |
| | | // 右侧滚动部分 |
| | | val scrollView = HorizontalScrollView(context).apply { |
| | | isHorizontalScrollBarEnabled = false |
| | | layoutParams = LinearLayout.LayoutParams( |
| | | ViewGroup.LayoutParams.MATCH_PARENT, |
| | | ViewGroup.LayoutParams.WRAP_CONTENT |
| | | ) |
| | | } |
| | | |
| | | val scrollContentLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.VERTICAL |
| | | } |
| | | |
| | | val gridLayout = GridLayout(context).apply { |
| | | rowCount = 8 |
| | | columnCount = 52 |
| | | } |
| | | |
| | | // 添加月份标签(估算) |
| | | val months = arrayOf( |
| | | "1月", "2月", "3月", "4月", "5月", "6月", |
| | | "7月", "8月", "9月", "10月", "11月", "12月" |
| | | ) |
| | | /* months.forEachIndexed { index, month -> |
| | | val label = TextView(context).apply { |
| | | text = month |
| | | textSize = 10f |
| | | setPadding(10, 0, 8, 4) |
| | | val weekPosition = (index * 4.3).toInt() |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | columnSpec = GridLayout.spec(weekPosition) |
| | | rowSpec = GridLayout.spec(0) |
| | | } |
| | | } |
| | | gridLayout.addView(label) |
| | | }*/ |
| | | months.forEachIndexed { index, month -> |
| | | val label = TextView(context).apply { |
| | | text = month |
| | | textSize = 10f |
| | | setPadding(10, 0, 8, 4) |
| | | |
| | | val weekPosition = (index * 4.3).toInt() |
| | | val span = 4 // 控制跨列范围 |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | columnSpec = GridLayout.spec(weekPosition, span, GridLayout.CENTER) |
| | | rowSpec = GridLayout.spec(0) |
| | | } |
| | | } |
| | | gridLayout.addView(label) |
| | | } |
| | | |
| | | // 添加热力格子 |
| | | for (day in 0..6) { |
| | | for (week in 0..51) { |
| | | val count = heatmapMatrix[day][week] |
| | | val cell = View(context).apply { |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | width = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | columnSpec = GridLayout.spec(week) |
| | | rowSpec = GridLayout.spec(day + 1) |
| | | setMargins(1, 1, 1, 1) |
| | | } |
| | | setBackgroundColor(getHeatmapColor(count)) |
| | | } |
| | | gridLayout.addView(cell) |
| | | } |
| | | } |
| | | |
| | | scrollContentLayout.addView(gridLayout) |
| | | scrollView.addView(scrollContentLayout) |
| | | |
| | | // 添加两个主要部分到外层布局 |
| | | outerLayout.addView(dayLabelLayout) |
| | | outerLayout.addView(scrollView) |
| | | |
| | | // 图例 |
| | | val legendLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.HORIZONTAL |
| | | gravity = Gravity.END or Gravity.CENTER_VERTICAL |
| | | setPadding(8, 16, 8, 0) |
| | | layoutParams = LinearLayout.LayoutParams( |
| | | ViewGroup.LayoutParams.MATCH_PARENT, |
| | | ViewGroup.LayoutParams.WRAP_CONTENT |
| | | ) |
| | | } |
| | | |
| | | val labelLow = TextView(context).apply { |
| | | text = "少" |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | legendLayout.addView(labelLow) |
| | | |
| | | val legendLevels = listOf(0, 5, 10, 15, 20) |
| | | legendLevels.forEach { level -> |
| | | val colorBox = View(context).apply { |
| | | setBackgroundColor(getHeatmapColor(level)) |
| | | val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | layoutParams = LinearLayout.LayoutParams(size, size).apply { |
| | | marginEnd = 4 |
| | | } |
| | | } |
| | | |
| | | val label = TextView(context).apply { |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | |
| | | legendLayout.addView(colorBox) |
| | | legendLayout.addView(label) |
| | | } |
| | | |
| | | val labelHigh = TextView(context).apply { |
| | | text = "多" |
| | | textSize = 10f |
| | | setPadding(8, 0, 0, 0) |
| | | } |
| | | legendLayout.addView(labelHigh) |
| | | |
| | | // 总容器垂直布局:热力图 + 图例 |
| | | val container = LinearLayout(context).apply { |
| | | orientation = LinearLayout.VERTICAL |
| | | addView(outerLayout) |
| | | addView(legendLayout) |
| | | } |
| | | |
| | | // addView(container) |
| | | |
| | | // 设置外部 margin |
| | | val params = LinearLayout.LayoutParams( |
| | | LinearLayout.LayoutParams.MATCH_PARENT, |
| | | LinearLayout.LayoutParams.WRAP_CONTENT |
| | | ).apply { |
| | | setMargins(16, 16, 16, 16) // 左、上、右、下的 margin,单位是像素 |
| | | } |
| | | |
| | | // 添加到父布局并应用 margin |
| | | addView(container, params) |
| | | |
| | | } |
| | | } |
| | | } |
| | | |
| | | |
| | | |
| | | private fun updateHeatmapData_a() { |
| | | viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats -> |
| | | if (stats.isEmpty()) return@observe |
| | | |
| | |
| | | } |
| | | |
| | | // 添加月份标签 |
| | | val months = arrayOf("1月", "2月", "3月", "4月", "5月", "6月", |
| | | "7月", "8月", "9月", "10月", "11月", "12月") |
| | | val months = arrayOf( |
| | | "1月", "2月", "3月", "4月", "5月", "6月", |
| | | "7月", "8月", "9月", "10月", "11月", "12月" |
| | | ) |
| | | months.forEachIndexed { index, month -> |
| | | val label = TextView(context).apply { |
| | | text = month |
| | |
| | | } |
| | | } |
| | | |
| | | private fun updateHeatmapData_aaa() { |
| | | viewModel.getYearlyHeatmap(currentDate.timeInMillis).observe(viewLifecycleOwner) { stats -> |
| | | if (stats.isEmpty()) return@observe |
| | | |
| | | // 创建52周x7天的数据矩阵 |
| | | val heatmapMatrix = Array(7) { IntArray(52) } |
| | | |
| | | // 填充数据 |
| | | stats.forEach { stat -> |
| | | val week = stat.weekOfYear - 1 // 0-51 |
| | | val dayOfWeek = stat.dayOfWeek - 1 // 0-6 |
| | | if (week in 0..51 && dayOfWeek in 0..6) { |
| | | heatmapMatrix[dayOfWeek][week] = stat.count |
| | | } |
| | | } |
| | | |
| | | // 更新UI |
| | | binding.layoutWeekStats.heatmapYearly.apply { |
| | | removeAllViews() |
| | | |
| | | val gridLayout = GridLayout(context).apply { |
| | | rowCount = 8 // 1行月份 + 7行星期 |
| | | columnCount = 53 // 1列星期 + 52列周数 |
| | | } |
| | | |
| | | // 添加月份标签 |
| | | val months = arrayOf("1月", "2月", "3月", "4月", "5月", "6月", |
| | | "7月", "8月", "9月", "10月", "11月", "12月") |
| | | months.forEachIndexed { index, month -> |
| | | val label = TextView(context).apply { |
| | | text = month |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 4) |
| | | val weekPosition = (index * 4.3).toInt() // 估算月份起始位置 |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | columnSpec = GridLayout.spec(weekPosition + 1) |
| | | rowSpec = GridLayout.spec(0) |
| | | } |
| | | } |
| | | gridLayout.addView(label) |
| | | } |
| | | |
| | | // 添加星期标签 |
| | | val dayLabels = arrayOf("周一", "周二", "周三", "周四", "周五", "周六", "周日") |
| | | dayLabels.forEachIndexed { index, label -> |
| | | val textView = TextView(context).apply { |
| | | text = label |
| | | textSize = 10f |
| | | setPadding(4, 0, 8, 0) |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | columnSpec = GridLayout.spec(0) |
| | | rowSpec = GridLayout.spec(index + 1) |
| | | } |
| | | } |
| | | gridLayout.addView(textView) |
| | | } |
| | | |
| | | // 添加热力图单元格 |
| | | for (day in 0..6) { |
| | | for (week in 0..51) { |
| | | val count = heatmapMatrix[day][week] |
| | | val cell = View(context).apply { |
| | | layoutParams = GridLayout.LayoutParams().apply { |
| | | width = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | height = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | columnSpec = GridLayout.spec(week + 1) |
| | | rowSpec = GridLayout.spec(day + 1) |
| | | setMargins(1, 1, 1, 1) |
| | | } |
| | | setBackgroundColor(getHeatmapColor(count)) |
| | | } |
| | | gridLayout.addView(cell) |
| | | } |
| | | } |
| | | |
| | | // ✅ 把 gridLayout 包裹进 HorizontalScrollView |
| | | val scrollView = HorizontalScrollView(context).apply { |
| | | layoutParams = LinearLayout.LayoutParams( |
| | | ViewGroup.LayoutParams.MATCH_PARENT, |
| | | ViewGroup.LayoutParams.WRAP_CONTENT |
| | | ) |
| | | isHorizontalScrollBarEnabled = true |
| | | } |
| | | scrollView.addView(gridLayout) |
| | | |
| | | |
| | | |
| | | |
| | | // 创建图例(Legend) |
| | | val legendLayout = LinearLayout(context).apply { |
| | | orientation = LinearLayout.HORIZONTAL |
| | | gravity = Gravity.END or Gravity.CENTER_VERTICAL // 靠右对齐 |
| | | setPadding(8, 16, 8, 0) |
| | | layoutParams = LinearLayout.LayoutParams( |
| | | ViewGroup.LayoutParams.MATCH_PARENT, |
| | | ViewGroup.LayoutParams.WRAP_CONTENT |
| | | ) |
| | | } |
| | | |
| | | // 左侧“少”标签 |
| | | val labelLow = TextView(context).apply { |
| | | text = "少" |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | legendLayout.addView(labelLow) |
| | | |
| | | // 渐变色块 + 数值 |
| | | val legendLevels = listOf(0, 5, 10, 15, 20) |
| | | legendLevels.forEach { level -> |
| | | val colorBox = View(context).apply { |
| | | setBackgroundColor(getHeatmapColor(level)) |
| | | val size = resources.getDimensionPixelSize(R.dimen.heatmap_cell_size) |
| | | layoutParams = LinearLayout.LayoutParams(size, size).apply { |
| | | marginEnd = 4 |
| | | } |
| | | } |
| | | |
| | | val label = TextView(context).apply { |
| | | // text = "$level" |
| | | textSize = 10f |
| | | setPadding(0, 0, 8, 0) |
| | | } |
| | | |
| | | legendLayout.addView(colorBox) |
| | | legendLayout.addView(label) |
| | | } |
| | | |
| | | // 右侧“多”标签 |
| | | val labelHigh = TextView(context).apply { |
| | | text = "多" |
| | | textSize = 10f |
| | | setPadding(8, 0, 0, 0) |
| | | } |
| | | legendLayout.addView(labelHigh) |
| | | |
| | | // 垂直组合 gridLayout + legendLayout |
| | | val container = LinearLayout(context).apply { |
| | | orientation = LinearLayout.VERTICAL |
| | | addView(scrollView) |
| | | addView(legendLayout) |
| | | } |
| | | |
| | | addView(container) |
| | | } |
| | | } |
| | | } |
| | | |
| | | private fun getHeatmapColor(count: Int): Int { |
| | | return when { |
| | | count == 0 -> Color.parseColor("#EBECF1") // 最淡 |