├── .gitignore
├── .idea
├── .gitignore
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── deploymentTargetDropDown.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── jarRepositories.xml
├── kotlinc.xml
├── misc.xml
└── vcs.xml
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── release
│ └── output-metadata.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── funny
│ │ └── compose
│ │ └── study
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── funny
│ │ │ └── compose
│ │ │ └── study
│ │ │ ├── App.kt
│ │ │ ├── MainActivity.kt
│ │ │ └── ui
│ │ │ ├── anim
│ │ │ ├── LazyColumnAnim.kt
│ │ │ └── NumberChangeAnimation.kt
│ │ │ ├── event_test
│ │ │ └── ClickEventTest.kt
│ │ │ ├── feature
│ │ │ ├── BasicMarquee1_4.kt
│ │ │ ├── FlowLayout1_4.kt
│ │ │ ├── Pager1_4.kt
│ │ │ └── README.md
│ │ │ ├── game
│ │ │ ├── Beans.kt
│ │ │ ├── Modifers.kt
│ │ │ ├── SnakeAssets.kt
│ │ │ ├── SnakeGame.kt
│ │ │ ├── SnakeGameViewModel.kt
│ │ │ ├── SnakeState.kt
│ │ │ └── ThemeConfig.kt
│ │ │ ├── like_keep
│ │ │ ├── FakeKeep.kt
│ │ │ └── FunnyCanvasUtils.java
│ │ │ ├── markdowntest
│ │ │ └── MarkdownTest.kt
│ │ │ ├── nav
│ │ │ └── NavigationTest.kt
│ │ │ ├── others
│ │ │ ├── RememberTest.kt
│ │ │ └── SimpleNavigationWithSaveableStateSample.kt
│ │ │ ├── pager
│ │ │ └── PagerTest.kt
│ │ │ ├── physics_layout
│ │ │ ├── Bound.kt
│ │ │ ├── LayoutRecomposeTest.kt
│ │ │ ├── Physics.kt
│ │ │ ├── PhysicsConfig.kt
│ │ │ ├── PhysicsLayout.kt
│ │ │ └── PhysicsParentData.kt
│ │ │ ├── post_layout
│ │ │ ├── CustomLayoutTest.kt
│ │ │ ├── ModifierTest.kt
│ │ │ ├── ParentDataTest.kt
│ │ │ ├── SwipeCrossFadeLayout.kt
│ │ │ ├── SwipeableDemo.kt
│ │ │ ├── VerticalLayout.kt
│ │ │ ├── VerticalWeightedTest.kt
│ │ │ └── WaterfallFlowLayout.kt
│ │ │ ├── post_lazycolumn
│ │ │ ├── FunnyLazyColumn.kt
│ │ │ ├── FunnyLazyList.kt
│ │ │ └── FunnyLazyListState.kt
│ │ │ ├── post_lazygrid
│ │ │ └── LazyGridTest.kt
│ │ │ ├── posta
│ │ │ ├── Beans.kt
│ │ │ ├── FScreen.kt
│ │ │ ├── PopularBooksDemo.kt
│ │ │ ├── ShuffleSample.kt
│ │ │ └── SwipeToDismissSample.kt
│ │ │ ├── refresh
│ │ │ └── SwipeToRefreshTest.kt
│ │ │ ├── theme
│ │ │ ├── Color.kt
│ │ │ ├── Shape.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ │ │ ├── videoa
│ │ │ ├── AScreen.kt
│ │ │ └── AViewModel.kt
│ │ │ ├── videob
│ │ │ └── BScreen.kt
│ │ │ ├── videoc
│ │ │ ├── BoxState.kt
│ │ │ └── CScreen.kt
│ │ │ ├── videod
│ │ │ └── ScreenD.kt
│ │ │ └── videoe
│ │ │ └── ScreenE.kt
│ └── res
│ │ ├── drawable-v24
│ │ └── ic_launcher_foreground.xml
│ │ ├── drawable
│ │ ├── bg_2.png
│ │ ├── bg_avator.jpg
│ │ ├── ic_bin.png
│ │ ├── ic_favorites.png
│ │ ├── ic_launcher_background.xml
│ │ ├── ic_run.png
│ │ └── part_tab.png
│ │ ├── 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-land
│ │ └── dimens.xml
│ │ ├── values-w1240dp
│ │ └── dimens.xml
│ │ ├── values-w600dp
│ │ └── dimens.xml
│ │ └── values
│ │ ├── colors.xml
│ │ ├── dimens.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ └── test
│ └── java
│ └── com
│ └── funny
│ └── compose
│ └── study
│ └── ExampleUnitTest.kt
├── build.gradle
├── demo.apk
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── 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/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | xmlns:android
17 |
18 | ^$
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | xmlns:.*
28 |
29 | ^$
30 |
31 |
32 | BY_NAME
33 |
34 |
35 |
36 |
37 |
38 |
39 | .*:id
40 |
41 | http://schemas.android.com/apk/res/android
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | .*:name
51 |
52 | http://schemas.android.com/apk/res/android
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 | name
62 |
63 | ^$
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 | style
73 |
74 | ^$
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | .*
84 |
85 | ^$
86 |
87 |
88 | BY_NAME
89 |
90 |
91 |
92 |
93 |
94 |
95 | .*
96 |
97 | http://schemas.android.com/apk/res/android
98 |
99 |
100 | ANDROID_ATTRIBUTE_ORDER
101 |
102 |
103 |
104 |
105 |
106 |
107 | .*
108 |
109 | .*
110 |
111 |
112 | BY_NAME
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.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 |
--------------------------------------------------------------------------------
/.idea/jarRepositories.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 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## 说明
2 | ---
3 |
4 | 本项目包含了我 Jetpack Compose 相关 [专栏](https://juejin.cn/column/7024350372680433672) 中出现的代码,您可以点击 [demo.apk](demo.apk) 下载 demo 查看对应演示。
5 |
6 |

7 |
8 | 受限于本人水平,部分地方可能有错误,欢迎不吝赐教
9 |
10 | 如有任何问题,可以开 `issue` 或在对应文章的评论区回复,我会尽快回复
11 |
12 | 为方便起见,以下用 `/ui` 指代路径`"\app\src\main\java\com\funny\compose\study\ui\"`
13 |
14 | ## 视频
15 | ---
16 |
17 | 所有代码可通过 [MainActivity](app/src/main/java/com/funny/compose/study/MainActivity.kt) 跳转到对应路径
18 |
19 | 项目涉及到的文章:
20 |
21 | - [深入Jetpack Compose——布局原理与自定义布局(一)](https://juejin.cn/post/7063451846861406245) : 代码位于 /ui/post_layout
22 | - [深入Jetpack Compose——布局原理与自定义布局(二)](https://juejin.cn/post/7063816490021027871) : 代码位于 /ui/post_layout
23 | - [深入Jetpack Compose——布局原理与自定义布局(三)](https://juejin.cn/post/7068164264363556872) : 代码位于 /ui/post_layout
24 | - [深入Jetpack Compose——布局原理与自定义布局(四)](https://juejin.cn/post/7073307559792214024) : 代码位于 /ui/post_layout
25 | - [Jetpack Compose LazyColumn列表项动画/滑动删除](https://juejin.cn/post/7042873050412351501) : 代码位于 /ui/posta
26 | - [Jetpack Compose LazyGrid使用全解](https://juejin.cn/post/7100120556192104484/) : 代码位于 /ui/post_lazygrid
27 | - [瀑布流布局、下拉加载、drawText](https://juejin.cn/post/7165805186118582308)
28 |
29 | 项目配套视频(早期作品):
30 | - [A-State及其简单应用](https://www.bilibili.com/video/BV1Xq4y1Q7iP/) 【Compose 1.0.1】:代码位于 /ui/videoa
31 | - [B-Snackbar&AlertDialog](https://www.bilibili.com/video/BV1iL411J7WR/) 【Compose 1.0.1】:代码位于 /ui/videob
32 | - [C-简易动画&组合动画&旋转动画](https://www.bilibili.com/video/bv1eq4y1D7Mq) 【Compose 1.0.1】:代码位于 /ui/videoc
33 |
34 |
35 | 如有帮助,欢迎 Star。
36 |
37 | 如果您对完整项目感兴趣,可点击[链接 - FunnyTranslation](https://github.com/FunnySaltyFish/FunnyTranslation) 查看
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | }
5 |
6 | android {
7 | compileSdk 34
8 | buildToolsVersion "34.0.0"
9 |
10 | defaultConfig {
11 | applicationId "com.funny.compose.study"
12 | minSdk 21
13 | targetSdk 34
14 | versionCode 1
15 | versionName "1.00"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_17
31 | targetCompatibility JavaVersion.VERSION_17
32 | }
33 | kotlinOptions {
34 | jvmTarget = '1.8'
35 | }
36 | buildFeatures {
37 | compose true
38 | }
39 | composeOptions {
40 | kotlinCompilerExtensionVersion compose_compiler_version
41 | }
42 | packagingOptions {
43 | resources {
44 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
45 | }
46 | }
47 | }
48 |
49 | dependencies {
50 | implementation 'androidx.core:core-ktx:1.9.0'
51 |
52 | // Compose BOM, see https://developer.android.com/jetpack/compose/bom/bom-mapping
53 | implementation platform("androidx.compose:compose-bom:2023.08.00")
54 | implementation "androidx.compose.ui:ui"
55 | implementation "androidx.compose.material:material"
56 | implementation "androidx.compose.material3:material3"
57 | implementation "androidx.compose.ui:ui-tooling-preview"
58 | implementation 'androidx.compose.runtime:runtime-livedata'
59 | implementation 'androidx.compose.animation:animation-graphics'
60 |
61 | def accompanist_version = "0.32.0"
62 | implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version"
63 | implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version"
64 |
65 | //提供MaterialColors
66 | implementation "com.github.FunnySaltyFish:CMaterialColors:1.0.21"
67 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.6.1"
68 |
69 | implementation 'androidx.activity:activity-compose:1.7.2'
70 |
71 | implementation 'com.gitee.funnysaltyfish:FunnyBottomNavigation:v1.1.0'
72 | implementation "com.github.FunnySaltyFish.ComposeDataSaver:data-saver:v1.1.5"
73 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1'
74 |
75 | implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.5")
76 |
77 | implementation 'com.github.jeziellago:compose-markdown:0.2.6'
78 | implementation 'androidx.appcompat:appcompat:1.6.0'
79 |
80 | // def accompanist_version = "0.23.1"
81 | // implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version"
82 |
83 | implementation 'org.jbox2d:jbox2d-library:2.2.1.1'
84 | testImplementation 'junit:junit:4.13.2'
85 | androidTestImplementation 'androidx.test.ext:junit:1.1.3'
86 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
87 | androidTestImplementation "androidx.compose.ui:ui-test-junit4:1.0.0-alpha07"
88 | debugImplementation "androidx.compose.ui:ui-tooling"
89 | }
--------------------------------------------------------------------------------
/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/release/output-metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 3,
3 | "artifactType": {
4 | "type": "APK",
5 | "kind": "Directory"
6 | },
7 | "applicationId": "com.funny.compose.study",
8 | "variantName": "release",
9 | "elements": [
10 | {
11 | "type": "SINGLE",
12 | "filters": [],
13 | "attributes": [],
14 | "versionCode": 1,
15 | "versionName": "1.00",
16 | "outputFile": "app-release.apk"
17 | }
18 | ],
19 | "elementType": "File"
20 | }
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/funny/compose/study/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study
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("com.funny.compose.study", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/App.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study
2 |
3 | import android.app.Application
4 | import com.funny.compose.study.ui.game.SnakeAssets
5 | import com.funny.data_saver.core.DataSaverConverter
6 | import com.funny.data_saver.core.DataSaverInterface
7 | import com.funny.data_saver.core.DataSaverPreferences
8 |
9 | class App: Application() {
10 | override fun onCreate() {
11 | super.onCreate()
12 | ctx = this
13 | DataSaverUtils = DataSaverPreferences(this)
14 |
15 | DataSaverConverter.registerTypeConverters(save = SnakeAssets.Saver, restore = SnakeAssets.Restorer)
16 | }
17 |
18 | companion object {
19 | lateinit var ctx: Application
20 | lateinit var DataSaverUtils: DataSaverInterface
21 | }
22 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.BackHandler
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.animation.AnimatedContent
8 | import androidx.compose.animation.AnimatedContentTransitionScope
9 | import androidx.compose.animation.ExperimentalAnimationApi
10 | import androidx.compose.animation.core.tween
11 | import androidx.compose.animation.fadeOut
12 | import androidx.compose.animation.togetherWith
13 | import androidx.compose.foundation.ExperimentalFoundationApi
14 | import androidx.compose.foundation.background
15 | import androidx.compose.foundation.clickable
16 | import androidx.compose.foundation.layout.Arrangement
17 | import androidx.compose.foundation.layout.Box
18 | import androidx.compose.foundation.layout.IntrinsicSize
19 | import androidx.compose.foundation.layout.PaddingValues
20 | import androidx.compose.foundation.layout.fillMaxSize
21 | import androidx.compose.foundation.layout.fillMaxWidth
22 | import androidx.compose.foundation.layout.height
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.foundation.layout.safeContentPadding
25 | import androidx.compose.foundation.layout.size
26 | import androidx.compose.foundation.layout.width
27 | import androidx.compose.foundation.layout.wrapContentWidth
28 | import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
29 | import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
30 | import androidx.compose.foundation.lazy.staggeredgrid.itemsIndexed
31 | import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
32 | import androidx.compose.foundation.shape.RoundedCornerShape
33 | import androidx.compose.material.Card
34 | import androidx.compose.material.Text
35 | import androidx.compose.runtime.Composable
36 | import androidx.compose.runtime.getValue
37 | import androidx.compose.runtime.mutableStateOf
38 | import androidx.compose.runtime.remember
39 | import androidx.compose.runtime.setValue
40 | import androidx.compose.ui.Alignment.Companion.Center
41 | import androidx.compose.ui.Alignment.Companion.CenterHorizontally
42 | import androidx.compose.ui.Modifier
43 | import androidx.compose.ui.graphics.Color
44 | import androidx.compose.ui.unit.dp
45 | import androidx.compose.ui.unit.sp
46 | import com.funny.cmaterialcolors.MaterialColors
47 | import com.funny.compose.study.ui.anim.LazyListFadeInAnim
48 | import com.funny.compose.study.ui.anim.LazyListSlideInFromRightAnim
49 | import com.funny.compose.study.ui.anim.NumberChangeAnimationTextTest
50 | import com.funny.compose.study.ui.event_test.ClickEventTest
51 | import com.funny.compose.study.ui.feature.BasicMarqueeTest
52 | import com.funny.compose.study.ui.feature.FlowRowTest
53 | import com.funny.compose.study.ui.feature.HorizontalPagerWithIndicator
54 | import com.funny.compose.study.ui.game.SnakeGame
55 | import com.funny.compose.study.ui.like_keep.FakeKeep
56 | import com.funny.compose.study.ui.markdowntest.MarkdownTest
57 | import com.funny.compose.study.ui.nav.NavigationTest
58 | import com.funny.compose.study.ui.others.RememberTest
59 | import com.funny.compose.study.ui.pager.VerticalPagerTest
60 | import com.funny.compose.study.ui.physics_layout.PhysicsLayoutTest
61 | import com.funny.compose.study.ui.post_layout.CountNumTest
62 | import com.funny.compose.study.ui.post_layout.SwipeCrossFadeLayoutTest
63 | import com.funny.compose.study.ui.post_layout.SwipeableDemo
64 | import com.funny.compose.study.ui.post_layout.VerticalLayout
65 | import com.funny.compose.study.ui.post_layout.VerticalLayoutWithIntrinsic
66 | import com.funny.compose.study.ui.post_layout.WaterfallFlowLayout
67 | import com.funny.compose.study.ui.post_layout.WeightedVerticalLayoutTest
68 | import com.funny.compose.study.ui.post_layout.randomColor
69 | import com.funny.compose.study.ui.post_lazygrid.SimpleLazyGrid
70 | import com.funny.compose.study.ui.post_lazygrid.SimpleLazyGridAda
71 | import com.funny.compose.study.ui.post_lazygrid.SimpleLazyGridWithSpace
72 | import com.funny.compose.study.ui.refresh.SwipeToRefreshTest
73 | import com.funny.compose.study.ui.saveable.SimpleNavigationWithSaveableStateSample
74 | import com.funny.compose.study.ui.theme.JetpackComposeStudyTheme
75 | import kotlin.random.Random
76 |
77 | class MainActivity : ComponentActivity() {
78 | override fun onCreate(savedInstanceState: Bundle?) {
79 | super.onCreate(savedInstanceState)
80 |
81 | setContent {
82 | JetpackComposeStudyTheme {
83 | Catalog()
84 | }
85 | }
86 | }
87 | }
88 |
89 | // 下面可能在 Android Studio 中报错:
90 | // “Type inference failed. Expected type mismatch: inferred type is @Composable () -> Unit but () -> Unit was expected”
91 | // 这是 Kotlin 插件的问题,已经提交了 bug 反馈,可以正常编译运行
92 |
93 | val pages: List Unit>> =
94 | arrayListOf(
95 | "物理引擎+自定义布局" to { PhysicsLayoutTest() },
96 | "高仿Keep周界面(自定义绘制)" to { FakeKeep() },
97 | "自定义布局(1-1):简易纵向布局" to {
98 | VerticalLayout {
99 | (1..5).forEach { _ ->
100 | Box(
101 | modifier = Modifier
102 | .size(40.dp)
103 | .background(randomColor())
104 | )
105 | }
106 | }
107 | },
108 | "自定义布局(1-2):简易瀑布流" to {
109 | WaterfallFlowLayout(
110 | modifier = Modifier.fillMaxWidth(),
111 | columns = 3
112 | ) {
113 | (1..10).forEach { _ ->
114 | Box(
115 | modifier = Modifier
116 | .height(Random.nextInt(50, 100).dp)
117 | .background(randomColor())
118 | )
119 | }
120 | }
121 | },
122 | "自定义布局(3):固有特性测量(最小)" to {
123 | val text = arrayOf("Funny", "Salty", "Fish", "is", "Very", "Salty")
124 | VerticalLayoutWithIntrinsic(
125 | Modifier
126 | .width(IntrinsicSize.Min)
127 | .padding(12.dp)
128 | .background(MaterialColors.Yellow100)
129 | ) {
130 | text.forEach {
131 | Text(text = it, fontSize = 24.sp)
132 | }
133 | }
134 | },
135 | "自定义布局(3):固有特性测量(最大)" to {
136 | val text = arrayOf("Funny", "Salty", "Fish", "is", "Very", "Salty")
137 | VerticalLayoutWithIntrinsic(
138 | Modifier
139 | .width(IntrinsicSize.Max)
140 | .padding(12.dp)
141 | .background(MaterialColors.Yellow100)
142 | ) {
143 | text.forEach {
144 | Text(text = it, fontSize = 24.sp)
145 | }
146 | }
147 | },
148 | "自定义布局(4-1):ParentData之咸鱼的地摊(输出见logcat)" to { CountNumTest() },
149 | "自定义布局(4-1):ParentData之自定义weight" to { WeightedVerticalLayoutTest() },
150 | "简易网格布局" to { SimpleLazyGrid() },
151 | "简易网格布局(带内边距)" to { SimpleLazyGridWithSpace() },
152 | "简易网格布局(自适应宽度,请横屏测试)" to { SimpleLazyGridAda() },
153 | "Markdown测试" to { MarkdownTest() },
154 | "下拉刷新测试" to { SwipeToRefreshTest() },
155 | "点击事件传递" to { ClickEventTest() },
156 | "动画变化的文本" to { NumberChangeAnimationTextTest() },
157 | "跨屏状态保存(Google官方示例)" to { SimpleNavigationWithSaveableStateSample() },
158 | "Navigation使用" to { NavigationTest() },
159 | "PagerTest" to { VerticalPagerTest() },
160 | "RememberTest(参数不变,跳过重组,请见日志输出)" to { RememberTest() },
161 | "MVI 贪吃蛇小游戏" to { SnakeGame() },
162 | "1.4:PagerWithIndicator" to { HorizontalPagerWithIndicator() },
163 | "1.4:FlowRow" to { FlowRowTest() },
164 | "1.4:跑马灯效果" to { BasicMarqueeTest() },
165 | "列表动画(右侧位移进入)" to { LazyListSlideInFromRightAnim() },
166 | "列表动画(FadeIn)" to { LazyListFadeInAnim() },
167 | "下拉渐变切换的布局" to { SwipeCrossFadeLayoutTest() },
168 | "SwipeableTest" to { SwipeableDemo() }
169 | )
170 |
171 |
172 | @OptIn(ExperimentalAnimationApi::class, ExperimentalFoundationApi::class)
173 | @Composable
174 | fun Catalog() {
175 | var content: (@Composable () -> Unit)? by remember {
176 | mutableStateOf(null)
177 | }
178 | AnimatedContent(
179 | modifier = Modifier
180 | .fillMaxSize()
181 | .safeContentPadding(),
182 | targetState = content,
183 | transitionSpec = {
184 | slideIntoContainer(
185 | AnimatedContentTransitionScope.SlideDirection.Right,
186 | tween(500)
187 | ) togetherWith fadeOut() + slideOutOfContainer(
188 | AnimatedContentTransitionScope.SlideDirection.Left,
189 | tween(500)
190 | )
191 | },
192 | label = "Catalog",
193 | ) {
194 | when (it) {
195 | null -> LazyVerticalStaggeredGrid(
196 | columns = StaggeredGridCells.Fixed(2),
197 | // 整体内边距
198 | contentPadding = PaddingValues(8.dp, 8.dp),
199 | // item 和 item 之间的纵向间距
200 | verticalItemSpacing = 8.dp,
201 | // item 和 item 之间的横向间距
202 | horizontalArrangement = Arrangement.spacedBy(8.dp),
203 | state = rememberLazyStaggeredGridState()
204 | ) {
205 | itemsIndexed(pages, key = { _, p -> p.first }) { _, pair ->
206 | Card(
207 | modifier = Modifier.clickable { content = pair.second },
208 | shape = RoundedCornerShape(4.dp),
209 | backgroundColor = MaterialColors.Blue200.copy(0.8f)
210 | ) {
211 | Text(
212 | text = pair.first,
213 | modifier = Modifier
214 | .fillMaxWidth()
215 | .wrapContentWidth(CenterHorizontally)
216 | .padding(16.dp),
217 | color = Color.White
218 | )
219 | }
220 | }
221 | }
222 |
223 | else -> Box(
224 | Modifier
225 | .fillMaxSize()
226 | .padding(8.dp),
227 | contentAlignment = Center
228 | ) {
229 | BackHandler {
230 | content = null
231 | }
232 | it()
233 | }
234 | }
235 | }
236 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/anim/LazyColumnAnim.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.anim
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.Animatable
5 | import androidx.compose.animation.core.FastOutSlowInEasing
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.PaddingValues
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.foundation.lazy.items
14 | import androidx.compose.foundation.lazy.itemsIndexed
15 | import androidx.compose.material.MaterialTheme
16 | import androidx.compose.material.Text
17 | import androidx.compose.runtime.*
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.graphicsLayer
20 | import androidx.compose.ui.unit.dp
21 | import kotlinx.collections.immutable.toImmutableList
22 | import kotlinx.coroutines.delay
23 |
24 | data class Student(val name: String, val age: Int) {
25 | override fun toString() = "I am $name, $age years old"
26 | }
27 |
28 | val list = List(100) {
29 | Student("name$it", it)
30 | }.toImmutableList()
31 |
32 | @Composable
33 | fun LazyListSlideInFromRightAnim() {
34 | LazyColumn(
35 | contentPadding = PaddingValues(12.dp),
36 | verticalArrangement = Arrangement.spacedBy(8.dp)
37 | ) {
38 | items(list) {
39 | val animatedProgress = remember { Animatable(initialValue = 300f) }
40 | LaunchedEffect(Unit) {
41 | animatedProgress.animateTo(
42 | targetValue = 0f,
43 | animationSpec = tween(700, easing = FastOutSlowInEasing)
44 | )
45 | }
46 | StudentItem(
47 | Modifier
48 | .background(MaterialTheme.colors.surface)
49 | .graphicsLayer(translationX = animatedProgress.value), it)
50 | }
51 | }
52 | }
53 |
54 | @Composable
55 | fun LazyListFadeInAnim() {
56 | LazyColumn(
57 | contentPadding = PaddingValues(12.dp)
58 | ) {
59 | items(list) {
60 | val animatedProgress = remember { Animatable(initialValue = 0f) }
61 | LaunchedEffect(Unit) {
62 | animatedProgress.animateTo(
63 | targetValue = 1f,
64 | animationSpec = tween(700, easing = FastOutSlowInEasing)
65 | )
66 | }
67 | StudentItem(Modifier.graphicsLayer(alpha = animatedProgress.value), it)
68 | }
69 | }
70 | }
71 |
72 | @Composable
73 | fun LazyListAnimatedVisibility() {
74 | LazyColumn(
75 | contentPadding = PaddingValues(12.dp)
76 | ) {
77 | itemsIndexed(list) { index, item ->
78 | FireAndForgetEnter(index = index) {
79 | StudentItem(Modifier, item)
80 | }
81 | }
82 | }
83 | }
84 |
85 | @Composable
86 | fun FireAndForgetEnter(
87 | index : Int,
88 | modifier: Modifier = Modifier,
89 | enter: EnterTransition = fadeIn() + expandIn(),
90 | label: String = "FireAndForgetEnter",
91 | content: @Composable AnimatedVisibilityScope.() -> Unit
92 | ) {
93 | var trigger by remember { mutableStateOf(false) }
94 |
95 | LaunchedEffect(Unit) {
96 | delay(100L)
97 | trigger = true
98 | }
99 |
100 | AnimatedVisibility(
101 | visible = trigger,
102 | modifier = modifier,
103 | enter = enter,
104 | exit = ExitTransition.None,
105 | label = label,
106 | content = content
107 | )
108 | }
109 |
110 | @Composable
111 | private fun StudentItem(modifier: Modifier, student: Student) {
112 | Text(
113 | modifier = modifier
114 | .fillMaxWidth()
115 | .padding(8.dp),
116 | text = student.toString(),
117 | style = MaterialTheme.typography.subtitle1
118 | )
119 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/anim/NumberChangeAnimation.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.anim
2 |
3 | import androidx.compose.animation.*
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.material.Text
7 | import androidx.compose.material.TextButton
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.text.font.FontWeight
13 | import androidx.compose.ui.unit.TextUnit
14 | import androidx.compose.ui.unit.dp
15 | import androidx.compose.ui.unit.sp
16 | import kotlinx.coroutines.delay
17 | import kotlin.math.roundToInt
18 |
19 | @Composable
20 | fun NumberChangeAnimationTextTest() {
21 | Column(modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally) {
22 | var text by remember { mutableStateOf("103") }
23 | NumberChangeAnimatedText(text = text)
24 |
25 | Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) {
26 | // 加一 和 减一
27 | listOf(1, -1).forEach { i ->
28 | TextButton(onClick = {
29 | text = (text.toInt() + i).toString()
30 | }) {
31 | Text(text = if (i == 1) "加一" else "减一")
32 | }
33 | }
34 | }
35 | Spacer(modifier = Modifier.height(16.dp))
36 |
37 | val longText = remember {
38 | mutableStateOf("————————————")
39 | }
40 |
41 | LaunchedEffect(key1 = Unit, block = {
42 | delay(2000)
43 | longText.value = "这是测试动画"
44 | })
45 |
46 | Box(
47 | modifier = Modifier
48 | .height(100.dp)
49 | .fillMaxWidth(), contentAlignment = Alignment.Center
50 | ) {
51 | NumberChangeAnimatedText(text = longText.value)
52 | }
53 |
54 | Box(
55 | modifier = Modifier
56 | .height(100.dp)
57 | .fillMaxWidth(), contentAlignment = Alignment.Center
58 | ) {
59 | AutoIncreaseAnimatedNumber(number = 10000, durationMills = 11000)
60 | }
61 | }
62 |
63 | }
64 |
65 | @OptIn(ExperimentalAnimationApi::class)
66 | @Composable
67 | fun NumberChangeAnimatedText(
68 | modifier: Modifier = Modifier,
69 | text: String,
70 | textPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 12.dp),
71 | textSize: TextUnit = 24.sp,
72 | textColor: Color = Color.Black,
73 | textWeight: FontWeight = FontWeight.Normal,
74 | ) {
75 | Row(modifier = modifier) {
76 | text.forEach {
77 | AnimatedContent(
78 | targetState = it,
79 | transitionSpec = {
80 | slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Up) with
81 | fadeOut() + slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.Up)
82 | },
83 | label = "NumberChange"
84 | ) { char ->
85 | Text(
86 | text = char.toString(),
87 | modifier = modifier.padding(textPadding),
88 | fontSize = textSize,
89 | color = textColor,
90 | fontWeight = textWeight
91 | )
92 | }
93 | }
94 | }
95 | }
96 |
97 | @Composable
98 | fun AutoIncreaseAnimatedNumber(
99 | modifier: Modifier = Modifier,
100 | startAnim: Boolean = true,
101 | number: Int,
102 | durationMills: Int = 16000,
103 | textPadding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 12.dp),
104 | textSize: TextUnit = 24.sp,
105 | textColor: Color = Color.Black,
106 | textWeight: FontWeight = FontWeight.Normal
107 | ) {
108 | // 动画,Animatable 相关介绍可以见 https://compose.funnysaltyfish.fun/docs/design/animation/animatable?source=trans
109 | val animatedNumber = remember {
110 | androidx.compose.animation.core.Animatable(0f)
111 | }
112 | // 数字格式化后的长度
113 | val l = remember(number) {
114 | number.toString().length
115 | }
116 |
117 | // Composable 进入 Composition 阶段,且 startAnim 为 true 时开启动画
118 | LaunchedEffect(number, startAnim) {
119 | if (startAnim)
120 | animatedNumber.animateTo(
121 | targetValue = number.toFloat(),
122 | animationSpec = tween(durationMillis = durationMills)
123 | )
124 | }
125 |
126 | NumberChangeAnimatedText(
127 | modifier = modifier,
128 | text = "%0${l}d".format(animatedNumber.value.roundToInt()),
129 | textPadding = textPadding,
130 | textColor = textColor,
131 | textSize = textSize,
132 | textWeight = textWeight
133 | )
134 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/event_test/ClickEventTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.event_test
2 |
3 | import android.widget.Toast
4 | import androidx.compose.foundation.gestures.Orientation
5 | import androidx.compose.foundation.gestures.draggable
6 | import androidx.compose.foundation.gestures.rememberDraggableState
7 | import androidx.compose.foundation.interaction.MutableInteractionSource
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.rememberScrollState
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material.Button
12 | import androidx.compose.material.Text
13 | import androidx.compose.runtime.*
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.platform.LocalContext
16 | import androidx.compose.ui.unit.IntOffset
17 | import androidx.compose.ui.unit.dp
18 | import kotlin.math.roundToInt
19 |
20 | // 下面的代码在探究这样一个事情
21 | // 把 Column 拖开后,才能点击 button
22 | @Composable
23 | fun ClickEventTest() {
24 | val ctx = LocalContext.current
25 | var offset by remember {
26 | mutableStateOf(0f)
27 | }
28 | Box(Modifier.fillMaxSize()) {
29 | Button(
30 | onClick = { Toast.makeText(ctx, "我是Toast", Toast.LENGTH_SHORT).show() },
31 | ) {
32 | Text(text = "点我")
33 | }
34 | Column(
35 | Modifier
36 | .fillMaxSize()
37 | .offset { IntOffset(0, offset.roundToInt()) }
38 | .draggable(rememberDraggableState(onDelta = { offset += it}), Orientation.Vertical)
39 | ) {
40 | repeat(20){ i->
41 | key(i) {
42 | Text(text = "Item $i", modifier = Modifier.padding(20.dp))
43 | }
44 | }
45 | }
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/feature/BasicMarquee1_4.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.feature
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.basicMarquee
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.Spacer
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.res.painterResource
18 | import androidx.compose.ui.unit.dp
19 | import com.funny.compose.study.R
20 |
21 | @OptIn(ExperimentalFoundationApi::class)
22 | @Composable
23 | fun BasicMarqueeTest() {
24 | Column {
25 | Label(text = "Text")
26 | Text(
27 | text = "Hello World... This is FunnySaltyFish, a Jetpack Compose lover. I'm learning Jetpack Compose",
28 | style = MaterialTheme.typography.h1,
29 | modifier = Modifier
30 | .fillMaxWidth()
31 | .basicMarquee()
32 | )
33 | Label(text = "Image Row")
34 | Row(modifier = Modifier.basicMarquee()) {
35 | (0..5).forEach { _ ->
36 | Image(modifier = Modifier.size(100.dp), painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = "")
37 | }
38 | }
39 | }
40 | }
41 |
42 | @Composable
43 | private fun Label(text: String) {
44 | Text(text = text, style = MaterialTheme.typography.h5)
45 | Spacer(modifier = Modifier.height(8.dp))
46 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/feature/FlowLayout1_4.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.feature
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.ExperimentalLayoutApi
5 | import androidx.compose.foundation.layout.FlowRow
6 | import androidx.compose.material.ExperimentalMaterialApi
7 | import androidx.compose.material.FilterChip
8 | import androidx.compose.material.Icon
9 | import androidx.compose.material.Text
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Check
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.unit.dp
18 |
19 | @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterialApi::class)
20 | @Composable
21 | fun FlowRowTest() {
22 | val filters = listOf(
23 | "Washer/Dryer", "Ramp access", "Garden", "Cats OK", "Dogs OK", "Smoke-free"
24 | )
25 | FlowRow(
26 | horizontalArrangement = Arrangement.spacedBy(8.dp)
27 | ) {
28 | filters.forEach { title ->
29 | var selected by remember { mutableStateOf(false) }
30 | val leadingIcon: @Composable () -> Unit = { Icon(Icons.Default.Check, null) }
31 | FilterChip(
32 | selected,
33 | onClick = { selected = !selected },
34 | content = { Text(title) },
35 | leadingIcon = if (selected) leadingIcon else null
36 | )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/feature/Pager1_4.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.feature
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.wrapContentSize
10 | import androidx.compose.foundation.pager.HorizontalPager
11 | import androidx.compose.foundation.pager.rememberPagerState
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Tab
14 | import androidx.compose.material.TabRow
15 | import androidx.compose.material.TabRowDefaults
16 | import androidx.compose.material.Text
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.mutableStateListOf
19 | import androidx.compose.runtime.rememberCoroutineScope
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.text.style.TextAlign
23 | import com.google.accompanist.pager.ExperimentalPagerApi
24 | import com.google.accompanist.pager.pagerTabIndicatorOffset
25 | import kotlinx.coroutines.launch
26 |
27 | private val pages = listOf("生活", "美食", "科技", "娱乐", "涩涩")
28 |
29 | @OptIn(ExperimentalFoundationApi::class, ExperimentalPagerApi::class)
30 | @Composable
31 | fun HorizontalPagerWithIndicator() {
32 | val pagerState = rememberPagerState(pageCount = { pages.size })
33 | val scope = rememberCoroutineScope()
34 |
35 | Column {
36 | TabRow(
37 | modifier = Modifier.fillMaxWidth(),
38 | // Our selected tab is our current page
39 | selectedTabIndex = pagerState.currentPage,
40 | // Override the indicator, using the provided pagerTabIndicatorOffset modifier
41 | indicator = { tabPositions ->
42 | TabRowDefaults.Indicator(
43 | Modifier.pagerTabIndicatorOffset(
44 | pagerState = pagerState,
45 | tabPositions = tabPositions
46 | )
47 | )
48 | }
49 | ) {
50 | // Add tabs for all of our pages
51 | pages.forEachIndexed { index, title ->
52 | Tab(
53 | text = { Text(title) },
54 | selected = pagerState.currentPage == index,
55 | onClick = {
56 | scope.launch {
57 | pagerState.animateScrollToPage(index)
58 | }
59 | },
60 | )
61 | }
62 | }
63 |
64 | HorizontalPager(
65 | state = pagerState,
66 | modifier = Modifier.fillMaxSize().background(MaterialTheme.colors.surface)
67 | ) { page ->
68 | Text(
69 | text = "Page $page",
70 | style = MaterialTheme.typography.h5,
71 | modifier = Modifier
72 | .fillMaxSize()
73 | .wrapContentSize(Alignment.Center),
74 | textAlign = TextAlign.Center,
75 | )
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/feature/README.md:
--------------------------------------------------------------------------------
1 |
2 | 此目录用来测试一些新特性,文件名为 项目+版本,比如 Pager1_4,表示 Jetpack Compose 1.4.0 的 Pager
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/Beans.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.runtime.saveable.Saver
5 | import androidx.compose.ui.geometry.Offset
6 | import androidx.compose.ui.geometry.Size
7 | import androidx.compose.ui.graphics.Color
8 | import java.util.*
9 |
10 | enum class MoveDirection {
11 | RIGHT, UP, DOWN, LEFT;
12 | }
13 |
14 | /**
15 | * 代表相对位置
16 | * @property x Int
17 | * @property y Int
18 | * @constructor
19 | */
20 | @Stable
21 | data class Point(val x: Int, val y: Int) {
22 | override fun toString(): String {
23 | return "Point(x=$x, y=$y)"
24 | }
25 |
26 | fun asOffset(blockSize: Size) = Offset(x * blockSize.width, y * blockSize.height)
27 | }
28 |
29 | @Stable
30 | data class Snake(val body: LinkedList, val bodySize: Float, val direction: MoveDirection) {
31 | val head: Point
32 | get() = body.first
33 |
34 | private val headPlace
35 | get() = head
36 |
37 | fun nextPos() = when (direction) {
38 | MoveDirection.RIGHT -> Point(headPlace.x + 1, headPlace.y)
39 | MoveDirection.LEFT -> Point(headPlace.x - 1, headPlace.y)
40 | MoveDirection.UP -> Point(headPlace.x, headPlace.y - 1)
41 | MoveDirection.DOWN -> Point(headPlace.x, headPlace.y + 1)
42 | }
43 |
44 | fun grow(pos: Point) = this.apply {
45 | body.addFirst(pos)
46 | }
47 |
48 | fun move(pos: Point) = this.copy(body = this.body.apply {
49 | body.removeLast()
50 | body.addFirst(pos)
51 | })
52 |
53 | fun changeDirection(newDirection: MoveDirection) =
54 | // 无法直接反向
55 | if (direction.ordinal + newDirection.ordinal != 3) {
56 | this.copy(direction = newDirection)
57 | } else this
58 |
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/Modifers.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.foundation.gestures.awaitEachGesture
4 | import androidx.compose.foundation.gestures.awaitFirstDown
5 | import androidx.compose.foundation.gestures.forEachGesture
6 | import androidx.compose.foundation.gestures.waitForUpOrCancellation
7 | import androidx.compose.runtime.Stable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.input.pointer.pointerInput
10 | import androidx.compose.ui.layout.layout
11 | import org.jbox2d.common.MathUtils.abs
12 |
13 | @Stable
14 | fun Modifier.square() =
15 | this.layout { measureable, constraints ->
16 | val size = minOf(constraints.maxWidth, constraints.maxHeight)
17 | layout(size, size) {
18 | val placeable = measureable.measure(constraints.copy(minWidth = size, minHeight = size, maxWidth = size, maxHeight = size))
19 | placeable.placeRelative(0, 0)
20 | }
21 | }
22 |
23 | @Stable
24 | fun Modifier.detectDirectionalMove(
25 | validMoveDelta: Float = 50f,
26 | updateDirection: (MoveDirection) -> Unit
27 | ) = this.pointerInput(Unit) {
28 | // 监听如下操作:
29 | // 手指从按下到滑动到抬起,移动的方向如果大于 validMoveDelta,则视为往对应方向产生了一次滑动
30 | awaitEachGesture {
31 | val down = awaitFirstDown(requireUnconsumed = false)
32 | val start = down.position
33 | val up = waitForUpOrCancellation()
34 | up?.position?.let { end ->
35 | val dx = end.x - start.x
36 | val dy = end.y - start.y
37 | if (abs(dx) > validMoveDelta && abs(dx) > abs(dy)) {
38 | updateDirection(
39 | if (dx > 0) MoveDirection.RIGHT else MoveDirection.LEFT
40 | )
41 | } else if (abs(dy) > validMoveDelta) {
42 | updateDirection(
43 | if (dy > 0) MoveDirection.DOWN else MoveDirection.UP
44 | )
45 | }
46 | }
47 | }
48 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/SnakeAssets.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import com.funny.cmaterialcolors.MaterialColors
5 |
6 |
7 | sealed class SnakeAssets(
8 | val foodColor: Color= MaterialColors.Orange700,
9 | val lineColor: Color= Color.LightGray.copy(alpha = 0.8f),
10 | val headColor: Color= MaterialColors.Red700,
11 | val bodyColor: Color= MaterialColors.Blue200
12 | ) {
13 | object SnakeAssets1: SnakeAssets()
14 |
15 | object SnakeAssets2: SnakeAssets(
16 | foodColor = MaterialColors.Purple700,
17 | lineColor = MaterialColors.Brown200.copy(alpha = 0.8f),
18 | headColor = MaterialColors.Blue700,
19 | bodyColor = MaterialColors.Pink300
20 | )
21 |
22 | override fun toString(): String {
23 | return this.javaClass.simpleName
24 | }
25 |
26 | companion object {
27 | val Saver = { assets: SnakeAssets ->
28 | assets.javaClass.simpleName
29 | }
30 | val Restorer = { str: String ->
31 | when(str) {
32 | "SnakeAssets1" -> SnakeAssets1
33 | "SnakeAssets2" -> SnakeAssets2
34 | else -> SnakeAssets1
35 | }
36 | }
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/SnakeGame.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.ColumnScope
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.material.DropdownMenu
12 | import androidx.compose.material.DropdownMenuItem
13 | import androidx.compose.material.OutlinedButton
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.CompositionLocalProvider
17 | import androidx.compose.runtime.LaunchedEffect
18 | import androidx.compose.runtime.ProvidableCompositionLocal
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.runtime.staticCompositionLocalOf
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.geometry.Offset
27 | import androidx.compose.ui.graphics.drawscope.DrawScope
28 | import androidx.compose.ui.layout.onGloballyPositioned
29 | import androidx.compose.ui.text.style.TextAlign
30 | import androidx.compose.ui.unit.dp
31 | import androidx.lifecycle.viewmodel.compose.viewModel
32 | import kotlinx.coroutines.delay
33 |
34 | internal val LocalSnakeAssets: ProvidableCompositionLocal = staticCompositionLocalOf { SnakeAssets.SnakeAssets1 }
35 | private const val TAG = "SnakeGame"
36 |
37 | @Composable
38 | fun SnakeGame(
39 | modifier: Modifier = Modifier
40 | ) {
41 | val vm: SnakeGameViewModel = viewModel()
42 | val snakeState by vm.snakeState
43 |
44 | LaunchedEffect(key1 = snakeState.gameState) {
45 | if (snakeState.gameState != GameState.PLAYING) return@LaunchedEffect
46 | while (true) {
47 | vm.dispatch(GameAction.GameTick)
48 | delay(snakeState.getSleepTime())
49 | }
50 | }
51 |
52 | val snakeAssets by ThemeConfig.savedSnakeAssets
53 | CompositionLocalProvider(LocalSnakeAssets provides snakeAssets) {
54 | Column(
55 | modifier = modifier
56 | .fillMaxSize()
57 | .padding(horizontal = 8.dp),
58 | verticalArrangement = Arrangement.Center,
59 | horizontalAlignment = Alignment.CenterHorizontally
60 | ) {
61 | when (snakeState.gameState) {
62 | GameState.PLAYING -> Playing(snakeState, snakeAssets, vm::dispatch)
63 | GameState.LOST -> Lost(snakeState.getScore(), vm.historyBestScore.value, vm::dispatch)
64 | GameState.WAITING -> Waiting(vm::dispatch)
65 | }
66 | }
67 | }
68 | }
69 |
70 | @Composable
71 | fun ColumnScope.Waiting(dispatchAction: (GameAction) -> Unit) {
72 | OutlinedButton(onClick = { dispatchAction(GameAction.StartGame) }) {
73 | Text(text = "开始游戏")
74 | }
75 | Spacer(modifier = Modifier.height(16.dp))
76 | val snakeAssets by ThemeConfig.savedSnakeAssets
77 | var expanded by remember { mutableStateOf(false) }
78 | OutlinedButton(onClick = { expanded = true }) {
79 | Text(text = "选择主题:$snakeAssets")
80 | DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
81 | ThemeConfig.themeList.forEach { theme ->
82 | DropdownMenuItem(onClick = {
83 | ThemeConfig.savedSnakeAssets.value = theme
84 | expanded = false
85 | }) {
86 | Text(text = theme.toString())
87 | }
88 | }
89 | }
90 | }
91 | }
92 |
93 | @Composable
94 | fun ColumnScope.Playing(
95 | snakeState: SnakeState,
96 | snakeAssets: SnakeAssets,
97 | dispatchAction: (GameAction) -> Unit
98 | ) {
99 | Canvas(
100 | modifier = Modifier
101 | .fillMaxSize()
102 | .square()
103 | .onGloballyPositioned {
104 | val size = it.size
105 | dispatchAction(GameAction.ChangeSize(size.width to size.height))
106 | }
107 | .detectDirectionalMove {
108 | dispatchAction(GameAction.MoveSnake(it))
109 | }
110 | ) {
111 | drawBackgroundGrid(snakeState, snakeAssets)
112 | drawSnake(snakeState, snakeAssets)
113 | drawFood(snakeState, snakeAssets)
114 | }
115 | }
116 |
117 | @Composable
118 | fun ColumnScope.Lost(
119 | score: Int,
120 | bestHistoryScore: Int,
121 | dispatchAction: (GameAction) -> Unit
122 | ) {
123 | Text(text = "糟糕,失败了!\n您的分数:$score,历史最佳:$bestHistoryScore", textAlign = TextAlign.Center)
124 | Spacer(modifier = Modifier.height(8.dp))
125 | OutlinedButton(onClick = { dispatchAction(GameAction.RestartGame) }) {
126 | Text(text = "再来一次", textAlign = TextAlign.Center)
127 | }
128 | }
129 |
130 |
131 | private fun DrawScope.drawSnake(snakeState: SnakeState, snakeAssets: SnakeAssets) {
132 | val size = snakeState.blockSize
133 | snakeState.snake.body.forEach {
134 | val offset = it.asOffset(snakeState.blockSize)
135 | if (it == snakeState.snake.head) {
136 | drawRect(snakeAssets.headColor, offset, size)
137 | } else {
138 | drawRect(snakeAssets.bodyColor, offset, size)
139 | }
140 | }
141 | }
142 |
143 | fun DrawScope.drawFood(snakeState: SnakeState, snakeAssets: SnakeAssets) {
144 | val size = snakeState.blockSize
145 | val offset = snakeState.food.asOffset(snakeState.blockSize)
146 | drawRect(snakeAssets.foodColor, offset, size)
147 | }
148 |
149 | private fun DrawScope.drawBackgroundGrid(snakeState: SnakeState, snakeAssets: SnakeAssets) {
150 | val (width, height) = snakeState.size
151 | for (x in 0..width step snakeState.blockSize.width.toInt()) {
152 | drawLine(
153 | snakeAssets.lineColor,
154 | start = Offset(x.toFloat(), 0f),
155 | end = Offset(x.toFloat(), height.toFloat()),
156 | strokeWidth = 1f
157 | )
158 | }
159 | for (y in 0..height step snakeState.blockSize.height.toInt()) {
160 | drawLine(
161 | snakeAssets.lineColor,
162 | start = Offset(0f, y.toFloat()),
163 | end = Offset(width.toFloat(), y.toFloat()),
164 | strokeWidth = 1f
165 | )
166 | }
167 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/SnakeGameViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.mutableStateOf
5 | import androidx.compose.ui.geometry.Size
6 | import androidx.lifecycle.ViewModel
7 | import com.funny.compose.study.App
8 | import com.funny.data_saver.core.mutableDataSaverStateOf
9 | import java.util.*
10 | import kotlin.random.Random
11 |
12 | class SnakeGameViewModel : ViewModel(){
13 | companion object {
14 | const val TAG = "SnakeGameVM"
15 | internal const val COL_NUM = 20
16 | internal const val ROW_NUM = 20
17 | private val INITIAL_SNAKE get() = Snake(LinkedList().apply {
18 | add(Point(10,10))
19 | add(Point(11,10))
20 | add(Point(12,10))
21 | },20f, MoveDirection.LEFT)
22 | }
23 |
24 | // This value will be automatically saved by using Preferences via https://github.com/FunnySaltyFish/ComposeDataSaver
25 | val historyBestScore = mutableDataSaverStateOf(App.DataSaverUtils, key = "history_best_score", initialValue = 0)
26 |
27 | val snakeState = mutableStateOf(SnakeState(
28 | snake = INITIAL_SNAKE,
29 | size = 400 to 400,
30 | blockSize = Size(20f, 20f),
31 | food = generateFood(INITIAL_SNAKE.body)
32 | ))
33 |
34 | fun dispatch(gameAction: GameAction){
35 | Log.d(TAG, "dispatch: event: $gameAction")
36 | snakeState.value = reduce(snakeState.value, gameAction)
37 | }
38 |
39 | private fun reduce(state: SnakeState, gameAction: GameAction): SnakeState {
40 | val snake = state.snake
41 | return when(gameAction){
42 | GameAction.GameTick -> {
43 | val nextPos = snake.nextPos()
44 | when {
45 | nextPos == snakeState.value.food -> {
46 | val newBody = snake.grow(nextPos)
47 | val newFood = generateFood(newBody.body)
48 | val newDifficulty = (state.difficulty + 0.8f).coerceAtMost(10f)
49 | state.copy(snake = newBody, food = newFood, difficulty = newDifficulty)
50 | }
51 | state.collideWall(nextPos) || state.collideSelf(nextPos) -> {
52 | val score = state.getScore()
53 | if (score > historyBestScore.value) {
54 | historyBestScore.value = score
55 | }
56 | state.copy(gameState = GameState.LOST)
57 | }
58 | else -> state.copy(snake = snake.move(nextPos))
59 | }
60 | }
61 | GameAction.StartGame -> state.copy(gameState = GameState.PLAYING)
62 | GameAction.RestartGame -> state.copy(snake = INITIAL_SNAKE, food = generateFood(INITIAL_SNAKE.body), gameState = GameState.WAITING, difficulty = 1f)
63 | is GameAction.ChangeSize -> {
64 | val newSize = gameAction.size
65 | state.copy(size = newSize, blockSize = Size((newSize.first / COL_NUM).toFloat(), (newSize.second / ROW_NUM).toFloat()))
66 | }
67 | is GameAction.MoveSnake -> state.copy(snake = state.snake.changeDirection(gameAction.direction))
68 | else -> state
69 | }
70 | }
71 |
72 | private fun generateFood(body: List): Point {
73 | val x = Random.nextInt(0, COL_NUM)
74 | val y = Random.nextInt(0, ROW_NUM)
75 | val p = Point(x, y)
76 | return if (body.contains(p)) generateFood(body) else p
77 | }
78 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/SnakeState.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.runtime.Stable
4 | import androidx.compose.ui.geometry.Size
5 | import com.funny.compose.study.ui.game.SnakeGameViewModel.Companion.COL_NUM
6 | import com.funny.compose.study.ui.game.SnakeGameViewModel.Companion.ROW_NUM
7 |
8 |
9 | @Stable
10 | class SnakeState(
11 | val snake: Snake,
12 | val size: Pair,
13 | val blockSize: Size,
14 | val food: Point,
15 | val gameState: GameState = GameState.WAITING,
16 | val difficulty: Float = 1f,
17 | ) {
18 | fun collideWall(nextPos: Point) =
19 | nextPos.x < 0 || nextPos.y < 0 || nextPos.x >= COL_NUM || nextPos.y >= ROW_NUM
20 |
21 | fun collideSelf(nextPos: Point) =
22 | snake.body.any { it == nextPos }
23 |
24 | fun getSleepTime() = (1000.0f / difficulty).toLong()
25 |
26 | fun getScore() = this.snake.body.size * 100
27 |
28 | fun copy(
29 | snake: Snake = this.snake,
30 | size: Pair = this.size,
31 | blockSize: Size = this.blockSize,
32 | food: Point = this.food,
33 | gameState: GameState = this.gameState,
34 | difficulty: Float = this.difficulty
35 | ) = SnakeState(snake, size, blockSize, food, gameState, difficulty = difficulty)
36 | }
37 |
38 | sealed class GameAction {
39 | data class MoveSnake(val direction: MoveDirection) : GameAction()
40 | data class ChangeSize(val size: Pair) : GameAction()
41 | object GameTick : GameAction()
42 | object StartGame : GameAction()
43 | object LoseGame : GameAction()
44 | object RestartGame : GameAction()
45 | object QuitGame : GameAction()
46 |
47 | override fun toString(): String {
48 | return when (this) {
49 | is MoveSnake -> "MoveSnake(direction=$direction)"
50 | is ChangeSize -> "ChangeSize(size=$size)"
51 | GameTick -> "GameTick"
52 | StartGame -> "StartGame"
53 | LoseGame -> "LoseGame"
54 | RestartGame -> "RestartGame"
55 | QuitGame -> "QuitGame"
56 | }
57 | }
58 |
59 | }
60 |
61 | enum class GameState {
62 | WAITING, PLAYING, LOST
63 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/game/ThemeConfig.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.game
2 |
3 | import androidx.compose.runtime.MutableState
4 | import com.funny.compose.study.App.Companion.DataSaverUtils
5 | import com.funny.data_saver.core.mutableDataSaverStateOf
6 |
7 |
8 | object ThemeConfig {
9 | val themeList = listOf(
10 | SnakeAssets.SnakeAssets1, SnakeAssets.SnakeAssets2
11 | )
12 | val savedSnakeAssets: MutableState = mutableDataSaverStateOf(DataSaverUtils ,key = "saved_snake_assets", initialValue = SnakeAssets.SnakeAssets1)
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/like_keep/FakeKeep.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.like_keep
2 |
3 | import androidx.compose.foundation.Canvas
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.material.Button
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Alignment
11 | import androidx.compose.ui.Modifier
12 | import androidx.compose.ui.geometry.CornerRadius
13 | import androidx.compose.ui.geometry.Offset
14 | import androidx.compose.ui.geometry.Size
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.graphics.PathEffect
17 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
18 | import androidx.compose.ui.graphics.nativeCanvas
19 | import androidx.compose.ui.graphics.toArgb
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.res.painterResource
22 | import androidx.compose.ui.text.ExperimentalTextApi
23 | import androidx.compose.ui.text.drawText
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import com.funny.compose.study.R
28 | import com.funny.compose.study.ui.post_layout.VerticalScopeInstance.weight
29 | import java.lang.Integer.min
30 | import kotlin.random.Random
31 |
32 | data class ItemData(val content :String, val num : Int)
33 |
34 | /**
35 | * 高仿 Keep 统计页面——周视图
36 | * 不可以动,只有形似
37 | * 作者: FunnySaltyFish
38 | */
39 | @Composable
40 | fun FakeKeep() {
41 | val times = arrayListOf("3/1-3/6","3/7-3/13","3/14-3/20","3/21-3/27","3/28-4/3","4/4-4/10","4/11-4/17","4/18-4/24","4/25-5/1","5/2-5/8","5/9-5/15","5/16-5/22", "本周")
42 | val nums = arrayListOf(0, 0, 134, 119, 90, 93, 142, 101, 145, 114, 131, 125, 138)
43 | // times.map { Random.nextInt(90,150) }
44 | // println(nums)
45 |
46 | val listData = List(times.size) { i ->
47 | ItemData(times[i], nums[i])
48 | }
49 |
50 | var startIndex by remember {
51 | mutableStateOf(0)
52 | }
53 |
54 | val centerData = listData[startIndex + 2]
55 |
56 | Column(modifier = Modifier
57 | .fillMaxSize()
58 | .background(Color.White)) {
59 | Image(painter = painterResource(id = R.drawable.part_tab), contentDescription = "", modifier = Modifier.fillMaxWidth(), contentScale = ContentScale.FillWidth)
60 | Row(
61 | Modifier
62 | .fillMaxWidth()
63 | .padding(horizontal = 16.dp, vertical = 6.dp), horizontalArrangement = Arrangement.SpaceBetween) {
64 | Text(text = "时长", color = Color.Gray, fontSize = 14.sp)
65 | Text(text = centerData.content.replace("/","月").replace("-","日至")+"日", color = Color.Gray, fontSize = 14.sp)
66 | }
67 | Row(
68 | Modifier
69 | .fillMaxWidth()
70 | .padding(horizontal = 16.dp, vertical = 6.dp), verticalAlignment = Alignment.Bottom) {
71 | Text(text = "${centerData.num}", color = Color.Black, fontSize = 48.sp, fontWeight = FontWeight.ExtraBold, modifier = Modifier.alignByBaseline())
72 | Spacer(modifier = Modifier.width(8.dp))
73 | Text(text = "分钟", color = Color.Gray, fontSize = 14.sp, modifier = Modifier.alignByBaseline())
74 | }
75 | Row(
76 | Modifier
77 | .fillMaxWidth()
78 | .padding(horizontal = 16.dp, vertical = 6.dp)
79 | ) {
80 | Column(Modifier.weight(0.3f)) {
81 | Text(text = "消耗(千卡)", color = Color.Gray, fontSize = 12.sp)
82 | Text(text = "${(centerData.num*Random.nextDouble(1.4,1.6)).toInt()}", color = Color.Black, fontSize = 24.sp, fontWeight = FontWeight.W600)
83 | }
84 | val time = Random.nextInt(3,6)
85 | Column(Modifier.weight(0.3f)) {
86 | Text(text = "完成(次)", color = Color.Gray, fontSize = 12.sp)
87 | Text(text = "$time", color = Color.Black, fontSize = 24.sp, fontWeight = FontWeight.W600)
88 | }
89 | Column(Modifier.weight(0.3f)) {
90 | Text(text = "累计(天)", color = Color.Gray, fontSize = 12.sp)
91 | Text(text = "${Random.nextInt(3, time+1)}", color = Color.Black, fontSize = 24.sp, fontWeight = FontWeight.W600)
92 | }
93 | Spacer(modifier = Modifier.weight(0.1f))
94 | }
95 | Statistics(modifier = Modifier
96 | .fillMaxWidth()
97 | .height(270.dp)
98 | .background(Color(250, 250, 250)), listData = listData, startIndex = startIndex)
99 | Spacer(modifier = Modifier.height(50.dp))
100 | Button(onClick = {
101 | startIndex += 1
102 | if (startIndex == listData.size - 2) startIndex = 0
103 | }) {
104 | Text(text = "移动")
105 | }
106 | }
107 |
108 | }
109 |
110 | /**
111 | * 数据统计图
112 | * @param modifier Modifier
113 | * @param listData List 数据
114 | * @param startIndex Int 最左侧的矩阵对应的index
115 | */
116 | @OptIn(ExperimentalTextApi::class)
117 | @Composable
118 | fun Statistics(
119 | modifier: Modifier,
120 | listData: List,
121 | startIndex : Int = 0,
122 | ) {
123 | // 最大的数字,所有矩形以这个为基准计算高度
124 | val maxNum = listData.maxOf { it.num }
125 | // 深浅两种绿色
126 | val lightColor = Color(144, 225, 193)
127 | val highlightColor = Color(36, 198, 138)
128 | // 这个rect用于存放paint测出的文字大小
129 | val rect = remember {
130 | android.graphics.Rect()
131 | }
132 | // 这个paint用于绘制文字
133 | val paint = remember {
134 | android.graphics.Paint().apply {
135 | isAntiAlias = true
136 | color = android.graphics.Color.GRAY
137 | textSize = 32f
138 | }
139 | }
140 |
141 | Canvas(modifier = modifier){
142 | // 画布的宽高
143 | val w = size.width
144 | val h = size.height
145 | // 最高的矩形占的高度(3/6)
146 | val maxH = h / 2
147 | // 举行的宽度
148 | val blockW = w / 12f
149 |
150 | for (i in 0 until min(5, listData.size - startIndex)){
151 | val data = listData[startIndex + i]
152 | if (i != 2){
153 | // 画四个浅色矩形
154 | val blockH = data.num.toFloat() / maxNum * maxH
155 | drawRect(lightColor, Offset(w / 4f * i - blockW / 2, 0.833f * h - blockH), Size(blockW, blockH))
156 | drawLine(Color.LightGray, Offset(w / 4f * i, h * 11 / 12), Offset(w / 4f * i, h * 11 / 12 + 16f), 4f)
157 | // 浅色矩形下方的文字
158 | paint.color = Color.Gray.toArgb()
159 | drawIntoCanvas {
160 | FunnyCanvasUtils.drawCenterText(it.nativeCanvas, data.content, w / 4f * i , h * 7 / 8 + 2f, rect, paint)
161 | }
162 | }
163 | }
164 | // 三条浅色横线
165 | for (i in 2 until 5){
166 | drawLine(Color.Black.copy(alpha = 0.5f), Offset(0f, h * i / 6), Offset(w, h * i / 6), pathEffect = PathEffect.dashPathEffect(
167 | floatArrayOf(10f,10f)))
168 | }
169 | // 深色横线
170 | drawLine(highlightColor, Offset(0f, h * 5 / 6), Offset(w, h * 5 / 6), 4f)
171 | // 中间深色矩形
172 | val blockH = listData[startIndex + 2].num.toFloat() / maxNum * maxH
173 | drawRect(highlightColor, Offset(w / 2 - blockW / 2, 0.833f * h - blockH), Size(blockW, blockH))
174 | drawLine(highlightColor, Offset(w/2, 20f), Offset(w / 2, 0.833f * h - blockH), 4f)
175 | // 显示文本的圆角矩形
176 | drawRoundRect(Color(35,199,136), Offset(w / 2 - blockW * 2 / 3 - 4f, h / 8), Size(blockW * 4f / 3 + 8f, h / 12f), CornerRadius(12f, 12f))
177 | drawLine(highlightColor, Offset(w / 2f, h * 11 / 12), Offset(w / 2f, h - 20f), 4f)
178 |
179 | // 中间底部文字
180 | paint.color = Color.Black.toArgb()
181 | drawIntoCanvas {
182 | FunnyCanvasUtils.drawCenterText(it.nativeCanvas, listData[startIndex+2].content, w / 2 , h * 7 / 8 + 2f, rect, paint)
183 | }
184 |
185 | // 圆角矩形内部文字
186 | paint.color = Color.White.toArgb()
187 | drawIntoCanvas {
188 | FunnyCanvasUtils.drawCenterText(it.nativeCanvas, "${listData[startIndex+2].num} 分钟", w / 2, h / 8 + h / 24f, rect, paint)
189 | }
190 | }
191 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/like_keep/FunnyCanvasUtils.java:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.like_keep;
2 | import android.graphics.Rect;
3 | import android.graphics.Paint;
4 | import android.graphics.Canvas;
5 |
6 | public class FunnyCanvasUtils
7 | {
8 | public static void drawCenterText(Canvas canvas,String text,float x,float y,Rect rect,Paint paint){
9 | paint.getTextBounds(text,0,text.length(),rect);
10 | Paint.FontMetrics fontMetrics=paint.getFontMetrics();
11 | float distance=(fontMetrics.bottom - fontMetrics.top)/2 - fontMetrics.bottom;
12 | float baseline=y+distance;
13 | canvas.drawText(text,x-rect.width()/2f,baseline,paint);
14 | }
15 |
16 | public static void drawCenterText(Canvas canvas,int text,float x,float y,Rect rect,Paint paint){
17 | drawCenterText(canvas,""+text,x,y,rect,paint);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/markdowntest/MarkdownTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.markdowntest
2 |
3 | import androidx.compose.runtime.Composable
4 | import dev.jeziellago.compose.markdowntext.MarkdownText
5 |
6 | @Composable
7 | fun MarkdownTest() {
8 | val text = """
9 | 小字体
10 | see how to change output with twemoji.
\n
11 | This is HTML abbreviation example.
12 | Test
13 | null
14 | Nǐ hǎo
15 |
16 | 词性:感叹词
17 | 相似词汇:
18 | Hello! 你好!;喂!;
19 | Hi! 嗨!;你好!;
20 | Hallo! 你好!;
21 |
22 |
23 | | 源 | 片假名 | 平假名 | 罗马字 |
24 | |:---|:---|:---|:---|
25 | | 理解 | リカイ | りかい | rikai |
26 | | できない | デキナイ | できない | dekinai |
27 |
28 |
29 | 别不信
30 |
31 | """.trimIndent()
32 | MarkdownText(markdown = text)
33 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/nav/NavigationTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.nav
2 |
3 | import androidx.compose.animation.ExperimentalAnimationApi
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.foundation.lazy.LazyListState
8 | import androidx.compose.material.Text
9 | import androidx.compose.material.TextButton
10 | import androidx.compose.runtime.*
11 | import androidx.compose.runtime.saveable.rememberSaveable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.dp
15 | import androidx.navigation.NavHostController
16 | import com.google.accompanist.navigation.animation.AnimatedNavHost
17 | import com.google.accompanist.navigation.animation.composable
18 | import com.google.accompanist.navigation.animation.rememberAnimatedNavController
19 |
20 | // 这个文件在研究 Navigation 库的 saveState 和 restoreState
21 | // 它让页面切走在切回来时能够记住页面的数据
22 | // 前提是使用 rememberSaveable
23 | @OptIn(ExperimentalAnimationApi::class)
24 | @Composable
25 | fun NavigationTest() {
26 | val navController = rememberAnimatedNavController()
27 | AnimatedNavHost(navController = navController, startDestination ="screen1"){
28 | composable("screen1") { Screen1(navController) }
29 | composable("screen2") { Screen2(navController) }
30 | composable("screen3") { Screen3(navController) }
31 | }
32 | }
33 |
34 | @Composable
35 | fun Screen1(navHostController: NavHostController) {
36 | Row(Modifier.fillMaxSize(), horizontalArrangement = Arrangement.SpaceAround, verticalAlignment = Alignment.CenterVertically) {
37 | TextButton(onClick = {
38 | navHostController.navigateSingleTop("screen2")
39 | }) {
40 | Text("Screen 2")
41 | }
42 |
43 | TextButton(onClick = {
44 | navHostController.navigateSingleTop("screen3")
45 | }) {
46 | Text("Screen 3")
47 | }
48 | }
49 | }
50 |
51 | @Composable
52 | fun Screen2(navHostController: NavHostController) {
53 | var number by rememberSaveable { mutableStateOf(0) }
54 | Column(Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
55 | TextButton(onClick = {
56 | number++
57 | }) {
58 | Text("Increment")
59 | }
60 | Text("Number: $number")
61 |
62 | TextButton(onClick = {
63 | navHostController.navigateSingleTop("screen1")
64 | }) {
65 | Text(text = "To Screen1")
66 | }
67 | }
68 | }
69 |
70 | @Composable
71 | fun Screen3(navHostController: NavHostController) {
72 | val lazyListState = rememberSaveable(saver = LazyListState.Saver) {
73 | LazyListState()
74 | }
75 |
76 | LazyColumn(
77 | Modifier
78 | .fillMaxSize()
79 | .padding(8.dp), state = lazyListState) {
80 | items(100) {
81 | Text("Item $it, Click Me To Go To Screen1", Modifier.fillMaxWidth().clickable {
82 | navHostController.navigateSingleTop("screen1")
83 | }.padding(8.dp))
84 | }
85 | }
86 | }
87 |
88 | fun NavHostController.navigateSingleTop(route: String, popUpToMain: Boolean = true){
89 | val navController = this
90 | navController.navigate(route) {
91 | //当底部导航导航到在非首页的页面时,执行手机的返回键 回到首页
92 | if (popUpToMain) {
93 | popUpTo(navController.graph.startDestinationId) {
94 | saveState = true
95 | //currentScreen = TranslateScreen.MainScreen
96 | }
97 | }
98 | //从名字就能看出来 跟activity的启动模式中的SingleTop模式一样 避免在栈顶创建多个实例
99 | launchSingleTop = true
100 | //切换状态的时候保存页面状态
101 | restoreState = true
102 | }
103 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/others/RememberTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.others
2 |
3 | import android.util.Log
4 | import androidx.compose.material.Text
5 | import androidx.compose.runtime.*
6 | import kotlinx.coroutines.delay
7 |
8 | // 回复文章 https://juejin.cn/post/7194816465290133560
9 | // 全部回复:
10 | /**
11 | * 两点个人想说的:
12 | 1. “我们需要频繁展示相同的数据,如果使用Text() 直接进行展示,就会每次就会重新计算”,这句话是不正确的,就你这个例子来看,你的 `ShowCharLength` 这个 Composable 是 restartable 且 skippable 的 (参考 juejin.cn),且唯一的参数是 Stable 的。也就是,如果 value 的内容不变,这个 Composable 会被完全跳过重组,你可以运行 github.com 自行查看输出以验证
13 | 2. 如果某值依赖于其他值,且频繁变化(速度快过 UI ),那么可以使用 derivedStateOf 避免不必要的重组,这也是官方给出的建议 (见视频:Jetpack Compose 性能提升最佳实践 www.youtube.com)
14 | Compose 想学深并不是一件容易的事,望共同进步
15 | */
16 | @Composable
17 | fun RememberTest() {
18 | var name by remember { mutableStateOf("hello compose") }
19 | LaunchedEffect(key1 = Unit){
20 | while (true){
21 | name = "hello compose"
22 | delay(1000)
23 | }
24 | }
25 | ShowCharLength(name)
26 | }
27 |
28 | @Composable
29 | fun ShowCharLength(value: String) {
30 | LaunchedEffect(key1 = Unit){
31 | Log.d("ShowCharLength", "Composition Happens ")
32 | }
33 | Text(value)
34 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/others/SimpleNavigationWithSaveableStateSample.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.saveable
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.shape.RoundedCornerShape
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.TextButton
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.getValue
16 | import androidx.compose.runtime.mutableStateOf
17 | import androidx.compose.runtime.saveable.rememberSaveable
18 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder
19 | import androidx.compose.runtime.setValue
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.unit.dp
23 |
24 | @Composable
25 | fun SimpleNavigationWithSaveableStateSample() {
26 | @Composable
27 | fun Navigation(
28 | currentScreen: T,
29 | modifier: Modifier = Modifier,
30 | content: @Composable (T) -> Unit
31 | ) {
32 | // create SaveableStateHolder.
33 | val saveableStateHolder = rememberSaveableStateHolder()
34 | Box(modifier) {
35 | // Wrap the content representing the `currentScreen` inside `SaveableStateProvider`.
36 | // Here you can also add a screen switch animation like Crossfade where during the
37 | // animation multiple screens will be displayed at the same time.
38 | saveableStateHolder.SaveableStateProvider(currentScreen) {
39 | content(currentScreen)
40 | }
41 | }
42 | }
43 |
44 | Column {
45 | var screen by rememberSaveable { mutableStateOf("screen1") }
46 | Row(horizontalArrangement = Arrangement.SpaceEvenly) {
47 | TextButton(onClick = { screen = "screen1" }) {
48 | Text("Go to screen1")
49 | }
50 | TextButton(onClick = { screen = "screen2" }) {
51 | Text("Go to screen2")
52 | }
53 | }
54 | Navigation(screen, Modifier.fillMaxSize()) { currentScreen ->
55 | if (currentScreen == "screen1") {
56 | Screen1()
57 | } else {
58 | Screen2()
59 | }
60 | }
61 | }
62 | }
63 |
64 | @Composable
65 | fun Screen1() {
66 | var counter by rememberSaveable { mutableStateOf(0) }
67 | TextButton(onClick = { counter++ }) {
68 | Text("Counter=$counter on Screen1")
69 | }
70 | }
71 |
72 | @Composable
73 | fun Screen2() {
74 | Text("Screen2")
75 | }
76 |
77 | @Composable
78 | fun Button(modifier: Modifier = Modifier, onClick: () -> Unit, content: @Composable () -> Unit) {
79 | Box(
80 | modifier
81 | .clickable(onClick = onClick)
82 | .background(Color(0xFF6200EE), RoundedCornerShape(4.dp))
83 | .padding(horizontal = 16.dp, vertical = 8.dp)
84 | ) {
85 | content()
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/pager/PagerTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.pager
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.Canvas
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.lazy.items
9 | import androidx.compose.foundation.lazy.rememberLazyListState
10 | import androidx.compose.foundation.pager.VerticalPager
11 | import androidx.compose.foundation.pager.rememberPagerState
12 | import androidx.compose.material.MaterialTheme
13 | import androidx.compose.material.Text
14 | import androidx.compose.runtime.*
15 | import androidx.compose.runtime.saveable.rememberSaveable
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.geometry.center
18 | import androidx.compose.ui.text.ExperimentalTextApi
19 | import androidx.compose.ui.text.drawText
20 | import androidx.compose.ui.text.rememberTextMeasurer
21 | import androidx.compose.ui.unit.dp
22 | import kotlinx.coroutines.launch
23 |
24 | @OptIn(ExperimentalFoundationApi::class)
25 | @Composable
26 | fun VerticalPagerTest() {
27 | val logs = remember { mutableStateListOf() }
28 | val lazyListState = rememberLazyListState()
29 | val scope = rememberCoroutineScope()
30 | val state = rememberPagerState {
31 | 10
32 | }
33 | VerticalPager(state = state, modifier = Modifier.fillMaxSize()) { page ->
34 | RecordSelfText(modifier = Modifier.fillMaxSize(), page = page, updateLog = {
35 | scope.launch {
36 | logs.add(it)
37 | lazyListState.animateScrollToItem(logs.size - 1)
38 | }
39 | })
40 | }
41 | LazyColumn(modifier = Modifier.heightIn(0.dp, 300.dp), state = lazyListState){
42 | items(logs){
43 | Text(text = it)
44 | }
45 | }
46 | }
47 |
48 | @OptIn(ExperimentalTextApi::class)
49 | @Composable
50 | fun RecordSelfText(
51 | modifier: Modifier,
52 | page: Int,
53 | updateLog: (String) -> Unit
54 | ) {
55 | var num by rememberSaveable {
56 | mutableStateOf(0)
57 | }
58 | LaunchedEffect(key1 = Unit){
59 | num += 1
60 | Log.d("RecordSelfText", "Page: $page, num: $num")
61 | updateLog("Page: $page, num: $num")
62 | }
63 | val textMeasurer = rememberTextMeasurer(cacheSize = 8)
64 | val textStyle = MaterialTheme.typography.subtitle2
65 | Canvas(modifier = modifier){
66 | drawText(textMeasurer, "Page: $page, num: $num", style = textStyle, topLeft = size.center)
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/Bound.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import org.jbox2d.dynamics.Body
4 |
5 | /**
6 | * A bound around the edge of the view
7 | */
8 | data class Bound(
9 | val widthInPixels: Float,
10 | val heightInPixels: Float,
11 | val body: Body,
12 | val side: Side
13 | ) {
14 | enum class Side {
15 | TOP,
16 | LEFT,
17 | RIGHT,
18 | BOTTOM
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/LayoutRecomposeTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.runtime.*
5 | import androidx.compose.ui.Modifier
6 | import androidx.compose.ui.layout.Layout
7 | import androidx.compose.ui.layout.Measurable
8 | import androidx.compose.ui.unit.Constraints
9 | import kotlinx.coroutines.delay
10 | data class Point(var x : Int, var y : Int)
11 | private const val TAG = "LayoutRecomposeTest"
12 | @Composable
13 | fun LayoutRecomposeTest(
14 | modifier: Modifier = Modifier,
15 | content: @Composable () -> Unit
16 | ) {
17 | val positions = remember {
18 | mutableStateListOf(
19 | Point(0,0), Point(50,50)
20 | )
21 | }
22 |
23 | var yy by remember {
24 | mutableStateOf(0)
25 | }
26 |
27 | LaunchedEffect(key1 = Unit){
28 | Log.d(TAG, "LayoutRecomposeTest: Recompose")
29 | while (true){
30 | delay(400)
31 | positions.forEach {
32 | it.x += 1
33 | it.y += 1
34 | yy += 1
35 | // Log.d(TAG, "LayoutRecomposeTest: $it")
36 | }
37 | }
38 | }
39 |
40 | Layout(
41 | modifier = modifier,
42 | content = content
43 | ) { measurables: List, constraints: Constraints ->
44 | val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
45 | val placeables = measurables.map { it.measure(childConstraints) }
46 | // 宽度:最宽的一项
47 | val width = placeables.maxOf { it.width }
48 | // 高度:所有子微件高度之和
49 | val height = placeables.sumOf { it.height }
50 | layout(width, height) {
51 | // yy 为 mutableStateOf 可以
52 | val y = yy
53 | // Log.d(TAG, "LayoutRecomposeTest: $y")
54 | placeables.forEachIndexed { i , placeable ->
55 | placeable.placeRelative(0, derivedStateOf { positions[i].y }.value)
56 | }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/Physics.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import android.graphics.Color
4 | import android.graphics.Paint
5 | import android.util.Log
6 | import android.view.MotionEvent
7 | import android.view.View
8 | import org.jbox2d.callbacks.ContactImpulse
9 | import org.jbox2d.callbacks.ContactListener
10 | import org.jbox2d.collision.Manifold
11 | import org.jbox2d.collision.shapes.CircleShape
12 | import org.jbox2d.collision.shapes.PolygonShape
13 | import org.jbox2d.common.Vec2
14 | import org.jbox2d.dynamics.*
15 | import org.jbox2d.dynamics.contacts.Contact
16 | import java.util.*
17 | import kotlin.collections.ArrayList
18 | import kotlin.math.max
19 | import kotlin.properties.Delegates
20 |
21 | class ComposableGroup(var physicsParentDatas: ArrayList){
22 | val childCount : Int
23 | get() = physicsParentDatas.size
24 | fun getChildAt(index : Int) = physicsParentDatas[index]
25 | }
26 |
27 | /**
28 | * Implementation for physics layout is found here, since we want to offer the main
29 | * layouts without requiring further extension (LinearLayout, RelativeLayout, etc.)
30 | */
31 | @Suppress("unused", "MemberVisibilityCanBePrivate")
32 | class Physics constructor(val composableGroup: ComposableGroup) {
33 |
34 | companion object {
35 | private val TAG = Physics::class.java.simpleName
36 | const val NO_GRAVITY = 0.0f
37 | const val MOON_GRAVITY = 1.6f
38 | const val EARTH_GRAVITY = 9.8f
39 | const val JUPITER_GRAVITY = 24.8f
40 |
41 | // Size in DP of the bounds (world walls) of the view
42 | private const val BOUND_SIZE_DP = 20
43 | private const val FRAME_RATE = 1 / 60f
44 |
45 | const val ID_BOUND_TOP = 0
46 | const val ID_BOUND_LEFT = 1
47 | const val ID_BOUND_RIGHT = 2
48 | const val ID_BOUND_BOTTOM = 3
49 |
50 | }
51 |
52 | private val debugDraw = false
53 | private val debugLog = false
54 |
55 | /**
56 | * Set the number of velocity iterations the world will perform at each step.
57 | * Default is 8
58 | */
59 | var velocityIterations = 8
60 |
61 | /**
62 | * Set the number of position iterations the world will perform at each step.
63 | * Default is 3
64 | */
65 | var positionIterations = 3
66 |
67 | /**
68 | * Set the number of pixels per meter. Basically makes the world feel bigger or smaller
69 | * Default is 20dp. More pixels per meter = ui feeling bigger in the world (faster movement)
70 | */
71 | var pixelsPerMeter = 0f
72 |
73 | /**
74 | * Get the current Box2D [World] controlling the physics of this view
75 | */
76 | var world: World? = null
77 | private set
78 |
79 | /**
80 | * Enable/disable physics on the view
81 | */
82 | var isPhysicsEnabled = true
83 |
84 | /**
85 | * Enable/disable fling for this View
86 | */
87 | var isFlingEnabled = false
88 |
89 | /**
90 | * Enables/disables if the view has bounds or not
91 | */
92 | var hasBounds = true
93 |
94 | var boundsSizeInPixel = 1f
95 | val bounds = mutableListOf()
96 | private var gravityX = 0.0f
97 | private var gravityY = EARTH_GRAVITY
98 |
99 | private val debugPaint: Paint by lazy {
100 | val paint = Paint()
101 | paint.color = Color.MAGENTA
102 | paint.style = Paint.Style.STROKE
103 | paint
104 | }
105 |
106 | var density by Delegates.notNull()
107 | var width = 0
108 | var height = 0
109 |
110 | private var viewBeingDragged: View? = null
111 | private var onFlingListener: OnFlingListener? = null
112 | private var onCollisionListener: OnCollisionListener? = null
113 | private var onPhysicsProcessedListeners = mutableListOf()
114 |
115 | private val contactListener: ContactListener = object : ContactListener {
116 | override fun beginContact(contact: Contact) {
117 | if (onCollisionListener != null) {
118 | onCollisionListener!!.onCollisionEntered(contact.fixtureA.m_userData as Int,
119 | contact.fixtureB.m_userData as Int)
120 | }
121 | }
122 |
123 | override fun endContact(contact: Contact) {
124 | if (onCollisionListener != null) {
125 | onCollisionListener!!.onCollisionExited(contact.fixtureA.m_userData as Int,
126 | contact.fixtureB.m_userData as Int)
127 | }
128 | }
129 |
130 | override fun preSolve(contact: Contact, oldManifold: Manifold) {}
131 | override fun postSolve(contact: Contact, impulse: ContactImpulse) {}
132 | }
133 |
134 | fun step(){
135 | world?.step(FRAME_RATE, velocityIterations, positionIterations)
136 | }
137 |
138 | fun metersToPixels(meters: Float): Float {
139 | return meters * pixelsPerMeter
140 | }
141 |
142 | fun pixelsToMeters(pixels: Float): Float {
143 | return pixels / pixelsPerMeter
144 | }
145 |
146 | private fun radiansToDegrees(radians: Float): Float {
147 | return radians / 3.14f * 180f
148 | }
149 |
150 | private fun degreesToRadians(degrees: Float): Float {
151 | return degrees / 180f * 3.14f
152 | }
153 |
154 | /**
155 | * Call this every time your view gets a call to onSizeChanged so that the world can
156 | * respond to this change.
157 | */
158 | fun setSize(width: Int, height: Int) {
159 | this.width = width
160 | this.height = height
161 | }
162 |
163 | /**
164 | * Call this in your ViewGroup if you plan on using fling
165 | * @return true if consumed, false otherwise
166 | */
167 | fun onTouchEvent(ev: MotionEvent): Boolean {
168 | if (!isFlingEnabled) {
169 | return false
170 | }
171 | return true
172 | }
173 |
174 | /**
175 | * Recreate the physics world. Will traverse all views in the hierarchy, get their current
176 | * PhysicsConfigs and create a body in the world. This will override the current world if it exists.
177 | */
178 | fun createWorld(onBodyCreatedListener : (Body, Int) -> Unit) {
179 | // Null out all the bodies
180 | val oldBodiesArray = ArrayList()
181 | for (i in 0 until composableGroup.childCount) {
182 | val body = composableGroup.getChildAt(i).body
183 | oldBodiesArray.add(body)
184 | composableGroup.getChildAt(i).body = null
185 | }
186 | bounds.clear()
187 | if (debugLog) {
188 | Log.d(TAG, "createWorld")
189 | }
190 | world = World(Vec2(gravityX, gravityY)).apply {
191 | setContactListener(contactListener)
192 | }
193 | if (hasBounds) {
194 | enableBounds()
195 | }
196 | for (i in 0 until composableGroup.childCount) {
197 | val body = createBody(composableGroup.getChildAt(i), oldBodiesArray[i])
198 | onBodyCreatedListener(body, i)
199 | }
200 | }
201 |
202 | private fun enableBounds() {
203 | hasBounds = true
204 | createBounds()
205 | }
206 |
207 | private fun disableBounds() {
208 | hasBounds = false
209 | for (body in bounds) {
210 | world?.destroyBody(body.body)
211 | }
212 | bounds.clear()
213 | }
214 |
215 | private fun createBounds() {
216 | val top = createBound(
217 | widthInPixels = width.toFloat(),
218 | heightInPixels = boundsSizeInPixel,
219 | id = ID_BOUND_TOP,
220 | side = Bound.Side.TOP
221 | )
222 | bounds.add(top)
223 |
224 | val bottom = createBound(
225 | widthInPixels = width.toFloat(),
226 | heightInPixels = boundsSizeInPixel,
227 | id = ID_BOUND_BOTTOM,
228 | side = Bound.Side.BOTTOM
229 | )
230 | bounds.add(bottom)
231 |
232 | val left = createBound(
233 | widthInPixels = boundsSizeInPixel,
234 | heightInPixels = height.toFloat(),
235 | id = ID_BOUND_LEFT,
236 | side = Bound.Side.LEFT
237 | )
238 | bounds.add(left)
239 |
240 | val right = createBound(
241 | widthInPixels = boundsSizeInPixel,
242 | heightInPixels = height.toFloat(),
243 | id = ID_BOUND_RIGHT,
244 | side = Bound.Side.RIGHT
245 | )
246 | bounds.add(right)
247 | }
248 |
249 | private fun createBound(widthInPixels: Float, heightInPixels: Float, id: Int, side: Bound.Side): Bound {
250 | val bodyDef = BodyDef()
251 | bodyDef.type = BodyType.STATIC
252 | val box = PolygonShape()
253 | val boxWidthMeters = pixelsToMeters(widthInPixels)
254 | val boxHeightMeters = pixelsToMeters(heightInPixels)
255 | box.setAsBox(boxWidthMeters / 2, boxHeightMeters / 2)
256 | val fixtureDef = createBoundFixtureDef(box, id)
257 | val pair = when (side) {
258 | Bound.Side.TOP -> Pair(boxWidthMeters / 2, boxHeightMeters / 2)
259 | Bound.Side.BOTTOM -> Pair(boxWidthMeters / 2, pixelsToMeters(height.toFloat()) - boxHeightMeters / 2)
260 | Bound.Side.LEFT -> Pair(boxWidthMeters / 2, pixelsToMeters(height.toFloat()) / 2)
261 | Bound.Side.RIGHT -> Pair(pixelsToMeters(width.toFloat()) - boxWidthMeters / 2, pixelsToMeters(height.toFloat()) / 2)
262 | }
263 | bodyDef.position = Vec2(pair.first, pair.second)
264 | val body = world!!.createBody(bodyDef)
265 | body.createFixture(fixtureDef)
266 | return Bound(
267 | widthInPixels = widthInPixels,
268 | heightInPixels = heightInPixels,
269 | body = body,
270 | side = side
271 | )
272 | }
273 |
274 | private fun createBoundFixtureDef(box: PolygonShape, id: Int): FixtureDef {
275 | val fixtureDef = FixtureDef()
276 | fixtureDef.shape = box
277 | fixtureDef.density = 0.5f
278 | fixtureDef.friction = 0.3f
279 | fixtureDef.restitution = 0.5f
280 | fixtureDef.userData = id
281 | return fixtureDef
282 | }
283 |
284 | private fun createBody(physicsParentData: PhysicsParentData, oldBody: Body?): Body {
285 | val config = physicsParentData.physicsConfig
286 | val bodyDef = config.bodyDef
287 | bodyDef.position[pixelsToMeters(physicsParentData.initialX + physicsParentData.width / 2)] = pixelsToMeters(physicsParentData.initialY + physicsParentData.height / 2)
288 |
289 | // Log.d(TAG, "createBody: position: ${bodyDef.position}")
290 |
291 | if (oldBody != null) {
292 | bodyDef.angle = oldBody.angle
293 | bodyDef.angularVelocity = oldBody.angularVelocity
294 | bodyDef.linearVelocity = oldBody.linearVelocity
295 | bodyDef.angularDamping = oldBody.angularDamping
296 | bodyDef.linearDamping = oldBody.linearDamping
297 | } else {
298 | bodyDef.angularVelocity = degreesToRadians(physicsParentData.rotation)
299 | }
300 | val fixtureDef = config.fixtureDef
301 | fixtureDef.shape = if (config.shape == PhysicsShape.RECTANGLE) createBoxShape(physicsParentData) else createCircleShape(physicsParentData, config)
302 | fixtureDef.userData = physicsParentData.id
303 | val body = world!!.createBody(bodyDef)
304 | body.createFixture(fixtureDef)
305 | physicsParentData.body = body
306 | return body
307 | }
308 |
309 | private fun createBoxShape(physicsParentData: PhysicsParentData): PolygonShape {
310 | val box = PolygonShape()
311 | val boxWidth = pixelsToMeters(physicsParentData.width / 2.toFloat())
312 | val boxHeight = pixelsToMeters(physicsParentData.height / 2.toFloat())
313 | box.setAsBox(boxWidth, boxHeight)
314 | return box
315 | }
316 |
317 | private fun createCircleShape(physicsParentData: PhysicsParentData, config: PhysicsConfig): CircleShape {
318 | val circle = CircleShape()
319 | //radius was not set, set it to max of the width and height
320 | if (config.radius == -1f) {
321 | config.radius = max(physicsParentData.width / 2f, physicsParentData.height / 2f)
322 | }
323 | circle.m_radius = pixelsToMeters(config.radius)
324 | return circle
325 | }
326 |
327 | /**
328 | * Gives a random impulse to all the view bodies in the layout. Really just useful for testing,
329 | * but try it out if you want :)
330 | */
331 | fun giveRandomImpulse() {
332 | var body: Body?
333 | var impulse: Vec2
334 | val random = Random()
335 | for (i in 0 until composableGroup.childCount) {
336 | impulse = Vec2((random.nextInt(1000) - 1000).toFloat(), (random.nextInt(1000) - 1000).toFloat())
337 | body = composableGroup.getChildAt(i).body
338 | Log.d(TAG, "giveRandomImpulse: pody's position : ${body?.position}")
339 | body?.applyLinearImpulse(impulse, body.position)
340 | }
341 | }
342 |
343 | private fun translateBodyToView(body: Body, view: View) {
344 | body.setTransform(
345 | Vec2(pixelsToMeters(view.x + view.width / 2),
346 | pixelsToMeters(view.y + view.height / 2)),
347 | body.angle)
348 | }
349 |
350 | /**
351 | * Sets the fling listener
352 | *
353 | * @param onFlingListener listener that will respond to fling events
354 | */
355 | fun setOnFlingListener(onFlingListener: OnFlingListener?) {
356 | this.onFlingListener = onFlingListener
357 | }
358 |
359 | /**
360 | * Sets the collision listener
361 | *
362 | * @param onCollisionListener listener that will listen for collisions
363 | */
364 | fun setOnCollisionListener(onCollisionListener: OnCollisionListener?) {
365 | this.onCollisionListener = onCollisionListener
366 | }
367 |
368 | /**
369 | * Sets the size of the bounds and enables the bounds
370 | *
371 | * @param size the size of the bounds in dp
372 | */
373 | fun setBoundsSize(size: Float) {
374 | boundsSizeInPixel = size * density
375 | if (hasBounds) {
376 | disableBounds()
377 | }
378 | enableBounds()
379 | }
380 |
381 | /**
382 | * Sets the gravity in the x direction for the world. Positive is right, negative is left.
383 | */
384 | fun setGravityX(newGravityX: Float) {
385 | setGravity(newGravityX, gravityY)
386 | }
387 |
388 | /**
389 | * The gravity in the x direction for the world. Positive is right, negative is left.
390 | */
391 | fun getGravityX(): Float {
392 | return gravityX
393 | }
394 |
395 | /**
396 | * Sets the gravity in the y direction for the world. Positive is down, negative is up.
397 | */
398 | fun setGravityY(newGravityY: Float) {
399 | setGravity(gravityX, newGravityY)
400 | }
401 |
402 | /**
403 | * The gravity in the x direction for the world. Positive is right, negative is left.
404 | */
405 | fun getGravityY(): Float {
406 | return gravityY
407 | }
408 |
409 | /**
410 | * Sets the gravity for the world. Positive x is right, negative is left. Positive
411 | * y is down, negative is up.
412 | */
413 | fun setGravity(gravityX: Float, gravityY: Float) {
414 | this.gravityX = gravityX
415 | this.gravityY = gravityY
416 | world?.gravity = Vec2(gravityX, gravityY)
417 | }
418 |
419 | /**
420 | * Returns the gravity of the world. Returns null if the world doesn't exist yet (view hasn't
421 | * called onLayout)
422 | */
423 | fun getGravity(): Vec2? {
424 | return world?.gravity
425 | }
426 |
427 | /**
428 | * Add a physics process listener
429 | */
430 | fun addOnPhysicsProcessedListener(listener: OnPhysicsProcessedListener) {
431 | onPhysicsProcessedListeners.add(listener)
432 | }
433 |
434 | /**
435 | * Remove a physics process listener
436 | */
437 | fun removeOnPhysicsProcessedListener(listener: OnPhysicsProcessedListener?) {
438 | onPhysicsProcessedListeners.remove(listener)
439 | }
440 |
441 | /**
442 | * Interface that allows hooks into the layout so that you can process or modify physics bodies each time that JBox2D processes physics
443 | */
444 | interface OnPhysicsProcessedListener {
445 |
446 | /**
447 | * Physics has been processed. Commence doing things that you want to do such as applying additional forces
448 | * @param physics the [Physics] that belongs to the view
449 | * @param world the Box2d world
450 | */
451 | fun onPhysicsProcessed(physics: Physics, world: World)
452 | }
453 |
454 | /**
455 | * A controller that will receive the drag events.
456 | */
457 | interface OnFlingListener {
458 | fun onGrabbed(grabbedView: View?)
459 | fun onReleased(releasedView: View?)
460 | }
461 |
462 | /**
463 | * Alerts you to collisions between views within the layout
464 | */
465 | interface OnCollisionListener {
466 | /**
467 | * Called when a collision is entered between two bodies. ViewId can also be
468 | * R.id.physics_layout_bound_top,
469 | * R.id.physics_layout_bound_bottom, R.id.physics_layout_bound_left, or
470 | * R.id.physics_layout_bound_right.
471 | * If view was not assigned an id, the return value will be [View.NO_ID].
472 | *
473 | * @param viewIdA view id of body A
474 | * @param viewIdB view id of body B
475 | */
476 | fun onCollisionEntered(viewIdA: Int, viewIdB: Int)
477 |
478 | /**
479 | * Called when a collision is exited between two bodies. ViewId can also be
480 | * R.id.physics_layout_bound_top,
481 | * R.id.physics_layout_bound_bottom, R.id.physics_layout_bound_left, or
482 | * R.id.physics_layout_bound_right.
483 | * If view was not assigned an id, the return value will be [View.NO_ID].
484 | *
485 | * @param viewIdA view id of body A
486 | * @param viewIdB view id of body B
487 | */
488 | fun onCollisionExited(viewIdA: Int, viewIdB: Int)
489 | }
490 | }
491 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/PhysicsConfig.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import org.jbox2d.dynamics.BodyDef
4 | import org.jbox2d.dynamics.BodyType
5 | import org.jbox2d.dynamics.FixtureDef
6 |
7 | enum class PhysicsShape {
8 | RECTANGLE,
9 | CIRCLE
10 | }
11 |
12 | /**
13 | * Configuration used when creating the [org.jbox2d.dynamics.Body] for each of the views in the view group
14 | */
15 | data class PhysicsConfig(
16 | /**
17 | * The shape of the physics body, either rectangle or circle. This changes how the body will
18 | * collide with other bodies.
19 | */
20 | var shape: PhysicsShape = PhysicsShape.RECTANGLE,
21 | /**
22 | * The fixture definition. Leave alone if you want the defaults. Learn more: [FixtureDef]
23 | */
24 | var fixtureDef: FixtureDef = createDefaultFixtureDef(),
25 | /**
26 | * The body definition. Leave alone if you want the defaults. Learn more: [BodyDef]
27 | */
28 | var bodyDef: BodyDef = createDefaultBodyDef()
29 | ) {
30 |
31 | /**
32 | * Only used if shape == CIRCLE, otherwise it is ignored. The radius of the circle in pixels.
33 | * Will be processed and set by its view size.
34 | */
35 | internal var radius: Float = -1f
36 |
37 | companion object {
38 |
39 | fun createDefaultFixtureDef(): FixtureDef {
40 | val fixtureDef = FixtureDef()
41 | fixtureDef.friction = 0.3f
42 | fixtureDef.restitution = 0.2f
43 | fixtureDef.density = 0.2f
44 | return fixtureDef
45 | }
46 |
47 | fun createDefaultBodyDef(): BodyDef {
48 | val bodyDef = BodyDef()
49 | bodyDef.type = BodyType.DYNAMIC //movable by default
50 | return bodyDef
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/PhysicsLayout.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.layout.*
7 | import androidx.compose.foundation.lazy.LazyColumn
8 | import androidx.compose.foundation.shape.CircleShape
9 | import androidx.compose.material.Card
10 | import androidx.compose.material.Checkbox
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.*
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.draw.drawWithContent
16 | import androidx.compose.ui.geometry.Offset
17 | import androidx.compose.ui.geometry.Size
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.graphics.graphicsLayer
20 | import androidx.compose.ui.layout.*
21 | import androidx.compose.ui.platform.LocalDensity
22 | import androidx.compose.ui.res.painterResource
23 | import androidx.compose.ui.text.font.FontWeight.Companion.W500
24 | import androidx.compose.ui.unit.IntOffset
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import com.funny.cmaterialcolors.MaterialColors
28 | import com.funny.compose.study.R
29 | import com.funny.compose.study.ui.post_lazygrid.RandomColorBox
30 | import kotlinx.coroutines.delay
31 | import org.jbox2d.common.Vec2
32 |
33 | // 使用 Jetpack Compose 实现物理引擎布局
34 | // 代码已经挪到:https://github.com/FunnySaltyFish/JetpackComposePhysicsLayout
35 |
36 | private const val TAG = "PhysicsLayout"
37 |
38 | interface PhysicsLayoutScope {
39 | @Stable
40 | fun Modifier.physics(physicsConfig: PhysicsConfig, initialX : Float = 0f, initialY : Float = 0f) : Modifier
41 | }
42 |
43 | object PhysicsLayoutScopeInstance : PhysicsLayoutScope {
44 | @Stable
45 | override fun Modifier.physics(
46 | physicsConfig: PhysicsConfig,
47 | initialX: Float,
48 | initialY: Float
49 | ): Modifier = this.then(PhysicsParentData(physicsConfig, initialX, initialY))
50 | }
51 |
52 | data class PhysicsLayoutState(val physics: Physics = Physics(ComposableGroup(arrayListOf())))
53 |
54 | @Composable
55 | fun PhysicsLayout(
56 | modifier: Modifier = Modifier,
57 | physicsLayoutState: PhysicsLayoutState = remember { PhysicsLayoutState() },
58 | boundColor: Color? = MaterialColors.Blue600,
59 | boundSize : Float? = 20f,
60 | gravity : Vec2 = Vec2(0f, 9.8f),
61 | content : @Composable PhysicsLayoutScope.()->Unit
62 | ){
63 | val parentDataList = physicsLayoutState.physics.composableGroup.physicsParentDatas
64 | val physics = physicsLayoutState.physics
65 | val density = LocalDensity.current
66 | var initialized by remember {
67 | mutableStateOf(false)
68 | }
69 |
70 | LaunchedEffect(key1 = density){
71 | physics.density = density.density
72 | physics.pixelsPerMeter = with(density){
73 | 10.dp.toPx()
74 | }
75 | }
76 |
77 | var recompose by remember {
78 | mutableStateOf(0)
79 | }
80 |
81 | LaunchedEffect(initialized, gravity, boundSize){
82 | Log.d(TAG, "PhysicsLayout: launchedEffect ${parentDataList.size} ${physics.width}")
83 | if (!initialized) return@LaunchedEffect
84 | if (parentDataList.isEmpty()) return@LaunchedEffect
85 | if (physics.width * physics.height == 0) return@LaunchedEffect
86 | physics.createWorld { body, i ->
87 | parentDataList[i].body = body
88 | // Log.d(TAG, "PhysicsLayout: createBody: $body")
89 | }
90 | physics.world?.gravity = gravity
91 | if (boundSize != null && boundSize > 0){
92 | physics.setBoundsSize(boundSize)
93 | }
94 | physics.giveRandomImpulse()
95 | }
96 |
97 | LaunchedEffect(key1 = Unit){
98 | while (true){
99 | delay(16)
100 | physics.step() // 模拟 16ms
101 | // Log.d(TAG, "PhysicsLayout: ${physics.world?.bodyList?.position}")
102 | recompose++
103 | }
104 | }
105 |
106 | val drawBoundModifier =
107 | Modifier.drawWithContent {
108 | if (physics.hasBounds && boundColor != null){
109 | // 绘制 bound
110 | val s = physics.boundsSizeInPixel
111 | val w = physics.width.toFloat()
112 | val h = physics.height.toFloat()
113 | drawRect(boundColor, Offset.Zero, Size(w,s))
114 | drawRect(boundColor, Offset(0f, h-s), Size(w,s))
115 | drawRect(boundColor, Offset(0f, s), Size(s, h - 2 * s))
116 | drawRect(boundColor, Offset(w - s, s), Size(s, h - 2 * s))
117 | }
118 | drawContent()
119 | }
120 |
121 | Layout(content = { PhysicsLayoutScopeInstance.content() }, modifier = modifier.then(drawBoundModifier)){ measurables, constraints ->
122 | if (!initialized) {
123 | physics.setSize(constraints.maxWidth, constraints.maxHeight)
124 | }
125 |
126 | val childConstraints = constraints.copy(minWidth = 0, minHeight = 0)
127 | val placeables = measurables.mapIndexed { index, measurable ->
128 | val physicsParentData = (measurable.parentData as? PhysicsParentData) ?: PhysicsParentData(initialX = 100f)
129 | // Log.d(TAG, "PhysicsLayout: init : $initialized")
130 | if (!initialized){
131 | parentDataList.add(index, physicsParentData)
132 | // Log.d(TAG, "PhysicsLayout: addParentData: (${physicsParentData.initialX}, ${physicsParentData.initialY})")
133 | }
134 | measurable.measure(childConstraints)
135 | }
136 |
137 | layout(constraints.maxWidth, constraints.maxHeight){
138 | placeables.forEachIndexed { i, placeable: Placeable ->
139 | // 正确设置各body大小
140 | parentDataList[i].width = placeable.width
141 | parentDataList[i].height = placeable.height
142 |
143 | val x = physics.metersToPixels(parentDataList[i].x).toInt() - placeable.width / 2
144 | val y = physics.metersToPixels(parentDataList[i].y).toInt() - placeable.height / 2
145 |
146 | val c = recompose // 这行代码什么用也没有,目的是触发重新 Layout
147 |
148 | // Log.d(TAG, "PhysicsLayout: $i -> x : $x y : $y")
149 | // Log.d(TAG, "PhysicsLayout: $recompose")
150 | // placeable.place(x, y)
151 | placeable.placeWithLayer(IntOffset(x,y), zIndex = 0f, layerBlock = {
152 | rotationZ = parentDataList[i].rotation
153 | })
154 | }
155 | }.also {
156 | // 各类初始化只进行一次即可
157 | if (!initialized) {
158 | initialized = true
159 | }
160 | }
161 | }
162 | }
163 |
164 |
165 | val physicsConfig = PhysicsConfig()
166 | @Composable
167 | fun PhysicsLayoutTest() {
168 | PhysicsLayout(
169 | Modifier
170 | .fillMaxSize()
171 | .padding(12.dp)
172 | .background(Color.LightGray)) {
173 | RandomColorBox(modifier = Modifier
174 | .size(40.dp)
175 | .physics(physicsConfig, initialX = 300f, initialY = 500f))
176 | RandomColorBox(modifier = Modifier
177 | .clip(CircleShape)
178 | .size(50.dp)
179 | .physics(physicsConfig.copy(shape = PhysicsShape.CIRCLE), 300f, 1000f))
180 | RandomColorBox(modifier = Modifier
181 | .size(60.dp)
182 | .physics(physicsConfig))
183 | var checked by remember {
184 | mutableStateOf(false)
185 | }
186 | Checkbox(checked = checked, onCheckedChange = { checked = it })
187 | Card(modifier = Modifier
188 | .clip(CircleShape)
189 | .physics(physicsConfig.copy(shape = PhysicsShape.CIRCLE), initialX = 200f)) {
190 | Image(painter = painterResource(id = R.drawable.bg_avator), contentDescription = "", modifier = Modifier.size(100.dp))
191 | }
192 | LazyColumn(modifier = Modifier
193 | .height(100.dp)
194 | .background(MaterialColors.Orange200)
195 | .physics(physicsConfig, initialY = 300f)){
196 | items(10){
197 | Text(text = "FunnySaltyFish", modifier = Modifier.padding(8.dp), fontWeight = W500, fontSize = 18.sp)
198 | }
199 | }
200 | }
201 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/physics_layout/PhysicsParentData.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.physics_layout
2 |
3 | import androidx.compose.ui.layout.ParentDataModifier
4 | import androidx.compose.ui.unit.Density
5 | import org.jbox2d.dynamics.Body
6 |
7 | class PhysicsParentData(
8 | var physicsConfig: PhysicsConfig = PhysicsConfig(),
9 | var initialX: Float = 0f,
10 | var initialY: Float = 0f,
11 | var width: Int = 0,
12 | var height: Int = 0
13 | ) : ParentDataModifier {
14 | override fun Density.modifyParentData(parentData: Any?): Any = this@PhysicsParentData
15 |
16 | var body : Body? = null
17 |
18 | val rotation
19 | get() = body?.angle?.times(180f)?.div(Math.PI)?.toFloat() ?: 0f
20 |
21 | val id = hashCode()
22 | val x : Float
23 | get() = body?.position?.x ?: 0f
24 | val y : Float
25 | get() = body?.position?.y ?: 0f
26 |
27 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/CustomLayoutTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.material.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.runtime.saveable.Saver
9 | import androidx.compose.runtime.saveable.rememberSaveable
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.graphics.Color
12 | import androidx.compose.ui.graphics.toArgb
13 | import androidx.compose.ui.unit.dp
14 | import androidx.compose.ui.unit.sp
15 | import com.funny.cmaterialcolors.MaterialColors
16 | import kotlin.random.Random
17 |
18 | fun randomColor() = Color(Random.nextInt(255),Random.nextInt(255),Random.nextInt(255))
19 |
20 | @Composable
21 | fun rememberRandomColor() = rememberSaveable(
22 | saver = Saver(
23 | save = {
24 | value -> value.toArgb()
25 | },
26 | restore = {
27 | Color(it)
28 | }
29 | )
30 | ) {
31 | randomColor()
32 | }
33 |
34 | @Composable
35 | fun CustomLayoutTest() {
36 | // VerticalLayout() {
37 | // (1..5).forEach { _ ->
38 | // Box(modifier = Modifier.size(40.dp).background(randomColor()))
39 | // }
40 | // }
41 | // WaterfallFlowLayout(
42 | // columns = 3
43 | // ) {
44 | // (1..10).forEach { _ ->
45 | // Box(modifier = Modifier.height(Random.nextInt(50, 100).dp).background(randomColor()))
46 | // }
47 | // }
48 | val text = arrayOf("Funny","Salty","Fish","is","Very","Salty")
49 | VerticalLayoutWithIntrinsic(
50 | Modifier
51 | .width(IntrinsicSize.Min)
52 | .padding(12.dp)
53 | .background(MaterialColors.Yellow100)) {
54 | text.forEach {
55 | Text(text = it, fontSize = 24.sp)
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/ModifierTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.foundation.shape.CircleShape
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.LaunchedEffect
9 | import androidx.compose.ui.Alignment
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.clip
12 | import androidx.compose.ui.graphics.Color
13 | import androidx.compose.ui.layout.IntrinsicMeasurable
14 | import androidx.compose.ui.layout.Layout
15 | import androidx.compose.ui.unit.dp
16 |
17 | private const val TAG = "ModifierTest"
18 |
19 | @Composable
20 | fun TraverseModifier() {
21 | val modifier = Modifier
22 | .size(40.dp)
23 | .background(Color.Gray)
24 | .clip(CircleShape)
25 | LaunchedEffect(modifier){
26 | modifier.foldIn(0){ index , element : Modifier.Element ->
27 | Log.d(TAG, "$index -> $element")
28 | index + 1
29 | }
30 | }
31 | Column() {
32 |
33 | }
34 | Box(modifier = modifier)
35 | }
36 |
37 | @Composable
38 | fun ModifierSample1() {
39 | // 父元素
40 | Box(modifier = Modifier
41 | .width(200.dp)
42 | .height(300.dp)
43 | .background(Color.Yellow)){
44 | // 子元素
45 | Box(modifier = Modifier
46 | .fillMaxSize()
47 | .wrapContentSize(align = Alignment.Center)
48 | .size(50.dp)
49 | .background(Color.Blue))
50 | }
51 | }
52 | /**
53 | * 一个修饰符[Modifier],为父布局[Layout]提供数据.
54 | * 可在[Layout]的 measurement 和 positioning 过程中通过 [IntrinsicMeasurable.parentData] 读取到.
55 | * parent data 通常被用于告诉父布局:子微件应该如何测量和定位
56 | */
57 | @Composable
58 | fun ModifierSample2() {
59 | // 父元素
60 | Box(modifier = Modifier
61 | .width(200.dp)
62 | .height(300.dp)
63 | .background(Color.Yellow)){
64 | // 子元素
65 | Box(modifier = Modifier
66 | .align(Alignment.Center)
67 | .size(50.dp)
68 | .background(Color.Blue))
69 | }
70 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/ParentDataTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material.Text
9 | import androidx.compose.runtime.*
10 | import androidx.compose.ui.Modifier
11 | import androidx.compose.ui.draw.drawWithContent
12 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
13 | import androidx.compose.ui.graphics.nativeCanvas
14 | import androidx.compose.ui.layout.Layout
15 | import androidx.compose.ui.layout.Measurable
16 | import androidx.compose.ui.layout.ParentDataModifier
17 | import androidx.compose.ui.unit.Constraints
18 | import androidx.compose.ui.unit.Density
19 | import androidx.compose.ui.unit.dp
20 | import kotlin.random.Random
21 |
22 | private const val TAG = "ParentDataTest"
23 | fun Modifier.count(num: Int) = this
24 | .drawWithContent {
25 | drawIntoCanvas { canvas ->
26 | val paint = android.graphics
27 | .Paint()
28 | .apply {
29 | textSize = 40F
30 | }
31 | canvas.nativeCanvas.drawText(num.toString(), 0F, 40F, paint)
32 | }
33 | // 绘制 Box 自身内容
34 | drawContent()
35 | }
36 | .then(
37 | // 这部分是 父级数据修饰符
38 | CountNumParentData(num)
39 | )
40 |
41 | class CountNumParentData(var countNum: Int) : ParentDataModifier {
42 | override fun Density.modifyParentData(parentData: Any?) = this@CountNumParentData
43 | }
44 |
45 | //interface CountNumScope {
46 | // @Stable
47 | // fun Modifier.count(num : Int) : Modifier
48 | //}
49 | //
50 | //class CountNumScopeImpl : CountNumScope {
51 | // @Stable
52 | // override fun Modifier.count(num: Int): Modifier = this.then(
53 | // CountNumParentData(num)
54 | // )
55 | //}
56 |
57 |
58 | @Composable
59 | fun CountChildrenNumber(
60 | modifier: Modifier = Modifier,
61 | content: @Composable () -> Unit
62 | ) {
63 | var num = 0
64 | Layout(
65 | modifier = modifier,
66 | content = content
67 | ) { measurables: List, constraints: Constraints ->
68 | val placeables = measurables.map {
69 | if (it.parentData is CountNumParentData) {
70 | num += (it.parentData as CountNumParentData).countNum
71 | }
72 | it.measure(constraints.copy(minWidth = 0, minHeight = 0))
73 | }
74 | // 宽度:最宽的一项
75 | val width = placeables.maxOf { it.width }
76 | // 高度:所有子微件高度之和
77 | val height = placeables.sumOf { it.height }
78 | Log.d(TAG, "CountChildrenNumber: 总价格是:$num")
79 | layout(width, height) {
80 | var y = 0
81 | placeables.forEach {
82 | it.placeRelative(0, y)
83 | y += it.height
84 | }
85 | }
86 | }
87 |
88 | }
89 |
90 | @Composable
91 | fun CountNumTest() {
92 | CountChildrenNumber {
93 | repeat(5) {
94 | Box(
95 | modifier = Modifier
96 | .size(40.dp)
97 | .background(randomColor())
98 | .count(Random.nextInt(30, 100))
99 | )
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/SwipeCrossFadeLayout.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.gestures.Orientation
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.lazy.LazyColumn
12 | import androidx.compose.material.ExperimentalMaterialApi
13 | import androidx.compose.material.FractionalThreshold
14 | import androidx.compose.material.SwipeableState
15 | import androidx.compose.material.Text
16 | import androidx.compose.material.rememberSwipeableState
17 | import androidx.compose.material.swipeable
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.remember
22 | import androidx.compose.runtime.rememberCoroutineScope
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.geometry.Offset
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.graphics.TransformOrigin
28 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
29 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource
30 | import androidx.compose.ui.input.nestedscroll.nestedScroll
31 | import androidx.compose.ui.layout.SubcomposeLayout
32 | import androidx.compose.ui.unit.Constraints
33 | import androidx.compose.ui.unit.Velocity
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.lerp
36 | import kotlinx.coroutines.launch
37 |
38 | enum class SwipeLayoutState {
39 | Main, Foreground
40 | }
41 |
42 | private const val TAG = "SwipeCrossFadeLayout"
43 |
44 | @OptIn(ExperimentalMaterialApi::class)
45 | @Composable
46 | fun SwipeCrossFadeLayout(
47 | modifier: Modifier = Modifier,
48 | state: SwipeableState = rememberSwipeableState(initialValue = SwipeLayoutState.Main),
49 | mainUpper: @Composable () -> Unit,
50 | mainLower: @Composable () -> Unit,
51 | foreground: @Composable () -> Unit,
52 | ) {
53 | var mainUpperHeight by remember { mutableStateOf(0) }
54 | var mainLowerHeight by remember { mutableStateOf(1) }
55 | SubcomposeLayout(
56 | modifier = modifier.swipeable(
57 | state,
58 | anchors = mapOf(
59 | 0f to SwipeLayoutState.Main,
60 | mainLowerHeight.toFloat() to SwipeLayoutState.Foreground
61 | ),
62 | thresholds = { _, _ ->
63 | FractionalThreshold(0.3f)
64 | },
65 | orientation = Orientation.Vertical
66 | ),
67 | ) { constraints: Constraints ->
68 | val mainLowerPlaceable = subcompose(MAIN_LOWER_KEY, mainLower).first().measure(
69 | constraints.copy(minHeight = 0, maxHeight = constraints.maxHeight)
70 | )
71 |
72 | mainLowerHeight = mainLowerPlaceable.height
73 |
74 | val mainUpperPlaceable = subcompose(MAIN_UPPER_KEY, mainUpper).first().measure(
75 | constraints.copy(minHeight = constraints.maxHeight - mainLowerHeight, maxHeight = constraints.maxHeight)
76 | )
77 | mainUpperHeight = mainUpperPlaceable.height
78 |
79 | val progress = (state.offset.value / mainLowerHeight).coerceIn(0f, 1f)
80 | val foregroundPlaceable = subcompose(FOREGROUND_KEY, foreground).first().measure(
81 | constraints.copy(minHeight = constraints.maxHeight, maxHeight = constraints.maxHeight)
82 | )
83 |
84 |
85 | layout(constraints.maxWidth, constraints.maxHeight) {
86 | val scale = lerp(1f, 0.8f, progress)
87 | mainLowerPlaceable.placeRelativeWithLayer(0, mainUpperHeight) {
88 | alpha = 1f - progress
89 | scaleX = scale
90 | scaleY = scale
91 | transformOrigin = TransformOrigin(0.5f, 0f)
92 | }
93 |
94 | mainUpperPlaceable.placeRelativeWithLayer(0, 0) {
95 | alpha = 1f - progress
96 | scaleX = scale
97 | scaleY = scale
98 | transformOrigin = TransformOrigin(0.5f, 1f)
99 | }
100 |
101 | if (progress > 0.01f) {
102 | foregroundPlaceable.placeRelativeWithLayer(0, lerp(-mainLowerHeight, 0, progress)) {
103 | alpha = progress
104 | }
105 | }
106 | }
107 | }
108 | }
109 |
110 | @OptIn(ExperimentalMaterialApi::class)
111 | @Composable
112 | fun SwipeCrossFadeLayoutTest() {
113 | val state = rememberSwipeableState(initialValue = SwipeLayoutState.Main)
114 | val nestedScrollConnection = remember {
115 | object : NestedScrollConnection {
116 | override fun onPostScroll(
117 | consumed: Offset,
118 | available: Offset,
119 | source: NestedScrollSource
120 | ): Offset {
121 | // 因为前景是列表,如果滑到底部仍然有多余的滑动距离,就关闭
122 | // Log.d("NestedScrollConnection", "onPostScroll: $available")
123 | // 读者可以自行运行这行代码,滑动列表到底部后仍然上滑,看看上面会打印什么,就能明白这个 available 的作用了
124 | return if (available.y < 0 && source == NestedScrollSource.Drag) {
125 | state.performDrag(available.toFloat()).toOffset()
126 | } else {
127 | Offset.Zero
128 | }
129 | }
130 |
131 | override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
132 | state.performFling(velocity = Offset(available.x, available.y).toFloat())
133 | return available
134 | }
135 |
136 | private fun Float.toOffset(): Offset = Offset(0f, this)
137 |
138 | private fun Offset.toFloat(): Float = this.y
139 | }
140 | }
141 | SwipeCrossFadeLayout(
142 | modifier = Modifier
143 | .fillMaxSize(),
144 | state = state,
145 | mainLower = {
146 | Box(
147 | modifier = Modifier
148 | .fillMaxWidth()
149 | .height(200.dp)
150 | .background(Color.Blue)
151 | )
152 | },
153 | mainUpper = {
154 | Box(
155 | modifier = Modifier
156 | .fillMaxWidth()
157 | .background(Color.Red)
158 | )
159 | },
160 | foreground = {
161 | // Box(
162 | // modifier = Modifier
163 | // .fillMaxSize()
164 | // .background(Color.Green)
165 | // )
166 | LazyColumn(
167 | Modifier
168 | .fillMaxSize()
169 | .nestedScroll(nestedScrollConnection)
170 | ) {
171 | items(100) {
172 | Text(text = "Item $it")
173 | }
174 | }
175 | }
176 | )
177 | }
178 |
179 |
180 | private const val MAIN_UPPER_KEY = "main_upper"
181 | private const val MAIN_LOWER_KEY = "main_lower"
182 | private const val FOREGROUND_KEY = "foreground"
183 |
184 | private fun lerp(start: Int, end: Int, fraction: Float): Int {
185 | return (start + fraction * (end - start)).toInt()
186 | }
187 |
188 | private fun lerp(start: Float, end: Float, fraction: Float): Float {
189 | return (start + fraction * (end - start))
190 | }
191 |
192 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/SwipeableDemo.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.gestures.Orientation
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.offset
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.material.ExperimentalMaterialApi
9 | import androidx.compose.material.FractionalThreshold
10 | import androidx.compose.material.rememberSwipeableState
11 | import androidx.compose.material.swipeable
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.graphics.Color
15 | import androidx.compose.ui.platform.LocalDensity
16 | import androidx.compose.ui.unit.IntOffset
17 | import androidx.compose.ui.unit.dp
18 |
19 | private enum class Status {
20 | OPEN, CLOSE
21 | }
22 |
23 | @OptIn(ExperimentalMaterialApi::class)
24 | @Composable
25 | fun SwipeableDemo() {
26 | val blockSize = 48.dp
27 | val blockSizePx = with(LocalDensity.current) { blockSize.toPx() }
28 | val swipeableState = rememberSwipeableState(initialValue = Status.CLOSE)
29 | Box(
30 | modifier = Modifier
31 | .size(height = blockSize, width = blockSize * 4)
32 | .background(Color.LightGray)
33 | ) {
34 | Box(
35 | modifier = Modifier
36 | .offset {
37 | // 读取 swipeableState 的 offset 值,设置为 Box 的偏移量
38 | IntOffset(swipeableState.offset.value.toInt(), 0)
39 | }
40 | .swipeable(
41 | state = swipeableState,
42 | // 关键参数 anchors,表示 offset 和自定义状态的对应关系
43 | anchors = mapOf(
44 | 0f to Status.CLOSE,
45 | blockSizePx * 3 to Status.OPEN
46 | ),
47 | // 关键参数 thresholds,表示位置到达多少时,自动切换到下一个状态
48 | thresholds = { from, to ->
49 | if (from == Status.CLOSE) {
50 | FractionalThreshold(0.3f)
51 | } else {
52 | FractionalThreshold(0.5f)
53 | }
54 | },
55 | // orientation,表示滑动方向
56 | orientation = Orientation.Horizontal
57 | )
58 | .size(blockSize)
59 | .background(Color.DarkGray)
60 | )
61 | }
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/VerticalLayout.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.IntrinsicSize
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.layout.*
8 | import androidx.compose.ui.unit.Constraints
9 |
10 | @Composable
11 | fun VerticalLayout(
12 | modifier: Modifier = Modifier,
13 | content: @Composable () -> Unit
14 | ) {
15 | Layout(
16 | modifier = modifier,
17 | content = content
18 | ) { measurables: List, constraints: Constraints ->
19 | val placeables = measurables.map { it.measure(constraints.copy(minHeight = 0)) }
20 | // 宽度:最宽的一项
21 | val width = placeables.maxOf { it.width }
22 | // 高度:所有子微件高度之和
23 | val height = placeables.sumOf { it.height }
24 | layout(width, height) {
25 | var y = 0
26 | placeables.forEach {
27 | it.placeRelative(0, y)
28 | y += it.height
29 | }
30 | }
31 | }
32 | }
33 |
34 | @Composable
35 | fun VerticalLayoutWithIntrinsic(
36 | modifier: Modifier = Modifier,
37 | content: @Composable () -> Unit
38 | ) {
39 | val measurePolicy = object : MeasurePolicy {
40 | override fun MeasureScope.measure(
41 | measurables: List,
42 | constraints: Constraints
43 | ): MeasureResult {
44 | val placeables = measurables.map { it.measure(constraints.copy(minWidth = 0, minHeight = 0)) }
45 | // 宽度:最宽的一项
46 | val width = placeables.maxOf { it.width }
47 | // 高度:所有子微件高度之和
48 | val height = placeables.sumOf { it.height }
49 | return layout(width, height) {
50 | var y = 0
51 | placeables.forEach {
52 | it.placeRelative(0, y)
53 | y += it.height
54 | }
55 | }
56 | }
57 |
58 | override fun IntrinsicMeasureScope.maxIntrinsicWidth(
59 | measurables: List,
60 | height: Int
61 | ): Int {
62 | var width = 0
63 | measurables.forEach {
64 | val childWidth = it.maxIntrinsicWidth(height)
65 | if (childWidth > width) width = childWidth
66 | }
67 | return width
68 | }
69 |
70 | override fun IntrinsicMeasureScope.minIntrinsicWidth(
71 | measurables: List,
72 | height: Int
73 | ): Int {
74 | var width = Int.MAX_VALUE
75 | measurables.forEach {
76 | val childWidth = it.maxIntrinsicWidth(height)
77 | if (childWidth < width) width = childWidth
78 | }
79 | return width
80 | }
81 | }
82 |
83 | Layout(
84 | modifier = modifier,
85 | content = content,
86 | measurePolicy = measurePolicy
87 | )
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/VerticalWeightedTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.runtime.Stable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.layout.*
9 | import androidx.compose.ui.unit.Constraints
10 | import androidx.compose.ui.unit.Density
11 | import androidx.compose.ui.unit.dp
12 | import kotlin.math.roundToInt
13 |
14 | interface VerticalScope {
15 | @Stable
16 | fun Modifier.weight(weight: Float) : Modifier
17 | }
18 |
19 | class WeightParentData(val weight: Float=0f) : ParentDataModifier {
20 | override fun Density.modifyParentData(parentData: Any?) = this@WeightParentData
21 | }
22 |
23 | object VerticalScopeInstance : VerticalScope {
24 | @Stable
25 | override fun Modifier.weight(weight: Float): Modifier = this.then(
26 | WeightParentData(weight)
27 | )
28 | }
29 |
30 | /**
31 | * FunnySaltyFish 2022-03-10
32 | * @param modifier Modifier
33 | * @param content [@androidx.compose.runtime.Composable] [@kotlin.ExtensionFunctionType] Function1
34 | */
35 | @Composable
36 | fun WeightedVerticalLayout(
37 | modifier: Modifier = Modifier,
38 | content: @Composable VerticalScope.() -> Unit
39 | ) {
40 | val measurePolicy = MeasurePolicy { measurables, constraints: Constraints ->
41 | // 获取各weight值
42 | val weights = measurables.map {
43 | (it.parentData as WeightParentData).weight
44 | }
45 | val totalHeight = constraints.maxHeight
46 | val totalWeight = weights.sum()
47 |
48 | val placeables = measurables.mapIndexed { i, mesurable ->
49 | // 根据比例计算高度
50 | val h = (weights[i] / totalWeight * totalHeight).roundToInt()
51 | mesurable.measure(constraints.copy(minHeight = h, maxHeight = h))
52 | }
53 | // 宽度:最宽的一项
54 | val width = placeables.maxOf { it.width }
55 |
56 | layout(width, totalHeight) {
57 | var y = 0
58 | placeables.forEachIndexed { i, placeable ->
59 | placeable.placeRelative(0, y)
60 | // 按比例设置大小
61 | y += placeable.height
62 | }
63 | }
64 | }
65 | Layout(modifier = modifier, content = { VerticalScopeInstance.content() }, measurePolicy=measurePolicy)
66 | }
67 |
68 | @Composable
69 | fun WeightedVerticalLayoutTest() {
70 | WeightedVerticalLayout(Modifier.padding(16.dp).height(200.dp)) {
71 | Box(modifier = Modifier.width(40.dp).weight(1f).background(randomColor()))
72 | Box(modifier = Modifier.width(40.dp).weight(2f).background(randomColor()))
73 | Box(modifier = Modifier.width(40.dp).weight(7f).background(randomColor()))
74 | }
75 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_layout/WaterfallFlowLayout.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_layout
2 |
3 | import android.util.Log
4 | import androidx.compose.foundation.layout.size
5 | import androidx.compose.runtime.Composable
6 | import androidx.compose.ui.Modifier
7 | import androidx.compose.ui.layout.Layout
8 | import androidx.compose.ui.layout.Measurable
9 | import androidx.compose.ui.unit.Constraints
10 | import androidx.compose.ui.unit.dp
11 |
12 | private const val TAG = "Waterfall"
13 |
14 | fun IntArray.minIndex() : Int {
15 | var i = 0
16 | var min = Int.MAX_VALUE
17 | this.forEachIndexed { index, e ->
18 | if (eUnit
31 | ) {
32 | Layout(
33 | modifier = modifier,
34 | content = content,
35 | ) { measurables: List, constrains: Constraints ->
36 | val itemWidth = constrains.maxWidth / columns
37 | val itemConstraints = constrains.copy(minWidth = itemWidth, maxWidth = itemWidth, minHeight = 0)
38 | val placeables = measurables.map { it.measure(itemConstraints) }
39 | // 记录当前各列高度
40 | val heights = IntArray(columns)
41 | layout(width = constrains.maxWidth, height = constrains.maxHeight){
42 | placeables.forEach { placeable ->
43 | val minIndex = heights.minIndex()
44 | // Log.d(TAG, "WaterfallFlowLayout: $minIndex ${placeable.height}")
45 | placeable.placeRelative(itemWidth * minIndex, heights[minIndex])
46 | heights[minIndex] += placeable.height
47 | }
48 | }
49 | }
50 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_lazycolumn/FunnyLazyColumn.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_lazycolumn
2 |
3 | import androidx.compose.runtime.Composable
4 |
5 | // 我曾经有一段时间试图探究 LazyColumn 的原理
6 | // 之后太复杂,不了了之了……
7 |
8 | @Composable
9 | fun FunnyLazyColumn() {
10 | // LazyColumn()
11 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_lazycolumn/FunnyLazyList.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_lazycolumn
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.ui.unit.LayoutDirection
5 |
6 |
7 | @Composable
8 | fun FunnyLazyList(
9 | funnyLazyListState: FunnyLazyListState = rememberFunnyLazyListState(),
10 | isVertical : Boolean,
11 | ) {
12 |
13 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_lazycolumn/FunnyLazyListState.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_lazycolumn
2 |
3 | import androidx.compose.foundation.MutatePriority
4 | import androidx.compose.foundation.gestures.ScrollScope
5 | import androidx.compose.foundation.gestures.ScrollableState
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 |
9 | class FunnyLazyListState(var firstVisibleItemIndex : Int, var firstVisibleItemOffset : Int) : ScrollableState {
10 | override val isScrollInProgress: Boolean
11 | get() = TODO("Not yet implemented")
12 |
13 | override fun dispatchRawDelta(delta: Float): Float {
14 | TODO("Not yet implemented")
15 | }
16 |
17 | override suspend fun scroll(
18 | scrollPriority: MutatePriority,
19 | block: suspend ScrollScope.() -> Unit
20 | ) {
21 | TODO()
22 | }
23 | }
24 |
25 | @Composable
26 | fun rememberFunnyLazyListState() = remember {
27 | FunnyLazyListState(0, 0)
28 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/post_lazygrid/LazyGridTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.post_lazygrid
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.*
5 | import androidx.compose.foundation.lazy.LazyColumn
6 | import androidx.compose.foundation.lazy.grid.GridCells
7 | import androidx.compose.foundation.lazy.grid.GridItemSpan
8 | import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
10 | import androidx.compose.foundation.verticalScroll
11 | import androidx.compose.material.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.Density
15 | import androidx.compose.ui.unit.dp
16 | import com.funny.compose.study.ui.post_layout.randomColor
17 | import com.funny.compose.study.ui.post_layout.rememberRandomColor
18 | import kotlin.random.Random
19 |
20 | @Composable
21 | fun RandomColorBox(modifier: Modifier) {
22 | Box(modifier = modifier.background(rememberRandomColor()))
23 | }
24 |
25 | @Composable
26 | fun SimpleLazyGrid(){
27 | LazyVerticalGrid(
28 | modifier = Modifier.fillMaxWidth(),
29 | // 固定两列
30 | columns = GridCells.Fixed(2) ,
31 | content = {
32 | items(12){
33 | RandomColorBox(modifier = Modifier.height(200.dp))
34 | }
35 | }
36 | )
37 | }
38 |
39 | @Composable
40 | fun SimpleLazyGridWithSpace(){
41 | LazyVerticalGrid(
42 | modifier = Modifier.fillMaxWidth(),
43 | // 固定两列
44 | columns = GridCells.Fixed(2) ,
45 | content = {
46 | items(12){
47 | RandomColorBox(modifier = Modifier.height(200.dp))
48 | }
49 | },
50 | horizontalArrangement = Arrangement.spacedBy(12.dp),
51 | verticalArrangement = Arrangement.spacedBy(8.dp),
52 | contentPadding = PaddingValues(12.dp)
53 | )
54 | }
55 |
56 | @Composable
57 | fun SimpleLazyGridAda(){
58 | LazyVerticalGrid(
59 | modifier = Modifier.fillMaxWidth(),
60 | // 固定宽度,自适应列数
61 | columns = GridCells.Adaptive(200.dp) ,
62 | content = {
63 | items(12){
64 | RandomColorBox(modifier = Modifier.height(200.dp))
65 | }
66 | },
67 | horizontalArrangement = Arrangement.spacedBy(12.dp),
68 | verticalArrangement = Arrangement.spacedBy(8.dp),
69 | contentPadding = PaddingValues(12.dp)
70 | )
71 | }
72 |
73 | @Composable
74 | fun SimpleLazyGridWithSpan(){
75 | LazyVerticalGrid(
76 | modifier = Modifier.fillMaxWidth(),
77 | // 固定列数
78 | columns = GridCells.Fixed(3) ,
79 | content = {
80 | item(span = {
81 | GridItemSpan(maxLineSpan)
82 | }){
83 | RandomColorBox(modifier = Modifier.height(50.dp))
84 | }
85 | items(12){
86 | RandomColorBox(modifier = Modifier.height(200.dp))
87 | }
88 | },
89 | horizontalArrangement = Arrangement.spacedBy(12.dp),
90 | verticalArrangement = Arrangement.spacedBy(8.dp),
91 | contentPadding = PaddingValues(12.dp)
92 | )
93 |
94 | LazyColumn {
95 | item {
96 | // 两个微件放在同一item里
97 | RandomColorBox(modifier = Modifier.size(40.dp))
98 | RandomColorBox(modifier = Modifier.size(40.dp))
99 | }
100 | }
101 | }
102 |
103 | @Composable
104 | fun SimpleLazyGridCustom(){
105 | LazyVerticalGrid(
106 | modifier = Modifier.fillMaxWidth(),
107 | // 自定义实现1:2:1
108 | columns = object : GridCells {
109 | override fun Density.calculateCrossAxisCellSizes(
110 | availableSize: Int,
111 | spacing: Int
112 | ): List {
113 | // 总共三个元素,所以其实两个间隔
114 | // |元素|间隔|元素|间隔|元素|
115 | val availableSizeWithoutSpacing = availableSize - 2 * spacing
116 | // 小的两个大小即为剩余空间(总空间-间隔)/4
117 | val smallSize = availableSizeWithoutSpacing / 4
118 | // 大的那个就是除以2呗
119 | val largeSize = availableSizeWithoutSpacing / 2
120 | return listOf(smallSize, largeSize, smallSize)
121 | }
122 | },
123 | content = {
124 | item(span = {
125 | GridItemSpan(maxLineSpan)
126 | }){
127 | RandomColorBox(modifier = Modifier.height(50.dp))
128 | }
129 | items(12){
130 | RandomColorBox(modifier = Modifier.height(200.dp))
131 | }
132 | },
133 | horizontalArrangement = Arrangement.spacedBy(12.dp),
134 | verticalArrangement = Arrangement.spacedBy(8.dp),
135 | contentPadding = PaddingValues(12.dp)
136 | )
137 | }
138 |
139 | // 下面是对自己实现瀑布流的尝试哈哈
140 | // Grid绘制的时候要求每一行高度一样……所以整不起来啊
141 |
142 | /**
143 | * 返回整形数组中最小的那一项对应的值
144 | * @receiver IntArray
145 | * @return Int
146 | */
147 | fun IntArray.minIndex() : Int {
148 | var i = 0
149 | var min = Int.MAX_VALUE
150 | this.forEachIndexed { index, e ->
151 | if (e
164 | val minIdx = heights.minIndex()
165 | outPositions[i] = heights[minIdx]
166 | heights[minIdx] += size
167 | }
168 | }
169 | }
170 |
171 | private fun Arrangement.verticalStaggered(columnNum : Int = 2) = VerticalStaggeredArrangement(columnNum)
172 |
173 | // 然并不行……
174 | @Composable
175 | fun SimpleLazyGridStaggered(){
176 | LazyVerticalGrid(
177 | modifier = Modifier.fillMaxWidth(),
178 | // 固定两列
179 | columns = GridCells.Fixed(2) ,
180 | content = {
181 | items(12){
182 | RandomColorBox(modifier = Modifier.height(Random.nextInt(100, 200).dp))
183 | }
184 | },
185 | horizontalArrangement = Arrangement.spacedBy(12.dp),
186 | verticalArrangement = Arrangement.verticalStaggered(2),
187 | contentPadding = PaddingValues(12.dp)
188 | )
189 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/posta/Beans.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.posta
2 |
3 | internal data class Student(val id:Int, val name:String)
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/posta/FScreen.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.posta
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.animation.core.animateDpAsState
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.items
11 | import androidx.compose.material.*
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Delete
14 | import androidx.compose.material.icons.filled.Done
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import kotlin.random.Random
23 |
24 | @ExperimentalFoundationApi
25 | @ExperimentalMaterialApi
26 | @Composable
27 | fun FScreen() {
28 | // ShuffleSample()
29 | SwipeToDismissSample()
30 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/posta/PopularBooksDemo.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | // 此例子来自 https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/PopularBooksDemo.kt
18 | // 仅修改标题为中文
19 |
20 | package com.funny.compose.study.ui.posta
21 |
22 | import androidx.compose.foundation.ExperimentalFoundationApi
23 | import androidx.compose.foundation.clickable
24 | import androidx.compose.foundation.layout.Arrangement
25 | import androidx.compose.foundation.layout.Column
26 | import androidx.compose.foundation.layout.IntrinsicSize
27 | import androidx.compose.foundation.layout.Row
28 | import androidx.compose.foundation.layout.fillMaxHeight
29 | import androidx.compose.foundation.layout.fillMaxSize
30 | import androidx.compose.foundation.layout.height
31 | import androidx.compose.foundation.layout.padding
32 | import androidx.compose.foundation.layout.width
33 | import androidx.compose.foundation.layout.wrapContentHeight
34 | import androidx.compose.foundation.lazy.LazyColumn
35 | import androidx.compose.foundation.lazy.items
36 | import androidx.compose.material.Divider
37 | import androidx.compose.material.MaterialTheme
38 | import androidx.compose.material.Text
39 | import androidx.compose.runtime.Composable
40 | import androidx.compose.runtime.getValue
41 | import androidx.compose.runtime.mutableStateOf
42 | import androidx.compose.runtime.remember
43 | import androidx.compose.runtime.setValue
44 | import androidx.compose.ui.Alignment
45 | import androidx.compose.ui.Modifier
46 | import androidx.compose.ui.graphics.Color
47 | import androidx.compose.ui.text.style.TextAlign
48 | import androidx.compose.ui.unit.Dp
49 | import androidx.compose.ui.unit.dp
50 |
51 | @OptIn(ExperimentalFoundationApi::class)
52 | @Composable
53 | fun PopularBooksDemo() {
54 | MaterialTheme {
55 | var comparator by remember { mutableStateOf(TitleComparator) }
56 | Column {
57 | Row(
58 | modifier = Modifier.height(IntrinsicSize.Max),
59 | horizontalArrangement = Arrangement.spacedBy(8.dp)
60 | ) {
61 | Text(
62 | "标题",
63 | Modifier.clickable { comparator = TitleComparator }
64 | .weight(5f)
65 | .fillMaxHeight()
66 | .padding(4.dp)
67 | .wrapContentHeight(Alignment.CenterVertically),
68 | textAlign = TextAlign.Center
69 | )
70 | Text(
71 | "作者",
72 | Modifier.clickable { comparator = AuthorComparator }
73 | .weight(2f)
74 | .fillMaxHeight()
75 | .padding(4.dp)
76 | .wrapContentHeight(Alignment.CenterVertically),
77 | textAlign = TextAlign.Center
78 | )
79 | Text(
80 | "年份",
81 | Modifier.clickable { comparator = YearComparator }
82 | .width(50.dp)
83 | .fillMaxHeight()
84 | .padding(4.dp)
85 | .wrapContentHeight(Alignment.CenterVertically),
86 | textAlign = TextAlign.Center
87 | )
88 | Text(
89 | "销售量 (百万)",
90 | Modifier.clickable { comparator = SalesComparator }
91 | .width(65.dp)
92 | .fillMaxHeight()
93 | .padding(4.dp)
94 | .wrapContentHeight(Alignment.CenterVertically),
95 | textAlign = TextAlign.Center
96 | )
97 | }
98 | Divider(color = Color.LightGray, thickness = Dp.Hairline)
99 | LazyColumn(
100 | Modifier.fillMaxSize(),
101 | verticalArrangement = Arrangement.spacedBy(16.dp)
102 | ) {
103 | val sortedList = PopularBooksList.sortedWith(comparator)
104 | items(sortedList, key = { it.title }) {
105 | Row(
106 | Modifier.animateItemPlacement()
107 | .height(IntrinsicSize.Max),
108 | horizontalArrangement = Arrangement.spacedBy(8.dp)
109 | ) {
110 | Text(
111 | it.title,
112 | Modifier.weight(5f)
113 | .fillMaxHeight()
114 | .padding(4.dp)
115 | .wrapContentHeight(Alignment.CenterVertically),
116 | textAlign = TextAlign.Center
117 | )
118 | Text(
119 | it.author,
120 | Modifier.weight(2f)
121 | .fillMaxHeight()
122 | .padding(4.dp)
123 | .wrapContentHeight(Alignment.CenterVertically),
124 | textAlign = TextAlign.Center
125 | )
126 | Text(
127 | "${it.published}",
128 | Modifier.width(55.dp)
129 | .fillMaxHeight()
130 | .padding(4.dp)
131 | .wrapContentHeight(Alignment.CenterVertically),
132 | textAlign = TextAlign.Center
133 | )
134 | Text(
135 | "${it.salesInMillions}",
136 | Modifier.width(65.dp)
137 | .fillMaxHeight()
138 | .padding(4.dp)
139 | .wrapContentHeight(Alignment.CenterVertically),
140 | textAlign = TextAlign.Center
141 | )
142 | }
143 | }
144 | }
145 | }
146 | }
147 | }
148 |
149 | private val TitleComparator = Comparator { left, right ->
150 | left.title.compareTo(right.title)
151 | }
152 |
153 | private val AuthorComparator = Comparator { left, right ->
154 | left.author.compareTo(right.author)
155 | }
156 |
157 | private val YearComparator = Comparator { left, right ->
158 | right.published.compareTo(left.published)
159 | }
160 |
161 | private val SalesComparator = Comparator { left, right ->
162 | right.salesInMillions.compareTo(left.salesInMillions)
163 | }
164 |
165 | private val PopularBooksList = listOf(
166 | Book("The Hobbit", "J. R. R. Tolkien", 1937, 140),
167 | Book("Harry Potter and the Philosopher's Stone", "J. K. Rowling", 1997, 120),
168 | Book("Dream of the Red Chamber", "Cao Xueqin", 1800, 100),
169 | Book("And Then There Were None", "Agatha Christie", 1939, 100),
170 | Book("The Little Prince", "Antoine de Saint-Exupéry", 1943, 100),
171 | Book("The Lion, the Witch and the Wardrobe", "C. S. Lewis", 1950, 85),
172 | Book("The Adventures of Pinocchio", "Carlo Collodi", 1881, 80),
173 | Book("The Da Vinci Code", "Dan Brown", 2003, 80),
174 | Book("Harry Potter and the Chamber of Secrets", "J. K. Rowling", 1998, 77),
175 | Book("The Alchemist", "Paulo Coelho", 1988, 65),
176 | Book("Harry Potter and the Prisoner of Azkaban", "J. K. Rowling", 1999, 65),
177 | Book("Harry Potter and the Goblet of Fire", "J. K. Rowling", 2000, 65),
178 | Book("Harry Potter and the Order of the Phoenix", "J. K. Rowling", 2003, 65)
179 | )
180 |
181 | private class Book(
182 | val title: String,
183 | val author: String,
184 | val published: Int,
185 | val salesInMillions: Int
186 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/posta/ShuffleSample.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.posta
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.material.Button
7 | import androidx.compose.material.Text
8 | import androidx.compose.runtime.*
9 | import androidx.compose.ui.Modifier
10 |
11 | @ExperimentalFoundationApi
12 | @Composable
13 | fun ShuffleSample() {
14 | var list by remember { mutableStateOf(listOf("A", "B", "C", "D", "E")) }
15 | LazyColumn {
16 | item {
17 | Button(onClick = { list = list.shuffled() }) {
18 | Text("打乱顺序")
19 | }
20 | }
21 | items(items = list, key = { it }) {
22 | Text("列表项:$it", Modifier.animateItemPlacement())
23 | }
24 | }
25 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/posta/SwipeToDismissSample.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.posta
2 |
3 | import androidx.compose.animation.animateColorAsState
4 | import androidx.compose.animation.core.animateDpAsState
5 | import androidx.compose.animation.core.animateFloatAsState
6 | import androidx.compose.foundation.ExperimentalFoundationApi
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.foundation.lazy.items
11 | import androidx.compose.material.*
12 | import androidx.compose.material.icons.Icons
13 | import androidx.compose.material.icons.filled.Delete
14 | import androidx.compose.material.icons.filled.Done
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.scale
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.unit.dp
21 | import androidx.compose.ui.unit.sp
22 | import kotlin.random.Random
23 |
24 |
25 | private fun randomColor() = Color(Random.nextInt(255), Random.nextInt(255), Random.nextInt(255))
26 |
27 | @ExperimentalFoundationApi
28 | @ExperimentalMaterialApi
29 | @Composable
30 | fun SwipeToDismissSample() {
31 | var studentList by remember {
32 | mutableStateOf( (1..100).map { Student(it, "Student $it") } )
33 | }
34 | LazyColumn(
35 | modifier = Modifier.fillMaxWidth(),
36 | verticalArrangement = Arrangement.spacedBy(8.dp)
37 | ) {
38 | items(studentList, key = {item: Student -> item.id }){ item ->
39 | // 侧滑删除所需State
40 | val dismissState = rememberDismissState()
41 | // 按指定方向触发删除后的回调,在此处变更具体数据
42 | if(dismissState.isDismissed(DismissDirection.StartToEnd)){
43 | studentList = studentList.toMutableList().also { it.remove(item) }
44 | }
45 | SwipeToDismiss(
46 | state = dismissState,
47 | modifier = Modifier.fillMaxWidth().animateItemPlacement(),
48 | // 下面这个参数为触发滑动删除的移动阈值
49 | dismissThresholds = { direction ->
50 | FractionalThreshold(if (direction == DismissDirection.StartToEnd) 0.25f else 0.5f)
51 | },
52 | // 允许滑动删除的方向
53 | directions = setOf(DismissDirection.StartToEnd),
54 | // "背景 ",即原来显示的内容被划走一部分时显示什么
55 | background = {
56 | val direction = dismissState.dismissDirection ?: return@SwipeToDismiss
57 | val color by animateColorAsState(
58 | when (dismissState.targetValue) {
59 | DismissValue.Default -> Color.LightGray
60 | DismissValue.DismissedToEnd -> Color.Green
61 | DismissValue.DismissedToStart -> Color.Red
62 | }
63 | )
64 | val alignment = when (direction) {
65 | DismissDirection.StartToEnd -> Alignment.CenterStart
66 | DismissDirection.EndToStart -> Alignment.CenterEnd
67 | }
68 | val icon = when (direction) {
69 | DismissDirection.StartToEnd -> Icons.Default.Done
70 | DismissDirection.EndToStart -> Icons.Default.Delete
71 | }
72 | val scale by animateFloatAsState(
73 | if (dismissState.targetValue == DismissValue.Default) 0.75f else 1f
74 | )
75 |
76 | Box(
77 | Modifier
78 | .fillMaxSize()
79 | .background(color)
80 | .padding(horizontal = 20.dp),
81 | contentAlignment = alignment
82 | ) {
83 | Icon(
84 | icon,
85 | contentDescription = "Localized description",
86 | modifier = Modifier.scale(scale)
87 | )
88 | }
89 | }
90 | ) {
91 | // ”前景“ 显示的内容
92 | Card(
93 | elevation = animateDpAsState(
94 | if (dismissState.dismissDirection != null) 4.dp else 0.dp
95 | ).value
96 | ) {
97 | Text(item.name, Modifier.padding(8.dp), fontSize = 28.sp)
98 | }
99 | }
100 | }
101 | }
102 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/refresh/SwipeToRefreshTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.refresh
2 |
3 | import androidx.compose.foundation.layout.*
4 | import androidx.compose.foundation.lazy.LazyColumn
5 | import androidx.compose.foundation.lazy.items
6 | import androidx.compose.material.ExperimentalMaterialApi
7 | import androidx.compose.material.Text
8 | import androidx.compose.material.pullrefresh.PullRefreshIndicator
9 | import androidx.compose.material.pullrefresh.pullRefresh
10 | import androidx.compose.material.pullrefresh.rememberPullRefreshState
11 | import androidx.compose.runtime.*
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.unit.dp
15 | import kotlinx.coroutines.delay
16 | import kotlinx.coroutines.launch
17 |
18 | @OptIn(ExperimentalMaterialApi::class)
19 | @Composable
20 | fun SwipeToRefreshTest(
21 | modifier: Modifier = Modifier
22 | ) {
23 | val list = remember {
24 | List(4){ "Item $it" }.toMutableStateList()
25 | }
26 | var refreshing by remember {
27 | mutableStateOf(false)
28 | }
29 | val scope = rememberCoroutineScope()
30 | val state = rememberPullRefreshState(refreshing = refreshing, onRefresh = {
31 | scope.launch {
32 | refreshing = true
33 | // delay(1000) // 模拟数据加载
34 | list+="Item ${list.size+1}"
35 | refreshing = false
36 | }
37 | })
38 | Box(modifier = modifier
39 | .fillMaxSize()
40 | .pullRefresh(state)
41 | ){
42 | LazyColumn(Modifier.fillMaxWidth()){
43 | items(list) {
44 | Text(
45 | text = it,
46 | Modifier
47 | .padding(16.dp)
48 | .fillMaxWidth()
49 | )
50 | }
51 | }
52 | PullRefreshIndicator(refreshing, state, Modifier.align(Alignment.TopCenter))
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple200 = Color(0xFFBB86FC)
6 | val Purple500 = Color(0xFF6200EE)
7 | val Purple700 = Color(0xFF3700B3)
8 | val Teal200 = Color(0xFF03DAC5)
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/theme/Shape.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.theme
2 |
3 | import androidx.compose.foundation.shape.RoundedCornerShape
4 | import androidx.compose.material.Shapes
5 | import androidx.compose.ui.unit.dp
6 |
7 | val Shapes = Shapes(
8 | small = RoundedCornerShape(4.dp),
9 | medium = RoundedCornerShape(4.dp),
10 | large = RoundedCornerShape(0.dp)
11 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.theme
2 |
3 | import androidx.compose.foundation.isSystemInDarkTheme
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.material.darkColors
6 | import androidx.compose.material.lightColors
7 | import androidx.compose.runtime.Composable
8 |
9 | private val DarkColorPalette = darkColors(
10 | primary = Purple200,
11 | primaryVariant = Purple700,
12 | secondary = Teal200
13 | )
14 |
15 | private val LightColorPalette = lightColors(
16 | primary = Purple500,
17 | primaryVariant = Purple700,
18 | secondary = Teal200
19 |
20 | /* Other default colors to override
21 | background = Color.White,
22 | surface = Color.White,
23 | onPrimary = Color.White,
24 | onSecondary = Color.Black,
25 | onBackground = Color.Black,
26 | onSurface = Color.Black,
27 | */
28 | )
29 |
30 | @Composable
31 | fun JetpackComposeStudyTheme(
32 | darkTheme: Boolean = isSystemInDarkTheme(),
33 | content: @Composable() () -> Unit
34 | ) {
35 | val colors = if (darkTheme) {
36 | DarkColorPalette
37 | } else {
38 | LightColorPalette
39 | }
40 |
41 | MaterialTheme(
42 | colors = colors,
43 | typography = Typography,
44 | shapes = Shapes,
45 | content = content
46 | )
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | body1 = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp
15 | )
16 | /* Other default text styles to override
17 | button = TextStyle(
18 | fontFamily = FontFamily.Default,
19 | fontWeight = FontWeight.W500,
20 | fontSize = 14.sp
21 | ),
22 | caption = TextStyle(
23 | fontFamily = FontFamily.Default,
24 | fontWeight = FontWeight.Normal,
25 | fontSize = 12.sp
26 | )
27 | */
28 | )
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videoa/AScreen.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videoa
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.*
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import androidx.compose.ui.unit.dp
11 | import androidx.lifecycle.viewmodel.compose.viewModel
12 | import com.funny.cmaterialcolors.MaterialColors.Companion.BlueA700
13 | import kotlin.random.Random
14 |
15 | @Composable
16 | fun AScreen() {
17 | //View
18 | // findViewById(box).setBackground(
19 | // Compose 声明
20 | // state -> 视图
21 | Column {
22 | val viewModel : AViewModel = viewModel()
23 | val colorState by viewModel.color
24 |
25 | ABox(
26 | updateColor = {
27 | viewModel.updateColor(it)
28 | }
29 | )
30 | Spacer(modifier = Modifier.height(10.dp))
31 | BBox(colorState)
32 | }
33 | }
34 |
35 | @Composable
36 | fun ABox(
37 | updateColor : (color:Color)->Unit
38 | ) {
39 | Box(modifier = Modifier
40 | .size(100.dp)
41 | .background(BlueA700)
42 | .clickable {
43 | updateColor(
44 | Color(
45 | Random.nextInt(255),
46 | Random.nextInt(255),
47 | Random.nextInt(255),
48 | 255
49 | )
50 | )
51 | })
52 | }
53 |
54 | @Composable
55 | fun BBox(
56 | color: Color
57 | ) {
58 | Box(modifier = Modifier
59 | .size(100.dp)
60 | .background(color)
61 | )
62 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videoa/AViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videoa
2 |
3 | import androidx.compose.runtime.mutableStateOf
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.lifecycle.MutableLiveData
6 | import androidx.lifecycle.ViewModel
7 | import com.funny.cmaterialcolors.MaterialColors.Companion.BlueA700
8 |
9 | class AViewModel : ViewModel(){
10 | val color = mutableStateOf(BlueA700)
11 |
12 | fun updateColor(newColor: Color){
13 | this.color.value = newColor
14 | }
15 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videob/BScreen.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videob
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.fillMaxSize
5 | import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape
6 | import androidx.compose.material.*
7 | import androidx.compose.runtime.*
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.unit.dp
10 | import com.funny.cmaterialcolors.MaterialColors.Companion.Green200
11 |
12 | @Composable
13 | fun BScreen() {
14 | val scaffoldState = rememberScaffoldState()
15 | val scope = rememberCoroutineScope()
16 | var dialogState by remember {
17 | mutableStateOf(false)
18 | }
19 | Scaffold(
20 | scaffoldState = scaffoldState,
21 | snackbarHost = { state->
22 | SnackbarHost(hostState = state){ data->
23 | Snackbar(
24 | snackbarData = data,
25 | shape = AbsoluteRoundedCornerShape(8.dp),
26 | backgroundColor = Green200
27 | )
28 |
29 | }
30 | }
31 | ){
32 | Box(modifier = Modifier.fillMaxSize()) {
33 | Button(onClick = {
34 | dialogState = true
35 | // scope.launch {
36 | // scaffoldState.snackbarHostState.showSnackbar("我是一个Snackbar")
37 | // }
38 | }) {
39 | Text("点我")
40 | }
41 |
42 | if(dialogState){
43 | AlertDialog(
44 | onDismissRequest = {
45 | dialogState = false
46 | },
47 | title = {
48 | Text(text = "Title")
49 | },
50 | text = {
51 | Text("This is message")
52 | },
53 | confirmButton = {
54 | Button(onClick = { dialogState = false }) {
55 | Text("确定")
56 | }
57 | },
58 | dismissButton = {
59 | Button(onClick = { dialogState = false }) {
60 | Text("取消")
61 | }
62 | }
63 | )
64 | }
65 | }
66 |
67 | }
68 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videoc/BoxState.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videoc
2 |
3 | import androidx.compose.ui.graphics.Color
4 | import androidx.compose.ui.unit.Dp
5 | import androidx.compose.ui.unit.dp
6 | import com.funny.cmaterialcolors.MaterialColors.Companion.BlueA200
7 | import com.funny.cmaterialcolors.MaterialColors.Companion.GreenA200
8 |
9 | sealed class BoxState(val color : Color,val size : Dp){
10 | object Small : BoxState(BlueA200,100.dp)
11 | object Large : BoxState(GreenA200,200.dp)
12 | operator fun not() = when(this){
13 | Small -> Large
14 | Large -> Small
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videoc/CScreen.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videoc
2 |
3 | import androidx.compose.animation.animateColor
4 | import androidx.compose.animation.animateColorAsState
5 | import androidx.compose.animation.animateContentSize
6 | import androidx.compose.animation.core.*
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.*
9 | import androidx.compose.material.Button
10 | import androidx.compose.material.Icon
11 | import androidx.compose.material.IconButton
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.icons.Icons
14 | import androidx.compose.material.icons.filled.ArrowDropDown
15 | import androidx.compose.runtime.*
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.graphics.TransformOrigin
19 | import androidx.compose.ui.graphics.graphicsLayer
20 | import androidx.compose.ui.unit.dp
21 |
22 | @Composable
23 | fun CScreen() {
24 | // ButtonAndBox()
25 | ExpandableText()
26 | }
27 |
28 | @Composable
29 | fun ButtonAndBox() {
30 | Column {
31 | var boxState : BoxState by remember{
32 | mutableStateOf(BoxState.Small)
33 | }
34 | val transition = updateTransition(targetState = boxState, label = "Box")
35 | val animSize by transition.animateDp(label = "Size") { state ->
36 | state.size
37 | }
38 | val animColor by transition.animateColor(label = "Color") { state ->
39 | state.color
40 | }
41 | Button(onClick = { boxState = !boxState }) {
42 | Text("点击我")
43 | }
44 | Spacer(modifier = Modifier.height(16.dp))
45 | Box(modifier = Modifier
46 | .size(animSize)
47 | .background(animColor)
48 | )
49 | }
50 | }
51 |
52 | val text = """
53 | 春江潮水连海平,海上明月共潮生。
54 | 滟滟随波千万里,何处春江无月明!
55 | 江流宛转绕芳甸,月照花林皆似霰;
56 | 空里流霜不觉飞,汀上白沙看不见。
57 | 江天一色无纤尘,皎皎空中孤月轮。
58 | 江畔何人初见月?江月何年初照人?
59 | 人生代代无穷已,江月年年望相似。
60 | """.trimIndent()
61 | @Composable
62 | fun ExpandableText() {
63 | var expand by remember {
64 | mutableStateOf(false)
65 | }
66 | val rotationValue by animateFloatAsState(targetValue = if (expand) -180f else 0f)
67 | Column(
68 | horizontalAlignment = Alignment.End
69 | ) {
70 | Box(modifier = Modifier
71 | .fillMaxWidth()
72 | .animateContentSize()) {
73 | Text(text = text, maxLines = if(expand) 10 else 2,modifier = Modifier.fillMaxWidth())
74 | }
75 | IconButton(onClick = { expand = !expand }) {
76 | Icon(
77 | Icons.Default.ArrowDropDown,
78 | "expand",
79 | modifier = Modifier.graphicsLayer {
80 | rotationX = rotationValue
81 | }
82 | )
83 | }
84 | }
85 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videod/ScreenD.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videod
2 |
3 | import androidx.compose.material.*
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.MoreVert
6 | import androidx.compose.runtime.*
7 |
8 | @Composable
9 | fun ScreenD() {
10 | Scaffold(
11 | topBar = {
12 | AppBar()
13 | }
14 | ) {
15 | Text("content padding: $it")
16 | }
17 | }
18 |
19 | @Composable
20 | fun AppBar() {
21 | var expanded by remember {
22 | mutableStateOf(false)
23 | }
24 |
25 | var isChildExpanded by remember {
26 | mutableStateOf(false)
27 | }
28 | TopAppBar(
29 | title = { Text(text = "App") },
30 | actions = {
31 | IconButton(onClick = { expanded = true }) {
32 | Icon(
33 | Icons.Filled.MoreVert,
34 | contentDescription = "More"
35 | )
36 | }
37 | DropdownMenu(
38 | expanded = expanded,
39 | onDismissRequest = { expanded = false }
40 | ) {
41 | DropdownMenuItem(onClick = {
42 |
43 | }) {
44 | // DropdownMenu(expanded = isChildExpanded, onDismissRequest = { isChildExpanded = false }) {
45 | // DropdownMenuItem(onClick = {}) {
46 | // Text(text = "展开项1")
47 | // }
48 | // DropdownMenuItem(onClick = {}) {
49 | // Text(text = "展开项2")
50 | // }
51 | // }
52 | Text("Normal Item")
53 |
54 | }
55 | ExpandableDropdownItem(text = "Expandable") {
56 | DropdownMenuItem(onClick = {}) {
57 | Text(text = "Nested 1")
58 | }
59 | DropdownMenuItem(onClick = {}) {
60 | Text(text = "Nested 2")
61 | }
62 | }
63 |
64 | }
65 | }
66 | )
67 | }
68 |
69 | @Composable
70 | fun ExpandableDropdownItem(
71 | text: String,
72 | dropDownItems: @Composable () -> Unit
73 | ) {
74 | var expanded by remember {
75 | mutableStateOf(false)
76 | }
77 | DropdownMenuItem(onClick = {
78 | expanded = true
79 | }) {
80 | Text(text = text)
81 | DropdownMenu(
82 | expanded = expanded,
83 | onDismissRequest = { expanded = false }
84 | ) {
85 | dropDownItems()
86 | }
87 | }
88 |
89 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/funny/compose/study/ui/videoe/ScreenE.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study.ui.videoe
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.gestures.Orientation
5 | import androidx.compose.foundation.gestures.draggable
6 | import androidx.compose.foundation.gestures.rememberDraggableState
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.offset
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.runtime.*
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.graphics.Color
14 | import androidx.compose.ui.platform.LocalDensity
15 | import androidx.compose.ui.unit.IntOffset
16 | import androidx.compose.ui.unit.dp
17 | import com.funny.cmaterialcolors.MaterialColors
18 | //import com.google.accompanist.systemuicontroller.rememberSystemUiController
19 |
20 | @Composable
21 | fun EScreen() {
22 | DraggableBox()
23 | }
24 |
25 |
26 | @Composable
27 | fun DraggableBox() {
28 | val size = 40
29 | val length = 4
30 |
31 | var offsetX by remember {
32 | mutableStateOf(0f)
33 | }
34 | val sizePx = with(LocalDensity.current){
35 | size.dp.toPx()
36 | }
37 | val draggableState = rememberDraggableState{
38 | offsetX = (offsetX + it).coerceIn(0f,sizePx*(length-1).toFloat())
39 | }
40 | Box(modifier = Modifier
41 | .width((length * size).dp)
42 | .height(size.dp)
43 | .background(color = MaterialColors.Grey500)){
44 | Box(
45 | modifier = Modifier
46 | .width(size.dp)
47 | .height(size.dp)
48 | .offset { IntOffset(offsetX.toInt(), 0) }
49 | .draggable(draggableState, Orientation.Horizontal)
50 | .background(color = MaterialColors.Blue700)
51 | )
52 | }
53 |
54 | }
--------------------------------------------------------------------------------
/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/bg_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/bg_2.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/bg_avator.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/bg_avator.jpg
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_bin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/ic_bin.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_favorites.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/ic_favorites.png
--------------------------------------------------------------------------------
/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_run.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/ic_run.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/part_tab.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/drawable/part_tab.png
--------------------------------------------------------------------------------
/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/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values-land/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w1240dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 200dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values-w600dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 48dp
3 |
--------------------------------------------------------------------------------
/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/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 | 16dp
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | JetpackComposeStudy
3 | SecondActivity
4 |
5 | First Fragment
6 | Second Fragment
7 | Next
8 | Previous
9 |
10 | Hello first fragment
11 | Hello second fragment. Arg: %1$s
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
14 |
15 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app/src/test/java/com/funny/compose/study/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.funny.compose.study
2 |
3 | import com.funny.compose.study.ui.game.MoveDirection
4 | import com.funny.compose.study.ui.game.Point
5 | import com.funny.compose.study.ui.game.Snake
6 | import com.funny.compose.study.ui.game.SnakeState
7 | import org.junit.Test
8 |
9 | import java.text.SimpleDateFormat
10 | import java.util.*
11 |
12 | /**
13 | * Example local unit test, which will execute on the development machine (host).
14 | *
15 | * See [testing documentation](http://d.android.com/tools/testing).
16 | */
17 | class ExampleUnitTest {
18 | @Test
19 | fun stateTest(){
20 | val state = SnakeState(
21 | snake = Snake(LinkedList().apply {
22 | add(Point(200f,200f))
23 | add(Point(220f,200f))
24 | add(Point(240f,200f))
25 | },40f, MoveDirection.LEFT),
26 | size = 400 to 400,
27 | blockSize = 20f,
28 | food = Point(0f, 0f)
29 | )
30 | println(state.hashCode())
31 |
32 | val state1=state
33 | println(state1.hashCode())
34 |
35 | val state2 = state.copy(blockSize = 21f)
36 | println(state2.hashCode())
37 | }
38 |
39 | @Test
40 | fun createData(){
41 | val calendar = Calendar.getInstance(Locale.CHINA)
42 | println(getDateRange("2022-2-28","2022-5-29"))
43 | }
44 |
45 | // 获取日期范围(用于折线图)
46 | fun getDateRange(startDate: String, endDate: String): List {
47 | val sdf = SimpleDateFormat("yyyy-MM-dd")
48 | val ssdf = SimpleDateFormat("MM/dd")
49 | val calendar = Calendar.getInstance()
50 | val sDate = sdf.parse(startDate)
51 | calendar.time = sDate
52 | val calendarEnd = Calendar.getInstance()
53 | calendarEnd.time = sDate
54 | val dateList = ArrayList()
55 | dateList.add(ssdf.format(sDate))
56 | val edate = sdf.parse(endDate)
57 | while (calendarEnd.time.before(edate)) {
58 | calendarEnd.add(Calendar.DAY_OF_MONTH, 1)
59 | val tempDate = ssdf.format(calendarEnd.time)
60 | dateList.add(tempDate)
61 | }
62 | return dateList
63 | }
64 |
65 |
66 |
67 | }
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | buildscript {
3 | ext {
4 | compose_compiler_version = "1.4.0"
5 | kotlin_version = "1.8.0"
6 | }
7 | repositories {
8 | mavenLocal()
9 | //maven { url 'https://repo.huaweicloud.com/repository/maven/'}
10 |
11 | maven { url 'https://maven.aliyun.com/repository/public/' }
12 | maven { url 'https://maven.aliyun.com/repository/google/'}
13 | maven { url 'https://maven.aliyun.com/repository/jcenter/'}
14 | google()
15 | maven { url "https://jitpack.io" }
16 |
17 | }
18 | dependencies {
19 | classpath "com.android.tools.build:gradle:7.1.3"
20 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
21 |
22 | // NOTE: Do not place your application dependencies here; they belong
23 | // in the individual module build.gradle files
24 | }
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
31 | subprojects {
32 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
33 | kotlinOptions {
34 | if (project.findProperty("myapp.enableComposeCompilerReports") == "true") {
35 | freeCompilerArgs += ["-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ]
36 | freeCompilerArgs += ["-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" + project.buildDir.absolutePath + "/compose_metrics" ]
37 | }
38 | }
39 | }
40 | }
41 |
42 | // 上面的配置为启用 编译报告的配置,来自:
43 | //作者:Pika
44 | //链接:https://juejin.cn/post/7139328582836289566
45 | //来源:稀土掘金
46 | //著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
--------------------------------------------------------------------------------
/demo.apk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/demo.apk
--------------------------------------------------------------------------------
/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 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=false
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FunnySaltyFish/JetpackComposeStudy/82720a5da04936732b3ddbadb4fe1367c5279b4b/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 10 18:06:21 CST 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | // maven { url 'https://maven.aliyun.com/repository/public/' }
5 | // maven { url 'https://maven.aliyun.com/repository/google/'}
6 | // maven { url 'https://maven.aliyun.com/repository/jcenter/'}
7 | maven { url "https://jitpack.io" }
8 | google()
9 | mavenCentral()
10 | }
11 | }
12 | rootProject.name = "JetpackComposeStudy"
13 | include ':app'
14 | //include ':keep'
15 |
--------------------------------------------------------------------------------