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