بالإضافة إلى 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 } )
استراتيجية التركيب
قد لا يكون العمل على ألفا والشفافية بسيطًا مثل تغيير قيمة
ألفا واحدة. بالإضافة إلى تغيير ألفا، هناك أيضًا خيار ضبط 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