├── .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 | 6 | 7 | 8 | 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 | 121 |
122 |
-------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 37 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 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 |

image.png

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 | 别不信biebuxin 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 |