بالإضافة إلى العنصر القابل للإنشاء 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 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