├── .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 | ![fire_157.gif](https://github.com/soloong/ComposeLearning/blob/main/fire_157.gif) 7 | 8 | ![fire_157.gif](https://github.com/soloong/ComposeLearning/blob/main/fire_161.gif) 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 |