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

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

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

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

การเลื่อนที่ฝังไว้โดยอัตโนมัติ

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

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

ตัวอย่างต่อไปนี้แสดงองค์ประกอบที่มีตัวปรับแต่ง a 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 รายการ ซึ่งตอบสนองต่อท่าทางสัมผัสภายในและภายนอกองค์ประกอบด้านใน
รูปที่ 1 องค์ประกอบ UI การเลื่อนแนวตั้ง 2 รายการที่ฝังไว้ซึ่งตอบสนองต่อท่าทางภายในและภายนอกองค์ประกอบภายใน

การใช้ตัวปรับแต่ง nestedScroll

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

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

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

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

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

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

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

ระยะก่อนเลื่อน - ส่ง
ขึ้น
รูปที่ 3 ระยะก่อนเลื่อน: การส่งต่อขึ้นไป

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

ระยะก่อนเลื่อน - การฟอง
ลง
รูปที่ 4 ระยะก่อนเลื่อน - การส่งต่อลงมา

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

ระยะการใช้โหนด
รูปที่ 5 ระยะการใช้โหนด

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

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

ระยะหลังการเลื่อน - การส่ง
ขึ้น
รูปที่ 6 ระยะหลังเลื่อน - การส่งต่อขึ้นไป

ระยะหลังเลื่อนทำงานในลักษณะเดียวกับระยะก่อนเลื่อน โดยคอมโพเนนต์หลักใดก็ได้อาจเลือกใช้หรือไม่ใช้ก็ได้

ระยะหลังการเลื่อน - การแสดง
รายการที่เกี่ยวข้อง
รูปที่ 7 ระยะหลังเลื่อน - การส่งต่อลงมา

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

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

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

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

NestedScrollConnection มีวิธีตอบสนองต่อระยะของวงจรการเลื่อนที่ฝังไว้และควบคุมระบบการเลื่อนที่ฝังไว้ โดยประกอบด้วยเมธอด Callback 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 เริ่มต้นวงจรการเลื่อนที่ฝังไว้ การใช้ตัวส่งและเรียกเมธอดของตัวส่งจะทริกเกอร์วงจร คอนเทนเนอร์ที่เลื่อนได้มีตัวส่งในตัวที่ส่งค่าเดลต้าที่บันทึกไว้ระหว่างท่าทางไปยังระบบ ด้วยเหตุนี้ กรณีการใช้งานส่วนใหญ่ของการปรับแต่งการเลื่อนที่ฝังไว้จึงเกี่ยวข้องกับการใช้ 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 ช่วยให้รูปภาพยังคงอยู่ตรงกลางในแนวตั้งขณะที่ปรับสเกล

ผลลัพธ์

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

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

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

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

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

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

หลักที่ทำงานร่วมกัน View ซึ่งมี ย่อย ComposeView

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

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

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

กรณีการใช้งานที่พบบ่อยคือการใช้ 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>

ในกิจกรรมหรือ Fragment คุณต้องตั้งค่าคอมโพสย่อยและ ที่จำเป็น 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 หลักที่มี AndroidView ย่อย

สถานการณ์นี้ครอบคลุมการใช้ Nested Scrolling Interop API ในฝั่ง Compose เมื่อคุณมีคอมโพสหลักที่มี AndroidView ย่อย The AndroidView ใช้ NestedScrollDispatcher, เนื่องจากทำหน้าที่เป็นคอมโพเนนต์ย่อยของคอมโพสหลักที่เลื่อนได้ รวมถึง 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(
            title = {},
            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(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 มีหน้าที่รับผิดชอบในการส่งค่าเดลต้าจากระดับ Compose ไปยังระดับ View ซึ่งจะช่วยให้องค์ประกอบมีส่วนร่วมในการเลื่อนที่ฝังไว้ แต่ไม่ได้เปิดใช้การเลื่อนองค์ประกอบโดยอัตโนมัติ สำหรับคอมโพสที่เลื่อนไม่ได้โดยอัตโนมัติ เช่น Box หรือ Column ค่าเดลต้าของการเลื่อนในคอมโพเนนต์ดังกล่าวจะไม่เผยแพร่ในระบบการเลื่อนที่ฝังไว้ และค่าเดลต้าจะไม่ไปถึง NestedScrollConnection ที่ rememberNestedScrollInteropConnection() ให้ไว้ ดังนั้นค่าเดลต้าเหล่านั้นจะไม่ไปถึงคอมโพเนนต์หลัก View หากต้องการแก้ไขปัญหานี้ โปรดตรวจสอบว่าคุณได้ตั้งค่าตัวปรับแต่งที่เลื่อนได้ให้กับคอมโพสที่ฝังไว้ประเภทเหล่านี้ด้วย ดูข้อมูลโดยละเอียดเพิ่มเติมได้ในส่วนก่อนหน้าเกี่ยวกับการเลื่อนที่ฝังไว้

View หลักที่ไม่ทำงานร่วมกันซึ่งมี ComposeView ย่อย

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

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