بالإضافة إلى الدالة المركّبة Canvas، تتضمّن Compose العديد من الرسومية
Modifiers التي تساعد في رسم محتوى مخصّص. تكون هذه المعدِّلات مفيدة لأنّه يمكن تطبيقها على أي دالة مركّبة.
معدِّلات الرسم
يتم تنفيذ جميع أوامر الرسم باستخدام معدِّل رسم في Compose. هناك ثلاثة معدِّلات رسم رئيسية في Compose:
المعدِّل الأساسي للرسم هو drawWithContent، حيث يمكنك تحديد ترتيب رسم الدالة المركّبة وأوامر الرسم التي يتم إصدارها داخل المعدِّل. drawBehind هو برنامج تضمين مناسب حول drawWithContent تم ضبط ترتيب الرسم فيه على أن يكون خلف محتوى الدالة المركّبة. تستدعي الدالة drawWithCache إما onDrawBehind أو onDrawWithContent بداخلها، وتوفّر آلية لتخزين العناصر التي تم إنشاؤها فيها مؤقتًا.
Modifier.drawWithContent: اختيار ترتيب الرسم
Modifier.drawWithContent تتيح لك
تنفيذ عمليات DrawScope قبل محتوى الدالة المركّبة أو بعده. احرص على استدعاء drawContent لعرض المحتوى الفعلي للدالة المركّبة بعد ذلك. باستخدام هذا المعدِّل، يمكنك تحديد ترتيب العمليات، سواء أردت رسم المحتوى قبل عمليات الرسم المخصّصة أو بعدها.
على سبيل المثال، إذا أردت عرض تدرّج شعاعي فوق المحتوى لإنشاء تأثير ثقب المفتاح في ضوء الفلاش على واجهة المستخدم، يمكنك إجراء ما يلي:
var pointerOffset by remember { mutableStateOf(Offset(0f, 0f)) } Column( modifier = Modifier .fillMaxSize() .pointerInput("dragging") { detectDragGestures { change, dragAmount -> pointerOffset += dragAmount } } .onSizeChanged { pointerOffset = Offset(it.width / 2f, it.height / 2f) } .drawWithContent { drawContent() // draws a fully black area with a small keyhole at pointerOffset that’ll show part of the UI. drawRect( Brush.radialGradient( listOf(Color.Transparent, Color.Black), center = pointerOffset, radius = 100.dp.toPx(), ) ) } ) { // Your composables here }
Modifier.drawBehind: الرسم خلف دالة مركّبة
Modifier.drawBehind تتيح لك تنفيذ عمليات
DrawScope خلف محتوى الدالة المركّبة الذي يتم رسمه على الشاشة. إذا
ألقيت نظرة على تنفيذ Canvas، قد تلاحظ أنّه
مجرد برنامج تضمين مناسب حول Modifier.drawBehind.
لرسم مستطيل مستدير خلف Text:
Text( "Hello Compose!", modifier = Modifier .drawBehind { drawRoundRect( Color(0xFFBBAAEE), cornerRadius = CornerRadius(10.dp.toPx()) ) } .padding(4.dp) )
يؤدي ذلك إلى ظهور النتيجة التالية:
Modifier.drawWithCache: رسم العناصر وتخزينها مؤقتًا
Modifier.drawWithCache تحتفظ بالعناصر
التي يتم إنشاؤها بداخلها مخزّنة مؤقتًا. يتم تخزين العناصر مؤقتًا طالما أنّ حجم مساحة الرسم هو نفسه، أو لم تتغيّر أي عناصر حالة تمت قراءتها. يكون هذا المعدِّل مفيدًا لتحسين أداء طلبات الرسم لأنّه يتجنّب الحاجة إلى إعادة تخصيص العناصر (مثل: Brush, Shader, Path وما إلى ذلك) التي يتم إنشاؤها عند الرسم.
بدلاً من ذلك، يمكنك أيضًا تخزين العناصر مؤقتًا باستخدام remember خارج المعدِّل. ومع ذلك، ليس ذلك ممكنًا دائمًا لأنّه ليس بإمكانك الوصول دائمًا إلى التركيبة. يمكن أن يكون استخدام drawWithCache أكثر فعالية إذا كانت العناصر تُستخدم للرسم فقط.
على سبيل المثال، إذا أنشأت Brush لرسم تدرّج خلف Text، فإنّ استخدام drawWithCache يخزّن كائن Brush مؤقتًا إلى أن يتغيّر حجم مساحة الرسم:
Text( "Hello Compose!", modifier = Modifier .drawWithCache { val brush = Brush.linearGradient( listOf( Color(0xFF9E82F0), Color(0xFF42A5F5) ) ) onDrawBehind { drawRoundRect( brush, cornerRadius = CornerRadius(10.dp.toPx()) ) } } )
معدِّلات الرسومات
Modifier.graphicsLayer: تطبيق عمليات التحويل على الدوال المركّبة
Modifier.graphicsLayer
هو معدِّل يجعل محتوى الدالة المركّبة يتم رسمه في طبقة رسم. توفّر الطبقة بعض الوظائف المختلفة، مثل:
- عزل تعليمات الرسم (على غرار
RenderNode). يمكن أن يعيد مسار العرض إصدار تعليمات الرسم التي تم التقاطها كجزء من طبقة بكفاءة بدون إعادة تنفيذ الرمز البرمجي للتطبيق. - عمليات التحويل التي تنطبق على جميع تعليمات الرسم الواردة في طبقة.
- التحويل إلى صورة نقطية لإمكانات التركيب. عند تحويل طبقة إلى صورة نقطية، يتم تنفيذ تعليمات الرسم الخاصة بها ويتم التقاط الناتج في مخزن مؤقت خارج الشاشة. يكون تركيب هذا المخزن المؤقت للإطارات اللاحقة أسرع من تنفيذ التعليمات الفردية، ولكنّه سيتصرف كصورة نقطية عند تطبيق عمليات التحويل، مثل تغيير الحجم أو التدوير.
عمليات التحويل
توفّر الدالة Modifier.graphicsLayer عزل تعليمات الرسم، على سبيل المثال، يمكن تطبيق عمليات تحويل مختلفة باستخدام Modifier.graphicsLayer. يمكن تحريك هذه العمليات أو تعديلها بدون الحاجة إلى إعادة تنفيذ lambda للرسم.
لا تغيّر الدالة Modifier.graphicsLayer الحجم أو الموضع المقاسَين للدالة المركّبة، لأنّها تؤثر فقط في مرحلة الرسم. هذا يعني أنّ الدالة المركّبة قد تتداخل مع دوال مركّبة أخرى إذا انتهى بها الأمر إلى الرسم خارج حدود التنسيق.
يمكن تطبيق عمليات التحويل التالية باستخدام هذا المعدِّل:
تغيير الحجم - زيادة الحجم
تعمل الدالتان scaleX وscaleY على تكبير المحتوى أو تصغيره في الاتجاه الأفقي أو الرأسي على التوالي. تشير القيمة 1.0f إلى عدم حدوث تغيير في الحجم، بينما تشير القيمة 0.5f إلى نصف البُعد.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.scaleX = 1.2f this.scaleY = 0.8f } )
الترجمة
يمكن تغيير translationX وtranslationY باستخدام graphicsLayer، حيث تنقل translationX الدالة المركّبة إلى اليسار أو اليمين. وتنقل translationY الدالة المركّبة إلى الأعلى أو الأسفل.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.translationX = 100.dp.toPx() this.translationY = 10.dp.toPx() } )
تدوير
اضبط rotationX للتدوير أفقيًا، وrotationY للتدوير رأسيًا، وrotationZ للتدوير على المحور Z (التدوير العادي). يتم تحديد هذه القيمة بالدرجات (0-360).
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
الأصل
يمكن تحديد transformOrigin. يتم بعد ذلك استخدامه كنقطة تبدأ منها عمليات التحويل. استخدمت جميع الأمثلة حتى الآن TransformOrigin.Center، التي تقع عند (0.5f, 0.5f). إذا حددت الأصل عند (0f, 0f)، تبدأ عمليات التحويل بعد ذلك من أعلى يمين الدالة المركّبة.
إذا غيّرت الأصل باستخدام عملية تحويل rotationZ، يمكنك ملاحظة أنّ العنصر يدور حول أعلى يمين الدالة المركّبة:
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "Sunset", modifier = Modifier .graphicsLayer { this.transformOrigin = TransformOrigin(0f, 0f) this.rotationX = 90f this.rotationY = 275f this.rotationZ = 180f } )
القص والشكل
يحدّد الشكل المخطط الذي يتم قص المحتوى به عندما تكون clip = true. في هذا المثال، نضبط مربّعَين ليحتويا على قصَّتَين مختلفتَين، إحداهما باستخدام متغيّر قص graphicsLayer، والأخرى باستخدام برنامج التضمين المناسب Modifier.clip.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .size(200.dp) .graphicsLayer { clip = true shape = CircleShape } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(CircleShape) .background(Color(0xFF4DB6AC)) ) }
يتم قص محتويات المربّع الأول (النص "Hello Compose") على شكل دائرة:
إذا طبّقت بعد ذلك translationY على الدائرة الوردية العلوية، ستلاحظ أنّ حدود الدالة المركّبة لا تزال هي نفسها، ولكن يتم رسم الدائرة أسفل الدائرة السفلية (وخارج حدودها).
لقص الدالة المركّبة في المنطقة التي يتم رسمها فيها، يمكنك إضافة Modifier.clip(RectangleShape) آخر في بداية سلسلة المعدِّلات. يبقى المحتوى بعد ذلك داخل الحدود الأصلية.
Column(modifier = Modifier.padding(16.dp)) { Box( modifier = Modifier .clip(RectangleShape) .size(200.dp) .border(2.dp, Color.Black) .graphicsLayer { clip = true shape = CircleShape translationY = 50.dp.toPx() } .background(Color(0xFFF06292)) ) { Text( "Hello Compose", style = TextStyle(color = Color.Black, fontSize = 46.sp), modifier = Modifier.align(Alignment.Center) ) } Box( modifier = Modifier .size(200.dp) .clip(RoundedCornerShape(500.dp)) .background(Color(0xFF4DB6AC)) ) }
إصدار أولي
يمكن استخدام Modifier.graphicsLayer لضبط alpha (الشفافية) للطبقة بأكملها. تشير القيمة 1.0f إلى أنّ الطبقة غير شفافة تمامًا، بينما تشير القيمة 0.0f إلى أنّها غير مرئية.
Image( painter = painterResource(id = R.drawable.sunset), contentDescription = "clock", modifier = Modifier .graphicsLayer { this.alpha = 0.5f } )
استراتيجية التركيب
لا يكون التعامل مع alpha والشفافية دائمًا بسيطًا مثل تغيير قيمة alpha واحدة. بالإضافة إلى تغيير alpha، يتوفّر
أيضًا خيار ضبط CompositingStrategy على graphicsLayer. تحدّد CompositingStrategy كيفية تركيب محتوى الدالة المركّبة (تجميعه) مع المحتوى الآخر الذي تم رسمه على الشاشة.
الاستراتيجيات المختلفة هي:
تلقائي (الإعداد التلقائي)
يتم تحديد استراتيجية التركيب من خلال بقية graphicsLayer
المعلمات. تعرض هذه الاستراتيجية الطبقة في مخزن مؤقت خارج الشاشة إذا كانت قيمة alpha أقل من 1.0f أو تم ضبط RenderEffect. عندما تكون قيمة alpha أقل من 1f، يتم إنشاء طبقة تركيب تلقائيًا لعرض المحتويات ثم رسم هذا المخزن المؤقت خارج الشاشة في الوجهة باستخدام قيمة alpha المقابلة. يؤدي ضبط a
RenderEffect أو تجاوز حد التمرير دائمًا إلى عرض المحتوى في مخزن مؤقت خارج الشاشة
بغض النظر عن CompositingStrategy الذي تم ضبطه.
خارج الشاشة
يتم دائمًا تحويل محتويات الدالة المركّبة إلى صورة نقطية أو نسيج خارج الشاشة قبل العرض في الوجهة. يكون ذلك مفيدًا لـ
تطبيق BlendMode عمليات لإخفاء المحتوى، ولتحسين الأداء عند
عرض مجموعات معقدة من تعليمات الرسم.
أحد الأمثلة على استخدام CompositingStrategy.Offscreen هو BlendModes.
في المثال التالي، لنفترض أنّك تريد إزالة أجزاء من دالة مركّبة من نوع Image عن طريق إصدار أمر رسم يستخدم BlendMode.Clear. إذا لم تضبط compositingStrategy على CompositingStrategy.Offscreen، يتفاعل BlendMode مع كل محتوى الخلفية.
Image( painter = painterResource(id = R.drawable.dog), contentDescription = "Dog", contentScale = ContentScale.Crop, modifier = Modifier .size(120.dp) .aspectRatio(1f) .background( Brush.linearGradient( listOf( Color(0xFFC5E1A5), Color(0xFF80DEEA) ) ) ) .padding(8.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.Offscreen } .drawWithCache { val path = Path() path.addOval( Rect( topLeft = Offset.Zero, bottomRight = Offset(size.width, size.height) ) ) onDrawWithContent { clipPath(path) { // this draws the actual image - if you don't call drawContent, it wont // render anything this@onDrawWithContent.drawContent() } val dotSize = size.width / 8f // Clip a white border for the content drawCircle( Color.Black, radius = dotSize, center = Offset( x = size.width - dotSize, y = size.height - dotSize ), blendMode = BlendMode.Clear ) // draw the red circle indication drawCircle( Color(0xFFEF5350), radius = dotSize * 0.8f, center = Offset( x = size.width - dotSize, y = size.height - dotSize ) ) } } )
من خلال ضبط CompositingStrategy على Offscreen، يتم إنشاء نسيج خارج الشاشة لتنفيذ الأوامر (تطبيق BlendMode على محتويات هذه الدالة المركّبة فقط). يتم بعد ذلك عرضه فوق ما تم عرضه على الشاشة، بدون التأثير في المحتوى الذي تم رسمه من قبل.
إذا لم تستخدم CompositingStrategy.Offscreen، فإنّ نتائج تطبيق BlendMode.Clear تمحو جميع وحدات البكسل في الوجهة، بغض النظر عن ما تم ضبطه من قبل، ما يؤدي إلى ظهور مخزن العرض المؤقت للنافذة (باللون الأسود). لن تعمل العديد من BlendModes التي تتضمّن alpha على النحو المتوقّع بدون مخزن مؤقت خارج الشاشة. لاحظ الحلقة السوداء حول المؤشر الدائري الأحمر:
لفهم ذلك بشكلٍ أكبر، إذا كان للتطبيق خلفية نافذة شفافة ولم تستخدم CompositingStrategy.Offscreen، سيتفاعل BlendMode مع التطبيق بأكمله. سيزيل كل وحدات البكسل لعرض التطبيق أو الخلفية أدناه، كما في هذا المثال:
من الجدير بالذكر أنّه عند استخدام CompositingStrategy.Offscreen، يتم إنشاء نسيج خارج الشاشة بحجم مساحة الرسم وعرضه مرة أخرى على الشاشة. يتم قص أي أوامر رسم يتم تنفيذها باستخدام هذه الاستراتيجية في هذه المنطقة بشكلٍ تلقائي. يوضّح مقتطف الرمز التالي الاختلافات عند التبديل إلى استخدام نسيج خارج الشاشة:
@Composable fun CompositingStrategyExamples() { Column( modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) { // Does not clip content even with a graphics layer usage here. By default, graphicsLayer // does not allocate + rasterize content into a separate layer but instead is used // for isolation. That is draw invalidations made outside of this graphicsLayer will not // re-record the drawing instructions in this composable as they have not changed Canvas( modifier = Modifier .graphicsLayer() .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { // ... and drawing a size of 200 dp here outside the bounds drawRect(color = Color.Magenta, size = Size(200.dp.toPx(), 200.dp.toPx())) } Spacer(modifier = Modifier.size(300.dp)) /* Clips content as alpha usage here creates an offscreen buffer to rasterize content into first then draws to the original destination */ Canvas( modifier = Modifier // force to an offscreen buffer .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) .size(100.dp) // Note size of 100 dp here .border(2.dp, color = Color.Blue) ) { /* ... and drawing a size of 200 dp. However, because of the CompositingStrategy.Offscreen usage above, the content gets clipped */ drawRect(color = Color.Red, size = Size(200.dp.toPx(), 200.dp.toPx())) } } }
ModulateAlpha
تعدّل استراتيجية التركيب هذه قيمة alpha لكل تعليمات الرسم
المسجّلة ضمن graphicsLayer. لن تنشئ هذه الاستراتيجية مخزنًا مؤقتًا خارج الشاشة لقيمة alpha أقل من 1.0f ما لم يتم ضبط RenderEffect، لذا يمكن أن تكون أكثر فعالية لعرض alpha. ومع ذلك، يمكن أن توفّر نتائج مختلفة للمحتوى المتداخل. بالنسبة إلى حالات الاستخدام التي يُعرف فيها مسبقًا أنّ المحتوى غير متداخل، يمكن أن توفّر هذه الاستراتيجية أداءً أفضل من CompositingStrategy.Auto مع قيم alpha أقل من 1.
يوضّح المثال التالي استراتيجيات تركيب مختلفة، حيث يتم تطبيق قيم alpha مختلفة على أجزاء مختلفة من الدوال المركّبة، وتطبيق استراتيجية Modulate:
@Preview @Composable fun CompositingStrategy_ModulateAlpha() { Column( modifier = Modifier .fillMaxSize() .padding(32.dp) ) { // Base drawing, no alpha applied Canvas( modifier = Modifier.size(200.dp) ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // Alpha 0.5f applied to whole composable Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { alpha = 0.5f } ) { drawSquares() } Spacer(modifier = Modifier.size(36.dp)) // 0.75f alpha applied to each draw call when using ModulateAlpha Canvas( modifier = Modifier .size(200.dp) .graphicsLayer { compositingStrategy = CompositingStrategy.ModulateAlpha alpha = 0.75f } ) { drawSquares() } } } private fun DrawScope.drawSquares() { val size = Size(100.dp.toPx(), 100.dp.toPx()) drawRect(color = Red, size = size) drawRect( color = Purple, size = size, topLeft = Offset(size.width / 4f, size.height / 4f) ) drawRect( color = Yellow, size = size, topLeft = Offset(size.width / 4f * 2f, size.height / 4f * 2f) ) } val Purple = Color(0xFF7E57C2) val Yellow = Color(0xFFFFCA28) val Red = Color(0xFFEF5350)
كتابة محتويات دالة مركّبة في صورة نقطية
rememberGraphicsLayer()
إحدى حالات الاستخدام الشائعة هي إنشاء Bitmap من دالة مركّبة. لنسخ الـ
محتويات الدالة المركّبة إلى Bitmap، أنشئ GraphicsLayer باستخدام
rememberGraphicsLayer().
أعِد توجيه أوامر الرسم إلى الطبقة الجديدة باستخدام drawWithContent() وgraphicsLayer.record{}. ثم ارسم الطبقة في لوحة العرض المرئية باستخدام drawLayer:
val coroutineScope = rememberCoroutineScope() val graphicsLayer = rememberGraphicsLayer() Box( modifier = Modifier .drawWithContent { // call record to capture the content in the graphics layer graphicsLayer.record { // draw the contents of the composable into the graphics layer this@drawWithContent.drawContent() } // draw the graphics layer on the visible canvas drawLayer(graphicsLayer) } .clickable { coroutineScope.launch { val bitmap = graphicsLayer.toImageBitmap() // do something with the newly acquired bitmap } } .background(Color.White) ) { Text("Hello Android", fontSize = 26.sp) }
يمكنك حفظ الصورة النقطية على القرص ومشاركتها. لمزيد من التفاصيل، اطّلِع على مقتطف المثال الكامل. احرص على التحقّق من الأذونات على الجهاز قبل محاولة الحفظ على القرص.
معدِّل رسم مخصّص
لإنشاء معدِّل مخصّص، نفِّذ واجهة DrawModifier. يمنحك ذلك إمكانية الوصول إلى ContentDrawScope، وهو نفسه ما يتم عرضه عند استخدام Modifier.drawWithContent(). يمكنك بعد ذلك استخراج عمليات الرسم الشائعة إلى معدِّلات رسم مخصّصة لتنظيف الرمز وتوفير برامج تضمين مناسبة، على سبيل المثال، Modifier.background() هو DrawModifier مناسب.
على سبيل المثال، إذا أردت تنفيذ Modifier يعكس المحتوى رأسيًا، يمكنك إنشاء واحد على النحو التالي:
class FlippedModifier : DrawModifier { override fun ContentDrawScope.draw() { scale(1f, -1f) { this@draw.drawContent() } } } fun Modifier.flipped() = this.then(FlippedModifier())
ثم استخدِم هذا المعدِّل المعكوس الذي تم تطبيقه على Text:
Text( "Hello Compose!", modifier = Modifier .flipped() )
مراجع إضافية
لمزيد من الأمثلة على استخدام graphicsLayer والرسم المخصّص، اطّلِع على المراجع التالية:
مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عند إيقاف JavaScript
- الرسومات في Compose
- تخصيص صورة {:#customize-image}
- Kotlin لـ Jetpack Compose