app/src/main/assets/calendar-heatmap.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/MainActivity.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/activity/SettingActivity.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/res/layout/activity_phone_login.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/res/layout/activity_setting.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/res/layout/layout_week_stats.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
app/src/main/res/values/dimens.xml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
app/src/main/assets/calendar-heatmap.html
对比新文件 @@ -0,0 +1,124 @@ <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width,height=device-height"> <title>水平日历色块图</title> <style>::-webkit-scrollbar{display:none;}html,body{overflow:hidden;height:100%;margin:0;}</style> </head> <body> <div id="mountNode"></div> <script>/*Fixing iframe window.innerHeight 0 issue in Safari*/document.body.clientHeight;</script> <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.g2-3.5.1/dist/g2.min.js"></script> <script src="https://gw.alipayobjects.com/os/antv/pkg/_antv.data-set-0.10.1/dist/data-set.min.js"></script> <script src="https://gw.alipayobjects.com/os/antv/assets/lib/jquery-3.2.1.min.js"></script> <script> var Shape = G2.Shape; var Util = G2.Util; Shape.registerShape('polygon', 'boundary-polygon', { draw: function draw(cfg, container) { if (!Util.isEmpty(cfg.points)) { var attrs = { stroke: '#fff', lineWidth: 1, fill: cfg.color, fillOpacity: cfg.opacity }; var points = cfg.points; var path = [['M', points[0].x, points[0].y], ['L', points[1].x, points[1].y], ['L', points[2].x, points[2].y], ['L', points[3].x, points[3].y], ['Z']]; attrs.path = this.parsePath(path); var polygon = container.addShape('path', { attrs: attrs }); if (cfg.origin._origin.lastWeek) { var linePath = [['M', points[2].x, points[2].y], ['L', points[3].x, points[3].y]]; // 最后一周的多边形添加右侧边框 container.addShape('path', { zIndex: 1, attrs: { path: this.parsePath(linePath), lineWidth: 1, stroke: '#404040' } }); if (cfg.origin._origin.lastDay) { container.addShape('path', { zIndex: 1, attrs: { path: this.parsePath([['M', points[1].x, points[1].y], ['L', points[2].x, points[2].y]]), lineWidth: 1, stroke: '#404040' } }); } } container.sort(); return polygon; } } }); $.getJSON('https://gw.alipayobjects.com/os/antvdemo/assets/data/github-commit.json', function(data) { var chart = new G2.Chart({ container: 'mountNode', forceFit: true, height: window.innerHeight, padding: [window.innerHeight / 3, 20, window.innerHeight / 3, 80] }); chart.source(data, { day: { type: 'cat', values: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'] }, week: { type: 'cat' }, commits: { sync: true } }); chart.axis('week', { position: 'top', tickLine: null, line: null, label: { offset: 12, textStyle: { fontSize: 12, fill: '#666', textBaseline: 'top' }, formatter: function formatter(val) { if (val === '2') { return 'MAY'; } else if (val === '6') { return 'JUN'; } else if (val === '10') { return 'JUL'; } else if (val === '15') { return 'AUG'; } else if (val === '19') { return 'SEP'; } else if (val === '24') { return 'OCT'; } return ''; } } }); chart.axis('day', { grid: null }); chart.legend(false); chart.tooltip({ title: 'date' }); chart.coord().reflect('y'); chart.polygon().position('week*day*date').color('commits', '#BAE7FF-#1890FF-#0050B3').shape('boundary-polygon'); chart.render(); }); </script> </body> </html> app/src/main/java/com/example/firstapp/MainActivity.kt
@@ -14,6 +14,7 @@ import android.Manifest import android.content.Context import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Build import androidx.annotation.RequiresApi @@ -31,6 +32,7 @@ import java.util.Calendar import java.util.Date import java.util.Locale import android.app.AlertDialog class MainActivity : AppCompatActivity() { // 安全防护关键词数组 @@ -40,23 +42,41 @@ private var smsReceiver: SmsReceiver? = null private val multiplePermissionRequest = // 短信权限请求 private val smsPermissionRequest = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions -> when { permissions.getOrDefault( Manifest.permission.RECEIVE_SMS, false ) && permissions.getOrDefault(Manifest.permission.READ_SMS, false) -> { // 两个权限都获得授权 Toast.makeText(this, "短信权限已授予", Toast.LENGTH_SHORT).show() registerSmsReceiver() // 在Android O及以上版本同步最近短信 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { syncRecentSms() } } else -> { // 有权限被拒绝 Toast.makeText( this, "需要短信读取和接收权限才能正常使用功能", Toast.LENGTH_SHORT ).show() showSmsPermissionExplanationDialog() } } } // 通知权限请求 private val notificationPermissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { // 权限已授予 Toast.makeText(this, "通知权限已授予,您将能收到重要提醒", Toast.LENGTH_SHORT).show() } else { // 权限被拒绝 Toast.makeText(this, "通知权限被拒绝,应用将无法发送提醒通知", Toast.LENGTH_SHORT).show() } // 无论通知权限是否授予,都继续请求短信权限 requestSmsPermissions() } private val syncLock = Object() @@ -76,39 +96,122 @@ // 重置提醒计划并检查是否有错过的提醒 resetReminders() // 开始权限请求流程 startPermissionsFlow() } // 检查权限 if (ContextCompat.checkSelfPermission( this, Manifest.permission.RECEIVE_SMS ) != android.content.pm.PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission( this, Manifest.permission.READ_SMS ) != android.content.pm.PackageManager.PERMISSION_GRANTED ) { // 同时请求两个权限 multiplePermissionRequest.launch( override fun onResume() { super.onResume() // 每次恢复活动时检查短信权限 checkAndHandleSmsPermissions(showDialog = false) } // 权限请求主流程 private fun startPermissionsFlow() { // 先请求通知权限,再请求短信权限 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // Android 13+ 需要通知权限 if (ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS) } else { // 已有通知权限,直接请求短信权限 requestSmsPermissions() } } else { // 低版本Android无需请求通知权限,直接请求短信权限 requestSmsPermissions() } } // 请求短信权限 private fun requestSmsPermissions() { if (!hasSmsPermissions()) { // 没有短信权限,请求权限 smsPermissionRequest.launch( arrayOf( Manifest.permission.RECEIVE_SMS, Manifest.permission.READ_SMS ) ) } else { // 权限已经授予,继续执行相关操作 // 已有短信权限,直接初始化短信处理 registerSmsReceiver() syncRecentSms() // 在Android O及以上版本同步最近短信 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { syncRecentSms() } } } // 检查并处理短信权限(可选是否显示对话框) private fun checkAndHandleSmsPermissions(showDialog: Boolean = true) { if (!hasSmsPermissions()) { if (showDialog) { showSmsPermissionExplanationDialog() } } else { // 已有短信权限,确保接收器已注册 if (smsReceiver == null) { registerSmsReceiver() } } } // 检查是否有短信权限 private fun hasSmsPermissions(): Boolean { return ContextCompat.checkSelfPermission( this, Manifest.permission.RECEIVE_SMS ) == PackageManager.PERMISSION_GRANTED && ContextCompat.checkSelfPermission( this, Manifest.permission.READ_SMS ) == PackageManager.PERMISSION_GRANTED } // 显示短信权限解释对话框 private fun showSmsPermissionExplanationDialog() { AlertDialog.Builder(this) .setTitle("需要短信权限") .setMessage("应用需要短信权限来自动处理和分类您的短信信息,包括快递、付款等内容。没有此权限,应用核心功能将无法正常工作。") .setPositiveButton("授予权限") { _, _ -> requestSmsPermissions() } .setNegativeButton("暂时不要") { dialog, _ -> dialog.dismiss() Toast.makeText(this, "您可以稍后在设置中开启短信权限", Toast.LENGTH_LONG).show() } .setCancelable(false) .show() } // 提供给外部调用的请求通知权限方法 fun requestNotificationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // 判断是否已经有通知权限 if (ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS) } else { Toast.makeText(this, "已经拥有通知权限", Toast.LENGTH_SHORT).show() } } else { // Android 13以下的版本不需要请求权限 Toast.makeText(this, "当前系统版本无需单独请求通知权限", Toast.LENGTH_SHORT).show() } } private fun registerSmsReceiver() { // 应用启动时执行 registerSmsReceiver() // 创建 SmsReceiver 实例 // 注册广播接收器,开始监听短信 // 等待新短信到达 // 新短信到达时,系统发送广播 // SmsReceiver 的 onReceive 方法被调用 // 处理短信内容 // 发送数据更新广播 // MainActivity 接收到更新广播 // 更新 UI Log.d("SMS_DEBUG", "MainActivity收到数据更新广播") // 确保不重复注册 if (smsReceiver != null) return Log.d("SMS_DEBUG", "MainActivity注册短信接收器") smsReceiver = SmsReceiver() val filter = IntentFilter(Telephony.Sms.Intents.SMS_RECEIVED_ACTION) registerReceiver(smsReceiver, filter) app/src/main/java/com/example/firstapp/activity/SettingActivity.kt
@@ -10,6 +10,12 @@ import android.widget.Button import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope import android.os.Build import android.Manifest import android.content.Context import android.content.pm.PackageManager import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import com.example.firstapp.databinding.ActivitySettingBinding import com.example.firstapp.R @@ -30,6 +36,26 @@ ViewModelProvider(this).get(HomeViewModel::class.java) } private var isFullscreen: Boolean = false // 添加通知权限请求 private val notificationPermissionRequest = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> if (isGranted) { // 权限已授予,可以发送通知 AlertDialog.Builder(this) .setTitle("通知权限") .setMessage("通知权限已开启,您将能收到重要提醒") .setPositiveButton("确定", null) .show() } else { // 权限被拒绝 AlertDialog.Builder(this) .setTitle("通知权限") .setMessage("通知权限被拒绝,应用将无法发送提醒通知。您可以在系统设置中手动开启权限。") .setPositiveButton("确定", null) .show() } } @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { @@ -65,6 +91,8 @@ // }) // 通知权限 setupNotificationPermission() // 退出登录 logout() // 账号注销 @@ -76,6 +104,35 @@ } private fun setupNotificationPermission() { binding.notificationPermission.setOnClickListener { // 请求通知权限(在Android 13及以上需要) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { if (ContextCompat.checkSelfPermission( this, Manifest.permission.POST_NOTIFICATIONS ) != PackageManager.PERMISSION_GRANTED ) { notificationPermissionRequest.launch(Manifest.permission.POST_NOTIFICATIONS) } else { // 已经有权限 AlertDialog.Builder(this) .setTitle("通知权限") .setMessage("您已经开启了通知权限") .setPositiveButton("确定", null) .show() } } else { // Android 13以下版本不需要请求权限 AlertDialog.Builder(this) .setTitle("通知权限") .setMessage("当前系统版本无需单独请求通知权限") .setPositiveButton("确定", null) .show() } } } private fun aboutCompany(){ binding.aboutCompany.setOnClickListener { app/src/main/java/com/example/firstapp/ui/dashboard/DashboardFragment.kt
@@ -1,37 +1,40 @@ 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() { @@ -57,6 +60,7 @@ } private val viewModel: DashboardViewModel by viewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -74,6 +78,14 @@ 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() @@ -763,7 +775,242 @@ 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 @@ -789,8 +1036,10 @@ } // 添加月份标签 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 @@ -898,6 +1147,154 @@ } } 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") // 最淡 app/src/main/res/layout/activity_phone_login.xml
@@ -66,7 +66,7 @@ android:layout_weight="1" android:background="@null" android:hint="请输入验证码" android:text="888888" android:text="" android:inputType="number" android:maxLength="6" android:textSize="16sp" app/src/main/res/layout/activity_setting.xml
@@ -35,7 +35,7 @@ </androidx.appcompat.widget.Toolbar> <LinearLayout android:id="@+id/about_company" android:id="@+id/notification_permission" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" @@ -48,6 +48,24 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:text="开启通知权限" android:textColor="#000000" /> </LinearLayout> <LinearLayout android:id="@+id/about_company" android:layout_width="match_parent" android:layout_height="50dp" android:gravity="center" android:orientation="vertical" android:background="#F9F9F9" android:layout_marginTop="1dp" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:padding="5dp" android:text="关于我们" android:textColor="#000000" /> </LinearLayout> app/src/main/res/layout/layout_week_stats.xml
@@ -1,5 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -62,6 +63,14 @@ </androidx.cardview.widget.CardView> <!-- <WebView--> <!-- android:id="@+id/webView"--> <!-- android:layout_width="match_parent"--> <!-- android:layout_height="300dp"--> <!-- tools:ignore="WebViewLayout" />--> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" app/src/main/res/values/dimens.xml
@@ -4,7 +4,7 @@ <dimen name="activity_vertical_margin">16dp</dimen> <dimen name="list_item_spacing">16dp</dimen> <dimen name="list_item_spacing_half">8dp</dimen> <dimen name="heatmap_cell_size">12dp</dimen> <dimen name="heatmap_cell_size">25dp</dimen> <dimen name="fab_margin">16dp</dimen> <!-- 徽章尺寸 -->