بالإضافة إلى عنصر Canvas
القابل للإنشاء، يحتوي Compose على العديد من الرسومات المفيدة
Modifiers
التي تساعد في رسم محتوى مخصّص. هذه المُعدّلات مفيدة لأنه يمكن
تطبيقها على أي عنصر قابل للإنشاء.
معدِّلات الرسم
يتم تنفيذ جميع أوامر الرسم باستخدام معدِّل الرسم في Compose. هناك ثلاثة تعديلات رئيسية للرسم في Compose:
مفتاح التعديل الأساسي للرسم هو drawWithContent
، حيث يمكنك تحديد ترتيب الرسم في "Composable" وأوامر الرسم الصادرة داخل "التعديل". إنّ 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)) ) }
يتم اقتصاص محتويات المربع الأول (النص الذي يقول "مرحبًا Compose") إلى شكل الدائرة:
إذا قمت بعد ذلك بتطبيق translationY
على الدائرة الوردية العلوية، سترى أن حدود
"Composable" لا تزال كما هي، ولكن الدائرة يتم رسمها أسفل
الدائرة السفلية (وخارجها).
لاقتصاص العنصر القابل للإنشاء حسب المنطقة التي تم رسمه فيها، يمكنك إضافة
عنصر 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
أو التمرير الزائد إلى عرض المحتوى دائمًا في مخزن مؤقت
خارج الشاشة بغض النظر عن مجموعة 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 CompositingStratgey_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
- الرسومات في Compose
- تخصيص صورة {:#customize-image}
- Kotlin في Jetpack Compose