เลื่อน

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

ตัวแก้ไข 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())
    }
}

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

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

การเลื่อนที่ซ้อนกันคือระบบที่คอมโพเนนต์การเลื่อนหลายรายการที่อยู่ภายในกันและกันทำงานร่วมกันโดยการตอบสนองต่อท่าทางสัมผัสการเลื่อนเดียวและสื่อสารค่า 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)
                    )
                }
            }
        }
    }
}

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

การใช้ตัวแก้ไข 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 ช่วยให้รูปภาพอยู่ในแนวตั้งตรงกลางเสมอเมื่อปรับขนาด

ผลลัพธ์

ตัวอย่างข้อมูลก่อนหน้าจะทำให้เกิดเอฟเฟกต์การปรับขนาดรูปภาพเมื่อเลื่อน

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

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

คุณอาจพบปัญหาเมื่อพยายามฝังองค์ประกอบ 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