بالإضافة إلى Canvas
ComposableModifiers
، يتضمّن Compose العديد من الرسومات المفيدة التي تساعد في رسم محتوى مخصّص. تكون أدوات التعديل هذه مفيدة
لأنّه يمكن تطبيقها على أي عنصر قابل للإنشاء.
تعديل الرسم
يتم تنفيذ جميع أوامر الرسم باستخدام أداة تعديل الرسم في 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 } )

استراتيجية التركيب
قد لا يكون التعامل مع ألفا والشفافية بسيطًا مثل تغيير قيمة ألفا واحدة. بالإضافة إلى تغيير قيمة قناة ألفا، يتوفّر أيضًا خيار ضبط
CompositingStrategy
على graphicsLayer
. تحدّد CompositingStrategy
طريقة دمج محتوى العنصر القابل للإنشاء مع المحتوى الآخر الذي تم رسمه على الشاشة.
الاستراتيجيات المختلفة هي:
تلقائي (الإعداد التلقائي)
يتم تحديد استراتيجية التركيب من خلال بقية معلَمات graphicsLayer
. تعرض هذه السمة الطبقة في مخزن مؤقت خارج الشاشة إذا كانت قيمة ألفا أقل من 1.0f أو تم ضبط RenderEffect
. عندما تكون قيمة قناة ألفا أقل من 1f، يتم إنشاء طبقة تركيب تلقائيًا لعرض المحتوى ثم رسم المخزن المؤقت خارج الشاشة هذا إلى الوجهة باستخدام قناة ألفا المقابلة. يؤدي ضبط RenderEffect
أو overscroll دائمًا إلى عرض المحتوى في مخزن مؤقت خارج الشاشة بغض النظر عن قيمة 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
التي تتضمّن قناة ألفا على النحو المتوقّع بدون
مخزن مؤقت خارج الشاشة. لاحظ الحلقة السوداء حول مؤشر الدائرة الحمراء:

لتوضيح ذلك أكثر، إذا كان التطبيق يتضمّن خلفية نافذة شفافة ولم تستخدِم 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
تعدّل استراتيجية التركيب هذه قيمة قناة ألفا لكل تعليمات الرسم المسجّلة ضمن graphicsLayer
. لن يتم إنشاء مخزن مؤقت خارج الشاشة لقيمة ألفا أقل من 1.0f ما لم يتم ضبط RenderEffect
، لذا يمكن أن يكون ذلك أكثر فعالية في عرض ألفا. ومع ذلك، يمكن أن يقدّم نتائج مختلفة للمحتوى المتداخل. في حالات الاستخدام التي يُعرف فيها مسبقًا أنّ المحتوى لا يتداخل، يمكن أن يوفّر ذلك أداءً أفضل من CompositingStrategy.Auto
مع قيم ألفا أقل من 1.
في ما يلي مثال آخر على استراتيجيات التركيب المختلفة، وهو تطبيق قيم ألفا مختلفة على أجزاء مختلفة من العناصر القابلة للإنشاء، وتطبيق استراتيجية 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)

كتابة محتوى عنصر قابل للإنشاء إلى صورة نقطية
تتمثّل إحدى حالات الاستخدام الشائعة في إنشاء 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 غير مفعّلة
- الرسومات في "إنشاء"
- تخصيص صورة {:#customize-image}
- Kotlin لـ Jetpack Compose