เลื่อน

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

ตัวแก้ไข 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 และ Composables ขององค์ประกอบย่อย ดังที่แสดงในตัวอย่างนี้

<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

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