ตัวแก้ไขการเลื่อน
ตัวแก้ไข 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
จะตรวจจับท่าทางสัมผัสในการเลื่อนและบันทึกค่า Delta แต่จะไม่เลื่อนเนื้อหาโดยอัตโนมัติ แต่จะมอบสิทธิ์ให้ผู้ใช้ผ่าน ScrollableState
instead ซึ่งจําเป็นต่อการทํางานของเงื่อนไขเพิ่มเติมนี้
เมื่อสร้าง 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()) } }
การเลื่อนที่ฝังไว้
การเลื่อนที่ซ้อนกันคือระบบที่คอมโพเนนต์การเลื่อนหลายรายการที่อยู่ภายในกันและกันทำงานร่วมกันโดยการตอบสนองต่อท่าทางสัมผัสการเลื่อนเดียวและสื่อสารค่า Delta (การเปลี่ยนแปลง) ของการเลื่อน
ระบบการเลื่อนที่ซ้อนกันช่วยให้คอมโพเนนต์ที่เลื่อนได้และลิงก์ตามลําดับชั้น (ส่วนใหญ่โดยการแชร์องค์ประกอบหลักเดียวกัน) ทำงานร่วมกันได้ ระบบนี้จะลิงก์คอนเทนเนอร์การเลื่อนและอนุญาตให้โต้ตอบกับค่า Delta ของการเลื่อนที่กำลังเผยแพร่และแชร์ระหว่างกัน
Compose มีวิธีจัดการการเลื่อนที่ซ้อนกันระหว่างคอมโพสิเบิลหลายวิธี ตัวอย่างทั่วไปของการเลื่อนที่ซ้อนกันคือรายการภายในรายการอื่น และกรณีที่ซับซ้อนมากขึ้นคือแถบเครื่องมือที่ยุบได้
การเลื่อนที่ซ้อนกันอัตโนมัติ
คุณไม่จำเป็นต้องดำเนินการใดๆ กับการเลื่อนแบบซ้อนกันแบบง่าย ระบบจะส่งต่อท่าทางสัมผัสที่เริ่มการเลื่อนจากองค์ประกอบย่อยไปยังองค์ประกอบหลักโดยอัตโนมัติ เช่น เมื่อองค์ประกอบย่อยเลื่อนไม่ได้อีกต่อไป องค์ประกอบหลักจะจัดการท่าทางสัมผัสนั้น
คอมโพเนนต์และตัวแก้ไขบางอย่างของ Compose รองรับและพร้อมใช้งานโดยอัตโนมัติในการเลื่อนแบบซ้อนกัน ดังนี้
verticalScroll
,
horizontalScroll
,
scrollable
,
Lazy
API และ TextField
ซึ่งหมายความว่าเมื่อผู้ใช้เลื่อนองค์ประกอบย่อยภายในของคอมโพเนนต์ที่ฝังอยู่ ตัวแก้ไขก่อนหน้าจะส่งค่า Delta ของการเลื่อนไปยังองค์ประกอบหลักที่รองรับการเลื่อนที่ฝังอยู่
ตัวอย่างต่อไปนี้แสดงองค์ประกอบที่มีการใช้ตัวแก้ไข 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) ) } } } } }
การใช้ตัวแก้ไข nestedScroll
หากต้องการสร้างการเลื่อนแบบประสานขั้นสูงระหว่างองค์ประกอบหลายรายการ ตัวแปรปรวน nestedScroll
จะช่วยให้คุณมีความยืดหยุ่นมากขึ้นด้วยการกำหนดลําดับชั้นการเลื่อนที่ฝังอยู่ ดังที่กล่าวไว้ในส่วนก่อนหน้า คอมโพเนนต์บางรายการรองรับการเลื่อนที่ฝังอยู่ในตัว อย่างไรก็ตาม สำหรับคอมโพสิชันที่เลื่อนไม่ได้โดยอัตโนมัติ เช่น Box
หรือ Column
ค่า Delta ของการเลื่อนในคอมโพเนนต์ดังกล่าวจะไม่นำไปใช้กับระบบการเลื่อนแบบซ้อน และค่า Delta ดังกล่าวจะไม่ไปถึง NestedScrollConnection
หรือคอมโพเนนต์หลัก หากต้องการแก้ไขปัญหานี้ คุณสามารถใช้ nestedScroll
เพื่อมอบการสนับสนุนดังกล่าวให้กับคอมโพเนนต์อื่นๆ ซึ่งรวมถึงคอมโพเนนต์ที่กำหนดเอง
รอบการเลื่อนที่ฝังไว้
รอบการเลื่อนที่ฝังอยู่คือลำดับของ Delta การเลื่อนที่ส่งขึ้นและลงตามลำดับชั้นผ่านคอมโพเนนต์ (หรือโหนด) ทั้งหมดที่เป็นส่วนหนึ่งของระบบการเลื่อนที่ฝังอยู่ เช่น โดยใช้คอมโพเนนต์และตัวแก้ไขที่เลื่อนได้ หรือnestedScroll
ระยะต่างๆ ของรอบการเลื่อนที่ฝังไว้
เมื่อคอมโพเนนต์ที่เลื่อนได้ตรวจพบเหตุการณ์ทริกเกอร์ (เช่น ท่าทางสัมผัส) ระบบจะส่งค่า Delta ที่สร้างขึ้นไปยังระบบการเลื่อนที่ฝังอยู่และดำเนินการผ่าน 3 ระยะ ได้แก่ ระยะก่อนการเลื่อน ระยะการใช้โหนด และระยะหลังการเลื่อน ก่อนที่จะทริกเกอร์การเลื่อนจริง
ในเฟสแรกก่อนการเลื่อน คอมโพเนนต์ที่ได้รับเหตุการณ์ทริกเกอร์จะส่งเหตุการณ์เหล่านั้นไปยังส่วนบนผ่านต้นไม้ลําดับชั้นไปยังส่วนบนสุด จากนั้นเหตุการณ์ Delta จะทยอยส่งต่อ ซึ่งหมายความว่า Delta จะแพร่กระจายจากรูทระดับบนสุดไปยังรูทระดับล่างที่เริ่มรอบการเลื่อนแบบซ้อน
ซึ่งจะช่วยให้องค์ประกอบหลักที่มีการเลื่อนแบบซ้อนกัน (คอมโพสิเบิลที่ใช้ nestedScroll
หรือตัวแก้ไขที่เลื่อนได้) มีเวลาดำเนินการบางอย่างกับ Delta ก่อนที่โหนดเองจะใช้ได้
ในขั้นตอนการบริโภคโหนด โหนดจะใช้ Delta ที่โหนดหลักไม่ได้ใช้ นั่นคือเมื่อการเคลื่อนไหวในการเลื่อนเสร็จสมบูรณ์และมองเห็นได้
ในระหว่างระยะนี้ บุตรหลานอาจเลือกใช้เครดิตที่เหลือทั้งหมดหรือบางส่วน ระบบจะส่งรายการที่เหลือกลับขึ้นไปเพื่อเข้าสู่ระยะหลังการเลื่อน
สุดท้าย ในเฟสหลังการเลื่อน ทุกอย่างที่โหนดไม่ได้ใช้จะส่งไปยังโหนดหลักอีกครั้งเพื่อใช้
ระยะหลังการเลื่อนจะทํางานคล้ายกับระยะก่อนการเลื่อน ซึ่งโฆษณาหลักจะเลือกรับหรือไม่ก็ได้
เช่นเดียวกับการเลื่อน เมื่อท่าทางสัมผัสการลากสิ้นสุดลง ระบบอาจเปลี่ยนความตั้งใจของผู้ใช้เป็นความเร็วที่ใช้เพื่อปัด (เลื่อนโดยใช้ภาพเคลื่อนไหว) คอนเทนเนอร์ที่เลื่อนได้ การปัดยังเป็นส่วนหนึ่งของวงจรการเลื่อนที่ซ้อนกันด้วย และความเร็วที่เกิดจากเหตุการณ์การลากจะผ่านระยะต่างๆ ที่คล้ายกัน ได้แก่ ช่วงก่อนการปัด การใช้โหนด และการปัด โปรดทราบว่าภาพเคลื่อนไหวจากการปัดจะเชื่อมโยงกับท่าทางสัมผัสเท่านั้น และจะไม่ทริกเกอร์โดยเหตุการณ์อื่นๆ เช่น a11y หรือการเลื่อนด้วยฮาร์ดแวร์
เข้าร่วมรอบการเลื่อนที่ซ้อนกัน
การเข้าร่วมในวงจรหมายถึงการขัดจังหวะ การใช้ และการรายงานการใช้ Delta ตามลําดับชั้น Compose มีชุดเครื่องมือที่ส่งผลต่อวิธีการทำงานของระบบการเลื่อนที่ซ้อนกัน และวิธีโต้ตอบกับระบบโดยตรง เช่น เมื่อคุณต้องการดำเนินการบางอย่างกับค่า Delta ของการเลื่อนก่อนที่คอมโพเนนต์ที่เลื่อนได้จะเริ่มเลื่อน
หากรอบการเลื่อนที่ซ้อนกันคือระบบที่ทำงานกับเชนโหนด ตัวแก้ไข nestedScroll
จะเป็นวิธีสกัดกั้นและแทรกลงในการเปลี่ยนแปลงเหล่านี้ รวมถึงส่งผลต่อข้อมูล (การเลื่อนส่วนต่าง) ที่เผยแพร่ในเชน คุณสามารถวางตัวแก้ไขนี้ไว้ที่ใดก็ได้ในลําดับชั้น และตัวแก้ไขจะสื่อสารกับอินสแตนซ์ตัวแก้ไขการเลื่อนที่ฝังอยู่ตามลําดับชั้นเพื่อให้แชร์ข้อมูลผ่านช่องทางนี้ได้ องค์ประกอบพื้นฐานของตัวแปรนี้คือ NestedScrollConnection
และ NestedScrollDispatcher
NestedScrollConnection
มีวิธีตอบสนองต่อระยะต่างๆ ของวงจรการเลื่อนที่ซ้อนกัน และส่งผลต่อระบบการเลื่อนที่ซ้อนกัน ประกอบด้วยเมธอดการเรียกกลับ 4 รายการ ซึ่งแต่ละรายการแสดงถึงช่วงการบริโภค 1 ช่วง ได้แก่ ก่อน/หลังการเลื่อนและก่อน/หลังการฟลิง
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
เดลต้าที่ใช้ในระยะก่อนหน้า หากต้องการหยุดการเผยแพร่ Delta ขึ้นตามลําดับชั้นเมื่อใดก็ตาม คุณสามารถใช้การเชื่อมต่อการเลื่อนที่ซ้อนกันเพื่อดำเนินการดังนี้
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
เริ่มต้นรอบการเลื่อนที่ซ้อนกัน การใช้ตัวจัดเตรียมและการเรียกใช้เมธอดจะทริกเกอร์วงจร คอนเทนเนอร์ที่เลื่อนได้จะมีตัวจัดสรรในตัวซึ่งจะส่งการเปลี่ยนแปลงที่บันทึกระหว่างการใช้ท่าทางสัมผัสไปยังระบบ ด้วยเหตุนี้ กรณีการใช้งานส่วนใหญ่ในการปรับแต่งการเลื่อนที่ซ้อนกันจึงเกี่ยวข้องกับการใช้ NestedScrollConnection
แทนตัวจัดเตรียม เพื่อตอบสนองต่อค่า Delta ที่มีอยู่แทนการส่งค่าใหม่
ดูการใช้งานเพิ่มเติมที่ 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
จะคำนวณการเปลี่ยนแปลงขนาดรูปภาพตามค่า Delta ของการเลื่อน- ตัวแปรสถานะ
currentImageSize
จะจัดเก็บขนาดปัจจุบันของรูปภาพ ซึ่งถูกจำกัดระหว่างminImageSize
ถึงmaxImageSize. imageScale
โดยมาจากcurrentImageSize
LazyColumn
จะหักลบตามcurrentImageSize
Image
ใช้ตัวแก้ไขgraphicsLayer
เพื่อใช้มาตราส่วนที่คํานวณแล้วtranslationY
ภายในgraphicsLayer
ช่วยให้รูปภาพอยู่ในแนวตั้งตรงกลางเสมอเมื่อปรับขนาด
ผลลัพธ์
ตัวอย่างข้อมูลก่อนหน้าจะทำให้เกิดเอฟเฟกต์การปรับขนาดรูปภาพเมื่อเลื่อน
การทำงานร่วมกันของการเลื่อนที่ฝังไว้
คุณอาจพบปัญหาเมื่อพยายามฝังองค์ประกอบ View
ที่เลื่อนได้ในคอมโพสิชันที่เลื่อนได้ หรือในทางกลับกัน ข้อบกพร่องที่เห็นได้ชัดที่สุดจะเกิดขึ้นเมื่อคุณเลื่อนองค์ประกอบย่อยและไปถึงขอบเขตเริ่มต้นหรือขอบเขตสิ้นสุดขององค์ประกอบย่อย และคาดหวังว่าองค์ประกอบหลักจะเลื่อนต่อ อย่างไรก็ตาม ลักษณะการทำงานที่คาดไว้นี้อาจไม่เกิดขึ้นหรืออาจไม่ทำงานตามที่คาดไว้
ปัญหานี้เป็นผลมาจากความคาดหวังที่สร้างขึ้นในส่วนคอมโพสิเบิลที่เลื่อนได้
คอมโพสิชันที่เลื่อนได้จะมีกฎ "การเลื่อนแบบซ้อนกันโดยค่าเริ่มต้น" ซึ่งหมายความว่าคอนเทนเนอร์ที่เลื่อนได้ต้องเข้าร่วมในเชนการเลื่อนแบบซ้อนกัน ทั้งในฐานะคอนเทนเนอร์หลักผ่าน NestedScrollConnection
และในฐานะคอนเทนเนอร์ย่อยผ่าน NestedScrollDispatcher
จากนั้น องค์ประกอบย่อยจะขับเคลื่อนการเลื่อนที่ฝังอยู่สําหรับองค์ประกอบหลักเมื่อองค์ประกอบย่อยอยู่ตรงขอบเขต ตัวอย่างเช่น กฎนี้ช่วยให้ Compose Pager
และ Compose LazyRow
ทำงานร่วมกันได้ดี อย่างไรก็ตาม เมื่อมีการเลื่อนเพื่อการทำงานร่วมกันด้วย ViewPager2
หรือ RecyclerView
เนื่องจากรูปแบบเหล่านี้ไม่ได้ใช้ NestedScrollingParent3
คุณจะเลื่อนจากองค์ประกอบย่อยไปยังองค์ประกอบหลักอย่างต่อเนื่องไม่ได้
หากต้องการเปิดใช้ API การทํางานร่วมกันแบบเลื่อนซ้อนกันระหว่างองค์ประกอบ View
ที่เลื่อนได้และคอมโพสิชันที่เลื่อนได้ซึ่งซ้อนกันทั้ง 2 ทิศทาง คุณสามารถใช้ API การทํางานร่วมกันแบบเลื่อนซ้อนกันเพื่อบรรเทาปัญหาเหล่านี้ในสถานการณ์ต่อไปนี้
รายการหลักที่ร่วมมือกัน View
ที่มีรายการย่อย ComposeView
View
หลักที่ทำงานร่วมกันคือ View
ที่ใช้ NestedScrollingParent3
อยู่แล้ว จึงสามารถรับค่า Delta ของการเลื่อนจากคอมโพสิเบิลย่อยที่ฝังอยู่ซึ่งทำงานร่วมกัน ComposeView
จะทำหน้าที่เป็นผู้เผยแพร่โฆษณาย่อยในกรณีนี้ และจะต้องติดตั้งใช้งาน NestedScrollingChild3
(โดยอ้อม)
ตัวอย่างหนึ่งของผู้ปกครองที่ร่วมมือกันคือ
androidx.coordinatorlayout.widget.CoordinatorLayout
หากต้องการความสามารถในการทำงานร่วมกันของการเลื่อนที่ฝังไว้ระหว่างView
คอนเทนเนอร์หลักที่เลื่อนได้กับการคอมโพสิชันย่อยที่เลื่อนได้ซึ่งฝังไว้ ให้ใช้ rememberNestedScrollInteropConnection()
rememberNestedScrollInteropConnection()
อนุญาตและจดจำ NestedScrollConnection
ที่เปิดใช้การทำงานร่วมกันของแถบเลื่อนที่ซ้อนกันระหว่างองค์ประกอบหลัก View
ที่ใช้ NestedScrollingParent3
และองค์ประกอบย่อย Compose ซึ่งควรใช้ร่วมกับตัวแก้ไข nestedScroll
เนื่องจากระบบเปิดใช้การเลื่อนแบบซ้อนกันโดยค่าเริ่มต้นในฝั่ง Compose คุณจึงใช้การเชื่อมต่อนี้เพื่อเปิดใช้ทั้งการเลื่อนแบบซ้อนกันในฝั่ง View
และเพิ่มตรรกะการเชื่อมโยงที่จำเป็นระหว่าง Views
กับคอมโพสิเบิลได้
Use Case ที่พบบ่อยคือการใช้ CoordinatorLayout
, CollapsingToolbarLayout
และคอมโพสิชันย่อยดังที่แสดงในตัวอย่างนี้
<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>
ในกิจกรรมหรือแฟรกเมนต์ คุณต้องตั้งค่าคอมโพสิชันย่อยและ 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()) } } } } } } }
คอมโพสิชันหลักที่มี AndroidView
ย่อย
สถานการณ์นี้ครอบคลุมการใช้งาน API การทํางานร่วมกันแบบเลื่อนที่ซ้อนกันในด้าน Compose เมื่อคุณมีคอมโพสิชันหลักที่มีคอมโพสิชันย่อยAndroidView
AndroidView
ใช้
NestedScrollDispatcher
เนื่องจากทำหน้าที่เป็นองค์ประกอบย่อยขององค์ประกอบหลักแบบเลื่อนของ Compose และ
NestedScrollingParent3
เนื่องจากทำหน้าที่เป็นองค์ประกอบหลักขององค์ประกอบย่อยแบบเลื่อนของ View
จากนั้นองค์ประกอบหลักของข้อความจะรับค่า Delta ของการเลื่อนที่ซ้อนกันจากองค์ประกอบย่อยที่เลื่อนได้ซึ่งซ้อนกันView
ตัวอย่างต่อไปนี้แสดงวิธีทำให้การเลื่อนแบบซ้อนกันทำงานร่วมกันได้ในสถานการณ์นี้ พร้อมกับแถบเครื่องมือแบบยุบของเครื่องมือเขียน
@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)
}
}
)
}
}
สุดท้ายนี้ ตัวอย่างนี้จะแสดงวิธีใช้ Nested Scrolling Interop 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
มีหน้าที่รับผิดชอบในการส่งข้อมูล Delta จากระดับการเขียนไปยังระดับ View
ซึ่งจะเปิดใช้องค์ประกอบในการเลื่อนที่ซ้อนกัน แต่ไม่เปิดใช้การเลื่อนองค์ประกอบโดยอัตโนมัติ สำหรับคอมโพสิชันที่เลื่อนไม่ได้โดยอัตโนมัติ เช่น Box
หรือ Column
ส่วนต่างของการเลื่อนในคอมโพเนนต์ดังกล่าวจะไม่นำไปใช้กับระบบการเลื่อนที่ฝังอยู่ และส่วนต่างดังกล่าวจะไม่ไปถึง NestedScrollConnection
ที่ rememberNestedScrollInteropConnection()
ระบุไว้ ดังนั้นส่วนต่างเหล่านั้นจะไม่ไปถึงคอมโพเนนต์ View
หลัก วิธีแก้ปัญหานี้คือ ให้ตรวจสอบว่าคุณได้ตั้งค่าตัวปรับเปลี่ยนที่เลื่อนได้ให้กับคอมโพสิเบิลที่ฝังอยู่ประเภทเหล่านี้ด้วย ดูข้อมูลโดยละเอียดได้ในส่วนก่อนหน้าเกี่ยวกับการเลื่อนแบบซ้อน
ผู้ปกครองที่ไม่ให้ความร่วมมือ View
ที่มีบุตร ComposeView
มุมมองที่ไม่ร่วมมือคือมุมมองที่ไม่ได้ใช้อินเทอร์เฟซNestedScrolling
ที่จำเป็นในฝั่งView
โปรดทราบว่าการทำงานร่วมกันของการเลื่อนแบบซ้อนกับ Views
เหล่านี้จะไม่ทำงานโดยอัตโนมัติ Views
ที่ไม่ให้ความร่วมมือ ได้แก่ RecyclerView
และ ViewPager2
แนะนำสำหรับคุณ
- หมายเหตุ: ข้อความลิงก์จะแสดงเมื่อ JavaScript ปิดอยู่
- ทำความเข้าใจท่าทางสัมผัส
- ย้ายข้อมูล
CoordinatorLayout
ไปยัง "เขียน" - การใช้มุมมองในโหมดเขียน