เลื่อน

ตัวแก้ไขการเลื่อน

ตัวแก้ไข verticalScroll และ horizontalScroll เป็นวิธีที่ง่ายที่สุดในการอนุญาตให้ผู้ใช้เลื่อนองค์ประกอบเมื่อขอบเขตของเนื้อหามีขนาดใหญ่กว่าข้อจำกัดด้านขนาดสูงสุด โดยใช้ตัวแก้ไข verticalScroll และ horizontalScroll คุณไม่จำเป็นต้องแปลหรือชดเชยเนื้อหา

@Composable private fun ScrollBoxes() {     Column(         modifier = Modifier             .background(Color.LightGray)             .size(100.dp)             .verticalScroll(rememberScrollState())     ) {         repeat(10) {             Text("Item $it", modifier = Modifier.padding(2.dp))         }     } }

รายการแนวตั้งแบบง่ายที่ตอบสนองต่อท่าทางสัมผัสการเลื่อน

ScrollState ช่วยให้คุณเปลี่ยนตำแหน่งการเลื่อนหรือรับสถานะปัจจุบันได้ หากต้องการสร้างโดยใช้พารามิเตอร์เริ่มต้น ให้ใช้ rememberScrollState()

@Composable private fun ScrollBoxesSmooth() {     // Smoothly scroll 100px on first composition     val state = rememberScrollState()     LaunchedEffect(Unit) { state.animateScrollTo(100) }      Column(         modifier = Modifier             .background(Color.LightGray)             .size(100.dp)             .padding(horizontal = 8.dp)             .verticalScroll(state)     ) {         repeat(10) {             Text("Item $it", modifier = Modifier.padding(2.dp))         }     } }

ตัวแก้ไขที่เลื่อนได้

ตัวปรับแต่ง scrollable แตกต่างจากตัวปรับแต่งการเลื่อนตรงที่ scrollable จะตรวจหา ท่าทางการเลื่อนและบันทึกเดลต้า แต่จะไม่ชดเชยเนื้อหาโดยอัตโนมัติ แต่จะมอบสิทธิ์ให้ผู้ใช้ผ่าน ScrollableState แทน ซึ่งจำเป็นต่อการทำงานของตัวแก้ไขนี้อย่างถูกต้อง

เมื่อสร้าง ScrollableState คุณต้องระบุฟังก์ชัน consumeScrollDelta ซึ่งจะเรียกใช้ในแต่ละขั้นตอนการเลื่อน (โดยการป้อนท่าทางสัมผัส การเลื่อน อย่างราบรื่น หรือการปัด) พร้อมกับเดลต้าในหน่วยพิกเซล ฟังก์ชันนี้ต้องแสดงผล ระยะทางการเลื่อนที่ใช้ไป เพื่อให้มั่นใจว่าระบบจะส่งต่อเหตุการณ์อย่างถูกต้อง ในกรณีที่มีองค์ประกอบที่ซ้อนกันซึ่งมีตัวแก้ไข scrollable

ข้อมูลโค้ดต่อไปนี้ตรวจหาท่าทางสัมผัสและแสดงค่าตัวเลขสำหรับออฟเซ็ต แต่ไม่ได้ชดเชยองค์ประกอบใดๆ

@Composable private fun ScrollableSample() {     // actual composable state     var offset by remember { mutableStateOf(0f) }     Box(         Modifier             .size(150.dp)             .scrollable(                 orientation = Orientation.Vertical,                 // Scrollable state: describes how to consume                 // scrolling delta and update offset                 state = rememberScrollableState { delta ->                     offset += delta                     delta                 }             )             .background(Color.LightGray),         contentAlignment = Alignment.Center     ) {         Text(offset.toString())     } }

องค์ประกอบ UI ที่ตรวจหาการกดนิ้วและแสดงค่าตัวเลขสำหรับตำแหน่งของนิ้ว

การเลื่อนที่ฝังไว้

การเลื่อนที่ซ้อนกันเป็นระบบที่คอมโพเนนต์การเลื่อนหลายรายการซึ่งอยู่ ภายในกันทำงานร่วมกันโดยตอบสนองต่อท่าทางการเลื่อนเดียวและ สื่อสารเดลต้า (การเปลี่ยนแปลง) ของการเลื่อน

ระบบการเลื่อนที่ซ้อนกันช่วยให้คอมโพเนนต์ที่เลื่อนได้และลิงก์ตามลำดับชั้น (ส่วนใหญ่จะแชร์คอมโพเนนต์หลักเดียวกัน) ทำงานร่วมกันได้ ระบบนี้จะลิงก์คอนเทนเนอร์ที่เลื่อนได้และอนุญาตให้โต้ตอบกับ เดลต้าการเลื่อนที่กำลังเผยแพร่และแชร์ระหว่างกัน

Compose มีวิธีจัดการการเลื่อนที่ซ้อนกันระหว่าง Composable หลายวิธี ตัวอย่างทั่วไปของการเลื่อนที่ซ้อนกันคือรายการที่อยู่ภายในอีกรายการหนึ่ง และกรณีที่ซับซ้อนกว่านั้นคือแถบเครื่องมือที่ยุบได้

การเลื่อนที่ซ้อนกันโดยอัตโนมัติ

การเลื่อนที่ซ้อนกันแบบง่ายไม่จำเป็นต้องดำเนินการใดๆ ท่าทางสัมผัสที่เริ่ม การเลื่อนจะส่งต่อจากองค์ประกอบย่อยไปยังองค์ประกอบหลักโดยอัตโนมัติ เพื่อให้เมื่อองค์ประกอบย่อยเลื่อนต่อไม่ได้แล้ว องค์ประกอบหลักจะจัดการท่าทางสัมผัสนั้น

คอมโพเนนต์และตัวแก้ไขบางรายการของ Compose รองรับการเลื่อนที่ซ้อนกันโดยอัตโนมัติและพร้อมใช้งานทันที ได้แก่ verticalScroll horizontalScroll scrollable API Lazy และ TextField ซึ่งหมายความว่าเมื่อผู้ใช้เลื่อนองค์ประกอบย่อยด้านในของคอมโพเนนต์ที่ซ้อนกัน ตัวแก้ไขก่อนหน้าจะส่งต่อเดลต้าการเลื่อนไปยังองค์ประกอบระดับบนที่มีการรองรับการเลื่อนที่ซ้อนกัน

ตัวอย่างต่อไปนี้แสดงองค์ประกอบที่มีตัวแก้ไข verticalScroll นำไปใช้กับองค์ประกอบภายในคอนเทนเนอร์ที่มีตัวแก้ไข verticalScroll นำไปใช้ด้วย

@Composable private fun AutomaticNestedScroll() {     val gradient = Brush.verticalGradient(0f to Color.Gray, 1000f to Color.White)     Box(         modifier = Modifier             .background(Color.LightGray)             .verticalScroll(rememberScrollState())             .padding(32.dp)     ) {         Column {             repeat(6) {                 Box(                     modifier = Modifier                         .height(128.dp)                         .verticalScroll(rememberScrollState())                 ) {                     Text(                         "Scroll here",                         modifier = Modifier                             .border(12.dp, Color.DarkGray)                             .background(brush = gradient)                             .padding(24.dp)                             .height(150.dp)                     )                 }             }         }     } }

องค์ประกอบ UI แบบเลื่อนแนวตั้งที่ซ้อนกัน 2 รายการ ซึ่งตอบสนองต่อท่าทางสัมผัสภายในและภายนอกองค์ประกอบด้านใน

การใช้ตัวแก้ไข nestedScroll

หากต้องการสร้างการเลื่อนที่ประสานงานขั้นสูงระหว่างองค์ประกอบหลายรายการ ตัวแก้ไข nestedScroll จะช่วยให้คุณมีความยืดหยุ่นมากขึ้นด้วยการกำหนดลำดับชั้นการเลื่อนที่ซ้อนกัน ดังที่กล่าวไว้ในส่วนก่อนหน้า คอมโพเนนต์บางอย่างรองรับการเลื่อนที่ซ้อนกันในตัว อย่างไรก็ตาม สำหรับ Composable ที่เลื่อนไม่ได้โดยอัตโนมัติ เช่น Box หรือ Column เดลต้าการเลื่อนในคอมโพเนนต์ดังกล่าวจะไม่แพร่กระจายใน ระบบการเลื่อนที่ซ้อนกัน และเดลต้าจะไม่ไปถึง NestedScrollConnection หรือ คอมโพเนนต์ระดับบน หากต้องการแก้ไขปัญหานี้ คุณสามารถใช้ nestedScroll เพื่อให้การสนับสนุนดังกล่าวแก่คอมโพเนนต์อื่นๆ รวมถึงคอมโพเนนต์ที่กำหนดเอง

วงจรการเลื่อนที่ซ้อนกัน

วงจรการเลื่อนที่ซ้อนกันคือโฟลว์ของเดลต้าการเลื่อนที่ส่งขึ้นและลง ในแผนผังลำดับชั้นผ่านคอมโพเนนต์ (หรือโหนด) ทั้งหมดที่เป็นส่วนหนึ่งของระบบการเลื่อนที่ซ้อนกัน เช่น โดยใช้คอมโพเนนต์และตัวแก้ไขที่เลื่อนได้ หรือnestedScroll

ระยะของวงจรการเลื่อนที่ฝังไว้

เมื่อคอมโพเนนต์ที่เลื่อนได้ตรวจพบเหตุการณ์ทริกเกอร์ (เช่น ท่าทางสัมผัส) ก่อนที่จะทริกเกอร์การเลื่อนจริง ระบบจะส่งเดลต้าที่สร้างขึ้นไปยังระบบการเลื่อนที่ซ้อนกันและผ่าน 3 เฟส ได้แก่ ก่อนเลื่อน การใช้โหนด และหลังเลื่อน

ขั้นตอนของวงจรการเลื่อนที่ฝังไว้

ในระยะแรกก่อนเลื่อน คอมโพเนนต์ที่ได้รับเหตุการณ์ทริกเกอร์ deltas จะส่งเหตุการณ์เหล่านั้นขึ้นไปตามแผนผังลำดับชั้นไปยัง องค์ประกอบระดับบนสุด จากนั้นเหตุการณ์เดลต้าจะไหลลงมา ซึ่งหมายความว่าระบบจะ ส่งต่อเดลต้าจากระดับบนสุดไปยังองค์ประกอบย่อยที่เริ่ม วงจรการเลื่อนที่ซ้อนกัน

ระยะก่อนเลื่อน - ส่ง ขึ้น

ซึ่งจะทำให้องค์ประกอบระดับบนสุดของการเลื่อนที่ซ้อนกัน (Composable ที่ใช้ตัวแก้ไข nestedScroll หรือ scrollable) มีโอกาสดำเนินการกับเดลต้าก่อนที่ โหนดจะใช้เดลต้าได้

ระยะก่อนเลื่อน - การส่งต่อเหตุการณ์

ในระยะการใช้โหนด ตัวโหนดเองจะใช้เดลต้าใดก็ตามที่ไม่ได้ใช้โดยโหนดหลัก ซึ่งเป็นช่วงเวลาที่การเลื่อนเกิดขึ้นจริงและมองเห็นได้

ระยะการใช้โหนด

ในระยะนี้ บุตรหลานอาจเลือกดูฟีดที่เหลือทั้งหมดหรือบางส่วน ส่วนที่เหลือจะถูกส่งกลับขึ้นไปเพื่อเข้าสู่ระยะหลังการเลื่อน

สุดท้ายนี้ ในระยะหลังการเลื่อน สิ่งที่โหนดเองไม่ได้ใช้ จะถูกส่งขึ้นไปอีกครั้งไปยังบรรพบุรุษเพื่อใช้

ระยะหลังการเลื่อน - การส่ง ขึ้น

ระยะหลังการเลื่อนจะทำงานในลักษณะเดียวกับระยะก่อนการเลื่อน ซึ่งผู้ปกครองสามารถเลือกที่จะใช้หรือไม่ใช้ก็ได้

ระยะหลังการเลื่อน - การส่งต่อเหตุการณ์ ลงไป

เช่นเดียวกับการเลื่อน เมื่อท่าทางสัมผัสการลากสิ้นสุดลง ความตั้งใจของผู้ใช้อาจ เปลี่ยนเป็นความเร็วที่ใช้ในการดีด (เลื่อนโดยใช้ภาพเคลื่อนไหว) คอนเทนเนอร์ที่เลื่อนได้ การดีดเป็นส่วนหนึ่งของวงจรการเลื่อนที่ซ้อนกัน และ ความเร็วที่เกิดจากเหตุการณ์การลากจะผ่านระยะที่คล้ายกัน ได้แก่ ก่อนการดีด การใช้โหนด และหลังการดีด โปรดทราบว่าภาพเคลื่อนไหวแบบดีดจะเชื่อมโยงกับท่าทางสัมผัสเท่านั้น และจะไม่ทริกเกอร์ด้วยเหตุการณ์อื่นๆ เช่น การเลื่อนด้วยฮาร์ดแวร์หรือการเลื่อนที่เกี่ยวข้องกับ a11y

เข้าร่วมวงจรการเลื่อนที่ซ้อนกัน

การเข้าร่วมในวงจรหมายถึงการสกัดกั้น การใช้ และการรายงาน การใช้เดลต้าตามลําดับชั้น Compose มีชุดเครื่องมือสำหรับ กำหนดวิธีที่ระบบการเลื่อนที่ซ้อนกันทำงานและวิธีโต้ตอบกับระบบโดยตรง เช่น เมื่อคุณต้องการทำบางอย่างกับเดลต้าการเลื่อนก่อนที่ คอมโพเนนต์ที่เลื่อนได้จะเริ่มเลื่อน

หากวงจรการเลื่อนที่ซ้อนกันเป็นระบบที่ทำงานกับห่วงโซ่ของโหนด ตัวแก้ไข nestedScroll เป็นวิธีในการสกัดกั้นและแทรกในการเปลี่ยนแปลงเหล่านี้ และ มีอิทธิพลต่อข้อมูล (เดลต้าการเลื่อน) ที่เผยแพร่ในห่วงโซ่ ตัวแก้ไขนี้ วางไว้ที่ใดก็ได้ในลำดับชั้น และจะสื่อสารกับอินสแตนซ์ตัวแก้ไขการเลื่อนที่ซ้อนกันขึ้นไปในโครงสร้างเพื่อให้แชร์ข้อมูลผ่านช่องทางนี้ได้ องค์ประกอบพื้นฐานของตัวแก้ไขนี้คือ NestedScrollConnection และ NestedScrollDispatcher

NestedScrollConnection เป็นวิธีตอบสนองต่อเฟสของวงจรการเลื่อนที่ซ้อนกันและมีอิทธิพลต่อ ระบบการเลื่อนที่ซ้อนกัน โดยประกอบด้วยเมธอดการเรียกกลับ 4 รายการ ซึ่งแต่ละรายการ แสดงถึงระยะการใช้งานระยะใดระยะหนึ่ง ได้แก่ ก่อน/หลังการเลื่อนและก่อน/หลังการปัด

val nestedScrollConnection = object : NestedScrollConnection {     override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {         println("Received onPreScroll callback.")         return Offset.Zero     }      override fun onPostScroll(         consumed: Offset,         available: Offset,         source: NestedScrollSource     ): Offset {         println("Received onPostScroll callback.")         return Offset.Zero     } }

การเรียกกลับแต่ละครั้งจะให้ข้อมูลเกี่ยวกับเดลต้าที่เผยแพร่ด้วย ดังนี้ available เดลต้าสำหรับเฟสนั้นๆ และconsumed เดลต้าที่ใช้ใน เฟสก่อนหน้า หากต้องการหยุดการแพร่กระจายเดลต้าขึ้นไปตามลำดับชั้น คุณสามารถใช้การเชื่อมต่อการเลื่อนที่ซ้อนกันได้โดยทำดังนี้

val disabledNestedScrollConnection = remember {     object : NestedScrollConnection {         override fun onPostScroll(             consumed: Offset,             available: Offset,             source: NestedScrollSource         ): Offset {             return if (source == NestedScrollSource.SideEffect) {                 available             } else {                 Offset.Zero             }         }     } }

การเรียกกลับทั้งหมดจะให้ข้อมูลเกี่ยวกับประเภท NestedScrollSource

NestedScrollDispatcher เริ่มต้นรอบการเลื่อนที่ซ้อนกัน การใช้ Dispatcher และการเรียกใช้เมธอดของ Dispatcher จะทริกเกอร์วงจร คอนเทนเนอร์ที่เลื่อนได้มีตัวจัดส่งในตัวซึ่งจะส่ง เดลต้าที่บันทึกไว้ระหว่างท่าทางสัมผัสไปยังระบบ ด้วยเหตุนี้ กรณีการใช้งานส่วนใหญ่ ของการปรับแต่งการเลื่อนที่ซ้อนกันจึงเกี่ยวข้องกับการใช้ NestedScrollConnection แทน ตัวจัดส่ง เพื่อตอบสนองต่อเดลต้าที่มีอยู่แล้วแทนที่จะส่งเดลต้าใหม่ ดูการใช้งานเพิ่มเติมได้ที่ NestedScrollDispatcherSample

ปรับขนาดรูปภาพเมื่อเลื่อน

ขณะที่ผู้ใช้เลื่อน คุณสามารถสร้างเอฟเฟกต์ภาพแบบไดนามิกที่รูปภาพ เปลี่ยนขนาดตามตำแหน่งการเลื่อนได้

ปรับขนาดรูปภาพตามตำแหน่งการเลื่อน

ข้อมูลโค้ดนี้แสดงการปรับขนาดรูปภาพภายใน LazyColumn ตาม ตำแหน่งการเลื่อนแนวตั้ง รูปภาพจะเล็กลงเมื่อผู้ใช้เลื่อนลง และใหญ่ขึ้น เมื่อเลื่อนขึ้น โดยจะยังคงอยู่ภายในขอบเขตขนาดขั้นต่ำและสูงสุดที่กำหนดไว้

@Composable fun ImageResizeOnScrollExample(     modifier: Modifier = Modifier,     maxImageSize: Dp = 300.dp,     minImageSize: Dp = 100.dp ) {     var currentImageSize by remember { mutableStateOf(maxImageSize) }     var imageScale by remember { mutableFloatStateOf(1f) }      val nestedScrollConnection = remember {         object : NestedScrollConnection {             override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {                 // Calculate the change in image size based on scroll delta                 val delta = available.y                 val newImageSize = currentImageSize + delta.dp                 val previousImageSize = currentImageSize                  // Constrain the image size within the allowed bounds                 currentImageSize = newImageSize.coerceIn(minImageSize, maxImageSize)                 val consumed = currentImageSize - previousImageSize                  // Calculate the scale for the image                 imageScale = currentImageSize / maxImageSize                  // Return the consumed scroll amount                 return Offset(0f, consumed.value)             }         }     }      Box(Modifier.nestedScroll(nestedScrollConnection)) {         LazyColumn(             Modifier                 .fillMaxWidth()                 .padding(15.dp)                 .offset {                     IntOffset(0, currentImageSize.roundToPx())                 }         ) {             // Placeholder list items             items(100, key = { it }) {                 Text(                     text = "Item: $it",                     style = MaterialTheme.typography.bodyLarge                 )             }         }          Image(             painter = ColorPainter(Color.Red),             contentDescription = "Red color image",             Modifier                 .size(maxImageSize)                 .align(Alignment.TopCenter)                 .graphicsLayer {                     scaleX = imageScale                     scaleY = imageScale                     // Center the image vertically as it scales                     translationY = -(maxImageSize.toPx() - currentImageSize.toPx()) / 2f                 }         )     } }

ประเด็นสำคัญเกี่ยวกับโค้ด

  • โค้ดนี้ใช้ NestedScrollConnection เพื่อสกัดกั้นเหตุการณ์การเลื่อน
  • onPreScroll จะคำนวณการเปลี่ยนแปลงขนาดรูปภาพตามเดลต้าการเลื่อน
  • ตัวแปรสถานะ currentImageSize จะจัดเก็บขนาดปัจจุบันของรูปภาพ โดยมีข้อจำกัดระหว่าง minImageSize และ maxImageSize. imageScale ซึ่งได้มาจาก currentImageSize
  • LazyColumn จะชดเชยตาม currentImageSize
  • Image ใช้ตัวแก้ไข graphicsLayer เพื่อใช้สเกลที่คํานวณแล้ว
  • translationY ภายใน graphicsLayer ช่วยให้รูปภาพยังคงอยู่ ตรงกลางในแนวตั้งเมื่อมีการปรับขนาด

ผลลัพธ์

ข้อมูลโค้ดก่อนหน้าจะส่งผลให้เกิดเอฟเฟกต์การปรับขนาดรูปภาพเมื่อเลื่อน

รูปที่ 1 เอฟเฟกต์การปรับขนาดรูปภาพเมื่อเลื่อน

การทำงานร่วมกันของการเลื่อนที่ฝังไว้

เมื่อพยายามซ้อนองค์ประกอบ View ที่เลื่อนได้ใน Composable ที่เลื่อนได้ หรือ ในทางกลับกัน คุณอาจพบปัญหา การเลื่อนที่เห็นได้ชัดที่สุดจะเกิดขึ้นเมื่อคุณเลื่อนองค์ประกอบย่อยและไปถึงขอบเขตเริ่มต้นหรือสิ้นสุดขององค์ประกอบนั้น และคาดหวังว่าองค์ประกอบหลักจะรับช่วงการเลื่อน อย่างไรก็ตาม ลักษณะการทำงานที่คาดไว้นี้อาจไม่เกิดขึ้นหรืออาจไม่ทำงานตามที่คาดไว้

ปัญหานี้เกิดจากความคาดหวังที่สร้างขึ้นใน Composable ที่เลื่อนได้ Composable ที่เลื่อนได้มีกฎ "เลื่อนที่ซ้อนกันโดยค่าเริ่มต้น" ซึ่งหมายความว่า คอนเทนเนอร์ที่เลื่อนได้จะต้องเข้าร่วมในห่วงโซ่การเลื่อนที่ซ้อนกัน ทั้งในฐานะ องค์ประกอบระดับบนสุดผ่าน NestedScrollConnection และในฐานะองค์ประกอบย่อยผ่าน NestedScrollDispatcher จากนั้นองค์ประกอบย่อยจะขับเคลื่อนการเลื่อนที่ซ้อนกันสำหรับองค์ประกอบหลักเมื่อองค์ประกอบย่อยอยู่ที่ขอบเขต ตัวอย่างเช่น กฎนี้อนุญาตให้ Compose Pager และ Compose LazyRow ทำงานร่วมกันได้อย่างราบรื่น อย่างไรก็ตาม เมื่อเลื่อนแบบทำงานร่วมกันด้วย ViewPager2 หรือ RecyclerView เนื่องจากองค์ประกอบเหล่านี้ไม่ได้ใช้ NestedScrollingParent3 จึงไม่สามารถเลื่อนจากองค์ประกอบย่อยไปยังองค์ประกอบระดับบนสุดได้อย่างต่อเนื่อง

หากต้องการเปิดใช้ API การทำงานร่วมกันของการเลื่อนที่ซ้อนกันระหว่างองค์ประกอบ View ที่เลื่อนได้กับ Composable ที่เลื่อนได้ ซึ่งซ้อนกันทั้ง 2 ทิศทาง คุณสามารถใช้ API การทำงานร่วมกันของการเลื่อนที่ซ้อนกันเพื่อลดปัญหาเหล่านี้ในสถานการณ์ต่อไปนี้

ผู้ปกครองที่ให้ความร่วมมือ View ซึ่งมีบุตร ComposeView

ผู้ปกครองที่ทำงานร่วมกัน View คือผู้ปกครองที่ใช้ NestedScrollingParent3 อยู่แล้ว จึงรับเดลต้าการเลื่อนจาก Composable ย่อยที่ซ้อนกันซึ่งทำงานร่วมกันได้ ComposeView จะทำหน้าที่เป็นไคลเอ็นต์ในกรณีนี้และจะต้อง (โดยอ้อม) ใช้ NestedScrollingChild3 ตัวอย่างของอุปกรณ์ของผู้ปกครองที่ทำงานร่วมกันคือ androidx.coordinatorlayout.widget.CoordinatorLayout

หากต้องการความสามารถในการทำงานร่วมกันของการเลื่อนที่ฝังไว้ระหว่างViewคอนเทนเนอร์หลัก ที่เลื่อนได้กับ Composable ย่อยที่เลื่อนได้ซึ่งฝังไว้ คุณสามารถใช้ rememberNestedScrollInteropConnection() ได้

rememberNestedScrollInteropConnection() อนุญาตและจดจำ NestedScrollConnection ที่เปิดใช้การทำงานร่วมกันของการเลื่อนที่ซ้อนกันระหว่างองค์ประกอบหลัก View ที่ ใช้ NestedScrollingParent3 และองค์ประกอบย่อยของ Compose ควรใช้ร่วมกับตัวแก้ไข nestedScroll เนื่องจากมีการเปิดใช้การเลื่อนที่ซ้อนกันในฝั่ง Compose โดยค่าเริ่มต้น คุณจึงใช้การเชื่อมต่อนี้เพื่อเปิดใช้ทั้งการเลื่อนที่ซ้อนกันในฝั่ง View และเพิ่มตรรกะการเชื่อมต่อที่จำเป็นระหว่าง Views กับ Composable ได้

กรณีการใช้งานที่พบบ่อยคือการใช้ CoordinatorLayout, CollapsingToolbarLayout และ Composable ขององค์ประกอบย่อย ดังที่แสดงในตัวอย่างนี้

<androidx.coordinatorlayout.widget.CoordinatorLayout     xmlns:android="http://schemas.android.com/apk/res/android"     xmlns:app="http://schemas.android.com/apk/res-auto"     android:layout_width="match_parent"     android:layout_height="match_parent">      <com.google.android.material.appbar.AppBarLayout         android:id="@+id/app_bar"         android:layout_width="match_parent"         android:layout_height="100dp"         android:fitsSystemWindows="true">          <com.google.android.material.appbar.CollapsingToolbarLayout             android:id="@+id/collapsing_toolbar_layout"             android:layout_width="match_parent"             android:layout_height="match_parent"             android:fitsSystemWindows="true"             app:layout_scrollFlags="scroll|exitUntilCollapsed">              <!--...-->          </com.google.android.material.appbar.CollapsingToolbarLayout>      </com.google.android.material.appbar.AppBarLayout>      <androidx.compose.ui.platform.ComposeView         android:id="@+id/compose_view"         app:layout_behavior="@string/appbar_scrolling_view_behavior"         android:layout_width="match_parent"         android:layout_height="match_parent"/>  </androidx.coordinatorlayout.widget.CoordinatorLayout>

ในกิจกรรมหรือ Fragment คุณต้องตั้งค่า Composable ของบุตรหลานและ ต้องระบุ NestedScrollConnection

open class MainActivity : ComponentActivity() {     override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContentView(R.layout.activity_main)         findViewById<ComposeView>(R.id.compose_view).apply {             setContent {                 val nestedScrollInterop = rememberNestedScrollInteropConnection()                 // Add the nested scroll connection to your top level @Composable element                 // using the nestedScroll modifier.                 LazyColumn(modifier = Modifier.nestedScroll(nestedScrollInterop)) {                     items(20) { item ->                         Box(                             modifier = Modifier                                 .padding(16.dp)                                 .height(56.dp)                                 .fillMaxWidth()                                 .background(Color.Gray),                             contentAlignment = Alignment.Center                         ) {                             Text(item.toString())                         }                     }                 }             }         }     } }

Composable ระดับบนสุดที่มี Composable ระดับล่าง AndroidView

สถานการณ์นี้ครอบคลุมการใช้งาน API การทำงานร่วมกันของการเลื่อนที่ซ้อนกันในฝั่ง Compose เมื่อคุณมี Composable หลักที่มี Composable ย่อย AndroidView AndroidView จะใช้ NestedScrollDispatcher เนื่องจากทำหน้าที่เป็นองค์ประกอบย่อยขององค์ประกอบหลักที่เลื่อนได้ของ Compose รวมถึง NestedScrollingParent3 เนื่องจากทำหน้าที่เป็นองค์ประกอบหลักขององค์ประกอบย่อยที่เลื่อนได้ View จากนั้นองค์ประกอบหลักจะรับเดลต้าการเลื่อนที่ซ้อนกันจากองค์ประกอบย่อยที่เลื่อนได้ซึ่งซ้อนกันได้ View

ตัวอย่างต่อไปนี้แสดงวิธีที่คุณจะทำให้การเลื่อนที่ซ้อนกันทำงานร่วมกันได้ในสถานการณ์นี้ พร้อมกับแถบเครื่องมือแบบยุบได้ของ Compose

@Composable private fun NestedScrollInteropComposeParentWithAndroidChildExample() {     val toolbarHeightPx = with(LocalDensity.current) { ToolbarHeight.roundToPx().toFloat() }     val toolbarOffsetHeightPx = remember { mutableStateOf(0f) }      // Sets up the nested scroll connection between the Box composable parent     // and the child AndroidView containing the RecyclerView     val nestedScrollConnection = remember {         object : NestedScrollConnection {             override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {                 // Updates the toolbar offset based on the scroll to enable                 // collapsible behaviour                 val delta = available.y                 val newOffset = toolbarOffsetHeightPx.value + delta                 toolbarOffsetHeightPx.value = newOffset.coerceIn(-toolbarHeightPx, 0f)                 return Offset.Zero             }         }     }      Box(         Modifier             .fillMaxSize()             .nestedScroll(nestedScrollConnection)     ) {         TopAppBar(             modifier = Modifier                 .height(ToolbarHeight)                 .offset { IntOffset(x = 0, y = toolbarOffsetHeightPx.value.roundToInt()) }         )          AndroidView(             { context ->                 LayoutInflater.from(context)                     .inflate(R.layout.view_in_compose_nested_scroll_interop, null).apply {                         with(findViewById<RecyclerView>(R.id.main_list)) {                             layoutManager = LinearLayoutManager(context, VERTICAL, false)                             adapter = NestedScrollInteropAdapter()                         }                     }.also {                         // Nested scrolling interop is enabled when                         // nested scroll is enabled for the root View                         ViewCompat.setNestedScrollingEnabled(it, true)                     }             },             // ...         )     } }  private class NestedScrollInteropAdapter :     Adapter<NestedScrollInteropAdapter.NestedScrollInteropViewHolder>() {     val items = (1..10).map { it.toString() }      override fun onCreateViewHolder(         parent: ViewGroup,         viewType: Int     ): NestedScrollInteropViewHolder {         return NestedScrollInteropViewHolder(             LayoutInflater.from(parent.context)                 .inflate(R.layout.list_item, parent, false)         )     }      override fun onBindViewHolder(holder: NestedScrollInteropViewHolder, position: Int) {         // ...     }      class NestedScrollInteropViewHolder(view: View) : ViewHolder(view) {         fun bind(item: String) {             // ...         }     }     // ... } 

ตัวอย่างนี้แสดงวิธีใช้ API กับตัวแก้ไข scrollable

@Composable fun ViewInComposeNestedScrollInteropExample() {     Box(         Modifier             .fillMaxSize()             .scrollable(rememberScrollableState {                 // View component deltas should be reflected in Compose                 // components that participate in nested scrolling                 it             }, Orientation.Vertical)     ) {         AndroidView(             { context ->                 LayoutInflater.from(context)                     .inflate(android.R.layout.list_item, null)                     .apply {                         // Nested scrolling interop is enabled when                         // nested scroll is enabled for the root View                         ViewCompat.setNestedScrollingEnabled(this, true)                     }             }         )     } } 

และสุดท้าย ตัวอย่างนี้แสดงวิธีใช้ API การทำงานร่วมกันของการเลื่อนที่ซ้อนกันกับ BottomSheetDialogFragment เพื่อให้การลากและปิดทำงานได้สำเร็จ

class BottomSheetFragment : BottomSheetDialogFragment() {      override fun onCreateView(         inflater: LayoutInflater,         container: ViewGroup?,         savedInstanceState: Bundle?     ): View {         val rootView: View = inflater.inflate(R.layout.fragment_bottom_sheet, container, false)          rootView.findViewById<ComposeView>(R.id.compose_view).apply {             setContent {                 val nestedScrollInterop = rememberNestedScrollInteropConnection()                 LazyColumn(                     Modifier                         .nestedScroll(nestedScrollInterop)                         .fillMaxSize()                 ) {                     item {                         Text(text = "Bottom sheet title")                     }                     items(10) {                         Text(                             text = "List item number $it",                             modifier = Modifier.fillMaxWidth()                         )                     }                 }             }             return rootView         }     } } 

โปรดทราบว่า rememberNestedScrollInteropConnection() จะติดตั้ง NestedScrollConnection ในองค์ประกอบที่คุณแนบไว้ NestedScrollConnection มีหน้าที่ ส่งเดลต้าจากระดับ Compose ไปยังระดับ View ซึ่งจะช่วยให้องค์ประกอบเข้าร่วมการเลื่อนที่ซ้อนกันได้ แต่จะไม่เปิดใช้การเลื่อนองค์ประกอบโดยอัตโนมัติ สำหรับ Composable ที่เลื่อนไม่ได้โดยอัตโนมัติ เช่น Box หรือ Column เดลต้าการเลื่อนในคอมโพเนนต์ดังกล่าวจะไม่แพร่กระจายในระบบการเลื่อนที่ซ้อนกัน และเดลต้าจะไม่ไปถึง NestedScrollConnection ที่ rememberNestedScrollInteropConnection() ระบุไว้ ดังนั้น เดลต้าเหล่านั้นจะไม่ไปถึงคอมโพเนนต์ View ระดับบน หากต้องการแก้ไขปัญหานี้ โปรดตรวจสอบว่าคุณได้ตั้งค่าตัวแก้ไขที่เลื่อนได้ให้กับ Composables ที่ซ้อนกันประเภทนี้ด้วย คุณดูข้อมูลเพิ่มเติมได้ในส่วนก่อนหน้าเกี่ยวกับการเลื่อน ที่ซ้อนกัน

ผู้ปกครองที่ไม่ให้ความร่วมมือViewซึ่งมีบุตรหลานComposeView

การแสดงผลที่ไม่ทำงานร่วมกันคือการแสดงผลที่ไม่ได้ใช้NestedScrollingอินเทอร์เฟซViewที่จำเป็นในฝั่ง โปรดทราบว่าการทำงานร่วมกันของการเลื่อนที่ซ้อนกันกับ Views เหล่านี้จึงไม่สามารถใช้งานได้ทันที Viewsที่ไม่ให้ความร่วมมือคือ RecyclerView และ ViewPager2

แหล่งข้อมูลเพิ่มเติม