في Jetpack Compose، يُعدّ scrollable2D وdraggable2D من المعدِّلات المنخفضة المستوى المصمّمة للتعامل مع إدخال المؤشر في بُعدَين. في حين أنّ معدِّلات البُعد الواحد العادية scrollable وdraggable تقتصر على اتجاه واحد، فإنّ معدِّلات البُعدَين تتبع الحركة على محورَي X وY في الوقت نفسه.
على سبيل المثال، يتم استخدام المعدِّل الحالي scrollable للتمرير والتحريك في اتجاه واحد، بينما يتم استخدام scrollable2d للتمرير والتحريك في اتجاهين. يتيح لك ذلك إنشاء تخطيطات أكثر تعقيدًا تتحرك في جميع
الاتجاهات، مثل جداول البيانات أو عارض الصور. يتيح المُعدِّل scrollable2d أيضًا
التمرير المُدمَج في السيناريوهات الثنائية الأبعاد.
اختَر scrollable2D أو draggable2D
يعتمد اختيار واجهة برمجة التطبيقات المناسبة على عناصر في واجهة المستخدم التي تريد نقلها والسلوك المادي المفضّل لهذه العناصر.
Modifier.scrollable2D: استخدِم هذا المعدِّل على حاوية لنقل المحتوى بداخلها. على سبيل المثال، يمكنك استخدامها مع الخرائط أو جداول البيانات أو عارض الصور، حيث يحتاج محتوى الحاوية إلى التمرير في الاتجاهين الأفقي والعمودي. وتتضمّن هذه الميزة دعمًا مدمجًا للتمرير السريع، ما يتيح استمرار حركة المحتوى بعد التمرير، وتنسيقها مع مكوّنات التمرير الأخرى على الصفحة.
Modifier.draggable2D: استخدِم هذا المعدِّل لنقل أحد المكوّنات. وهو معدِّل خفيف، لذا تتوقف الحركة عندما يتوقف إصبع المستخدم بالضبط. ولا يتضمّن ميزة "الإلقاء".
إذا كنت تريد أن يكون أحد المكوّنات قابلاً للسحب، ولكنك لا تحتاج إلى دعم السحب السريع أو التمرير المتداخل، استخدِم draggable2D.
تنفيذ المُعدِّلات الثنائية الأبعاد
تقدّم الأقسام التالية أمثلة لتوضيح كيفية استخدام أدوات التعديل الثنائية الأبعاد.
تنفيذ Modifier.scrollable2D
استخدِم هذا المُعدِّل للحاويات التي يحتاج فيها المستخدم إلى نقل المحتوى في جميع الاتجاهات.
تسجيل بيانات الحركة الثنائية الأبعاد
يوضّح هذا المثال كيفية تسجيل بيانات الحركة الأولية الثنائية الأبعاد وعرض الإزاحة على المحورين X وY:
@Composable private fun Scrollable2DSample() { // 1. Manually track the total distance the user has moved in both X and Y directions var offset by remember { mutableStateOf(Offset.Zero) } Box( modifier = Modifier .fillMaxSize() // ... contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(200.dp) // 2. Attach the 2D scroll logic to capture XY movement deltas .scrollable2D( state = rememberScrollable2DState { delta -> // 3. Update the cumulative offset state with the new movement delta offset += delta // Return the delta to indicate the entire movement was handled by this box delta } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { // 4. Display the current X and Y values from the offset state in real-time Text( text = "X: ${offset.x.roundToInt()}", // ... ) Spacer(modifier = Modifier.height(8.dp)) Text( text = "Y: ${offset.y.roundToInt()}", // ... ) } } } }
ينفّذ المقتطف السابق ما يلي:
- يستخدم
offsetكحالة تتضمّن إجمالي المسافة التي انتقل إليها المستخدم. - داخل
rememberScrollable2DState، يتم تحديد دالة lambda للتعامل مع كل دلتا، يتم إنشاؤها بواسطة إصبع المستخدم. يعدّل الرمزoffset.value += deltaالحالة اليدوية بالموضع الجديد. - تعرض مكوّنات
Textقيمتَي X وY الحالية لحالةoffset، ويتم تعديلها في الوقت الفعلي أثناء سحب المستخدم.
تحريك إطار عرض كبير
يوضّح هذا المثال كيفية استخدام بيانات ثنائية الأبعاد قابلة للتمرير وتطبيق
translationX وtranslationY على محتوى أكبر من حاويته
الرئيسية:
@Composable private fun Panning2DImage() { // Manually track the total distance the user has moved in both X and Y directions val offset = remember { mutableStateOf(Offset.Zero) } // Define how gestures are captured. The lambda is called for every finger movement val scrollState = rememberScrollable2DState { delta -> offset.value += delta delta } // The Viewport (Container): A fixed-size box that acts as a window into the larger content Box( modifier = Modifier .size(600.dp, 400.dp) // The visible area dimensions // ... // Hide any parts of the large content that sit outside this container's boundaries .clipToBounds() // Apply the 2D scroll modifier to intercept touch and fling gestures in all directions .scrollable2D(state = scrollState), contentAlignment = Alignment.Center, ) { // The Content: An image given a much larger size than the container viewport Image( painter = painterResource(R.drawable.cheese_5), contentDescription = null, modifier = Modifier .requiredSize(1200.dp, 800.dp) // Manual Scroll Effect: Since scrollable2D doesn't move content automatically, // we use graphicsLayer to shift the drawing position based on the tracked offset. .graphicsLayer { translationX = offset.value.x translationY = offset.value.y }, contentScale = ContentScale.FillBounds ) } }
Modifier.scrollable2DModifier.scrollable2D.يتضمّن المقتطف السابق ما يلي:
- يتم ضبط الحاوية على حجم ثابت (
600x400dp)، بينما يتم منح المحتوى حجمًا أكبر بكثير (1200x800dp) لتجنُّب تغيير حجمه إلى حجم الحاوية الرئيسية. - يضمن المعدِّل
clipToBounds()في الحاوية إخفاء أي جزء من المحتوى الكبير الذي يقع خارج المربّع600x400عن العرض. - على عكس المكوّنات ذات المستوى العالي، مثل
LazyColumn، لا تنقلscrollable2Dالمحتوى تلقائيًا. بدلاً من ذلك، يجب تطبيقoffsetالذي يتم تتبُّعه على المحتوى، إما باستخدام تحويلاتgraphicsLayerأو إزاحات التنسيق. - داخل مجموعة
graphicsLayer، تعملtranslationX = offset.value.xوtranslationY = offset.value.yعلى تغيير موضع رسم الصورة أو النص استنادًا إلى حركة إصبعك، ما يؤدي إلى إنشاء التأثير المرئي للتمرير.
تنفيذ الانتقال المتداخل باستخدام scrollable2D
يوضح هذا المثال كيف يمكن دمج مكون ثنائي الاتجاه في مكون رئيسي قياسي أحادي البعد، مثل خلاصة الأخبار الرأسي.
يُرجى مراعاة النقاط التالية عند تنفيذ ميزة "التمرير المُدمَج":
- يجب أن تعرض دالة lambda الخاصة بـ
rememberScrollable2DStateالفرق المستخدَم فقط، وذلك للسماح للقائمة الرئيسية بالحلول محلّها تلقائيًا عندما يصل الطفل إلى الحدّ الأقصى. - عندما ينفّذ المستخدم حركة سريعة قُطرية، تتم مشاركة السرعة الثنائية الأبعاد. إذا وصل العنصر الثانوي إلى حدّ معيّن أثناء الحركة، يتم نشر الزخم المتبقي إلى العنصر الرئيسي لمواصلة الانتقال بشكل طبيعي.
@Composable private fun NestedScrollable2DSample() { var offset by remember { mutableStateOf(Offset.Zero) } val maxScrollDp = 250.dp val maxScrollPx = with(LocalDensity.current) { maxScrollDp.toPx() } Column( modifier = Modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .background(Color(0xFFF5F5F5)), horizontalAlignment = Alignment.CenterHorizontally ) { Text( "Scroll down to find the 2D Box", modifier = Modifier.padding(top = 100.dp, bottom = 500.dp), style = TextStyle(fontSize = 18.sp, color = Color.Gray) ) // The Child: A 2D scrollable box with nested scroll coordination Box( modifier = Modifier .size(250.dp) .scrollable2D( state = rememberScrollable2DState { delta -> val oldOffset = offset // Calculate new potential offset and clamp it to our boundaries val newX = (oldOffset.x + delta.x).coerceIn(-maxScrollPx, maxScrollPx) val newY = (oldOffset.y + delta.y).coerceIn(-maxScrollPx, maxScrollPx) val newOffset = Offset(newX, newY) // Calculate exactly how much was consumed by the child val consumed = newOffset - oldOffset offset = newOffset // IMPORTANT: Return ONLY the consumed delta. // The remaining (unconsumed) delta propagates to the parent Column. consumed } ) // ... contentAlignment = Alignment.Center ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { val density = LocalDensity.current Text("2D Panning Zone", color = Color.White.copy(alpha = 0.7f), fontSize = 12.sp) Spacer(Modifier.height(8.dp)) Text("X: ${with(density) { offset.x.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) Text("Y: ${with(density) { offset.y.toDp().value.roundToInt() }}dp", color = Color.White, fontWeight = FontWeight.Bold) } } Text( "Once the Purple Box hits Y: 250 or -250,\nthis parent list will take over the vertical scroll.", textAlign = TextAlign.Center, modifier = Modifier.padding(top = 40.dp, bottom = 800.dp), style = TextStyle(fontSize = 14.sp, color = Color.Gray) ) } }
في المقتطف السابق:
- يمكن أن يستهلك المكوّن الثنائي الأبعاد حركة المحور X للتنقل داخليًا مع إرسال حركة المحور Y في الوقت نفسه إلى القائمة الرئيسية عند الوصول إلى الحدود العمودية الخاصة بالعنصر التابع.
- وبدلاً من حصر المستخدم في المساحة الثنائية الأبعاد، يحسب النظام الفرق المستخدَم ويمرّر المتبقي إلى أعلى التسلسل الهرمي. يضمن ذلك أن يتمكن المستخدم من مواصلة التمرير خلال بقية الصفحة دون رفع إصبعه.
تنفيذ Modifier.draggable2D
لمزيد من المعلومات حول كيفية استخدام Modifier.draggable2D، يُرجى الاطّلاع على السحب في بُعدَين باستخدام Modifier.draggable2D.