├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── dbnavigator.xml ├── gradle.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── watermelon │ │ └── focus │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── watermelon │ │ │ └── focus │ │ │ ├── BaseApplication.kt │ │ │ ├── MainPageActivity.kt │ │ │ ├── model │ │ │ ├── bean │ │ │ │ └── TodoStar.kt │ │ │ ├── database │ │ │ │ ├── StarDao.kt │ │ │ │ └── StarDatabase.kt │ │ │ └── module │ │ │ │ └── StarModule.kt │ │ │ ├── service │ │ │ └── StarService.kt │ │ │ └── ui │ │ │ ├── page │ │ │ ├── page_countdown │ │ │ │ ├── CountDownPage.kt │ │ │ │ └── CountDownPageViewModel.kt │ │ │ ├── page_main │ │ │ │ ├── MainPage.kt │ │ │ │ └── MainPageViewModel.kt │ │ │ ├── page_newStar │ │ │ │ ├── NewStarPage.kt │ │ │ │ └── NewStarViewModel.kt │ │ │ └── page_starList │ │ │ │ ├── StarListPage.kt │ │ │ │ └── StarListPageViewModel.kt │ │ │ └── widget │ │ │ ├── Clock.kt │ │ │ ├── ColorButton.kt │ │ │ ├── ColorSelector.kt │ │ │ ├── DeleteButton.kt │ │ │ ├── MainPageBottomBar.kt │ │ │ ├── MyIconButton.kt │ │ │ ├── NewStar.kt │ │ │ ├── NewStarItem.kt │ │ │ ├── Signature.kt │ │ │ ├── Star.kt │ │ │ ├── StarIconButton.kt │ │ │ ├── StarItem.kt │ │ │ ├── StarName.kt │ │ │ ├── TimeNode.kt │ │ │ ├── TimeSelector.kt │ │ │ └── TimeText.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── bottom_star.xml │ │ ├── bottom_universe.xml │ │ ├── bottom_user.xml │ │ ├── button_choose_line.xml │ │ ├── button_choose_star.xml │ │ ├── ic_launcher_background.xml │ │ ├── ic_star.xml │ │ └── logo.png │ │ ├── font │ │ └── store_my_stamp_number.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-night │ │ └── themes.xml │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ └── test │ └── java │ └── watermelon │ └── focus │ └── ExampleUnitTest.kt ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── picture ├── 1651511878919.gif ├── 1651562261764.gif ├── 1651562374424.gif ├── 1651562434323.gif ├── 1651573171050.gif ├── 1651573977400.gif └── 84ED106B-EDEA-495F-9652-0BF51BD4CD07.png └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/dbnavigator.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 | 21 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 9 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Watermelon02 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Focus 2 | 3 | 4 | > Focus是一款帮助你集中的app——为自己的目标建立星球,将时间投入在上面。花在星球上的每一分钟都会被记录,每颗星球可以定制颜色与外观。为了贯彻简单干净不让人分心的设计理念,app采用白灰为主色调。为了不使界面显得单调,所以增加了不少的动画 5 | 6 | UI使用`Compose`实现,采用了MVI架构,涉及框架包括Navigation,Hilt,Room,Flow 7 | 8 | 9 | 10 | ## 预览 11 | 先看看目前的所有功能总的预览吧,使用流程主要就是: 12 | 13 | 1. `创建星球界面`设置星球名字,打卡时间和详细描述 14 | 2. `星球列表界面`选中要打卡的星球 15 | 3. `主界面`设置要打卡的时间,开始打卡 16 | 17 | ![1651511878919](./picture/1651511878919.gif) 18 | 19 | ## 功能 20 | ### 主界面 21 | - **左右滑动** 设置倒计时时间 22 | - **长按星球** 当卫星消失再出现并开始逆时针转动的时候开始计时。打卡的时长会被记录进对应的星球中 23 | 24 | 25 | 26 | ![1651511878919](./picture/1651511878919.gif) 27 | 28 | 29 | ### 星球列表界面 30 | > **双击星球** 进入星球列表界面 31 | - **上拉星球** 显示星球颜色的渐变背景,再点击可以删除星球 32 | - **左右滑动** 查看已有星球 33 | - **上滑界面** 查看星球详细信息 34 | - **点击✨** 选中当前星球为要打卡的星球 35 | 36 | ![1651562374424](./picture/1651562374424.gif) 37 | 38 | 39 | ### 新建星球界面 40 | > **点击** 星球列表界面的最后一个带加号的星球进入新建星球界面 41 | - **颜色选择** 选中新星球的颜色,这些颜色按钮也做了渐变的动画 42 | - **时间选择** 没啥好说的 43 | - **点击✨** 创建星球 44 | 45 | ![1651562434323](./picture/1651562434323.gif) 46 | 47 | 48 | ## 主要实现 49 | ### 架构 50 | - MVI 51 | >MVI相比MVVM更加强调`数据的单向流动`和`唯一数据源`,项目中将用户所有的操作包装为Action,传入到界面对应的ViewModel中进行处理,在ViewModel中对界面的状态进行统一集中管理。而UI层则订阅ViewState,当界面状态变化时,Compose函数会自动进行更新 52 | ![84ED106B-EDEA-495F-9652-0BF51BD4CD07](./picture/84ED106B-EDEA-495F-9652-0BF51BD4CD07.png) 53 | 54 | ```kotlin 55 | //将state的setter设置为私有,使状态只能在dispatch()中修改,保证数据只能单向修改 56 | var mainPageViewStates by mutableStateOf(MainPageViewState()) 57 | private set 58 | 59 | //主界面ViewModel中统一对事件进行处理 60 | fun dispatch(action: MainPageAction) { 61 | when (action) { 62 | is MainPageAction.DegreeUpdate -> degreeUpdate(action.degree) 63 | is MainPageAction.SelectStar -> selectStar(action.todoStar) 64 | is MainPageAction.StarChanged -> mainPageViewStates.starChanged.value = false 65 | is MainPageAction.NewStarPage -> mainPageViewStates.sheetPage.value = 66 | BottomSheetPage.NewStarPage 67 | is MainPageAction.BackToStarList -> mainPageViewStates.sheetPage.value = 68 | BottomSheetPage.StarListPage 69 | is MainPageAction.AddStar -> mainPageViewStates.addStar.value = true 70 | is MainPageAction.RefreshStar -> mainPageViewStates.refreshStarList.value++ 71 | is MainPageAction.CountdownStart->countDownStart() 72 | is MainPageAction.Countdown -> countDown() 73 | is MainPageAction.InitSelectedStar -> initSelectedStar() 74 | } 75 | 76 | //界面事件的密封类 77 | sealed class MainPageAction { 78 | class DegreeUpdate(val degree: Float) : MainPageAction() 79 | ....... 80 | } 81 | 82 | } 83 | 84 | ``` 85 | 86 | - 依赖注入 87 | 通过使用`Hilt`来对ViewModel和Room数据库的Dao进行依赖注入,可以非常简单地实现解耦 88 | ```kotlin 89 | //为ViewModel加上@HiltViewMode注解 90 | @HiltViewModel 91 | class NewStarViewModel constructor() : ViewModel() {...} 92 | //然后直接在Composable函数的参数中使用hiltViewModel()进行依赖注入 93 | @Composable 94 | fun NewStarPage( 95 | ... 96 | viewModel: NewStarViewModel = hiltViewModel() 97 | ) {...} 98 | 99 | 100 | ``` 101 | 102 | - 数据存储 103 | 直接使用`Room`数据库来进行存储,同时,Room数据库支持直接返回`Flow`,所以也可以使用协程配合Flow来获取查询结果 104 | ```kotlin 105 | //刷新星球列表数据,使用Flow来获取返回结果 106 | private fun refreshStarList() { 107 | viewModelScope.launch { 108 | starDao.queryAllStar().collect { 109 | if (it.isNotEmpty()){ 110 | starListPageStates.starList.value = it 111 | } 112 | } 113 | } 114 | } 115 | ``` 116 | 117 | ### 界面 118 | 119 | - `声明式`和`手势api` 120 | 121 | Compose的`声明式`写法和一些`手势api`让许多控件实现起来更为简单。 122 | 比如项目主界面中的星球倒计时时钟,这个时钟既需要能够处理用户的手指滑动来设置倒计时时间,还需要能够在用户长按之后开始倒计时。 123 | 124 | 在原先使用自定义view实现的时候,需要重写其`onTouchEvent()`,手动计算前后两次手指移动距离,然后旋转view,并回调给时钟View设置的接口来更新倒计时的时间,然后再将更新后的时间传递给上方的TextView。长按事件处理起来同样需要经过类似的步骤。 125 | 126 | 而使用compose则只需要一个记录滑动度数的state,然后将这个state传入`手势(Gesture)api`中。这样compose就会自动更新state的数值,而其它使用该state作为参数的compose函数也能自动重组。 127 | 128 | - `LazyRow&LazyPage` 129 | 130 | `LazyRow`和`LazyPage`类似于RecyclerView,但是不需要再去写adapter,layoutManager等,而且可以方便的将不同类型的item拼接在一起,不需要实现RecyclerViewConcatAdapter或是设置ViewHolder中不同的viewType。(但是好像目前性能不如RV 131 | ```kotlin 132 | LazyRow(modifier = Modifier 133 | .fillMaxHeight(0.35f) 134 | .fillMaxWidth(), content = { 135 | //已创建星球列表 136 | for (star in starList) {item {...}} 137 | //新增星球Item,点击进入新增星球界面 138 | item { 139 | NewStarItem (...) 140 | } 141 | }) 142 | ``` 143 | 144 | - 动画 145 | compose中自带不少强大的、可扩展的动画 API,可以轻松的实现一些效果。比如———— 146 | `AnimatedVisibility()`配合`ModalBottomSheetLayout()`实现伸缩列表: 147 | ```kotlin 148 | //star的详细资料,开始隐藏,当modalBottomSheet展开时出现 149 | AnimatedVisibility(visible = sheetState.currentValue == ModalBottomSheetValue.Expanded) { 150 | Card( 151 | shape = RoundedCornerShape(20.dp), 152 | modifier = Modifier 153 | .fillMaxHeight(0.7f) 154 | .fillMaxWidth(), 155 | ) {...} 156 | } 157 | ``` 158 | ![1651573977400](./picture/1651573977400.gif) 159 | 160 | 再比如`InfiniteTransition`实现的动态渐变Button (删除星球按钮: 161 | ```kotlin 162 | @Composable 163 | fun DeleteButton(color: Color, onClick: () -> Unit) { 164 | val colorAnimation1 by rememberInfiniteTransition().animateColor( 165 | initialValue = color.copy(alpha = 0.35f), 166 | targetValue = color.copy(alpha = 0.75f), 167 | animationSpec = InfiniteRepeatableSpec( 168 | animation = tween( 169 | durationMillis = 4750 + 500 * color.alpha.toInt(), 170 | easing = FastOutLinearInEasing, 171 | delayMillis = 2730 * color.alpha.toInt() 172 | ), 173 | repeatMode = RepeatMode.Reverse 174 | ) 175 | ) 176 | val colorAnimation2 by rememberInfiniteTransition().animateColor(类似上面的实现) 177 | Card( 178 | modifier = Modifier 179 | .padding(start = 10.dp, top = 20.dp, end = 10.dp) 180 | .height(200.dp) 181 | .width(175.dp), 182 | shape = RoundedCornerShape(20.dp) 183 | ) { 184 | IconButton(onClick = onClick) { 185 | Canvas(modifier = Modifier 186 | .padding(start = 10.dp, top = 40.dp, end = 10.dp) 187 | .height(170.dp) 188 | .width(175.dp), onDraw = { 189 | //渐变色块 190 | drawCircle( 191 | brush = Brush.linearGradient( 192 | colors = listOf( 193 | colorAnimation2, 194 | colorAnimation1 195 | ), 196 | start = Offset(0f,0f),end = Offset(400.dp.value,400.dp.value), 197 | ), 198 | radius = 300.dp.value, 199 | center = Offset(x = size.width / 2, y = size.height / 2) 200 | ) 201 | }) 202 | } 203 | } 204 | } 205 | ``` 206 | ![1651573171050](./picture/1651573171050.gif) 207 | 208 | - 自定义绘制 209 | 这方面感觉和原生的写法大同小异,而自定义布局还没来得及了解,这里就不赘述了 210 | 211 | ## 总结 212 | 首次上手Compose和MVI,项目中的实现可能有不小的问题。Compose在实现许多界面元素的时候感觉比View要更加简单高效,但是用到的许多api都带有实验性注解。而且目前compose的教程不是很多,在遇到问题的时候不太好解决。 213 | 214 | 这里非常推荐想要上手的同学们参考`Jetpack compose博物馆` https://jetpackcompose.cn/docs/ 进行学习,不仅介绍了许多api的使用,带有实战例子,而且还有compose原理的解析。感恩 215 | 216 | 因为时间限制所以还有很多想要实现的功能没来得及做,不出意外的话之后会继续修改bug和增加功能。最后再次希望您能够给个star😭 -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.application' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'kotlin-kapt' 5 | id 'dagger.hilt.android.plugin' 6 | } 7 | 8 | android { 9 | compileSdk 31 10 | 11 | defaultConfig { 12 | minSdk 21 13 | targetSdk 31 14 | versionCode 1 15 | versionName "1.0" 16 | 17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 18 | javaCompileOptions { 19 | annotationProcessorOptions { 20 | arguments += [AROUTER_MODULE_NAME: project.getName()] 21 | } 22 | } 23 | } 24 | 25 | buildTypes { 26 | release { 27 | minifyEnabled false 28 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 29 | } 30 | } 31 | compileOptions { 32 | sourceCompatibility JavaVersion.VERSION_1_8 33 | targetCompatibility JavaVersion.VERSION_1_8 34 | } 35 | buildFeatures { 36 | compose true 37 | } 38 | kotlinOptions { 39 | jvmTarget = '1.8' 40 | } 41 | composeOptions { 42 | kotlinCompilerExtensionVersion compose_version 43 | } 44 | } 45 | 46 | dependencies { 47 | 48 | implementation 'androidx.core:core-ktx:1.7.0' 49 | implementation "androidx.compose.ui:ui:$compose_version" 50 | implementation "androidx.compose.material:material:$compose_version" 51 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 52 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1' 53 | implementation 'androidx.activity:activity-compose:1.4.0' 54 | implementation 'androidx.room:room-ktx:2.4.2' 55 | testImplementation 'junit:junit:4.13.2' 56 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 57 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' 58 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 59 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 60 | 61 | implementation 'com.alibaba:arouter-api:1.5.2' 62 | kapt 'com.alibaba:arouter-compiler:1.5.2' 63 | kapt "androidx.room:room-compiler:2.5.0-alpha01" 64 | //以下为compose 65 | // 基础UI框架 66 | implementation "androidx.compose.ui:ui:$compose_version" 67 | // Material风格布局 68 | implementation "androidx.compose.material:material:$compose_version" 69 | // Compose扩展Activity 70 | implementation 'androidx.activity:activity-compose:1.6.0-alpha03' 71 | //compose pager 72 | implementation "com.google.accompanist:accompanist-pager:0.24.2-alpha" 73 | // UI测试 74 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" 75 | // UI工具包 76 | debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" 77 | implementation("androidx.constraintlayout:constraintlayout:2.1.3") 78 | implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0") 79 | //Navigation 80 | implementation "androidx.navigation:navigation-compose:2.5.0-beta01" 81 | //Hilt 82 | implementation "com.google.dagger:hilt-android:$hilt_version" 83 | kapt "com.google.dagger:hilt-android-compiler:$hilt_version" 84 | implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" 85 | implementation "androidx.hilt:hilt-navigation-compose:1.0.0" 86 | kapt 'androidx.hilt:hilt-compiler:1.0.0' 87 | implementation 'com.google.code.gson:gson:2.8.6' 88 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/watermelon/focus/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("xigua.module_main", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/BaseApplication.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Application 5 | import android.content.Context 6 | import com.alibaba.android.arouter.BuildConfig 7 | import com.alibaba.android.arouter.launcher.ARouter 8 | import dagger.hilt.android.HiltAndroidApp 9 | 10 | /** 11 | * description : 用于初始化Arouter的Application子类 12 | * author : Watermelon02 13 | * email : 1446157077@qq.com 14 | * date : 2022/3/29 08:54 15 | */ 16 | 17 | @HiltAndroidApp 18 | class BaseApplication : Application() { 19 | companion object{ 20 | //app生命周期context,用于创建数据库 21 | @SuppressLint("StaticFieldLeak", "CI_StaticFieldLeak") 22 | lateinit var context: Context 23 | } 24 | 25 | override fun attachBaseContext(base: Context) { 26 | super.attachBaseContext(base) 27 | context = base 28 | } 29 | 30 | override fun onCreate() { 31 | super.onCreate() 32 | // 这两行必须写在init之前,否则这些配置在init过程中将无效 33 | if (BuildConfig.DEBUG) { 34 | // 打印日志 35 | ARouter.openLog() 36 | // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险) 37 | ARouter.openDebug() 38 | } 39 | // 尽可能早,推荐在Application中初始化 40 | ARouter.init(this) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/MainPageActivity.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus 2 | 3 | import android.content.Intent 4 | import android.os.Bundle 5 | import androidx.activity.ComponentActivity 6 | import androidx.activity.compose.BackHandler 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.ExperimentalMaterialApi 10 | import androidx.compose.material.ModalBottomSheetLayout 11 | import androidx.compose.material.ModalBottomSheetValue 12 | import androidx.compose.material.Scaffold 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.rememberCoroutineScope 16 | import androidx.compose.ui.unit.dp 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | import androidx.navigation.NavType 19 | import androidx.navigation.compose.NavHost 20 | import androidx.navigation.compose.composable 21 | import androidx.navigation.compose.rememberNavController 22 | import androidx.navigation.navArgument 23 | import com.alibaba.android.arouter.facade.annotation.Route 24 | import dagger.hilt.android.AndroidEntryPoint 25 | import kotlinx.coroutines.Dispatchers 26 | import kotlinx.coroutines.launch 27 | import kotlinx.coroutines.withContext 28 | import watermelon.focus.service.StarService 29 | import watermelon.focus.ui.page.page_countdown.CountDownPageViewModel 30 | import watermelon.focus.ui.page.page_countdown.CountdownPage 31 | import watermelon.focus.ui.page.page_main.MainPage 32 | import watermelon.focus.ui.page.page_main.MainPageViewModel 33 | import watermelon.focus.ui.page.page_main.widget.MainPageBottomBar 34 | import watermelon.focus.ui.page.page_newStar.NewStarPage 35 | import watermelon.focus.ui.page.page_starList.StarListPage 36 | 37 | @AndroidEntryPoint 38 | @Route(path = "/main/mainPageActivity") 39 | class MainPageActivity : ComponentActivity() { 40 | 41 | @OptIn(ExperimentalMaterialApi::class) 42 | override fun onCreate(savedInstanceState: Bundle?) { 43 | super.onCreate(savedInstanceState) 44 | val serviceIntent = Intent(this, StarService::class.java) 45 | this.startService(serviceIntent) 46 | setContent { 47 | Scaffold( 48 | content = { 49 | MainPageContent() 50 | }, 51 | bottomBar = { 52 | MainPageBottomBar() 53 | } 54 | ) 55 | } 56 | } 57 | 58 | @OptIn(ExperimentalMaterialApi::class) 59 | @Composable 60 | private fun MainPageContent() { 61 | //先获取MainPageViewModel,以获取其中的sheetState 62 | val mainPageViewModel: MainPageViewModel = hiltViewModel() 63 | val sheetState = 64 | mainPageViewModel.mainPageViewStates.sheetState 65 | ModalBottomSheetLayout( 66 | sheetShape = RoundedCornerShape(30.dp), 67 | sheetState = sheetState, 68 | sheetContent = { 69 | //星球选择列表page 70 | if (mainPageViewModel.mainPageViewStates.sheetPage.value is MainPageViewModel.BottomSheetPage.StarListPage) { 71 | StarListPage( 72 | sheetState = mainPageViewModel.mainPageViewStates.sheetState, 73 | selectStarListener = { 74 | mainPageViewModel.dispatch( 75 | MainPageViewModel.MainPageAction.SelectStar(it) 76 | ) 77 | }, 78 | onAddStar = { mainPageViewModel.dispatch(MainPageViewModel.MainPageAction.NewStarPage) }, 79 | refreshStarList = mainPageViewModel.mainPageViewStates.refreshStarList 80 | ) 81 | } else {//新增星球page 82 | NewStarPage( 83 | sheetState = sheetState, 84 | onCreateStar = { MainPageViewModel.MainPageAction.AddStar }) 85 | } 86 | } 87 | ) { 88 | val navController = rememberNavController() 89 | val countdownViewModel = remember { CountDownPageViewModel() } 90 | //Navigation 91 | NavHost(navController = navController, startDestination = "page_main") { 92 | composable("page_main") { MainPage(navController, mainPageViewModel) } 93 | composable( 94 | route = "page_countdown/{settingMinutes}", 95 | arguments = listOf(navArgument("settingMinutes") { 96 | type = NavType.FloatType 97 | defaultValue = 0f 98 | } 99 | ) 100 | ) { 101 | val settingSeconds = it.arguments?.getFloat("settingMinutes")?.times(60) 102 | //倒计时时间不为空,导航到启动CountdownPage 103 | settingSeconds?.let { settingSeconds -> 104 | CountdownPage( 105 | countdownSeconds = settingSeconds.toInt(), 106 | viewModel = countdownViewModel 107 | ) 108 | } 109 | } 110 | } 111 | } 112 | //modalBottomBar不会自动在侧滑后退出,所以用BackHandler处理 113 | val scope = rememberCoroutineScope() 114 | BackHandler( 115 | enabled = (sheetState.currentValue == ModalBottomSheetValue.HalfExpanded 116 | || sheetState.currentValue == ModalBottomSheetValue.Expanded), 117 | onBack = { 118 | //如果当前为StarListPage,则隐藏BottomSheet 119 | if (mainPageViewModel.mainPageViewStates.sheetPage.value is MainPageViewModel.BottomSheetPage.StarListPage) { 120 | scope.launch { sheetState.hide() } 121 | } else { 122 | mainPageViewModel.dispatch(MainPageViewModel.MainPageAction.BackToStarList) 123 | //退回到starListPage后刷新数据 124 | scope.launch { 125 | withContext(Dispatchers.IO) { 126 | mainPageViewModel.dispatch(MainPageViewModel.MainPageAction.RefreshStar) 127 | } 128 | } 129 | } 130 | } 131 | ) 132 | } 133 | 134 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/model/bean/TodoStar.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.model.bean 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.room.ColumnInfo 5 | import androidx.room.Entity 6 | import androidx.room.PrimaryKey 7 | import com.google.gson.annotations.SerializedName 8 | import java.io.Serializable 9 | 10 | /** 11 | * description : 代办事项类 12 | * author : Watermelon02 13 | * email : 1446157077@qq.com 14 | * date : 2022/4/27 23:27 15 | * @param name 标题 16 | * @param reminderTime 提醒时间,用于service后台提醒 17 | * @param reminderDate 提醒日期,用于根据日期查找显示对应日期的所有Todo 18 | * @param focusTime 已经专注于该星球的事件 19 | * @param description 备注 20 | */ 21 | @Entity(tableName = "todo_star") 22 | data class TodoStar( 23 | @PrimaryKey(autoGenerate = true) 24 | @ColumnInfo(name = "id") 25 | val id: Long, 26 | @ColumnInfo(name = "name") 27 | var name: String, 28 | @SerializedName("color") 29 | var color: Long = Color.White.value.toLong(), 30 | @ColumnInfo(name = "reminder_time") 31 | val reminderTime: Long, 32 | @ColumnInfo(name = "reminder_date") 33 | var reminderDate: String, 34 | @ColumnInfo(name = "focus_time") 35 | val focusTime: Long, 36 | var description: String 37 | ) : Serializable -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/model/database/StarDao.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.model.database 2 | 3 | import androidx.room.* 4 | import kotlinx.coroutines.flow.Flow 5 | import watermelon.focus.model.bean.TodoStar 6 | 7 | /** 8 | * description : TODO:类的作用 9 | * author : Watermelon02 10 | * email : 1446157077@qq.com 11 | * date : 2022/4/28 13:02 12 | */ 13 | @Dao 14 | interface StarDao { 15 | @Query("SELECT * FROM todo_star WHERE id = :id") 16 | fun queryStar(id: Long): Flow 17 | 18 | @Query("SELECT * FROM todo_star") 19 | fun queryAllStar(): Flow> 20 | 21 | @Delete 22 | fun deleteStar(todo: TodoStar) 23 | 24 | @Update 25 | fun updateStar(todo: TodoStar) 26 | 27 | @Insert 28 | fun insertStar(todo: TodoStar) 29 | 30 | @Query("SELECT * FROM todo_star WHERE reminder_date = :reminderDate") 31 | fun queryStarListByDate(reminderDate: String): Flow> 32 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/model/database/StarDatabase.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.model.database 2 | 3 | import android.content.Context 4 | import androidx.room.* 5 | import androidx.room.RoomDatabase 6 | import watermelon.focus.model.bean.TodoStar 7 | 8 | /** 9 | * author : Watermelon02 10 | * email : 1446157077@qq.com 11 | * date : 2022/4/28 12:59 12 | */ 13 | @Database(entities = [TodoStar::class],version = 1,exportSchema = false) 14 | abstract class StarDatabase : RoomDatabase() { 15 | abstract fun starDao(): StarDao 16 | 17 | companion object { 18 | @Volatile 19 | private var noteDatabase: StarDatabase? = null 20 | 21 | fun getInstance(context: Context): StarDatabase { 22 | if (noteDatabase == null) { 23 | synchronized(this) { 24 | if (noteDatabase == null) noteDatabase = 25 | Room.databaseBuilder(context, StarDatabase::class.java, "todo_database") 26 | .build() 27 | } 28 | } 29 | return noteDatabase!! 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/model/module/StarModule.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.model.module 2 | 3 | import android.app.Application 4 | import androidx.room.Room 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import watermelon.focus.model.database.StarDao 10 | import watermelon.focus.model.database.StarDatabase 11 | import javax.inject.Singleton 12 | 13 | /** 14 | * description : TODO:类的作用 15 | * author : Watermelon02 16 | * email : 1446157077@qq.com 17 | * date : 2022/5/1 19:23 18 | */ 19 | @Module 20 | @InstallIn(SingletonComponent::class) 21 | object StarModule { 22 | 23 | @Singleton 24 | @Provides 25 | fun provideStarDatabase(application: Application): StarDatabase { 26 | return Room.databaseBuilder(application, StarDatabase::class.java, "todo_database").build() 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | fun provideStarDao(starDatabase: StarDatabase): StarDao { 32 | return starDatabase.starDao() 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/service/StarService.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.service 2 | 3 | import android.app.* 4 | import android.content.Intent 5 | import android.graphics.BitmapFactory 6 | import android.os.Build 7 | import android.os.IBinder 8 | import androidx.annotation.RequiresApi 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import kotlinx.coroutines.* 11 | import watermelon.focus.MainPageActivity 12 | import watermelon.focus.R 13 | import watermelon.focus.model.bean.TodoStar 14 | import watermelon.focus.model.database.StarDao 15 | import java.util.* 16 | import javax.inject.Inject 17 | 18 | /** 19 | * description : 前台服务,如果当天有star则发送通知 20 | * author : Watermelon02 21 | * email : 1446157077@qq.com 22 | * date : 2022/5/2 16:22 23 | */ 24 | @AndroidEntryPoint 25 | class StarService : Service() { 26 | private lateinit var job: Job 27 | private val calendar = Calendar.getInstance() 28 | 29 | @Inject 30 | lateinit var starDao: StarDao 31 | override fun onBind(intent: Intent): IBinder? { 32 | return null 33 | } 34 | 35 | @RequiresApi(Build.VERSION_CODES.O) 36 | @OptIn(DelicateCoroutinesApi::class) 37 | override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { 38 | job = GlobalScope.launch { 39 | while (true) { 40 | withContext(Dispatchers.IO) { 41 | val nowDate = 42 | "${calendar[Calendar.YEAR]}-${calendar[Calendar.MONTH] + 1}-${calendar[Calendar.DATE]}" 43 | starDao.queryStarListByDate(nowDate).collect { 44 | for (star in it) { 45 | val notification = createNotification(star) 46 | startForeground(1, notification) 47 | } 48 | } 49 | delay(3000000) 50 | } 51 | } 52 | } 53 | return super.onStartCommand(intent, flags, startId) 54 | } 55 | 56 | @RequiresApi(api = Build.VERSION_CODES.O) 57 | fun createNotification(todoStar: TodoStar): Notification { 58 | //传入营业进度,生成相应的Notification对象 59 | val manager = 60 | getSystemService(NOTIFICATION_SERVICE) as NotificationManager 61 | val channel = 62 | NotificationChannel("42", "Focus", NotificationManager.IMPORTANCE_HIGH) 63 | manager.createNotificationChannel(channel) 64 | val intent = Intent(this, MainPageActivity::class.java) 65 | val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0) 66 | return Notification.Builder(this, channel.id) 67 | .setContentTitle("Star coming") 68 | .setLargeIcon(BitmapFactory.decodeResource(resources, R.drawable.logo)) 69 | .setSmallIcon(R.drawable.logo).setContentIntent(pendingIntent) 70 | .setContentText("the star of ${todoStar.name} is coming,come to focus on it").setAutoCancel(true) 71 | .build() 72 | } 73 | 74 | override fun onDestroy() { 75 | super.onDestroy() 76 | job.cancel() 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_countdown/CountDownPage.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_countdown 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.wrapContentSize 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.drawscope.rotate 18 | import androidx.compose.ui.text.font.Font 19 | import androidx.compose.ui.text.font.FontFamily 20 | import androidx.compose.ui.unit.dp 21 | import watermelon.focus.R 22 | 23 | /** 24 | * description : 倒计时界面 25 | * author : Watermelon02 26 | * email : 1446157077@qq.com 27 | * date : 2022/4/26 22:42 28 | */ 29 | @Composable 30 | fun CountdownPage(countdownSeconds: Int, viewModel: CountDownPageViewModel) { 31 | val rotateTransition by rememberInfiniteTransition().animateFloat( 32 | initialValue = 0f, 33 | targetValue = 360f, 34 | animationSpec = InfiniteRepeatableSpec( 35 | animation = tween(durationMillis = 60000, easing = LinearEasing), 36 | repeatMode = RepeatMode.Restart 37 | ) 38 | ) 39 | var trigger by remember { mutableStateOf(countdownSeconds) } 40 | 41 | val lastTime by animateIntAsState( 42 | targetValue = trigger * 1000, 43 | animationSpec = tween(countdownSeconds * 1000, easing = LinearEasing) 44 | ) 45 | 46 | DisposableEffect(Unit) { 47 | trigger = 0 48 | onDispose { } 49 | } 50 | 51 | val (hou, min, sec) = remember(lastTime / 1000) { 52 | val elapsedInSec = lastTime / 1000 53 | val hou = elapsedInSec / 3600 54 | val min = elapsedInSec / 60 - hou * 60 55 | val sec = elapsedInSec % 60 56 | viewModel.lastSecond.value = elapsedInSec 57 | Triple(hou, min, sec) 58 | } 59 | 60 | Box( 61 | modifier = Modifier 62 | .fillMaxSize() 63 | .background(Color(0xFFF8F8F8)), 64 | contentAlignment = Alignment.Center 65 | ) { 66 | Canvas(modifier = Modifier.fillMaxSize(), onDraw = { 67 | val radius = size.width / 4f 68 | val height = maxOf(size.height, size.width) 69 | val width = minOf(size.height, size.width) 70 | drawCircle( 71 | center = Offset(x = width / 2, y = height / 2), 72 | radius = 2 * radius, 73 | color = Color(0x80000000) 74 | ) 75 | //白色圆形背景 76 | drawCircle( 77 | center = Offset(x = width / 2, y = height / 2), 78 | radius = 1.975f * radius, 79 | color = Color.White 80 | ) 81 | rotate(degrees = rotateTransition, pivot = Offset(x = width / 2, y = height / 2)) { 82 | drawLine( 83 | color = Color.White, 84 | start = Offset(x = width / 2, y = height / 2), 85 | end = Offset(x = width / 2, y = height / 2 - 2f * radius), 86 | strokeWidth = 12.dp.value 87 | ) 88 | } 89 | }) 90 | //剩余时间 91 | Box( 92 | modifier = Modifier 93 | .wrapContentSize() 94 | .alpha(0.5f) 95 | ) { 96 | Text( 97 | text = "$hou : $min", 98 | style = MaterialTheme.typography.h3, 99 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)) 100 | ) 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_countdown/CountDownPageViewModel.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_countdown 2 | 3 | import androidx.compose.runtime.mutableStateOf 4 | import androidx.lifecycle.ViewModel 5 | 6 | /** 7 | * author : Watermelon02 8 | * email : 1446157077@qq.com 9 | * date : 2022/4/26 23:15 10 | */ 11 | class CountDownPageViewModel: ViewModel() { 12 | val lastSecond = mutableStateOf(0) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_main/MainPage.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_main 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.gestures.detectTapGestures 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.alpha 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.input.pointer.pointerInput 17 | import androidx.compose.ui.unit.dp 18 | import androidx.navigation.NavController 19 | import kotlinx.coroutines.launch 20 | import watermelon.focus.ui.widget.* 21 | 22 | /** 23 | * author : Watermelon02 24 | * email : 1446157077@qq.com 25 | * date : 2022/4/26 22:39 26 | */ 27 | @Composable 28 | @OptIn(ExperimentalMaterialApi::class) 29 | fun MainPage(navController: NavController, viewModel: MainPageViewModel) { 30 | val mainPageViewStates = viewModel.mainPageViewStates 31 | //用于启动modalBottomSheet 32 | val scope = rememberCoroutineScope() 33 | //切换星球时的渐变动画 34 | val alphaTransition = animateFloatAsState( 35 | targetValue = if (mainPageViewStates.starChanged.value) 1f else 0f, 36 | finishedListener = { viewModel.dispatch(MainPageViewModel.MainPageAction.StarChanged) }, 37 | animationSpec = tween(1000) 38 | ) 39 | Box( 40 | modifier = Modifier 41 | .fillMaxSize() 42 | .background(Color.White), contentAlignment = Alignment.Center 43 | ) { 44 | //开始倒计时时让卫星消失再浮现,以掩盖突然地位移 45 | var flag by remember { mutableStateOf(false) } 46 | val countdownAlpha1 by animateFloatAsState( 47 | targetValue = if (mainPageViewStates.countdownStart.value) 0f else 1f, animationSpec = tween(1000), 48 | finishedListener = { 49 | flag = true 50 | viewModel.dispatch(MainPageViewModel.MainPageAction.Countdown) 51 | }) 52 | val countdownAlpha2 by animateFloatAsState( 53 | targetValue = if (flag) 1f else 0f, 54 | animationSpec = tween(1000), 55 | ) 56 | Clock( 57 | draggableDegree = mainPageViewStates.draggableDegree, 58 | draggableState = mainPageViewStates.draggableState, 59 | dragStoppedListener = { 60 | viewModel.dispatch( 61 | MainPageViewModel.MainPageAction.DegreeUpdate( 62 | mainPageViewStates.draggableDegree.value 63 | ) 64 | ) 65 | }, 66 | settingMinutes = viewModel.mainPageViewStates.settingMinutes, 67 | mainPageViewStates.isCountdown.value, if (flag)countdownAlpha2 else countdownAlpha1 68 | ) 69 | TimeText( 70 | settingMinutes = mainPageViewStates.settingMinutes, 71 | draggableDegree = mainPageViewStates.draggableDegree.value, 72 | isCountdown = mainPageViewStates.isCountdown.value 73 | ) 74 | Signature(text = "Focus") 75 | //为star增加点击事件和取消点击波纹,点击后通过协程展开ModalBottomSheet 76 | Box( 77 | modifier = Modifier 78 | .size(200.dp) 79 | .alpha(if (mainPageViewStates.starChanged.value) alphaTransition.value else 1f) 80 | .pointerInput(Unit) { 81 | detectTapGestures(onDoubleTap = { 82 | //弹出ModalBottomSheet 83 | scope.launch { viewModel.mainPageViewStates.sheetState.show() } 84 | }, onLongPress = { 85 | //开始倒计时 86 | viewModel.dispatch(MainPageViewModel.MainPageAction.CountdownStart) 87 | }) 88 | }, 89 | contentAlignment = Alignment.Center 90 | ) { 91 | Star(Color(viewModel.mainPageViewStates.selectedStar.color)) 92 | } 93 | //StarName 94 | StarName(viewModel.mainPageViewStates.selectedStar.name) 95 | } 96 | //默认展示列表中的第一课星球 97 | LaunchedEffect(Unit, block = { 98 | viewModel.dispatch(MainPageViewModel.MainPageAction.InitSelectedStar) 99 | }) 100 | } 101 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_main/MainPageViewModel.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_main 2 | 3 | import androidx.compose.foundation.gestures.DraggableState 4 | import androidx.compose.material.ExperimentalMaterialApi 5 | import androidx.compose.material.ModalBottomSheetState 6 | import androidx.compose.material.ModalBottomSheetValue 7 | import androidx.compose.runtime.MutableState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.setValue 11 | import androidx.lifecycle.ViewModel 12 | import androidx.lifecycle.viewModelScope 13 | import dagger.hilt.android.lifecycle.HiltViewModel 14 | import kotlinx.coroutines.Dispatchers 15 | import kotlinx.coroutines.delay 16 | import kotlinx.coroutines.launch 17 | import kotlinx.coroutines.withContext 18 | import watermelon.focus.model.bean.TodoStar 19 | import watermelon.focus.model.database.StarDao 20 | import javax.inject.Inject 21 | 22 | /** 23 | * author : Watermelon02 24 | * email : 1446157077@qq.com 25 | * date : 2022/4/26 09:32 26 | */ 27 | @OptIn(ExperimentalMaterialApi::class) 28 | @HiltViewModel 29 | class MainPageViewModel @Inject constructor() : ViewModel() { 30 | companion object { 31 | const val TOTAL_MINUTES: Int = 720//12*60 min 32 | } 33 | 34 | @Inject 35 | lateinit var starDao: StarDao 36 | var mainPageViewStates by mutableStateOf(MainPageViewState()) 37 | private set 38 | 39 | fun dispatch(action: MainPageAction) { 40 | when (action) { 41 | is MainPageAction.DegreeUpdate -> degreeUpdate(action.degree) 42 | is MainPageAction.SelectStar -> selectStar(action.todoStar) 43 | is MainPageAction.StarChanged -> mainPageViewStates.starChanged.value = false 44 | is MainPageAction.NewStarPage -> mainPageViewStates.sheetPage.value = 45 | BottomSheetPage.NewStarPage 46 | is MainPageAction.BackToStarList -> mainPageViewStates.sheetPage.value = 47 | BottomSheetPage.StarListPage 48 | is MainPageAction.AddStar -> mainPageViewStates.addStar.value = true 49 | is MainPageAction.RefreshStar -> mainPageViewStates.refreshStarList.value++ 50 | is MainPageAction.CountdownStart->countDownStart() 51 | is MainPageAction.Countdown -> countDown() 52 | is MainPageAction.InitSelectedStar -> initSelectedStar() 53 | } 54 | } 55 | 56 | private fun initSelectedStar() { 57 | viewModelScope.launch { 58 | withContext(Dispatchers.IO) { 59 | starDao.queryAllStar().collect { 60 | if (it.isNotEmpty()){ 61 | mainPageViewStates.selectedStar = it[0] 62 | } 63 | } 64 | } 65 | } 66 | mainPageViewStates.starChanged.value = true 67 | } 68 | 69 | private fun countDownStart(){ 70 | mainPageViewStates.countdownStart.value = true 71 | } 72 | 73 | private fun countDown() { 74 | val diffMinutes = 1f / (60 * 100) 75 | val diffDegree = 2f / 100 76 | var countdownTime = 0 77 | mainPageViewStates.isCountdown.value = true 78 | viewModelScope.launch { 79 | while (mainPageViewStates.settingMinutes > 0) { 80 | //每过一分钟保存star的focus状态 81 | if (countdownTime == 60000) { 82 | saveStarFocusTime() 83 | countdownTime = 0 84 | } 85 | mainPageViewStates.draggableDegree.value = 86 | mainPageViewStates.draggableDegree.value - diffDegree 87 | mainPageViewStates = 88 | mainPageViewStates.copy(settingMinutes = mainPageViewStates.settingMinutes - diffMinutes) 89 | countdownTime += 10 90 | delay(10) 91 | } 92 | mainPageViewStates.isCountdown.value = false 93 | mainPageViewStates.countdownStart.value = false 94 | } 95 | } 96 | 97 | private fun saveStarFocusTime() { 98 | viewModelScope.launch { 99 | withContext(Dispatchers.IO) { 100 | mainPageViewStates.selectedStar = 101 | mainPageViewStates.selectedStar.copy(focusTime = mainPageViewStates.selectedStar.focusTime + 1) 102 | starDao.updateStar(mainPageViewStates.selectedStar) 103 | } 104 | } 105 | } 106 | 107 | 108 | //更改选中的star,设置starChanged,以播放星球切换动画 109 | private fun selectStar(todoStar: TodoStar) { 110 | mainPageViewStates = mainPageViewStates.copy(selectedStar = todoStar) 111 | mainPageViewStates.starChanged.value = true 112 | } 113 | 114 | /**Clock的DraggableDegree变化后,更改打卡时间及显示的时间字符*/ 115 | private fun degreeUpdate(degree: Float) { 116 | if (!mainPageViewStates.isCountdown.value) { 117 | mainPageViewStates = 118 | mainPageViewStates.copy(settingMinutes = degree * TOTAL_MINUTES / 360) 119 | } 120 | } 121 | 122 | sealed class MainPageAction { 123 | class DegreeUpdate(val degree: Float) : MainPageAction() 124 | class SelectStar(val todoStar: TodoStar) : MainPageAction() 125 | object CountdownStart:MainPageAction() 126 | object Countdown : MainPageAction() 127 | 128 | //星球切换动画播放完毕 129 | object StarChanged : MainPageAction() 130 | 131 | //进入NewStarPage界面 132 | object NewStarPage : MainPageAction() 133 | 134 | //新增星球 135 | object AddStar : MainPageAction() 136 | 137 | //保存新星球,或是侧滑返回StarListPage 138 | object BackToStarList : MainPageAction() 139 | 140 | object RefreshStar : MainPageAction() 141 | object InitSelectedStar : MainPageAction() 142 | } 143 | 144 | data class MainPageViewState( 145 | val sheetState: ModalBottomSheetState = 146 | ModalBottomSheetState(initialValue = ModalBottomSheetValue.Hidden), 147 | //ModalBottomSheetPage要显示的界面 148 | val sheetPage: MutableState = mutableStateOf(BottomSheetPage.StarListPage), 149 | val draggableDegree: MutableState = mutableStateOf(0f), 150 | val draggableState: DraggableState = DraggableState(onDelta = { 151 | draggableDegree.value = (draggableDegree.value + it / 10).coerceIn(0f, 270f) 152 | }), 153 | //用户设置的打卡时长 154 | val settingMinutes: Float = 0f, 155 | //当前选中要Focus的star 156 | var selectedStar: TodoStar = TodoStar( 157 | id = 1, 158 | name = "Star", color = 0xFF000000, 159 | reminderTime = 0L, 160 | reminderDate = "Today", focusTime = 0L, 161 | description = "remarks" 162 | ), 163 | val starChanged: MutableState = mutableStateOf(false), 164 | //新增star 165 | val addStar: MutableState = mutableStateOf(false), 166 | //当新增star后,自增 167 | val refreshStarList: MutableState = mutableStateOf(0), 168 | val isCountdown: MutableState = mutableStateOf(false), 169 | val countdownStart: MutableState = mutableStateOf(false) 170 | ) 171 | 172 | sealed class BottomSheetPage { 173 | object StarListPage : BottomSheetPage() 174 | object NewStarPage : BottomSheetPage() 175 | } 176 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_newStar/NewStarPage.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_newStar 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.interaction.MutableInteractionSource 9 | import androidx.compose.foundation.layout.* 10 | import androidx.compose.foundation.shape.RoundedCornerShape 11 | import androidx.compose.material.* 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.remember 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.alpha 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | import androidx.hilt.navigation.compose.hiltViewModel 20 | import watermelon.focus.model.bean.TodoStar 21 | import watermelon.focus.ui.widget.ColorSelector 22 | import watermelon.focus.ui.widget.Star 23 | import watermelon.focus.ui.widget.TimeSelector 24 | 25 | /** 26 | * description : StarList选择新增Star后的page 27 | * author : Watermelon02 28 | * email : 1446157077@qq.com 29 | * date : 2022/5/1 13:37 30 | */ 31 | @OptIn(ExperimentalMaterialApi::class, com.google.accompanist.pager.ExperimentalPagerApi::class) 32 | @Composable 33 | fun NewStarPage( 34 | sheetState: ModalBottomSheetState, 35 | onCreateStar: (TodoStar) -> Unit, 36 | viewModel: NewStarViewModel = hiltViewModel() 37 | ) { 38 | val viewModelStates = viewModel.starListPageStates 39 | //切换星球时的渐变动画 40 | val alphaTransition = animateFloatAsState( 41 | targetValue = if (viewModelStates.starColorChanged.value) 1f else 0f, 42 | finishedListener = { viewModel.dispatch(NewStarViewModel.NewStarPageAction.ColorChanged) }, 43 | animationSpec = tween(500) 44 | ) 45 | Column( 46 | modifier = Modifier 47 | .fillMaxHeight(0.7f) 48 | .background(Color(0xFFF8F8F8)) 49 | ) { 50 | Row { 51 | Card( 52 | modifier = Modifier 53 | .padding(start = 10.dp, top = 20.dp, end = 10.dp, bottom = 10.dp) 54 | .height(200.dp) 55 | .width(175.dp) 56 | .clickable( 57 | indication = null, 58 | interactionSource = remember { MutableInteractionSource() }) {}, 59 | shape = RoundedCornerShape(20.dp) 60 | ) { 61 | Box( 62 | modifier = Modifier 63 | .size(200.dp) 64 | .alpha(if (viewModelStates.starColorChanged.value) alphaTransition.value else 1f), 65 | contentAlignment = Alignment.Center 66 | ) { 67 | //预览star 68 | Star(Color(viewModelStates.newStar.value.color)) 69 | } 70 | 71 | //NewStarName 72 | TextField( 73 | value = viewModelStates.newStar.value.name, 74 | onValueChange = { 75 | viewModelStates.newStar.value = 76 | viewModelStates.newStar.value.copy(name = it) 77 | }, 78 | textStyle = MaterialTheme.typography.body2, 79 | modifier = Modifier 80 | .alpha(0.5f) 81 | .padding(top = 150.dp, start = 50.dp, end = 50.dp) 82 | .width(20.dp), colors = TextFieldDefaults.textFieldColors( 83 | backgroundColor = Color.White, 84 | focusedIndicatorColor = Color(0X80000000), cursorColor = Color(0X80000000) 85 | ) 86 | ) 87 | } 88 | //色彩选择器 89 | ColorSelector(selectColorListener = { 90 | viewModel.dispatch(NewStarViewModel.NewStarPageAction.SelectColor(it)) 91 | }) 92 | } 93 | 94 | //时间选择器,设置newStar的reminderTime时间 95 | TimeSelector(viewModel = viewModel, onCreateStar = { 96 | viewModel.dispatch(NewStarViewModel.NewStarPageAction.AddStar(it)) 97 | }) 98 | 99 | //NewStar的备注,开始隐藏在下面,当modalBottomSheet展开 100 | AnimatedVisibility(visible = sheetState.currentValue == ModalBottomSheetValue.Expanded) { 101 | Card( 102 | shape = RoundedCornerShape(20.dp), 103 | modifier = Modifier 104 | .fillMaxHeight(0.7f) 105 | .fillMaxWidth() 106 | .padding(start = 10.dp, end = 10.dp, bottom = 20.dp), 107 | ) { 108 | TextField( 109 | value = viewModelStates.newStar.value.description, 110 | onValueChange = { 111 | viewModelStates.newStar.value = 112 | viewModelStates.newStar.value.copy(description = it) 113 | }, 114 | textStyle = MaterialTheme.typography.body2, 115 | colors = TextFieldDefaults.textFieldColors( 116 | backgroundColor = Color.White, 117 | focusedIndicatorColor = Color(0X80000000), cursorColor = Color(0X80000000), 118 | textColor = Color(0X80000000) 119 | ) 120 | ) 121 | } 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_newStar/NewStarViewModel.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_newStar 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.setValue 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import watermelon.focus.model.bean.TodoStar 15 | import watermelon.focus.model.database.StarDao 16 | import java.util.* 17 | import javax.inject.Inject 18 | 19 | /** 20 | * author : Watermelon02 21 | * email : 1446157077@qq.com 22 | * date : 2022/5/1 15:59 23 | */ 24 | @HiltViewModel 25 | class NewStarViewModel @Inject constructor() : ViewModel() { 26 | var starListPageStates by mutableStateOf(StarListPageViewStates()) 27 | private set 28 | private val calendar1 = Calendar.getInstance() 29 | private val calendar2 = Calendar.getInstance() 30 | var year by mutableStateOf(calendar2[Calendar.YEAR]) 31 | var month by mutableStateOf(calendar2[Calendar.MONTH]) 32 | var day by mutableStateOf(calendar2[Calendar.DATE]) 33 | var days by mutableStateOf(getMonthDays(calculateDiffMonth(year, month))) 34 | 35 | @Inject 36 | lateinit var starDao: StarDao 37 | 38 | fun dispatch(action: NewStarPageAction) { 39 | when (action) { 40 | is NewStarPageAction.SelectColor -> { 41 | starListPageStates.newStar.value = 42 | starListPageStates.newStar.value.copy(color = action.color) 43 | starListPageStates.starColorChanged.value = true 44 | } 45 | is NewStarPageAction.ColorChanged -> starListPageStates.starColorChanged.value = false 46 | 47 | is NewStarPageAction.AddStar -> addStar(action.date) 48 | } 49 | } 50 | 51 | private fun addStar(date: String) { 52 | viewModelScope.launch { 53 | starListPageStates.newStar.value = 54 | starListPageStates.newStar.value.copy(reminderDate = date) 55 | withContext(Dispatchers.IO) { 56 | starDao.insertStar(starListPageStates.newStar.value) 57 | } 58 | } 59 | } 60 | 61 | data class StarListPageViewStates( 62 | val newStar: MutableState = mutableStateOf( 63 | TodoStar( 64 | id = 0, 65 | name = "Name", color = 0xFF5B0FA5, 66 | reminderTime = 0L, 67 | reminderDate = "Today", focusTime = 0L, 68 | description = "description" 69 | ) 70 | ), 71 | //NewStarPage改变颜色后的动画标志位 72 | val starColorChanged: MutableState = mutableStateOf(false) 73 | ) 74 | 75 | sealed class NewStarPageAction { 76 | //NewStarPage中选择颜色 77 | class SelectColor(val color: Long) : NewStarPageAction() 78 | class AddStar(val date:String) : NewStarPageAction() 79 | 80 | //星球颜色改变动画播放完毕 81 | object ColorChanged : NewStarPageAction() 82 | } 83 | 84 | @SuppressLint("SimpleDateFormat") 85 | private fun calculateDiffMonth( 86 | year: Int, 87 | month: Int 88 | ): Int { 89 | return if (this.month > month) { 90 | -((this.month - month) + 12 * (this.year - year)) 91 | } else { 92 | -((this.month + 12 - month) + 12 * (this.year - year - 1)) 93 | } 94 | } 95 | 96 | /**@param diff 要计算的月份与当前月份的差*/ 97 | private fun getMonthDays(diff: Int): List { 98 | val mDays = ArrayList() 99 | var diffYear = (calendar1[Calendar.MONTH] + diff) / 12 100 | val diffMonth = diff % 12 101 | if (calendar1[Calendar.MONTH] + diff < 0) diffYear += -1 102 | if (calendar1[Calendar.MONTH] + diff > 12) diffYear += 1 103 | calendar1.roll(Calendar.YEAR, diffYear) 104 | calendar1.roll(Calendar.MONTH, diffMonth) 105 | calendar1[Calendar.DATE] = 1 //把日期设置为当月第一天 106 | calendar1.roll(Calendar.DATE, -1) //日期回滚一天,也就是最后一天 107 | for (i in 1..calendar1[Calendar.DATE]) { 108 | mDays.add(i) 109 | } 110 | return mDays.toList() 111 | } 112 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_starList/StarListPage.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_starList 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.lazy.LazyRow 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.alpha 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.text.font.Font 15 | import androidx.compose.ui.text.font.FontFamily 16 | import androidx.compose.ui.unit.dp 17 | import androidx.hilt.navigation.compose.hiltViewModel 18 | import watermelon.focus.R 19 | import watermelon.focus.model.bean.TodoStar 20 | import watermelon.focus.ui.widget.DeleteButton 21 | import watermelon.focus.ui.widget.NewStarItem 22 | import watermelon.focus.ui.widget.StarIconButton 23 | import watermelon.focus.ui.widget.StarItem 24 | 25 | /** 26 | * description : MainPage中ModalBottomSheetLayout中的todoList界面 27 | * author : Watermelon02 28 | * email : 1446157077@qq.com 29 | * date : 2022/4/27 22:59 30 | */ 31 | @OptIn(ExperimentalMaterialApi::class) 32 | @Composable 33 | fun StarListPage( 34 | sheetState: ModalBottomSheetState, 35 | selectStarListener: (TodoStar) -> Unit, 36 | onAddStar: () -> Unit, 37 | viewModel: StarListPageViewModel = hiltViewModel(), 38 | refreshStarList: MutableState 39 | ) { 40 | val viewPageStates = viewModel.starListPageStates 41 | Column( 42 | modifier = Modifier 43 | .fillMaxHeight(0.7f) 44 | .background(Color(0xFFF8F8F8)) 45 | ) { 46 | //存储最开始的starList,在AnimatedVisibility处作判断是否播放删除动画 47 | var lastStarList = remember { viewPageStates.starList.value } 48 | //当refreshStarList=0时,lastTodoList中为默认空List,在首次访问数据库后,才获取到真正的全部stars 49 | if (refreshStarList.value == 1) lastStarList = viewPageStates.starList.value 50 | //侧滑星球栏 51 | LazyRow(modifier = Modifier 52 | .fillMaxHeight(0.35f) 53 | .fillMaxWidth(), content = { 54 | for (star in lastStarList) { 55 | //已有星球列表 56 | item { 57 | AnimatedVisibility( 58 | visible = viewPageStates.starList.value.contains(star), 59 | exit = fadeOut( 60 | targetAlpha = 0f, 61 | animationSpec = tween(durationMillis = 600) 62 | ) 63 | ) { 64 | Box(modifier = Modifier.wrapContentSize()) {//将删除按钮和StarItem重叠 65 | DeleteButton(color = Color(star.color), onClick = { 66 | viewModel.dispatch( 67 | StarListPageViewModel.StarListPageAction.DeleteStar(star) 68 | ) 69 | }) 70 | StarItem( 71 | todoStar = star, 72 | clickable = { 73 | viewModel.dispatch( 74 | StarListPageViewModel.StarListPageAction.SelectStar(it) 75 | ) 76 | }) 77 | } 78 | } 79 | } 80 | } 81 | //新增StarItem,点击进入NewStarPage 82 | item { 83 | NewStarItem { onAddStar() } 84 | } 85 | }) 86 | //selectStar的信息 87 | SelectStarInformation(viewPageStates.selectStar, selectStar = selectStarListener) 88 | //selectStar的备注,开始隐藏在下面,当modalBottomSheet展开 89 | AnimatedVisibility(visible = sheetState.currentValue == ModalBottomSheetValue.Expanded) { 90 | Card( 91 | shape = RoundedCornerShape(20.dp), 92 | modifier = Modifier 93 | .fillMaxHeight(0.7f) 94 | .fillMaxWidth() 95 | .padding(start = 10.dp, end = 10.dp, bottom = 20.dp), 96 | backgroundColor = Color.White 97 | ) { 98 | Text( 99 | text = viewPageStates.selectStar.description, 100 | fontFamily = FontFamily( 101 | Font(R.font.store_my_stamp_number) 102 | ), modifier = Modifier 103 | .alpha(0.5f) 104 | .padding(20.dp) 105 | ) 106 | } 107 | } 108 | } 109 | //每次打开先进行一次刷新 110 | LaunchedEffect(null) { refreshStarList.value++ } 111 | //当refreshStarList变化时,刷新 112 | LaunchedEffect(key1 = refreshStarList.value, block = { 113 | viewModel.dispatch(StarListPageViewModel.StarListPageAction.RefreshStarList) 114 | }) 115 | } 116 | 117 | @Composable 118 | fun SelectStarInformation(todoStar: TodoStar, selectStar: (TodoStar) -> Unit) { 119 | Row(modifier = Modifier.wrapContentHeight().fillMaxWidth()) { 120 | Card( 121 | shape = RoundedCornerShape(20.dp), 122 | modifier = Modifier 123 | .fillMaxHeight(0.35f) 124 | .width(280.dp) 125 | .padding(10.dp), 126 | backgroundColor = Color.White 127 | ) { 128 | Column( 129 | verticalArrangement = Arrangement.SpaceAround, 130 | modifier = Modifier 131 | .alpha(0.5f) 132 | .padding(start = 20.dp) 133 | ) { 134 | Text( 135 | text = "StarName : ${todoStar.name}", 136 | fontFamily = FontFamily( 137 | Font(R.font.store_my_stamp_number) 138 | ) 139 | ) 140 | Text( 141 | text = "DateTime : ${todoStar.reminderDate}", 142 | fontFamily = FontFamily( 143 | Font(R.font.store_my_stamp_number) 144 | ) 145 | ) 146 | Text( 147 | text = "Focused : ${todoStar.focusTime} minutes", 148 | fontFamily = FontFamily( 149 | Font(R.font.store_my_stamp_number) 150 | ) 151 | ) 152 | } 153 | } 154 | Card( 155 | shape = RoundedCornerShape(20.dp), modifier = Modifier 156 | .fillMaxHeight(0.35f) 157 | .width(100.dp) 158 | .padding( top = 10.dp, bottom = 10.dp), backgroundColor = Color.White 159 | ) { 160 | //点击该IconButton选择当前Star为要Focus的Star,播放动画 161 | StarIconButton( 162 | modifier = Modifier, onSelectStar = { selectStar(todoStar) }) 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/page/page_starList/StarListPageViewModel.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_starList 2 | 3 | import androidx.compose.runtime.MutableState 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.setValue 7 | import androidx.lifecycle.ViewModel 8 | import androidx.lifecycle.viewModelScope 9 | import dagger.hilt.android.lifecycle.HiltViewModel 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.launch 12 | import kotlinx.coroutines.withContext 13 | import watermelon.focus.model.bean.TodoStar 14 | import watermelon.focus.model.database.StarDao 15 | import watermelon.focus.ui.page.page_starList.StarListPageViewModel.StarListPageAction.SelectStar 16 | import javax.inject.Inject 17 | 18 | /** 19 | * author : Watermelon02 20 | * email : 1446157077@qq.com 21 | * date : 2022/4/27 23:19 22 | */ 23 | @HiltViewModel 24 | class StarListPageViewModel @Inject constructor() : ViewModel() { 25 | var starListPageStates by mutableStateOf(StarListPageViewStates()) 26 | private set 27 | 28 | @Inject 29 | lateinit var starDao: StarDao 30 | 31 | fun dispatch(action: StarListPageAction) { 32 | when (action) { 33 | is SelectStar -> starListPageStates = 34 | starListPageStates.copy(selectStar = action.todoStar) 35 | is StarListPageAction.RefreshStarList -> refreshStarList() 36 | is StarListPageAction.DeleteStar -> { 37 | deleteStar(action.todoStar) 38 | } 39 | } 40 | } 41 | 42 | private fun deleteStar(todoStar: TodoStar) { 43 | viewModelScope.launch { 44 | withContext(Dispatchers.IO) { 45 | starDao.deleteStar(todoStar) 46 | starDao.queryAllStar().collect { 47 | it?.let { starListPageStates.starList.value = it } 48 | } 49 | } 50 | } 51 | } 52 | 53 | private fun refreshStarList() { 54 | viewModelScope.launch { 55 | starDao.queryAllStar().collect { 56 | if (it.isNotEmpty()){ 57 | starListPageStates.starList.value = it 58 | } 59 | } 60 | } 61 | } 62 | 63 | data class StarListPageViewStates( 64 | val starList: MutableState> = mutableStateOf(listOf()), 65 | val selectStar: TodoStar = TodoStar( 66 | id = 1, 67 | name = "Start", color = 0xFF5B0FA5, 68 | reminderTime = 0L, 69 | reminderDate = "Right Now !", focusTime = 0L, 70 | description = "Focus !" 71 | ) 72 | ) 73 | 74 | sealed class StarListPageAction { 75 | class SelectStar(val todoStar: TodoStar) : StarListPageAction() 76 | class DeleteStar(val todoStar: TodoStar) : StarListPageAction() 77 | object RefreshStarList : StarListPageAction() 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/Clock.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.gestures.DraggableState 6 | import androidx.compose.foundation.gestures.Orientation 7 | import androidx.compose.foundation.gestures.draggable 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.material.ExperimentalMaterialApi 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.geometry.Offset 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.drawscope.rotate 16 | import androidx.compose.ui.unit.dp 17 | import kotlinx.coroutines.CoroutineScope 18 | 19 | /** 20 | * description : TODO:类的作用 21 | * author : Watermelon02 22 | * email : 1446157077@qq.com 23 | * date : 2022/4/25 21:43 24 | */ 25 | 26 | @OptIn(ExperimentalMaterialApi::class) 27 | @Composable 28 | fun Clock( 29 | draggableDegree: MutableState, 30 | draggableState: DraggableState, 31 | dragStoppedListener: suspend CoroutineScope.(velocity: Float) -> Unit, 32 | settingMinutes: Float, 33 | isCountdown: Boolean,alphaTransition:Float 34 | ) { 35 | val rotateTransition by rememberInfiniteTransition().animateFloat( 36 | initialValue = 0f, 37 | targetValue = 360f, 38 | animationSpec = InfiniteRepeatableSpec( 39 | animation = tween(durationMillis = 60000, easing = LinearEasing), 40 | repeatMode = RepeatMode.Restart 41 | ) 42 | ) 43 | Box( 44 | modifier = Modifier 45 | .fillMaxSize() 46 | ) { 47 | Canvas(modifier = Modifier 48 | .fillMaxSize() 49 | //左右滑动设置打卡时间 50 | .draggable( 51 | state = draggableState, 52 | orientation = Orientation.Horizontal, 53 | enabled = true, 54 | onDragStopped = dragStoppedListener 55 | ), 56 | onDraw = { 57 | val height = size.height 58 | val width = size.width 59 | val radius = 0.4f * width - 15.dp.value 60 | rotate( 61 | draggableDegree.value, 62 | pivot = Offset(width / 2, height / 2) 63 | ) { 64 | //外层环 65 | drawCircle( 66 | center = Offset(x = width / 2, y = height / 2), 67 | radius = radius, 68 | color = Color(0x80000000) 69 | ) 70 | //内层圈 71 | drawCircle(radius = radius - 10.dp.value, color = Color(0xFFF8F8F8)) 72 | //旋转卫星 73 | rotate( 74 | degrees = if (isCountdown) settingMinutes * 60 else rotateTransition, 75 | pivot = Offset(x = width / 2, y = height / 2) 76 | ) { 77 | drawLine( 78 | color = Color.White, 79 | start = Offset(x = width / 2, y = height / 2 - radius + 15.dp.value), 80 | end = Offset(x = width / 2, y = height / 2 - radius), 81 | strokeWidth = 80.dp.value,alpha = alphaTransition 82 | ) 83 | //卫星 84 | drawCircle( 85 | center = Offset( 86 | x = width / 2, 87 | y = height / 2 - radius 88 | ), 89 | radius = 30.dp.value, 90 | color = Color.Gray, alpha = alphaTransition 91 | ) 92 | drawCircle( 93 | center = Offset( 94 | x = width / 2, 95 | y = height / 2 - radius 96 | ), 97 | radius = 20.dp.value, 98 | color = Color.White, alpha = alphaTransition 99 | ) 100 | } 101 | } 102 | }) 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/ColorButton.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.animateColor 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.material.IconButton 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.graphics.Brush 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | 15 | /** 16 | * description : NewStarPage中选择颜色的Button 17 | * author : Watermelon02 18 | * email : 1446157077@qq.com 19 | * date : 2022/5/1 14:38 20 | */ 21 | @Composable 22 | fun ColorButton(color: Color, onClick: () -> Unit) { 23 | //两个颜色渐变动画 24 | val colorAnimation1 by rememberInfiniteTransition().animateColor( 25 | initialValue = color.copy(alpha = 0.3f), 26 | targetValue = color.copy(alpha = 0.65f), 27 | animationSpec = InfiniteRepeatableSpec( 28 | animation = tween( 29 | durationMillis = 4750 + 500 * color.alpha.toInt(), 30 | easing = FastOutLinearInEasing, 31 | delayMillis = 2730 * color.alpha.toInt() 32 | ), 33 | repeatMode = RepeatMode.Reverse 34 | ) 35 | ) 36 | val colorAnimation2 by rememberInfiniteTransition().animateColor( 37 | initialValue = color.copy(alpha = 0.7f), 38 | targetValue = color.copy(alpha = 0.3f), 39 | animationSpec = InfiniteRepeatableSpec( 40 | animation = tween( 41 | durationMillis = 3371 + 500 * color.alpha.toInt(), 42 | easing = FastOutSlowInEasing, 43 | delayMillis = 2730 * color.alpha.toInt() 44 | ), 45 | repeatMode = RepeatMode.Reverse 46 | ) 47 | ) 48 | IconButton(onClick = { onClick() }) { 49 | Canvas(modifier = Modifier, onDraw = { 50 | //渐变色块 51 | drawCircle( 52 | brush = Brush.verticalGradient( 53 | colors = listOf( 54 | colorAnimation2, 55 | colorAnimation1 56 | ), startY = size.height / 2 - 15.dp.value, endY = size.height / 2 + 15.dp.value 57 | ), 58 | radius = 30.dp.value, 59 | center = Offset(x = size.width / 2, y = size.height / 2) 60 | ) 61 | }) 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/ColorSelector.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.Card 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | 15 | /** 16 | * description : NewStarPage中的色彩选择器 17 | * author : Watermelon02 18 | * email : 1446157077@qq.com 19 | * date : 2022/5/1 16:32 20 | */ 21 | @Composable 22 | fun ColorSelector(selectColorListener: (Long) -> Unit) { 23 | Card( 24 | modifier = Modifier 25 | .padding(start = 10.dp, top = 20.dp, end = 10.dp,bottom = 10.dp) 26 | .height(200.dp) 27 | .width(175.dp) 28 | .clickable( 29 | indication = null, 30 | interactionSource = remember { MutableInteractionSource() }) {}, 31 | shape = RoundedCornerShape(20.dp) 32 | ) { 33 | //左颜色列表 34 | Column( 35 | verticalArrangement = Arrangement.SpaceBetween, 36 | horizontalAlignment = Alignment.Start, 37 | modifier = Modifier.padding(start = 20.dp, top = 10.dp, bottom = 10.dp) 38 | ) { 39 | for (color in listOf( 40 | 0xFF5B0FA5, 41 | 0xA3F80C0C, 42 | 0xFF29B6F6, 43 | 0x9A26A69A 44 | )) { 45 | ColorButton(Color(color)) { selectColorListener(color) } 46 | } 47 | } 48 | //右颜色列表 49 | Column( 50 | verticalArrangement = Arrangement.SpaceBetween, 51 | horizontalAlignment = Alignment.End, 52 | modifier = Modifier.padding(end = 20.dp, top = 10.dp, bottom = 10.dp) 53 | ) { 54 | for (color in listOf( 55 | 0xDA0D9958, 0xEDFFA726, 0xFFFFCA28, 56 | 0x80000000 57 | )) { 58 | ColorButton(Color(color)) { selectColorListener(color) } 59 | } 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/DeleteButton.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.animateColor 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Card 11 | import androidx.compose.material.IconButton 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.geometry.Offset 16 | import androidx.compose.ui.graphics.Brush 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.unit.dp 19 | 20 | /** 21 | * description : TODO:类的作用 22 | * author : Watermelon02 23 | * email : 1446157077@qq.com 24 | * date : 2022/5/2 12:38 25 | */ 26 | @Composable 27 | fun DeleteButton(color: Color, onClick: () -> Unit) { 28 | val colorAnimation1 by rememberInfiniteTransition().animateColor( 29 | initialValue = color.copy(alpha = 0.35f), 30 | targetValue = color.copy(alpha = 0.75f), 31 | animationSpec = InfiniteRepeatableSpec( 32 | animation = tween( 33 | durationMillis = 4750 + 500 * color.alpha.toInt(), 34 | easing = FastOutLinearInEasing, 35 | delayMillis = 2730 * color.alpha.toInt() 36 | ), 37 | repeatMode = RepeatMode.Reverse 38 | ) 39 | ) 40 | val colorAnimation2 by rememberInfiniteTransition().animateColor( 41 | initialValue = color.copy(alpha = 0.9f), 42 | targetValue = color.copy(alpha = 0.3f), 43 | animationSpec = InfiniteRepeatableSpec( 44 | animation = tween( 45 | durationMillis = 3371 + 500 * color.alpha.toInt(), 46 | easing = FastOutSlowInEasing, 47 | delayMillis = 2730 * color.alpha.toInt() 48 | ), 49 | repeatMode = RepeatMode.Reverse 50 | ) 51 | ) 52 | Card( 53 | modifier = Modifier 54 | .padding(start = 10.dp, top = 20.dp, end = 10.dp) 55 | .height(200.dp) 56 | .width(175.dp), 57 | shape = RoundedCornerShape(20.dp) 58 | ) { 59 | IconButton(onClick = onClick) { 60 | Canvas(modifier = Modifier 61 | .padding(start = 10.dp, top = 40.dp, end = 10.dp) 62 | .height(170.dp) 63 | .width(175.dp), onDraw = { 64 | //渐变色块 65 | drawCircle( 66 | brush = Brush.linearGradient( 67 | colors = listOf( 68 | colorAnimation2, 69 | colorAnimation1 70 | ), 71 | start = Offset(0f,0f),end = Offset(400.dp.value,400.dp.value), 72 | ), 73 | radius = 400.dp.value, 74 | center = Offset(x = size.width / 2, y = size.height / 2) 75 | ) 76 | }) 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/MainPageBottomBar.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.page.page_main.widget 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | import androidx.compose.ui.res.vectorResource 15 | import androidx.compose.ui.text.font.Font 16 | import androidx.compose.ui.text.font.FontFamily 17 | import androidx.compose.ui.unit.dp 18 | import watermelon.focus.R 19 | 20 | /** 21 | * description : MainPage底部的Bar,这一段是参考的 22 | * author : Watermelon02 23 | * email : 1446157077@qq.com 24 | * date : 2022/4/28 22:17 25 | */ 26 | @Composable 27 | fun MainPageBottomBar() { 28 | BottomNavigation( 29 | backgroundColor = Color.White, modifier = Modifier 30 | .fillMaxHeight(0.1f) 31 | ) { 32 | var selectedItem by remember { mutableStateOf(0) } 33 | 34 | for (index in 0..2) { 35 | Column( 36 | modifier = Modifier 37 | .fillMaxHeight() 38 | .weight(1f) 39 | .clickable( 40 | onClick = { 41 | selectedItem = index 42 | }, 43 | indication = null, 44 | interactionSource = remember { MutableInteractionSource() } 45 | ), 46 | verticalArrangement = Arrangement.Center, 47 | horizontalAlignment = Alignment.CenterHorizontally 48 | ) { 49 | NavigationIcon(index, selectedItem) 50 | //Icon和选中小点之间的间距 51 | Spacer(Modifier.padding(top = 4.dp)) 52 | AnimatedVisibility(visible = index == selectedItem) { 53 | when (index) { 54 | 0 -> NavigationText(text = "star") 55 | 1 -> NavigationText(text = "universe") 56 | else -> NavigationText(text = "mine") 57 | } 58 | } 59 | } 60 | } 61 | 62 | } 63 | } 64 | 65 | 66 | @Composable 67 | fun NavigationIcon( 68 | index: Int, 69 | selectedItem: Int 70 | ) { 71 | val vectorStar = ImageVector.vectorResource(id = R.drawable.bottom_star) 72 | val vectorUniverse = ImageVector.vectorResource(id = R.drawable.bottom_universe) 73 | val vectorUser = ImageVector.vectorResource(id = R.drawable.bottom_user) 74 | val alpha = if (selectedItem != index) 0.2f else 0.5f 75 | 76 | when (index) { 77 | 0 -> Icon(vectorStar, contentDescription = null, modifier = Modifier.alpha(alpha)) 78 | 1 -> Icon(vectorUniverse, contentDescription = null, modifier = Modifier.alpha(alpha)) 79 | else -> Icon(vectorUser, contentDescription = null, modifier = Modifier.alpha(alpha)) 80 | } 81 | } 82 | 83 | @Composable 84 | fun NavigationText(text:String){ 85 | Text( 86 | text = text, 87 | style = MaterialTheme.typography.body2, 88 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)), 89 | modifier = Modifier.alpha(0.5f) 90 | ) 91 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/MyIconButton.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.semantics.Role 12 | import androidx.compose.ui.unit.dp 13 | 14 | /** 15 | * description : 无点击波纹的IconButton 16 | * author : Watermelon02 17 | * email : 1446157077@qq.com 18 | * date : 2022/4/30 21:36 19 | */ 20 | @Composable 21 | fun MyIconButton( 22 | onClick: () -> Unit, 23 | modifier: Modifier = Modifier, 24 | enabled: Boolean = true, 25 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 26 | content: @Composable () -> Unit 27 | ) { 28 | // 这也是源码的一部分 29 | val IconButtonSizeModifier = Modifier.size(48.dp) 30 | Box( 31 | modifier = modifier 32 | .clickable( 33 | onClick = onClick, 34 | enabled = enabled, 35 | role = Role.Button, 36 | interactionSource = interactionSource, 37 | indication = null 38 | ) 39 | .then(IconButtonSizeModifier), 40 | contentAlignment = Alignment.Center 41 | ) { content() } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/NewStar.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.wrapContentSize 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.drawscope.translate 11 | import androidx.compose.ui.graphics.vector.ImageVector 12 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 13 | import androidx.compose.ui.res.vectorResource 14 | import androidx.compose.ui.unit.dp 15 | import kotlin.random.Random 16 | 17 | /** 18 | * description : NewStarItem中的Star 19 | * author : Watermelon02 20 | * email : 1446157077@qq.com 21 | * date : 2022/5/1 11:14 22 | */ 23 | @Composable 24 | fun NewStar() { 25 | val vector = ImageVector.vectorResource(id = watermelon.focus.R.drawable.ic_star) 26 | val painter = rememberVectorPainter(image = vector) 27 | val verticalShakeTransition by rememberInfiniteTransition().animateFloat( 28 | initialValue = -40.dp.value * Random(System.currentTimeMillis()).nextFloat(), 29 | targetValue = 20.dp.value * Random(System.currentTimeMillis() - 10).nextFloat(), 30 | animationSpec = InfiniteRepeatableSpec( 31 | animation = tween(durationMillis = 4000, easing = FastOutSlowInEasing), 32 | repeatMode = RepeatMode.Reverse 33 | ) 34 | ) 35 | val alphaTransition by rememberInfiniteTransition().animateFloat( 36 | initialValue = 0.3f * Random(System.currentTimeMillis()).nextFloat(), 37 | targetValue = 0.2f * Random(System.currentTimeMillis() - 10).nextFloat(), 38 | animationSpec = InfiniteRepeatableSpec( 39 | animation = tween(durationMillis = 3947, easing = FastOutSlowInEasing), 40 | repeatMode = RepeatMode.Reverse 41 | ) 42 | ) 43 | Box( 44 | modifier = Modifier 45 | .wrapContentSize() 46 | ) { 47 | Canvas( 48 | modifier = Modifier, 49 | onDraw = { 50 | val height = size.height 51 | val width = size.width 52 | //外层灰环 53 | translate( 54 | top = verticalShakeTransition + height / 2 - 280.dp.value, 55 | left = width / 2 - 280.dp.value 56 | ) { 57 | with(painter) { 58 | draw(painter.intrinsicSize, alpha = alphaTransition) 59 | } 60 | } 61 | }) 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/NewStarItem.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.width 8 | import androidx.compose.foundation.layout.wrapContentHeight 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.Card 11 | import androidx.compose.material.MaterialTheme 12 | import androidx.compose.material.Surface 13 | import androidx.compose.material.Text 14 | import androidx.compose.runtime.Composable 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.alpha 18 | import androidx.compose.ui.graphics.Color 19 | import androidx.compose.ui.text.font.Font 20 | import androidx.compose.ui.text.font.FontFamily 21 | import androidx.compose.ui.text.style.TextAlign 22 | import androidx.compose.ui.unit.dp 23 | import watermelon.focus.R 24 | 25 | /** 26 | * description : 创建星球 27 | * author : Watermelon02 28 | * email : 1446157077@qq.com 29 | * date : 2022/5/1 10:48 30 | */ 31 | @Composable 32 | fun NewStarItem(clickable: () -> Unit) { 33 | Card( 34 | modifier = Modifier 35 | .padding(start = 10.dp, top = 20.dp, end = 10.dp) 36 | .height(200.dp) 37 | .width(175.dp) 38 | .clickable( 39 | indication = null, 40 | interactionSource = remember { MutableInteractionSource() }) { clickable() }, 41 | shape = RoundedCornerShape(20.dp) 42 | ) { 43 | NewStar() 44 | //StarName 45 | Surface( 46 | shape = RoundedCornerShape(20.dp), 47 | modifier = Modifier 48 | .padding(top = 150.dp, start = 70.dp, end = 70.dp) 49 | .width(20.dp) 50 | .wrapContentHeight(), color = Color(0xFFF8F8F8) 51 | ) { 52 | Text( 53 | text = "+", 54 | style = MaterialTheme.typography.h6, 55 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)), 56 | textAlign = TextAlign.Center, modifier = Modifier.alpha(0.5f) 57 | ) 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/Signature.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.text.font.Font 13 | import androidx.compose.ui.text.font.FontFamily 14 | import watermelon.focus.R 15 | 16 | /** 17 | * description : TODO:类的作用 18 | * author : Watermelon02 19 | * email : 1446157077@qq.com 20 | * date : 2022/4/30 15:15 21 | */ 22 | @Composable 23 | fun BoxScope.Signature(text:String){ 24 | Box( 25 | modifier = Modifier 26 | .fillMaxHeight(0.80f) 27 | .align(Alignment.BottomCenter) 28 | .alpha(0.5f) 29 | ) { 30 | Text( 31 | text = "$text", 32 | style = MaterialTheme.typography.h4, 33 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)) 34 | ) 35 | } 36 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/Star.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.core.* 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.wrapContentSize 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.BlendMode 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.ColorFilter 13 | import androidx.compose.ui.graphics.drawscope.translate 14 | import androidx.compose.ui.graphics.vector.ImageVector 15 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 16 | import androidx.compose.ui.res.vectorResource 17 | import androidx.compose.ui.unit.dp 18 | import kotlin.random.Random 19 | 20 | /** 21 | * author : Watermelon02 22 | * email : 1446157077@qq.com 23 | * date : 2022/4/30 15:10 24 | * @param color 通过设置ColorFilter的blender为SRCin模式,来展示不同颜色的星球 25 | */ 26 | @Composable 27 | fun Star(color: Color = Color(0xFF000000)) { 28 | val vector = ImageVector.vectorResource(id = watermelon.focus.R.drawable.ic_star) 29 | val painter = rememberVectorPainter(image = vector) 30 | val verticalShakeTransition by rememberInfiniteTransition().animateFloat( 31 | initialValue = -40.dp.value* Random(System.currentTimeMillis()).nextFloat(), 32 | targetValue = 20.dp.value* Random(System.currentTimeMillis()-10).nextFloat(), 33 | animationSpec = InfiniteRepeatableSpec( 34 | animation = tween(durationMillis = 4000, easing = FastOutSlowInEasing), 35 | repeatMode = RepeatMode.Reverse 36 | ) 37 | ) 38 | val alphaTransition by rememberInfiniteTransition().animateFloat( 39 | initialValue = 0.8f * Random(System.currentTimeMillis()).nextFloat(), 40 | targetValue = 0.5f * Random(System.currentTimeMillis() - 10).nextFloat(), 41 | animationSpec = InfiniteRepeatableSpec( 42 | animation = tween(durationMillis = 7231, easing = FastOutSlowInEasing), 43 | repeatMode = RepeatMode.Reverse 44 | ) 45 | ) 46 | Box( 47 | modifier = Modifier 48 | .wrapContentSize() 49 | ) { 50 | Canvas( 51 | modifier = Modifier, 52 | onDraw = { 53 | val height = size.height 54 | val width = size.width 55 | //外层灰环 56 | translate( 57 | top = verticalShakeTransition + height / 2 - 280.dp.value, 58 | left = width / 2 - 280.dp.value 59 | ) { 60 | with(painter) { 61 | draw(painter.intrinsicSize, alpha = alphaTransition,colorFilter = ColorFilter.tint(color = color,blendMode = BlendMode.SrcIn)) 62 | } 63 | } 64 | }) 65 | } 66 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/StarIconButton.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateDpAsState 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.layout.offset 8 | import androidx.compose.material.Icon 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.mutableStateOf 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.alpha 14 | import androidx.compose.ui.draw.scale 15 | import androidx.compose.ui.graphics.vector.ImageVector 16 | import androidx.compose.ui.res.vectorResource 17 | import androidx.compose.ui.unit.dp 18 | import watermelon.focus.R 19 | 20 | 21 | /** 22 | * description : 带有动画的IconButton,在StarListPage和NewStarPage中使用 23 | * author : Watermelon02 24 | * email : 1446157077@qq.com 25 | * date : 2022/5/1 16:40 26 | */ 27 | @Composable 28 | fun StarIconButton(onSelectStar: () -> Unit,modifier: Modifier){ 29 | val isClicked = remember { mutableStateOf(false) } 30 | val offsetAnimation = animateDpAsState( 31 | targetValue = if (isClicked.value) 10.dp else 0.dp, 32 | animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing), 33 | finishedListener = { 34 | isClicked.value = false 35 | }) 36 | val scaleAnimation = animateFloatAsState( 37 | targetValue = if (isClicked.value) 1.3f else 1f, 38 | animationSpec = tween(durationMillis = 1000, easing = FastOutSlowInEasing), 39 | finishedListener = { 40 | isClicked.value = false 41 | }) 42 | MyIconButton( 43 | onClick = { 44 | onSelectStar() 45 | isClicked.value = true 46 | }, 47 | modifier = modifier.alpha(0.5f) 48 | ) { 49 | Icon( 50 | imageVector = ImageVector.vectorResource(id = R.drawable.button_choose_star), 51 | contentDescription = "", 52 | modifier = Modifier.offset(x = -offsetAnimation.value, y = offsetAnimation.value) 53 | ) 54 | Icon( 55 | imageVector = ImageVector.vectorResource(id = R.drawable.button_choose_line), 56 | contentDescription = "", 57 | modifier = Modifier 58 | .offset( 59 | x = -offsetAnimation.value / 2, 60 | y = offsetAnimation.value / 2 61 | ) 62 | .scale(scaleAnimation.value) 63 | ) 64 | } 65 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/StarItem.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.gestures.Orientation 5 | import androidx.compose.foundation.interaction.MutableInteractionSource 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.* 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.text.font.Font 14 | import androidx.compose.ui.text.font.FontFamily 15 | import androidx.compose.ui.text.style.TextAlign 16 | import androidx.compose.ui.unit.dp 17 | import watermelon.focus.R 18 | import watermelon.focus.model.bean.TodoStar 19 | 20 | /** 21 | * description : TODO:类的作用 22 | * author : Watermelon02 23 | * email : 1446157077@qq.com 24 | * date : 2022/4/28 14:15 25 | */ 26 | @OptIn(ExperimentalMaterialApi::class) 27 | @Composable 28 | fun StarItem(todoStar: TodoStar, clickable: (TodoStar) -> Unit) { 29 | 30 | val swipeState = rememberSwipeableState(initialValue = StarItemSwipeStatus.Close) 31 | Card( 32 | modifier = Modifier 33 | .padding(start = 10.dp, top = 20.dp, end = 10.dp) 34 | .height(200.dp) 35 | .width(175.dp) 36 | .offset( 37 | y = swipeState.offset.value.dp 38 | ) 39 | .clickable( 40 | indication = null, 41 | interactionSource = remember { MutableInteractionSource() }) { clickable(todoStar) } 42 | .swipeable( 43 | state = swipeState, anchors = mapOf( 44 | 0f to StarItemSwipeStatus.Close, 45 | -40.dp.value to StarItemSwipeStatus.Open 46 | ), thresholds = { from, to -> 47 | if (from == StarItemSwipeStatus.Close) { 48 | FractionalThreshold(0.3f) 49 | } else FractionalThreshold(0.1f) 50 | }, orientation = Orientation.Vertical 51 | ), 52 | shape = RoundedCornerShape(20.dp) 53 | ) { 54 | Star(Color(todoStar.color)) 55 | //StarName 56 | Surface( 57 | shape = RoundedCornerShape(20.dp), 58 | modifier = Modifier 59 | .padding(top = 150.dp, start = 50.dp, end = 50.dp) 60 | .width(20.dp) 61 | .wrapContentHeight(), color = Color(0xFFF8F8F8) 62 | ) { 63 | Text( 64 | text = todoStar.name, 65 | style = MaterialTheme.typography.body2, 66 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)), 67 | textAlign = TextAlign.Center, modifier = Modifier.alpha(0.5f) 68 | ) 69 | } 70 | } 71 | } 72 | 73 | enum class StarItemSwipeStatus { 74 | Close, Open 75 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/StarName.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.alpha 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.text.font.Font 16 | import androidx.compose.ui.text.font.FontFamily 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.unit.dp 19 | import watermelon.focus.R 20 | 21 | /** 22 | * description : TODO:类的作用 23 | * author : Watermelon02 24 | * email : 1446157077@qq.com 25 | * date : 2022/4/30 18:29 26 | */ 27 | @Composable 28 | fun BoxScope.StarName(name:String) { 29 | Surface( 30 | color = Color(0xFFF8F8F8), 31 | shape = RoundedCornerShape(20.dp), 32 | modifier = Modifier 33 | .align(Alignment.BottomCenter) 34 | .width(100.dp) 35 | .padding(bottom = 200.dp) 36 | ) { 37 | Text( 38 | text = name, 39 | style = MaterialTheme.typography.h6, 40 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)), 41 | textAlign = TextAlign.Center,modifier = Modifier.alpha(0.5f) 42 | ) 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/TimeNode.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.graphicsLayer 10 | import androidx.compose.ui.text.font.Font 11 | import androidx.compose.ui.text.font.FontFamily 12 | import com.google.accompanist.pager.ExperimentalPagerApi 13 | import com.google.accompanist.pager.PagerScope 14 | import com.google.accompanist.pager.calculateCurrentOffsetForPage 15 | import watermelon.focus.R 16 | import kotlin.math.absoluteValue 17 | 18 | /** 19 | * description : TODO:类的作用 20 | * author : Watermelon02 21 | * email : 1446157077@qq.com 22 | * date : 2022/5/2 10:03 23 | */ 24 | @OptIn(ExperimentalPagerApi::class) 25 | @Composable 26 | fun PagerScope.TimeNode(time: Int) { 27 | Box(modifier = Modifier 28 | .graphicsLayer { 29 | //滑动时的缩放和渐变动画 30 | val pageOffset = calculateCurrentOffsetForPage(currentPage).absoluteValue 31 | leap( 32 | start = 1f, 33 | stop = 0.2f, 34 | fraction = 1f - pageOffset % 1 35 | ).also { 36 | scaleX = it 37 | scaleY = it 38 | } 39 | alpha = leap( 40 | start = 1f, 41 | stop = 0.1f, 42 | fraction = 1f - pageOffset % 1 43 | ) 44 | }) { 45 | Text( 46 | text = time.toString(), 47 | style = MaterialTheme.typography.h4, 48 | color = Color(0x40000000), 49 | fontFamily = FontFamily( 50 | Font(R.font.store_my_stamp_number) 51 | ) 52 | ) 53 | } 54 | } 55 | 56 | //差值器,前半段和后半段执行相反动画,动画前后状态一致 57 | fun leap(start: Float, stop: Float, fraction: Float): Float { 58 | return if (fraction < 0.5f) { 59 | (1 - fraction) * start + fraction * stop 60 | } else { 61 | (1 - fraction) * stop + fraction * start 62 | } 63 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/TimeSelector.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material.Card 6 | import androidx.compose.material.Surface 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.dp 13 | import com.google.accompanist.pager.ExperimentalPagerApi 14 | import com.google.accompanist.pager.VerticalPager 15 | import com.google.accompanist.pager.rememberPagerState 16 | import watermelon.focus.ui.page.page_newStar.NewStarViewModel 17 | import java.util.* 18 | 19 | /** 20 | * description : NewStarPage中的时间选择器 21 | * author : Watermelon02 22 | * email : 1446157077@qq.com 23 | * date : 2022/5/2 10:59 24 | */ 25 | 26 | @OptIn(ExperimentalPagerApi::class) 27 | @Composable 28 | fun TimeSelector(viewModel: NewStarViewModel, onCreateStar: (String) -> Unit) { 29 | Surface( 30 | modifier = Modifier 31 | .fillMaxHeight(0.35f) 32 | .fillMaxWidth() 33 | .padding(10.dp), shape = RoundedCornerShape(20.dp), 34 | color = Color(0xFFF8F8F8) 35 | ) { 36 | val calendar = remember { Calendar.getInstance() } 37 | val years = 38 | remember { (calendar[Calendar.YEAR]..(calendar[Calendar.YEAR] + 3)).toList() } 39 | val months = remember { (1..12).toList() } 40 | val days = viewModel.days 41 | val yearState = rememberPagerState() 42 | val monthState = rememberPagerState(initialPage = calendar[Calendar.MONTH]) 43 | //因为calendar中的月份,年份为序数,日期为基数,所以此处-1 44 | val dayState = rememberPagerState(initialPage = calendar[Calendar.DATE] - 1) 45 | Row( 46 | horizontalArrangement = Arrangement.SpaceBetween, 47 | verticalAlignment = Alignment.CenterVertically 48 | ) { 49 | //year 50 | VerticalPager(count = years.size, state = yearState) { page -> 51 | Card( 52 | shape = RoundedCornerShape(20.dp), modifier = Modifier 53 | .fillMaxHeight() 54 | .width(90.dp) 55 | .padding(bottom = 15.dp), backgroundColor = Color.White 56 | ) { 57 | Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { 58 | TimeNode(time = years[page]) 59 | } 60 | } 61 | } 62 | //month 63 | VerticalPager(count = months.size, state = monthState) { page -> 64 | Card( 65 | shape = RoundedCornerShape(20.dp), 66 | modifier = Modifier 67 | .fillMaxHeight() 68 | .width(90.dp) 69 | .padding(bottom = 15.dp), backgroundColor = Color.White 70 | ) { 71 | Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { 72 | TimeNode(time = months[page]) 73 | } 74 | } 75 | } 76 | //day 77 | VerticalPager(count = days.size, state = dayState) { page -> 78 | Card( 79 | shape = RoundedCornerShape(20.dp), modifier = Modifier 80 | .fillMaxHeight() 81 | .width(90.dp) 82 | .padding(bottom = 15.dp), backgroundColor = Color.White 83 | ) { 84 | Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { 85 | TimeNode(time = days[page]) 86 | } 87 | } 88 | } 89 | //确认添加按钮 90 | Card( 91 | shape = RoundedCornerShape(20.dp), modifier = Modifier 92 | .fillMaxHeight() 93 | .width(85.dp) 94 | .padding(bottom = 15.dp), backgroundColor = Color.White 95 | ) { 96 | StarIconButton( 97 | modifier = Modifier.align(Alignment.CenterVertically), 98 | onSelectStar = { 99 | onCreateStar("${yearState.currentPage + calendar[Calendar.YEAR]}-${monthState.currentPage + 1}-${dayState.currentPage+1}") 100 | }) 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /app/src/main/java/watermelon/focus/ui/widget/TimeText.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus.ui.widget 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.fillMaxHeight 6 | import androidx.compose.material.MaterialTheme 7 | import androidx.compose.material.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.alpha 12 | import androidx.compose.ui.text.font.Font 13 | import androidx.compose.ui.text.font.FontFamily 14 | import watermelon.focus.R 15 | 16 | /** 17 | * description : TODO:类的作用 18 | * author : Watermelon02 19 | * email : 1446157077@qq.com 20 | * date : 2022/4/26 21:15 21 | */ 22 | @Composable 23 | fun BoxScope.TimeText(draggableDegree:Float,settingMinutes:Float,isCountdown:Boolean) { 24 | Box( 25 | modifier = Modifier 26 | .fillMaxHeight(0.95f) 27 | .align(Alignment.BottomCenter) 28 | .alpha(0.5f) 29 | ) { 30 | val totalMinutes = if (isCountdown) settingMinutes else draggableDegree/ 360 * TOTAL_MINUTES 31 | val hourString = totalMinutes / 60 32 | val minuteString = totalMinutes % 60 33 | Text( 34 | text = "${hourString.toInt()} : ${minuteString.toInt()}", 35 | style = MaterialTheme.typography.h3, 36 | fontFamily = FontFamily(Font(R.font.store_my_stamp_number)) 37 | ) 38 | } 39 | } 40 | 41 | const val TOTAL_MINUTES = 720//12*60 min -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_universe.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/bottom_user.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_choose_line.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/button_choose_star.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_star.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/drawable/logo.png -------------------------------------------------------------------------------- /app/src/main/res/font/store_my_stamp_number.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/font/store_my_stamp_number.ttf -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Focus 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/test/java/watermelon/focus/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package watermelon.focus 2 | 3 | import org.junit.Test 4 | 5 | import org.junit.Assert.* 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * See [testing documentation](http://d.android.com/tools/testing). 11 | */ 12 | class ExampleUnitTest { 13 | @Test 14 | fun addition_isCorrect() { 15 | assertEquals(4, 2 + 2) 16 | } 17 | } -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | buildscript{ 3 | ext { 4 | config = "${rootDir}/config.gradle" 5 | api_config = "${rootDir}/api_config.gradle" 6 | compose_version = '1.1.1' 7 | hilt_version = '2.38.1' 8 | } 9 | dependencies{ 10 | classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" 11 | } 12 | } 13 | }// Top-level build file where you can add configuration options common to all sub-projects/modules. 14 | plugins { 15 | id 'com.android.application' version '7.1.2' apply false 16 | id 'com.android.library' version '7.1.2' apply false 17 | id 'org.jetbrains.kotlin.android' version '1.6.10' apply false 18 | } 19 | 20 | task clean(type: Delete) { 21 | delete rootProject.buildDir 22 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | android.enableJetifier=true 19 | 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | # Enables namespacing of each library's R class so that its R class includes only the 23 | # resources declared in the library itself and none from the library's dependencies, 24 | # thereby reducing the size of the R class for that library 25 | android.nonTransitiveRClass=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue May 03 12:09:20 CST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /picture/1651511878919.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651511878919.gif -------------------------------------------------------------------------------- /picture/1651562261764.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651562261764.gif -------------------------------------------------------------------------------- /picture/1651562374424.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651562374424.gif -------------------------------------------------------------------------------- /picture/1651562434323.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651562434323.gif -------------------------------------------------------------------------------- /picture/1651573171050.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651573171050.gif -------------------------------------------------------------------------------- /picture/1651573977400.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/1651573977400.gif -------------------------------------------------------------------------------- /picture/84ED106B-EDEA-495F-9652-0BF51BD4CD07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Watermelon02/ComposeFocus/490f83d9bac58b0390c9cec75543f3fc7a73c40c/picture/84ED106B-EDEA-495F-9652-0BF51BD4CD07.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "ComposeFocus" 16 | include ':app' 17 | --------------------------------------------------------------------------------