commit
e8644c9d7a
375 changed files with 17988 additions and 0 deletions
@ -0,0 +1,18 @@ |
|||
*.iml |
|||
.kotlin |
|||
.gradle |
|||
**/build/ |
|||
xcuserdata |
|||
!src/**/build/ |
|||
local.properties |
|||
.idea |
|||
.DS_Store |
|||
captures |
|||
.externalNativeBuild |
|||
.cxx |
|||
*.xcodeproj/* |
|||
!*.xcodeproj/project.pbxproj |
|||
!*.xcodeproj/xcshareddata/ |
|||
!*.xcodeproj/project.xcworkspace/ |
|||
!*.xcworkspace/contents.xcworkspacedata |
|||
**/xcshareddata/WorkspaceSettings.xcsettings |
@ -0,0 +1,14 @@ |
|||
This is a Kotlin Multiplatform project targeting Android, iOS. |
|||
|
|||
* `/composeApp` is for code that will be shared across your Compose Multiplatform applications. |
|||
It contains several subfolders: |
|||
- `commonMain` is for code that’s common for all targets. |
|||
- Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. |
|||
For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, |
|||
`iosMain` would be the right folder for such calls. |
|||
|
|||
* `/iosApp` contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, |
|||
you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. |
|||
|
|||
|
|||
Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)… |
@ -0,0 +1,10 @@ |
|||
plugins { |
|||
// this is necessary to avoid the plugins to be loaded multiple times |
|||
// in each subproject's classloader |
|||
alias(libs.plugins.androidApplication) apply false |
|||
alias(libs.plugins.androidLibrary) apply false |
|||
alias(libs.plugins.composeMultiplatform) apply false |
|||
alias(libs.plugins.composeCompiler) apply false |
|||
alias(libs.plugins.kotlinMultiplatform) apply false |
|||
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false |
|||
} |
@ -0,0 +1,135 @@ |
|||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat |
|||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi |
|||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget |
|||
|
|||
plugins { |
|||
alias(libs.plugins.kotlinMultiplatform) |
|||
alias(libs.plugins.androidApplication) |
|||
alias(libs.plugins.composeMultiplatform) |
|||
alias(libs.plugins.composeCompiler) |
|||
id("com.google.devtools.ksp") |
|||
id("kotlin-parcelize") |
|||
} |
|||
|
|||
kotlin { |
|||
androidTarget { |
|||
@OptIn(ExperimentalKotlinGradlePluginApi::class) |
|||
compilerOptions { |
|||
jvmTarget.set(JvmTarget.JVM_11) |
|||
} |
|||
} |
|||
|
|||
listOf( |
|||
iosX64(), |
|||
iosArm64(), |
|||
iosSimulatorArm64() |
|||
).forEach { iosTarget -> |
|||
iosTarget.binaries.framework { |
|||
baseName = "ComposeApp" |
|||
isStatic = true |
|||
} |
|||
} |
|||
|
|||
sourceSets { |
|||
|
|||
androidMain.dependencies { |
|||
implementation(compose.preview) |
|||
implementation(libs.androidx.activity.compose) |
|||
} |
|||
commonMain.dependencies { |
|||
implementation(compose.runtime) |
|||
implementation(compose.foundation) |
|||
implementation(compose.material3) |
|||
implementation(compose.ui) |
|||
implementation(compose.components.resources) |
|||
implementation(compose.components.uiToolingPreview) |
|||
implementation(libs.androidx.lifecycle.viewmodel) |
|||
implementation(libs.androidx.lifecycle.runtimeCompose) |
|||
} |
|||
commonTest.dependencies { |
|||
implementation(libs.kotlin.test) |
|||
} |
|||
} |
|||
} |
|||
|
|||
android { |
|||
namespace = "com.whitefish.app" |
|||
compileSdk = 35 |
|||
|
|||
defaultConfig { |
|||
applicationId = "com.whitefish.app" |
|||
minSdk = 27 |
|||
targetSdk = 35 |
|||
versionCode = 1 |
|||
versionName = "1.0" |
|||
|
|||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" |
|||
} |
|||
|
|||
signingConfigs { |
|||
create("release") { |
|||
storeFile = file("keystore.jks") |
|||
storePassword = "smart_ring" |
|||
keyAlias = "ring" |
|||
keyPassword = "smart_ring" |
|||
} |
|||
} |
|||
|
|||
buildFeatures { |
|||
dataBinding = true |
|||
} |
|||
|
|||
compileOptions { |
|||
sourceCompatibility = JavaVersion.VERSION_11 |
|||
targetCompatibility = JavaVersion.VERSION_11 |
|||
} |
|||
|
|||
buildTypes { |
|||
debug { |
|||
signingConfig = signingConfigs.findByName("release") |
|||
} |
|||
release { |
|||
isMinifyEnabled = true |
|||
signingConfig = signingConfigs.findByName("release") |
|||
proguardFiles( |
|||
getDefaultProguardFile("proguard-android-optimize.txt"), |
|||
"proguard-rules.pro" |
|||
) |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation(libs.androidx.room.runtime) |
|||
ksp(libs.androidx.room.compiler) |
|||
|
|||
implementation("com.github.ome450901:SimpleRatingBar:1.5.1") |
|||
implementation(libs.androidx.core.ktx) |
|||
implementation(libs.androidx.appcompat) |
|||
implementation(libs.material) |
|||
implementation(libs.androidx.room.ktx) |
|||
implementation("com.google.code.gson:gson:2.10.1") |
|||
testImplementation(libs.junit) |
|||
androidTestImplementation(libs.androidx.junit) |
|||
androidTestImplementation(libs.androidx.espresso.core) |
|||
|
|||
implementation(libs.immersionbar) |
|||
implementation(libs.immersionbar.ktx) |
|||
implementation(libs.glide) |
|||
implementation(libs.shadowLayout) |
|||
implementation(libs.mpandroidchart) |
|||
implementation(libs.androidx.fragment.ktx) |
|||
implementation(libs.flexbox) |
|||
implementation(libs.utilcodex) |
|||
implementation(libs.logger) |
|||
|
|||
implementation(fileTree("libs")) |
|||
implementation(project(":ecgAlgo")) |
|||
implementation(libs.android.database.sqlcipher) |
|||
} |
|||
} |
|||
|
|||
dependencies { |
|||
implementation(libs.lifecycle.viewmodel.compose) |
|||
debugImplementation(compose.uiTooling) |
|||
} |
|||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,97 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" |
|||
xmlns:tools="http://schemas.android.com/tools"> |
|||
|
|||
<!-- 蓝牙部分:对于低于Android 12的权限申请 --> |
|||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> |
|||
<uses-permission |
|||
android:name="android.permission.BLUETOOTH" |
|||
android:maxSdkVersion="30" /> |
|||
<uses-permission |
|||
android:name="android.permission.BLUETOOTH_ADMIN" |
|||
android:maxSdkVersion="30" /> |
|||
<uses-permission |
|||
android:name="android.permission.ACCESS_FINE_LOCATION" |
|||
android:maxSdkVersion="30" /> |
|||
<uses-permission |
|||
android:name="android.permission.ACCESS_COARSE_LOCATION" |
|||
android:maxSdkVersion="30" /> |
|||
|
|||
<!-- 蓝牙部分:对于高于Android 12的权限申请 --> |
|||
<uses-permission |
|||
android:name="android.permission.BLUETOOTH_SCAN" |
|||
android:usesPermissionFlags="neverForLocation" |
|||
tools:targetApi="s" /> |
|||
<uses-permission |
|||
android:name="android.permission.BLUETOOTH_CONNECT" |
|||
android:usesPermissionFlags="neverForLocation" |
|||
tools:targetApi="s" /> |
|||
|
|||
<uses-permission android:name="android.permission.INTERNET" /> |
|||
|
|||
<application |
|||
android:name=".Application" |
|||
android:allowBackup="true" |
|||
android:dataExtractionRules="@xml/data_extraction_rules" |
|||
android:fullBackupContent="@xml/backup_rules" |
|||
android:icon="@mipmap/ic_launcher" |
|||
android:label="@string/app_name" |
|||
android:roundIcon="@mipmap/ic_launcher_round" |
|||
android:supportsRtl="true" |
|||
android:theme="@style/Theme.RingApp" |
|||
tools:targetApi="31"> |
|||
|
|||
<activity |
|||
android:name=".feature.launcher.LauncherActivity" |
|||
android:exported="true"> |
|||
<intent-filter> |
|||
<category android:name="android.intent.category.LAUNCHER" /> |
|||
<action android:name="android.intent.action.MAIN" /> |
|||
</intent-filter> |
|||
</activity> |
|||
|
|||
<activity |
|||
android:name=".feature.login.LoginActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.connect.ConnectTipActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.userInfo.UserInfoActivity" |
|||
android:exported="false"> |
|||
|
|||
|
|||
</activity> |
|||
|
|||
<activity |
|||
android:name=".feature.devices.DeviceActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.home.HomeActivity" |
|||
android:exported="false" /> |
|||
|
|||
<activity |
|||
android:name=".feature.running.RunningActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.running.RunningHintActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.preference.PreferenceActivity" |
|||
android:exported="true" /> |
|||
<activity |
|||
android:name=".feature.plan.PlanActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.sleep.SleepDetailActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.sleep.SleepTypeActivity" |
|||
android:exported="false" /> |
|||
<activity |
|||
android:name=".feature.recovery.HrvAssessmentActivity" |
|||
android:exported="false" > |
|||
</activity> |
|||
</application> |
|||
|
|||
</manifest> |
@ -0,0 +1,32 @@ |
|||
package com.whitefish.app |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.app.Application |
|||
import com.orhanobut.logger.AndroidLogAdapter |
|||
import com.orhanobut.logger.Logger |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import com.whitefish.app.bt.BleManager |
|||
import com.whitefish.app.utils.ActivityLifecycleCb |
|||
|
|||
class Application : Application() { |
|||
val bleManager by lazy { |
|||
NexRingManager.init(this) |
|||
BleManager(this) |
|||
} |
|||
|
|||
val mActivityLifecycleCb = ActivityLifecycleCb() |
|||
|
|||
companion object { |
|||
@SuppressLint("StaticFieldLeak") |
|||
var INSTANTS: com.whitefish.app.Application? = null |
|||
private set |
|||
|
|||
} |
|||
|
|||
override fun onCreate() { |
|||
super.onCreate() |
|||
INSTANTS = this |
|||
Logger.addLogAdapter(AndroidLogAdapter()) |
|||
registerActivityLifecycleCallbacks(mActivityLifecycleCb) |
|||
} |
|||
} |
@ -0,0 +1,37 @@ |
|||
package com.whitefish.app |
|||
|
|||
import android.os.Bundle |
|||
import androidx.appcompat.app.AppCompatActivity |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.databinding.ViewDataBinding |
|||
import androidx.lifecycle.ViewModel |
|||
import androidx.lifecycle.ViewModelProvider |
|||
import com.gyf.immersionbar.ImmersionBar |
|||
|
|||
abstract class BaseActivity<VB : ViewDataBinding, VM : ViewModel> : AppCompatActivity() { |
|||
|
|||
protected val mBinding: VB by lazy { |
|||
DataBindingUtil.setContentView( |
|||
this, setLayout() |
|||
) |
|||
} |
|||
|
|||
protected val mViewModel: VM by lazy { |
|||
ViewModelProvider(this)[setViewModel()] |
|||
} |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
setContentView(mBinding.root) |
|||
|
|||
//状态栏透明 |
|||
ImmersionBar.with(this).transparentBar().init() |
|||
|
|||
bind() |
|||
} |
|||
|
|||
abstract fun setLayout(): Int |
|||
abstract fun setViewModel(): Class<VM> |
|||
abstract fun bind() |
|||
|
|||
} |
@ -0,0 +1,52 @@ |
|||
package com.whitefish.app |
|||
|
|||
import androidx.recyclerview.widget.DiffUtil |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
|
|||
abstract class BaseAdapter<T, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() { |
|||
|
|||
val data = arrayListOf<T>() |
|||
|
|||
open fun setData(newData: List<T>, diffUpdate: Boolean = true) { |
|||
if (diffUpdate) { |
|||
val diffResult = DiffUtil.calculateDiff(object : DiffUtil.Callback() { |
|||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = true |
|||
|
|||
override fun getOldListSize() = data.size |
|||
|
|||
override fun getNewListSize() = newData.size |
|||
|
|||
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = |
|||
data[oldItemPosition] == newData[newItemPosition] |
|||
|
|||
}) |
|||
data.clear() |
|||
data.addAll(newData) |
|||
diffResult.dispatchUpdatesTo(this) |
|||
} else { |
|||
data.clear() |
|||
data.addAll(newData) |
|||
notifyDataSetChanged() |
|||
} |
|||
} |
|||
|
|||
fun getItem(position: Int): T { |
|||
if (position >= data.size) { |
|||
return data.last() |
|||
} |
|||
return data[position] |
|||
} |
|||
|
|||
fun resetItem(index: Int, itemData: T) { |
|||
if (index == -1) { |
|||
return |
|||
} |
|||
if (index > data.size - 1) { |
|||
return |
|||
} |
|||
data[index] = itemData |
|||
notifyItemChanged(index, itemData) |
|||
} |
|||
|
|||
override fun getItemCount(): Int = data.size |
|||
} |
@ -0,0 +1,45 @@ |
|||
package com.whitefish.app |
|||
|
|||
import android.os.Bundle |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.databinding.ViewDataBinding |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.lifecycle.ViewModel |
|||
|
|||
abstract class BaseFragment<VB:ViewDataBinding,VM: ViewModel>:Fragment() { |
|||
protected lateinit var mViewModel: VM |
|||
|
|||
|
|||
private var _binding: VB? = null |
|||
protected val mBinding: VB |
|||
get() = _binding!! |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View? { |
|||
mViewModel = setViewModel() |
|||
_binding = DataBindingUtil.inflate(inflater,setLayout(),container,false) |
|||
return _binding!!.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
bind() |
|||
} |
|||
|
|||
override fun onDestroyView() { |
|||
super.onDestroyView() |
|||
//避免内存泄漏 |
|||
_binding = null |
|||
|
|||
} |
|||
abstract fun bind() |
|||
|
|||
abstract fun setLayout():Int |
|||
protected abstract fun setViewModel(): VM |
|||
} |
@ -0,0 +1,6 @@ |
|||
package com.whitefish.app |
|||
|
|||
import androidx.lifecycle.ViewModel |
|||
|
|||
abstract class BaseViewModel:ViewModel() { |
|||
} |
@ -0,0 +1,9 @@ |
|||
package com.whitefish.app |
|||
|
|||
import android.os.Build |
|||
|
|||
class AndroidPlatform : Platform { |
|||
override val name: String = "Android ${Build.VERSION.SDK_INT}" |
|||
} |
|||
|
|||
actual fun getPlatform(): Platform = AndroidPlatform() |
@ -0,0 +1,7 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class AnimalType ( |
|||
val animalName:String, |
|||
val coverUrl: Int, |
|||
val lastUpdate:String, |
|||
) |
@ -0,0 +1,5 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class BiologicalClock( |
|||
val tip: String |
|||
) |
@ -0,0 +1,14 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
import android.annotation.SuppressLint |
|||
import com.whitefish.app.bt.BleDevice |
|||
|
|||
data class Device( |
|||
val name:String, |
|||
val address:String, |
|||
) |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun BleDevice.toDevice(): Device{ |
|||
return Device(name = device.name, address = device.address) |
|||
} |
@ -0,0 +1,10 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class Evaluate ( |
|||
val score:Int, |
|||
val progress:Int, |
|||
val time:Int, |
|||
val benefit:Int, |
|||
val exercise:Int, |
|||
val efficiency:Int |
|||
) |
@ -0,0 +1,10 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
|
|||
data class Exercise( |
|||
val userName:String, |
|||
val userHeader:String, |
|||
val consume:Int, |
|||
val target:Int, |
|||
val exerciseState:List<Any> |
|||
) |
@ -0,0 +1,8 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class RecoverySleep( |
|||
val percentage: String, |
|||
val percentageAvg:String, |
|||
val totalTime:String, |
|||
val totalTimeAvg:String, |
|||
) |
@ -0,0 +1,6 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class SleepData( |
|||
val timeQuantum:Int, //数据持续时间(单位为 min) |
|||
val state:Int , |
|||
) |
@ -0,0 +1,101 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
import android.os.Parcelable |
|||
import com.whitefish.app.dao.bean.SleepDataBean |
|||
import com.whitefish.app.view.SleepSegment |
|||
import kotlinx.parcelize.Parcelize |
|||
import lib.linktop.nexring.api.SLEEP_STATE_DEEP |
|||
import lib.linktop.nexring.api.SLEEP_STATE_LIGHT |
|||
import lib.linktop.nexring.api.SLEEP_STATE_REM |
|||
import lib.linktop.nexring.api.SLEEP_STATE_WAKE |
|||
import java.util.Calendar |
|||
|
|||
@Parcelize |
|||
data class SleepState( |
|||
val date: String = "", |
|||
val totalTime: Long = 0L, |
|||
val sleepChartData: List<SleepSegment> = emptyList(), |
|||
val score: String = "", |
|||
val rating: Float = 0f, |
|||
var deepSleepTime: String = "", |
|||
var lightSleepTime: String = "", |
|||
var remSleepTime: String = "", |
|||
var wakeTime:String = "" |
|||
) : Parcelable{ |
|||
init { |
|||
val stateMap = getSleepStateTime() |
|||
|
|||
// 将分钟数转换为xx小时xx分钟格式的辅助函数 |
|||
fun formatMinutesToHourMinute(minutes: Float): String { |
|||
val totalMinutes = minutes.toInt() |
|||
val hours = totalMinutes / 60 |
|||
val remainingMinutes = totalMinutes % 60 |
|||
return if (hours > 0) { |
|||
"${hours}小时${remainingMinutes}分钟" |
|||
} else { |
|||
"${remainingMinutes}分钟" |
|||
} |
|||
} |
|||
|
|||
// 为各个睡眠阶段字段赋值 |
|||
deepSleepTime = formatMinutesToHourMinute(stateMap[SLEEP_STATE_DEEP] ?: 0f) |
|||
lightSleepTime = formatMinutesToHourMinute(stateMap[SLEEP_STATE_LIGHT] ?: 0f) |
|||
remSleepTime = formatMinutesToHourMinute(stateMap[SLEEP_STATE_REM] ?: 0f) |
|||
wakeTime = formatMinutesToHourMinute(stateMap[SLEEP_STATE_WAKE] ?: 0f) |
|||
} |
|||
} |
|||
|
|||
fun SleepDataBean.toState():SleepState{ |
|||
val cal = Calendar.getInstance().apply { |
|||
timeInMillis = date |
|||
} |
|||
|
|||
return SleepState( |
|||
"${cal.get(Calendar.MONTH) + 1}/${cal.get(Calendar.DAY_OF_MONTH)}", |
|||
this.totalSleepTime, |
|||
this.sleepSegments, |
|||
) |
|||
} |
|||
|
|||
//获取每个睡眠阶段的总分钟数 |
|||
fun SleepState.getSleepStateTime():HashMap<Int,Float>{ |
|||
var deepTime = 0F |
|||
var lightTime = 0F |
|||
var remTime = 0F |
|||
var wakeTime = 0F |
|||
this.sleepChartData.forEach { |
|||
when(it.state){ |
|||
SLEEP_STATE_WAKE -> { |
|||
wakeTime += it.durationMinutes |
|||
} |
|||
SLEEP_STATE_REM -> { |
|||
remTime += it.durationMinutes |
|||
} |
|||
SLEEP_STATE_DEEP -> { |
|||
deepTime += it.durationMinutes |
|||
} |
|||
SLEEP_STATE_LIGHT -> { |
|||
lightTime += it.durationMinutes |
|||
} |
|||
} |
|||
} |
|||
|
|||
return hashMapOf<Int, Float>().apply { |
|||
this[SLEEP_STATE_WAKE] = wakeTime |
|||
this[SLEEP_STATE_REM] = remTime |
|||
this[SLEEP_STATE_DEEP] = deepTime |
|||
this[SLEEP_STATE_LIGHT] = lightTime |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 零星小睡 |
|||
* @param start 睡眠开始时间 |
|||
* @param end 睡眠结束时间 |
|||
* @param duration 睡眠时长 |
|||
* */ |
|||
class SleepNap( |
|||
val start: Float = 0f, |
|||
val end: Float = 0f, |
|||
val duration: Long = 0L, |
|||
) |
@ -0,0 +1,29 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
import android.text.SpannableStringBuilder |
|||
import com.github.mikephil.charting.charts.LineChart |
|||
import com.github.mikephil.charting.data.BarDataSet |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
|
|||
data class UIRecoveryStateItem( |
|||
val icon: Int, |
|||
val title: String, |
|||
val description: String, |
|||
val lineData: LineDataSet?, |
|||
val barData: BarDataSet?, |
|||
val value: SpannableStringBuilder, |
|||
val viewType:ViewType, |
|||
val sleepData: SleepState? = null |
|||
){ |
|||
sealed class ViewType{ |
|||
data object SLEEP : ViewType() |
|||
data object HEAT : ViewType() |
|||
data object OXYGEN :ViewType() |
|||
data object PRESSURE: ViewType() |
|||
data object TEMPERATURE: ViewType() |
|||
} |
|||
} |
|||
|
|||
data class UIRecoveryState( |
|||
val data:List<UIRecoveryStateItem> |
|||
) |
@ -0,0 +1,13 @@ |
|||
package com.whitefish.app.bean |
|||
|
|||
data class UserInfo( |
|||
val sex:String? = null, |
|||
val birthday:String? = null, |
|||
val height:Double? = null, |
|||
val weight:Double? = null, |
|||
val target:String? = null, |
|||
val promote:String? = null, |
|||
val like:List<String> = emptyList(), |
|||
val wear:String? = null, |
|||
val handedness:String? = null, |
|||
) |
@ -0,0 +1,15 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
import android.bluetooth.BluetoothDevice |
|||
|
|||
data class BleDevice( |
|||
val device: BluetoothDevice, |
|||
val color: Int, |
|||
val size: Int, |
|||
val batteryState: Int? = null, |
|||
val batteryLevel: Int? = null, |
|||
/*val chipMode: Int = 0,*/ |
|||
val generation: Int? = null, |
|||
val sn: String? = null, |
|||
var rssi: Int, |
|||
) |
@ -0,0 +1,422 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.bluetooth.BluetoothDevice |
|||
import android.bluetooth.BluetoothGatt |
|||
import android.bluetooth.BluetoothGattDescriptor |
|||
import android.bluetooth.BluetoothManager |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.bluetooth.le.ScanCallback |
|||
import android.bluetooth.le.ScanResult |
|||
import android.bluetooth.le.ScanSettings |
|||
import android.content.Context |
|||
import android.content.pm.PackageManager |
|||
import android.os.Build |
|||
import androidx.appcompat.app.AlertDialog |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.utils.handlerRemove |
|||
import com.whitefish.app.utils.loge |
|||
import com.whitefish.app.utils.logi |
|||
import com.whitefish.app.utils.post |
|||
import com.whitefish.app.utils.postDelay |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import lib.linktop.nexring.api.NexRingBluetoothGattCallback |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_DECRYPT |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_FAILED_FOR_SN_NULL |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_START |
|||
import lib.linktop.nexring.api.OEM_AUTHENTICATION_SUCCESS |
|||
import lib.linktop.nexring.api.matchFromAdvertisementData |
|||
import lib.linktop.nexring.api.parseScanRecord |
|||
|
|||
|
|||
private const val OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS = 0 |
|||
private const val OEM_STEP_AUTHENTICATE_OEM = 1 |
|||
private const val OEM_STEP_TIMESTAMP_SYNC = 2 |
|||
private const val OEM_STEP_PROCESS_COMPLETED = 3 |
|||
|
|||
class BleManager(val app: Application) { |
|||
private val tag = "BleManager" |
|||
private val mBluetoothAdapter = |
|||
(app.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
|
|||
private val mOnBleConnectionListeners: MutableList<OnBleConnectionListener> = ArrayList() |
|||
|
|||
private var mOnBleScanCallback: OnBleScanCallback? = null |
|||
var bleGatt: BluetoothGatt? = null |
|||
private val scanDevMacList: MutableList<String> = ArrayList() |
|||
var isScanning = false |
|||
|
|||
private val mScanCallback = object : ScanCallback() { |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onScanResult(callbackType: Int, result: ScanResult) { |
|||
super.onScanResult(callbackType, result) |
|||
// loge( |
|||
// "JKL", |
|||
// "address ${result.device.address}, scanRecord.bytes ${result.scanRecord?.bytes.toByteArrayString()}" |
|||
// ) |
|||
synchronized(scanDevMacList) { |
|||
val scanRecord = result.scanRecord |
|||
if (scanRecord != null) { |
|||
val bytes = scanRecord.bytes |
|||
if (bytes.matchFromAdvertisementData()) { |
|||
val address = result.device.address |
|||
if (!scanDevMacList.contains(address).apply { |
|||
loge("scanDevMacList contains address($address) = ${!this}") |
|||
}) { |
|||
val bleDevice = bytes.parseScanRecord().run { |
|||
BleDevice( |
|||
result.device, color, size, |
|||
batteryState, batteryLevel, |
|||
/*chipMode,*/ generation, sn, |
|||
result.rssi |
|||
) |
|||
} |
|||
scanDevMacList.add(address) |
|||
mOnBleScanCallback?.apply { |
|||
post { |
|||
onScanning(bleDevice) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private val scanStopRunnable = Runnable { |
|||
cancelScan() |
|||
} |
|||
|
|||
var bleState = MutableStateFlow(0) |
|||
var connectedDevice: BluetoothDevice? = null |
|||
|
|||
private val _oemStepComplete = MutableStateFlow(false) |
|||
val oemStepComplete = _oemStepComplete.asStateFlow() |
|||
|
|||
private val mGattCallback = object : NexRingBluetoothGattCallback(NexRingManager.get()) { |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onConnectionStateChange( |
|||
gatt: BluetoothGatt, status: Int, newState: Int, |
|||
) { |
|||
super.onConnectionStateChange(gatt, status, newState) |
|||
loge( |
|||
tag, |
|||
"onConnectionStateChange->status:$status, newState:$newState" |
|||
) |
|||
when (newState) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
NexRingManager.get().apply { |
|||
setBleGatt(null) |
|||
unregisterRingService() |
|||
} |
|||
connectedDevice = null |
|||
gatt.close() |
|||
bleState.value = BluetoothProfile.STATE_DISCONNECTED |
|||
postBleState() |
|||
_oemStepComplete.value = false |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTING -> { |
|||
bleState.value = BluetoothProfile.STATE_CONNECTING |
|||
postBleState() |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
bleState.value = BluetoothProfile.STATE_CONNECTED |
|||
connectedDevice = gatt.device |
|||
postBleState() |
|||
// The default MTU for ATT in the core spec is 23 bytes, with 1 byte for ATT's Opcode, 2 bytes for ATT's Handle, and 20 bytes for GATT. |
|||
// So if you want to set 40, you should request the MTU to be set to 43. |
|||
gatt.requestMtu(40 + 3) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onMtuChanged(gatt: BluetoothGatt, mtu: Int, status: Int) { |
|||
super.onMtuChanged(gatt, mtu, status) |
|||
when (status) { |
|||
BluetoothGatt.GATT_SUCCESS -> { |
|||
loge(tag, "onMtuChanged success.") |
|||
gatt.discoverServices() |
|||
} |
|||
|
|||
BluetoothGatt.GATT_FAILURE -> { |
|||
loge(tag, "onMtuChanged failure.") |
|||
} |
|||
|
|||
else -> loge(tag, "onMtuChanged unknown status $status.") |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { |
|||
super.onServicesDiscovered(gatt, status) |
|||
loge(tag, "onServicesDiscovered(), status:${status}") |
|||
// Refresh device cache. This is the safest place to initiate the procedure. |
|||
if (status == BluetoothGatt.GATT_SUCCESS) { |
|||
NexRingManager.get().setBleGatt(gatt) |
|||
logi(tag, "onServicesDiscovered(), registerHealthData") |
|||
postDelay { |
|||
NexRingManager.get().registerRingService() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onDescriptorWrite( |
|||
gatt: BluetoothGatt, descriptor: BluetoothGattDescriptor, status: Int, |
|||
) { |
|||
super.onDescriptorWrite(gatt, descriptor, status) |
|||
if (status == BluetoothGatt.GATT_SUCCESS && |
|||
NexRingManager.get().isRingServiceRegistered() |
|||
) { |
|||
// post { |
|||
// //you need to synchronize the timestamp with the device first after |
|||
// //the the service registration is successful. |
|||
// NexRingManager.get() |
|||
// .settingsApi() |
|||
// .timestampSync(System.currentTimeMillis()) { |
|||
// synchronized(mOnBleConnectionListeners) { |
|||
// mOnBleConnectionListeners.forEach { |
|||
// it.onBleReady() |
|||
// } |
|||
// } |
|||
// } |
|||
// } |
|||
OemAuthenticationProcess().start() |
|||
} |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission", "ObsoleteSdkInt") |
|||
private fun connectInterval(device: BluetoothDevice) { |
|||
loge(tag, "connect gatt to ${device.address}") |
|||
bleGatt = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { |
|||
// device.connectGatt(context, false, gattCallback) |
|||
device.connectGatt(app, false, mGattCallback, BluetoothDevice.TRANSPORT_LE) |
|||
} else { |
|||
device.connectGatt(app, false, mGattCallback) |
|||
}.apply { connect() } |
|||
} |
|||
|
|||
fun isSupportBle(): Boolean = |
|||
// Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2 && |
|||
app.applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE) |
|||
|
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun startScan(timeoutMillis: Long, callback: OnBleScanCallback) { |
|||
isScanning = true |
|||
mOnBleScanCallback = callback |
|||
scanDevMacList.clear() |
|||
val scanSettings = ScanSettings.Builder() |
|||
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) |
|||
.build() |
|||
mBluetoothAdapter.bluetoothLeScanner.startScan(null, scanSettings, mScanCallback) |
|||
postDelay(scanStopRunnable, timeoutMillis) |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun cancelScan() { |
|||
if (isScanning) { |
|||
isScanning = false |
|||
mBluetoothAdapter.bluetoothLeScanner.stopScan(mScanCallback) |
|||
post { |
|||
mOnBleScanCallback?.onScanFinished() |
|||
mOnBleScanCallback = null |
|||
scanStopRunnable.handlerRemove() |
|||
} |
|||
} |
|||
scanDevMacList.clear() |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun connect(address: String): Boolean { |
|||
val remoteDevice = mBluetoothAdapter.getRemoteDevice(address) |
|||
loge("JKL", "connect to remoteDevice by address, ${remoteDevice.name}") |
|||
return if (!remoteDevice.name.isNullOrEmpty()) { |
|||
connect(remoteDevice) |
|||
true |
|||
} else { |
|||
loge("JKL", "reject, because it cannot connect success.") |
|||
false |
|||
} |
|||
} |
|||
|
|||
fun connect(device: BluetoothDevice) { |
|||
val delayConnect = isScanning |
|||
cancelScan() |
|||
if (delayConnect) { |
|||
loge("JKL", "connect to ${device.address}, delay 200L") |
|||
postDelay({ |
|||
loge("JKL", "delay finish, connect to ${device.address}") |
|||
connectInterval(device) |
|||
}, 200L) |
|||
} else { |
|||
loge("JKL", "connect to ${device.address} right now.") |
|||
connectInterval(device) |
|||
} |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun disconnect() { |
|||
bleGatt?.disconnect() |
|||
bleGatt = null |
|||
} |
|||
|
|||
|
|||
fun addOnBleConnectionListener(listener: OnBleConnectionListener) { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.add(listener) |
|||
} |
|||
} |
|||
|
|||
fun removeOnBleConnectionListener(listener: OnBleConnectionListener) { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.remove(listener) |
|||
} |
|||
} |
|||
|
|||
fun postBleState() { |
|||
post { |
|||
synchronized(mOnBleConnectionListeners) { |
|||
mOnBleConnectionListeners.forEach { |
|||
it.onBleState(bleState.value) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
inner class OemAuthenticationProcess : Thread() { |
|||
|
|||
private val innerTag = "OemAuthenticationProcess" |
|||
private val locked = Object() |
|||
private var step = OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS |
|||
|
|||
override fun run() { |
|||
while (step < OEM_STEP_PROCESS_COMPLETED) { |
|||
sleep(200L) |
|||
synchronized(locked) { |
|||
when (step) { |
|||
OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS -> { |
|||
loge(innerTag, "OEM_STEP_CHECK_OEM_AUTHENTICATION_STATUS") |
|||
NexRingManager.get().securityApi().checkOemAuthenticationStatus { |
|||
step = if (it) OEM_STEP_AUTHENTICATE_OEM else OEM_STEP_TIMESTAMP_SYNC |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
|
|||
OEM_STEP_AUTHENTICATE_OEM -> { |
|||
loge(innerTag, "OEM_STEP_AUTHENTICATE_OEM") |
|||
NexRingManager.get().securityApi().authenticateOem { result -> |
|||
when (result) { |
|||
OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 -> { |
|||
logi(innerTag, "OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_DECRYPT -> { |
|||
logi(innerTag, "OEM_AUTHENTICATION_FAILED_FOR_DECRYPT") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_SN_NULL -> { |
|||
logi(innerTag, "OEM_AUTHENTICATION_FAILED_FOR_SN_NULL") |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
result.showOemAuthFailDialog() |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_START -> { |
|||
logi(innerTag, "OEM_AUTHENTICATION_START") |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_SUCCESS -> { |
|||
logi(innerTag, "OEM_AUTHENTICATION_SUCCESS") |
|||
step = OEM_STEP_TIMESTAMP_SYNC |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
OEM_STEP_TIMESTAMP_SYNC -> { |
|||
loge(innerTag, "OEM_STEP_TIMESTAMP_SYNC") |
|||
NexRingManager.get() |
|||
.settingsApi() |
|||
.timestampSync(System.currentTimeMillis()) { |
|||
loge(innerTag, "OEM_STEP_TIMESTAMP_SYNC result $it") |
|||
synchronized(mOnBleConnectionListeners) { |
|||
post { |
|||
mOnBleConnectionListeners.forEach { listener -> |
|||
listener.onBleReady() |
|||
} |
|||
} |
|||
} |
|||
step = OEM_STEP_PROCESS_COMPLETED |
|||
synchronized(locked) { |
|||
locked.notify() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
locked.wait() |
|||
} |
|||
} |
|||
_oemStepComplete.value = true |
|||
loge(innerTag, "OEM_STEP_PROCESS_COMPLETED") |
|||
} |
|||
} |
|||
|
|||
private fun Int.showOemAuthFailDialog() { |
|||
app.mActivityLifecycleCb.currAct.apply { |
|||
if (this != null) { |
|||
val message = when (this@showOemAuthFailDialog) { |
|||
OEM_AUTHENTICATION_FAILED_FOR_SN_NULL -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_sn_null) |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_DECRYPT -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_r1_to_r2) |
|||
} |
|||
|
|||
OEM_AUTHENTICATION_FAILED_FOR_CHECK_R2 -> { |
|||
getString(R.string.dialog_msg_oem_auth_failed_cause_by_check_r2) |
|||
} |
|||
|
|||
else -> "Unknown error." |
|||
} |
|||
runOnUiThread { |
|||
AlertDialog.Builder(this) |
|||
.setCancelable(false) |
|||
.setTitle(R.string.dialog_title_oem_auth_failed) |
|||
.setMessage(message) |
|||
.setPositiveButton(R.string.btn_label_disconnected) { _, _ -> |
|||
disconnect() |
|||
}.create().show() |
|||
} |
|||
} else disconnect() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,8 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
interface OnBleConnectionListener { |
|||
|
|||
fun onBleState(state: Int) |
|||
|
|||
fun onBleReady() |
|||
} |
@ -0,0 +1,10 @@ |
|||
package com.whitefish.app.bt |
|||
|
|||
import com.whitefish.app.bt.BleDevice |
|||
|
|||
interface OnBleScanCallback { |
|||
|
|||
fun onScanning(result: BleDevice) |
|||
|
|||
fun onScanFinished() |
|||
} |
@ -0,0 +1,15 @@ |
|||
package com.whitefish.app.chart |
|||
|
|||
import com.github.mikephil.charting.formatter.ValueFormatter |
|||
|
|||
class DateValueFormatter: ValueFormatter() { |
|||
override fun getFormattedValue(value: Float): String { |
|||
val hour = value.toInt() |
|||
// 只在0,6,12,18,24小时处显示标签 |
|||
return if (hour % 6 == 0) { |
|||
String.format("%02d:00", hour) |
|||
} else { |
|||
"" // 其他位置不显示 |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,233 @@ |
|||
package com.whitefish.app.chart |
|||
|
|||
import android.graphics.Canvas |
|||
import com.github.mikephil.charting.charts.LineChart |
|||
import com.github.mikephil.charting.renderer.LineChartRenderer |
|||
import android.graphics.Color |
|||
import android.graphics.DashPathEffect |
|||
import android.graphics.LinearGradient |
|||
import android.graphics.Paint |
|||
import android.graphics.Path |
|||
import android.graphics.Shader |
|||
import androidx.core.content.ContextCompat |
|||
import com.github.mikephil.charting.animation.ChartAnimator |
|||
import com.github.mikephil.charting.components.YAxis |
|||
import com.github.mikephil.charting.utils.ViewPortHandler |
|||
import com.whitefish.app.R |
|||
|
|||
class GradientLineChartRenderer( |
|||
private val currentDate:Int, |
|||
private val chart: LineChart, |
|||
animator: ChartAnimator, |
|||
viewPortHandler: ViewPortHandler |
|||
) : LineChartRenderer(chart, animator, viewPortHandler) { |
|||
|
|||
// 创建虚线效果的Paint对象 |
|||
private val dashedLinePaint = Paint().apply { |
|||
color = chart.rootView.resources.getColor(R.color.white_25,chart.rootView.context.theme) |
|||
style = Paint.Style.STROKE |
|||
strokeWidth = 4f |
|||
isAntiAlias = true |
|||
// 设置虚线效果 |
|||
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f) |
|||
} |
|||
|
|||
override fun drawData(canvas: Canvas) { |
|||
drawVerticalDashedLines(canvas)//先绘制虚线,避免覆盖折线 |
|||
super.drawData(canvas) |
|||
drawHighlightedPoint(canvas) |
|||
} |
|||
|
|||
private fun drawHighlightedPoint(canvas: Canvas) { |
|||
val dataSet = mChart.data.getDataSetByIndex(0) |
|||
if (dataSet == null || dataSet.entryCount == 0) return |
|||
|
|||
val highlightIndex = currentDate |
|||
if (highlightIndex >= dataSet.entryCount) return |
|||
|
|||
val entry = dataSet.getEntryForIndex(highlightIndex) |
|||
val trans = mChart.getTransformer(dataSet.axisDependency) |
|||
|
|||
val point = FloatArray(2) |
|||
point[0] = entry.x |
|||
point[1] = entry.y |
|||
|
|||
trans.pointValuesToPixel(point) |
|||
|
|||
// 使用drawable资源绘制高亮点 |
|||
val drawable = ContextCompat.getDrawable(chart.context, R.drawable.ic_circle_indicator) |
|||
// 计算drawable的bounds,使其居中于数据点 |
|||
val size = 40 |
|||
val left = point[0].toInt() - size / 2 |
|||
val top = point[1].toInt() - size / 2 |
|||
val right = left + size |
|||
val bottom = top + size |
|||
|
|||
drawable?.setBounds(left, top, right, bottom) |
|||
drawable?.draw(canvas) |
|||
} |
|||
|
|||
private fun drawVerticalDashedLines(canvas: Canvas) { |
|||
val trans = |
|||
mChart.getTransformer(YAxis.AxisDependency.LEFT) |
|||
|
|||
val contentRect = mViewPortHandler.contentRect |
|||
|
|||
// 需要添加垂直虚线的X轴位置 |
|||
val rangeArray = arrayListOf<Float>() |
|||
for (i in 0..chart.lineData.xMax.toInt() step (chart.lineData.xMax/5).toInt()) { |
|||
rangeArray.add(i.toFloat()) |
|||
} |
|||
|
|||
// 遍历每个位置,绘制垂直虚线 |
|||
for (hour in rangeArray) { |
|||
// 转换X轴值到像素坐标 |
|||
val pixelX = trans.getPixelForValues(hour, 0f).x.toFloat() |
|||
|
|||
// 如果该位置在可视区域内,则绘制虚线 |
|||
if (pixelX >= contentRect.left && pixelX <= contentRect.right) { |
|||
val path = Path() |
|||
path.moveTo(pixelX, contentRect.bottom) |
|||
|
|||
// 计算该X位置对应的折线Y坐标点 |
|||
val dataSet = mChart.data.getDataSetByIndex(0) |
|||
var lineY = contentRect.top |
|||
|
|||
// 查找最接近该X位置的数据点 |
|||
for (i in 0 until dataSet.entryCount - 1) { |
|||
val e1 = dataSet.getEntryForIndex(i) |
|||
val e2 = dataSet.getEntryForIndex(i + 1) |
|||
|
|||
if (hour >= e1.x && hour <= e2.x) { |
|||
// 线性插值计算Y值 |
|||
val ratio = (hour - e1.x) / (e2.x - e1.x) |
|||
val y = e1.y + ratio * (e2.y - e1.y) |
|||
|
|||
// 转换为像素坐标 |
|||
val valPoint = FloatArray(2) |
|||
valPoint[0] = hour |
|||
valPoint[1] = y |
|||
trans.pointValuesToPixel(valPoint) |
|||
|
|||
lineY = valPoint[1] |
|||
break |
|||
} |
|||
} |
|||
|
|||
path.lineTo(pixelX, lineY + 10)//设置10像素偏移,避免虚线超越折线 |
|||
canvas.drawPath(path, dashedLinePaint) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun drawLinear( |
|||
canvas: Canvas, |
|||
dataSet: com.github.mikephil.charting.interfaces.datasets.ILineDataSet |
|||
) { |
|||
if (dataSet.entryCount < 2) |
|||
return |
|||
|
|||
val trans = mChart.getTransformer(dataSet.axisDependency) |
|||
|
|||
val entryCount = dataSet.entryCount |
|||
val isDrawSteppedEnabled = dataSet.isDrawSteppedEnabled |
|||
|
|||
val phaseY = mAnimator.phaseY |
|||
|
|||
// 存储点的坐标 |
|||
val points = FloatArray(entryCount * 4) // 每个点需要2个值(x,y),乘以2是因为每个点有两个关联点 |
|||
|
|||
for (i in 0 until entryCount) { |
|||
val e = dataSet.getEntryForIndex(i) ?: continue |
|||
|
|||
points[i * 4] = e.x |
|||
points[i * 4 + 1] = e.y * phaseY |
|||
|
|||
if (isDrawSteppedEnabled) { |
|||
val e2 = if (i >= entryCount - 1) e else dataSet.getEntryForIndex(i + 1) |
|||
points[i * 4 + 2] = e2.x |
|||
points[i * 4 + 3] = e.y * phaseY |
|||
} else { |
|||
points[i * 4 + 2] = e.x |
|||
points[i * 4 + 3] = e.y * phaseY |
|||
} |
|||
} |
|||
|
|||
trans.pointValuesToPixel(points) |
|||
|
|||
mRenderPaint.style = android.graphics.Paint.Style.STROKE |
|||
mRenderPaint.strokeWidth = dataSet.lineWidth |
|||
|
|||
// 创建渐变色 |
|||
val colorfulColors = intArrayOf( |
|||
Color.parseColor("#407EEFFF"), |
|||
Color.parseColor("#8088FF7E"), |
|||
Color.parseColor("#BFFFEF62"), |
|||
Color.parseColor("#EB3F3F"), |
|||
) |
|||
|
|||
val path = Path() |
|||
|
|||
for (i in 0 until entryCount) { |
|||
if (i == 0) { |
|||
path.moveTo(points[0], points[1]) |
|||
} else { |
|||
val x1 = points[(i - 1) * 4] |
|||
val y1 = points[(i - 1) * 4 + 1] |
|||
val x2 = points[i * 4] |
|||
val y2 = points[i * 4 + 1] |
|||
|
|||
// 使用贝塞尔曲线来平滑线条 |
|||
val cpx1 = x1 + (x2 - x1) / 2 |
|||
val cpx2 = x1 + (x2 - x1) / 2 |
|||
|
|||
path.cubicTo(cpx1, y1, cpx2, y2, x2, y2) |
|||
} |
|||
} |
|||
|
|||
// 创建线性渐变 |
|||
val startX = points[0] |
|||
val endX = points[points.size - 4] |
|||
|
|||
val colorfulGradient = LinearGradient( |
|||
startX, 0f, |
|||
endX, 0f, |
|||
colorfulColors, |
|||
floatArrayOf(0f, 0.25f, 0.5f,1.0f), |
|||
Shader.TileMode.CLAMP |
|||
) |
|||
|
|||
mRenderPaint.shader = colorfulGradient |
|||
canvas.drawPath(path, mRenderPaint) |
|||
mRenderPaint.shader = null |
|||
|
|||
|
|||
//得到高亮点位 |
|||
val highlightX = points[currentDate * 4] |
|||
val highlightY = points[currentDate * 4 + 1] |
|||
|
|||
// 创建高亮点之后的路径 |
|||
val grayPath = Path() |
|||
grayPath.moveTo(highlightX, highlightY) |
|||
|
|||
// 添加高亮点之后的所有点到灰色路径 |
|||
for (i in currentDate + 1 until entryCount) { |
|||
val x1 = points[(i - 1) * 4] |
|||
val y1 = points[(i - 1) * 4 + 1] |
|||
val x2 = points[i * 4] |
|||
val y2 = points[i * 4 + 1] |
|||
|
|||
val cpx1 = x1 + (x2 - x1) / 2 |
|||
val cpx2 = x1 + (x2 - x1) / 2 |
|||
|
|||
grayPath.cubicTo(cpx1, y1, cpx2, y2, x2, y2) |
|||
} |
|||
|
|||
//因着色效果为颜色叠加,因此需要绘制底色 |
|||
mRenderPaint.color = Color.parseColor("#ffffffff") |
|||
canvas.drawPath(grayPath, mRenderPaint) |
|||
|
|||
mRenderPaint.color = Color.parseColor("#20000000") |
|||
canvas.drawPath(grayPath, mRenderPaint) |
|||
} |
|||
} |
@ -0,0 +1,40 @@ |
|||
package com.whitefish.app.chart; |
|||
|
|||
import android.content.Context; |
|||
import android.graphics.Canvas; |
|||
import android.graphics.drawable.Drawable; |
|||
|
|||
import androidx.annotation.NonNull; |
|||
import androidx.core.content.ContextCompat; |
|||
|
|||
import java.util.Objects; |
|||
|
|||
public class ResizedDrawable extends Drawable { |
|||
private final Drawable mDrawable; |
|||
|
|||
public ResizedDrawable(Context context, int resId, int width, int height) { |
|||
mDrawable = ContextCompat.getDrawable(context, resId); |
|||
Objects.requireNonNull(mDrawable).setBounds( -width, -height, width, height); |
|||
} |
|||
|
|||
@Override |
|||
public void draw(@NonNull Canvas canvas) { |
|||
mDrawable.draw(canvas); |
|||
} |
|||
|
|||
@Override |
|||
public void setAlpha(int alpha) { |
|||
mDrawable.setAlpha(alpha); |
|||
} |
|||
|
|||
@Override |
|||
public void setColorFilter(android.graphics.ColorFilter colorFilter) { |
|||
mDrawable.setColorFilter(colorFilter); |
|||
} |
|||
|
|||
@Override |
|||
public int getOpacity() { |
|||
return mDrawable.getOpacity(); |
|||
} |
|||
} |
|||
|
@ -0,0 +1,13 @@ |
|||
package com.whitefish.app.chart; |
|||
|
|||
import com.github.mikephil.charting.data.Entry; |
|||
import com.github.mikephil.charting.formatter.ValueFormatter; |
|||
|
|||
public class RightAlignedValueFormatter extends ValueFormatter { |
|||
|
|||
@Override |
|||
public String getPointLabel(Entry entry) { |
|||
// 返回要显示的文本
|
|||
return (int)entry.getY() + "岁"; |
|||
} |
|||
} |
@ -0,0 +1,171 @@ |
|||
package com.whitefish.app.chart |
|||
|
|||
import android.graphics.Canvas |
|||
import android.graphics.Color |
|||
import android.graphics.Paint |
|||
import android.graphics.Path |
|||
import android.graphics.RectF |
|||
import com.github.mikephil.charting.animation.ChartAnimator |
|||
import com.github.mikephil.charting.data.BarEntry |
|||
import com.github.mikephil.charting.highlight.Highlight |
|||
import com.github.mikephil.charting.interfaces.dataprovider.BarDataProvider |
|||
import com.github.mikephil.charting.interfaces.datasets.IBarDataSet |
|||
import com.github.mikephil.charting.renderer.BarChartRenderer |
|||
import com.github.mikephil.charting.utils.Utils |
|||
import com.github.mikephil.charting.utils.ViewPortHandler |
|||
import kotlin.math.min |
|||
|
|||
class VerticalRadiusBarChartRender( |
|||
chart: BarDataProvider?, animator: ChartAnimator?, viewPortHandler: ViewPortHandler? |
|||
) : BarChartRenderer(chart, animator, viewPortHandler) { |
|||
private val mBarShadowRectBuffer = RectF() |
|||
private var mRadius = 0 |
|||
fun setRadius(radius: Int) { |
|||
this.mRadius = radius |
|||
} |
|||
|
|||
override fun drawDataSet(c: Canvas, dataSet: IBarDataSet, index: Int) { |
|||
val trans = mChart.getTransformer(dataSet.axisDependency) |
|||
mBarBorderPaint.strokeWidth = Utils.convertDpToPixel(dataSet.barBorderWidth) |
|||
val drawBorder = dataSet.barBorderWidth > 0f |
|||
val phaseX = mAnimator.phaseX |
|||
val phaseY = mAnimator.phaseY |
|||
if (mChart.isDrawBarShadowEnabled) { |
|||
val barData = mChart.barData |
|||
val barWidth = barData.barWidth |
|||
val barWidthHalf = barWidth / 2.0f |
|||
var x: Float |
|||
var i = 0 |
|||
val count = min( |
|||
(dataSet.entryCount.toFloat() * phaseX).toDouble().toInt().toDouble(), |
|||
dataSet.entryCount.toDouble() |
|||
) |
|||
while (i < count) { |
|||
val e = dataSet.getEntryForIndex(i) |
|||
x = e.x |
|||
mBarShadowRectBuffer.left = x - barWidthHalf |
|||
mBarShadowRectBuffer.right = x + barWidthHalf |
|||
trans.rectValueToPixel(mBarShadowRectBuffer) |
|||
if (!mViewPortHandler.isInBoundsLeft(mBarShadowRectBuffer.right)) { |
|||
i++ |
|||
continue |
|||
} |
|||
if (!mViewPortHandler.isInBoundsRight(mBarShadowRectBuffer.left)) { |
|||
break |
|||
} |
|||
mBarShadowRectBuffer.top = mViewPortHandler.contentTop() |
|||
mBarShadowRectBuffer.bottom = mViewPortHandler.contentBottom() |
|||
c.drawRoundRect(mBarRect, mRadius.toFloat(), mRadius.toFloat(), mShadowPaint) |
|||
i++ |
|||
} |
|||
} |
|||
|
|||
// initialize the buffer |
|||
val buffer = mBarBuffers[index] |
|||
buffer.setPhases(phaseX, phaseY) |
|||
buffer.setDataSet(index) |
|||
buffer.setInverted(mChart.isInverted(dataSet.axisDependency)) |
|||
buffer.setBarWidth(mChart.barData.barWidth) |
|||
buffer.feed(dataSet) |
|||
trans.pointValuesToPixel(buffer.buffer) |
|||
var j = 0 |
|||
|
|||
|
|||
while (j < buffer.size()) { |
|||
if (!mViewPortHandler.isInBoundsLeft(buffer.buffer[j + 2])) { |
|||
j += 4 |
|||
continue |
|||
} |
|||
if (!mViewPortHandler.isInBoundsRight(buffer.buffer[j])) { |
|||
break |
|||
} |
|||
|
|||
mRenderPaint.color = dataSet.getColor(j / 4) |
|||
|
|||
val path2 = roundRect( |
|||
RectF( |
|||
buffer.buffer[j], |
|||
buffer.buffer[j + 1], |
|||
buffer.buffer[j + 2], |
|||
buffer.buffer[j + 3] |
|||
), mRadius.toFloat(), mRadius.toFloat(), true, true, true, true |
|||
) |
|||
c.drawPath(path2, mRenderPaint) |
|||
if (drawBorder) { |
|||
val path = roundRect( |
|||
RectF( |
|||
buffer.buffer[j], |
|||
buffer.buffer[j + 1], |
|||
buffer.buffer[j + 2], |
|||
buffer.buffer[j + 3] |
|||
), mRadius.toFloat(), mRadius.toFloat(), true, true, true, true |
|||
) |
|||
c.drawPath(path, mBarBorderPaint) |
|||
} |
|||
j += 4 |
|||
} |
|||
} |
|||
|
|||
private fun roundRect( |
|||
rect: RectF, rx: Float, ry: Float, tl: Boolean, tr: Boolean, br: Boolean, bl: Boolean |
|||
): Path { |
|||
var rx = rx |
|||
var ry = ry |
|||
val top = rect.top |
|||
val left = rect.left |
|||
val right = rect.right |
|||
val bottom = rect.bottom |
|||
val path = Path() |
|||
if (rx < 0) { |
|||
rx = 0f |
|||
} |
|||
if (ry < 0) { |
|||
ry = 0f |
|||
} |
|||
val width = right - left |
|||
val height = bottom - top |
|||
if (rx > width / 2) { |
|||
rx = width / 2 |
|||
} |
|||
if (ry > height / 2) { |
|||
ry = height / 2 |
|||
} |
|||
val widthMinusCorners = width - 2 * rx |
|||
val heightMinusCorners = height - 2 * ry |
|||
path.moveTo(right, top + ry) |
|||
if (tr) { |
|||
//top-right corner |
|||
path.rQuadTo(0f, -ry, -rx, -ry) |
|||
} else { |
|||
path.rLineTo(0f, -ry) |
|||
path.rLineTo(-rx, 0f) |
|||
} |
|||
path.rLineTo(-widthMinusCorners, 0f) |
|||
if (tl) { |
|||
//top-left corner |
|||
path.rQuadTo(-rx, 0f, -rx, ry) |
|||
} else { |
|||
path.rLineTo(-rx, 0f) |
|||
path.rLineTo(0f, ry) |
|||
} |
|||
path.rLineTo(0f, heightMinusCorners) |
|||
if (bl) { |
|||
//bottom-left corner |
|||
path.rQuadTo(0f, ry, rx, ry) |
|||
} else { |
|||
path.rLineTo(0f, ry) |
|||
path.rLineTo(rx, 0f) |
|||
} |
|||
path.rLineTo(widthMinusCorners, 0f) |
|||
if (br) { |
|||
//bottom-right corner |
|||
path.rQuadTo(rx, 0f, rx, -ry) |
|||
} else { |
|||
path.rLineTo(rx, 0f) |
|||
path.rLineTo(0f, -ry) |
|||
} |
|||
path.rLineTo(0f, -heightMinusCorners) |
|||
path.close() //Given close, last lineto can be removed. |
|||
return path |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
package com.whitefish.app.constants |
|||
|
|||
object Constants { |
|||
const val SP_NAME = "ring_app" |
|||
const val SP_KEY_BOUND_DEVICE_ADDRESS = "sp_key_bound_device_address" |
|||
} |
@ -0,0 +1,16 @@ |
|||
package com.whitefish.app.dao |
|||
|
|||
import androidx.room.Dao |
|||
import androidx.room.Insert |
|||
import androidx.room.OnConflictStrategy |
|||
import androidx.room.Query |
|||
import com.whitefish.app.dao.bean.HeartRateDataBean |
|||
|
|||
@Dao |
|||
interface HeartRateDao{ |
|||
@Insert(onConflict = OnConflictStrategy.REPLACE) |
|||
suspend fun putHeartRate(beans: List<HeartRateDataBean>) |
|||
|
|||
@Query("select * from HeartRateDataBean order by time desc") |
|||
suspend fun getHeartRateList():List<HeartRateDataBean> |
|||
} |
@ -0,0 +1,17 @@ |
|||
package com.whitefish.app.dao |
|||
|
|||
import androidx.room.Dao |
|||
import androidx.room.Insert |
|||
import androidx.room.OnConflictStrategy |
|||
import androidx.room.Query |
|||
import com.whitefish.app.dao.bean.SleepDataBean |
|||
|
|||
@Dao |
|||
interface SleepDao { |
|||
|
|||
@Insert(onConflict = OnConflictStrategy.REPLACE) |
|||
suspend fun insert(sleep:SleepDataBean) |
|||
|
|||
@Query("select * from SleepDataBean where date = :date order by startTime desc") |
|||
suspend fun getAllSleepData(date:Long):SleepDataBean |
|||
} |
@ -0,0 +1,18 @@ |
|||
package com.whitefish.app.dao |
|||
|
|||
import androidx.room.Dao |
|||
import androidx.room.Insert |
|||
import androidx.room.Query |
|||
import com.whitefish.app.dao.bean.WorkoutDbData |
|||
|
|||
@Dao |
|||
interface WorkoutDao { |
|||
@Insert |
|||
suspend fun insert(workout: WorkoutDbData) |
|||
|
|||
@Query("SELECT * FROM WorkoutDbData ORDER BY startTs DESC") |
|||
suspend fun getAllWorkouts(): List<WorkoutDbData> |
|||
|
|||
@Query("SELECT * FROM WorkoutDbData WHERE date = :date") |
|||
suspend fun getWorkoutsByDate(date: String): List<WorkoutDbData> |
|||
} |
@ -0,0 +1,13 @@ |
|||
package com.whitefish.app.dao.bean |
|||
|
|||
import android.os.Parcelable |
|||
import androidx.room.Entity |
|||
import androidx.room.PrimaryKey |
|||
import kotlinx.parcelize.Parcelize |
|||
|
|||
@Entity |
|||
@Parcelize |
|||
data class HeartRateDataBean ( |
|||
@PrimaryKey val time:Long, |
|||
val value:Int, |
|||
) : Parcelable |
@ -0,0 +1,21 @@ |
|||
package com.whitefish.app.dao.bean |
|||
|
|||
import android.os.Parcelable |
|||
import androidx.room.Entity |
|||
import androidx.room.PrimaryKey |
|||
import com.whitefish.app.view.SleepSegment |
|||
import kotlinx.parcelize.Parcelize |
|||
import java.sql.Timestamp |
|||
import java.util.Date |
|||
|
|||
|
|||
@Parcelize |
|||
@Entity |
|||
data class SleepDataBean( |
|||
val startTime:Long, |
|||
@PrimaryKey val date: Long, |
|||
val totalSleepTime: Long, |
|||
val sleepSegments: List<SleepSegment> = emptyList(), |
|||
val score: String = "", |
|||
val rating:Float = 0f, |
|||
) : Parcelable |
@ -0,0 +1,43 @@ |
|||
package com.whitefish.app.dao.bean |
|||
|
|||
import android.os.Parcelable |
|||
import androidx.room.Entity |
|||
import androidx.room.PrimaryKey |
|||
import kotlinx.parcelize.Parcelize |
|||
|
|||
@Entity |
|||
@Parcelize |
|||
data class WorkoutDbData( |
|||
@PrimaryKey val startTs: Long, /* 锻炼开始时间戳,也是该条记录时间戳 */ |
|||
var endTs: Long, /* 锻炼结束时间 */ |
|||
val date: Long, /* 上面时间戳对应的日期(yyyy-MM-dd'T'HH:mm:ssXXX) */ |
|||
val btMac: String, /* 该条记录对应的设备蓝牙地址 */ |
|||
/** |
|||
* [WORKOUT_TYPE_WALKING] |
|||
* |
|||
* [WORKOUT_TYPE_INDOOR_RUNNING] |
|||
* |
|||
* [WORKOUT_TYPE_OUTDOOR_RUNNING] |
|||
* |
|||
* [WORKOUT_TYPE_INDOOR_CYCLING] |
|||
* |
|||
* [WORKOUT_TYPE_OUTDOOR_CYCLING] |
|||
* |
|||
* [WORKOUT_TYPE_MOUNTAIN_BIKING] |
|||
* |
|||
* [WORKOUT_TYPE_SWIMMING] |
|||
* */ |
|||
val type: Int, |
|||
val target: Int, |
|||
) : Parcelable { |
|||
} |
|||
|
|||
enum class WorkoutType{ |
|||
WORKOUT_TYPE_WALKING, |
|||
WORKOUT_TYPE_INDOOR_RUNNING, |
|||
WORKOUT_TYPE_OUTDOOR_RUNNING, |
|||
WORKOUT_TYPE_INDOOR_CYCLING, |
|||
WORKOUT_TYPE_OUTDOOR_CYCLING, |
|||
WORKOUT_TYPE_MOUNTAIN_BIKING, |
|||
WORKOUT_TYPE_SWIMMING, |
|||
} |
@ -0,0 +1,21 @@ |
|||
package com.whitefish.app.dao.converter |
|||
|
|||
import androidx.room.TypeConverter |
|||
import com.google.gson.Gson |
|||
import com.google.gson.reflect.TypeToken |
|||
import com.whitefish.app.view.SleepSegment |
|||
|
|||
class SleepSegmentConverter { |
|||
private val gson = Gson() |
|||
private val type = object : TypeToken<List<SleepSegment>>() {}.type |
|||
|
|||
@TypeConverter |
|||
fun fromString(value: String): List<SleepSegment> { |
|||
return gson.fromJson(value, type) |
|||
} |
|||
|
|||
@TypeConverter |
|||
fun fromList(list: List<SleepSegment>): String { |
|||
return gson.toJson(list) |
|||
} |
|||
} |
@ -0,0 +1,280 @@ |
|||
package com.whitefish.app.data |
|||
|
|||
import com.github.mikephil.charting.data.Entry |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
import com.whitefish.app.bean.Evaluate |
|||
import com.whitefish.app.bean.Exercise |
|||
import com.whitefish.app.feature.home.bean.ExerciseHistory |
|||
import com.whitefish.app.feature.home.bean.RecoveryLineChart |
|||
import com.whitefish.app.feature.home.bean.Tips |
|||
import com.whitefish.app.utils.getPastDayCalendar |
|||
import lib.linktop.nexring.api.SLEEP_STATE_DEEP |
|||
import lib.linktop.nexring.api.SLEEP_STATE_LIGHT |
|||
import lib.linktop.nexring.api.SLEEP_STATE_REM |
|||
import lib.linktop.nexring.api.SLEEP_STATE_WAKE |
|||
import lib.linktop.nexring.api.SleepData |
|||
import lib.linktop.nexring.api.SleepStage |
|||
|
|||
/** |
|||
* Test repository |
|||
*/ |
|||
object FakeRepository { |
|||
// Mock数据构造函数 |
|||
fun createMockSleepData(offset: Int): lib.linktop.nexring.api.SleepData { |
|||
// 时间戳:假设从昨晚23:30到今早7:00的睡眠 |
|||
val sleepStartTime = getPastDayCalendar(offset).timeInMillis - (8 * 60 * 60 * 1000) // 8小时前 |
|||
val sleepEndTime = getPastDayCalendar(offset).timeInMillis |
|||
val totalDuration = sleepEndTime - sleepStartTime |
|||
|
|||
// 构造睡眠阶段数据 |
|||
val mockSleepStages = arrayListOf( |
|||
// 入睡阶段 - 浅睡眠 |
|||
SleepStage(-0.30f, -0.15f, SLEEP_STATE_LIGHT), // 23:30-23:45 |
|||
SleepStage(-0.15f, 0.45f, SLEEP_STATE_DEEP), // 23:45-00:45 |
|||
SleepStage(0.45f, 1.30f, SLEEP_STATE_LIGHT), // 00:45-01:30 |
|||
SleepStage(1.30f, 2.15f, SLEEP_STATE_REM), // 01:30-02:15 |
|||
SleepStage(2.15f, 3.45f, SLEEP_STATE_DEEP), // 02:15-03:45 |
|||
SleepStage(3.45f, 4.30f, SLEEP_STATE_LIGHT), // 03:45-04:30 |
|||
SleepStage(4.30f, 5.15f, SLEEP_STATE_REM), // 04:30-05:15 |
|||
SleepStage(5.15f, 6.30f, SLEEP_STATE_LIGHT), // 05:15-06:30 |
|||
SleepStage(6.30f, 6.45f, SLEEP_STATE_WAKE), // 06:30-06:45 |
|||
SleepStage(6.45f, 7.00f, SLEEP_STATE_LIGHT) // 06:45-07:00 |
|||
) |
|||
|
|||
// 构造睡眠状态统计数据 |
|||
val mockSleepStates = arrayOf( |
|||
lib.linktop.nexring.api.SleepState( |
|||
duration = 30 * 60 * 1000L, |
|||
percent = 6.25f |
|||
), // 清醒:30分钟 |
|||
lib.linktop.nexring.api.SleepState( |
|||
duration = 135 * 60 * 1000L, |
|||
percent = 28.125F |
|||
), // REM:2小时15分钟 |
|||
lib.linktop.nexring.api.SleepState( |
|||
duration = 165 * 60 * 1000L, |
|||
percent = 34.375F |
|||
), // 浅睡眠:2小时45分钟 |
|||
lib.linktop.nexring.api.SleepState( |
|||
duration = 150 * 60 * 1000L, |
|||
percent = 31.25F |
|||
), // 深睡眠:2小时30分钟 |
|||
lib.linktop.nexring.api.SleepState( |
|||
duration = 0L, |
|||
percent = 0.0F |
|||
) // 小憩:0分钟 |
|||
) |
|||
|
|||
return SleepData( |
|||
startTs = sleepStartTime, |
|||
endTs = sleepEndTime, |
|||
btMac = "AA:BB:CC:DD:EE:FF", |
|||
duration = totalDuration, |
|||
hr = 58.5, // 平均心率 |
|||
hrv = 42.3, // 平均心率变异性 |
|||
rr = 16.2, // 平均呼吸率 |
|||
spo2 = 97.8, // 平均血氧饱和度 |
|||
hrDip = 12.5, // 心率下降百分比 |
|||
sleepStages = mockSleepStages, |
|||
sleepStates = mockSleepStates, |
|||
isNap = false, |
|||
efficiency = 90.0 |
|||
) |
|||
} |
|||
|
|||
// suspend fun deviceList(): List<Device> { |
|||
// return arrayListOf(Device("", "Test", "123"), Device("", "Test", "123")) |
|||
// } |
|||
|
|||
// suspend fun stateList(): List<Any> { |
|||
// return arrayListOf( |
|||
// ExerciseTarget(35, "1234"), |
|||
// RecoveryScore( |
|||
// 55, |
|||
// "预计2月31日00:00后\n完全恢复", |
|||
// "啊八八八八八八Abba艾伯克兰十大事件都可能", |
|||
// Date(System.currentTimeMillis()) |
|||
// ), |
|||
// HeardRate( |
|||
// HeardRate.SIZE_HALF_ROW, LineDataSet( |
|||
// arrayListOf<Entry>( |
|||
// Entry(1.0f, 2.0f), |
|||
// Entry(2.0f, 1.0f), |
|||
// Entry(3.0f, 5.0f), |
|||
// Entry(4.0f, 2.0f), |
|||
// Entry(5.0f, 2.0f), |
|||
// Entry(6.0f, 1.0f), |
|||
// Entry(7.0f, 5.0f), |
|||
// Entry(8.0f, 2.0f), |
|||
// Entry(9.0f, 2.0f), |
|||
// Entry(10.0f, 1.0f), |
|||
// Entry(11.0f, 5.0f), |
|||
// Entry(12.0f, 2.0f), |
|||
// Entry(13.0f, 2.0f), |
|||
// Entry(14.0f, 1.0f), |
|||
// Entry(15.0f, 5.0f), |
|||
// Entry(16.0f, 2.0f) |
|||
// ), "" |
|||
// ), "999次/一分钟", "7/29", "你的心率非常不正常" |
|||
// ), |
|||
//// SleepState("7/12","7小时", sleepData(),"7",7f,"5小时","2小时","0小时") |
|||
// ) |
|||
// } |
|||
|
|||
suspend fun exerciseStateList(): Exercise { |
|||
return Exercise( |
|||
"Alin", |
|||
"", |
|||
123, |
|||
456, |
|||
arrayListOf( |
|||
ExerciseHistory("123", "!@#", "!23"), |
|||
Evaluate(70, 85, 30, 20, 10, 5), |
|||
Tips("巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉巴拉") |
|||
) |
|||
) |
|||
} |
|||
|
|||
suspend fun recoveryStateList(): List<Any> { |
|||
return arrayListOf( |
|||
// RecoveryScore( |
|||
// 55, |
|||
// "预计2/32日18:00后完全恢复", |
|||
// "你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", |
|||
// Date(System.currentTimeMillis()) |
|||
// ), |
|||
RecoveryLineChart( |
|||
-1f, |
|||
lineChartData = LineDataSet( |
|||
arrayListOf<Entry>( |
|||
Entry(1.0f, 0.0f), |
|||
Entry(2.0f, -1.0f), |
|||
Entry(3.0f, -1.0f), |
|||
Entry(4.0f, -2.0f), |
|||
Entry(5.0f, -2.0f), |
|||
Entry(6.0f, -4.0f), |
|||
Entry(7.0f, -3.0f), |
|||
|
|||
), "" |
|||
) |
|||
) |
|||
) |
|||
} |
|||
|
|||
suspend fun gapListData(): List<String> { |
|||
return arrayListOf( |
|||
"10", |
|||
"20", |
|||
"30", |
|||
"40", |
|||
"50", |
|||
"60" |
|||
) |
|||
} |
|||
|
|||
suspend fun distanceListData(): List<String> { |
|||
return arrayListOf( |
|||
"10", |
|||
"20", |
|||
"30", |
|||
"40", |
|||
"50", |
|||
"60" |
|||
) |
|||
} |
|||
|
|||
suspend fun targetList(): List<String> { |
|||
return arrayListOf( |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
"全身减脂", |
|||
|
|||
) |
|||
} |
|||
|
|||
suspend fun promoteList(): List<String> { |
|||
return arrayListOf( |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
"速度和爆发力", |
|||
|
|||
) |
|||
} |
|||
|
|||
suspend fun exerciseList(): List<String> { |
|||
return listOf( |
|||
"131", |
|||
"1", |
|||
"dfgds", |
|||
"as", |
|||
"adcdddsadasda", |
|||
"gserwrW", |
|||
"sdfgsbdgfsdggsdfg", |
|||
"FDGssFDGSB", |
|||
"e", |
|||
"341DSFGFDGAGFSDSGsfd", |
|||
"dfhgjvbfsdajfdsjhb", |
|||
"vhjcsbvjhsbvchjdsvbaskjhd", |
|||
"asdklasdn", |
|||
"asassaasd", |
|||
"6523123", |
|||
"a54fds124aasdf" |
|||
) |
|||
} |
|||
|
|||
|
|||
// suspend fun sleepData() = listOf( |
|||
// // 入睡前(清醒) |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_AWAKE, 15), |
|||
// |
|||
// // 初次入睡(浅睡) |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_LIGHT, 40), |
|||
// |
|||
// |
|||
// // REM睡眠 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_REM, 25), |
|||
// |
|||
// // 短暂醒来 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_AWAKE, 8), |
|||
// |
|||
// // 再次浅睡 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_LIGHT, 35), |
|||
// |
|||
// // 再次深睡 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_DEEP, 45), |
|||
// |
|||
// // 浅睡 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_LIGHT, 20), |
|||
// |
|||
// // 再次短暂醒来 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_AWAKE, 5), |
|||
// |
|||
// // 浅睡 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_LIGHT, 25), |
|||
// |
|||
// // 深睡 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_DEEP, 30), |
|||
// |
|||
// // 第三次REM睡眠(梦眠) |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_REM, 35), |
|||
// |
|||
// // 醒来 |
|||
// SleepChartView.SleepSegment(SLEEP_STATE_AWAKE, 10) |
|||
// ) |
|||
} |
@ -0,0 +1,39 @@ |
|||
package com.whitefish.app.db |
|||
|
|||
import androidx.room.Database |
|||
import androidx.room.Room |
|||
import androidx.room.RoomDatabase |
|||
import androidx.room.TypeConverters |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.dao.bean.WorkoutDbData |
|||
import com.whitefish.app.dao.HeartRateDao |
|||
import com.whitefish.app.dao.SleepDao |
|||
import com.whitefish.app.dao.WorkoutDao |
|||
import com.whitefish.app.dao.bean.HeartRateDataBean |
|||
import com.whitefish.app.dao.bean.SleepDataBean |
|||
import com.whitefish.app.dao.converter.SleepSegmentConverter |
|||
|
|||
@Database(entities = [WorkoutDbData::class,HeartRateDataBean::class,SleepDataBean::class], version = 1) |
|||
@TypeConverters(SleepSegmentConverter::class) |
|||
abstract class AppDatabase : RoomDatabase() { |
|||
abstract fun workoutDao(): WorkoutDao |
|||
abstract fun heartRateDao(): HeartRateDao |
|||
abstract fun sleepDao(): SleepDao |
|||
|
|||
companion object { |
|||
@Volatile |
|||
private var INSTANCE: AppDatabase? = null |
|||
|
|||
fun getDatabase(): AppDatabase { |
|||
return INSTANCE ?: synchronized(this) { |
|||
val instance = Room.databaseBuilder( |
|||
Application.INSTANTS!!.applicationContext, |
|||
AppDatabase::class.java, |
|||
"ring_db" |
|||
).build() |
|||
INSTANCE = instance |
|||
instance |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,237 @@ |
|||
package com.whitefish.app.device |
|||
|
|||
import android.content.Context |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.bean.SleepNap |
|||
import com.whitefish.app.bean.SleepState |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.dao.bean.HeartRateDataBean |
|||
import com.whitefish.app.dao.bean.SleepDataBean |
|||
import com.whitefish.app.data.FakeRepository |
|||
import com.whitefish.app.db.AppDatabase |
|||
import com.whitefish.app.utils.getDayStart |
|||
import com.whitefish.app.utils.getPastDayCalendar |
|||
import com.whitefish.app.utils.getString |
|||
import com.whitefish.app.utils.todayCalendar |
|||
import com.whitefish.app.view.SleepSegment |
|||
import com.whitefish.app.view.toSegment |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.async |
|||
import kotlinx.coroutines.launch |
|||
import lib.linktop.nexring.api.BatteryInfo |
|||
import lib.linktop.nexring.api.DoubleData |
|||
import lib.linktop.nexring.api.IntData |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.Statistics |
|||
import java.sql.Timestamp |
|||
import java.util.Calendar |
|||
import kotlin.coroutines.resume |
|||
import kotlin.coroutines.resumeWithException |
|||
import kotlin.coroutines.suspendCoroutine |
|||
import kotlin.math.min |
|||
|
|||
class DeviceDataProvider { |
|||
|
|||
companion object { |
|||
val INSTANCE by lazy { |
|||
DeviceDataProvider() |
|||
} |
|||
} |
|||
|
|||
private val address = getString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS, "") |
|||
private val scope = CoroutineScope(Dispatchers.IO) |
|||
private val hoursOfDay = 24 |
|||
private val oneHourTs = 3600000L |
|||
|
|||
|
|||
suspend fun getRhr() = scope.async { |
|||
var result: Int? = 0 |
|||
NexRingManager.get().sleepApi().getRhr(address, todayCalendar()) { |
|||
Logger.i("Rhr is :${it}") |
|||
result = it |
|||
} |
|||
return@async result |
|||
}.await() |
|||
|
|||
/** |
|||
* 获取指定日期的24小时心率数据 |
|||
* @param calendar 指定日期 |
|||
* @return 返回24小时的心率数据列表,每个元素代表一个小时的平均心率 |
|||
*/ |
|||
suspend fun getHeartRate(calendar: Calendar): List<Pair<Int, Int>> = scope.async { |
|||
val dayStartTime = calendar.getDayStart() |
|||
return@async collectHourlyHeartRates(dayStartTime) |
|||
}.await() |
|||
|
|||
/** |
|||
* 收集24小时的心率数据 |
|||
* @param dayStartTime 当天开始的时间戳 |
|||
* @return Pair(time,heartRate) |
|||
*/ |
|||
private suspend fun collectHourlyHeartRates(dayStartTime: Long): List<Pair<Int, Int>> { |
|||
val heartRates = mutableListOf<Pair<Int, Int>>() |
|||
for (hour in 0 until hoursOfDay) { |
|||
val hourData = getHourlyHeartRate(dayStartTime, hour) |
|||
hourData?.let { |
|||
heartRates.add(Pair(hour, it)) |
|||
} |
|||
} |
|||
|
|||
return heartRates |
|||
} |
|||
|
|||
/** |
|||
* 获取指定小时的心率数据 |
|||
* @param dayStartTime 当天开始的时间戳 |
|||
* @param hour 小时(0-23) |
|||
* @return 该小时的平均心率 |
|||
*/ |
|||
private suspend fun getHourlyHeartRate(dayStartTime: Long, hour: Int): Int? = |
|||
suspendCoroutine { continuation -> |
|||
val hourStartTime = oneHourTs.times(hour).plus(dayStartTime) |
|||
val hourEndTime = hourStartTime.plus(oneHourTs).minus(1) |
|||
|
|||
NexRingManager.get().sleepApi() |
|||
.getHrList(address, hourStartTime, hourEndTime) { result -> |
|||
result?.let { |
|||
continuation.resume(it.first.avg.toInt()) |
|||
} ?: let { |
|||
continuation.resume(null) |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
suspend fun getSleepData(date: Calendar): SleepDataBean? = suspendCoroutine { continuation -> |
|||
NexRingManager.get().sleepApi().getSleepDataByDate(address, date) { sleepData -> |
|||
try { |
|||
val sleepSessionList: MutableList<SleepSegment> = ArrayList() |
|||
var totalTime = 0L |
|||
|
|||
var totalStartTime = 0L |
|||
if (!sleepData.isNullOrEmpty()) { |
|||
sleepData.forEach { |
|||
if (!it.isNap) { |
|||
totalTime += it.duration |
|||
it.sleepStages.forEach { stage -> |
|||
sleepSessionList.add(stage.toSegment()) |
|||
} |
|||
totalStartTime = min(totalStartTime,it.startTs) |
|||
}else{ |
|||
Logger.i("Nap sleep: ${it}") |
|||
} |
|||
} |
|||
val sleepDataBean = SleepDataBean( |
|||
totalStartTime, |
|||
date.timeInMillis, |
|||
totalTime, |
|||
sleepSessionList, |
|||
) |
|||
continuation.resume(sleepDataBean) |
|||
} else { |
|||
continuation.resume(null) |
|||
} |
|||
} catch (e: Exception) { |
|||
continuation.resumeWithException(e) |
|||
} |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 通用的获取最近24小时数据的函数 |
|||
* @param dataFetcher 数据获取函数,接收开始时间和结束时间,返回数据 |
|||
* @return 返回最近24小时的数据,key为距离当前时间的小时数,value为对应的数据 |
|||
*/ |
|||
private suspend fun <T> getLast24HoursData( |
|||
dataFetcher: (startTime: Long, endTime: Long) -> T? |
|||
): HashMap<Int, T> = scope.async { |
|||
val currentTime = System.currentTimeMillis() |
|||
val result = hashMapOf<Int, T>() |
|||
|
|||
for (hourOffset in 0 until hoursOfDay) { |
|||
val targetTime = currentTime - (hourOffset * oneHourTs) |
|||
val calendar = Calendar.getInstance().apply { |
|||
timeInMillis = targetTime |
|||
} |
|||
|
|||
val dayStartTime = calendar.getDayStart() |
|||
val hour = calendar.get(Calendar.HOUR_OF_DAY) |
|||
val hourStartTime = oneHourTs.times(hour).plus(dayStartTime) |
|||
val hourEndTime = hourStartTime.plus(oneHourTs).minus(1) |
|||
|
|||
dataFetcher(hourStartTime, hourEndTime)?.let { |
|||
result[hourOffset] = it |
|||
} |
|||
} |
|||
|
|||
return@async result |
|||
}.await() |
|||
|
|||
/** |
|||
* 获取最近24小时的心率数据 |
|||
* @return 返回最近24小时的心率数据列表,每个元素为 Pair(距离当前时间的小时数, 该小时的平均心率) |
|||
*/ |
|||
suspend fun getLast24HoursHeartRate(): HashMap<Int, Int> { |
|||
return getLast24HoursData { startTime, endTime -> |
|||
var hourlyRate: Int? = null |
|||
NexRingManager.get().sleepApi().getHrList(address, startTime, endTime) { result -> |
|||
result?.let { |
|||
hourlyRate = it.first.avg.toInt() |
|||
} |
|||
} |
|||
hourlyRate |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* 获取最近24小时的Spo2数据 |
|||
* @return 返回最近24小时的Spo2数据列表,每个元素为 Pair(距离当前时间的小时数, 该小时的平均Spo2值) |
|||
*/ |
|||
suspend fun getLast24HoursSpo2(): HashMap<Int, List<IntData>> { |
|||
return getLast24HoursData { startTime, endTime -> |
|||
var spo2Data: List<IntData>? = null |
|||
NexRingManager.get().sleepApi().getSpo2List(address, startTime, endTime) { |
|||
Logger.i("start:${startTime},end:${endTime}:${it}") |
|||
spo2Data = it |
|||
} |
|||
spo2Data |
|||
} |
|||
} |
|||
|
|||
suspend fun getAllHrList(startTs:Long,endTs: Long): List<IntData> = suspendCoroutine { continuation -> |
|||
NexRingManager.get().sleepApi().getHrList( |
|||
address, |
|||
startTs, |
|||
endTs |
|||
) { |
|||
it?.let { it1 -> continuation.resume(it1.second) }?: continuation.resume(emptyList()) |
|||
} |
|||
} |
|||
|
|||
suspend fun getLastTemperature(): HashMap<Int, List<DoubleData>> { |
|||
return getLast24HoursData { startTime, endTime -> |
|||
var spo2Data: List<DoubleData>? = null |
|||
NexRingManager.get().sleepApi().getFingerTemperatureList(address, startTime, endTime) { |
|||
Logger.i("start:${startTime},end:${endTime}:${it}") |
|||
spo2Data = it?.second |
|||
} |
|||
spo2Data |
|||
} |
|||
} |
|||
|
|||
|
|||
suspend fun getHrvList(startTs: Long, endTs: Long): Pair<Statistics, List<IntData>>? = |
|||
suspendCoroutine { continuation -> |
|||
NexRingManager.get().sleepApi().getHrvList(address, startTs, endTs) { |
|||
continuation.resume(it) |
|||
} |
|||
} |
|||
|
|||
suspend fun getDayCount(): Int? = suspendCoroutine { continuation -> |
|||
NexRingManager.get().sleepApi().getDayCount(address) { |
|||
continuation.resume(it) |
|||
} |
|||
} |
|||
} |
|||
|
@ -0,0 +1,202 @@ |
|||
package com.whitefish.app.device |
|||
|
|||
import android.annotation.SuppressLint |
|||
import android.bluetooth.BluetoothAdapter |
|||
import android.bluetooth.BluetoothManager |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import androidx.lifecycle.MutableLiveData |
|||
import com.blankj.utilcode.util.ActivityUtils.startActivity |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.bt.BleDevice |
|||
import com.whitefish.app.bt.OnBleConnectionListener |
|||
import com.whitefish.app.bt.OnBleScanCallback |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import com.whitefish.app.utils.cmdErrorTip |
|||
import com.whitefish.app.utils.postDelay |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import lib.linktop.nexring.api.BATTERY_STATE_CHARGING |
|||
import lib.linktop.nexring.api.LOAD_DATA_EMPTY |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_COMPLETED |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_PROCESSING |
|||
import lib.linktop.nexring.api.LOAD_DATA_STATE_START |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.OnSleepDataLoadListener |
|||
import lib.linktop.nexring.api.SleepData |
|||
|
|||
|
|||
class DeviceManager(private val app: Application) : OnBleConnectionListener, OnSleepDataLoadListener { |
|||
|
|||
companion object{ |
|||
const val STATE_DEVICE_CHARGING = 1 |
|||
const val STATE_DEVICE_DISCHARGING = 0 |
|||
const val STATE_DEVICE_DISCONNECTED = -3 |
|||
const val STATE_DEVICE_CONNECTING = -2 |
|||
const val STATE_DEVICE_CONNECTED = -1 |
|||
|
|||
val INSTANCE by lazy { |
|||
DeviceManager(Application.INSTANTS!!) |
|||
} |
|||
} |
|||
|
|||
private val _scanStateFlow = MutableStateFlow(RefreshEmit()) |
|||
val scanStateFlow = _scanStateFlow.asStateFlow() |
|||
val scannedDeviceList = arrayListOf<BleDevice>() |
|||
|
|||
private var isRegisterBattery = false |
|||
val batteryLevel = MutableLiveData(STATE_DEVICE_DISCONNECTED to 0) |
|||
private val sycProgress = MutableLiveData(0) |
|||
var isSyncingData: Boolean = false |
|||
// var homeViewModel: demo.linktop.nexring.ui.HomeViewModel? = null |
|||
// var workoutDetailViewModel: demo.linktop.nexring.ui.workout.WorkoutDetailViewModel? = null |
|||
|
|||
override fun onBleState(state: Int) { |
|||
when (state) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
isRegisterBattery = false |
|||
batteryLevel.postValue(STATE_DEVICE_DISCONNECTED to 0) |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
batteryLevel.postValue(STATE_DEVICE_CONNECTED to 0) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onBleReady() { |
|||
postDelay { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.getBatteryInfo { |
|||
if (it.state == BATTERY_STATE_CHARGING) { |
|||
batteryLevel.postValue(STATE_DEVICE_CHARGING to 0) |
|||
} else { |
|||
batteryLevel.postValue(STATE_DEVICE_DISCHARGING to it.level) |
|||
} |
|||
if (!isRegisterBattery) { |
|||
isRegisterBattery = true |
|||
postDelay { |
|||
NexRingManager.get() |
|||
.sleepApi() |
|||
.syncDataFromDev() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onSyncDataFromDevice(state: Int, progress: Int) { |
|||
Logger.i( |
|||
"onSyncDataFromDevice state: $state, progress: $progress" |
|||
) |
|||
when (state) { |
|||
LOAD_DATA_EMPTY -> { |
|||
Logger.e("Empty data") |
|||
//TODO Callback when no data is received from the device. |
|||
} |
|||
|
|||
LOAD_DATA_STATE_START -> { |
|||
isSyncingData = true |
|||
sycProgress.postValue(progress) |
|||
} |
|||
|
|||
LOAD_DATA_STATE_PROCESSING -> sycProgress.postValue(progress) |
|||
LOAD_DATA_STATE_COMPLETED -> { |
|||
sycProgress.postValue(progress) |
|||
isSyncingData = false |
|||
//todo sync data complete |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onSyncDataError(errorCode: Int) { |
|||
app.cmdErrorTip(errorCode) |
|||
} |
|||
|
|||
override fun onOutputNewSleepData(sleepData: ArrayList<SleepData>?) { |
|||
sleepData.also { |
|||
if (it.isNullOrEmpty()) { |
|||
Logger.i( |
|||
"onOutputNewSleepData NULL" |
|||
) |
|||
} else { |
|||
Logger.i( |
|||
"onOutputNewSleepData size ${it.size}" |
|||
) |
|||
it.forEachIndexed { index, data -> |
|||
Logger.i( |
|||
"onOutputNewSleepData $index sleep from ${data.startTs} to ${data.endTs}" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun registerCb() { |
|||
app.bleManager.addOnBleConnectionListener(this) |
|||
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(this) |
|||
} |
|||
|
|||
fun unregisterCb() { |
|||
NexRingManager.get().sleepApi().setOnSleepDataLoadListener(null) |
|||
app.bleManager.removeOnBleConnectionListener(this) |
|||
} |
|||
|
|||
@SuppressLint("MissingPermission") |
|||
fun scan(context: Context) { |
|||
val bluetoothAdapter = |
|||
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
if (bluetoothAdapter.isEnabled) { |
|||
if (!app.bleManager.isScanning) { |
|||
scannedDeviceList.clear() |
|||
app.bleManager.startScan(20 * 1000L, |
|||
object : OnBleScanCallback { |
|||
override fun onScanning(result: BleDevice) { |
|||
Logger.i("scanned device:${result}") |
|||
scannedDeviceList.add(result) |
|||
_scanStateFlow.value = RefreshEmit() |
|||
} |
|||
|
|||
override fun onScanFinished() { |
|||
_scanStateFlow.value = RefreshEmit() |
|||
} |
|||
}) |
|||
} |
|||
} else { |
|||
startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) |
|||
} |
|||
} |
|||
|
|||
fun connect(address: String) { |
|||
with(app.bleManager) { |
|||
when (bleState.value) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
batteryLevel.postValue(STATE_DEVICE_CONNECTING to 0) |
|||
if (!connect(address)) { |
|||
startScan( |
|||
20 * 1000L, |
|||
object : OnBleScanCallback { |
|||
override fun onScanning(result: BleDevice) { |
|||
if (result.device.address == address) { |
|||
connect(result.device) |
|||
} |
|||
} |
|||
|
|||
override fun onScanFinished() { |
|||
|
|||
} |
|||
}) |
|||
} |
|||
} |
|||
|
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
onBleState(bleState.value) |
|||
onBleReady() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,220 @@ |
|||
package com.whitefish.app.device |
|||
|
|||
import androidx.lifecycle.MutableLiveData |
|||
import androidx.lifecycle.ViewModel |
|||
import com.ecg.algo.ECGAnalysisAlgo |
|||
import com.ecg.algo.OnECGAnalysisResultListener |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.utils.loge |
|||
import lib.linktop.nexring.api.* |
|||
import lib.linktop.nexring.api.ECGResult |
|||
|
|||
const val WEARING_HABIT_LEFT_FINGER = -1 |
|||
const val WEARING_HABIT_RIGHT_FINGER = 1 |
|||
|
|||
class ECGViewModel : ViewModel() { |
|||
|
|||
var sampleRate = SAMPLE_RATE_512HZ |
|||
private val totalSecs = 30 |
|||
|
|||
/** |
|||
* Due to the ECG electrode definition of the device structure, under normal circumstances, |
|||
* the users need to wear the ring on the right finger to get a correct ECG signal waveform. |
|||
* But we should not restrict users’ wearing habits because of this. |
|||
* |
|||
* It has been confirmed that even if the user wear the ring on your left finger, the ECG module |
|||
* of the ring will still work normally, but the signal waveform obtained will be reversed to that |
|||
* when worn on the right finger. |
|||
* |
|||
* Therefore, when the user wears the ring on the left finger and wants to get a correct ECG |
|||
* signal waveform, the developer should multiply the obtained ECG signal value by -1 before |
|||
* displaying it on the widget. |
|||
* |
|||
* Note: We cannot detect whether the user wears the ring on the left finger or the right finger, |
|||
* nor can we detect whether the ECG is reversed. [Wearing Habit] is just an objective setting behavior. |
|||
* For example: If the user sets the wearing habit to the left finger and wears it on the right finger |
|||
* intentionally or unintentionally during measurement, although the signal reported by the ring is correct, |
|||
* the user still sees a reverse ECG. There is nothing we can do about this situation. The APP can only |
|||
* try its best to guide users to perform ECG measurements on fingers that conform to [Wearing Habit]. |
|||
* */ |
|||
var wearingHabit = WEARING_HABIT_LEFT_FINGER |
|||
set(value) { |
|||
if (field != value) { |
|||
loge("wearingHabit $value") |
|||
field = value |
|||
} |
|||
} |
|||
|
|||
val maxDataSize = sampleRate.times(totalSecs) |
|||
|
|||
// val ecgDrawWave = ECGDrawWave().apply { |
|||
// dataPerSec = sampleRate |
|||
// paperSpeed = PaperSpeed.VAL_25MM_PER_SEC |
|||
// gain = Gain.VAL_10MM_PER_MV |
|||
// } |
|||
|
|||
// val paperSpeedLd = MutableLiveData(PaperSpeed.VAL_25MM_PER_SEC) |
|||
// val gainLd = MutableLiveData(Gain.VAL_10MM_PER_MV) |
|||
|
|||
val isECGMeasuring = MutableLiveData(false) |
|||
val isLeadOnLd = MutableLiveData<Boolean?>(null) |
|||
val countdownLd = MutableLiveData<Int?>(null) |
|||
var ecgResult: ECGResult? = null |
|||
set(value) { |
|||
if (field != value) { |
|||
field = value |
|||
resultLd.postValue(value) |
|||
} |
|||
} |
|||
val resultLd = MutableLiveData<ECGResult?>(ecgResult) |
|||
val ecgDataList = ArrayList<Float>() |
|||
var startTs: Long = 0L |
|||
private var skipZeroData = true |
|||
private var ecgDataIndex = 0 |
|||
var goDetails = false |
|||
|
|||
var ecgPgaGain = PGA_GAIN_4VV |
|||
|
|||
var dataSrc = ECG_DATA_SRC_FILTERED_WITH_RESULT |
|||
|
|||
var useLocalECGAlgo = false |
|||
|
|||
private var countdown: Int = 0 |
|||
set(value) { |
|||
if (field != value) { |
|||
field = value |
|||
countdownLd.postValue( |
|||
if (value == 0) null |
|||
else value |
|||
) |
|||
} |
|||
} |
|||
|
|||
private val ecgLocalAlgo = ECGAnalysisAlgo(object : OnECGAnalysisResultListener { |
|||
|
|||
override fun onOutputFilteredECGData(data: Int) { |
|||
if (isECGMeasuring.value == true && isLeadOnLd.value == true && !(skipZeroData && data == 0) && ecgDataIndex < maxDataSize) { |
|||
skipZeroData = false |
|||
val vol = data.times(wearingHabit).toECGVoltage(dataSrc, PGA_GAIN_4VV) |
|||
ecgDataList.add(vol) |
|||
// ecgDrawWave.addData(vol) |
|||
ecgDataIndex++ |
|||
val newCountdown = totalSecs - ecgDataIndex / sampleRate |
|||
if (newCountdown != countdown) { |
|||
countdown = newCountdown |
|||
} |
|||
if (ecgDataIndex >= maxDataSize) { |
|||
goDetails = true |
|||
toggle() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onOutputECGResult(realtimeHr: Int) { |
|||
if (isLeadOnLd.value == true && dataSrc == ECG_DATA_SRC_RAW) { |
|||
ecgResult = ECGResult( |
|||
activeCal = 0, |
|||
hr = realtimeHr, |
|||
avgHr = null, |
|||
arrhythmia = 0, |
|||
isLowAmplitude = false, |
|||
isSignificantNoise = false, |
|||
isUnstableSignal = false, |
|||
isNotEnoughData = false, |
|||
rmssd = 0, |
|||
sdnn = 0, |
|||
stress = null, |
|||
bmr = 0, |
|||
signalQuality = 0, |
|||
isPresent = false, |
|||
isAlive = false |
|||
) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
private val listener = object : OnECGMeasurementListener { |
|||
|
|||
override fun onReceiveECGLeadStatus(isLeadON: Boolean) { |
|||
loge("onReceiveECGLeadStatus isLeadON ? $isLeadON") |
|||
isLeadOnLd.postValue(isLeadON) |
|||
if (isLeadON) { |
|||
ecgLocalAlgo.start() |
|||
startTs = System.currentTimeMillis() |
|||
countdown = totalSecs |
|||
} else { |
|||
ecgLocalAlgo.stop() |
|||
reset() |
|||
} |
|||
} |
|||
|
|||
override fun onReceivedECGData(data: Int) { |
|||
if(useLocalECGAlgo && (dataSrc == ECG_DATA_SRC_RAW || dataSrc == ECG_DATA_SRC_RAW_WITH_RESULT)) { |
|||
ecgLocalAlgo.inputData(data) |
|||
} else if (isECGMeasuring.value == true && isLeadOnLd.value == true && !(skipZeroData && data == 0) && ecgDataIndex < maxDataSize) { |
|||
skipZeroData = false |
|||
val vol = data.times(wearingHabit).toECGVoltage(dataSrc, PGA_GAIN_4VV) |
|||
ecgDataList.add(vol) |
|||
// ecgDrawWave.addData(vol) |
|||
ecgDataIndex++ |
|||
val newCountdown = totalSecs - ecgDataIndex / sampleRate |
|||
if (newCountdown != countdown) { |
|||
countdown = newCountdown |
|||
} |
|||
if (ecgDataIndex >= maxDataSize) { |
|||
goDetails = true |
|||
toggle() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onReceivedECGResult(result: ECGResult) { |
|||
if (isLeadOnLd.value == true) { |
|||
ecgResult = result |
|||
} |
|||
} |
|||
} |
|||
|
|||
init { |
|||
NexRingManager.get().healthApi().setOnECGMeasurementListener(listener) |
|||
} |
|||
|
|||
override fun onCleared() { |
|||
super.onCleared() |
|||
if (isECGMeasuring.value == true) { |
|||
NexRingManager.get() |
|||
.healthApi() |
|||
.endECG(null) |
|||
} |
|||
NexRingManager.get().healthApi().setOnECGMeasurementListener(null) |
|||
ecgLocalAlgo.destroy() |
|||
} |
|||
|
|||
fun toggle() { |
|||
if (isECGMeasuring.value == true) { |
|||
//代表正在测量中 |
|||
isECGMeasuring.postValue(false) |
|||
isLeadOnLd.postValue(null) |
|||
NexRingManager.get().healthApi().endECG(null) |
|||
ecgLocalAlgo.stop() |
|||
} else { |
|||
goDetails = false |
|||
ecgResult = null |
|||
ecgDataList.clear() |
|||
NexRingManager.get().healthApi() |
|||
.startECG(sampleRate, ecgPgaGain, dataSrc) { |
|||
if (it == 0) |
|||
isECGMeasuring.postValue(true) |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun reset() { |
|||
skipZeroData = true |
|||
ecgDataIndex = 0 |
|||
ecgDataList.clear() |
|||
// ecgDrawWave.clear() |
|||
countdown = 0 |
|||
ecgResult = null |
|||
} |
|||
} |
@ -0,0 +1,60 @@ |
|||
package com.whitefish.app.ext |
|||
|
|||
import android.graphics.Color |
|||
import com.github.mikephil.charting.charts.BarChart |
|||
import com.github.mikephil.charting.charts.LineChart |
|||
import com.github.mikephil.charting.components.XAxis |
|||
|
|||
fun LineChart.clearDraw(){ |
|||
setBackgroundColor(Color.TRANSPARENT) |
|||
setTouchEnabled(false) |
|||
isDragEnabled = false |
|||
setScaleEnabled(false) |
|||
|
|||
//隐藏背景线 |
|||
xAxis.setDrawGridLines(false) |
|||
axisLeft.setDrawGridLines(false) |
|||
axisRight.setDrawGridLines(false) |
|||
setGridBackgroundColor(Color.TRANSPARENT) |
|||
|
|||
//隐藏坐标轴 |
|||
xAxis.isEnabled = false |
|||
axisLeft.isEnabled = false |
|||
axisRight.isEnabled = false |
|||
|
|||
//禁用图例 |
|||
legend.isEnabled = false |
|||
description.isEnabled = false |
|||
} |
|||
|
|||
fun BarChart.clearDraw(){ |
|||
setBackgroundColor(Color.TRANSPARENT) |
|||
setTouchEnabled(false) |
|||
isDragEnabled = false |
|||
setScaleEnabled(false) |
|||
|
|||
//隐藏背景线 |
|||
xAxis.setDrawGridLines(false) |
|||
axisLeft.setDrawGridLines(false) |
|||
axisRight.setDrawGridLines(false) |
|||
setGridBackgroundColor(Color.TRANSPARENT) |
|||
|
|||
//隐藏坐标轴 |
|||
xAxis.isEnabled = false |
|||
axisLeft.isEnabled = false |
|||
axisRight.isEnabled = false |
|||
|
|||
//禁用图例 |
|||
legend.isEnabled = false |
|||
description.isEnabled = false |
|||
} |
|||
|
|||
fun BarChart.customStyle(count:Int){ |
|||
xAxis.position = XAxis.XAxisPosition.BOTTOM |
|||
xAxis.setCenterAxisLabels(true) |
|||
xAxis.setAxisMinimum(0f) |
|||
xAxis.setAxisMaximum(count.toFloat()) |
|||
xAxis.setLabelCount(count) |
|||
xAxis.granularity = 0.1F |
|||
xAxis.isGranularityEnabled = true |
|||
} |
@ -0,0 +1,68 @@ |
|||
package com.whitefish.app.ext |
|||
|
|||
import androidx.lifecycle.DefaultLifecycleObserver |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.lifecycle.LifecycleOwner |
|||
import androidx.lifecycle.lifecycleScope |
|||
import kotlinx.coroutines.CoroutineScope |
|||
import kotlinx.coroutines.Job |
|||
import kotlinx.coroutines.flow.Flow |
|||
import kotlinx.coroutines.launch |
|||
|
|||
inline fun <T> Flow<T>.collectWith( |
|||
lifecycleOwner: LifecycleOwner, |
|||
lifecycleState: Lifecycle.State, |
|||
crossinline action: suspend (T) -> Unit |
|||
){ |
|||
lifecycleOwner.launchWith(lifecycleState){ |
|||
collect{ |
|||
action(it) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
inline fun LifecycleOwner.launchWith( |
|||
lifecycleState: Lifecycle.State, |
|||
crossinline block: suspend CoroutineScope.() -> Unit |
|||
) { |
|||
lifecycle.addObserver(object : DefaultLifecycleObserver { |
|||
var job: Job? = null |
|||
override fun onCreate(owner: LifecycleOwner) { |
|||
launchBlockWhen(Lifecycle.State.CREATED) |
|||
} |
|||
|
|||
override fun onStart(owner: LifecycleOwner) { |
|||
launchBlockWhen(Lifecycle.State.STARTED) |
|||
} |
|||
|
|||
override fun onResume(owner: LifecycleOwner) { |
|||
launchBlockWhen(Lifecycle.State.RESUMED) |
|||
} |
|||
|
|||
override fun onPause(owner: LifecycleOwner) { |
|||
cancelWhen(Lifecycle.State.RESUMED) |
|||
} |
|||
|
|||
override fun onStop(owner: LifecycleOwner) { |
|||
cancelWhen(Lifecycle.State.STARTED) |
|||
} |
|||
|
|||
override fun onDestroy(owner: LifecycleOwner) { |
|||
cancelWhen(Lifecycle.State.CREATED) |
|||
} |
|||
|
|||
private fun launchBlockWhen(state: Lifecycle.State) { |
|||
if (lifecycleState == state) { |
|||
job = lifecycleScope.launch { block() } |
|||
} |
|||
} |
|||
|
|||
private fun cancelWhen(state: Lifecycle.State) { |
|||
if (lifecycleState == state) { |
|||
job?.takeUnless { it.isCancelled }?.cancel() |
|||
job = null |
|||
} |
|||
} |
|||
}) |
|||
} |
@ -0,0 +1,8 @@ |
|||
package com.whitefish.app.ext |
|||
|
|||
import androidx.recyclerview.widget.GridLayoutManager |
|||
|
|||
fun GridLayoutManager.isLeftItem(position: Int,totalColum:Int): Boolean { |
|||
val spanIndex: Int = spanSizeLookup.getSpanIndex(position, totalColum) |
|||
return spanIndex %2 == 0 |
|||
} |
@ -0,0 +1,143 @@ |
|||
package com.whitefish.app.feature.connect |
|||
|
|||
import android.Manifest |
|||
import android.annotation.SuppressLint |
|||
import android.bluetooth.BluetoothAdapter |
|||
import android.bluetooth.BluetoothManager |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.content.pm.PackageManager |
|||
import android.location.LocationManager |
|||
import android.os.Build |
|||
import android.os.Bundle |
|||
import androidx.activity.result.contract.ActivityResultContracts |
|||
import androidx.appcompat.app.AlertDialog |
|||
import androidx.core.app.ActivityCompat |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bt.OnBleConnectionListener |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.databinding.ActivityConnectBinding |
|||
import com.whitefish.app.device.DeviceManager |
|||
import com.whitefish.app.feature.devices.DeviceActivity |
|||
import com.whitefish.app.feature.devices.DevicesViewModel |
|||
import com.whitefish.app.feature.home.HomeActivity |
|||
import com.whitefish.app.utils.goEnableLocationServicePage |
|||
import com.whitefish.app.utils.postDelay |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
|
|||
class ConnectTipActivity : BaseActivity<ActivityConnectBinding, DevicesViewModel>() { |
|||
companion object { |
|||
fun start(context: Context) { |
|||
val intent = Intent(context, ConnectTipActivity::class.java) |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
|
|||
private var permissionChecker = |
|||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { |
|||
checkPermission() |
|||
} |
|||
|
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_connect |
|||
} |
|||
|
|||
override fun setViewModel(): Class<DevicesViewModel> { |
|||
return DevicesViewModel::class.java |
|||
} |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
} |
|||
|
|||
override fun bind() { |
|||
checkPermission() |
|||
mBinding.btLogin.setOnClickListener { |
|||
DeviceActivity.start(this) |
|||
} |
|||
val address = com.whitefish.app.utils.getString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS,"") |
|||
if (address.isNotBlank()){ |
|||
HomeActivity.start(this) |
|||
finish() |
|||
} |
|||
} |
|||
|
|||
|
|||
@SuppressLint("MissingPermission") |
|||
private fun Context.checkPermission() { |
|||
val dinedPermissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { |
|||
checkDeniedPermissions( |
|||
this, |
|||
Manifest.permission.BLUETOOTH_SCAN, |
|||
Manifest.permission.BLUETOOTH_CONNECT |
|||
) |
|||
} else { |
|||
checkDeniedPermissions( |
|||
this, |
|||
Manifest.permission.ACCESS_FINE_LOCATION, |
|||
Manifest.permission.ACCESS_COARSE_LOCATION |
|||
) |
|||
} |
|||
if (dinedPermissions != null) { |
|||
permissionChecker.launch(dinedPermissions) |
|||
return |
|||
} |
|||
if (!locationServiceAllowed()) { |
|||
AlertDialog.Builder(this) |
|||
.setMessage(R.string.dialog_msg_turn_on_location_service) |
|||
.setCancelable(false) |
|||
.setPositiveButton(android.R.string.ok) { _, _ -> |
|||
goEnableLocationServicePage() |
|||
} |
|||
.create().show() |
|||
return |
|||
} |
|||
val bluetoothAdapter = |
|||
(getSystemService(BLUETOOTH_SERVICE) as BluetoothManager).adapter |
|||
if (!bluetoothAdapter.isEnabled) { |
|||
startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) |
|||
|
|||
} |
|||
// app.accountSp.getString(SP_KEY_BOUND_DEVICE_ADDRESS, "").apply { |
|||
// if (isNullOrEmpty()) { |
|||
// findNavController().navigate(R.id.action_Home_to_BindDevice) |
|||
// } else { |
|||
// viewModel.currBtMac = this |
|||
// viewModel.loadDateData() |
|||
// else { |
|||
// startActivity(Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)) |
|||
// } |
|||
// } |
|||
// } |
|||
} |
|||
|
|||
private fun Context.locationServiceAllowed(): Boolean { |
|||
return if (Build.VERSION.SDK_INT in Build.VERSION_CODES.M..Build.VERSION_CODES.R) { |
|||
val manager = getSystemService(Context.LOCATION_SERVICE) as LocationManager |
|||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) manager.isLocationEnabled |
|||
else manager.isProviderEnabled(LocationManager.GPS_PROVIDER) |
|||
} else { |
|||
//Other versions do not need to turn on location services, |
|||
//so it can be considered that location services are turned on. |
|||
true |
|||
} |
|||
} |
|||
|
|||
private fun checkDeniedPermissions( |
|||
context: Context, |
|||
vararg permissions: String, |
|||
): Array<String>? { |
|||
val dinedPermissions: MutableList<String> = ArrayList() |
|||
for (permission in permissions) { |
|||
if (ActivityCompat.checkSelfPermission(context, permission) |
|||
!= PackageManager.PERMISSION_GRANTED |
|||
) { |
|||
dinedPermissions.add(permission) |
|||
} |
|||
} |
|||
return if (dinedPermissions.isEmpty()) null else dinedPermissions.toTypedArray() |
|||
} |
|||
} |
@ -0,0 +1,207 @@ |
|||
package com.whitefish.app.feature.devices |
|||
|
|||
import android.animation.ObjectAnimator |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.graphics.Rect |
|||
import android.os.Bundle |
|||
import android.view.View |
|||
import android.view.animation.LinearInterpolator |
|||
import androidx.appcompat.app.AlertDialog |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bt.OnBleConnectionListener |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.databinding.ActivityDevicesBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.device.DeviceManager |
|||
import com.whitefish.app.feature.userInfo.UserInfoActivity |
|||
import com.whitefish.app.utils.postDelay |
|||
import com.whitefish.app.utils.putString |
|||
import com.whitefish.app.view.ConnectErrorDialog |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
|
|||
class DeviceActivity : BaseActivity<ActivityDevicesBinding, DevicesViewModel>() { |
|||
companion object { |
|||
fun start(context: Context) { |
|||
val intent = Intent(context, DeviceActivity::class.java) |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
private var isConnecting = false |
|||
|
|||
private lateinit var deviceAdapter: DevicesAdapter |
|||
private var loadingAnimator: ObjectAnimator? = null |
|||
|
|||
private val mOnBleConnectionListener = object : OnBleConnectionListener { |
|||
|
|||
override fun onBleState(state: Int) { |
|||
/** |
|||
* when call [factoryReset], device will disconnect. |
|||
*/ |
|||
if (state == BluetoothProfile.STATE_DISCONNECTED) { |
|||
if (isConnecting){ |
|||
ConnectErrorDialog().show(supportFragmentManager,"error") |
|||
} |
|||
deviceAdapter.clear() |
|||
DeviceManager.INSTANCE.scan(this@DeviceActivity) |
|||
} |
|||
} |
|||
|
|||
override fun onBleReady() { |
|||
postDelay { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.getBindState { |
|||
Logger.i("bind state:${it}") |
|||
if (it) { |
|||
AlertDialog.Builder(this@DeviceActivity) |
|||
.setCancelable(false) |
|||
.setTitle(R.string.dialog_title_restricted_mode) |
|||
.setMessage(R.string.dialog_msg_restricted_mode) |
|||
.setNegativeButton(android.R.string.cancel) { _, _ -> |
|||
|
|||
}.setPositiveButton(android.R.string.ok) { _, _ -> |
|||
Logger.i("reset device") |
|||
deviceAdapter.clear() |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.factoryReset() |
|||
switchUI(true) |
|||
postDelay { |
|||
Thread.sleep(200) |
|||
DeviceManager.INSTANCE.scan(this@DeviceActivity) |
|||
} |
|||
isConnecting = false |
|||
}.create().show() |
|||
} else { |
|||
bindDevice() |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun bindDevice() { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.bind { |
|||
Logger.i("bind result:${it}") |
|||
if (it == 0) { |
|||
Logger.i("bind device") |
|||
putString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS, deviceAdapter.data[deviceAdapter.connectingPosition].address) |
|||
UserInfoActivity.start(this@DeviceActivity) |
|||
finish() |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_devices |
|||
} |
|||
|
|||
override fun setViewModel(): Class<DevicesViewModel> { |
|||
return DevicesViewModel::class.java |
|||
} |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
Application.INSTANTS?.bleManager?.addOnBleConnectionListener(mOnBleConnectionListener) |
|||
} |
|||
|
|||
override fun bind() { |
|||
initAdapter(mBinding.rvDevices) |
|||
initLoadingAnimation() |
|||
switchUI(true) |
|||
collectData() |
|||
DeviceManager.INSTANCE.scan(this) |
|||
} |
|||
|
|||
private fun initAdapter(recyclerView: RecyclerView) { |
|||
deviceAdapter = DevicesAdapter { device -> |
|||
isConnecting = true |
|||
DeviceManager.INSTANCE.connect(device.address) |
|||
} |
|||
|
|||
recyclerView.adapter = deviceAdapter |
|||
recyclerView.layoutManager = LinearLayoutManager(this) |
|||
recyclerView.addItemDecoration(object : ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
val position = parent.getChildAdapterPosition(view) |
|||
if (position == RecyclerView.NO_POSITION) { |
|||
return |
|||
} |
|||
//22dp - 上下5dp阴影 |
|||
outRect.bottom = |
|||
resources.getDimensionPixelOffset(R.dimen.device_item_bottom_offset) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
private fun initLoadingAnimation() { |
|||
loadingAnimator = ObjectAnimator.ofFloat(mBinding.ivLoading, View.ROTATION, 0f, 360f).apply { |
|||
duration = 1000 |
|||
repeatCount = ObjectAnimator.INFINITE |
|||
interpolator = LinearInterpolator() |
|||
} |
|||
} |
|||
|
|||
private fun collectData() { |
|||
mViewModel.viewState.collectWith(this, Lifecycle.State.RESUMED) { |
|||
when (it) { |
|||
is ViewState.Success -> { |
|||
switchUI(it.items.isEmpty()) |
|||
deviceAdapter.setData(it.items) |
|||
} |
|||
|
|||
is ViewState.Empty -> {} |
|||
is ViewState.Error -> {} |
|||
is ViewState.Default -> {} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun switchUI(scanning:Boolean) { |
|||
if (!scanning) { |
|||
mBinding.tvTitle.text = getString(R.string.devices_list) |
|||
stopLoadingAnimation() |
|||
mBinding.ivLoading.visibility = View.GONE |
|||
mBinding.rvDevices.visibility = View.VISIBLE |
|||
mBinding.tip.text = getString(R.string.devices_connect_fail) |
|||
} else { |
|||
mBinding.tvTitle.text = getString(R.string.devices_scan) |
|||
mBinding.ivLoading.visibility = View.VISIBLE |
|||
startLoadingAnimation() |
|||
mBinding.rvDevices.visibility = View.GONE |
|||
mBinding.tip.text = getString(R.string.devices_can_not_find) |
|||
} |
|||
} |
|||
|
|||
private fun startLoadingAnimation() { |
|||
loadingAnimator?.start() |
|||
} |
|||
|
|||
private fun stopLoadingAnimation() { |
|||
loadingAnimator?.cancel() |
|||
} |
|||
|
|||
override fun onDestroy() { |
|||
super.onDestroy() |
|||
stopLoadingAnimation() |
|||
Application.INSTANTS?.bleManager?.removeOnBleConnectionListener(mOnBleConnectionListener) |
|||
} |
|||
} |
@ -0,0 +1,100 @@ |
|||
package com.whitefish.app.feature.devices |
|||
|
|||
import android.animation.ObjectAnimator |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import android.view.animation.LinearInterpolator |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bean.Device |
|||
import com.whitefish.app.databinding.ListItemDeviceBinding |
|||
|
|||
class DevicesAdapter( |
|||
private val onItemClickListener: ListItemDeviceBinding.(Device) -> Unit |
|||
) : BaseAdapter<Device, DeviceListItemHolder>() { |
|||
private lateinit var binding: ListItemDeviceBinding |
|||
|
|||
// 记录当前正在连接的项位置 |
|||
var connectingPosition: Int = -1 |
|||
private set |
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DeviceListItemHolder { |
|||
binding = DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_device, |
|||
parent, |
|||
false |
|||
) |
|||
return DeviceListItemHolder(binding) { device, position -> |
|||
// 更新连接状态并刷新 |
|||
connectingPosition = position |
|||
notifyDataSetChanged() |
|||
|
|||
// 调用原始的点击监听器 |
|||
onItemClickListener.invoke(binding, device) |
|||
} |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: DeviceListItemHolder, position: Int) { |
|||
holder.bind(data[position], position, position == connectingPosition) |
|||
} |
|||
|
|||
fun clear(){ |
|||
setData(emptyList()) |
|||
connectingPosition = -1 |
|||
notifyDataSetChanged() |
|||
} |
|||
|
|||
fun resetSelected(){ |
|||
connectingPosition = -1 |
|||
notifyDataSetChanged() |
|||
} |
|||
} |
|||
|
|||
class DeviceListItemHolder( |
|||
private val binding: ListItemDeviceBinding, |
|||
private val onItemClickListener: (Device, Int) -> Unit |
|||
) : ViewHolder(binding.root) { |
|||
private var loadingAnimator: ObjectAnimator? = null |
|||
|
|||
init { |
|||
// 初始化旋转动画 |
|||
loadingAnimator = ObjectAnimator.ofFloat( |
|||
binding.connectLoading, |
|||
View.ROTATION, |
|||
0f, |
|||
360f |
|||
).apply { |
|||
duration = 1000 |
|||
repeatCount = ObjectAnimator.INFINITE |
|||
interpolator = LinearInterpolator() |
|||
} |
|||
} |
|||
|
|||
fun bind(device: Device, position: Int, isConnecting: Boolean) { |
|||
binding.device = device |
|||
|
|||
// 根据连接状态显示或隐藏连接中的视图 |
|||
if (isConnecting) { |
|||
binding.connecting.visibility = View.VISIBLE |
|||
startLoadingAnimation() |
|||
} else { |
|||
binding.connecting.visibility = View.GONE |
|||
stopLoadingAnimation() |
|||
} |
|||
|
|||
binding.root.setOnClickListener { |
|||
onItemClickListener.invoke(device, position) |
|||
} |
|||
} |
|||
|
|||
private fun startLoadingAnimation() { |
|||
loadingAnimator?.start() |
|||
} |
|||
|
|||
private fun stopLoadingAnimation() { |
|||
loadingAnimator?.cancel() |
|||
} |
|||
} |
@ -0,0 +1,61 @@ |
|||
package com.whitefish.app.feature.devices |
|||
|
|||
import androidx.lifecycle.viewModelScope |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.bean.Device |
|||
import com.whitefish.app.bean.toDevice |
|||
import com.whitefish.app.device.DeviceManager |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import kotlinx.coroutines.flow.MutableSharedFlow |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.SharedFlow |
|||
import kotlinx.coroutines.flow.StateFlow |
|||
import kotlinx.coroutines.flow.asSharedFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
|
|||
class DevicesViewModel:BaseViewModel() { |
|||
private val _viewState = MutableSharedFlow<ViewState>(0) |
|||
internal val viewState: SharedFlow<ViewState> = _viewState.asSharedFlow() |
|||
val refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun refresh(){ |
|||
viewModelScope.launch { |
|||
refreshFlow.emit(RefreshEmit()) |
|||
} |
|||
} |
|||
|
|||
|
|||
private fun collect(){ |
|||
viewModelScope.launch { |
|||
DeviceManager.INSTANCE.scanStateFlow.collectLatest { |
|||
val bleDevices = DeviceManager.INSTANCE.scannedDeviceList |
|||
val devices = arrayListOf<Device>() |
|||
bleDevices.forEach { |
|||
devices.add(it.toDevice()) |
|||
} |
|||
|
|||
if (devices.isNotEmpty()){ |
|||
_viewState.emit(ViewState.Success(devices)) |
|||
}else{ |
|||
_viewState.emit(ViewState.Empty) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
internal sealed class ViewState { |
|||
data class Success(val items: MutableList<Device>) : |
|||
ViewState() |
|||
|
|||
data class Error(val error: Exception) : ViewState() |
|||
data object Empty : ViewState() |
|||
data object Default: ViewState() |
|||
} |
@ -0,0 +1,206 @@ |
|||
package com.whitefish.app.feature.home |
|||
|
|||
import android.animation.ObjectAnimator |
|||
import android.bluetooth.BluetoothProfile |
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.content.res.ColorStateList |
|||
import android.os.Bundle |
|||
import android.view.View |
|||
import android.view.animation.LinearInterpolator |
|||
import androidx.core.graphics.toColorInt |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.lifecycle.Lifecycle |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bt.OnBleConnectionListener |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.databinding.ActivityHomeBinding |
|||
import com.whitefish.app.device.DeviceManager |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.home.exercise.ExerciseFragment |
|||
import com.whitefish.app.feature.home.recovery.RecoveryFragment |
|||
import com.whitefish.app.feature.home.setting.SettingFragment |
|||
import com.whitefish.app.feature.home.state.StateFragment |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
|
|||
|
|||
class HomeActivity : BaseActivity<ActivityHomeBinding, HomeViewModel>() { |
|||
private var currentFragmentTag = StateFragment.TAG |
|||
val address = |
|||
com.whitefish.app.utils.getString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS, "") |
|||
|
|||
private var loadingAnimator: ObjectAnimator? = null |
|||
|
|||
private val mOnBleConnectionListener = object : OnBleConnectionListener { |
|||
|
|||
override fun onBleState(state: Int) { |
|||
|
|||
when (state) { |
|||
BluetoothProfile.STATE_DISCONNECTED -> { |
|||
Application.INSTANTS!!.bleManager.connect(address) |
|||
startLoadingAnimation() |
|||
} |
|||
BluetoothProfile.STATE_CONNECTED -> { |
|||
mBinding.loading.visibility = View.GONE |
|||
stopLoadingAnimation() |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun onBleReady() { |
|||
NexRingManager.get() |
|||
.deviceApi() |
|||
.getBatteryInfo { |
|||
Logger.i("power:${it}") |
|||
} |
|||
} |
|||
} |
|||
|
|||
companion object { |
|||
fun start(context: Context) { |
|||
val intent = Intent(context, HomeActivity::class.java) |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_home |
|||
} |
|||
|
|||
override fun setViewModel(): Class<HomeViewModel> { |
|||
return HomeViewModel::class.java |
|||
} |
|||
|
|||
override fun onCreate(savedInstanceState: Bundle?) { |
|||
super.onCreate(savedInstanceState) |
|||
DeviceManager.INSTANCE.registerCb() |
|||
Application.INSTANTS!!.bleManager.addOnBleConnectionListener(mOnBleConnectionListener) |
|||
initLoadingAnimation() |
|||
|
|||
if (Application.INSTANTS!!.bleManager.bleState.value == BluetoothProfile.STATE_CONNECTED){ |
|||
mBinding.loading.visibility= View.GONE |
|||
} else if (Application.INSTANTS!!.bleManager.bleState.value != BluetoothProfile.STATE_CONNECTING){ |
|||
Application.INSTANTS!!.bleManager.connect(address) |
|||
startLoadingAnimation() |
|||
} |
|||
|
|||
Application.INSTANTS!!.bleManager.oemStepComplete.collectWith(this,Lifecycle.State.CREATED){ |
|||
if (it){ |
|||
mViewModel.cacheData() |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun initLoadingAnimation() { |
|||
loadingAnimator = ObjectAnimator.ofFloat(mBinding.icLoading, View.ROTATION, 0f, 360f).apply { |
|||
duration = 1000 |
|||
repeatCount = ObjectAnimator.INFINITE |
|||
interpolator = LinearInterpolator() |
|||
} |
|||
} |
|||
|
|||
private fun startLoadingAnimation() { |
|||
loadingAnimator?.start() |
|||
} |
|||
|
|||
private fun stopLoadingAnimation() { |
|||
loadingAnimator?.cancel() |
|||
} |
|||
|
|||
override fun bind() { |
|||
|
|||
showFragment(currentFragmentTag) |
|||
mBinding.icState.backgroundTintList = ColorStateList.valueOf("#352764".toColorInt()) |
|||
mBinding.icExerices.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icRecovery.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icSetting.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
|
|||
mBinding.llState.setOnClickListener { |
|||
showFragment(StateFragment.TAG) |
|||
mBinding.icState.backgroundTintList = ColorStateList.valueOf("#352764".toColorInt()) |
|||
mBinding.icExerices.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icRecovery.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icSetting.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
} |
|||
|
|||
mBinding.llExercise.setOnClickListener { |
|||
showFragment(ExerciseFragment.TAG) |
|||
mBinding.icExerices.backgroundTintList = ColorStateList.valueOf("#352764".toColorInt()) |
|||
mBinding.icState.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icRecovery.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icSetting.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
} |
|||
|
|||
mBinding.llRecovery.setOnClickListener { |
|||
showFragment(RecoveryFragment.TAG) |
|||
mBinding.icRecovery.backgroundTintList = ColorStateList.valueOf("#352764".toColorInt()) |
|||
mBinding.icExerices.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icState.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icSetting.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
} |
|||
|
|||
mBinding.llSetting.setOnClickListener { |
|||
showFragment(SettingFragment.TAG) |
|||
mBinding.icSetting.backgroundTintList = ColorStateList.valueOf("#352764".toColorInt()) |
|||
mBinding.icExerices.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icRecovery.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
mBinding.icState.backgroundTintList = ColorStateList.valueOf("#636363".toColorInt()) |
|||
} |
|||
|
|||
// AddExerciseDialog().show(supportFragmentManager,"Dialog") |
|||
} |
|||
|
|||
private fun showFragment(targetTag: String) { |
|||
switchContent( |
|||
obtainFragmentInstance(currentFragmentTag), |
|||
obtainFragmentInstance(targetTag), |
|||
targetTag |
|||
) |
|||
} |
|||
|
|||
private fun switchContent(from: Fragment?, to: Fragment, tag: String) { |
|||
currentFragmentTag = tag |
|||
|
|||
supportFragmentManager.beginTransaction().apply { |
|||
|
|||
from?.let { |
|||
hide(from) |
|||
} |
|||
|
|||
if (to.isAdded) |
|||
show(to) |
|||
else |
|||
add(R.id.fragmentContainer, to, tag) |
|||
|
|||
}.commit() |
|||
} |
|||
|
|||
override fun onDestroy() { |
|||
super.onDestroy() |
|||
stopLoadingAnimation() |
|||
DeviceManager.INSTANCE.unregisterCb() |
|||
Application.INSTANTS!!.bleManager.removeOnBleConnectionListener(mOnBleConnectionListener) |
|||
} |
|||
|
|||
private fun obtainFragmentInstance(tag: String): Fragment { |
|||
|
|||
return when (tag) { |
|||
ExerciseFragment.TAG -> supportFragmentManager.findFragmentByTag(ExerciseFragment.TAG) |
|||
?: ExerciseFragment() |
|||
|
|||
RecoveryFragment.TAG -> supportFragmentManager.findFragmentByTag(RecoveryFragment.TAG) |
|||
?: RecoveryFragment() |
|||
|
|||
SettingFragment.TAG -> supportFragmentManager.findFragmentByTag(SettingFragment.TAG) |
|||
?: SettingFragment() |
|||
|
|||
else -> |
|||
supportFragmentManager.findFragmentByTag(StateFragment.TAG) ?: StateFragment() |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
@ -0,0 +1,39 @@ |
|||
package com.whitefish.app.feature.home |
|||
|
|||
import androidx.lifecycle.ViewModel |
|||
import androidx.lifecycle.viewModelScope |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.dao.bean.HeartRateDataBean |
|||
import com.whitefish.app.db.AppDatabase |
|||
import com.whitefish.app.device.DeviceDataProvider |
|||
import com.whitefish.app.utils.getString |
|||
import com.whitefish.app.utils.todayCalendar |
|||
import kotlinx.coroutines.Dispatchers |
|||
import kotlinx.coroutines.launch |
|||
|
|||
|
|||
class HomeViewModel : ViewModel() { |
|||
|
|||
fun cacheData(){ |
|||
viewModelScope.launch(Dispatchers.IO) { |
|||
val address = getString( |
|||
Constants.SP_KEY_BOUND_DEVICE_ADDRESS, |
|||
"" |
|||
) |
|||
if (address.isNotBlank()) { |
|||
val hrList = DeviceDataProvider.INSTANCE.getAllHrList(todayCalendar().timeInMillis,System.currentTimeMillis()).map { |
|||
HeartRateDataBean(it.ts,it.value) |
|||
} |
|||
if (hrList.isNotEmpty()){ |
|||
AppDatabase.getDatabase().heartRateDao().putHeartRate(hrList) |
|||
} |
|||
Logger.i("cache hr data:${hrList}") |
|||
val sleepData = DeviceDataProvider.INSTANCE.getSleepData(todayCalendar()) |
|||
sleepData?.let { AppDatabase.getDatabase().sleepDao().insert(it) } |
|||
Logger.i("cache sleep data:${sleepData}") |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,607 @@ |
|||
package com.whitefish.app.feature.home |
|||
|
|||
import android.graphics.Color |
|||
import android.util.Log |
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import androidx.core.content.ContextCompat |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.github.mikephil.charting.components.YAxis |
|||
import com.github.mikephil.charting.data.BarData |
|||
import com.github.mikephil.charting.data.Entry |
|||
import com.github.mikephil.charting.data.LineData |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bean.Evaluate |
|||
import com.whitefish.app.bean.Exercise |
|||
import com.whitefish.app.bean.SleepState |
|||
import com.whitefish.app.chart.ResizedDrawable |
|||
import com.whitefish.app.chart.RightAlignedValueFormatter |
|||
import com.whitefish.app.chart.VerticalRadiusBarChartRender |
|||
import com.whitefish.app.databinding.* |
|||
import com.whitefish.app.ext.clearDraw |
|||
import com.whitefish.app.ext.customStyle |
|||
import com.whitefish.app.feature.home.bean.* |
|||
import com.whitefish.app.view.SleepChartView |
|||
import kotlin.math.absoluteValue |
|||
|
|||
class StateAdapter : BaseAdapter<Any, ViewHolder>() { |
|||
|
|||
enum class ViewType { |
|||
CARD_EXERCISE_TARGET, |
|||
CARD_RECOVERY_SCORE, |
|||
CARD_HEART_RATE_HALF_ROW, |
|||
CARD_BAR_CHART_SMALL, |
|||
CARD_EXERCISE_HISTORY, |
|||
CARD_TEMPERATURE_LINE_CHART_FULL_ROW, |
|||
CARD_FOOT, |
|||
CARD_TIPS, |
|||
CARD_EVALUATE, |
|||
CARD_SLEEP_STATE, |
|||
CARD_HEART_RATE_FULL_ROW, |
|||
CARD_BLOOD_OXYGEN_FULL_ROW, |
|||
CARD_HEART_HEALTH |
|||
} |
|||
|
|||
companion object { |
|||
const val TAG = "StateAdapter" |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
|||
|
|||
if (viewType == ViewType.CARD_EXERCISE_TARGET.ordinal) { |
|||
return ListExerciseTargetHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_exercise_target, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_RECOVERY_SCORE.ordinal) { |
|||
return ListRecoveryScoreHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_recovery_score, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_HEART_RATE_HALF_ROW.ordinal) { |
|||
return ListHeartRateHalfRowCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_heart_rate_half_row, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_EXERCISE_HISTORY.ordinal) { |
|||
return ListExerciseHistoryHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_exercise_history, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_TEMPERATURE_LINE_CHART_FULL_ROW.ordinal) { |
|||
return ListTemperatureLineFullRowCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_temperature_line_chart_full_row, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_FOOT.ordinal) { |
|||
return ListFootHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_foot, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_TIPS.ordinal) { |
|||
return ListTipsCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_tips, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_EVALUATE.ordinal) { |
|||
return ListEvaluateCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_evaluate, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_SLEEP_STATE.ordinal) { |
|||
return ListSleepStateCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_sleep_state, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_HEART_RATE_FULL_ROW.ordinal) { |
|||
return ListHeartRateFullRowCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_heart_rate_full_row, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_BLOOD_OXYGEN_FULL_ROW.ordinal) { |
|||
return ListBloodOxygenFullRowCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_blood_oxygen_full_row, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
if (viewType == ViewType.CARD_HEART_HEALTH.ordinal) { |
|||
return ListHeartHealthCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_heart_health, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
return ListBarSmallCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_bar_chart_small, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
|||
val item = data[position] |
|||
//todo homepage |
|||
when (getItemViewType(position)) { |
|||
ViewType.CARD_EXERCISE_TARGET.ordinal -> { |
|||
(holder as ListExerciseTargetHolder).run { |
|||
bind(item as ExerciseTarget) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_HEART_RATE_HALF_ROW.ordinal -> { |
|||
(holder as ListHeartRateHalfRowCardHolder).run { |
|||
bind(item as HeardRate) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_RECOVERY_SCORE.ordinal -> { |
|||
(holder as ListRecoveryScoreHolder).run { |
|||
bind(item as RecoveryScore) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_BAR_CHART_SMALL.ordinal -> { |
|||
(holder as ListBarSmallCardHolder).run { |
|||
bind(item as HalfRowBarChart) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_EXERCISE_HISTORY.ordinal -> { |
|||
(holder as ListExerciseHistoryHolder).run { |
|||
bind(item as ExerciseHistory) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_TEMPERATURE_LINE_CHART_FULL_ROW.ordinal -> { |
|||
(holder as ListTemperatureLineFullRowCardHolder).run { |
|||
bind(item as Temperature) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_TIPS.ordinal -> { |
|||
(holder as ListTipsCardHolder).run { |
|||
bind(item as Tips) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_FOOT.ordinal -> { |
|||
(holder as ListFootHolder) |
|||
} |
|||
|
|||
ViewType.CARD_SLEEP_STATE.ordinal -> { |
|||
(holder as ListSleepStateCardHolder).run { |
|||
bind(item as SleepState) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_EVALUATE.ordinal -> { |
|||
(holder as ListEvaluateCardHolder).run { |
|||
bind(item as Evaluate) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_HEART_RATE_FULL_ROW.ordinal -> { |
|||
(holder as ListHeartRateFullRowCardHolder).run { |
|||
bind(item as HeardRate) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_BLOOD_OXYGEN_FULL_ROW.ordinal -> { |
|||
(holder as ListBloodOxygenFullRowCardHolder).run { |
|||
bind(item as BloodOxygen) |
|||
} |
|||
} |
|||
|
|||
ViewType.CARD_HEART_HEALTH.ordinal -> { |
|||
(holder as ListHeartHealthCardHolder).run { |
|||
bind(item as HeartHealth) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun getItemViewType(position: Int): Int { |
|||
return when (val item = data[position]) { |
|||
is ExerciseTarget -> { |
|||
ViewType.CARD_EXERCISE_TARGET.ordinal |
|||
} |
|||
|
|||
is HeardRate -> { |
|||
if (item.cardSize == HeardRate.SIZE_HALF_ROW) { |
|||
ViewType.CARD_HEART_RATE_HALF_ROW.ordinal |
|||
} else { |
|||
ViewType.CARD_HEART_RATE_FULL_ROW.ordinal |
|||
} |
|||
} |
|||
|
|||
is HalfRowBarChart -> { |
|||
ViewType.CARD_BAR_CHART_SMALL.ordinal |
|||
} |
|||
|
|||
is RecoveryScore -> { |
|||
ViewType.CARD_RECOVERY_SCORE.ordinal |
|||
} |
|||
|
|||
is ExerciseHistory -> { |
|||
ViewType.CARD_EXERCISE_HISTORY.ordinal |
|||
} |
|||
|
|||
is Temperature -> { |
|||
ViewType.CARD_TEMPERATURE_LINE_CHART_FULL_ROW.ordinal |
|||
} |
|||
|
|||
is Tips -> { |
|||
ViewType.CARD_TIPS.ordinal |
|||
} |
|||
|
|||
is Evaluate -> { |
|||
ViewType.CARD_EVALUATE.ordinal |
|||
} |
|||
|
|||
is BloodOxygen -> { |
|||
ViewType.CARD_BLOOD_OXYGEN_FULL_ROW.ordinal |
|||
} |
|||
|
|||
is SleepState -> { |
|||
ViewType.CARD_SLEEP_STATE.ordinal |
|||
} |
|||
|
|||
is HeartHealth -> { |
|||
ViewType.CARD_HEART_HEALTH.ordinal |
|||
} |
|||
|
|||
else -> { |
|||
ViewType.CARD_FOOT.ordinal |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
class ListExerciseTargetHolder(val binding: CardExerciseTargetBinding) : ViewHolder(binding.root) { |
|||
fun bind(data: ExerciseTarget) { |
|||
binding.cpvProgress.setProgress(data.progress) |
|||
binding.score = data.score |
|||
} |
|||
} |
|||
|
|||
class ListRecoveryScoreHolder(val binding: CardRecoveryScoreBinding) : ViewHolder(binding.root) { |
|||
fun bind(score: RecoveryScore) { |
|||
binding.score = score |
|||
binding.tvScore.text = score.score.toString()//databinding 不能自动转String,傻逼 |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
class ListHeartRateHalfRowCardHolder(val binding: CardHeartRateHalfRowBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(data: HeardRate) { |
|||
|
|||
binding.date.text = data.date |
|||
binding.tvHeartRate.text = data.rate + "次/分钟" |
|||
//设置画布 |
|||
binding.chart.clearDraw() |
|||
|
|||
//设置统计图 |
|||
data.lineData.apply { |
|||
setDrawFilled(true) |
|||
fillDrawable = |
|||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_line_gradient) |
|||
setColor(Color.RED) |
|||
setDrawCircles(false) |
|||
setDrawValues(false) |
|||
} |
|||
binding.chart.data = LineData(data.lineData) |
|||
Logger.i("heard rate:${data.lineData}") |
|||
binding.chart.invalidate() |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
class ListBarSmallCardHolder(val binding: CardBarChartSmallBinding) : ViewHolder(binding.root) { |
|||
fun bind(barData: HalfRowBarChart) { |
|||
binding.data = barData |
|||
|
|||
barData.data.isHighlightEnabled = false |
|||
val data = BarData(barData.data) |
|||
data.setDrawValues(false) |
|||
|
|||
val barWidth = 0.6f // 柱体宽度,这个值越小,柱体越窄 |
|||
data.barWidth = barWidth |
|||
|
|||
binding.chart.customStyle(barData.data.entryCount) |
|||
|
|||
|
|||
binding.chart.renderer = |
|||
VerticalRadiusBarChartRender( |
|||
binding.chart, |
|||
binding.chart.animator, |
|||
binding.chart.viewPortHandler |
|||
).apply { |
|||
setRadius(15) |
|||
} |
|||
|
|||
binding.chart.clearDraw() |
|||
|
|||
binding.chart.axisLeft.setAxisMinimum(0f) |
|||
binding.chart.setVisibleXRange(0f, barData.data.entryCount.toFloat() + 1) |
|||
binding.chart.setData(data) |
|||
binding.chart.invalidate() |
|||
|
|||
|
|||
} |
|||
} |
|||
|
|||
class ListExerciseHistoryHolder(val binding: CardExerciseHistoryBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(history: ExerciseHistory) { |
|||
//todo need change |
|||
binding.totalTime.text = history.time |
|||
binding.totalDistance.text = history.alreadyComplete |
|||
binding.totalSpeed.text = history.frequency |
|||
} |
|||
} |
|||
|
|||
class ListTemperatureLineFullRowCardHolder(val binding: CardTemperatureLineChartFullRowBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(temperature: Temperature) { |
|||
val dataSet = temperature.dataSet |
|||
val dataList = dataSet.values |
|||
|
|||
if (!dataList.isNullOrEmpty()) { |
|||
val yAxis = binding.chart.axisLeft |
|||
|
|||
val max = dataList.maxOfOrNull { |
|||
it.y |
|||
} |
|||
|
|||
val min = dataList.minOfOrNull { |
|||
it.y |
|||
} |
|||
|
|||
yAxis.axisMinimum = min!! - 2 |
|||
yAxis.axisMaximum = max!! + 2 |
|||
|
|||
dataSet.setDrawCircles(false) |
|||
|
|||
dataSet.axisDependency = YAxis.AxisDependency.LEFT |
|||
dataSet.mode = LineDataSet.Mode.CUBIC_BEZIER |
|||
dataSet.color = Color.GREEN |
|||
dataSet.setDrawValues(false) |
|||
val lineData = LineData(dataSet) |
|||
|
|||
binding.chart.clearDraw() |
|||
binding.chart.setData(lineData) |
|||
binding.chart.invalidate() |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
class ListTipsCardHolder(val binding: CardTipsBinding) : ViewHolder(binding.root) { |
|||
fun bind(tips: Tips) { |
|||
binding.tips = tips |
|||
} |
|||
} |
|||
|
|||
class ListEvaluateCardHolder(val binding: CardEvaluateBinding) : ViewHolder(binding.root) { |
|||
fun bind(evaluate: Evaluate) { |
|||
//todo 根据数值改变进度和颜色 |
|||
binding.cpvProgress.setProgress(evaluate.progress) |
|||
binding.tvScore.text = evaluate.score.toString() |
|||
|
|||
binding.lpvBenefit.apply { |
|||
setProgressColor(Color.RED) |
|||
setProgress(evaluate.benefit) |
|||
} |
|||
binding.lpvEfficiency.apply { |
|||
setProgressColor(Color.RED) |
|||
setProgress(evaluate.efficiency) |
|||
} |
|||
binding.lpvExercise.apply { |
|||
setProgressColor(Color.RED) |
|||
setProgress(evaluate.exercise) |
|||
} |
|||
binding.lpvTime.apply { |
|||
setProgressColor(Color.RED) |
|||
setProgress(evaluate.time) |
|||
} |
|||
} |
|||
} |
|||
|
|||
class ListHeartRateFullRowCardHolder(val binding: CardHeartRateFullRowBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(data: HeardRate) { |
|||
binding.data = data |
|||
//设置画布 |
|||
binding.chart.clearDraw() |
|||
|
|||
//设置统计图 |
|||
data.lineData.apply { |
|||
setDrawFilled(true) |
|||
fillDrawable = |
|||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_line_gradient) |
|||
setColor(binding.root.context.getColor(R.color.heart_rate_line)) |
|||
setDrawCircles(false) |
|||
setDrawValues(false) |
|||
} |
|||
Log.d("StateAdapter", "${data.lineData}") |
|||
binding.chart.data = LineData(data.lineData) |
|||
binding.chart.invalidate() |
|||
} |
|||
} |
|||
|
|||
class ListSleepStateCardHolder(val binding: CardSleepStateBinding):ViewHolder(binding.root) { |
|||
fun bind(sleep: SleepState) { |
|||
binding.date.text = sleep.date |
|||
binding.chart.setSleepData(sleep.sleepChartData) |
|||
} |
|||
|
|||
} |
|||
|
|||
class ListBloodOxygenFullRowCardHolder(val binding: CardBloodOxygenFullRowBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(state: BloodOxygen) { |
|||
binding.data = state |
|||
state.barDataSet.color = binding.root.context.getColor(R.color.blood_oxygen_bar) |
|||
state.barDataSet.isHighlightEnabled = false |
|||
val data = BarData(state.barDataSet) |
|||
data.setDrawValues(false) |
|||
|
|||
data.barWidth = 0.6f |
|||
|
|||
binding.chart.customStyle(state.barDataSet.entryCount) |
|||
|
|||
|
|||
binding.chart.renderer = |
|||
VerticalRadiusBarChartRender( |
|||
binding.chart, |
|||
binding.chart.animator, |
|||
binding.chart.viewPortHandler |
|||
).apply { |
|||
setRadius(15) |
|||
} |
|||
|
|||
binding.chart.clearDraw() |
|||
|
|||
binding.chart.axisLeft.setAxisMinimum(0f) |
|||
binding.chart.setVisibleXRange(0f, state.barDataSet.entryCount.toFloat() + 1) |
|||
binding.chart.setData(data) |
|||
binding.chart.invalidate() |
|||
|
|||
} |
|||
} |
|||
|
|||
class ListHeartHealthCardHolder(val binding: CardHeartHealthBinding) : ViewHolder(binding.root) { |
|||
fun bind(data: HeartHealth) { |
|||
binding.data = data |
|||
val dataSet = data.dataSet |
|||
val dataList = dataSet.values |
|||
|
|||
|
|||
if (!dataList.isNullOrEmpty()) { |
|||
val yAxis = binding.chart.axisLeft |
|||
val xAxis = binding.chart.xAxis |
|||
val max = dataList.maxOfOrNull { |
|||
it.y.absoluteValue |
|||
} |
|||
|
|||
yAxis.axisMinimum = 0 - 2 - max!! |
|||
yAxis.axisMaximum = max + 2 |
|||
|
|||
|
|||
dataSet.setDrawCircles(false) |
|||
dataSet.axisDependency = YAxis.AxisDependency.LEFT |
|||
dataSet.mode = LineDataSet.Mode.CUBIC_BEZIER |
|||
dataSet.setDrawValues(false) |
|||
|
|||
//设置发光点 |
|||
val lastPoint = dataSet.values.last() |
|||
lastPoint.icon = ResizedDrawable(binding.root.context, R.drawable.bg_hold, 100, 100) |
|||
val lastPointSet = LineDataSet(arrayListOf(lastPoint), "") |
|||
lastPointSet.setDrawIcons(true) |
|||
lastPointSet.setDrawValues(false) |
|||
|
|||
//设置文本点位 |
|||
val textPoint = lastPoint.copy() |
|||
textPoint.y -= 1f |
|||
textPoint.x += 1f |
|||
val textValuePointSet = LineDataSet(arrayListOf(textPoint), "") |
|||
textValuePointSet.setDrawIcons(false) |
|||
textValuePointSet.setDrawCircles(false) |
|||
textValuePointSet.valueFormatter = RightAlignedValueFormatter() |
|||
textValuePointSet.valueTextColor = Color.CYAN |
|||
textValuePointSet.valueTextSize = 15f |
|||
|
|||
|
|||
val lineData = LineData(dataSet, lastPointSet, textValuePointSet) |
|||
|
|||
binding.chart.clearDraw() |
|||
xAxis.setAxisMaximum(dataList.size + 2f) |
|||
binding.chart.setDrawMarkers(true) |
|||
binding.chart.setData(lineData) |
|||
binding.chart.invalidate() |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
class ListFootHolder(binding: CardFootBinding) : ViewHolder(binding.root) |
@ -0,0 +1,5 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.BarDataSet |
|||
|
|||
data class BloodOxygen(val avg:String,val barDataSet: BarDataSet) |
@ -0,0 +1,7 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
data class ExerciseHistory( |
|||
val alreadyComplete:String, |
|||
val time:String, |
|||
val frequency:String |
|||
) |
@ -0,0 +1,3 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
data class ExerciseTarget(val progress:Int,val score:String) |
@ -0,0 +1,20 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.BarDataSet |
|||
import com.github.mikephil.charting.data.BarEntry |
|||
|
|||
data class HalfRowBarChart(val title:String,val date:String, val detail:String,val suffix:String, val data:BarDataSet){ |
|||
companion object{ |
|||
/** |
|||
* @param valueColorMap 设置数值与颜色的映射关系 |
|||
*/ |
|||
fun build(title:String,date:String,detail:String,suffix:String,data:BarDataSet,valueColorMap:(BarEntry) -> Int):HalfRowBarChart{ |
|||
val colors = ArrayList<Int>() |
|||
data.values.forEach{ barEntry -> |
|||
colors.add(valueColorMap.invoke(barEntry)) |
|||
} |
|||
data.colors = colors |
|||
return HalfRowBarChart(title,date,detail,suffix, data) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,10 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
|
|||
data class HeardRate(val cardSize:Int, val lineData : LineDataSet, val rate:String, val date:String,val description:String){ |
|||
companion object{ |
|||
const val SIZE_HALF_ROW = 1 |
|||
const val SIZE_FULL_ROW = 2 |
|||
} |
|||
} |
@ -0,0 +1,9 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
|
|||
data class HeartHealth( |
|||
val state:String, |
|||
val description:String, |
|||
val dataSet: LineDataSet |
|||
) |
@ -0,0 +1,8 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
|
|||
data class RecoveryLineChart( |
|||
val targetScore:Float, |
|||
val lineChartData: LineDataSet |
|||
) |
@ -0,0 +1,11 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import java.util.Calendar |
|||
import java.util.Date |
|||
|
|||
data class RecoveryScore( |
|||
val score:Int, |
|||
val recoveryTime:String, |
|||
val tips:String, |
|||
val date: Calendar, |
|||
) |
@ -0,0 +1,3 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
data class Row(val data:Pair<Any?,Any?>) |
@ -0,0 +1,16 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
|
|||
enum class SettingType{ |
|||
FULL_ROW_BLOCK, |
|||
HALF_ROW_BLOCK, |
|||
TITLE, |
|||
FULL_ROW_LINE |
|||
} |
|||
|
|||
data class Setting( |
|||
val title: String, |
|||
val subTitle:String? = null, |
|||
val icon: Int? = null, |
|||
val type:SettingType |
|||
) |
@ -0,0 +1,9 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
|
|||
data class Temperature( |
|||
val date:String, |
|||
val temperature:String, |
|||
val dataSet:LineDataSet |
|||
) |
@ -0,0 +1,5 @@ |
|||
package com.whitefish.app.feature.home.bean |
|||
|
|||
class Tips( |
|||
val content: String |
|||
) |
@ -0,0 +1,83 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.os.Bundle |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import android.widget.Toast |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.lifecycle.Lifecycle |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.dao.bean.WorkoutType |
|||
import com.whitefish.app.databinding.DialogAddExerciseBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.running.RunningActivity |
|||
|
|||
|
|||
class AddExerciseDialog:Fragment() { |
|||
companion object{ |
|||
const val TAG = "AddExerciseDialog" |
|||
} |
|||
private lateinit var _binding:DialogAddExerciseBinding |
|||
private val context by lazy { |
|||
parentFragment as ExerciseDialog |
|||
} |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View { |
|||
_binding = DataBindingUtil.inflate(inflater, R.layout.dialog_add_exercise,container,false) |
|||
return _binding.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
bind() |
|||
} |
|||
|
|||
|
|||
private fun bind(){ |
|||
|
|||
_binding.clToSetTarget.setOnClickListener { |
|||
context.showFragment(SetTargetDialog.TAG) |
|||
} |
|||
|
|||
_binding.ivGo.setOnClickListener { |
|||
if (!Application.INSTANTS!!.bleManager.oemStepComplete.value){ |
|||
Toast.makeText(Application.INSTANTS!!,"OEM检查中", Toast.LENGTH_SHORT).show() |
|||
return@setOnClickListener |
|||
} |
|||
|
|||
context.hide() |
|||
val type = if (_binding.tab.selectedTabPosition == 0){ |
|||
WorkoutType.WORKOUT_TYPE_OUTDOOR_RUNNING |
|||
}else{ |
|||
WorkoutType.WORKOUT_TYPE_INDOOR_RUNNING |
|||
} |
|||
RunningActivity.start(requireContext(),10,type,context.viewModel.selectType.value) |
|||
} |
|||
|
|||
context.viewModel.selectType.collectWith(viewLifecycleOwner,Lifecycle.State.CREATED){ |
|||
when(it){ |
|||
DialogExerciseViewModel.TARGET_TYPE_FREE -> { |
|||
_binding.tvTarget.text = "无目标" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_HEAT -> { |
|||
_binding.tvTarget.text = "热量" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_TIME -> { |
|||
_binding.tvTarget.text = "时间" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_DISTANCE -> { |
|||
_binding.tvTarget.text = "距离" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
|
|||
} |
@ -0,0 +1,54 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import androidx.lifecycle.viewModelScope |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.data.FakeRepository |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
|
|||
class DialogExerciseViewModel:BaseViewModel() { |
|||
companion object{ |
|||
const val TYPE_GAP = 1 |
|||
const val TYPE_DISTANCE = 2 |
|||
|
|||
const val TARGET_TYPE_DISTANCE = 0 |
|||
const val TARGET_TYPE_TIME = 1 |
|||
const val TARGET_TYPE_HEAT = 2 |
|||
const val TARGET_TYPE_FREE = 3 |
|||
|
|||
} |
|||
val listFlow = MutableStateFlow(emptyList<String>()) |
|||
private val refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
|
|||
val selectType = MutableStateFlow(TARGET_TYPE_FREE) |
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun refresh(type:Int){ |
|||
viewModelScope.launch { |
|||
refreshFlow.emit(RefreshEmit(type)) |
|||
} |
|||
} |
|||
|
|||
private fun collect(){ |
|||
viewModelScope.launch { |
|||
refreshFlow.collectLatest { |
|||
when(it.code){ |
|||
TYPE_DISTANCE -> { |
|||
listFlow.value = FakeRepository.distanceListData() |
|||
} |
|||
|
|||
TYPE_GAP -> { |
|||
listFlow.value = FakeRepository.gapListData() |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.databinding.ViewDataBinding |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.bumptech.glide.Glide |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ListItemExerciseBinding |
|||
|
|||
class ExerciseAdapter:BaseAdapter<String, ExerciseHolder>() { |
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ExerciseHolder { |
|||
return ExerciseHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context),R.layout.list_item_exercise,parent,false |
|||
) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ExerciseHolder, position: Int) { |
|||
holder.bind(data[position]) |
|||
} |
|||
} |
|||
|
|||
class ExerciseHolder(private val binding:ListItemExerciseBinding):ViewHolder(binding.root){ |
|||
fun bind(name:String){ |
|||
binding.name = name |
|||
when(binding.name){ |
|||
"跑步" -> { |
|||
Glide.with(binding.root).load(R.drawable.running).into(binding.image) |
|||
} |
|||
"骑行" -> { |
|||
Glide.with(binding.root).load(R.drawable.riding).into(binding.image) |
|||
} |
|||
"游泳" -> { |
|||
Glide.with(binding.root).load(R.drawable.swimming).into(binding.image) |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,98 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.graphics.Color |
|||
import android.graphics.drawable.ColorDrawable |
|||
import android.os.Bundle |
|||
import android.view.Gravity |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import android.view.WindowManager |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.fragment.app.DialogFragment |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.fragment.app.viewModels |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.DialogExerciseBinding |
|||
|
|||
|
|||
class ExerciseDialog : DialogFragment() { |
|||
private lateinit var _binding: DialogExerciseBinding |
|||
private var currentFragmentTag = "" |
|||
val viewModel by viewModels<DialogExerciseViewModel>() |
|||
|
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View { |
|||
_binding = DataBindingUtil.inflate(inflater, R.layout.dialog_exercise, container, false) |
|||
return _binding.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
setWindow() |
|||
showFragment(AddExerciseDialog.TAG) |
|||
_binding.container.setOnClickListener { |
|||
dismiss() |
|||
} |
|||
} |
|||
|
|||
|
|||
fun showFragment(targetTag: String) { |
|||
switchContent( |
|||
obtainFragmentInstance(currentFragmentTag), |
|||
obtainFragmentInstance(targetTag), |
|||
targetTag |
|||
) |
|||
} |
|||
|
|||
private fun obtainFragmentInstance(tag: String): Fragment { |
|||
|
|||
return if (tag == AddExerciseDialog.TAG) { |
|||
childFragmentManager.findFragmentByTag(AddExerciseDialog.TAG) |
|||
?: AddExerciseDialog() |
|||
}else{ |
|||
childFragmentManager.findFragmentByTag(SetTargetDialog.TAG)?: SetTargetDialog() |
|||
} |
|||
} |
|||
|
|||
private fun switchContent(from: Fragment?, to: Fragment, tag: String) { |
|||
currentFragmentTag = tag |
|||
|
|||
childFragmentManager.beginTransaction().apply { |
|||
|
|||
from?.let { |
|||
hide(from) |
|||
} |
|||
|
|||
if (to.isAdded) |
|||
show(to) |
|||
else |
|||
add(R.id.dialogFragmentContainer, to, tag) |
|||
|
|||
}.commit() |
|||
} |
|||
|
|||
private fun setWindow() { |
|||
val window = dialog!!.window |
|||
window?.decorView?.setPadding(0, 0, 0, 0) |
|||
val layoutParams = window?.attributes |
|||
layoutParams?.width = WindowManager.LayoutParams.MATCH_PARENT |
|||
layoutParams?.height = WindowManager.LayoutParams.MATCH_PARENT |
|||
window?.attributes = layoutParams |
|||
window?.decorView?.setBackgroundColor(Color.TRANSPARENT) |
|||
window?.setGravity(Gravity.CENTER) |
|||
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) |
|||
|
|||
// 添加这一行来禁用背景调光效果 |
|||
window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) |
|||
} |
|||
|
|||
fun hide(){ |
|||
this.dismiss() |
|||
} |
|||
|
|||
} |
@ -0,0 +1,91 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.graphics.Rect |
|||
import android.util.Log |
|||
import android.view.View |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.lifecycle.ViewModelProvider |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentExerciseBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.home.StateAdapter |
|||
|
|||
class ExerciseFragment:BaseFragment<FragmentExerciseBinding, ExerciseViewModel>() { |
|||
companion object{ |
|||
const val TAG = "ExerciseFragment" |
|||
} |
|||
|
|||
|
|||
val exerciseStateAdapter = StateAdapter() |
|||
|
|||
override fun bind() { |
|||
mBinding.vm = mViewModel |
|||
initExerciseAdapter() |
|||
initStateAdapter() |
|||
collect() |
|||
} |
|||
|
|||
|
|||
override fun onHiddenChanged(hidden: Boolean) { |
|||
super.onHiddenChanged(hidden) |
|||
if (!hidden){ |
|||
mViewModel.refresh() |
|||
} |
|||
} |
|||
|
|||
private fun initExerciseAdapter(){ |
|||
val exerciseAdapter = ExerciseAdapter() |
|||
mBinding.rvExercise.adapter = exerciseAdapter |
|||
mBinding.rvExercise.layoutManager = LinearLayoutManager(requireContext(),LinearLayoutManager.HORIZONTAL,false) |
|||
mBinding.rvExercise.addItemDecoration(object : ItemDecoration(){ |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
outRect.right = resources.getDimensionPixelOffset(R.dimen.exercise_card_bottom_offset) |
|||
} |
|||
}) |
|||
exerciseAdapter.setData(mViewModel.exerciseList) |
|||
mBinding.clAddExerciseHistory.setOnClickListener { |
|||
ExerciseDialog().show(requireActivity().supportFragmentManager, "Dialog") |
|||
} |
|||
} |
|||
|
|||
private fun initStateAdapter(){ |
|||
mBinding.rvState.adapter = exerciseStateAdapter |
|||
mBinding.rvState.layoutManager = LinearLayoutManager(requireContext()) |
|||
mBinding.rvState.addItemDecoration(object : ItemDecoration(){ |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
outRect.bottom = resources.getDimensionPixelOffset(R.dimen.exercise_state_card_bottom_offset) |
|||
} |
|||
}) |
|||
} |
|||
|
|||
private fun collect(){ |
|||
mViewModel.exerciseStateFlow.collectWith(viewLifecycleOwner,Lifecycle.State.RESUMED){ |
|||
Log.d(TAG,"data: ${it}") |
|||
exerciseStateAdapter.setData(it.exerciseState) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_exercise |
|||
} |
|||
|
|||
override fun setViewModel(): ExerciseViewModel { |
|||
return ViewModelProvider(this)[ExerciseViewModel::class.java] |
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import androidx.lifecycle.viewModelScope |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.bean.Evaluate |
|||
import com.whitefish.app.bean.Exercise |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.data.FakeRepository |
|||
import com.whitefish.app.db.AppDatabase |
|||
import com.whitefish.app.feature.home.bean.ExerciseHistory |
|||
import com.whitefish.app.feature.home.bean.Tips |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import com.whitefish.app.utils.getString |
|||
import com.whitefish.app.utils.toTimeString |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import lib.linktop.nexring.api.Statistics |
|||
import lib.linktop.nexring.api.WorkoutData |
|||
import java.text.NumberFormat |
|||
|
|||
class ExerciseViewModel : BaseViewModel() { |
|||
val exerciseStateFlow = MutableStateFlow(Exercise("", "", 0, 0, emptyList())) |
|||
val exerciseList = arrayListOf("跑步", "骑行", "游泳") |
|||
private val refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
val dao = AppDatabase.getDatabase().workoutDao() |
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun refresh() { |
|||
refreshFlow.value = RefreshEmit() |
|||
} |
|||
|
|||
private fun collect() { |
|||
|
|||
viewModelScope.launch { |
|||
refreshFlow.collectLatest { |
|||
/** |
|||
* 1 StepLength (m) = 0.45 × Height (cm) ÷ 100 |
|||
* @note No height data was collected, use 170 |
|||
* |
|||
* 2 Distance (m) = StepLength × Steps |
|||
* 3 Calories (Cal) = Distance × WeightCoefficients[Intensity] |
|||
*/ |
|||
val workout = getLastWorkout() |
|||
|
|||
var totalTime = workout?.second?.run { |
|||
if (workout.second.size > 1) { |
|||
last().ts.minus(first().ts) |
|||
} else { |
|||
last().ts |
|||
} |
|||
} |
|||
val totalSteps = workout?.second?.run { |
|||
if (workout.second.size > 1) { |
|||
last().steps.minus(first().steps) |
|||
} else { |
|||
last().steps |
|||
} |
|||
} |
|||
|
|||
val stepLength = 0.45 * 170 / 100 |
|||
val totalDistance = totalSteps?.times(stepLength) |
|||
val lastDbWorkout = dao.getAllWorkouts().firstOrNull() |
|||
val distanceForKm = lastDbWorkout?.let { |
|||
val totalMs = (it.endTs - it.startTs) |
|||
val totalTime = totalMs.div(100).div(60) |
|||
totalDistance?.div(totalTime) |
|||
?.div(1000) |
|||
}?:0 |
|||
|
|||
val uiList = arrayListOf( |
|||
ExerciseHistory( |
|||
"$totalDistance 米", |
|||
(totalTime?.div(1000))?.toInt()?.toTimeString().toString(), |
|||
"${NumberFormat.getNumberInstance().apply { |
|||
maximumFractionDigits = 2 |
|||
isGroupingUsed = false |
|||
}.format(distanceForKm)}公里/分钟"//todo get length for 1000m |
|||
), |
|||
Evaluate(0, 0, 0, 0, 0, 0), |
|||
Tips("对于初跑者,\u200C建议采用MAF180训练法或储备心率百分比的E区、\u200C最大心率百分比的Z3区进行慢跑或其他有氧锻炼。\u200C这种低强度的运动有助于减重或锻炼,\u200C同时能较好地保持较长时间,\u200C减重或锻炼效果较好。\u200C提高步频可以预防受伤并提升跑步水平。\u200C高步频可以减少关节所承受的压力,\u200C增加落地的次数,\u200C在跑的过程中更容易调整并维持稳定的跑步姿态,\u200C减少不必要的体能消耗,\u200C增加跑步经济性。") |
|||
) |
|||
val view = Exercise("Alin", "准备好开始了吗?", 0, 0, uiList) |
|||
exerciseStateFlow.value = view |
|||
} |
|||
} |
|||
} |
|||
|
|||
suspend fun getLastWorkout(): Pair<Statistics, List<WorkoutData>>? { |
|||
val lastWorkout = dao.getAllWorkouts().firstOrNull() |
|||
Logger.i("last work out is:${lastWorkout}") |
|||
var result: Pair<Statistics, List<WorkoutData>>? = null |
|||
lastWorkout?.let { |
|||
NexRingManager.get().sleepApi().getWorkoutData( |
|||
getString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS, ""), lastWorkout.startTs, |
|||
lastWorkout.endTs |
|||
) { |
|||
result = it |
|||
} |
|||
} |
|||
|
|||
return result |
|||
|
|||
} |
|||
|
|||
// fun loadLastExercise |
|||
} |
@ -0,0 +1,64 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.graphics.Color |
|||
import android.graphics.drawable.ColorDrawable |
|||
import android.os.Bundle |
|||
import android.view.Gravity |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import android.view.WindowManager |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.fragment.app.DialogFragment |
|||
import androidx.fragment.app.viewModels |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.DialogSelectBinding |
|||
|
|||
class SelectDialog(val title:String,val data:List<String>): DialogFragment() { |
|||
private lateinit var _binding: DialogSelectBinding |
|||
private val viewModel: DialogExerciseViewModel by viewModels() |
|||
|
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View { |
|||
_binding = DataBindingUtil.inflate(inflater, R.layout.dialog_select,container,false) |
|||
return _binding.root |
|||
} |
|||
|
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
setWindow() |
|||
bind() |
|||
} |
|||
|
|||
private fun bind(){ |
|||
_binding.setValueTitle.text = title |
|||
_binding.rvList.layoutManager = LinearLayoutManager(requireContext()) |
|||
_binding.rvList.setData(data,data.size / 2) |
|||
_binding.clSetValueDone.setOnClickListener { |
|||
this.dismiss() |
|||
} |
|||
|
|||
_binding.background.setOnClickListener { |
|||
this.dismiss() |
|||
} |
|||
} |
|||
|
|||
private fun setWindow() { |
|||
val window = dialog!!.window |
|||
window?.decorView?.setPadding(0, 0, 0, 0) |
|||
val layoutParams = window?.attributes |
|||
layoutParams?.width = WindowManager.LayoutParams.MATCH_PARENT |
|||
layoutParams?.height = WindowManager.LayoutParams.MATCH_PARENT |
|||
window?.attributes = layoutParams |
|||
window?.decorView?.setBackgroundColor(Color.TRANSPARENT) |
|||
window?.setGravity(Gravity.BOTTOM) |
|||
window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT)) |
|||
|
|||
} |
|||
} |
@ -0,0 +1,106 @@ |
|||
package com.whitefish.app.feature.home.exercise |
|||
|
|||
import android.os.Bundle |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.lifecycle.Lifecycle |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.DialogSetTargetBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
|
|||
class SetTargetDialog:Fragment() { |
|||
companion object{ |
|||
const val TAG = "SetTargetDialog" |
|||
} |
|||
|
|||
private lateinit var _binding: DialogSetTargetBinding |
|||
private val context by lazy { |
|||
parentFragment as ExerciseDialog |
|||
} |
|||
|
|||
override fun onCreateView( |
|||
inflater: LayoutInflater, |
|||
container: ViewGroup?, |
|||
savedInstanceState: Bundle? |
|||
): View { |
|||
_binding = DataBindingUtil.inflate(inflater, R.layout.dialog_set_target,container,false) |
|||
return _binding.root |
|||
} |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
bind() |
|||
} |
|||
|
|||
private fun bind() { |
|||
|
|||
_binding.llSelectItemDistance.setOnClickListener { |
|||
context.viewModel.selectType.value = DialogExerciseViewModel.TARGET_TYPE_DISTANCE |
|||
} |
|||
|
|||
_binding.llSelectItemTime.setOnClickListener { |
|||
context.viewModel.selectType.value = DialogExerciseViewModel.TARGET_TYPE_TIME |
|||
} |
|||
|
|||
_binding.llSelectItemHeat.setOnClickListener { |
|||
context.viewModel.selectType.value = DialogExerciseViewModel.TARGET_TYPE_HEAT |
|||
} |
|||
|
|||
_binding.llSelectItemFree.setOnClickListener { |
|||
context.viewModel.selectType.value = DialogExerciseViewModel.TARGET_TYPE_FREE |
|||
} |
|||
_binding.clTargetSetDone.setOnClickListener { |
|||
context.showFragment(AddExerciseDialog.TAG) |
|||
} |
|||
|
|||
context.viewModel.selectType.collectWith(viewLifecycleOwner, Lifecycle.State.CREATED){ |
|||
when(it){ |
|||
DialogExerciseViewModel.TARGET_TYPE_FREE -> { |
|||
_binding.tvSetTarget.text = "无目标" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_HEAT -> { |
|||
_binding.tvSetTarget.text = "热量" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_TIME -> { |
|||
_binding.tvSetTarget.text = "时间" |
|||
} |
|||
DialogExerciseViewModel.TARGET_TYPE_DISTANCE -> { |
|||
_binding.tvSetTarget.text = "距离" |
|||
} |
|||
} |
|||
} |
|||
|
|||
_binding.tvHeartRateNotify.setOnClickListener { |
|||
SelectDialog("间隔提醒", |
|||
listOf( |
|||
"10分钟", |
|||
"20分钟", |
|||
"30分钟", |
|||
"40分钟", |
|||
"50分钟", |
|||
"60分钟", |
|||
) |
|||
).show(requireActivity().supportFragmentManager,TAG) |
|||
} |
|||
|
|||
|
|||
_binding.tvGapNotify.setOnClickListener { |
|||
SelectDialog("距离", |
|||
listOf( |
|||
"10公里", |
|||
"20公里", |
|||
"30公里", |
|||
"40公里", |
|||
"50公里", |
|||
"60公里", |
|||
) |
|||
).show(requireActivity().supportFragmentManager,TAG) |
|||
} |
|||
|
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,36 @@ |
|||
package com.whitefish.app.feature.home.recovery |
|||
|
|||
import android.database.DatabaseUtils |
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ListItemRecoveryDateBinding |
|||
|
|||
class DateAdapter:BaseAdapter<String,DateAdapter.DateHolder>() { |
|||
|
|||
class DateHolder(val binding:ListItemRecoveryDateBinding):ViewHolder(binding.root){ |
|||
|
|||
fun bind(data:String){ |
|||
binding.data = data |
|||
} |
|||
|
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DateHolder { |
|||
return DateHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_recovery_date, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: DateHolder, position: Int) { |
|||
holder.bind(data[position]) |
|||
} |
|||
} |
@ -0,0 +1,353 @@ |
|||
package com.whitefish.app.feature.home.recovery |
|||
|
|||
import android.graphics.Color |
|||
import android.graphics.Rect |
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.core.content.ContextCompat |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.bumptech.glide.Glide |
|||
import com.github.mikephil.charting.components.LimitLine |
|||
import com.github.mikephil.charting.components.XAxis |
|||
import com.github.mikephil.charting.data.BarData |
|||
import com.github.mikephil.charting.data.LineData |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bean.UIRecoveryState |
|||
import com.whitefish.app.bean.UIRecoveryStateItem |
|||
import com.whitefish.app.chart.DateValueFormatter |
|||
import com.whitefish.app.chart.GradientLineChartRenderer |
|||
import com.whitefish.app.chart.VerticalRadiusBarChartRender |
|||
import com.whitefish.app.databinding.CardRecoveryLineChartBinding |
|||
import com.whitefish.app.databinding.CardRecoveryStateListBinding |
|||
import com.whitefish.app.databinding.CardRecoveryTransprantBinding |
|||
import com.whitefish.app.databinding.ItemRecoveryStateBinding |
|||
import com.whitefish.app.ext.clearDraw |
|||
import com.whitefish.app.ext.customStyle |
|||
import com.whitefish.app.feature.home.bean.RecoveryLineChart |
|||
import com.whitefish.app.feature.home.bean.RecoveryScore |
|||
import com.whitefish.app.feature.recovery.HrvAssessmentActivity |
|||
import com.whitefish.app.feature.sleep.SleepDetailActivity |
|||
import com.whitefish.app.utils.formatTime |
|||
import com.whitefish.app.utils.toSpannableChineseTime |
|||
import com.whitefish.app.utils.toSpannableTime |
|||
import java.util.Calendar |
|||
|
|||
|
|||
class RecoveryAdapter : BaseAdapter<Any, ViewHolder>() { |
|||
|
|||
enum class ViewType { |
|||
CARD_RECOVERY_SCORE, |
|||
CARD_RECOVERY_LINE_CHART, |
|||
CARD_RECOVERY_STATE_LIST |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
|||
return when (viewType) { |
|||
ViewType.CARD_RECOVERY_LINE_CHART.ordinal -> RecoveryLineChartHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_recovery_line_chart, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
|
|||
ViewType.CARD_RECOVERY_STATE_LIST.ordinal -> ListRecoveryStateListCardHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_recovery_state_list, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
|
|||
else -> RecoveryScoreHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.card_recovery_transprant, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
|||
val item = data[position] |
|||
when (getItemViewType(position)) { |
|||
|
|||
ViewType.CARD_RECOVERY_LINE_CHART.ordinal -> { |
|||
(holder as RecoveryLineChartHolder).bind(item as RecoveryLineChart) |
|||
} |
|||
|
|||
ViewType.CARD_RECOVERY_SCORE.ordinal -> { |
|||
(holder as RecoveryScoreHolder).bind(item as RecoveryScore) |
|||
} |
|||
|
|||
else -> { |
|||
(holder as ListRecoveryStateListCardHolder).bind(data[position] as UIRecoveryState) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun getItemViewType(position: Int): Int { |
|||
return when (data[position]) { |
|||
is RecoveryLineChart -> { |
|||
ViewType.CARD_RECOVERY_LINE_CHART.ordinal |
|||
} |
|||
|
|||
is RecoveryScore -> { |
|||
ViewType.CARD_RECOVERY_SCORE.ordinal |
|||
|
|||
|
|||
} |
|||
|
|||
else -> { |
|||
ViewType.CARD_RECOVERY_STATE_LIST.ordinal |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
class RecoveryLineChartHolder(val binding: CardRecoveryLineChartBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(chart: RecoveryLineChart) { |
|||
binding.enter.setOnClickListener { |
|||
SleepDetailActivity.start(binding.root.context) |
|||
} |
|||
|
|||
val dataSet = chart.lineChartData |
|||
dataSet.lineWidth = 3f |
|||
|
|||
|
|||
binding.lineChart.clearDraw() |
|||
binding.lineChart.renderer = GradientLineChartRenderer( |
|||
10, |
|||
binding.lineChart, |
|||
binding.lineChart.animator, |
|||
binding.lineChart.viewPortHandler |
|||
) |
|||
|
|||
binding.lineChart.axisLeft.apply { |
|||
isEnabled = true |
|||
setDrawGridLines(false) |
|||
setDrawLabels(false) |
|||
setDrawAxisLine(false) |
|||
// 添加水平参考线 |
|||
val limitLine = LimitLine(chart.targetScore, "").apply { |
|||
lineColor = binding.root.resources.getColor( |
|||
R.color.white_50, |
|||
binding.root.context.theme |
|||
) |
|||
lineWidth = 2f |
|||
enableDashedLine(10f, 10f, 0f) |
|||
} |
|||
addLimitLine(limitLine) |
|||
} |
|||
|
|||
binding.lineChart.xAxis.apply { |
|||
isEnabled = true |
|||
position = XAxis.XAxisPosition.BOTTOM |
|||
axisLineColor = Color.TRANSPARENT |
|||
textColor = |
|||
binding.root.resources.getColor(R.color.white_50, binding.root.context.theme) |
|||
valueFormatter = DateValueFormatter() |
|||
} |
|||
|
|||
val dataList = dataSet.values |
|||
if (!dataList.isNullOrEmpty()) { |
|||
val yAxis = binding.lineChart.axisLeft |
|||
|
|||
val max = dataList.maxOfOrNull { |
|||
it.y |
|||
} |
|||
|
|||
val min = dataList.minOfOrNull { |
|||
it.y |
|||
} |
|||
|
|||
yAxis.axisMinimum = min!! - 2 |
|||
yAxis.axisMaximum = max!! + 2 |
|||
|
|||
dataSet.setDrawCircles(false) |
|||
dataSet.setDrawValues(false) |
|||
val lineData = LineData(dataSet) |
|||
|
|||
binding.lineChart.setData(lineData) |
|||
binding.lineChart.invalidate() |
|||
} |
|||
|
|||
} |
|||
|
|||
} |
|||
|
|||
class ListRecoveryStateListCardHolder(val binding: CardRecoveryStateListBinding) : |
|||
ViewHolder(binding.root) { |
|||
|
|||
fun bind(data: UIRecoveryState) { |
|||
binding.stateList.adapter = StateAdapter().apply { |
|||
setData(data.data) |
|||
} |
|||
|
|||
binding.stateList.addItemDecoration(object : ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
outRect.bottom = |
|||
binding.root.resources.getDimensionPixelSize(R.dimen.state_item_bottom_margin) |
|||
} |
|||
|
|||
}) |
|||
|
|||
binding.stateList.layoutManager = |
|||
LinearLayoutManager(binding.root.context, LinearLayoutManager.VERTICAL, false) |
|||
} |
|||
} |
|||
|
|||
class RecoveryScoreHolder(val binding: CardRecoveryTransprantBinding) : |
|||
ViewHolder(binding.root) { |
|||
fun bind(score: RecoveryScore) { |
|||
binding.score = score |
|||
binding.recoveryTip = score.recoveryTime |
|||
binding.date = |
|||
"${score.date.get(Calendar.MONTH) + 1}/ \n ${score.date.get(Calendar.DAY_OF_MONTH)}" |
|||
binding.tvScore.text = score.score.toString() |
|||
binding.enter.setOnClickListener { |
|||
HrvAssessmentActivity.start(binding.root.context) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
|
|||
class StateAdapter : BaseAdapter<UIRecoveryStateItem, StateAdapter.ViewHolder>() { |
|||
class ViewHolder(val binding: ItemRecoveryStateBinding) : |
|||
RecyclerView.ViewHolder(binding.root) { |
|||
fun bind(data: UIRecoveryStateItem) { |
|||
Glide.with(binding.icon.context).load(data.icon).into(binding.icon) |
|||
binding.title.text = data.title |
|||
binding.description.text = data.description |
|||
binding.value.text = data.value |
|||
|
|||
when (data.viewType) { |
|||
UIRecoveryStateItem.ViewType.OXYGEN, UIRecoveryStateItem.ViewType.HEAT -> { |
|||
binding.lineChart.visibility = View.VISIBLE |
|||
binding.barChart.visibility = View.INVISIBLE |
|||
binding.sleepChart.visibility = View.INVISIBLE |
|||
binding.lineChart.clearDraw() |
|||
binding.lineChart.setExtraOffsets(0f, 0f, 0f, 0f) |
|||
binding.lineChart.minOffset = 0f |
|||
//设置统计图 |
|||
data.lineData?.apply { |
|||
binding.lineChart.data = LineData(data.lineData) |
|||
setDrawFilled(true) |
|||
fillDrawable = |
|||
ContextCompat.getDrawable( |
|||
binding.root.context, |
|||
R.drawable.bg_line_gradient |
|||
) |
|||
setColor(binding.root.context.getColor(R.color.heart_rate_line)) |
|||
setDrawCircles(false) |
|||
setDrawValues(false) |
|||
} |
|||
binding.lineChart.invalidate() |
|||
} |
|||
|
|||
UIRecoveryStateItem.ViewType.SLEEP -> { |
|||
binding.lineChart.visibility = View.INVISIBLE |
|||
binding.barChart.visibility = View.INVISIBLE |
|||
binding.sleepChart.visibility = View.VISIBLE |
|||
binding.sleepChart.setBarHeight(3F) |
|||
data.sleepData?.let { binding.value.text = (it.totalTime / 1000).toSpannableChineseTime(binding.root.context, numberTextSize = 20, unitTextSize = 14) } |
|||
data.sleepData?.let { binding.sleepChart.setSleepData(it.sleepChartData) } |
|||
} |
|||
|
|||
UIRecoveryStateItem.ViewType.PRESSURE -> { |
|||
|
|||
data.barData?.let { |
|||
binding.lineChart.visibility = View.INVISIBLE |
|||
binding.barChart.visibility = View.VISIBLE |
|||
binding.sleepChart.visibility = View.INVISIBLE |
|||
|
|||
binding.barChart.clearDraw() |
|||
binding.barChart.setExtraOffsets(0f, 0f, 0f, 0f) |
|||
binding.barChart.minOffset = 0f |
|||
|
|||
it.isHighlightEnabled = false |
|||
val barData = BarData(it) |
|||
|
|||
barData.setDrawValues(false) |
|||
|
|||
barData.barWidth = 0.8f // 柱体宽度,这个值越小,柱体越窄 |
|||
|
|||
binding.barChart.customStyle(barData.entryCount) |
|||
|
|||
binding.barChart.renderer = |
|||
VerticalRadiusBarChartRender( |
|||
binding.barChart, |
|||
binding.barChart.animator, |
|||
binding.barChart.viewPortHandler |
|||
).apply { |
|||
setRadius(15) |
|||
} |
|||
|
|||
binding.barChart.clearDraw() |
|||
|
|||
binding.barChart.axisLeft.setAxisMinimum(0f) |
|||
binding.barChart.setVisibleXRange(0f, barData.entryCount.toFloat() + 1) |
|||
binding.barChart.setData(barData) |
|||
binding.barChart.invalidate() |
|||
|
|||
} |
|||
} |
|||
|
|||
UIRecoveryStateItem.ViewType.TEMPERATURE -> { |
|||
binding.lineChart.visibility = View.VISIBLE |
|||
binding.barChart.visibility = View.INVISIBLE |
|||
binding.sleepChart.visibility = View.INVISIBLE |
|||
binding.lineChart.clearDraw() |
|||
binding.lineChart.setExtraOffsets(0f, 0f, 0f, 0f) |
|||
binding.lineChart.minOffset = 0f |
|||
data.lineData?.apply { |
|||
lineWidth = 3f |
|||
mode = LineDataSet.Mode.CUBIC_BEZIER |
|||
setDrawCircles(false) |
|||
setDrawValues(false) |
|||
setDrawFilled(false) |
|||
binding.lineChart.data = LineData(this) |
|||
} |
|||
binding.lineChart.invalidate() |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { |
|||
return ViewHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.item_recovery_state, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) { |
|||
holder.bind(data[position]) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,64 @@ |
|||
package com.whitefish.app.feature.home.recovery |
|||
|
|||
import android.graphics.Rect |
|||
import android.view.View |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.lifecycle.ViewModelProvider |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import androidx.recyclerview.widget.RecyclerView.OnScrollListener |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentRecoveryBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
|
|||
class RecoveryFragment : BaseFragment<FragmentRecoveryBinding, RecoveryViewModel>() { |
|||
|
|||
companion object { |
|||
const val TAG = "RecoveryFragment" |
|||
} |
|||
|
|||
val adapter = RecoveryAdapter() |
|||
|
|||
override fun bind() { |
|||
initAdapter() |
|||
collect() |
|||
} |
|||
|
|||
private fun initAdapter() { |
|||
|
|||
mBinding.rvRecovery.adapter = adapter |
|||
val layoutManager = |
|||
LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false) |
|||
mBinding.rvRecovery.layoutManager = layoutManager |
|||
mBinding.rvRecovery.addItemDecoration(object : ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
val position = parent.getChildLayoutPosition(view) |
|||
if (position != 0) { |
|||
outRect.top = resources.getDimensionPixelSize(R.dimen.recovery_item_top_margin) |
|||
} |
|||
} |
|||
}) |
|||
} |
|||
|
|||
private fun collect() { |
|||
mViewModel.stateList.collectWith(viewLifecycleOwner, Lifecycle.State.RESUMED) { |
|||
adapter.setData(it) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_recovery |
|||
} |
|||
|
|||
override fun setViewModel(): RecoveryViewModel { |
|||
return ViewModelProvider(this)[RecoveryViewModel::class.java] |
|||
} |
|||
} |
@ -0,0 +1,117 @@ |
|||
package com.whitefish.app.feature.home.recovery |
|||
|
|||
import android.icu.util.Calendar |
|||
import androidx.lifecycle.viewModelScope |
|||
import com.github.mikephil.charting.data.BarDataSet |
|||
import com.github.mikephil.charting.data.Entry |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bean.UIRecoveryState |
|||
import com.whitefish.app.bean.UIRecoveryStateItem |
|||
import com.whitefish.app.bean.toState |
|||
import com.whitefish.app.constants.Constants |
|||
import com.whitefish.app.data.FakeRepository |
|||
import com.whitefish.app.device.DeviceDataProvider |
|||
import com.whitefish.app.feature.home.bean.RecoveryLineChart |
|||
import com.whitefish.app.feature.home.bean.RecoveryScore |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import com.whitefish.app.utils.SpannableTimeFormatter |
|||
import com.whitefish.app.utils.append |
|||
import com.whitefish.app.utils.createSpannableText |
|||
import com.whitefish.app.utils.getString |
|||
import com.whitefish.app.utils.toDateString |
|||
import com.whitefish.app.utils.toTimeString |
|||
import com.whitefish.app.utils.todayCalendar |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
|
|||
class RecoveryViewModel : BaseViewModel() { |
|||
val stateList = MutableStateFlow(emptyList<Any>()) |
|||
|
|||
val refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
val address = getString(Constants.SP_KEY_BOUND_DEVICE_ADDRESS, "") |
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun collect() { |
|||
viewModelScope.launch { |
|||
refreshFlow.collectLatest { |
|||
|
|||
val lastHeartRate = DeviceDataProvider.INSTANCE.getLast24HoursHeartRate() |
|||
val heartList = arrayListOf<Entry>() |
|||
val temperatureList = arrayListOf<Entry>() |
|||
val temperature = DeviceDataProvider.INSTANCE.getLastTemperature() |
|||
val sleepData = DeviceDataProvider.INSTANCE.getSleepData(todayCalendar()) |
|||
|
|||
for (time in 0..23) { |
|||
heartList.add( |
|||
Entry( |
|||
(23 - time).toFloat(), |
|||
(lastHeartRate[time] ?: 0).toFloat() |
|||
) |
|||
) |
|||
temperature[time]?.lastOrNull()?.let { |
|||
temperatureList.add( |
|||
Entry( |
|||
(time - 23).toFloat(), |
|||
it.value.toFloat() |
|||
) |
|||
) |
|||
} |
|||
} |
|||
|
|||
heartList.apply { |
|||
sortBy { it.x } |
|||
dropLastWhile { it.y != 0F } |
|||
} |
|||
temperatureList.sortBy { it.x } |
|||
|
|||
val view = arrayListOf( |
|||
RecoveryScore( |
|||
0, "预期${todayCalendar().get(Calendar.MONTH + 1)}月${ |
|||
todayCalendar().get( |
|||
Calendar.DAY_OF_MONTH |
|||
) |
|||
}日恢复", "", todayCalendar() |
|||
), |
|||
RecoveryLineChart(0f, LineDataSet(emptyList<Entry>(), "")), |
|||
UIRecoveryState( |
|||
arrayListOf( |
|||
UIRecoveryStateItem( |
|||
R.drawable.ic_sleep, "睡眠", "睡眠质量不错", null, null, createSpannableText("",20,true), UIRecoveryStateItem.ViewType.SLEEP, sleepData = sleepData?.toState() |
|||
), |
|||
UIRecoveryStateItem( |
|||
R.drawable.ic_heart, "心率", "您的心率正常", LineDataSet( |
|||
heartList, "" |
|||
), null, createSpannableText(heartList.last().y.toString(),20,true).append("次/每分钟",10), UIRecoveryStateItem.ViewType.HEAT |
|||
), |
|||
UIRecoveryStateItem( |
|||
R.drawable.ic_oxygen, "血氧", "您的血氧正常", LineDataSet( |
|||
arrayListOf(), "" |
|||
), null, createSpannableText("0",20,true), UIRecoveryStateItem.ViewType.OXYGEN |
|||
), |
|||
UIRecoveryStateItem( |
|||
R.drawable.ic_pressure, "压力", "您的压力正常", null, BarDataSet( |
|||
arrayListOf(), "" |
|||
), createSpannableText("0",20,true), UIRecoveryStateItem.ViewType.PRESSURE |
|||
), |
|||
UIRecoveryStateItem( |
|||
R.drawable.ic_temperature, "体温", "您的体温正常", LineDataSet( |
|||
temperatureList, "" |
|||
), null, createSpannableText(temperatureList.lastOrNull()?.y.toString(),20,true).append("℃",10), UIRecoveryStateItem.ViewType.TEMPERATURE |
|||
), |
|||
) |
|||
) |
|||
) |
|||
stateList.value = view |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
@ -0,0 +1,112 @@ |
|||
package com.whitefish.app.feature.home.setting |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.View |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.bumptech.glide.Glide |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ListItemSettingFullRowBlockBinding |
|||
import com.whitefish.app.databinding.ListItemSettingFullRowLineBinding |
|||
import com.whitefish.app.databinding.ListItemSettingHalfRowBlockBinding |
|||
import com.whitefish.app.databinding.ListItemSettingTitleBinding |
|||
import com.whitefish.app.feature.home.bean.Setting |
|||
import com.whitefish.app.feature.home.bean.SettingType |
|||
|
|||
class SettingAdapter:BaseAdapter<Setting, SettingAdapter.SettingsViewHolder>() { |
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SettingsViewHolder { |
|||
return when(viewType){ |
|||
SettingType.FULL_ROW_BLOCK.ordinal -> { |
|||
FullRowBlockHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_setting_full_row_block, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
SettingType.HALF_ROW_BLOCK.ordinal -> { |
|||
HalfRowBlockHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_setting_half_row_block, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
SettingType.TITLE.ordinal -> { |
|||
TitleHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_setting_title, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
else -> { |
|||
FullRowLineHolder( |
|||
DataBindingUtil.inflate( |
|||
LayoutInflater.from(parent.context), |
|||
R.layout.list_item_setting_full_row_line, |
|||
parent, |
|||
false |
|||
) |
|||
) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun getItemViewType(position: Int): Int { |
|||
return when(data[position].type){ |
|||
SettingType.FULL_ROW_BLOCK -> SettingType.FULL_ROW_BLOCK.ordinal |
|||
SettingType.HALF_ROW_BLOCK -> SettingType.HALF_ROW_BLOCK.ordinal |
|||
SettingType.TITLE -> SettingType.TITLE.ordinal |
|||
SettingType.FULL_ROW_LINE -> SettingType.FULL_ROW_LINE.ordinal |
|||
} |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: SettingsViewHolder, position: Int) { |
|||
holder.bind(data[position]) |
|||
} |
|||
|
|||
abstract class SettingsViewHolder(view: View):ViewHolder(view){ |
|||
abstract fun bind(item:Setting) |
|||
} |
|||
|
|||
class FullRowBlockHolder(val binding:ListItemSettingFullRowBlockBinding):SettingsViewHolder(binding.root){ |
|||
|
|||
override fun bind(item: Setting){ |
|||
binding.connectState.text = item.title |
|||
binding.powerCount.text = item.subTitle |
|||
} |
|||
|
|||
} |
|||
class HalfRowBlockHolder(val binding:ListItemSettingHalfRowBlockBinding):SettingsViewHolder(binding.root){ |
|||
override fun bind(item: Setting){ |
|||
binding.title.text = item.title |
|||
if (!item.subTitle.isNullOrEmpty()){ |
|||
binding.subTitle.text = item.subTitle |
|||
binding.subTitle.visibility = View.VISIBLE |
|||
} |
|||
Glide.with(binding.root.context).load(item.icon).into(binding.icon) |
|||
} |
|||
} |
|||
|
|||
class FullRowLineHolder(val binding:ListItemSettingFullRowLineBinding):SettingsViewHolder(binding.root){ |
|||
override fun bind(item: Setting){ |
|||
binding.title.text = item.title |
|||
} |
|||
} |
|||
|
|||
class TitleHolder(val binding: ListItemSettingTitleBinding):SettingsViewHolder(binding.root){ |
|||
override fun bind(item: Setting){ |
|||
binding.title.text = item.title |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,112 @@ |
|||
package com.whitefish.app.feature.home.setting |
|||
|
|||
import android.graphics.Rect |
|||
import android.view.View |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.recyclerview.widget.GridLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentSettingBinding |
|||
import com.whitefish.app.device.DeviceManager |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.ext.isLeftItem |
|||
import com.whitefish.app.feature.home.bean.Setting |
|||
import com.whitefish.app.feature.home.bean.SettingType |
|||
|
|||
class SettingFragment : BaseFragment<FragmentSettingBinding, SettingViewModel>() { |
|||
companion object { |
|||
const val TAG = "SettingFragment" |
|||
private const val TOTAL_COLUM = 2 |
|||
} |
|||
|
|||
|
|||
private val adapter = SettingAdapter() |
|||
|
|||
override fun bind() { |
|||
mBinding.list.adapter = adapter |
|||
val layoutManager = GridLayoutManager(requireContext(), TOTAL_COLUM).apply { |
|||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { |
|||
override fun getSpanSize(position: Int): Int { |
|||
return when (adapter.getItemViewType(position)) { |
|||
SettingType.HALF_ROW_BLOCK.ordinal -> TOTAL_COLUM / 2 |
|||
else -> TOTAL_COLUM |
|||
} |
|||
} |
|||
} |
|||
} |
|||
mBinding.list.layoutManager = layoutManager |
|||
|
|||
mBinding.list.addItemDecoration(object : ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
|
|||
|
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
val position = parent.getChildAdapterPosition(view) |
|||
if (position == RecyclerView.NO_POSITION) { |
|||
return |
|||
} |
|||
|
|||
val layoutType = adapter.getItemViewType(position) |
|||
if (layoutType != SettingType.TITLE.ordinal) { |
|||
outRect.bottom = |
|||
resources.getDimensionPixelSize(R.dimen.setting_item_vertical_offset) |
|||
} |
|||
if (layoutManager.isLeftItem(position, TOTAL_COLUM)) { |
|||
outRect.right = |
|||
resources.getDimensionPixelSize(R.dimen.setting_item_horizontal_offset) |
|||
} else { |
|||
outRect.left = |
|||
resources.getDimensionPixelSize(R.dimen.setting_item_horizontal_offset) |
|||
} |
|||
} |
|||
}) |
|||
|
|||
DeviceManager.INSTANCE.batteryLevel.observe(this) { |
|||
adapter.setData(buildSettingList(it.second)) |
|||
} |
|||
} |
|||
|
|||
override fun onHiddenChanged(hidden: Boolean) { |
|||
super.onHiddenChanged(hidden) |
|||
} |
|||
|
|||
override fun onResume() { |
|||
super.onResume() |
|||
} |
|||
|
|||
fun buildSettingList(powerCount: Int): List<Setting> { |
|||
return arrayListOf( |
|||
Setting("已连接", type = SettingType.FULL_ROW_BLOCK, subTitle = "$powerCount%"), |
|||
Setting("智慧生活联动", type = SettingType.HALF_ROW_BLOCK, icon = R.drawable.ic_connect_mutiple), |
|||
Setting("钱包", type = SettingType.HALF_ROW_BLOCK,icon = R.drawable.ic_wallet), |
|||
Setting("消息通知", "已开启", type = SettingType.HALF_ROW_BLOCK,icon = R.drawable.ic_notify), |
|||
Setting("找设备", type = SettingType.HALF_ROW_BLOCK,icon = R.drawable.ic_finder), |
|||
Setting("闹钟", type = SettingType.HALF_ROW_BLOCK,icon = R.drawable.ic_clock), |
|||
Setting("消息提醒", type = SettingType.HALF_ROW_BLOCK, icon = R.drawable.ic_message), |
|||
Setting("微信绑定", type = SettingType.HALF_ROW_BLOCK, icon = R.drawable.ic_wechat), |
|||
Setting("产品手册", type = SettingType.HALF_ROW_BLOCK, icon = R.drawable.ic_help), |
|||
Setting("其他", type = SettingType.TITLE), |
|||
Setting("设备设置", type = SettingType.FULL_ROW_LINE), |
|||
Setting("恢复出厂设置", type = SettingType.FULL_ROW_LINE), |
|||
Setting("固件更新", type = SettingType.FULL_ROW_LINE), |
|||
Setting("帮助与客服", type = SettingType.FULL_ROW_LINE), |
|||
Setting("设备信息", type = SettingType.FULL_ROW_LINE), |
|||
) |
|||
} |
|||
|
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_setting |
|||
} |
|||
|
|||
override fun setViewModel(): SettingViewModel { |
|||
return SettingViewModel() |
|||
} |
|||
} |
@ -0,0 +1,14 @@ |
|||
package com.whitefish.app.feature.home.setting |
|||
|
|||
import androidx.lifecycle.viewModelScope |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.device.DeviceDataProvider |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.asStateFlow |
|||
import kotlinx.coroutines.launch |
|||
import lib.linktop.nexring.api.NexRingManager |
|||
import kotlin.coroutines.resume |
|||
|
|||
class SettingViewModel:BaseViewModel() { |
|||
} |
@ -0,0 +1,121 @@ |
|||
package com.whitefish.app.feature.home.state |
|||
|
|||
import android.bluetooth.BluetoothProfile |
|||
import android.graphics.Rect |
|||
import android.view.View |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.lifecycle.ViewModelProvider |
|||
import androidx.recyclerview.widget.GridLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import androidx.recyclerview.widget.RecyclerView.ItemDecoration |
|||
import com.orhanobut.logger.Logger |
|||
import com.whitefish.app.Application |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.bt.OnBleConnectionListener |
|||
import com.whitefish.app.databinding.FragmentStateBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.home.StateAdapter |
|||
|
|||
class StateFragment : BaseFragment<FragmentStateBinding, StateViewModel>() { |
|||
|
|||
private lateinit var adapter: StateAdapter |
|||
private val mOnBleConnectionListener = object : OnBleConnectionListener { |
|||
|
|||
override fun onBleState(state: Int) { |
|||
if (state == BluetoothProfile.STATE_CONNECTED) { |
|||
mViewModel.refresh() |
|||
} |
|||
} |
|||
|
|||
override fun onBleReady() { |
|||
mViewModel.refresh() |
|||
} |
|||
} |
|||
|
|||
companion object { |
|||
const val TAG = "StateFragment" |
|||
private const val FULL_ROW = 2 |
|||
private const val HALF_ROW = 1 |
|||
private const val TOTAL_ROW = 2 |
|||
} |
|||
|
|||
override fun bind() { |
|||
initAdapter(mBinding.rvContent) |
|||
collectData() |
|||
Application.INSTANTS!!.bleManager.addOnBleConnectionListener(mOnBleConnectionListener) |
|||
} |
|||
|
|||
private fun initAdapter(recyclerView: RecyclerView) { |
|||
adapter = StateAdapter() |
|||
recyclerView.adapter = adapter |
|||
recyclerView.layoutManager = GridLayoutManager(requireContext(), TOTAL_ROW).apply { |
|||
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { |
|||
override fun getSpanSize(position: Int): Int { |
|||
return when (adapter.getItemViewType(position)) { |
|||
StateAdapter.ViewType.CARD_HEART_RATE_HALF_ROW.ordinal, |
|||
StateAdapter.ViewType.CARD_SLEEP_STATE.ordinal, |
|||
StateAdapter.ViewType.CARD_BAR_CHART_SMALL.ordinal -> { |
|||
HALF_ROW |
|||
} |
|||
|
|||
else -> { |
|||
FULL_ROW |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
recyclerView.addItemDecoration(object : ItemDecoration() { |
|||
override fun getItemOffsets( |
|||
outRect: Rect, |
|||
view: View, |
|||
parent: RecyclerView, |
|||
state: RecyclerView.State |
|||
) { |
|||
super.getItemOffsets(outRect, view, parent, state) |
|||
val position = parent.getChildAdapterPosition(view) |
|||
if (position == RecyclerView.NO_POSITION) { |
|||
return |
|||
} |
|||
outRect.bottom = resources.getDimensionPixelSize(R.dimen.state_card_vertical_offset) |
|||
|
|||
when (adapter.getItemViewType(position)) { |
|||
StateAdapter.ViewType.CARD_SLEEP_STATE.ordinal, StateAdapter.ViewType.CARD_HEART_RATE_HALF_ROW.ordinal, StateAdapter.ViewType.CARD_BAR_CHART_SMALL.ordinal -> { |
|||
if (position % 2 != 0) { |
|||
outRect.left = |
|||
resources.getDimensionPixelSize(R.dimen.state_card_horizontal_offset) |
|||
} else { |
|||
outRect.right = |
|||
resources.getDimensionPixelSize(R.dimen.state_card_horizontal_offset) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}) |
|||
|
|||
} |
|||
|
|||
private fun collectData() { |
|||
|
|||
mViewModel.stateList.collectWith(this, Lifecycle.State.RESUMED) { |
|||
Logger.i("view update:${it}") |
|||
adapter.setData(it) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_state |
|||
} |
|||
|
|||
override fun setViewModel(): StateViewModel { |
|||
return ViewModelProvider(this)[StateViewModel::class.java] |
|||
} |
|||
|
|||
override fun onDestroyView() { |
|||
super.onDestroyView() |
|||
Application.INSTANTS!!.bleManager.removeOnBleConnectionListener(mOnBleConnectionListener) |
|||
} |
|||
|
|||
} |
@ -0,0 +1,90 @@ |
|||
package com.whitefish.app.feature.home.state |
|||
|
|||
import androidx.lifecycle.ViewModel |
|||
import androidx.lifecycle.viewModelScope |
|||
import com.github.mikephil.charting.data.Entry |
|||
import com.github.mikephil.charting.data.LineDataSet |
|||
import com.whitefish.app.bean.SleepState |
|||
import com.whitefish.app.bean.toState |
|||
import com.whitefish.app.device.DeviceDataProvider |
|||
import com.whitefish.app.feature.home.bean.ExerciseTarget |
|||
import com.whitefish.app.feature.home.bean.HeardRate |
|||
import com.whitefish.app.feature.home.bean.RecoveryScore |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import com.whitefish.app.utils.todayCalendar |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.collectLatest |
|||
import kotlinx.coroutines.launch |
|||
import java.util.Calendar |
|||
|
|||
class StateViewModel : ViewModel() { |
|||
|
|||
val stateList = MutableStateFlow(emptyList<Any>()) |
|||
|
|||
private val _refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
|
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun refresh() { |
|||
viewModelScope.launch { |
|||
_refreshFlow.value = RefreshEmit() |
|||
} |
|||
} |
|||
|
|||
fun collect() { |
|||
viewModelScope.launch { |
|||
_refreshFlow.collectLatest { |
|||
val exerciseTarget = ExerciseTarget(0, "0") |
|||
val recoveryCard = RecoveryScore( |
|||
score = 0, |
|||
recoveryTime = "0", |
|||
"你当天的身体状态似乎不太好,请注意保持适量的运动,晚上早点休息,养成良好的锻炼和睡眠规律。", |
|||
todayCalendar() |
|||
) |
|||
|
|||
val hrList = DeviceDataProvider.INSTANCE.getHeartRate(todayCalendar()) |
|||
val sleepData = DeviceDataProvider.INSTANCE.getSleepData(todayCalendar()) |
|||
stateList.value = processViewData(exerciseTarget, recoveryCard, hrList, sleepData?.toState()) |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
|||
private fun processViewData( |
|||
exerciseTarget: ExerciseTarget, |
|||
recoveryCard: RecoveryScore, |
|||
hrData: List<Pair<Int, Int>>, |
|||
sleepData: SleepState? |
|||
): List<Any> { |
|||
val today = todayCalendar() |
|||
val viewList = arrayListOf<Any>() |
|||
val hrEntry = arrayListOf<Entry>() |
|||
hrData.forEachIndexed { index, item -> |
|||
hrEntry.add(Entry(item.first.toFloat(), item.second.toFloat())) |
|||
} |
|||
val hrLineChart = LineDataSet(hrEntry, "") |
|||
val hrView = HeardRate( |
|||
HeardRate.SIZE_HALF_ROW, |
|||
hrLineChart, |
|||
(hrData.lastOrNull()?.second ?: 0).toString(), |
|||
"${today.get(Calendar.MONTH) + 1}/${today.get(Calendar.DAY_OF_MONTH)}", "" |
|||
) |
|||
viewList.add(exerciseTarget) |
|||
viewList.add(recoveryCard) |
|||
viewList.add(hrView) |
|||
if (sleepData != null) { |
|||
viewList.add(sleepData) |
|||
} else { |
|||
viewList.add( |
|||
SleepState( |
|||
"${today.get(Calendar.MONTH) + 1}/${today.get(Calendar.DAY_OF_MONTH)}", 0L, |
|||
emptyList(), "0", 0F, "0", "0", "0" |
|||
) |
|||
) |
|||
} |
|||
return viewList |
|||
} |
|||
} |
@ -0,0 +1,26 @@ |
|||
package com.whitefish.app.feature.launcher |
|||
|
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ActivityLauncherBinding |
|||
import com.whitefish.app.feature.login.LoginActivity |
|||
|
|||
class LauncherActivity: BaseActivity<ActivityLauncherBinding,BaseViewModel>() { |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_launcher |
|||
} |
|||
|
|||
override fun setViewModel(): Class<BaseViewModel> { |
|||
return BaseViewModel::class.java |
|||
} |
|||
|
|||
override fun bind() { |
|||
mBinding.btUse.setOnClickListener { |
|||
LoginActivity.start(this) |
|||
finish() |
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,34 @@ |
|||
package com.whitefish.app.feature.login |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ActivityLoginBinding |
|||
import com.whitefish.app.feature.connect.ConnectTipActivity |
|||
import com.whitefish.app.feature.devices.DeviceActivity |
|||
|
|||
class LoginActivity:BaseActivity<ActivityLoginBinding,BaseViewModel>() { |
|||
companion object{ |
|||
fun start(context:Context){ |
|||
val intent = Intent(context,LoginActivity::class.java) |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_login |
|||
} |
|||
|
|||
override fun setViewModel(): Class<BaseViewModel> { |
|||
return BaseViewModel::class.java |
|||
} |
|||
|
|||
override fun bind() { |
|||
mBinding.btLogin.setOnClickListener { |
|||
ConnectTipActivity.start(this) |
|||
finish() |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,32 @@ |
|||
package com.whitefish.app.feature.plan |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ActivityPlanBinding |
|||
import com.whitefish.app.feature.home.HomeActivity |
|||
|
|||
class PlanActivity:BaseActivity<ActivityPlanBinding,PlanViewModel>() { |
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_plan |
|||
} |
|||
|
|||
override fun setViewModel(): Class<PlanViewModel> { |
|||
return PlanViewModel::class.java |
|||
} |
|||
|
|||
override fun bind() { |
|||
mBinding.done.setOnClickListener { |
|||
HomeActivity.start(this) |
|||
finish() |
|||
} |
|||
} |
|||
|
|||
companion object{ |
|||
fun start(context:Context){ |
|||
context.startActivity(Intent(context,PlanActivity::class.java)) |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
package com.whitefish.app.feature.plan |
|||
|
|||
import com.whitefish.app.BaseViewModel |
|||
|
|||
class PlanViewModel:BaseViewModel() { |
|||
} |
@ -0,0 +1,105 @@ |
|||
package com.whitefish.app.feature.preference |
|||
|
|||
import android.content.Context |
|||
import android.content.Intent |
|||
import android.util.Log |
|||
import android.view.View |
|||
import androidx.fragment.app.Fragment |
|||
import androidx.lifecycle.Lifecycle |
|||
import com.whitefish.app.BaseActivity |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ActivityPreferenceBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.preference.fragment.LikeFragment |
|||
import com.whitefish.app.feature.preference.fragment.PromoteFragment |
|||
import com.whitefish.app.feature.preference.fragment.RemindFragment |
|||
import com.whitefish.app.feature.preference.fragment.TargetFragment |
|||
import com.whitefish.app.feature.preference.fragment.WeightFragment |
|||
|
|||
|
|||
class PreferenceActivity : BaseActivity<ActivityPreferenceBinding, PreferenceViewModel>() { |
|||
|
|||
enum class ViewType { |
|||
TYPE_TARGET, |
|||
TYPE_PROMOTE, |
|||
TYPE_WEIGHT, |
|||
TYPE_LIKE, |
|||
TYPE_REMIND |
|||
} |
|||
companion object{ |
|||
private const val TAG = "PreferenceActivity" |
|||
|
|||
fun start(context:Context){ |
|||
val intent = Intent(context,PreferenceActivity::class.java) |
|||
context.startActivity(intent) |
|||
} |
|||
} |
|||
override fun setLayout(): Int { |
|||
return R.layout.activity_preference |
|||
} |
|||
|
|||
override fun setViewModel(): Class<PreferenceViewModel> { |
|||
return PreferenceViewModel::class.java |
|||
} |
|||
|
|||
override fun bind() { |
|||
showFragment(ViewType.TYPE_WEIGHT) |
|||
collect() |
|||
} |
|||
|
|||
fun collect(){ |
|||
mViewModel.weightFlow.collectWith(this,Lifecycle.State.RESUMED){ |
|||
if (it.isNotEmpty()){ |
|||
mBinding.tvPreferenceWeight.text = "体重 : $it" |
|||
mBinding.tvPreferenceWeight.visibility = View.VISIBLE |
|||
mBinding.lpvProgress.setProgress(mBinding.lpvProgress.getProgress() + 25) |
|||
} |
|||
} |
|||
|
|||
mViewModel.targetFlow.collectWith(this,Lifecycle.State.RESUMED){ |
|||
if (it.isNotEmpty()){ |
|||
mBinding.tvPreferenceExerciseTarget.text = "运动目标 : $it" |
|||
mBinding.tvPreferenceExerciseTarget.visibility = View.VISIBLE |
|||
mBinding.lpvProgress.setProgress(mBinding.lpvProgress.getProgress() + 25) |
|||
} |
|||
} |
|||
|
|||
mViewModel.promoteFlow.collectWith(this,Lifecycle.State.RESUMED){ |
|||
if (it.isNotEmpty()){ |
|||
mBinding.tvPreferencePromoteTarget.text = "提升目标 : $it" |
|||
mBinding.tvPreferencePromoteTarget.visibility = View.VISIBLE |
|||
mBinding.lpvProgress.setProgress(mBinding.lpvProgress.getProgress() + 25) |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun showFragment(tag: ViewType) { |
|||
supportFragmentManager.beginTransaction().replace(R.id.flPreferenceFragmentContainer,obtainFragment(tag)).commit() |
|||
} |
|||
|
|||
fun obtainFragment(tag: ViewType): Fragment { |
|||
Log.d(TAG,"obtain fragment:${tag}") |
|||
return when (tag) { |
|||
ViewType.TYPE_TARGET -> { |
|||
TargetFragment() |
|||
} |
|||
|
|||
ViewType.TYPE_WEIGHT -> { |
|||
WeightFragment() |
|||
} |
|||
ViewType.TYPE_LIKE -> { |
|||
LikeFragment() |
|||
} |
|||
ViewType.TYPE_PROMOTE -> { |
|||
PromoteFragment() |
|||
} |
|||
ViewType.TYPE_REMIND -> { |
|||
mBinding.clTopArray.visibility = View.GONE |
|||
mBinding.tvSkip.text = "下次再说" |
|||
RemindFragment() |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,93 @@ |
|||
package com.whitefish.app.feature.preference |
|||
|
|||
import androidx.lifecycle.viewModelScope |
|||
import com.whitefish.app.BaseViewModel |
|||
import com.whitefish.app.data.FakeRepository |
|||
import com.whitefish.app.utils.RefreshEmit |
|||
import kotlinx.coroutines.flow.MutableSharedFlow |
|||
import kotlinx.coroutines.flow.MutableStateFlow |
|||
import kotlinx.coroutines.flow.combine |
|||
import kotlinx.coroutines.launch |
|||
|
|||
class PreferenceViewModel : BaseViewModel() { |
|||
val weightFlow = MutableStateFlow("") |
|||
val targetFlow = MutableStateFlow("") |
|||
val promoteFlow = MutableStateFlow("") |
|||
|
|||
val exerciseList = MutableStateFlow(emptyList<String>()) |
|||
val weightList = arrayListOf<String>().apply { |
|||
for (i in 1..100) { |
|||
add("$i kg") |
|||
} |
|||
} |
|||
|
|||
val targetList = MutableStateFlow(emptyList<String>()) |
|||
val promoteList = MutableStateFlow(emptyList<String>()) |
|||
private val refreshFlow = MutableStateFlow(RefreshEmit()) |
|||
|
|||
init { |
|||
collect() |
|||
} |
|||
|
|||
fun refresh(type: PreferenceActivity.ViewType) { |
|||
viewModelScope.launch { |
|||
refreshFlow.value = RefreshEmit(type.ordinal) |
|||
} |
|||
} |
|||
|
|||
private fun collect() { |
|||
viewModelScope.launch { |
|||
refreshFlow.collect { |
|||
when(it.code){ |
|||
PreferenceActivity.ViewType.TYPE_PROMOTE.ordinal -> { |
|||
promoteList.value = arrayListOf( |
|||
"跑步能力", |
|||
"速度和爆发力", |
|||
"心肺能力", |
|||
"肌肉量和体脂率", |
|||
"协调性和柔韧性", |
|||
"耐力" |
|||
) |
|||
} |
|||
|
|||
PreferenceActivity.ViewType.TYPE_TARGET.ordinal -> { |
|||
targetList.value = arrayListOf( |
|||
"全身减脂减重", |
|||
"局部变瘦,紧致塑型", |
|||
"增加肌肉,雕塑线条", |
|||
"提升专业运动能力/成绩", |
|||
"保持身体健康", |
|||
"生病复健" |
|||
) |
|||
} |
|||
|
|||
PreferenceActivity.ViewType.TYPE_LIKE.ordinal -> { |
|||
exerciseList.value = arrayListOf( |
|||
"跑走骑运动","球类运动","徒手有氧训练(HIIT 等","无器械塑型力量训练","全身热身","拉伸运动","瑜伽" |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
fun setSelectValue(value: String, type: PreferenceActivity.ViewType) { |
|||
viewModelScope.launch { |
|||
when (type) { |
|||
PreferenceActivity.ViewType.TYPE_PROMOTE -> { |
|||
promoteFlow.value = value |
|||
} |
|||
|
|||
PreferenceActivity.ViewType.TYPE_TARGET -> { |
|||
targetFlow.value = value |
|||
} |
|||
PreferenceActivity.ViewType.TYPE_WEIGHT ->{ |
|||
weightFlow.value = value |
|||
} |
|||
else -> {} |
|||
} |
|||
} |
|||
} |
|||
|
|||
} |
|||
|
@ -0,0 +1,28 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ListItemLikeBinding |
|||
|
|||
class LikeAdapter:BaseAdapter<String,LikeAdapter.ListItemLikeHolder>() { |
|||
|
|||
inner class ListItemLikeHolder(private val binding: ListItemLikeBinding): RecyclerView.ViewHolder(binding.root){ |
|||
fun bind(data:String){ |
|||
binding.data = data |
|||
} |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemLikeHolder { |
|||
return ListItemLikeHolder( |
|||
DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.list_item_like,parent,false) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ListItemLikeHolder, position: Int) { |
|||
holder.bind(data[position]) |
|||
} |
|||
} |
@ -0,0 +1,95 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import android.os.Bundle |
|||
import android.view.View |
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import androidx.recyclerview.widget.RecyclerView |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentLikeBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.preference.PreferenceActivity |
|||
import com.whitefish.app.feature.preference.PreferenceViewModel |
|||
|
|||
class LikeFragment: BaseFragment<FragmentLikeBinding,PreferenceViewModel>() { |
|||
private val viewModel: PreferenceViewModel by activityViewModels() |
|||
private val row1Adapter = LikeAdapter() |
|||
private val row2Adapter = LikeAdapter() |
|||
private val row3Adapter = LikeAdapter() |
|||
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { |
|||
super.onViewCreated(view, savedInstanceState) |
|||
bind() |
|||
mViewModel.refresh(PreferenceActivity.ViewType.TYPE_LIKE) |
|||
} |
|||
|
|||
override fun bind(){ |
|||
|
|||
initAdapter() |
|||
collect() |
|||
|
|||
mBinding.tvDone.setOnClickListener { |
|||
(requireActivity() as PreferenceActivity).showFragment(PreferenceActivity.ViewType.TYPE_REMIND) |
|||
} |
|||
|
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_like |
|||
} |
|||
|
|||
override fun setViewModel(): PreferenceViewModel { |
|||
return viewModel |
|||
} |
|||
|
|||
private fun collect(){ |
|||
mViewModel.exerciseList.collectWith(viewLifecycleOwner,Lifecycle.State.RESUMED){ |
|||
val chunks = it.chunked(it.size / 3) |
|||
row1Adapter.setData(chunks.first()) |
|||
row2Adapter.setData(chunks[1]) |
|||
row3Adapter.setData(chunks.last()) |
|||
} |
|||
} |
|||
|
|||
private fun initAdapter(){ |
|||
mBinding.row1.adapter = row1Adapter |
|||
mBinding.row1.setLayoutManager(LinearLayoutManager(requireContext(),LinearLayoutManager.HORIZONTAL,false)) |
|||
|
|||
mBinding.row2.adapter = row2Adapter |
|||
mBinding.row2.setLayoutManager(LinearLayoutManager(requireContext(),LinearLayoutManager.HORIZONTAL,false)) |
|||
|
|||
mBinding.row3.adapter = row3Adapter |
|||
mBinding.row3.setLayoutManager(LinearLayoutManager(requireContext(),LinearLayoutManager.HORIZONTAL,false)) |
|||
|
|||
setupSyncedRecyclerViews(mBinding.row1,mBinding.row2,mBinding.row3) |
|||
} |
|||
|
|||
private fun setupSyncedRecyclerViews(recyclerView1: RecyclerView, recyclerView2: RecyclerView, recyclerView3: RecyclerView) { |
|||
val allRecyclerViews = listOf(recyclerView1, recyclerView2, recyclerView3) |
|||
|
|||
allRecyclerViews.forEach { recyclerView -> |
|||
recyclerView.addOnScrollListener(SyncScrollListener(allRecyclerViews.filter { it != recyclerView })) |
|||
} |
|||
} |
|||
|
|||
class SyncScrollListener(private val syncRecyclerViews: List<RecyclerView>) : RecyclerView.OnScrollListener() { |
|||
private var isScrolling = false |
|||
|
|||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { |
|||
if (isScrolling) { |
|||
return |
|||
} |
|||
|
|||
// 同步其他的RecyclerView的滚动位置 |
|||
syncRecyclerViews.forEach { otherRecyclerView -> |
|||
if (otherRecyclerView != recyclerView) { |
|||
isScrolling = true |
|||
otherRecyclerView.scrollBy(dx, dy) |
|||
isScrolling = false |
|||
} |
|||
} |
|||
} |
|||
} |
|||
} |
@ -0,0 +1,53 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentSelectRadiusListBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.preference.PreferenceActivity |
|||
import com.whitefish.app.feature.preference.PreferenceViewModel |
|||
|
|||
class PromoteFragment:BaseFragment<FragmentSelectRadiusListBinding,PreferenceViewModel>() { |
|||
private val viewModel:PreferenceViewModel by activityViewModels() |
|||
private val adapter = SelectAdapter(false) |
|||
|
|||
override fun bind(){ |
|||
collect() |
|||
initAdapter() |
|||
mViewModel.refresh(PreferenceActivity.ViewType.TYPE_PROMOTE) |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_select_radius_list |
|||
} |
|||
|
|||
override fun setViewModel(): PreferenceViewModel { |
|||
return viewModel |
|||
} |
|||
|
|||
private fun collect(){ |
|||
mViewModel.promoteList.collectWith(viewLifecycleOwner,Lifecycle.State.RESUMED){ |
|||
adapter.setData(it) |
|||
} |
|||
|
|||
mBinding.tvDone.setOnClickListener { |
|||
adapter.getSelectValue()?.let { |
|||
mViewModel.setSelectValue(it,PreferenceActivity.ViewType.TYPE_PROMOTE) |
|||
(requireActivity() as PreferenceActivity).apply { |
|||
showFragment(PreferenceActivity.ViewType.TYPE_LIKE) |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
private fun initAdapter(){ |
|||
|
|||
mBinding.rvList.adapter = adapter |
|||
mBinding.rvList.layoutManager = LinearLayoutManager(requireContext()) |
|||
|
|||
} |
|||
|
|||
} |
@ -0,0 +1,44 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import androidx.lifecycle.ViewModelProvider |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentRemindBinding |
|||
import com.whitefish.app.feature.plan.PlanActivity |
|||
|
|||
class RemindFragment : BaseFragment<FragmentRemindBinding, RemindViewModel>() { |
|||
override fun bind() { |
|||
initAdapter() |
|||
mBinding.tvDone.setOnClickListener { |
|||
PlanActivity.start(requireContext()) |
|||
} |
|||
} |
|||
|
|||
private fun initAdapter() { |
|||
mBinding.rvList.adapter = SelectAdapter(true) |
|||
.apply { |
|||
setData( |
|||
listOf( |
|||
"每周一", |
|||
"每周二", |
|||
"每周三", |
|||
"每周四", |
|||
"每周五", |
|||
"每周六", |
|||
"每周日", |
|||
) |
|||
) |
|||
} |
|||
mBinding.rvList.layoutManager = LinearLayoutManager(requireContext()) |
|||
|
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_remind |
|||
} |
|||
|
|||
override fun setViewModel(): RemindViewModel { |
|||
return ViewModelProvider(this)[RemindViewModel::class.java] |
|||
} |
|||
} |
@ -0,0 +1,6 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import com.whitefish.app.BaseViewModel |
|||
|
|||
class RemindViewModel:BaseViewModel() { |
|||
} |
@ -0,0 +1,80 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import android.util.Log |
|||
import android.view.LayoutInflater |
|||
import android.view.ViewGroup |
|||
import androidx.appcompat.content.res.AppCompatResources |
|||
import androidx.databinding.DataBindingUtil |
|||
import androidx.recyclerview.widget.RecyclerView.ViewHolder |
|||
import com.bumptech.glide.Glide |
|||
import com.whitefish.app.BaseAdapter |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.ListItemSelectRadiusBinding |
|||
|
|||
class SelectAdapter(val checkBox:Boolean):BaseAdapter<String, SelectAdapter.ListItemTargetHolder>() { |
|||
|
|||
private var selectedPosition = -1 |
|||
private val selectMap = HashMap<Int,String>() |
|||
|
|||
|
|||
fun getSelectValue() = if (selectedPosition == -1){null}else{data[selectedPosition]} |
|||
|
|||
fun getSelectList() = selectMap.values.toList() |
|||
|
|||
private fun setOnSelect(position: Int){ |
|||
if (checkBox){ |
|||
if (selectMap[position] == null){ |
|||
selectMap[position] = data[position] |
|||
}else{ |
|||
selectMap.remove(position) |
|||
} |
|||
notifyItemChanged(position) |
|||
}else{ |
|||
val oldPosition = selectedPosition |
|||
selectedPosition = position |
|||
notifyItemChanged(oldPosition) |
|||
notifyItemChanged(selectedPosition) |
|||
} |
|||
|
|||
} |
|||
|
|||
override fun setData(newData:List<String>, diffUpdate:Boolean){ |
|||
super.setData(newData,diffUpdate) |
|||
selectedPosition = -1 |
|||
} |
|||
|
|||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ListItemTargetHolder { |
|||
return ListItemTargetHolder( |
|||
DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.list_item_select_radius,parent,false) |
|||
) |
|||
} |
|||
|
|||
override fun onBindViewHolder(holder: ListItemTargetHolder, position: Int) { |
|||
holder.bind(data[position],position) |
|||
} |
|||
|
|||
inner class ListItemTargetHolder(private val binding: ListItemSelectRadiusBinding):ViewHolder(binding.root){ |
|||
fun bind(data:String,position: Int){ |
|||
binding.item.setOnClickListener { |
|||
setOnSelect(position) |
|||
} |
|||
|
|||
binding.data = data |
|||
if (checkBox){ |
|||
if (selectMap[position] != null){ |
|||
Glide.with(binding.root.context).load(AppCompatResources.getDrawable(binding.root.context,R.drawable.item_checkbox_selected)).into(binding.ivSelectState) |
|||
}else{ |
|||
Glide.with(binding.root.context).load(AppCompatResources.getDrawable(binding.root.context,R.drawable.item_checkbox_unselected)).into(binding.ivSelectState) |
|||
} |
|||
}else{ |
|||
if (position == selectedPosition){ |
|||
Glide.with(binding.root.context).load(AppCompatResources.getDrawable(binding.root.context,R.drawable.item_radius_selected)).into(binding.ivSelectState) |
|||
}else{ |
|||
Glide.with(binding.root.context).load(AppCompatResources.getDrawable(binding.root.context,R.drawable.item_radius_unselect)).into(binding.ivSelectState) |
|||
} |
|||
} |
|||
|
|||
} |
|||
} |
|||
|
|||
} |
@ -0,0 +1,50 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.lifecycle.Lifecycle |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentSelectRadiusListBinding |
|||
import com.whitefish.app.ext.collectWith |
|||
import com.whitefish.app.feature.preference.PreferenceActivity |
|||
import com.whitefish.app.feature.preference.PreferenceViewModel |
|||
|
|||
class TargetFragment:BaseFragment<FragmentSelectRadiusListBinding,PreferenceViewModel>() { |
|||
private val viewModel: PreferenceViewModel by activityViewModels() |
|||
private val adapter = SelectAdapter(false) |
|||
|
|||
|
|||
override fun bind(){ |
|||
collect() |
|||
initAdapter() |
|||
mViewModel.refresh(PreferenceActivity.ViewType.TYPE_TARGET) |
|||
mBinding.tvDone.setOnClickListener { |
|||
adapter.getSelectValue()?.let { |
|||
mViewModel.setSelectValue(it,PreferenceActivity.ViewType.TYPE_TARGET) |
|||
(requireActivity() as PreferenceActivity).showFragment(PreferenceActivity.ViewType.TYPE_PROMOTE) |
|||
} |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_select_radius_list |
|||
} |
|||
|
|||
override fun setViewModel(): PreferenceViewModel { |
|||
return viewModel |
|||
} |
|||
|
|||
private fun collect(){ |
|||
mViewModel.targetList.collectWith(viewLifecycleOwner,Lifecycle.State.RESUMED){ |
|||
adapter.setData(it) |
|||
} |
|||
|
|||
} |
|||
|
|||
private fun initAdapter(){ |
|||
mBinding.rvList.adapter = adapter |
|||
mBinding.rvList.layoutManager = LinearLayoutManager(requireContext()) |
|||
} |
|||
|
|||
} |
@ -0,0 +1,40 @@ |
|||
package com.whitefish.app.feature.preference.fragment |
|||
|
|||
import androidx.fragment.app.activityViewModels |
|||
import androidx.recyclerview.widget.LinearLayoutManager |
|||
import com.whitefish.app.BaseFragment |
|||
import com.whitefish.app.R |
|||
import com.whitefish.app.databinding.FragmentWeightBinding |
|||
import com.whitefish.app.feature.preference.PreferenceActivity |
|||
import com.whitefish.app.feature.preference.PreferenceViewModel |
|||
|
|||
class WeightFragment : BaseFragment<FragmentWeightBinding, PreferenceViewModel>() { |
|||
private val viewModel: PreferenceViewModel by activityViewModels() |
|||
|
|||
override fun bind() { |
|||
initAdapter() |
|||
mBinding.tvDone.setOnClickListener { |
|||
mViewModel.setSelectValue( |
|||
mBinding.spvList.currentValue, |
|||
PreferenceActivity.ViewType.TYPE_WEIGHT |
|||
) |
|||
(requireActivity() as PreferenceActivity).showFragment(PreferenceActivity.ViewType.TYPE_TARGET) |
|||
} |
|||
mBinding.tvSkip.setOnClickListener { |
|||
(requireActivity() as PreferenceActivity).showFragment(PreferenceActivity.ViewType.TYPE_TARGET) |
|||
} |
|||
} |
|||
|
|||
override fun setLayout(): Int { |
|||
return R.layout.fragment_weight |
|||
} |
|||
|
|||
override fun setViewModel(): PreferenceViewModel { |
|||
return viewModel |
|||
} |
|||
|
|||
private fun initAdapter() { |
|||
mBinding.spvList.setData(mViewModel.weightList, 50) |
|||
mBinding.spvList.layoutManager = LinearLayoutManager(requireContext()) |
|||
} |
|||
} |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue