بالإضافة إلى العناصر القابلة للتجميع Canvas
، تتضمّن ميزة "إنشاء" العديد من الرسومات المفيدة
Modifiers
التي تساعد في رسم محتوى مخصّص. تكون هذه المُعدِّلات مفيدة
لأنّه يمكن تطبيقها على أيّ عنصر قابل للتجميع.
عوامل تعديل الرسم
يتم تنفيذ جميع أوامر الرسم باستخدام مُعدِّل للرسم في ميزة "الإنشاء". هناك ثلاثة عناصر رئيسية لتعديل الرسم في ميزة "الإنشاء":
المُعدِّل الأساسي للرسم هو 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
.
ويمكن تحريكها أو تعديلها بدون الحاجة إلى إعادة تنفيذ دالة رسم الأشكال
لامدا.
لا يغيّر 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
على الدائرة الوردية في أعلى الشاشة، ستلاحظ أنّ حدود
العنصر القابل للتجميع لا تزال كما هي، ولكن يتم رسم الدائرة أسفل الدائرة
في أسفل الشاشة (وخارج حدودها).
لقص العنصر القابل للتجميع على المنطقة التي تم رسمه فيها، يمكنك إضافة رمز
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
.
خارج الشاشة
يتم دائمًا تحويل محتوى العنصر القابل للتجميع إلى ملف bitmap أو ملف ملف bmp مخصّص للعرض خارج الشاشة قبل عرضه في الوجهة. يكون ذلك مفيدًا عند
تطبيق عمليات 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
، يتم إنشاء ملف shader
خارج الشاشة لتنفيذ الأوامر (مع تطبيق BlendMode
على ملف shader
المرتبط بمحتوى هذا المكوّن فقط). وبعد ذلك، يتم عرضها فوق ما تم عرضَه
سابقًا على الشاشة، بدون التأثير في المحتوى الذي سبق أن تم رسمه.
إذا لم تستخدم 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.
في ما يلي مثال آخر على استراتيجيات التركيب المختلفة، وهو تطبيق ترانزِيبات dithered مختلفة على أجزاء مختلفة من العناصر القابلة للتجميع، وتطبيق استراتيجية 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
يقلب المحتوى عموديًا، يمكنك إنشاء 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