├── .gitignore
├── README.md
├── app
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── example
│ │ └── compose
│ │ └── ExampleInstrumentedTest.kt
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── example
│ │ │ └── compose
│ │ │ ├── AnimationActivity.kt
│ │ │ ├── BookActivity.kt
│ │ │ ├── BookCanvasActivity.kt
│ │ │ ├── CircleBox.kt
│ │ │ ├── CircleMenuActivity.kt
│ │ │ ├── DragAndDropActivity.kt
│ │ │ ├── DraggableGrid.kt
│ │ │ ├── FavoritesScreen.kt
│ │ │ ├── GridDragDropState.kt
│ │ │ ├── GuaguaCardActivity.kt
│ │ │ ├── HomeScreen.kt
│ │ │ ├── LazyListActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── PinnedActivityActivity.kt
│ │ │ ├── RelectionUtil.java
│ │ │ ├── SearchScreen.kt
│ │ │ ├── SettingsScreen.kt
│ │ │ ├── StickyActivity.kt
│ │ │ ├── SwipeRefreshActivity.kt
│ │ │ ├── SwipeRefreshActivityV1.kt
│ │ │ ├── TabActivity.kt
│ │ │ ├── TextFiledActivity.kt
│ │ │ ├── TouchEventActivity.kt
│ │ │ └── ui
│ │ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Theme.kt
│ │ │ └── Type.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_launcher_background.xml
│ │ └── ic_launcher_foreground.xml
│ │ ├── 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
│ │ └── img_pic.webp
│ │ ├── mipmap-xxhdpi
│ │ ├── ic_launcher.webp
│ │ ├── ic_launcher_round.webp
│ │ ├── icon_cat.png
│ │ ├── icon_png_1.png
│ │ ├── img_02.png
│ │ └── img_checken.webp
│ │ ├── mipmap-xxxhdpi
│ │ ├── ic_launcher.webp
│ │ └── ic_launcher_round.webp
│ │ ├── values
│ │ ├── colors.xml
│ │ ├── strings.xml
│ │ └── themes.xml
│ │ └── xml
│ │ ├── backup_rules.xml
│ │ └── data_extraction_rules.xml
│ └── test
│ └── java
│ └── com
│ └── example
│ └── compose
│ └── ExampleUnitTest.kt
├── build.gradle.kts
├── fire_157.gif
├── fire_161.gif
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | # Gradle files
2 | .gradle/
3 | build/
4 |
5 | # Local configuration file (sdk path, etc)
6 | local.properties
7 |
8 | # Log/OS Files
9 | *.log
10 |
11 | # Android Studio generated files and folders
12 | captures/
13 | .externalNativeBuild/
14 | .cxx/
15 | *.apk
16 | output.json
17 |
18 | # IntelliJ
19 | *.iml
20 | .idea/
21 | misc.xml
22 | deploymentTargetDropDown.xml
23 | render.experimental.xml
24 |
25 | # Keystore files
26 | *.jks
27 | *.keystore
28 |
29 | # Google Services (e.g. APIs or Firebase)
30 | google-services.json
31 |
32 | # Android Profiling
33 | *.hprof
34 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ComposeLearning
2 | Compose Learning
3 |
4 | blog: https://juejin.cn/user/923245500710296/posts
5 |
6 | 
7 |
8 | 
9 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.androidApplication)
3 | alias(libs.plugins.jetbrainsKotlinAndroid)
4 | }
5 |
6 | android {
7 | namespace = "com.example.compose"
8 | compileSdk = 34
9 |
10 | defaultConfig {
11 | applicationId = "com.example.compose"
12 | minSdk = 24
13 | targetSdk = 34
14 | versionCode = 1
15 | versionName = "1.0"
16 |
17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary = true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | isMinifyEnabled = false
26 | proguardFiles(
27 | getDefaultProguardFile("proguard-android-optimize.txt"),
28 | "proguard-rules.pro"
29 | )
30 | }
31 | }
32 | compileOptions {
33 | sourceCompatibility = JavaVersion.VERSION_1_8
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | }
36 | kotlinOptions {
37 | jvmTarget = "1.8"
38 | }
39 | buildFeatures {
40 | compose = true
41 | }
42 | composeOptions {
43 | kotlinCompilerExtensionVersion = "1.5.1"
44 | }
45 | packaging {
46 | resources {
47 | excludes += "/META-INF/{AL2.0,LGPL2.1}"
48 | }
49 | }
50 | }
51 |
52 | dependencies {
53 |
54 | implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
55 | implementation(libs.androidx.core.ktx)
56 | implementation(libs.androidx.lifecycle.runtime.ktx)
57 | implementation(libs.androidx.activity.compose)
58 | implementation(platform(libs.androidx.compose.bom))
59 | implementation(libs.androidx.ui)
60 | implementation(libs.androidx.foundation)
61 | implementation(libs.androidx.webkit)
62 | implementation(libs.androidx.ui.graphics)
63 | implementation(libs.androidx.ui.tooling.preview)
64 | implementation(libs.androidx.material3)
65 | implementation(libs.androidx.constraintlayout)
66 | testImplementation(libs.junit)
67 | androidTestImplementation(libs.androidx.junit)
68 | androidTestImplementation(libs.androidx.espresso.core)
69 | androidTestImplementation(platform(libs.androidx.compose.bom))
70 | androidTestImplementation(libs.androidx.ui.test.junit4)
71 | debugImplementation(libs.androidx.ui.tooling)
72 | debugImplementation(libs.androidx.ui.test.manifest)
73 | }
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/example/compose/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
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.example.compose", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/AnimationActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.animation.core.animateIntOffsetAsState
8 | import androidx.compose.foundation.background
9 | import androidx.compose.foundation.clickable
10 | import androidx.compose.foundation.interaction.MutableInteractionSource
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Surface
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.setValue
23 | import androidx.compose.ui.ExperimentalComposeUiApi
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.layout.layout
27 | import androidx.compose.ui.unit.IntOffset
28 | import androidx.compose.ui.unit.dp
29 | import com.example.compose.ui.theme.ComposeTheme
30 |
31 |
32 | class AnimationActivity : ComponentActivity() {
33 |
34 | override fun onCreate(savedInstanceState: Bundle?) {
35 | super.onCreate(savedInstanceState)
36 | setContent {
37 | AnimationComposeTheme()
38 | }
39 | }
40 | }
41 |
42 |
43 | @OptIn(ExperimentalComposeUiApi::class)
44 | @Composable
45 | fun AnimationComposeTheme() {
46 | ComposeTheme {
47 | // A surface container using the 'background' color from the theme
48 | Surface(
49 | modifier = Modifier.fillMaxSize(),
50 | color = MaterialTheme.colorScheme.background,
51 | ) {
52 |
53 | var toggled by remember {
54 | mutableStateOf(false)
55 | }
56 | val interactionSource = remember {
57 | MutableInteractionSource()
58 | }
59 | Column(
60 | modifier = Modifier
61 | .padding(16.dp)
62 | .fillMaxSize()
63 | .clickable(indication = null, interactionSource = interactionSource) {
64 | toggled = !toggled
65 | }
66 | ) {
67 | val offsetTarget = if (toggled) {
68 | IntOffset(150, 150)
69 | } else {
70 | IntOffset.Zero
71 | }
72 | val offset = animateIntOffsetAsState(
73 | targetValue = offsetTarget,
74 | label = "offset"
75 | )
76 | Box(
77 | modifier = Modifier
78 | .size(100.dp)
79 | .background(Color.Yellow)
80 | )
81 | Box(
82 | modifier = Modifier
83 | .layout { measurable, constraints ->
84 | val offsetValue = if (isLookingAhead) offsetTarget else offset.value
85 | val placeable = measurable.measure(constraints)
86 | Log.i(TAG,"A");
87 | layout(
88 | placeable.width + offsetValue.x,
89 | placeable.height + offsetValue.y
90 | ) {
91 | placeable.placeRelative(offsetValue)
92 | Log.i(TAG,"b");
93 | }
94 | }
95 | .size(100.dp)
96 | .background(Color.Red)
97 | )
98 | Box(
99 | modifier = Modifier
100 | .size(100.dp)
101 | .background(Color.Cyan)
102 | )
103 | }
104 | }
105 | }
106 | }
107 |
108 |
109 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/BookActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.text.TextPaint
5 | import android.util.Log
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.gestures.detectDragGestures
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.ColumnScope
12 | import androidx.compose.foundation.layout.fillMaxSize
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.material3.MaterialTheme
16 | import androidx.compose.material3.Surface
17 | import androidx.compose.material3.Text
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.Stable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableIntStateOf
22 | import androidx.compose.runtime.mutableStateOf
23 | import androidx.compose.runtime.remember
24 | import androidx.compose.runtime.setValue
25 | import androidx.compose.ui.Alignment
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.draw.drawWithContent
28 | import androidx.compose.ui.geometry.Offset
29 | import androidx.compose.ui.geometry.Rect
30 | import androidx.compose.ui.geometry.Size
31 | import androidx.compose.ui.graphics.Canvas
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.graphics.ImageBitmap
34 | import androidx.compose.ui.graphics.Matrix
35 | import androidx.compose.ui.graphics.Paint
36 | import androidx.compose.ui.graphics.PaintingStyle
37 | import androidx.compose.ui.graphics.Path
38 | import androidx.compose.ui.graphics.PathOperation
39 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope
40 | import androidx.compose.ui.graphics.drawscope.clipPath
41 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
42 | import androidx.compose.ui.graphics.drawscope.rotate
43 | import androidx.compose.ui.graphics.drawscope.translate
44 | import androidx.compose.ui.graphics.drawscope.withTransform
45 | import androidx.compose.ui.input.pointer.pointerInput
46 | import androidx.compose.ui.layout.ContentScale
47 | import androidx.compose.ui.layout.onSizeChanged
48 | import androidx.compose.ui.node.DrawModifierNode
49 | import androidx.compose.ui.res.painterResource
50 | import androidx.compose.ui.text.drawText
51 | import androidx.compose.ui.unit.dp
52 | import com.example.compose.ui.theme.ComposeTheme
53 | import kotlin.math.PI
54 | import kotlin.math.abs
55 | import kotlin.math.atan
56 | import kotlin.math.atan2
57 | import kotlin.math.cos
58 | import kotlin.math.hypot
59 | import kotlin.math.min
60 | import kotlin.math.sin
61 | import kotlin.math.tan
62 |
63 |
64 | class BookActivity() : ComponentActivity() {
65 |
66 | private val TAG = "BookPager";
67 |
68 | override fun onCreate(savedInstanceState: Bundle?) {
69 | super.onCreate(savedInstanceState)
70 |
71 | setContent {
72 | ComposeTheme {
73 | // A surface container using the 'background' color from the theme
74 | Surface(
75 | modifier = Modifier.fillMaxSize(),
76 | color = MaterialTheme.colorScheme.background
77 | ) {
78 |
79 | BookPager{
80 | Image(
81 | modifier = Modifier.fillMaxWidth(),
82 | alignment = Alignment.Center,
83 | contentScale = ContentScale.FillWidth,
84 | painter = painterResource(id = R.mipmap.img_checken),
85 | contentDescription = ""
86 | )
87 | Text(
88 | modifier = Modifier
89 | .fillMaxWidth()
90 | .padding(5.dp),
91 | text = "\t\t我为什么要关心她?"
92 | +"\n\t\t你曾经说过,此生只爱她一个人的?因此你一直单身,对吧!"
93 | +"\n\t\t梁医生,你忘了?"
94 | +"\n\t\t我不是医生,我只是个打工仔,我也没有忘记,但是那份爱已经不会再有了"
95 | +"\n\t\t嗨,她可是主动让我找你哦!"
96 | +"\n\t\t听说,她小孩生病了!——张铭生说到。"
97 | +"\n\t\t她真会找时间,她永远会在最困难的时候找我,永远会在没有困难的时候离我而去。"
98 | +"\n\t\t事实或许相反,她离开你时已经是迫不得已,张铭生调高嗓门说到。"
99 | +"\n\t\t"
100 | )
101 | }
102 | BookPager{
103 | Image(
104 | modifier = Modifier.fillMaxWidth(),
105 | contentScale = ContentScale.FillWidth,
106 | alignment = Alignment.Center,
107 | painter = painterResource(id = R.mipmap.img_02),
108 | contentDescription = ""
109 | )
110 | Text(
111 | modifier = Modifier
112 | .fillMaxWidth()
113 | .padding(15.dp),
114 | text = "\t\t他清楚的知道,这个实验成功的概率是多么的低,他望着窗台的透进来的晨光,内心无比的焦灼,这才是早上九点种,但他仿佛看到了落日的余晖。"
115 | +"\n\t\t病床的男人抽搐不停,他已经没有多长时间了,长期的抽搐,导致他无法入眠,如果这种状态再延续下去,走向人生的重点已成必然。"
116 | +"\n\t\t梁雨,你有什么遗言么?"
117 | +"\n\t\t他从窗台方向转向过来,看见他的初中老同学张铭生。"
118 | +"\n\t\t我能有什么遗言,孤家寡人而已!"
119 | +"\n\t\t嗯~啊?不想给张桐说几句么?听说她离婚了"
120 | +"\n\t\t她说有很多话要对你说"
121 | )
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 |
129 | @Composable
130 | fun BookPager(page: Page = Page(), content: @Composable ColumnScope.() -> Unit) {
131 | var pointerOffset by remember {
132 | mutableStateOf(Offset(0f, 0f))
133 | }
134 | var dragState by remember {
135 | mutableIntStateOf(Page.STATE_IDLE)
136 | }
137 |
138 | Column(
139 | modifier = Modifier
140 | .fillMaxSize()
141 | .onSizeChanged {
142 | pointerOffset = Offset(it.width.toFloat(), it.height.toFloat())
143 | }
144 | .pointerInput("DraggerInput") {
145 | detectDragGestures(
146 | onDragStart = { it ->
147 |
148 | val offsetLeft = 150.dp.toPx();
149 | val offsetTop = 150.dp.toPx();
150 |
151 | dragState = if (Rect(
152 | size.width - offsetLeft,
153 | size.height - 100.dp.toPx(),
154 | size.width.toFloat(),
155 | size.height.toFloat()
156 | ).contains(it)
157 | ) {
158 | Page.STATE_DRAGING_BOTTOM
159 | } else if (Rect(
160 | size.width - offsetLeft,
161 | 0F,
162 | size.width.toFloat(),
163 | 100.dp.toPx()
164 | ).contains(it)
165 | ) {
166 | Page.STATE_DRAGING_TOP
167 | } else if (Rect(
168 | size.width - offsetLeft - 20.dp.toPx(),
169 | offsetTop,
170 | size.width - 20.dp.toPx(),
171 | size.height - offsetTop
172 | ).contains(it)
173 | ) {
174 | Page.STATE_DRAGING_MIDDLE
175 | } else {
176 | Page.STATE_DRAGING_EXCEEDE
177 | }
178 |
179 | },
180 | onDragEnd = {
181 | dragState = Page.STATE_IDLE
182 | pointerOffset = Offset(size.width.toFloat(), size.height.toFloat())
183 | }
184 | ) { change, dragAmount ->
185 | if (dragState == Page.STATE_DRAGING_BOTTOM || dragState == Page.STATE_DRAGING_MIDDLE || dragState == Page.STATE_DRAGING_TOP) {
186 | pointerOffset = change.position
187 | }
188 | }
189 | }
190 | .drawWithContent {
191 | if (dragState == Page.STATE_DRAGING_TOP) {
192 | drawTopRightRightDragState(this, pointerOffset, page)
193 | } else if (dragState == Page.STATE_DRAGING_BOTTOM) {
194 | drawBottomRightDragState(this, pointerOffset, page)
195 | } else if (dragState == Page.STATE_DRAGING_MIDDLE) {
196 | drawMiddleDragState(this, pointerOffset, page)
197 | } else {
198 | drawIdleState(this, page)
199 | }
200 | },
201 | content = content
202 | )
203 |
204 | }
205 |
206 | private fun drawIdleState(
207 | canvas: ContentDrawScope,
208 | page: Page
209 | ) {
210 |
211 | if (page.snapshot) {
212 | //如果不想StackOverflow的话,立即置为false,否则就做倒霉蛋吧
213 | page.snapshot = false
214 |
215 |
216 | val LayoutNodeDrawScopeKlass =
217 | Class.forName("androidx.compose.ui.node.LayoutNodeDrawScope")
218 | if (LayoutNodeDrawScopeKlass.isInstance(canvas)) {
219 | val imageBitmap = ImageBitmap(canvas.size.width.toInt(), canvas.size.height.toInt())
220 | val drawNodeField = LayoutNodeDrawScopeKlass.getDeclaredField("drawNode")
221 | drawNodeField.isAccessible = true
222 | val drawModifierNode = drawNodeField.get(canvas) as DrawModifierNode
223 | val performDrawMethod =
224 | LayoutNodeDrawScopeKlass.getDeclaredMethod(
225 | "performDraw",
226 | DrawModifierNode::class.java,
227 | Canvas::class.java
228 | )
229 | performDrawMethod.isAccessible = true
230 | val snapshotCanvas = Canvas(imageBitmap)
231 | val frontColor = page.frontColor
232 | page.frontColor = Color.Transparent
233 |
234 | snapshotCanvas.save()
235 |
236 | //翻转图像
237 | val matrix = Matrix()
238 | matrix[0,0] = -1f;
239 | matrix[3,0] = canvas.size.width
240 | snapshotCanvas.concat(matrix)
241 |
242 | performDrawMethod.invoke(canvas, drawModifierNode, snapshotCanvas)
243 |
244 | snapshotCanvas.restore()
245 | Log.d(TAG, "performDrawMethod = $imageBitmap")
246 | page.imageBitmap = imageBitmap
247 | page.frontColor = frontColor
248 | }
249 | }
250 |
251 | canvas.drawRect(page.frontColor)
252 | canvas.drawContent()
253 | }
254 |
255 | private fun drawMiddleDragState(
256 | canvas: ContentDrawScope,
257 | pointerOffset: Offset,
258 | page: Page
259 | ) {
260 |
261 | val size = canvas.size
262 | val foldPath = page.foldPath
263 | val pageOutline = page.pageOutline
264 | val blankOutline = page.blankOutline
265 | val clipPath = page.clipPath
266 |
267 | canvas.translate(size.width, 0F){
268 | val pointerPoint = Offset(
269 | pointerOffset.x - size.width,
270 | pointerOffset.y - 0
271 | )
272 |
273 | val verticalPoint = Offset(pointerPoint.x, 0F)
274 | val halfVerticalPoint = Offset(pointerPoint.x / 2F, 0F)
275 |
276 | foldPath.reset()
277 | foldPath.moveTo(verticalPoint.x, verticalPoint.y)
278 | foldPath.lineTo(halfVerticalPoint.x, halfVerticalPoint.y)
279 | foldPath.lineTo(halfVerticalPoint.x, size.height)
280 | foldPath.lineTo(verticalPoint.x, size.height)
281 | foldPath.close()
282 |
283 |
284 | pageOutline.reset()
285 | pageOutline.moveTo(-size.width, 0F)
286 | pageOutline.lineTo(-size.width, size.height)
287 | pageOutline.lineTo(0F, size.height)
288 | pageOutline.lineTo(0F, 0F)
289 | pageOutline.close()
290 |
291 | blankOutline.reset()
292 | blankOutline.moveTo(0F, 0F)
293 | blankOutline.lineTo(halfVerticalPoint.x, halfVerticalPoint.y)
294 | blankOutline.lineTo(halfVerticalPoint.x, size.height)
295 | blankOutline.lineTo(0F, size.height)
296 | blankOutline.close()
297 |
298 | clipPath.reset()
299 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
300 |
301 | canvas.withTransform({
302 | clipPath(clipPath)
303 | translate(-size.width, 0F)
304 | }){
305 | canvas.drawRect(page.frontColor)
306 | canvas.drawContent()
307 | }
308 |
309 | clipPath(foldPath){
310 | canvas.drawRect(page.backColor, topLeft = Offset(verticalPoint.x,0f),size= Size(size.width,size.height))
311 | page.imageBitmap?.let {
312 | drawImage(it, Offset(verticalPoint.x,0f))
313 | }
314 | }
315 |
316 | canvas.drawLine(start = Offset(size.width, pointerPoint.y), end=pointerPoint, color = Color.Red)
317 | canvas.drawLine(start =halfVerticalPoint, end=Offset(halfVerticalPoint.x, size.height), color = Color.Blue)
318 | canvas.drawLine(start = verticalPoint, end=Offset(verticalPoint.x, size.height), color = Color.Blue)
319 |
320 | }
321 | }
322 |
323 | private fun drawBottomRightDragState(
324 | canvas: ContentDrawScope,
325 | pointerOffset: Offset,
326 | page: Page
327 | ) {
328 |
329 | val size = canvas.size
330 | val blankOutline = page.blankOutline
331 | val foldPath = page.foldPath
332 | val clipPath = page.clipPath
333 | val pageOutline = page.pageOutline
334 |
335 |
336 | canvas.translate(size.width, size.height){
337 |
338 | var startPoint = Offset(0F, 0F)
339 |
340 | var pointerPoint = Offset(
341 | pointerOffset.x - size.width,
342 | pointerOffset.y - size.height
343 | )
344 |
345 | // atan2斜率范围在 -PI到PI之间,因此第三象限为atan2 = atan - PI, 那么atan = PI + atan2
346 | val pointerRotate = atan2(pointerPoint.y - startPoint.y, pointerPoint.x - startPoint.x) + PI
347 |
348 | val _xLength = hypot(
349 | pointerPoint.x - startPoint.x,
350 | pointerPoint.y - startPoint.y
351 | ) / cos(pointerRotate);
352 |
353 | var xLength = 0F
354 | var yLength = 0F
355 |
356 |
357 | if (_xLength > size.width*2) {
358 | //如果满足这个条件,意味着需要重新计算pointerPoint,因为没有形成垂直关系
359 | xLength = (size.width * 2);
360 | yLength = xLength / tan(pointerRotate).toFloat()
361 |
362 | var adjustRotate = atan(abs(yLength) / abs(xLength))
363 |
364 | val pointerDistance = abs(yLength * cos(adjustRotate))
365 | val y = abs(pointerDistance * sin(PI/2 - adjustRotate))
366 | val x = abs(pointerDistance * cos(PI/2 - adjustRotate))
367 |
368 | pointerPoint = Offset(
369 | -x.toFloat(),
370 | -y.toFloat()
371 | )
372 | }else{
373 | xLength = _xLength.toFloat()
374 | yLength = (xLength / tan(pointerRotate)).toFloat()
375 | }
376 |
377 | val XHalfAxisPoint = Offset(-xLength / 2F, 0F)
378 | val YHalfAxisPoint = Offset(0F, -yLength / 2F)
379 |
380 |
381 | val controlOffset = abs(Page.CONTROL_MAX_OFFSET * (2 * pointerPoint.x / size.width))
382 |
383 | val ld = Offset(
384 | (pointerPoint.x + XHalfAxisPoint.x) / 2F + controlOffset,
385 | (pointerPoint.y + XHalfAxisPoint.y) / 2F
386 | )
387 | val rt = Offset(
388 | (pointerPoint.x + YHalfAxisPoint.x) / 2F,
389 | (pointerPoint.y + YHalfAxisPoint.y) / 2F + controlOffset
390 | )
391 |
392 |
393 | val XControlAxisPoint = Offset(-xLength * 3 / 4F, 0F)
394 | val YControlfAxisPoint = Offset(0F, -yLength * 3 / 4F)
395 |
396 |
397 | foldPath.reset()
398 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
399 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
400 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
401 | foldPath.close()
402 |
403 |
404 | pageOutline.reset()
405 | pageOutline.moveTo(-size.width, -size.height)
406 | pageOutline.lineTo(-size.width, 0F)
407 | pageOutline.lineTo(0F, 0F)
408 | pageOutline.lineTo(0F, -size.height)
409 | pageOutline.close()
410 |
411 | blankOutline.reset()
412 | blankOutline.moveTo(0F, 0F)
413 | blankOutline.lineTo(YHalfAxisPoint.x, YHalfAxisPoint.y)
414 | blankOutline.lineTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
415 | blankOutline.close()
416 |
417 | clipPath.reset()
418 | //剔除被裁剪的部分blankOutline
419 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
420 |
421 | canvas.clipPath(clipPath){
422 | canvas.translate(-size.width, -size.height){
423 | canvas.drawRect(page.frontColor)
424 | canvas.drawContent()
425 | }
426 | }
427 |
428 |
429 | //绘制折角
430 | clipPath(foldPath){
431 | //这里我们铺满把,就不旋转灰色背景了,反正都要裁剪
432 | canvas.drawRect(page.backColor, topLeft = Offset(-size.width,-size.height),size= Size(size.width,size.height))
433 | val t = atan2(pointerPoint.y,(pointerPoint.x - XHalfAxisPoint.x)) + PI
434 | //我们要把(XHalfAxisPoint.x,0)作为旋转中心,这里要计算新的夹角,但是在第三象限计算夹角需要做转换,转为第一象限便于计算,当然也可以使用atan
435 | val degree = Math.toDegrees(t).toFloat()
436 | Log.d(TAG,"drawBottomRightDragState degree = $degree")
437 | rotate(degrees = Math.toDegrees(t).toFloat(),pivot = Offset(XHalfAxisPoint.x,0f)){ //图片按“露出”的1/2位置(XHalfAxisPoint.x,0f))旋转
438 | page.imageBitmap?.let {
439 | //由于原点在(size.width,size.height),所以,x轴为负值,当然,图片展示在地下是不对的,需要和灰色背景一样往上移动size.height
440 | // (我们这里使用的size.height,其实因为这里和image大小一样,理论上应该用image.width)
441 | drawImage(it, Offset(-xLength + 0.5f ,-size.height))
442 | }
443 | }
444 | }
445 |
446 |
447 | //绘制原点与触点的连线
448 | canvas.drawLine(start=Offset(0F, 0F), end = pointerPoint, color = Color.Red)
449 | //绘制切线
450 | canvas.drawLine(start=XHalfAxisPoint, end =YHalfAxisPoint, color = Color.Blue)
451 | //绘制1/2等距离切线
452 | canvas.drawLine(start=Offset(-xLength, 0F),end = Offset(0F, -yLength), color = Color.Blue)
453 | //绘制3/4等距离切线
454 | canvas.drawLine(start=XControlAxisPoint, end =YControlfAxisPoint, color = Color.Blue)
455 |
456 | }
457 |
458 |
459 | }
460 |
461 | private fun drawTopRightRightDragState(
462 | canvas: ContentDrawScope,
463 | pointerOffset: Offset,
464 | page: Page
465 | ) {
466 | val size = canvas.size
467 | val blankOutline = page.blankOutline
468 | val foldPath = page.foldPath
469 | val clipPath = page.clipPath
470 | val pageOutline = page.pageOutline
471 |
472 | canvas.translate(size.width, 0F) {
473 |
474 | var pointerPoint = Offset(
475 | pointerOffset.x - size.width,
476 | pointerOffset.y - 0
477 | )
478 | // atan2斜率范围在 -PI到PI之间,因此第三象限为atan2 = atan - PI, 那么atan = PI + atan2
479 |
480 | val startPoint = Offset(0F, 0F);
481 |
482 | val pointerRotate = atan2(pointerPoint.y - startPoint.y, pointerPoint.x - startPoint.x) + PI
483 |
484 |
485 | val _xLength = hypot(
486 | pointerPoint.x - startPoint.x,
487 | pointerPoint.y - startPoint.y
488 | ) / cos(pointerRotate)
489 |
490 | var xLength = 0F
491 | var yLength = 0F
492 |
493 |
494 | if (_xLength > size.width * 2.0) {
495 | //如果满足这个条件,意味着需要重新计算pointerPoint,因为没有形成垂直关系
496 | xLength = (size.width * 2);
497 | yLength = xLength / tan(pointerRotate).toFloat()
498 |
499 | var adjustRotate = atan(abs(yLength) / abs(xLength))
500 |
501 | val pointerDistance = abs(yLength * cos(adjustRotate))
502 | val y = abs(pointerDistance * sin(PI/2 - adjustRotate))
503 | val x = abs(pointerDistance * cos(PI/2 - adjustRotate))
504 |
505 | pointerPoint = Offset(
506 | -x.toFloat(),
507 | y.toFloat()
508 | )
509 |
510 | }else{
511 | xLength = _xLength.toFloat();
512 | yLength = xLength / tan(pointerRotate).toFloat()
513 |
514 | }
515 |
516 | val XHalfAxisPoint = Offset(-xLength / 2F, 0F)
517 | val YHalfAxisPoint = Offset(0F, -yLength / 2F)
518 |
519 | val controlOffset = abs(Page.CONTROL_MAX_OFFSET * (2 * pointerPoint.x / size.width))
520 |
521 | val ld = Offset(
522 | (pointerPoint.x + XHalfAxisPoint.x) / 2F + controlOffset,
523 | (pointerPoint.y + XHalfAxisPoint.y) / 2F
524 | )
525 | val rt = Offset(
526 | (pointerPoint.x + YHalfAxisPoint.x) / 2F,
527 | (pointerPoint.y + YHalfAxisPoint.y) / 2F - controlOffset
528 | )
529 |
530 | val XControlAxisPoint = Offset(-xLength * 3 / 4F, 0F)
531 | val YControlfAxisPoint = Offset(0F, -yLength * 3 / 4F)
532 |
533 |
534 | foldPath.reset()
535 |
536 |
537 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
538 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
539 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
540 | foldPath.close()
541 |
542 |
543 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
544 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
545 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
546 | foldPath.close()
547 |
548 |
549 | pageOutline.reset()
550 | pageOutline.moveTo(-size.width, 0F)
551 | pageOutline.lineTo(-size.width, size.height)
552 | pageOutline.lineTo(0F, size.height)
553 | pageOutline.lineTo(0F, 0F)
554 | pageOutline.close()
555 |
556 | blankOutline.reset()
557 | blankOutline.moveTo(0F, 0F)
558 | blankOutline.lineTo(YHalfAxisPoint.x, YHalfAxisPoint.y)
559 | blankOutline.lineTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
560 | blankOutline.close()
561 |
562 |
563 | clipPath.reset()
564 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
565 | canvas.clipPath(clipPath){
566 | canvas.translate(-size.width, 0F){
567 | canvas.drawRect(page.frontColor)
568 | canvas.drawContent()
569 | }
570 | }
571 |
572 |
573 | //绘制折角
574 | clipPath(foldPath){
575 | //这里我们铺满把,就不旋转灰色背景了,反正都要裁剪
576 | canvas.drawRect(page.backColor, topLeft = Offset(-size.width,0f),size= Size(size.width,size.height))
577 | val t = atan2(pointerPoint.y,(pointerPoint.x - XHalfAxisPoint.x)) + PI
578 | //我们要把(XHalfAxisPoint.x,0)作为旋转中心,这里要计算新的夹角,但是在第三象限计算夹角需要做转换,转为第一象限便于计算,当然也可以使用atan
579 | // val degree = Math.toDegrees(t).toFloat()
580 | // Log.d(TAG,"drawTopRightDragState degree = $degree")
581 | rotate(degrees = Math.toDegrees(t).toFloat(),pivot = Offset(XHalfAxisPoint.x,0f)){ //图片按“露出”的1/2位置(XHalfAxisPoint.x,0f))旋转
582 | page.imageBitmap?.let {
583 | //由于原点在(size.width,size.height),所以,x轴为负值,当然,图片展示在地下是不对的,需要和灰色背景一样往上移动size.height
584 | // (我们这里使用的size.height,其实因为这里和image大小一样,理论上应该用image.width)
585 | drawImage(it, Offset(-xLength+0.5f,0f))
586 | }
587 | }
588 | }
589 |
590 |
591 | canvas.drawLine(start = Offset(0F, 0F), end = pointerPoint, color = Color.Red)
592 | canvas.drawLine(start = Offset(-xLength, 0F), end = Offset(0F, -yLength), color = Color.Blue)
593 | canvas.drawLine(start = XHalfAxisPoint, end = YHalfAxisPoint, color = Color.Blue)
594 | canvas.drawLine(start = XControlAxisPoint,end = YControlfAxisPoint, color = Color.Blue)
595 |
596 | }
597 |
598 |
599 | }
600 |
601 | }
602 |
603 | @Stable
604 | class Page {
605 |
606 | val textPaint: android.graphics.Paint = TextPaint();
607 |
608 | var paint: Paint = Paint();
609 |
610 | var foldPath: Path = Path()
611 |
612 | var blankOutline = Path()
613 |
614 | var pageOutline = Path()
615 |
616 | var clipPath = Path()
617 |
618 | var frontColor = Color.White
619 | var backColor = Color.LightGray
620 |
621 | var snapshot = true
622 |
623 | var imageBitmap : ImageBitmap? = null;
624 |
625 | init {
626 | paint.style = PaintingStyle.Fill
627 | paint.color = Color.Red
628 | paint.isAntiAlias = true
629 |
630 | textPaint.textSize = 36F;
631 | textPaint.color = 0xFF000000.toInt();
632 | }
633 |
634 |
635 | companion object {
636 | const val STATE_IDLE = 0
637 | const val STATE_DRAGING_EXCEEDE = 1
638 | const val STATE_DRAGING_TOP = 2
639 | const val STATE_DRAGING_MIDDLE = 3
640 | const val STATE_DRAGING_BOTTOM = 4
641 |
642 | const val CONTROL_MAX_OFFSET = 40
643 |
644 | }
645 | }
646 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/BookCanvasActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.text.TextPaint
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.Canvas
8 | import androidx.compose.foundation.gestures.detectDragGestures
9 | import androidx.compose.foundation.layout.fillMaxHeight
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.material3.MaterialTheme
13 | import androidx.compose.material3.Surface
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.runtime.Stable
16 | import androidx.compose.runtime.getValue
17 | import androidx.compose.runtime.mutableIntStateOf
18 | import androidx.compose.runtime.mutableStateOf
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.runtime.setValue
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.geometry.Offset
23 | import androidx.compose.ui.geometry.Rect
24 | import androidx.compose.ui.graphics.Canvas
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.graphics.ImageBitmap
27 | import androidx.compose.ui.graphics.ImageBitmapConfig
28 | import androidx.compose.ui.graphics.Paint
29 | import androidx.compose.ui.graphics.PaintingStyle
30 | import androidx.compose.ui.graphics.Path
31 | import androidx.compose.ui.graphics.PathOperation
32 | import androidx.compose.ui.graphics.drawscope.DrawScope
33 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
34 | import androidx.compose.ui.input.pointer.pointerInput
35 | import androidx.compose.ui.layout.onSizeChanged
36 | import androidx.compose.ui.res.imageResource
37 | import androidx.compose.ui.unit.IntSize
38 | import androidx.compose.ui.unit.dp
39 | import com.example.compose.ui.theme.ComposeTheme
40 | import kotlin.math.PI
41 | import kotlin.math.abs
42 | import kotlin.math.atan2
43 | import kotlin.math.cos
44 | import kotlin.math.hypot
45 | import kotlin.math.min
46 | import kotlin.math.tan
47 |
48 |
49 | class BookCanvasActivity() : ComponentActivity() {
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 |
54 | setContent {
55 | ComposeTheme {
56 | // A surface container using the 'background' color from the theme
57 | Surface(
58 | modifier = Modifier.fillMaxSize(),
59 | color = MaterialTheme.colorScheme.background
60 | ) {
61 | BookCanvas()
62 | }
63 | }
64 | }
65 | }
66 |
67 |
68 | @Composable
69 | fun BookCanvas(bookPageNode: BookPageElement = BookPageElement()) {
70 |
71 | var pointerOffset by remember {
72 | mutableStateOf(Offset(0f, 0f))
73 | }
74 | var dragState by remember {
75 | mutableIntStateOf(BookPageElement.STATE_IDLE)
76 | }
77 |
78 | Canvas(
79 | modifier = Modifier
80 | .fillMaxWidth()
81 | .fillMaxHeight()
82 | .onSizeChanged {
83 | pointerOffset = Offset(it.width.toFloat(), it.height.toFloat())
84 | }
85 | .pointerInput("DraggerInput") {
86 | detectDragGestures(
87 | onDragStart = { it ->
88 |
89 | val offsetLeft = 150.dp.toPx();
90 | val offsetTop = 150.dp.toPx();
91 |
92 | dragState = if (Rect(
93 | size.width - offsetLeft,
94 | size.height - 100.dp.toPx(),
95 | size.width.toFloat(),
96 | size.height.toFloat()
97 | ).contains(it)
98 | ) {
99 | BookPageElement.STATE_DRAGING_BOTTOM
100 | } else if (Rect(
101 | size.width - offsetLeft,
102 | 0F,
103 | size.width.toFloat(),
104 | 100.dp.toPx()
105 | ).contains(it)
106 | ) {
107 | BookPageElement.STATE_DRAGING_TOP
108 | } else if (Rect(
109 | size.width - offsetLeft - 20.dp.toPx(),
110 | offsetTop,
111 | size.width - 20.dp.toPx(),
112 | size.height - offsetTop
113 | ).contains(it)
114 | ) {
115 | BookPageElement.STATE_DRAGING_MIDDLE
116 | } else {
117 | BookPageElement.STATE_DRAGING_EXCEEDE
118 | }
119 |
120 | },
121 | onDragEnd = {
122 | dragState = BookPageElement.STATE_IDLE
123 | pointerOffset = Offset(size.width.toFloat(), size.height.toFloat())
124 | }
125 | ) { change, dragAmount ->
126 | if (dragState == BookPageElement.STATE_DRAGING_BOTTOM || dragState == BookPageElement.STATE_DRAGING_MIDDLE || dragState == BookPageElement.STATE_DRAGING_TOP) {
127 | pointerOffset = change.position
128 | }
129 | }
130 | },
131 | ) {
132 |
133 |
134 | drawIntoCanvas { canvas ->
135 |
136 | val bitmap = ImageBitmap(
137 | size.width.toInt(),
138 | size.height.toInt(),
139 | config = ImageBitmapConfig.Argb8888,
140 | true
141 | )
142 | val bitmapCanvas = Canvas(bitmap)
143 | val imageBitmap = ImageBitmap.imageResource(resources, R.mipmap.img_pic)
144 | bitmapCanvas.drawImageRect(
145 | image = imageBitmap,
146 | dstSize = IntSize(size.width.toInt(), 300.dp.toPx().toInt()),
147 | paint = bookPageNode.paint
148 | )
149 |
150 | if (dragState == BookPageElement.STATE_DRAGING_TOP) {
151 | drawTopRightFoldBook(canvas, pointerOffset, bookPageNode, imageBitmap)
152 | } else if (dragState == BookPageElement.STATE_DRAGING_BOTTOM) {
153 | drawBottomFoldBook(canvas, pointerOffset, bookPageNode, imageBitmap)
154 | } else if (dragState == BookPageElement.STATE_DRAGING_MIDDLE) {
155 | drawMiddleFoldPage(canvas, pointerOffset, bookPageNode, imageBitmap)
156 | } else {
157 | drawIdleBook(canvas, pointerOffset, bookPageNode, imageBitmap)
158 | }
159 |
160 | }
161 |
162 | }
163 |
164 | }
165 |
166 | private fun DrawScope.drawMiddleFoldPage(
167 | canvas: Canvas,
168 | pointerOffset: Offset,
169 | bookPageNode: BookPageElement,
170 | imageBitmap: ImageBitmap
171 | ) {
172 |
173 | val paint = bookPageNode.paint
174 | val foldPath = bookPageNode.foldPath
175 | val pageOutline = bookPageNode.pageOutline
176 | val blankOutline = bookPageNode.blankOutline
177 | val clipPath = bookPageNode.clipPath
178 |
179 | canvas.save()
180 |
181 | canvas.translate(size.width, 0F)
182 |
183 | val pointerPoint = Offset(
184 | pointerOffset.x - size.width,
185 | pointerOffset.y - 0
186 | )
187 |
188 | val verticalPoint = Offset(pointerPoint.x, 0F)
189 | val halfVerticalPoint = Offset(pointerPoint.x / 2F, 0F)
190 |
191 | foldPath.reset()
192 | foldPath.moveTo(verticalPoint.x, verticalPoint.y)
193 | foldPath.lineTo(halfVerticalPoint.x, halfVerticalPoint.y)
194 | foldPath.lineTo(halfVerticalPoint.x, size.height)
195 | foldPath.lineTo(verticalPoint.x, size.height)
196 | foldPath.close()
197 |
198 |
199 | pageOutline.reset()
200 | pageOutline.moveTo(-size.width, 0F)
201 | pageOutline.lineTo(-size.width, size.height)
202 | pageOutline.lineTo(0F, size.height)
203 | pageOutline.lineTo(0F, 0F)
204 | pageOutline.close()
205 |
206 | blankOutline.reset()
207 | blankOutline.moveTo(0F, 0F)
208 | blankOutline.lineTo(halfVerticalPoint.x, halfVerticalPoint.y)
209 | blankOutline.lineTo(halfVerticalPoint.x, size.height)
210 | blankOutline.lineTo(0F, size.height)
211 | blankOutline.close()
212 |
213 | clipPath.reset()
214 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
215 | canvas.clipPath(clipPath)
216 |
217 | val currentColor = paint.color
218 | paint.color = Color.Cyan
219 | canvas.drawRect(Rect(0F, 0F, -size.width, size.height), paint)
220 | paint.color = currentColor
221 |
222 | canvas.save()
223 | canvas.translate(-size.width, 0F)
224 | canvas.drawImageRect(imageBitmap, paint = paint)
225 | canvas.restore()
226 |
227 | canvas.drawPath(foldPath, paint)
228 |
229 | canvas.drawLine(Offset(size.width, pointerPoint.y), pointerPoint, paint)
230 | canvas.drawLine(halfVerticalPoint, Offset(halfVerticalPoint.x, size.height), paint)
231 | canvas.drawLine(verticalPoint, Offset(verticalPoint.x, size.height), paint)
232 |
233 | canvas.restore()
234 |
235 | }
236 |
237 |
238 | private fun DrawScope.drawTopRightFoldBook(
239 | canvas: Canvas,
240 | pointerOffset: Offset,
241 | bookPageNode: BookPageElement,
242 | imageBitmap: ImageBitmap
243 | ) {
244 | val paint = bookPageNode.paint
245 | val blankOutline = bookPageNode.blankOutline
246 | val foldPath = bookPageNode.foldPath
247 | val clipPath = bookPageNode.clipPath
248 | val pageOutline = bookPageNode.pageOutline
249 |
250 | canvas.save()
251 | canvas.translate(size.width, 0F)
252 |
253 | var pointerPoint = Offset(
254 | pointerOffset.x - size.width,
255 | pointerOffset.y - 0
256 | )
257 | // atan2斜率范围在 -PI到PI之间,因此第三象限为atan2 = atan - PI, 那么atan = PI + atan2
258 |
259 | val startPoint = Offset(0F, 0F);
260 |
261 | val pointerRotate = atan2(pointerPoint.y - startPoint.y, pointerPoint.x - startPoint.x) + PI
262 |
263 | val xLength = min(
264 | hypot(
265 | pointerPoint.x - startPoint.x,
266 | pointerPoint.y - startPoint.y
267 | ) / cos(pointerRotate),
268 | size.width * 2.0
269 | ).toFloat()
270 |
271 | //由于计算出来的Y按0,0点计算的,因此需要转换为
272 | val yLength = xLength / tan(pointerRotate).toFloat()
273 |
274 | // xLength / YLength = (xLength - abs(pointerPoint.x)) / maxY
275 |
276 | val minY = -yLength * (xLength - abs(pointerPoint.x)) / xLength;
277 |
278 | if (minY < pointerPoint.y) {
279 | pointerPoint = Offset(
280 | pointerPoint.x,
281 | minY
282 | )
283 | }
284 |
285 | val XHalfAxisPoint = Offset(-xLength / 2F, 0F)
286 | val YHalfAxisPoint = Offset(0F, -yLength / 2F)
287 |
288 | val controlOffset = abs(50 * (2 * pointerPoint.x / size.width))
289 |
290 | val ld = Offset(
291 | (pointerPoint.x + XHalfAxisPoint.x) / 2F + controlOffset,
292 | (pointerPoint.y + XHalfAxisPoint.y) / 2F
293 | )
294 | val rt = Offset(
295 | (pointerPoint.x + YHalfAxisPoint.x) / 2F,
296 | (pointerPoint.y + YHalfAxisPoint.y) / 2F - controlOffset
297 | )
298 |
299 | val XControlAxisPoint = Offset(-xLength * 3 / 4F, 0F)
300 | val YControlfAxisPoint = Offset(0F, -yLength * 3 / 4F)
301 |
302 |
303 | foldPath.reset()
304 |
305 |
306 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
307 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
308 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
309 | foldPath.close()
310 |
311 |
312 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
313 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
314 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
315 | foldPath.close()
316 |
317 |
318 | pageOutline.reset()
319 | pageOutline.moveTo(-size.width, 0F)
320 | pageOutline.lineTo(-size.width, size.height)
321 | pageOutline.lineTo(0F, size.height)
322 | pageOutline.lineTo(0F, 0F)
323 | pageOutline.close()
324 |
325 | blankOutline.reset()
326 | blankOutline.moveTo(0F, 0F)
327 | blankOutline.lineTo(YHalfAxisPoint.x, YHalfAxisPoint.y)
328 | blankOutline.lineTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
329 | blankOutline.close()
330 |
331 | clipPath.reset()
332 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
333 | canvas.clipPath(clipPath)
334 |
335 | val currentColor = paint.color
336 | paint.color = Color.Cyan
337 | canvas.drawRect(Rect(0F, 0F, -size.width, size.height), paint)
338 | paint.color = currentColor
339 |
340 | canvas.save()
341 | canvas.translate(-size.width, 0F)
342 | canvas.drawImageRect(imageBitmap, paint = paint)
343 | canvas.restore()
344 |
345 | canvas.drawPath(foldPath, paint)
346 |
347 | canvas.drawLine(Offset(-xLength, 0F), Offset(0F, -yLength), paint)
348 | canvas.drawLine(XHalfAxisPoint, YHalfAxisPoint, paint)
349 | canvas.drawLine(XControlAxisPoint, YControlfAxisPoint, paint)
350 | canvas.drawLine(Offset(0F, 0F), pointerPoint, paint)
351 |
352 | canvas.restore()
353 | }
354 |
355 | private fun DrawScope.drawBottomFoldBook(
356 | canvas: Canvas,
357 | pointerOffset: Offset,
358 | bookPageNode: BookPageElement,
359 | imageBitmap: ImageBitmap
360 | ) {
361 |
362 | val paint = bookPageNode.paint
363 | val blankOutline = bookPageNode.blankOutline
364 | val foldPath = bookPageNode.foldPath
365 | val clipPath = bookPageNode.clipPath
366 | val pageOutline = bookPageNode.pageOutline
367 |
368 | canvas.save()
369 | canvas.translate(size.width, size.height)
370 |
371 | var startPoint = Offset(0F, 0F)
372 |
373 | var pointerPoint = Offset(
374 | pointerOffset.x - size.width,
375 | pointerOffset.y - size.height
376 | )
377 |
378 |
379 | // atan2斜率范围在 -PI到PI之间,因此第三象限为atan2 = atan - PI, 那么atan = PI + atan2
380 |
381 | val pointerRotate = atan2(pointerPoint.y - startPoint.y, pointerPoint.x - startPoint.x) + PI
382 |
383 | val xLength = min(
384 | hypot(
385 | pointerPoint.x - startPoint.x,
386 | pointerPoint.y - startPoint.y
387 | ) / cos(pointerRotate),
388 | size.width * 2.0
389 | ).toFloat()
390 | val yLength = (xLength / tan(pointerRotate)).toFloat()
391 |
392 | // xLength / YLength = (xLength - abs(pointerPoint.x)) / maxY
393 |
394 | val minY = -yLength * (xLength - abs(pointerPoint.x)) / xLength;
395 |
396 | if (minY > pointerPoint.y) {
397 | pointerPoint = Offset(
398 | pointerPoint.x,
399 | minY
400 | )
401 | }
402 |
403 | val XHalfAxisPoint = Offset(-xLength / 2F, 0F)
404 | val YHalfAxisPoint = Offset(0F, -yLength / 2F)
405 |
406 |
407 | val controlOffset = abs(55 * (2 * pointerPoint.x / size.width))
408 |
409 | val ld = Offset(
410 | (pointerPoint.x + XHalfAxisPoint.x) / 2F + controlOffset,
411 | (pointerPoint.y + XHalfAxisPoint.y) / 2F
412 | )
413 | val rt = Offset(
414 | (pointerPoint.x + YHalfAxisPoint.x) / 2F,
415 | (pointerPoint.y + YHalfAxisPoint.y) / 2F + controlOffset
416 | )
417 |
418 |
419 | val XControlAxisPoint = Offset(-xLength * 3 / 4F, 0F)
420 | val YControlfAxisPoint = Offset(0F, -yLength * 3 / 4F)
421 |
422 |
423 | foldPath.reset()
424 | foldPath.moveTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
425 | foldPath.quadraticBezierTo(ld.x, ld.y, pointerPoint.x, pointerPoint.y)
426 | foldPath.quadraticBezierTo(rt.x, rt.y, YHalfAxisPoint.x, YHalfAxisPoint.y)
427 | foldPath.close()
428 |
429 |
430 | pageOutline.reset()
431 | pageOutline.moveTo(-size.width, -size.height)
432 | pageOutline.lineTo(-size.width, 0F)
433 | pageOutline.lineTo(0F, 0F)
434 | pageOutline.lineTo(0F, -size.height)
435 | pageOutline.close()
436 |
437 | blankOutline.reset()
438 | blankOutline.moveTo(0F, 0F)
439 | blankOutline.lineTo(YHalfAxisPoint.x, YHalfAxisPoint.y)
440 | blankOutline.lineTo(XHalfAxisPoint.x, XHalfAxisPoint.y)
441 | blankOutline.close()
442 |
443 | clipPath.reset()
444 | //剔除被裁剪的部分blankOutline
445 | clipPath.op(pageOutline, blankOutline, PathOperation.Difference)
446 | canvas.clipPath(clipPath)
447 |
448 | val currentColor = paint.color
449 | paint.color = Color.Cyan
450 | canvas.drawRect(Rect(0F, 0F, -size.width, -size.height), paint)
451 | paint.color = currentColor
452 |
453 | canvas.save()
454 | canvas.translate(-size.width, -size.height)
455 | canvas.drawImageRect(imageBitmap, paint = paint)
456 | canvas.restore()
457 |
458 | //绘制折角
459 | canvas.drawPath(foldPath, paint)
460 |
461 | //绘制原点与触点的连线
462 | canvas.drawLine(Offset(0F, 0F), pointerPoint, paint)
463 | //绘制切线
464 | canvas.drawLine(XHalfAxisPoint, YHalfAxisPoint, paint)
465 | //绘制1/2等距离切线
466 | canvas.drawLine(Offset(-xLength, 0F), Offset(0F, -yLength), paint)
467 | //绘制3/4等距离切线
468 | canvas.drawLine(XControlAxisPoint, YControlfAxisPoint, paint)
469 | canvas.restore()
470 | }
471 |
472 | private fun DrawScope.drawIdleBook(
473 | canvas: Canvas,
474 | pointerOffset: Offset,
475 | bookPageNode: BookPageElement,
476 | imageBitmap: ImageBitmap
477 | ) {
478 | val paint = bookPageNode.paint
479 | canvas.save()
480 | canvas.translate(size.width, size.height)
481 | val currentColor = paint.color
482 | paint.color = Color.Cyan
483 | canvas.drawRect(Rect(0F, 0F, -size.width, -size.height), paint)
484 | paint.color = currentColor
485 | canvas.restore()
486 |
487 | canvas.drawImageRect(imageBitmap, paint = paint)
488 |
489 | }
490 |
491 |
492 | }
493 |
494 | @Stable
495 | class BookPageElement {
496 |
497 | val textPaint: android.graphics.Paint = TextPaint();
498 |
499 | var paint: Paint = Paint();
500 |
501 | var foldPath: Path = Path()
502 |
503 | var blankOutline = Path()
504 |
505 | var pageOutline = Path()
506 |
507 | var clipPath = Path()
508 |
509 |
510 |
511 | init {
512 | paint.style = PaintingStyle.Fill
513 | paint.color = Color.Red
514 | paint.isAntiAlias = true
515 |
516 | textPaint.textSize = 36F;
517 | textPaint.color = 0xFF000000.toInt();
518 | }
519 |
520 |
521 | companion object {
522 | const val STATE_IDLE = 0
523 | const val STATE_DRAGING_EXCEEDE = 1
524 | const val STATE_DRAGING_TOP = 2
525 | const val STATE_DRAGING_MIDDLE = 3
526 | const val STATE_DRAGING_BOTTOM = 4
527 | }
528 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/CircleBox.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 |
4 | import android.util.Log
5 | import androidx.compose.foundation.gestures.detectDragGestures
6 | import androidx.compose.foundation.layout.LayoutScopeMarker
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.Immutable
9 | import androidx.compose.runtime.Stable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableFloatStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.input.pointer.pointerInput
17 | import androidx.compose.ui.layout.Layout
18 | import androidx.compose.ui.layout.Measurable
19 | import androidx.compose.ui.layout.MeasurePolicy
20 | import androidx.compose.ui.layout.MeasureResult
21 | import androidx.compose.ui.layout.MeasureScope
22 | import androidx.compose.ui.layout.Placeable
23 | import androidx.compose.ui.node.ModifierNodeElement
24 | import androidx.compose.ui.node.ParentDataModifierNode
25 | import androidx.compose.ui.platform.InspectorInfo
26 | import androidx.compose.ui.platform.debugInspectorInfo
27 | import androidx.compose.ui.unit.Constraints
28 | import androidx.compose.ui.unit.Density
29 | import androidx.compose.ui.unit.IntOffset
30 | import androidx.compose.ui.unit.IntSize
31 | import androidx.compose.ui.unit.LayoutDirection
32 | import java.lang.Math.min
33 | import kotlin.math.atan2
34 | import kotlin.math.cos
35 | import kotlin.math.max
36 | import kotlin.math.sin
37 |
38 |
39 | @Composable
40 | inline fun CircleBox(
41 | modifier: Modifier = Modifier,
42 | propagateMinConstraints: Boolean = false,
43 | content: @Composable CircleBoxScope.() -> Unit
44 | ) {
45 |
46 | var rotateDegree by remember {
47 | mutableFloatStateOf(0F)
48 | }
49 |
50 | val measurePolicy = rememberSwipeRefreshMeasurePolicy(Alignment.Center, propagateMinConstraints,rotateDegree)
51 |
52 | Layout(
53 | content = { CircleBoxScopeInstance.content() },
54 | measurePolicy = measurePolicy,
55 | modifier = modifier then Modifier.pointerInput("CircleBoxInputEvent"){
56 | var startDegree = 0F
57 |
58 | detectDragGestures { change, dragAmount ->
59 | val dr = atan2(change.position.y.toDouble() - size.height/2f, change.position.x.toDouble() - size.width/2f);
60 | var toFloat = (dr - startDegree).toFloat()
61 | if(toFloat == Float.POSITIVE_INFINITY || toFloat == Float.NEGATIVE_INFINITY){
62 | toFloat = 0F
63 | }
64 | rotateDegree += toFloat
65 | startDegree = dr.toFloat();
66 | }
67 | }
68 | )
69 | }
70 |
71 | @PublishedApi
72 | @Composable
73 | internal fun rememberSwipeRefreshMeasurePolicy(
74 | alignment: Alignment,
75 | propagateMinConstraints: Boolean,
76 | rotateDegree: Float
77 | ) = remember(alignment, propagateMinConstraints,rotateDegree) {
78 | circleBoxMeasurePolicy(alignment, propagateMinConstraints,rotateDegree)
79 | }
80 |
81 | internal class CircleBoxMeasurePolicy (
82 | var alignment: Alignment,
83 | var propagateMinConstraints: Boolean = false,
84 | var rotateDegree: Float = 0F
85 | ): MeasurePolicy {
86 | override fun MeasureScope.measure(
87 | measurables: List,
88 | constraints: Constraints
89 | ): MeasureResult {
90 |
91 | Log.d(TAG,"rotateDegree = $rotateDegree")
92 |
93 | if (measurables.isEmpty()) {
94 | return layout(
95 | constraints.minWidth,
96 | constraints.minHeight
97 | ) {}
98 | }
99 |
100 | val contentConstraints = if (propagateMinConstraints) {
101 | constraints
102 | } else {
103 | constraints.copy(minWidth = 0, minHeight = 0)
104 | }
105 |
106 | if (measurables.size == 1) {
107 | val measurable = measurables[0]
108 | val boxWidth: Int
109 | val boxHeight: Int
110 | val placeable: Placeable
111 | if (!measurable.matchesParentSize) {
112 | placeable = measurable.measure(contentConstraints)
113 | boxWidth = max(constraints.minWidth, placeable.width)
114 | boxHeight = max(constraints.minHeight, placeable.height)
115 | } else {
116 | boxWidth = constraints.minWidth
117 | boxHeight = constraints.minHeight
118 | placeable = measurable.measure(
119 | Constraints.fixed(constraints.minWidth, constraints.minHeight)
120 | )
121 | }
122 | return layout(boxWidth, boxHeight) {
123 | placeInBox(placeable, measurable, layoutDirection, boxWidth, boxHeight, alignment)
124 | }
125 | }
126 |
127 | val placeables = arrayOfNulls(measurables.size)
128 | // First measure non match parent size children to get the size of the Box.
129 | var boxWidth = constraints.minWidth
130 | var boxHeight = constraints.minHeight
131 | measurables.forEachIndexed { index, measurable ->
132 | if (!measurable.matchesParentSize) {
133 | val placeable = measurable.measure(contentConstraints)
134 | placeables[index] = placeable
135 | boxWidth = max(boxWidth, placeable.width)
136 | boxHeight = max(boxHeight, placeable.height)
137 | }
138 | }
139 |
140 |
141 | val radian = Math.toRadians((360 / placeables.size).toDouble()) ;
142 | val radius = min(constraints.minWidth, constraints.minHeight) / 2;
143 | // Specify the size of the Box and position its children.
144 | return layout(boxWidth, boxHeight) {
145 | placeables.forEachIndexed { index, placeable ->
146 | placeable as Placeable
147 | val innerRadius = radius - max(placeable.height,placeable.width);
148 | val x = cos(radian * index + rotateDegree) * innerRadius + boxWidth / 2F - placeable.width / 2F;
149 | val y = sin(radian * index + rotateDegree) * innerRadius + boxHeight / 2F - placeable.height / 2F;
150 | placeable.place(IntOffset(x.toInt(), y.toInt()))
151 |
152 | }
153 | }
154 | }
155 |
156 | }
157 | internal fun circleBoxMeasurePolicy(
158 | alignment: Alignment,
159 | propagateMinConstraints: Boolean,
160 | rotateDegree: Float
161 | ) =
162 | CircleBoxMeasurePolicy(alignment,propagateMinConstraints,rotateDegree)
163 |
164 |
165 | @Composable
166 | fun CircleBox(modifier: Modifier) {
167 | Layout({}, measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier)
168 | }
169 |
170 | internal val EmptyBoxMeasurePolicy = MeasurePolicy { _, constraints ->
171 | layout(constraints.minWidth, constraints.minHeight) {}
172 | }
173 |
174 | @LayoutScopeMarker
175 | @Immutable
176 | interface CircleBoxScope {
177 | @Stable
178 | fun Modifier.align(alignment: Alignment): Modifier
179 | @Stable
180 | fun Modifier.matchParentSize(): Modifier
181 | }
182 |
183 | internal object CircleBoxScopeInstance : CircleBoxScope {
184 | @Stable
185 | override fun Modifier.align(alignment: Alignment) = this.then(
186 | CircleBoxChildDataElement(
187 | alignment = alignment,
188 | matchParentSize = false,
189 | inspectorInfo = debugInspectorInfo {
190 | name = "align"
191 | value = alignment
192 | }
193 | ))
194 |
195 | @Stable
196 | override fun Modifier.matchParentSize() = this.then(
197 | CircleBoxChildDataElement(
198 | alignment = Alignment.Center,
199 | matchParentSize = true,
200 | inspectorInfo = debugInspectorInfo {
201 | name = "matchParentSize"
202 | }
203 | ))
204 | }
205 |
206 | private val Measurable.boxChildDataNode: CircleBoxChildDataNode? get() = parentData as? CircleBoxChildDataNode
207 | private val Measurable.matchesParentSize: Boolean get() = boxChildDataNode?.matchParentSize ?: false
208 |
209 | private class CircleBoxChildDataElement(
210 | val alignment: Alignment,
211 | val matchParentSize: Boolean,
212 | val inspectorInfo: InspectorInfo.() -> Unit
213 |
214 | ) : ModifierNodeElement() {
215 | override fun create(): CircleBoxChildDataNode {
216 | return CircleBoxChildDataNode(alignment, matchParentSize)
217 | }
218 |
219 | override fun update(node: CircleBoxChildDataNode) {
220 | node.alignment = alignment
221 | node.matchParentSize = matchParentSize
222 | }
223 |
224 | override fun InspectorInfo.inspectableProperties() {
225 | inspectorInfo()
226 | }
227 |
228 | override fun hashCode(): Int {
229 | var result = alignment.hashCode()
230 | result = 31 * result + matchParentSize.hashCode()
231 | return result
232 | }
233 |
234 | override fun equals(other: Any?): Boolean {
235 | if (this === other) return true
236 | val otherModifier = other as? CircleBoxChildDataElement ?: return false
237 | return alignment == otherModifier.alignment &&
238 | matchParentSize == otherModifier.matchParentSize
239 | }
240 | }
241 |
242 | private fun Placeable.PlacementScope.placeInBox(
243 | placeable: Placeable,
244 | measurable: Measurable,
245 | layoutDirection: LayoutDirection,
246 | boxWidth: Int,
247 | boxHeight: Int,
248 | alignment: Alignment
249 | ) {
250 | val childAlignment = measurable.boxChildDataNode?.alignment ?: alignment
251 | val position = childAlignment.align(
252 | IntSize(placeable.width, placeable.height),
253 | IntSize(boxWidth, boxHeight),
254 | layoutDirection
255 | )
256 | placeable.place(position)
257 | }
258 |
259 | private class CircleBoxChildDataNode(
260 | var alignment: Alignment,
261 | var matchParentSize: Boolean,
262 | ) : ParentDataModifierNode, Modifier.Node() {
263 | override fun Density.modifyParentData(parentData: Any?) = this@CircleBoxChildDataNode
264 | }
265 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/CircleMenuActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.width
10 | import androidx.compose.material3.MaterialTheme
11 | import androidx.compose.material3.Surface
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.Composer
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.draw.drawBehind
18 | import androidx.compose.ui.graphics.Color
19 | import androidx.compose.ui.input.pointer.PointerEventType
20 | import androidx.compose.ui.input.pointer.pointerInput
21 | import androidx.compose.ui.layout.Layout
22 | import androidx.compose.ui.unit.dp
23 | import com.example.compose.ui.theme.ComposeTheme
24 |
25 |
26 | class CircleMenuActivity : ComponentActivity() {
27 |
28 | override fun onCreate(savedInstanceState: Bundle?) {
29 | super.onCreate(savedInstanceState)
30 |
31 | // val menuItems = arrayOf("A", "B", "C", "D", "E", "F","G")
32 | val menuItems = mapOf(
33 | "A" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
34 | "B" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
35 | "C" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
36 | "D" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
37 | "E" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
38 | "F" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F),
39 | "G" to Color.hsl((360 * Math.random()).toFloat(), 0.5F, 0.5F)
40 | )
41 |
42 | setContent {
43 | ComposeTheme {
44 | // A surface container using the 'background' color from the theme
45 | Surface(
46 | modifier = Modifier.fillMaxSize(),
47 | color = MaterialTheme.colorScheme.background
48 | ) {
49 | CircleBox(modifier = Modifier.fillMaxSize()) {
50 | menuItems.forEach {
51 | MenuBox(it.key, it.value);
52 | }
53 | }
54 |
55 | }
56 | }
57 | }
58 | }
59 | }
60 |
61 | @Composable
62 | fun MenuBox(menu: String, color: Color) {
63 | Box(
64 | modifier = Modifier
65 | .width(50.dp)
66 | .height(50.dp)
67 | .drawBehind {
68 | drawCircle(color)
69 | },
70 | contentAlignment = Alignment.Center
71 | ) {
72 | Text(text = menu);
73 | }
74 | }
75 |
76 |
77 |
78 | @Composable
79 | fun MyBasicColumn(
80 | modifier: Modifier = Modifier,
81 | content: @Composable () -> Unit
82 | ) {
83 | Layout(
84 | modifier = modifier,
85 | content = content
86 | ) { measurables, constraints ->
87 | // Don't constrain child views further, measure them with given constraints
88 | // List of measured children
89 | val placeables = measurables.map { measurable ->
90 | // Measure each children
91 | measurable.measure(constraints)
92 | }
93 |
94 | // Set the size of the layout as big as it can
95 | layout(constraints.maxWidth, constraints.maxHeight) {
96 | // Track the y co-ord we have placed children up to
97 | var yPosition = 0
98 |
99 | // Place children in the parent layout
100 | placeables.forEach { placeable ->
101 | // Position item on the screen
102 | placeable.placeRelative(x = 0, y = yPosition)
103 |
104 | // Record the y co-ord placed up to
105 | yPosition += placeable.height
106 | }
107 | }
108 | }
109 | }
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/DragAndDropActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.fillMaxSize
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.material3.ExperimentalMaterial3Api
13 | import androidx.compose.material3.MaterialTheme
14 | import androidx.compose.material3.Scaffold
15 | import androidx.compose.material3.Surface
16 | import androidx.compose.material3.Text
17 | import androidx.compose.material3.TopAppBar
18 | import androidx.compose.material3.TopAppBarDefaults
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.res.stringResource
28 | import androidx.compose.ui.unit.dp
29 | import com.example.compose.ui.theme.DragAndDropTheme
30 |
31 | // 官方demo
32 | // https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyGridDragAndDropDemo.kt;bpv=1
33 | class DragAndDropActivity : ComponentActivity() {
34 |
35 | override fun onCreate(savedInstanceState: Bundle?) {
36 | super.onCreate(savedInstanceState)
37 | setContent {
38 | var items by remember { mutableStateOf(createItems(18)) }
39 | DraggableGrid(items = items, itemKey = { index, item ->
40 | item.id
41 | }, onMove = { dragingIndex, targetIndex ->
42 | val mutableList = items.toMutableList().apply {
43 | add(targetIndex, removeAt(dragingIndex)) // 交换位置
44 | }
45 | items = mutableList // 更新状态,触发动画
46 |
47 | }) { item, isDragging ->
48 | Box(modifier = Modifier
49 | .background(item.color)
50 | .height(100.dp),
51 | contentAlignment = Alignment.Center
52 | ) {
53 | Text(text = "${item.id}")
54 | }
55 | }
56 |
57 | }
58 | }
59 |
60 | fun createItems(count: Int): List- {
61 | return (1..count).map {
62 | Item(it, colors[it % colors.size])
63 | }
64 | }
65 |
66 | }
67 |
68 | data class Item(
69 | val id: Int,
70 | val color: Color
71 | )
72 |
73 | private val colors = listOf(
74 | Color(0xFFF44336),
75 | Color(0xFFE91E63),
76 | Color(0xFF9C27B0),
77 | Color(0xFF673AB7),
78 | Color(0xFF3F51B5),
79 | Color(0xFF2196F3),
80 | Color(0xFF03A9F4),
81 | Color(0xFF00BCD4),
82 | Color(0xFF009688),
83 | Color(0xFF4CAF50),
84 | Color(0xFF8BC34A),
85 | Color(0xFFCDDC39),
86 | Color(0xFFFFEB3B),
87 | Color(0xFFFFC107),
88 | Color(0xFFFF9800),
89 | Color(0xFFFF5722)
90 | )
91 |
92 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/DraggableGrid.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.foundation.ExperimentalFoundationApi
4 | import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Box
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.lazy.grid.GridCells
9 | import androidx.compose.foundation.lazy.grid.LazyGridItemScope
10 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
11 | import androidx.compose.foundation.lazy.grid.itemsIndexed
12 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.graphicsLayer
16 | import androidx.compose.ui.input.pointer.pointerInput
17 | import androidx.compose.ui.unit.dp
18 | import androidx.compose.ui.zIndex
19 |
20 | @OptIn(ExperimentalFoundationApi::class)
21 | @Composable
22 | fun DraggableGrid(
23 | items: List,
24 | itemKey:(Int,T) -> Any,
25 | onMove: (Int, Int) -> Unit,
26 | content: @Composable (T, Boolean) -> Unit,
27 | ) {
28 |
29 | val gridState = rememberLazyGridState()
30 | val dragDropState = rememberGridDragDropState(gridState, onMove)
31 | LazyVerticalGrid(
32 | columns = GridCells.Fixed(3),
33 | modifier = Modifier.dragContainer(dragDropState),
34 | state = gridState,
35 | contentPadding = PaddingValues(16.dp),
36 | verticalArrangement = Arrangement.spacedBy(5.dp),
37 | horizontalArrangement = Arrangement.spacedBy(5.dp),
38 | ) {
39 | itemsIndexed(items, key = { index, item ->
40 | itemKey(index,item)
41 | }) { index, item ->
42 | DraggableItem(dragDropState, index) { isDragging ->
43 | content(item, isDragging)
44 | }
45 | }
46 | }
47 | }
48 |
49 | fun Modifier.dragContainer(dragDropState: GridDragDropState): Modifier {
50 | return pointerInput(key1 = dragDropState) {
51 | detectDragGesturesAfterLongPress(
52 | onDrag = { change, offset ->
53 | change.consume()
54 | dragDropState.onDrag(offset = offset)
55 | },
56 | onDragStart = { offset ->
57 | dragDropState.onDragStart(offset)
58 | },
59 | onDragEnd = { dragDropState.onDragInterrupted() },
60 | onDragCancel = { dragDropState.onDragInterrupted() }
61 | )
62 | }
63 | }
64 |
65 | @ExperimentalFoundationApi
66 | @Composable
67 | fun LazyGridItemScope.DraggableItem(
68 | dragDropState: GridDragDropState,
69 | index: Int,
70 | content: @Composable (isDragging: Boolean) -> Unit,
71 | ) {
72 | val dragging = index == dragDropState.draggingItemIndex
73 | val draggingModifier = if (dragging) {
74 | //被拖拽时
75 | Modifier
76 | .zIndex(1f) //防止被遮挡
77 | .graphicsLayer {
78 | translationX = dragDropState.draggingItemOffset.x
79 | translationY = dragDropState.draggingItemOffset.y
80 | }
81 | } else if (index == dragDropState.previousIndexOfDraggedItem) {
82 | //松手后的"回归"动画
83 | Modifier
84 | .zIndex(1f) //防止被遮挡
85 | .graphicsLayer {
86 | translationX = dragDropState.previousItemOffset.value.x
87 | translationY = dragDropState.previousItemOffset.value.y
88 | }
89 | } else {
90 | //idle状态
91 | Modifier.animateItemPlacement()
92 | }
93 | Box(modifier = Modifier.then(draggingModifier) , propagateMinConstraints = true) {
94 | content(dragging)
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/FavoritesScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
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.tooling.preview.Preview
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun FavoritesScreen() {
18 |
19 | Column(
20 | Modifier
21 | .fillMaxSize()
22 | .background(Color.White),
23 | verticalArrangement = Arrangement.Center,
24 | horizontalAlignment = Alignment.CenterHorizontally
25 | ) {
26 | Text(text = "Favorites", fontWeight = FontWeight.Bold, fontSize = 20.sp)
27 | }
28 |
29 | }
30 |
31 | @Preview(showSystemUi = true)
32 | @Composable
33 | fun FavoritesScreenPreview() {
34 | FavoritesScreen()
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/GridDragDropState.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.Spring
5 | import androidx.compose.animation.core.VectorConverter
6 | import androidx.compose.animation.core.VisibilityThreshold
7 | import androidx.compose.animation.core.spring
8 | import androidx.compose.foundation.gestures.scrollBy
9 | import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
10 | import androidx.compose.foundation.lazy.grid.LazyGridState
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.LaunchedEffect
13 | import androidx.compose.runtime.getValue
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.rememberCoroutineScope
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.geometry.Offset
19 | import androidx.compose.ui.geometry.Size
20 | import androidx.compose.ui.unit.IntOffset
21 | import androidx.compose.ui.unit.IntSize
22 | import androidx.compose.ui.unit.toOffset
23 | import androidx.compose.ui.unit.toSize
24 | import kotlinx.coroutines.CoroutineScope
25 | import kotlinx.coroutines.channels.Channel
26 | import kotlinx.coroutines.launch
27 |
28 | @Composable
29 | fun rememberGridDragDropState(
30 | gridState: LazyGridState,
31 | onMove: (Int, Int) -> Unit,
32 | ): GridDragDropState {
33 | val scope = rememberCoroutineScope()
34 | val state = remember(gridState) {
35 | GridDragDropState(
36 | state = gridState,
37 | onMove = onMove,
38 | scope = scope
39 | )
40 | }
41 | LaunchedEffect(state) {
42 | while (true) {
43 | val diff = state.scrollChannel.receive()
44 | gridState.scrollBy(diff)
45 | }
46 | }
47 | return state
48 | }
49 |
50 | class GridDragDropState internal constructor(
51 | private val state: LazyGridState,
52 | private val scope: CoroutineScope,
53 | private val onMove: (Int, Int) -> Unit,
54 | ) {
55 |
56 | //事件通道,辅助滑动
57 | internal val scrollChannel = Channel()
58 | //触摸事件偏移的距离,不是触摸位置
59 | private var draggingItemDraggedDelta by mutableStateOf(Offset.Zero)
60 | //记录被触摸的item在布局中位置
61 | private var draggingItemInitialOffset by mutableStateOf(Offset.Zero)
62 |
63 | //当前被触摸的Item
64 | var draggingItemIndex by mutableStateOf(null)
65 | private set
66 |
67 | // LazyVerticalGrid 本身可以滑动,这里目标应该是矫正初始位置,draggingItemInitialOffset可能包含滚动的偏移量,防止拖拽过程中滚动而导致计算错误
68 | internal val draggingItemOffset: Offset
69 | get() = draggingItemLayoutInfo?.let { item ->
70 | draggingItemInitialOffset + draggingItemDraggedDelta - item.offset.toOffset()
71 | } ?: Offset.Zero
72 |
73 | //当前被触摸的Item的布局信息
74 | private val draggingItemLayoutInfo: LazyGridItemInfo?
75 | get() = state.layoutInfo.visibleItemsInfo
76 | .firstOrNull {
77 | it.index == draggingItemIndex
78 | }
79 | // touch cancel或者touch up 之后继续保存被拖拽的Item,辅助通过动画方式将其Item偏移到指定位置
80 | internal var previousIndexOfDraggedItem by mutableStateOf(null)
81 | private set
82 | // 辅助 previousIndexOfDraggedItem 进行位置移动
83 | internal var previousItemOffset = Animatable(Offset.Zero, Offset.VectorConverter)
84 | private set
85 |
86 | internal fun onDragStart(offset: Offset) {
87 | state.layoutInfo.visibleItemsInfo
88 | .firstOrNull { item ->
89 | /**
90 | * 查找当前触摸的Item
91 | */
92 | offset.x.toInt() in item.offset.x..item.offsetEnd.x &&
93 | offset.y.toInt() in item.offset.y..item.offsetEnd.y
94 | }?.also {
95 | draggingItemIndex = it.index //当前被触摸Item
96 | draggingItemInitialOffset = it.offset.toOffset() //当前Item的Offset位置
97 |
98 | }
99 | }
100 |
101 | internal fun onDragInterrupted() {
102 | if (draggingItemIndex != null) {
103 | //touch up 或者 touch cancel后保存位置,辅助之前被拖拽的Item到指定的位置
104 | previousIndexOfDraggedItem = draggingItemIndex
105 | val startOffset = draggingItemOffset //目标位置
106 | scope.launch {
107 | //启动协程,进行偏移
108 | previousItemOffset.snapTo(startOffset)
109 | previousItemOffset.animateTo(
110 | Offset.Zero,
111 | spring(
112 | stiffness = Spring.StiffnessMediumLow,
113 | visibilityThreshold = Offset.VisibilityThreshold
114 | )
115 | )
116 | //snapTo 和 animateTo是suspend函数,因此到这里是执行完成
117 | previousIndexOfDraggedItem = null
118 | }
119 | }
120 | draggingItemDraggedDelta = Offset.Zero
121 | draggingItemIndex = null
122 | draggingItemInitialOffset = Offset.Zero
123 | }
124 |
125 | internal fun onDrag(offset: Offset) {
126 | draggingItemDraggedDelta += offset
127 |
128 | //是否检测到Item被拖拽,空白区域的拖拽无效
129 | val draggingItem = draggingItemLayoutInfo ?: return
130 |
131 | //开始位置,类似传统View的left和top
132 | val startOffset = draggingItem.offset.toOffset() + draggingItemOffset
133 | //结束位置,类似传统View的right和bottom
134 | val endOffset = startOffset + draggingItem.size.toSize()
135 | //centerX和centerY
136 | val middleOffset = startOffset + (endOffset - startOffset) / 2f //运算符重载
137 |
138 | /**
139 | * 查找相交的Item,这和RecyclerView的ItemTouchHelper有些区别,后者会先过滤相交
140 | * 的Item,然后按中心点距离排序,距离越小越优先,排序之后进行打分,偏离的距离越远越优先,
141 | * 因此,理论上ItemTouchHelper稳定性要高一些,而Compose的灵敏度更高
142 | */
143 | val targetItem = state.layoutInfo.visibleItemsInfo.find { item ->
144 | middleOffset.x.toInt() in item.offset.x..item.offsetEnd.x &&
145 | middleOffset.y.toInt() in item.offset.y..item.offsetEnd.y &&
146 | draggingItem.index != item.index
147 | }
148 | if (targetItem != null) {
149 | val scrollToIndex = if (targetItem.index == state.firstVisibleItemIndex) {
150 | draggingItem.index
151 | } else if (draggingItem.index == state.firstVisibleItemIndex) {
152 | targetItem.index
153 | } else {
154 | null
155 | }
156 | if (scrollToIndex != null) {
157 | scope.launch {
158 | // this is needed to neutralize automatic keeping the first item first.
159 | state.scrollToItem(scrollToIndex, state.firstVisibleItemScrollOffset)
160 | //回调到ViewModel层面,进行数据交换
161 | onMove.invoke(draggingItem.index, targetItem.index)
162 | }
163 | } else {
164 | //回调到ViewModel层面,进行数据交换
165 | onMove.invoke(draggingItem.index, targetItem.index)
166 | }
167 | /**
168 | * 这里不太好理解,这行代码的意思是被拖拽的Item索引已经变了
169 | * 因此需要重新更新布局信息,而draggingItemIndex是mutableStateOf包裹的,设置后会触发状态更新
170 | */
171 | draggingItemIndex = targetItem.index
172 | } else {
173 | /**
174 | * 尝试滑动布局
175 | */
176 | val overscroll = when {
177 | draggingItemDraggedDelta.y > 0 ->
178 | (endOffset.y - state.layoutInfo.viewportEndOffset).coerceAtLeast(0f)
179 |
180 | draggingItemDraggedDelta.y < 0 ->
181 | (startOffset.y - state.layoutInfo.viewportStartOffset).coerceAtMost(0f)
182 |
183 | else -> 0f
184 | }
185 | if (overscroll != 0f) {
186 | scrollChannel.trySend(overscroll)
187 | }
188 | }
189 | }
190 |
191 | private val LazyGridItemInfo.offsetEnd: IntOffset
192 | get() = this.offset + this.size
193 | }
194 |
195 | operator fun IntOffset.plus(size: IntSize): IntOffset {
196 | return IntOffset(x + size.width, y + size.height)
197 | }
198 |
199 | operator fun Offset.plus(size: Size): Offset {
200 | return Offset(x + size.width, y + size.height)
201 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/GuaguaCardActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.Image
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.material3.MaterialTheme
11 | import androidx.compose.material3.Surface
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.Modifier
18 | import androidx.compose.ui.draw.DrawModifier
19 | import androidx.compose.ui.geometry.Offset
20 | import androidx.compose.ui.geometry.Rect
21 | import androidx.compose.ui.graphics.BlendMode
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.Paint
24 | import androidx.compose.ui.graphics.PaintingStyle
25 | import androidx.compose.ui.graphics.Path
26 | import androidx.compose.ui.graphics.StrokeCap
27 | import androidx.compose.ui.graphics.StrokeJoin
28 | import androidx.compose.ui.graphics.drawscope.ContentDrawScope
29 | import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
30 | import androidx.compose.ui.input.pointer.PointerEventType.Companion.Move
31 | import androidx.compose.ui.input.pointer.PointerEventType.Companion.Press
32 | import androidx.compose.ui.input.pointer.pointerInput
33 | import androidx.compose.ui.res.painterResource
34 | import com.example.compose.ui.theme.ComposeTheme
35 |
36 |
37 | class GuaguaCardActivity : ComponentActivity() {
38 |
39 | override fun onCreate(savedInstanceState: Bundle?) {
40 | super.onCreate(savedInstanceState)
41 |
42 | setContent {
43 | ComposeTheme {
44 | // A surface container using the 'background' color from the theme
45 | Surface(
46 | modifier = Modifier.fillMaxSize(),
47 | color = MaterialTheme.colorScheme.background
48 | ) {
49 | ScrapeLayerPage()
50 | }
51 | }
52 | }
53 | }
54 | }
55 |
56 | @Composable
57 | fun ScrapeLayerPage(){
58 | var linePath by remember {
59 | mutableStateOf(Offset.Zero)
60 | }
61 | val path by remember {
62 | mutableStateOf(Path())
63 | }
64 | Column(modifier = Modifier
65 | .fillMaxWidth()
66 | .pointerInput("dragging") {
67 | awaitPointerEventScope {
68 | while (true) {
69 | val event = awaitPointerEvent()
70 | when (event.type) {
71 | //按住时,更新起始点
72 | Press -> {
73 | path.moveTo(
74 | event.changes.first().position.x,
75 | event.changes.first().position.y
76 | )
77 | }
78 | //移动时,更新起始点 移动时,记录路径path
79 | Move -> {
80 | linePath = event.changes.first().position
81 | }
82 | }
83 | }
84 | }
85 | }
86 | .scrapeLayer(path, linePath)
87 | ) {
88 | Image(
89 | modifier = Modifier.fillMaxSize(),
90 | painter = painterResource(id = R.mipmap.img_pic),
91 | contentDescription = ""
92 | )
93 | }
94 | }
95 |
96 | fun Modifier.scrapeLayer(startPath: Path, moveOffset: Offset) =
97 | this.then(ScrapeLayer(startPath, moveOffset))
98 |
99 | class ScrapeLayer(private val strokePath: Path, private val moveOffset: Offset) : DrawModifier {
100 |
101 | private val pathPaint = Paint().apply {
102 | alpha = 0f
103 | style = PaintingStyle.Stroke
104 | strokeWidth = 70f
105 | blendMode = BlendMode.Clear
106 | strokeJoin = StrokeJoin.Round
107 | strokeCap = StrokeCap.Round
108 | }
109 |
110 | private val layerPaint = Paint().apply {
111 | color = Color.Gray
112 | }
113 |
114 | override fun ContentDrawScope.draw() {
115 | drawContent()
116 | drawIntoCanvas {
117 | val rect = Rect(0f, 0f, size.width, size.height)
118 | it.saveLayer(rect, layerPaint)
119 | //从当前画布,裁切一个新的图层
120 | it.drawRect(rect, layerPaint)
121 | strokePath.lineTo(moveOffset.x, moveOffset.y)
122 | it.drawPath(strokePath, pathPaint)
123 | it.restore()
124 | }
125 | }
126 | }
127 |
128 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
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.tooling.preview.Preview
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun HomeScreen() {
18 |
19 | Column(
20 | Modifier
21 | .fillMaxSize()
22 | .background(Color.White),
23 | verticalArrangement = Arrangement.Center,
24 | horizontalAlignment = Alignment.CenterHorizontally
25 | ) {
26 | Text(text = "Home", fontWeight = FontWeight.Bold, fontSize = 20.sp)
27 | }
28 |
29 |
30 |
31 | }
32 |
33 | @Preview(showSystemUi = true)
34 | @Composable
35 | fun HomeScreenPreview() {
36 | HomeScreen()
37 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/LazyListActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Favorite
12 | import androidx.compose.material.icons.filled.Menu
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.MediumTopAppBar
18 | import androidx.compose.material3.Scaffold
19 | import androidx.compose.material3.Text
20 | import androidx.compose.material3.TopAppBarDefaults
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.input.nestedscroll.nestedScroll
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.dp
25 |
26 | class LazyListActivity : ComponentActivity() {
27 |
28 | @OptIn(ExperimentalMaterial3Api::class)
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 |
32 |
33 | setContent {
34 | val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
35 | Scaffold(
36 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
37 | topBar = {
38 | MediumTopAppBar(
39 | title = {
40 |
41 | Text(
42 | "Medium TopAppBar",
43 | maxLines = 1,
44 | overflow = TextOverflow.Ellipsis
45 | )
46 | },
47 | navigationIcon = {
48 | IconButton(onClick = { /* doSomething() */ }) {
49 | Icon(
50 | imageVector = Icons.Filled.Menu,
51 | contentDescription = "Localized description"
52 | )
53 | }
54 | },
55 | actions = {
56 | IconButton(onClick = { /* doSomething() */ }) {
57 | Icon(
58 | imageVector = Icons.Filled.Favorite,
59 | contentDescription = "Localized description"
60 | )
61 | }
62 | },
63 | scrollBehavior = scrollBehavior
64 | )
65 | },
66 | content = { innerPadding ->
67 | LazyColumn(
68 | contentPadding = innerPadding,
69 | verticalArrangement = Arrangement.spacedBy(8.dp)
70 | ) {
71 | val list = (0..75).map { it.toString() }
72 | items(count = list.size) {
73 | Text(
74 | text = list[it],
75 | style = MaterialTheme.typography.bodyLarge,
76 | modifier = Modifier
77 | .fillMaxWidth()
78 | .padding(horizontal = 16.dp)
79 | )
80 | }
81 | }
82 | }
83 | )
84 | }
85 |
86 | }
87 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.os.Handler
5 | import android.os.Looper
6 | import android.util.Log
7 | import android.view.MotionEvent
8 | import androidx.activity.ComponentActivity
9 | import androidx.activity.compose.setContent
10 | import androidx.compose.foundation.gestures.detectDragGestures
11 | import androidx.compose.foundation.layout.Box
12 | import androidx.compose.foundation.layout.Column
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.fillMaxWidth
15 | import androidx.compose.foundation.layout.wrapContentHeight
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Surface
18 | import androidx.compose.material3.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.runtime.getValue
21 | import androidx.compose.runtime.mutableStateOf
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.setValue
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.ExperimentalComposeUiApi
26 | import androidx.compose.ui.Modifier
27 | import androidx.compose.ui.draw.drawBehind
28 | import androidx.compose.ui.draw.drawWithContent
29 | import androidx.compose.ui.geometry.Offset
30 | import androidx.compose.ui.graphics.Brush
31 | import androidx.compose.ui.graphics.Color
32 | import androidx.compose.ui.graphics.ImageBitmap
33 | import androidx.compose.ui.graphics.drawscope.translate
34 | import androidx.compose.ui.input.pointer.motionEventSpy
35 | import androidx.compose.ui.input.pointer.pointerInput
36 | import androidx.compose.ui.layout.onSizeChanged
37 | import androidx.compose.ui.res.imageResource
38 | import androidx.compose.ui.text.style.TextAlign
39 | import androidx.compose.ui.tooling.preview.Preview
40 | import androidx.compose.ui.unit.IntSize
41 | import androidx.compose.ui.unit.dp
42 | import com.example.compose.ui.theme.ComposeTheme
43 |
44 |
45 | class MainActivity : ComponentActivity() {
46 |
47 | var runTask: Runnable? = null
48 |
49 | val handler = Handler(Looper.getMainLooper())
50 |
51 | override fun onCreate(savedInstanceState: Bundle?) {
52 | super.onCreate(savedInstanceState)
53 | val imageResource = ImageBitmap.imageResource(resources, R.mipmap.img_pic)
54 | setContent {
55 | MainComposeTheme(imageResource)
56 | }
57 |
58 | }
59 |
60 | override fun onPause() {
61 | super.onPause()
62 | Log.d(TAG,"onPause")
63 | }
64 | }
65 | @Composable
66 | fun MainComposeTheme(imageResource: ImageBitmap) {
67 | ComposeTheme {
68 | // A surface container using the 'background' color from the theme
69 | Surface(
70 | modifier = Modifier.fillMaxSize(),
71 | color = MaterialTheme.colorScheme.background,
72 | ) {
73 | Column(
74 | modifier = Modifier
75 | .fillMaxSize()
76 | .drawBehind {
77 | drawImage(
78 | image = imageResource,
79 | dstSize = IntSize(size.width.toInt(), size.height.toInt())
80 | )
81 | }
82 | ) {
83 | val greetingState = Greeting("Android")
84 | Log.d("MainComposeTheme","greetingState $greetingState")
85 | }
86 | }
87 | }
88 | }
89 |
90 | @OptIn(ExperimentalComposeUiApi::class)
91 | @Composable
92 | fun Greeting(name: String, modifier: Modifier = Modifier):Any {
93 |
94 | var pointerOffset by remember {
95 | mutableStateOf(Offset(0f, 0f))
96 | }
97 | Box(
98 | modifier = Modifier
99 | .fillMaxSize()
100 | .pointerInput("dragging") {
101 | detectDragGestures(onDragStart = {
102 | //pointerOffset和it类型不同,这里会隐式转换,实现拖转开始点赋值给pointerOffset
103 | pointerOffset = it //拖转一定距离后才会触发此处的调用
104 | }) { change, dragAmount ->
105 | pointerOffset += dragAmount
106 | }
107 |
108 | }
109 | .motionEventSpy {
110 | if (it.actionMasked == MotionEvent.ACTION_DOWN) {
111 | pointerOffset = Offset(it.x, it.y) //获取按下的位置
112 | }
113 |
114 | }
115 | .onSizeChanged {
116 | pointerOffset = Offset(it.width / 2f, it.height / 2f)
117 | }
118 | .drawWithContent {
119 | // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI.
120 | val shader = Brush.radialGradient(
121 | listOf(Color.Transparent, Color.Black),
122 | center = pointerOffset,
123 | radius = 120.dp.toPx(),
124 | )
125 |
126 | drawRect(
127 | shader
128 | )
129 | }
130 | ) {
131 | Text(
132 | text = "Hello $name!,Welcome to use compose",
133 | modifier = modifier
134 | .fillMaxWidth()
135 | .wrapContentHeight(Alignment.CenterVertically)
136 | .drawWithContent {
137 | },
138 | textAlign = TextAlign.Center,
139 | onTextLayout = {
140 | Log.d("A", "onTextLayout")
141 | }
142 | )
143 |
144 | }
145 | return pointerOffset
146 | }
147 |
148 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/PinnedActivityActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyColumn
10 | import androidx.compose.material.icons.Icons
11 | import androidx.compose.material.icons.filled.Favorite
12 | import androidx.compose.material.icons.filled.Menu
13 | import androidx.compose.material3.ExperimentalMaterial3Api
14 | import androidx.compose.material3.Icon
15 | import androidx.compose.material3.IconButton
16 | import androidx.compose.material3.MaterialTheme
17 | import androidx.compose.material3.Scaffold
18 | import androidx.compose.material3.Text
19 | import androidx.compose.material3.TopAppBar
20 | import androidx.compose.material3.TopAppBarDefaults
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.input.nestedscroll.nestedScroll
23 | import androidx.compose.ui.text.style.TextOverflow
24 | import androidx.compose.ui.unit.dp
25 |
26 | class PinnedActivityActivity : ComponentActivity() {
27 |
28 | @OptIn(ExperimentalMaterial3Api::class)
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 |
32 | setContent {
33 | val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
34 | Scaffold(
35 | modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
36 | topBar = {
37 | TopAppBar(
38 | title = {
39 |
40 | Text(
41 | "Medium TopAppBar",
42 | maxLines = 1,
43 | overflow = TextOverflow.Ellipsis
44 | )
45 | },
46 | navigationIcon = {
47 | IconButton(onClick = { /* doSomething() */ }) {
48 | Icon(
49 | imageVector = Icons.Filled.Menu,
50 | contentDescription = "Localized description"
51 | )
52 | }
53 | },
54 | actions = {
55 | IconButton(onClick = { /* doSomething() */ }) {
56 | Icon(
57 | imageVector = Icons.Filled.Favorite,
58 | contentDescription = "Localized description"
59 | )
60 | }
61 | },
62 | scrollBehavior = scrollBehavior
63 | )
64 | },
65 | content = { innerPadding ->
66 | LazyColumn(
67 | contentPadding = innerPadding,
68 | verticalArrangement = Arrangement.spacedBy(8.dp)
69 | ) {
70 | val list = (0..75).map { it.toString() }
71 | items(count = list.size) {
72 | Text(
73 | text = list[it],
74 | style = MaterialTheme.typography.bodyLarge,
75 | modifier = Modifier
76 | .fillMaxWidth()
77 | .padding(horizontal = 16.dp)
78 | )
79 | }
80 | }
81 | }
82 | )
83 | }
84 |
85 | }
86 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/RelectionUtil.java:
--------------------------------------------------------------------------------
1 | package com.example.compose;
2 |
3 | import java.lang.reflect.Field;
4 | import java.lang.reflect.Modifier;
5 |
6 | public class RelectionUtil {
7 |
8 | public static void changeFinalValue(Object object, Field field, Object newValue, boolean isSafeThread) {
9 |
10 | try {
11 | if (!field.isAccessible()) {
12 | field.setAccessible(true);
13 | }
14 | // 如果field为private,则需要使用该方法使其可被访问
15 | Field modifersField = null;
16 | try {
17 | modifersField = Field.class.getDeclaredField("modifiers");
18 | } catch (Throwable e) {
19 | e.printStackTrace();
20 | }
21 | try {
22 | modifersField = Field.class.getDeclaredField("accessFlags");
23 | } catch (Throwable e) {
24 | e.printStackTrace();
25 | }
26 | modifersField.setAccessible(true);
27 |
28 | int modifiers = field.getModifiers();
29 | if (Modifier.isFinal(modifiers)) {
30 | // 把指定的field中的final修饰符去掉
31 | modifersField.setInt(field, modifiers & ~Modifier.FINAL);
32 | }
33 |
34 | field.set(object, newValue); // 为指定field设置新值
35 | if (isSafeThread) { //如果要考虑线程安全,建议还原
36 | if (Modifier.isFinal(modifiers)) {
37 | modifersField.setInt(field, modifiers | Modifier.FINAL);
38 | }
39 | }
40 | } catch (Throwable e) {
41 | e.printStackTrace();
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
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.tooling.preview.Preview
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun SearchScreen() {
18 |
19 | Column(
20 | Modifier
21 | .fillMaxSize()
22 | .background(Color.White),
23 | verticalArrangement = Arrangement.Center,
24 | horizontalAlignment = Alignment.CenterHorizontally
25 | ) {
26 | Text(text = "Search", fontWeight = FontWeight.Bold, fontSize = 20.sp)
27 | }
28 |
29 | }
30 |
31 | @Preview(showSystemUi = true)
32 | @Composable
33 | fun SearchScreenPreview() {
34 | SearchScreen()
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/SettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.Text
8 | import androidx.compose.runtime.Composable
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.tooling.preview.Preview
14 | import androidx.compose.ui.unit.sp
15 |
16 | @Composable
17 | fun SettingsScreen() {
18 |
19 | Column(
20 | Modifier
21 | .fillMaxSize()
22 | .background(Color.White),
23 | verticalArrangement = Arrangement.Center,
24 | horizontalAlignment = Alignment.CenterHorizontally
25 | ) {
26 | Text(text = "Settings", fontWeight = FontWeight.Bold, fontSize = 20.sp)
27 | }
28 |
29 | }
30 |
31 | @Preview(showSystemUi = true)
32 | @Composable
33 | fun SettingsScreenPreview() {
34 | SettingsScreen()
35 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/StickyActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.compose.foundation.ExperimentalFoundationApi
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.lazy.LazyColumn
11 | import androidx.compose.material3.MaterialTheme
12 | import androidx.compose.material3.Text
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.text.style.TextAlign
17 | import androidx.compose.ui.unit.dp
18 |
19 | class StickyActivity : ComponentActivity() {
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | }
23 | }
24 |
25 | @OptIn(ExperimentalFoundationApi::class)
26 | @Composable
27 | fun GroupedList() {
28 | val sections = listOf("A", "B", "C")
29 |
30 | LazyColumn {
31 | sections.forEach { section ->
32 | stickyHeader {
33 | Column(
34 | modifier = Modifier
35 | .height(40.dp)
36 | .fillMaxWidth()
37 | .background(Color.LightGray)
38 | ) {
39 | Text(
40 | text = section,
41 | modifier = Modifier.fillMaxWidth(),
42 | style = MaterialTheme.typography.headlineLarge,
43 | textAlign = TextAlign.Center
44 | )
45 | }
46 | }
47 | items(100) { item ->
48 | Text(text = "Some item $item")
49 | }
50 | }
51 | }
52 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/SwipeRefreshActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.LocalOverscrollConfiguration
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.gestures.detectDragGestures
11 | import androidx.compose.foundation.layout.Arrangement
12 | import androidx.compose.foundation.layout.Box
13 | import androidx.compose.foundation.layout.fillMaxWidth
14 | import androidx.compose.foundation.layout.height
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.lazy.LazyColumn
17 | import androidx.compose.foundation.lazy.LazyListState
18 | import androidx.compose.foundation.lazy.rememberLazyListState
19 | import androidx.compose.material3.MaterialTheme
20 | import androidx.compose.material3.Text
21 | import androidx.compose.runtime.Composable
22 | import androidx.compose.runtime.CompositionLocalProvider
23 | import androidx.compose.runtime.mutableFloatStateOf
24 | import androidx.compose.runtime.remember
25 | import androidx.compose.runtime.rememberCoroutineScope
26 | import androidx.compose.ui.Alignment
27 | import androidx.compose.ui.Modifier
28 | import androidx.compose.ui.geometry.Offset
29 | import androidx.compose.ui.graphics.Color
30 | import androidx.compose.ui.graphics.graphicsLayer
31 | import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
32 | import androidx.compose.ui.input.nestedscroll.NestedScrollSource
33 | import androidx.compose.ui.input.nestedscroll.nestedScroll
34 | import androidx.compose.ui.input.pointer.pointerInput
35 | import androidx.compose.ui.layout.Layout
36 | import androidx.compose.ui.unit.Constraints
37 | import androidx.compose.ui.unit.Velocity
38 | import androidx.compose.ui.unit.dp
39 | import kotlinx.coroutines.CoroutineScope
40 | import kotlinx.coroutines.launch
41 | import kotlin.math.abs
42 | import kotlin.math.absoluteValue
43 |
44 | class SwipeRefreshActivity : ComponentActivity() {
45 | override fun onCreate(savedInstanceState: Bundle?) {
46 | super.onCreate(savedInstanceState)
47 | setContent {
48 | SwipeRefreshColumn(headerIndicator = {
49 | Box (modifier = Modifier
50 | .fillMaxWidth()
51 | .height(100.dp)
52 | .background(Color.White),
53 | contentAlignment = Alignment.Center
54 | ){
55 | Text(text = "Hi, I am header")
56 | }
57 | }, footerIndicator = {
58 | Box (modifier = Modifier
59 | .fillMaxWidth()
60 | .height(100.dp)
61 | .background(Color.White),
62 | contentAlignment = Alignment.Center){
63 | Text(text = "ooh,long time no see")
64 | }
65 | }) { nestedScrollModifierNode ->
66 | val state: LazyListState = rememberLazyListState()
67 | nestedScrollModifierNode.initLazyState(state)
68 | LazyColumn (
69 | state = state,
70 | verticalArrangement = Arrangement.spacedBy(1.dp)
71 | ){
72 | val list = (0..5).map { it.toString() }
73 | items(count = list.size) {
74 | Box (modifier = Modifier
75 | .fillMaxWidth()
76 | .height(80.dp)
77 | .background(Color.LightGray),
78 | contentAlignment = Alignment.CenterStart){
79 | Text(
80 | text = list[it],
81 | style = MaterialTheme.typography.bodyLarge,
82 | modifier = Modifier
83 | .fillMaxWidth()
84 | .padding(horizontal = 16.dp)
85 | )
86 | }
87 | }
88 | }
89 | }
90 | }
91 |
92 | }
93 |
94 | }
95 |
96 |
97 | @OptIn(ExperimentalFoundationApi::class)
98 | @Composable
99 | fun SwipeRefreshColumn(
100 | modifier: Modifier = Modifier,
101 | headerIndicator: (@Composable () -> Unit)?,
102 | footerIndicator: (@Composable () -> Unit)?,
103 | content: (@Composable (SimpleNestedScrollConnection) -> Unit)
104 | ) {
105 | var contentIndex = 0
106 | val TAG = "SwipeRefreshList"
107 | val coroutineScope = rememberCoroutineScope()
108 |
109 | val connection = remember {
110 | SimpleNestedScrollConnection(coroutineScope)
111 | }
112 |
113 | Layout(
114 | modifier = modifier
115 | .nestedScroll(connection)
116 | .pointerInput("header-footer-capture"){
117 | //由于事件存在优先级,lazyList的优先级更高,我们只需要处理header和footer即可
118 | detectDragGestures(onDragStart = {
119 | if(findDragTarget(connection,it) == connection.headerOffset){
120 | Log.d(TAG,"onDragStart Header $it")
121 | connection.dispatchUserDragger(connection.headerOffset)
122 | }else if(findDragTarget(connection,it) == connection.footerOffset){
123 | Log.d(TAG,"onDragStart Footer $it")
124 | connection.dispatchUserDragger(connection.footerOffset)
125 | }
126 | }, onDragEnd = {
127 | connection.dispatchUserDragger(null)
128 | }){change, dragAmount ->
129 | Log.d(TAG,"onDrag $dragAmount")
130 | connection.dispatchUserScroll(dragAmount);
131 | }
132 | }
133 | .graphicsLayer {
134 | translationY = connection.graphicYOffset.value
135 | Log.d(TAG, "translationY = ${connection.graphicYOffset}")
136 | },
137 | content = {
138 | headerIndicator?.let {
139 | contentIndex++;
140 | headerIndicator()
141 | }
142 | CompositionLocalProvider(LocalOverscrollConfiguration.provides(null)) {
143 | content(connection)
144 | }
145 | footerIndicator?.let {
146 | footerIndicator()
147 | }
148 | }
149 | ) { measurables, constraints ->
150 | // Don't constrain child views further, measure them with given constraints
151 | // List of measured children
152 |
153 | val placeables = measurables.mapIndexed { index, measurable ->
154 |
155 | if (contentIndex == index) {
156 | val boxWidth = constraints.maxWidth
157 | val boxHeight = constraints.maxHeight
158 | val matchParentSizeConstraints = Constraints(
159 | minWidth = if (boxWidth != Constraints.Infinity) boxWidth else 0,
160 | minHeight = if (boxHeight != Constraints.Infinity) boxHeight else 0,
161 | maxWidth = boxWidth,
162 | maxHeight = boxHeight
163 | )
164 | connection.contentOffset.max = boxHeight.toFloat()
165 | measurable.measure(matchParentSizeConstraints)
166 | } else {
167 | val measure = measurable.measure(constraints)
168 | if(index < contentIndex){
169 | connection.headerOffset.max = measure.height.toFloat()
170 | }else if(index > contentIndex){
171 | connection.footerOffset.max = measure.height.toFloat()
172 | }
173 | measure
174 | }
175 | }
176 |
177 | // Set the size of the layout as big as it can
178 | layout(constraints.maxWidth, constraints.maxHeight) {
179 | var yPosition = 0
180 | placeables.forEach() { placeable ->
181 | placeable.placeRelative(x = 0, y = yPosition)
182 | yPosition += placeable.height
183 | }
184 |
185 | }
186 | }
187 | }
188 |
189 | fun findDragTarget(connection: SimpleNestedScrollConnection,dragStart: Offset): ComposeNestedOffset? {
190 | connection?.apply {
191 | headerOffset.let {
192 | if(it.value + it.max > dragStart.y){
193 | return headerOffset;
194 | }
195 | }
196 | val offset = contentOffset?.max ?: 0f
197 | footerOffset.let {
198 | if(it.value + it.max + offset > dragStart.y){
199 | return footerOffset;
200 | }
201 | }
202 | }
203 | return null;
204 | }
205 |
206 |
207 | data class ComposeNestedOffset(var key:String, var max: Float, var value: Float)
208 |
209 | class SimpleNestedScrollConnection(
210 | var coroutineScope: CoroutineScope
211 | ) : NestedScrollConnection{
212 |
213 | private var dragger: ComposeNestedOffset? = null
214 | private var lazyListState: LazyListState? = null
215 | val headerOffset = ComposeNestedOffset("header",0F, 0F)
216 | val footerOffset = ComposeNestedOffset("footer",0F, 0F)
217 | var contentOffset = ComposeNestedOffset("content",0F, 0F)
218 | var graphicYOffset = mutableFloatStateOf(0F)
219 |
220 |
221 | override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
222 | Log.d(TAG,"$available")
223 | return when {
224 | available.y < 0 && headerOffset.max != 0f -> {
225 | if(headerOffset.value > -headerOffset.max) {
226 | //拦截向上滑动,不能超过最大范围,这里只处理header ,因为header优先级较高于footer,同时防止被lazylist消费
227 | val offset = if(available.y + headerOffset.value < -headerOffset.max){
228 | -headerOffset.max - headerOffset.value
229 | }else{
230 | available.y
231 | }
232 | scroll(headerOffset, ComposeNestedOffset("",0F, 0F), offset)
233 | }else{
234 | Offset.Zero
235 | }
236 | }
237 |
238 | available.y > 0 -> {
239 | if(lazyListState?.canScrollForward == false && footerOffset.max != 0f){
240 | //footer向下滚动需要提前拦截,否则可能导致被LazyList消费,这时footer比header优先级高
241 | val offset = if(available.y + footerOffset.value > 0){
242 | abs(footerOffset.value)
243 | }else{
244 | available.y
245 | }
246 | scroll(footerOffset, headerOffset, offset)
247 | }else{
248 | Offset.Zero
249 | }
250 | }
251 |
252 | else ->{
253 | Offset.Zero
254 | }
255 | }
256 | }
257 |
258 | //下面是处理没有被lazylist 消费的事件
259 | override fun onPostScroll(
260 | consumed: Offset,
261 | available: Offset,
262 | source: NestedScrollSource
263 | ): Offset {
264 | return when {
265 | available.y < 0 -> {
266 | //拦截向上滑动,不能超过最大范围,这里只处理footer ,因为footer这个时候优先级最低
267 | if(lazyListState?.canScrollForward == false && footerOffset.max != 0f) {
268 | val offset = if(available.y + footerOffset.value < -footerOffset.max){
269 | -footerOffset.max - footerOffset.value //保证不小于边界值
270 | }else{
271 | available.y
272 | }
273 | // 这个时候底部漏出来,那么translationY 是两者之和
274 | scroll(footerOffset, headerOffset, offset)
275 | }else{
276 | Offset.Zero
277 | }
278 | }
279 |
280 | available.y > 0 -> {
281 | //拦截向上滑动,不能超过最大范围,这里只处理header ,因为header这个时候优先级最低
282 | if(lazyListState?.canScrollBackward == false && headerOffset.max != 0f && headerOffset.value < 0){
283 | val offset = if(available.y + headerOffset.value > 0){
284 | abs(headerOffset.value) //保证不大于边界值
285 | }else{
286 | available.y
287 | }
288 | //说明在顶部,这时候footerOffset理论上也是0,这里写成这样为了更加直观
289 | scroll(headerOffset, ComposeNestedOffset("",0F, 0F), offset)
290 | }else{
291 | Offset.Zero
292 | }
293 | }
294 | else -> {
295 | Offset.Zero
296 | }
297 | }
298 | }
299 |
300 |
301 | fun dispatchUserScroll(dragAmount: Offset){
302 | when{
303 | dragAmount.y < 0 -> {
304 | if(dragger == headerOffset && headerOffset.max != 0f) {
305 | //向上时,header优先拦截
306 | onPreScroll(dragAmount,NestedScrollSource.Drag);
307 | }else if(dragger == footerOffset && footerOffset.max != 0f){
308 | //向下时,footer优先拦截
309 | onPostScroll(Offset(0f,0f),dragAmount,NestedScrollSource.Drag)
310 | }
311 | }
312 | dragAmount.y > 0 ->{
313 | if(dragger == headerOffset && headerOffset.max != 0f) {
314 | //向下时,header优先拦截
315 | onPostScroll(Offset(0f,0f),dragAmount,NestedScrollSource.Drag)
316 | }else if(dragger == footerOffset && footerOffset.max != 0f){
317 | onPreScroll(dragAmount,NestedScrollSource.Drag);
318 | }
319 | }
320 | else -> {
321 | Offset.Zero
322 | }
323 | }
324 |
325 | }
326 |
327 | private fun scroll(target: ComposeNestedOffset, offset : ComposeNestedOffset, canConsumed: Float): Offset {
328 | return if (canConsumed.absoluteValue > 0.0f) {
329 | target.value += canConsumed
330 | //在这里更新而不是在协程中,避免同步事件触发多次
331 | coroutineScope.launch {
332 | contentOffset.value = lazyListState?.firstVisibleItemScrollOffset?.toFloat() ?: 0f;
333 | graphicYOffset.value = target.value + offset.value //更新偏移距离
334 |
335 | lazyListState?.apply {
336 | if((this.firstVisibleItemIndex + this.layoutInfo.visibleItemsInfo.size) == this.layoutInfo.totalItemsCount){
337 | loadMore(); //利用公式触发加载更多
338 | }
339 | }
340 | }
341 | Offset(0f, canConsumed)
342 | } else {
343 | Offset.Zero
344 | }
345 | }
346 |
347 | override suspend fun onPreFling(available: Velocity): Velocity {
348 | return super.onPreFling(available)
349 | }
350 |
351 | override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
352 | return super.onPostFling(consumed, available)
353 | }
354 |
355 | fun loadMore(){
356 | }
357 |
358 | fun initLazyState(state: LazyListState) {
359 | lazyListState = state
360 | }
361 |
362 | fun dispatchUserDragger(dragger: ComposeNestedOffset?) {
363 | this.dragger = dragger;
364 | }
365 |
366 | }
367 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/SwipeRefreshActivityV1.kt:
--------------------------------------------------------------------------------
1 | //package com.example.compose
2 | //
3 | //import android.os.Bundle
4 | //import android.util.Log
5 | //import androidx.activity.ComponentActivity
6 | //import androidx.activity.compose.setContent
7 | //import androidx.compose.foundation.ExperimentalFoundationApi
8 | //import androidx.compose.foundation.LocalOverscrollConfiguration
9 | //import androidx.compose.foundation.background
10 | //import androidx.compose.foundation.gestures.detectDragGestures
11 | //import androidx.compose.foundation.layout.Arrangement
12 | //import androidx.compose.foundation.layout.Box
13 | //import androidx.compose.foundation.layout.fillMaxWidth
14 | //import androidx.compose.foundation.layout.height
15 | //import androidx.compose.foundation.layout.padding
16 | //import androidx.compose.foundation.lazy.LazyColumn
17 | //import androidx.compose.foundation.lazy.LazyListState
18 | //import androidx.compose.foundation.lazy.rememberLazyListState
19 | //import androidx.compose.material3.MaterialTheme
20 | //import androidx.compose.material3.Text
21 | //import androidx.compose.runtime.Composable
22 | //import androidx.compose.runtime.CompositionLocalProvider
23 | //import androidx.compose.runtime.mutableFloatStateOf
24 | //import androidx.compose.runtime.remember
25 | //import androidx.compose.runtime.rememberCoroutineScope
26 | //import androidx.compose.ui.Alignment
27 | //import androidx.compose.ui.Modifier
28 | //import androidx.compose.ui.geometry.Offset
29 | //import androidx.compose.ui.graphics.Color
30 | //import androidx.compose.ui.graphics.graphicsLayer
31 | //import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
32 | //import androidx.compose.ui.input.nestedscroll.NestedScrollSource
33 | //import androidx.compose.ui.input.nestedscroll.nestedScroll
34 | //import androidx.compose.ui.input.pointer.pointerInput
35 | //import androidx.compose.ui.layout.Layout
36 | //import androidx.compose.ui.unit.Constraints
37 | //import androidx.compose.ui.unit.Velocity
38 | //import androidx.compose.ui.unit.dp
39 | //import kotlinx.coroutines.CoroutineScope
40 | //import kotlinx.coroutines.launch
41 | //import kotlin.math.abs
42 | //import kotlin.math.absoluteValue
43 | //
44 | //class SwipeRefreshActivity : ComponentActivity() {
45 | // override fun onCreate(savedInstanceState: Bundle?) {
46 | // super.onCreate(savedInstanceState)
47 | // setContent {
48 | // SwipeRefreshColumn(headerIndicator = {
49 | // Box (modifier = Modifier
50 | // .fillMaxWidth()
51 | // .height(100.dp)
52 | // .background(Color.White),
53 | // contentAlignment = Alignment.Center
54 | // ){
55 | // Text(text = "Hi, I am header")
56 | // }
57 | // }, footerIndicator = {
58 | // Box (modifier = Modifier
59 | // .fillMaxWidth()
60 | // .height(100.dp)
61 | // .background(Color.White),
62 | // contentAlignment = Alignment.Center){
63 | // Text(text = "ooh,long time no see")
64 | // }
65 | // }) { nestedScrollModifierNode ->
66 | // val state: LazyListState = rememberLazyListState()
67 | // nestedScrollModifierNode.initLazyState(state)
68 | // LazyColumn (
69 | // state = state,
70 | // verticalArrangement = Arrangement.spacedBy(1.dp)
71 | // ){
72 | // val list = (0..5).map { it.toString() }
73 | // items(count = list.size) {
74 | // Box (modifier = Modifier
75 | // .fillMaxWidth()
76 | // .height(80.dp)
77 | // .background(Color.LightGray),
78 | // contentAlignment = Alignment.CenterStart){
79 | // Text(
80 | // text = list[it],
81 | // style = MaterialTheme.typography.bodyLarge,
82 | // modifier = Modifier
83 | // .fillMaxWidth()
84 | // .padding(horizontal = 16.dp)
85 | // )
86 | // }
87 | // }
88 | // }
89 | // }
90 | // }
91 | //
92 | // }
93 | //
94 | //}
95 | //
96 | //
97 | //@OptIn(ExperimentalFoundationApi::class)
98 | //@Composable
99 | //fun SwipeRefreshColumn(
100 | // modifier: Modifier = Modifier,
101 | // headerIndicator: (@Composable () -> Unit)?,
102 | // footerIndicator: (@Composable () -> Unit)?,
103 | // content: (@Composable (SimpleNestedScrollConnection) -> Unit)
104 | //) {
105 | // var contentIndex = 0
106 | // val TAG = "SwipeRefreshList"
107 | // val coroutineScope = rememberCoroutineScope()
108 | //
109 | // val connection = remember {
110 | // SimpleNestedScrollConnection(coroutineScope)
111 | // }
112 | //
113 | // Layout(
114 | // modifier = modifier
115 | // .nestedScroll(connection)
116 | // .pointerInput("header-footer-capture"){
117 | // //由于事件存在优先级,lazyList的优先级更高,我们只需要处理header和footer即可
118 | // detectDragGestures(onDragStart = {
119 | // if(findDragTarget(connection,it) == connection.headerOffset){
120 | // Log.d(TAG,"onDragStart Header $it")
121 | // connection.dispatchUserDragger(connection.headerOffset)
122 | // }else if(findDragTarget(connection,it) == connection.footerOffset){
123 | // Log.d(TAG,"onDragStart Footer $it")
124 | // connection.dispatchUserDragger(connection.footerOffset)
125 | // }
126 | // }, onDragEnd = {
127 | // connection.dispatchUserDragger(null)
128 | // }){change, dragAmount ->
129 | // Log.d(TAG,"onDrag $dragAmount")
130 | // connection.dispatchUserScroll(dragAmount);
131 | // }
132 | // }
133 | // .graphicsLayer {
134 | // translationY = connection.graphicYOffset.value
135 | // Log.d(TAG, "translationY = ${connection.graphicYOffset}")
136 | // },
137 | // content = {
138 | // headerIndicator?.let {
139 | // contentIndex++;
140 | // headerIndicator()
141 | // }
142 | // CompositionLocalProvider(LocalOverscrollConfiguration.provides(null)) {
143 | // content(connection)
144 | // }
145 | // footerIndicator?.let {
146 | // footerIndicator()
147 | // }
148 | // }
149 | // ) { measurables, constraints ->
150 | // // Don't constrain child views further, measure them with given constraints
151 | // // List of measured children
152 | //
153 | // val placeables = measurables.mapIndexed { index, measurable ->
154 | //
155 | // if (contentIndex == index) {
156 | // val boxWidth = constraints.maxWidth
157 | // val boxHeight = constraints.maxHeight
158 | // val matchParentSizeConstraints = Constraints(
159 | // minWidth = if (boxWidth != Constraints.Infinity) boxWidth else 0,
160 | // minHeight = if (boxHeight != Constraints.Infinity) boxHeight else 0,
161 | // maxWidth = boxWidth,
162 | // maxHeight = boxHeight
163 | // )
164 | // connection.contentOffset.max = boxHeight.toFloat()
165 | // measurable.measure(matchParentSizeConstraints)
166 | // } else {
167 | // val measure = measurable.measure(constraints)
168 | // if(index < contentIndex){
169 | // connection.headerOffset.max = measure.height.toFloat()
170 | // }else if(index > contentIndex){
171 | // connection.footerOffset.max = measure.height.toFloat()
172 | // }
173 | // measure
174 | // }
175 | // }
176 | //
177 | // // Set the size of the layout as big as it can
178 | // layout(constraints.maxWidth, constraints.maxHeight) {
179 | // var yPosition = 0
180 | // placeables.forEach() { placeable ->
181 | // placeable.placeRelative(x = 0, y = yPosition)
182 | // yPosition += placeable.height
183 | // }
184 | //
185 | // }
186 | // }
187 | //}
188 | //
189 | //fun findDragTarget(connection: SimpleNestedScrollConnection,dragStart: Offset): NestedOffset? {
190 | // connection?.apply {
191 | // headerOffset.let {
192 | // if(it.value + it.max > dragStart.y){
193 | // return headerOffset;
194 | // }
195 | // }
196 | // val offset = contentOffset?.max ?: 0f
197 | // footerOffset.let {
198 | // if(it.value + it.max + offset > dragStart.y){
199 | // return footerOffset;
200 | // }
201 | // }
202 | // }
203 | // return null;
204 | //}
205 | //
206 | //
207 | //data class NestedOffset(var key:String,var max: Float, var value: Float)
208 | //
209 | //class SimpleNestedScrollConnection(
210 | // var coroutineScope: CoroutineScope
211 | //) : NestedScrollConnection{
212 | //
213 | // private var dragger: NestedOffset? = null
214 | // private var lazyListState: LazyListState? = null
215 | // val headerOffset = NestedOffset("header",0F, 0F)
216 | // val footerOffset = NestedOffset("footer",0F, 0F)
217 | // var contentOffset = NestedOffset("content",0F, 0F)
218 | // var graphicYOffset = mutableFloatStateOf(0F)
219 | //
220 | //
221 | // override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
222 | // Log.d(TAG,"$available")
223 | // return when {
224 | // available.y < 0 && headerOffset.max != 0f -> {
225 | // if(headerOffset.value > -headerOffset.max) {
226 | // //拦截向上滑动,不能超过最大范围,这里只处理header ,因为header优先级较高于footer,同时防止被lazylist消费
227 | // val offset = if(available.y + headerOffset.value < -headerOffset.max){
228 | // -headerOffset.max - headerOffset.value
229 | // }else{
230 | // available.y
231 | // }
232 | // scroll(headerOffset, NestedOffset("",0F, 0F), offset)
233 | // }else{
234 | // Offset.Zero
235 | // }
236 | // }
237 | //
238 | // available.y > 0 -> {
239 | // if(lazyListState?.canScrollForward == false && lazyListState?.canScrollBackward == true && footerOffset.max != 0f){
240 | // //footer向下滚动需要提前拦截,否则可能导致被LazyList消费,这时footer比header优先级高
241 | // val offset = if(available.y + footerOffset.value > 0){
242 | // abs(footerOffset.value)
243 | // }else{
244 | // available.y
245 | // }
246 | // scroll(footerOffset, headerOffset, offset)
247 | // }else{
248 | // Offset.Zero
249 | // }
250 | // }
251 | //
252 | // else ->{
253 | // Offset.Zero
254 | // }
255 | // }
256 | // }
257 | //
258 | // //下面是处理没有被lazylist 消费的事件
259 | // override fun onPostScroll(
260 | // consumed: Offset,
261 | // available: Offset,
262 | // source: NestedScrollSource
263 | // ): Offset {
264 | // return when {
265 | // available.y < 0 -> {
266 | // //拦截向上滑动,不能超过最大范围,这里只处理footer ,因为footer这个时候优先级最低
267 | // if(lazyListState?.canScrollForward == false && lazyListState?.canScrollBackward == true && footerOffset.max != 0f) {
268 | // val offset = if(available.y + footerOffset.value < -footerOffset.max){
269 | // -footerOffset.max - footerOffset.value //保证不小于边界值
270 | // }else{
271 | // available.y
272 | // }
273 | // // 这个时候底部漏出来,那么translationY 是两者之和
274 | // scroll(footerOffset, headerOffset, offset)
275 | // }else{
276 | // Offset.Zero
277 | // }
278 | // }
279 | //
280 | // available.y > 0 -> {
281 | // //拦截向上滑动,不能超过最大范围,这里只处理header ,因为header这个时候优先级最低
282 | // if(lazyListState?.canScrollBackward == false && headerOffset.max != 0f && headerOffset.value < 0){
283 | // val offset = if(available.y + headerOffset.value > 0){
284 | // abs(headerOffset.value) //保证不大于边界值
285 | // }else{
286 | // available.y
287 | // }
288 | // //说明在顶部,这时候footerOffset理论上也是0,这里写成这样为了更加直观
289 | // scroll(headerOffset, NestedOffset("",0F, 0F), offset)
290 | // }else{
291 | // Offset.Zero
292 | // }
293 | // }
294 | // else -> {
295 | // Offset.Zero
296 | // }
297 | // }
298 | // }
299 | //
300 | //
301 | // fun dispatchUserScroll(dragAmount: Offset){
302 | // when{
303 | // dragAmount.y < 0 -> {
304 | // if(dragger == headerOffset && headerOffset.max != 0f) {
305 | // //向上时,header优先拦截
306 | // onPreScroll(dragAmount,NestedScrollSource.Drag);
307 | // }else if(dragger == footerOffset && footerOffset.max != 0f){
308 | // //向下时,footer优先拦截
309 | // onPostScroll(Offset(0f,0f),dragAmount,NestedScrollSource.Drag)
310 | // }
311 | // }
312 | // dragAmount.y > 0 ->{
313 | // if(dragger == headerOffset && headerOffset.max != 0f) {
314 | // //向下时,header优先拦截
315 | // onPostScroll(Offset(0f,0f),dragAmount,NestedScrollSource.Drag)
316 | // }else if(dragger == footerOffset && footerOffset.max != 0f){
317 | // onPreScroll(dragAmount,NestedScrollSource.Drag);
318 | // }
319 | // }
320 | // else -> {
321 | // Offset.Zero
322 | // }
323 | // }
324 | //
325 | // }
326 | //
327 | // private fun scroll(target: NestedOffset, offset : NestedOffset,canConsumed: Float): Offset {
328 | // return if (canConsumed.absoluteValue > 0.0f) {
329 | // target.value += canConsumed
330 | // //在这里更新而不是在协程中,避免同步事件触发多次
331 | // coroutineScope.launch {
332 | // contentOffset.value = lazyListState?.firstVisibleItemScrollOffset?.toFloat() ?: 0f;
333 | // graphicYOffset.value = target.value + offset.value //更新偏移距离
334 | //
335 | // lazyListState?.apply {
336 | // if((this.firstVisibleItemIndex + this.layoutInfo.visibleItemsInfo.size) == this.layoutInfo.totalItemsCount){
337 | // loadMore(); //利用公式触发加载更多
338 | // }
339 | // }
340 | // }
341 | // Offset(0f, canConsumed)
342 | // } else {
343 | // Offset.Zero
344 | // }
345 | // }
346 | //
347 | // override suspend fun onPreFling(available: Velocity): Velocity {
348 | // return super.onPreFling(available)
349 | // }
350 | //
351 | // override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
352 | // return super.onPostFling(consumed, available)
353 | // }
354 | //
355 | // fun loadMore(){
356 | // }
357 | //
358 | // fun initLazyState(state: LazyListState) {
359 | // lazyListState = state
360 | // }
361 | //
362 | // fun dispatchUserDragger(dragger: NestedOffset?) {
363 | // this.dragger = dragger;
364 | // }
365 | //
366 | //}
367 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/TabActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.view.MotionEvent
5 | import androidx.activity.ComponentActivity
6 | import androidx.activity.compose.setContent
7 | import androidx.compose.foundation.ExperimentalFoundationApi
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.fillMaxWidth
12 | import androidx.compose.foundation.layout.height
13 | import androidx.compose.foundation.layout.wrapContentHeight
14 | import androidx.compose.foundation.pager.HorizontalPager
15 | import androidx.compose.foundation.pager.PagerState
16 | import androidx.compose.foundation.pager.rememberPagerState
17 | import androidx.compose.material.icons.Icons
18 | import androidx.compose.material.icons.filled.Favorite
19 | import androidx.compose.material.icons.filled.Home
20 | import androidx.compose.material.icons.filled.Search
21 | import androidx.compose.material.icons.filled.Settings
22 | import androidx.compose.material3.Icon
23 | import androidx.compose.material3.LocalContentColor
24 | import androidx.compose.material3.MaterialTheme
25 | import androidx.compose.material3.Surface
26 | import androidx.compose.material3.Tab
27 | import androidx.compose.material3.TabRow
28 | import androidx.compose.material3.TabRowDefaults
29 | import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
30 | import androidx.compose.material3.Text
31 | import androidx.compose.runtime.Composable
32 | import androidx.compose.runtime.getValue
33 | import androidx.compose.runtime.mutableIntStateOf
34 | import androidx.compose.runtime.remember
35 | import androidx.compose.runtime.rememberCoroutineScope
36 | import androidx.compose.runtime.saveable.rememberSaveableStateHolder
37 | import androidx.compose.runtime.setValue
38 | import androidx.compose.ui.ExperimentalComposeUiApi
39 | import androidx.compose.ui.Modifier
40 | import androidx.compose.ui.draw.drawBehind
41 | import androidx.compose.ui.draw.drawWithContent
42 | import androidx.compose.ui.draw.scale
43 | import androidx.compose.ui.graphics.Color
44 | import androidx.compose.ui.graphics.Color.Companion.Red
45 | import androidx.compose.ui.graphics.vector.ImageVector
46 | import androidx.compose.ui.input.pointer.motionEventSpy
47 | import androidx.compose.ui.layout.layout
48 | import androidx.compose.ui.unit.dp
49 | import androidx.compose.ui.unit.sp
50 | import com.example.compose.ui.theme.ComposeTheme
51 | import com.example.compose.ui.theme.PurpleGrey80
52 | import kotlinx.coroutines.launch
53 |
54 | const val PAGER_STATE_DRAG_START = 0;
55 | const val PAGER_STATE_DRAGGING = 1;
56 | const val PAGER_STATE_IDLE = 2;
57 |
58 | class TabActivity : ComponentActivity() {
59 | val tabData = getTabList()
60 |
61 | override fun onCreate(savedInstanceState: Bundle?) {
62 | super.onCreate(savedInstanceState)
63 | setContent {
64 | ComposeTheme {
65 | // A surface container using the 'background' color from the theme
66 | Surface(
67 | modifier = Modifier.fillMaxSize(),
68 | color = MaterialTheme.colorScheme.background
69 | ) {
70 | MainScreen()
71 | }
72 | }
73 | }
74 |
75 | }
76 |
77 | @OptIn(ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
78 | @Composable
79 | fun MainScreen() {
80 | val pagerState = rememberPagerState(initialPage = 0) {
81 | tabData.size
82 | }
83 | var dragState by remember {
84 | mutableIntStateOf(PAGER_STATE_IDLE)
85 | }
86 | Column(modifier = Modifier.fillMaxSize()) {
87 | TabContent(pagerState, modifier = Modifier
88 | .weight(1f)
89 | .motionEventSpy { event ->
90 | when (event.actionMasked) {
91 | MotionEvent.ACTION_DOWN ->
92 | dragState = PAGER_STATE_DRAG_START
93 |
94 | MotionEvent.ACTION_MOVE ->
95 | dragState = PAGER_STATE_DRAGGING
96 |
97 | MotionEvent.ACTION_UP ->
98 | dragState = PAGER_STATE_IDLE
99 |
100 | else -> {
101 | dragState = dragState
102 | }
103 |
104 | }
105 | }
106 | )
107 | TabLayout(tabData, pagerState,dragState)
108 | }
109 | }
110 | }
111 |
112 |
113 |
114 | @OptIn(ExperimentalFoundationApi::class)
115 | @Composable
116 | fun TabLayout(tabData: List>, pagerState: PagerState, dragState: Int) {
117 |
118 | val scope = rememberCoroutineScope()
119 | var selectIndex by remember { mutableIntStateOf(0) }
120 | /* val tabColor = listOf(
121 | Color.Gray,
122 | Color.Yellow,
123 | Color.Blue,
124 | Color.Red
125 | )
126 | */
127 | TabRow(
128 | selectedTabIndex = pagerState.currentPage,
129 | divider = {
130 | Spacer(modifier = Modifier.height(0.dp))
131 | },
132 | indicator = { tabPositions ->
133 | TabRowDefaults.Indicator(
134 | modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]),
135 | height = 0.dp,
136 | color = Color.White
137 | )
138 | },
139 | modifier = Modifier
140 | .fillMaxWidth()
141 | .wrapContentHeight()
142 | ) {
143 | tabData.forEachIndexed { index, s ->
144 |
145 | if(dragState == PAGER_STATE_DRAG_START){
146 | selectIndex = pagerState.currentPage
147 | }
148 | val isSelectedItem =
149 | if (dragState == PAGER_STATE_DRAG_START || dragState == PAGER_STATE_DRAGGING || pagerState.isScrollInProgress) {
150 | selectIndex == index
151 | } else if (pagerState.targetPage == index) {
152 | selectIndex = index;
153 | true
154 | } else {
155 | false
156 | }
157 | val tabTintColor = if (isSelectedItem) {
158 | Red
159 | } else {
160 | LocalContentColor.current
161 | }
162 | Tab(
163 | modifier = Modifier.drawBehind {
164 | if(isSelectedItem) {
165 | drawCircle( color = PurpleGrey80, radius = (size.minDimension - 8.dp.toPx())/2f)
166 | }
167 | },
168 | selected = pagerState.currentPage == index,
169 | onClick = {
170 | scope.launch {
171 | selectIndex = index
172 | pagerState.animateScrollToPage(index)
173 | }
174 | },
175 | icon = {
176 | Icon(imageVector = s.second, contentDescription = null, tint = tabTintColor,
177 | modifier = Modifier
178 | .drawWithContent {
179 | drawContent()
180 | }
181 | .layout { measurable, constraints ->
182 | val placeable = measurable.measure(constraints)
183 | layout(placeable.width, placeable.height) {
184 | placeable.placeRelative(0, 15)
185 | }
186 | }
187 | )
188 | },
189 | text = {
190 | Text(text = s.first, color = tabTintColor, fontSize = 12.sp, modifier = Modifier.scale(0.8f))
191 | },
192 | selectedContentColor = TabRowDefaults.containerColor
193 | )
194 | }
195 | }
196 | }
197 |
198 |
199 |
200 | @OptIn(ExperimentalFoundationApi::class)
201 | @Composable
202 | fun TabContent(
203 | pagerState: PagerState,
204 | modifier: Modifier
205 | ) {
206 | HorizontalPager(state = pagerState, modifier = modifier) { index ->
207 | when (index) {
208 | 0 -> {
209 | HomeScreen()
210 | }
211 |
212 | 1 -> {
213 | SearchScreen()
214 | }
215 |
216 | 2 -> {
217 | FavoritesScreen()
218 | }
219 |
220 | 3 -> {
221 | SettingsScreen()
222 | }
223 | }
224 |
225 | }
226 | }
227 |
228 |
229 | private fun getTabList(): List> {
230 | return listOf(
231 | "Home" to Icons.Default.Home,
232 | "Search" to Icons.Default.Search,
233 | "Favorites" to Icons.Default.Favorite,
234 | "Settings" to Icons.Default.Settings,
235 | )
236 | }
237 |
238 |
239 |
240 |
241 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/TextFiledActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.os.Bundle
4 | import android.util.Log
5 | import android.view.MotionEvent.ACTION_DOWN
6 | import androidx.activity.ComponentActivity
7 | import androidx.activity.compose.setContent
8 | import androidx.compose.foundation.Image
9 | import androidx.compose.foundation.background
10 | import androidx.compose.foundation.clickable
11 | import androidx.compose.foundation.interaction.MutableInteractionSource
12 | import androidx.compose.foundation.interaction.collectIsPressedAsState
13 | import androidx.compose.foundation.layout.Column
14 | import androidx.compose.foundation.layout.Spacer
15 | import androidx.compose.foundation.layout.fillMaxSize
16 | import androidx.compose.foundation.layout.height
17 | import androidx.compose.foundation.layout.padding
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.foundation.text.KeyboardActions
20 | import androidx.compose.foundation.text.KeyboardOptions
21 | import androidx.compose.material3.ExperimentalMaterial3Api
22 | import androidx.compose.material3.MaterialTheme
23 | import androidx.compose.material3.OutlinedTextField
24 | import androidx.compose.material3.Surface
25 | import androidx.compose.material3.Text
26 | import androidx.compose.material3.TextField
27 | import androidx.compose.material3.TextFieldDefaults
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.runtime.mutableStateOf
30 | import androidx.compose.runtime.remember
31 | import androidx.compose.ui.ExperimentalComposeUiApi
32 | import androidx.compose.ui.Modifier
33 | import androidx.compose.ui.draw.drawBehind
34 | import androidx.compose.ui.draw.scale
35 | import androidx.compose.ui.graphics.Color
36 | import androidx.compose.ui.input.pointer.motionEventSpy
37 | import androidx.compose.ui.res.painterResource
38 | import androidx.compose.ui.text.input.ImeAction
39 | import androidx.compose.ui.text.input.KeyboardCapitalization
40 | import androidx.compose.ui.text.input.KeyboardType
41 | import androidx.compose.ui.text.input.PasswordVisualTransformation
42 | import androidx.compose.ui.tooling.preview.Preview
43 | import androidx.compose.ui.unit.dp
44 | import com.example.compose.ui.theme.ComposeTheme
45 |
46 | class TextFiledActivity : ComponentActivity() {
47 |
48 | override fun onCreate(savedInstanceState: Bundle?) {
49 | super.onCreate(savedInstanceState)
50 |
51 | setContent {
52 | ComposeTheme {
53 | // A surface container using the 'background' color from the theme
54 | Surface(
55 | modifier = Modifier.fillMaxSize(),
56 | color = MaterialTheme.colorScheme.background
57 | ) {
58 | textFieldCompose()
59 | }
60 | }
61 | }
62 | }
63 |
64 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
65 | @Preview()
66 | @Composable
67 | fun textFieldCompose() {
68 | var phone = remember {
69 | mutableStateOf("")
70 | }
71 | var password = remember {
72 | mutableStateOf("")
73 | }
74 | val interactionSource = remember {
75 | MutableInteractionSource()
76 | }
77 | val pressState = interactionSource.collectIsPressedAsState()
78 | val lableText = if (pressState.value) "手机号" else "手机号码"
79 |
80 | Column(
81 | ) {
82 | Text(
83 | text = "ClickMe",
84 | modifier = Modifier
85 | .padding(horizontal = 30.dp, vertical = 50.dp)
86 | .background(Color.Cyan)
87 | .drawBehind {
88 | Log.i("ClickMe", "size = $size");
89 | }
90 | .clickable {
91 | Log.i("ClickMe", "happen Click");
92 | }.motionEventSpy {
93 | when(it.actionMasked){
94 | ACTION_DOWN -> {
95 | Log.d(TAG,"Other Event")
96 | }
97 | else ->
98 | Log.d(TAG,"Other Event")
99 | }
100 | }
101 | )
102 | Text(
103 | text = "BaseLine",
104 | modifier = Modifier.background(Color.Yellow)
105 | )
106 |
107 | // TextField(
108 | // value = phone.value,
109 | // onValueChange = {
110 | // phone.value = it
111 | // },
112 | // label = {
113 | // Text(lableText)
114 | // },
115 | // placeholder = {
116 | // Text("请输入手机号码")
117 | // },
118 | // leadingIcon = {
119 | // // 左边的图片
120 | // Image(
121 | // painter = painterResource(id = R.mipmap.icon_png_1),
122 | // contentDescription = "输入框前面的图标"
123 | // )
124 | // },
125 | // trailingIcon = {
126 | // Image(
127 | // painter = painterResource(id = R.mipmap.icon_cat),
128 | // contentDescription = "输入框后面的图标"
129 | // )
130 | // },
131 | // isError = false,
132 | // keyboardOptions = KeyboardOptions(
133 | // keyboardType = KeyboardType.Text,
134 | // imeAction = ImeAction.Next,
135 | // autoCorrect = true,
136 | // capitalization = KeyboardCapitalization.Sentences
137 | // ),
138 | // keyboardActions = KeyboardActions(
139 | // onDone = {
140 | //
141 | // },
142 | // onGo = {
143 | //
144 | // },
145 | // onNext = {
146 | //
147 | // },
148 | // onPrevious = {
149 | //
150 | // },
151 | // onSearch = {
152 | //
153 | // },
154 | // onSend = {
155 | //
156 | // }),
157 | // interactionSource = interactionSource,
158 | // // singleLine 设置单行
159 | // singleLine = true,
160 | // // maxLines设置最大行数
161 | // maxLines = 2,
162 | // // 设置背景的形状。比如圆角,圆形等
163 | // shape = RoundedCornerShape(4f),
164 | // // 简单举个focusedIndicatorColor的颜色就好其他一样
165 | // colors = TextFieldDefaults.textFieldColors(focusedIndicatorColor = Color.Red)
166 | // )
167 | // OutlinedTextField(
168 | // value = password.value,
169 | // onValueChange = { password.value = it },
170 | // label = { Text("密码") },
171 | // // 设置输入的文本样式,比如密码的时候输入变成....
172 | // visualTransformation = PasswordVisualTransformation('*')
173 | // )
174 | }
175 | }
176 |
177 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/TouchEventActivity.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import android.content.Context
4 | import android.os.Bundle
5 | import android.text.SpannableString
6 | import android.util.AttributeSet
7 | import android.util.Log
8 | import android.view.KeyEvent
9 | import android.view.MotionEvent
10 | import android.view.View
11 | import android.view.ViewGroup
12 | import android.view.ViewGroup.LayoutParams
13 | import android.widget.FrameLayout
14 | import androidx.activity.ComponentActivity
15 | import androidx.activity.compose.setContent
16 | import androidx.compose.foundation.background
17 | import androidx.compose.foundation.clickable
18 | import androidx.compose.foundation.gestures.detectDragGestures
19 | import androidx.compose.foundation.layout.Box
20 | import androidx.compose.foundation.layout.Column
21 | import androidx.compose.foundation.layout.fillMaxSize
22 | import androidx.compose.foundation.layout.height
23 | import androidx.compose.foundation.layout.padding
24 | import androidx.compose.foundation.layout.width
25 | import androidx.compose.material3.MaterialTheme
26 | import androidx.compose.material3.Surface
27 | import androidx.compose.material3.Text
28 | import androidx.compose.runtime.Composable
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.draw.drawWithContent
32 | import androidx.compose.ui.graphics.Color
33 | import androidx.compose.ui.input.pointer.pointerInput
34 | import androidx.compose.ui.text.ParagraphStyle
35 | import androidx.compose.ui.text.SpanStyle
36 | import androidx.compose.ui.text.buildAnnotatedString
37 | import androidx.compose.ui.text.withStyle
38 | import androidx.compose.ui.unit.dp
39 | import com.example.compose.ui.theme.ComposeTheme
40 |
41 |
42 | const val TAG = "TouchEventActivity"
43 |
44 | class TouchEventActivity : ComponentActivity() {
45 |
46 | private var rootFrameLayout: FrameLayout? = null
47 |
48 | override fun onCreate(savedInstanceState: Bundle?) {
49 | super.onCreate(savedInstanceState)
50 |
51 |
52 |
53 | setContent {
54 | ComposeTheme {
55 | // A surface container using the 'background' color from the theme
56 | Surface(
57 | modifier = Modifier.fillMaxSize(),
58 | color = MaterialTheme.colorScheme.background
59 | ) {
60 | TouchScreen()
61 | }
62 | }
63 | }
64 | }
65 |
66 | override fun setContentView(view: View?, params: ViewGroup.LayoutParams?) {
67 | if(rootFrameLayout == null) {
68 | rootFrameLayout = TouchFrameLayout(this);
69 | super.setContentView(rootFrameLayout, ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT))
70 | }
71 | rootFrameLayout?.let {
72 | it.removeAllViews()
73 | it.addView(view,params)
74 | }
75 | }
76 |
77 |
78 | @Composable
79 | fun TouchScreen() {
80 | Box(
81 | modifier = Modifier
82 | .fillMaxSize()
83 | .pointerInput("Box#1") {
84 | detectDragGestures(onDragStart = {
85 | Log.d(TAG, "Box#1 onDragStart")
86 |
87 | }) { change, dragAmount ->
88 | Log.d(TAG, "Box#1 dragging")
89 | }
90 |
91 | }
92 |
93 | ) {
94 | Column(
95 | modifier = Modifier
96 | .align(Alignment.Center)
97 | .pointerInput("Column#1") {
98 | detectDragGestures(onDragStart = {
99 | Log.d(TAG, "Column#1 onDragStart")
100 |
101 | }) { change, dragAmount ->
102 | Log.d(TAG, "Column#1 dragging")
103 | }
104 |
105 | }
106 | ) {
107 | Box(
108 | modifier = Modifier
109 | .width(100.dp)
110 | .height(100.dp)
111 | .align(Alignment.CenterHorizontally)
112 | .background(Color.Cyan)
113 | ) {
114 | Text(text = "A", modifier = Modifier
115 | .align(Alignment.Center)
116 | .padding(horizontal = 20.dp, vertical = 20.dp)
117 | .background(Color.Red)
118 | .clickable {
119 | Log.d(TAG,"A Click")
120 | })
121 | }
122 | Box(
123 | modifier = Modifier
124 | .width(100.dp)
125 | .height(100.dp)
126 | .align(Alignment.CenterHorizontally)
127 | .background(Color(0xFFFF6666))
128 | ) {
129 | Text(text = "B", modifier = Modifier.align(Alignment.Center))
130 | }
131 | Box(
132 | modifier = Modifier
133 | .width(100.dp)
134 | .height(100.dp)
135 | .align(Alignment.CenterHorizontally)
136 | .background(Color(0xFFff9922))
137 | ) {
138 | Text(text = buildAnnotatedString {
139 | withStyle(style = SpanStyle(color = Color.Red)) {
140 | append("Vibrant Text")
141 | }
142 | append("\n\n")
143 | append(SpannableString("Regular Text"))
144 |
145 | withStyle(style = ParagraphStyle()){
146 |
147 | }
148 |
149 | withStyle(style = SpanStyle()){
150 |
151 | }
152 | }, modifier = Modifier
153 | .align(Alignment.Center)
154 | .drawWithContent {
155 | })
156 | }
157 | }
158 | }
159 | }
160 |
161 |
162 | }
163 |
164 | class TouchFrameLayout : FrameLayout{
165 | constructor(context: Context) : super(context)
166 | constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
167 | constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
168 | context,
169 | attrs,
170 | defStyleAttr
171 | )
172 |
173 | init{
174 | viewTreeObserver.addOnGlobalFocusChangeListener { oldFocus, newFocus ->
175 | Log.d(TAG,"oldFocus : $oldFocus , newFocus : $newFocus")
176 | }
177 | }
178 |
179 | override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
180 | val dispatchTouchEvent = super.dispatchTouchEvent(ev)
181 | if(dispatchTouchEvent){
182 | findTouchTarget(this);
183 | }
184 | return dispatchTouchEvent;
185 | }
186 |
187 | private fun findTouchTarget(touchFrameLayout: TouchFrameLayout): Boolean {
188 |
189 | return false;
190 | }
191 |
192 | override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
193 | return super.dispatchKeyEvent(event)
194 | }
195 | }
196 |
197 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun ComposeTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
71 |
72 | private val _DarkColorScheme = darkColorScheme(
73 | primary = Purple80,
74 | secondary = PurpleGrey80,
75 | tertiary = Pink80
76 | )
77 |
78 | private val _LightColorScheme = lightColorScheme(
79 | primary = Purple40,
80 | secondary = PurpleGrey40,
81 | tertiary = Pink40
82 |
83 | /* Other default colors to override
84 | background = Color(0xFFFFFBFE),
85 | surface = Color(0xFFFFFBFE),
86 | onPrimary = Color.White,
87 | onSecondary = Color.White,
88 | onTertiary = Color.White,
89 | onBackground = Color(0xFF1C1B1F),
90 | onSurface = Color(0xFF1C1B1F),
91 | */
92 | )
93 |
94 | @Composable
95 | fun DragAndDropTheme(
96 | darkTheme: Boolean = isSystemInDarkTheme(),
97 | content: @Composable () -> Unit,
98 | ) {
99 | val colorScheme = if (darkTheme) _DarkColorScheme else _LightColorScheme
100 | val view = LocalView.current
101 | if (!view.isInEditMode) {
102 | SideEffect {
103 | val window = (view.context as Activity).window
104 | window.statusBarColor = colorScheme.primary.toArgb()
105 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
106 | }
107 | }
108 |
109 | MaterialTheme(
110 | colorScheme = colorScheme,
111 | typography = Typography,
112 | content = content
113 | )
114 | }
115 |
116 |
--------------------------------------------------------------------------------
/app/src/main/java/com/example/compose/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose.ui.theme
2 |
3 | import androidx.compose.material3.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 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/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_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/img_pic.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xhdpi/img_pic.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/icon_cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/icon_cat.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/icon_png_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/icon_png_1.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/img_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/img_02.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/img_checken.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxhdpi/img_checken.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Compose
3 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/src/test/java/com/example/compose/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.example.compose
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 | plugins {
3 | alias(libs.plugins.androidApplication) apply false
4 | alias(libs.plugins.jetbrainsKotlinAndroid) apply false
5 | }
--------------------------------------------------------------------------------
/fire_157.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/fire_157.gif
--------------------------------------------------------------------------------
/fire_161.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/fire_161.gif
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. For more details, visit
12 | # https://developer.android.com/r/tools/gradle-multi-project-decoupled-projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | agp = "8.1.0-rc01"
3 | kotlin = "1.9.0"
4 | coreKtx = "1.10.1"
5 | junit = "4.13.2"
6 | junitVersion = "1.1.5"
7 | espressoCore = "3.5.1"
8 | lifecycleRuntimeKtx = "2.6.1"
9 | activityCompose = "1.7.0"
10 | composeBom = "2023.08.00"
11 | webkit = "1.9.0"
12 |
13 | [libraries]
14 | androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
15 | junit = { group = "junit", name = "junit", version.ref = "junit" }
16 | androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
17 | androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
18 | androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
19 | androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
20 | androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
21 | androidx-ui = { group = "androidx.compose.ui", name = "ui" }
22 | androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
23 | androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
24 | androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
25 | androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
26 | androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
27 | androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
28 | androidx-webkit = { group = "androidx.webkit", name = "webkit", version.ref="webkit"}
29 | androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version = "2.1.4" }
30 |
31 |
32 | [plugins]
33 | androidApplication = { id = "com.android.application", version.ref = "agp" }
34 | jetbrainsKotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
35 |
36 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/soloong/ComposeLearning/648a93e1e45e0ad827622fa082a608bdabd9974f/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Fri Apr 12 13:39:07 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file is automatically generated by Android Studio.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file should *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 | #
7 | # Location of the SDK. This is only used by Gradle.
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=/Users/wintertian/Library/Android/sdk
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google {
4 | content {
5 | includeGroupByRegex("com\\.android.*")
6 | includeGroupByRegex("com\\.google.*")
7 | includeGroupByRegex("androidx.*")
8 | }
9 | }
10 | mavenCentral()
11 | gradlePluginPortal()
12 | }
13 | }
14 | dependencyResolutionManagement {
15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
16 | repositories {
17 | google()
18 | mavenCentral()
19 | }
20 | }
21 |
22 | rootProject.name = "Compose"
23 | include(":app")
24 |
--------------------------------------------------------------------------------