أدوات تعديل الرسومات

بالإضافة إلى الدالة المركّبة 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
}

الشكل 1: Modifier.drawWithContent يُستخدَم فوق دالة مركّبة لإنشاء تجربة واجهة مستخدم من نوع ضوء الفلاش.

‫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.drawBehind
الشكل 2: نص وخلفية مرسومان باستخدام Modifier.drawBehind

‫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())
                )
            }
        }
)

تخزين عنصر Brush مؤقتًا باستخدام drawWithCache
الشكل 3: تخزين عنصر Brush مؤقتًا باستخدام drawWithCache

معدِّلات الرسومات

‫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
        }
)

الشكل 4: تطبيق scaleX وscaleY على دالة Image مركّبة
الترجمة

يمكن تغيير 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()
        }
)

الشكل 5: تم تطبيق translationX وtranslationY على الصورة باستخدام Modifier.graphicsLayer
تدوير

اضبط 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
        }
)

الشكل 6: تم ضبط rotationX وrotationY وrotationZ على الصورة باستخدام Modifier.graphicsLayer
الأصل

يمكن تحديد 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
        }
)

الشكل 7: تطبيق التدوير مع ضبط TransformOrigin على 0f, 0f

القص والشكل

يحدّد الشكل المخطط الذي يتم اقتصاص المحتوى إليه عند استخدام 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") إلى الشكل الدائري:

تم تطبيق دالة Clip على دالة Box المركّبة
الشكل 8: تم تطبيق Clip على الدالة المركّبة Box

إذا طبّقت بعد ذلك translationY على الدائرة الوردية العلوية، ستلاحظ أنّ حدود الدالة المركّبة لا تزال كما هي، ولكن يتم رسم الدائرة أسفل الدائرة السفلية (وخارج حدودها).

تم تطبيق Clip مع translationY، وحدود حمراء للمخطط
الشكل 9: تم تطبيق القطع باستخدام 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))
    )
}

تم تطبيق الاقتصاص فوق تحويل graphicsLayer
الشكل 10: تم تطبيق Clip على تحويل graphicsLayer

إصدار أولي

يمكن استخدام Modifier.graphicsLayer لضبط alpha (التعتيم) للطبقة بأكملها. ‫1.0f معتم تمامًا و0.0f غير مرئي.

Image(
    painter = painterResource(id = R.drawable.sunset),
    contentDescription = "clock",
    modifier = Modifier
        .graphicsLayer {
            this.alpha = 0.5f
        }
)

صورة تم تطبيق قناة ألفا عليها
الشكل 11: صورة تم تطبيق قناة ألفا عليها

استراتيجية التركيب

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

‫Modifier.drawWithContent على صورة تعرض مؤشرًا دائريًا، مع BlendMode.Clear داخل التطبيق
الشكل 12: Modifier.drawWithContent على صورة تعرض مؤشرًا دائريًا، مع BlendMode.Clear وCompositingStrategy.Offscreen داخل التطبيق

إذا لم تستخدم CompositingStrategy.Offscreen، فإنّ نتائج تطبيق BlendMode.Clear ستؤدي إلى محو جميع وحدات البكسل في الوجهة، بغض النظر عمّا تم ضبطه مسبقًا، ما يؤدي إلى ظهور مخزن العرض المؤقت للنافذة (باللون الأسود). العديد من BlendModes التي تتضمن ألفا لن تعمل كما هو متوقع بدون مخزن مؤقت خارج الشاشة. لاحظ الحلقة السوداء حول مؤشر الدائرة الحمراء:

‫Modifier.drawWithContent على صورة تعرض مؤشرًا دائريًا، مع ضبط BlendMode.Clear وعدم ضبط CompositingStrategy
الشكل 13: Modifier.drawWithContent على صورة تعرض مؤشرًا دائريًا، مع BlendMode.Clear وبدون ضبط CompositingStrategy

لفهم ذلك بشكل أفضل: إذا كان التطبيق يحتوي على خلفية نافذة شفافة، ولم تستخدم CompositingStrategy.Offscreen، سيتفاعل BlendMode مع التطبيق بأكمله. سيؤدي ذلك إلى محو جميع وحدات البكسل لإظهار التطبيق أو الخلفية تحته، كما في هذا المثال:

عدم ضبط CompositingStrategy واستخدام BlendMode.Clear مع تطبيق يحتوي على خلفية نافذة شفافة تظهر الخلفية الوردية من خلال المنطقة المحيطة بدائرة الحالة الحمراء.
الشكل 14: لم يتم ضبط CompositingStrategy ويتم استخدام BlendMode.Clear مع تطبيق يحتوي على خلفية نافذة شفافة. لاحظ كيف تظهر الخلفية الوردية من خلال المنطقة المحيطة بدائرة الحالة الحمراء.

تجدر الإشارة إلى أنّه عند استخدام 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()))
        }
    }
}

‫CompositingStrategy.Auto مقابل CompositingStrategy.Offscreen: يقتصّ Offscreen المحتوى خارج المنطقة، بينما لا يفعل Auto ذلك
الشكل 15: CompositingStrategy.Auto مقابل CompositingStrategy.Offscreen - يقتصّ Offscreen المحتوى خارج المنطقة، بينما لا يفعل Auto ذلك
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)
    )
}

@Preview
@Composable
fun ModifierGraphicsLayerOutsets() {
    Column(
        modifier = Modifier
            .background(Color(0xFF0A0E14))
            .padding(48.dp),
        verticalArrangement = Arrangement.spacedBy(48.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        val pillShape = RoundedCornerShape(percent = 50)

        // Without outsets: alpha = 0.5f creates an offscreen buffer that clips the 24.dp glow
        Box(
            modifier = Modifier
                .size(
                    width = 140.dp,
                    height = 56.dp
                )
                .graphicsLayer(
                    alpha = 0.5f,
                    clip = false
                )
                .dropShadow(
                    shape = pillShape,
                    shadow = Shadow(radius = 24.dp, color = Color.Cyan)
                )
                .background(color = Color(0xFF141824), shape = pillShape),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "✨ Glow",
                color = Color.White,
                fontSize = 18.sp
            )
        }

        Box(
            modifier = Modifier
                .size(
                    width = 140.dp,
                    height = 56.dp
                )
                .graphicsLayer(
                    alpha = 0.5f,
                    outsets = LayerOutsets(24.dp),
                    clip = false
                )
                .dropShadow(
                    shape = pillShape,
                    shadow = Shadow(radius = 24.dp, color = Color.Cyan)
                )
                .background(color = Color(0xFF141824), shape = pillShape),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "✨ Glow",
                color = Color.White,
                fontSize = 18.sp
            )
        }
    }
}


val Purple = Color(0xFF7E57C2)
val Yellow = Color(0xFFFFCA28)
val Red = Color(0xFFEF5350)

تطبّق ModulateAlpha مجموعة ألفا على كل أمر رسم فردي
الشكل 16: يطبّق ModulateAlpha مجموعة ألفا على كل أمر رسم فردي

الطبقات الخارجية

عند رسم دالة مركّبة في مخزن مؤقت خارج الشاشة (على سبيل المثال، عندما تكون قيمة alpha أقل من 1.0f، أو عند ضبط RenderEffect، أو عند استخدام CompositingStrategy.Offscreen)، يتم ضبط مادة العرض خارج الشاشة تلقائيًا على حدود التنسيق المقاسة للدالة المركّبة. يتم تلقائيًا اقتصاص أي رسم أو ظل داخل الطبقة يمتد خارج حدود تنسيق الدالة المركّبة (مثل توهج خارجي مرسوم باستخدام Modifier.dropShadow) عند حدود المخزن المؤقت خارج الشاشة.

بدلاً من إضافة Modifier.padding() (الذي يغيّر قياس التنسيق لعناصر واجهة المستخدم المجاورة)، يمكنك تمرير LayerOutsets إلى المَعلمة outsets في Modifier.graphicsLayer. يؤدي ذلك إلى توسيع الحدود المرئية للمخزن المؤقت خارج الشاشة على كل جانب بدون تغيير حجم التنسيق الذي تم قياسه للدالة المركّبة. يُرجى العِلم أنّ LayerOutsets لا يوسّع شكل الاقتصاص أو حدود الظل للطبقة، والتي تظل مستندة إلى الحجم الأصلي الذي تم قياسه للدالة المركّبة (لذا يجب إبقاء clip = false على graphicsLayer لتجنُّب الاقتصاص إلى الحدود الأصلية):

Box(
    modifier = Modifier
        .size(
            width = 140.dp,
            height = 56.dp
        )
        .graphicsLayer(
            alpha = 0.5f,
            outsets = LayerOutsets(24.dp),
            clip = false
        )
        .dropShadow(
            shape = pillShape,
            shadow = Shadow(radius = 24.dp, color = Color.Cyan)
        )
        .background(color = Color(0xFF141824), shape = pillShape),
    contentAlignment = Alignment.Center
) {
    Text(
        text = "✨ Glow",
        color = Color.White,
        fontSize = 18.sp
    )
}

مقارنة بين توهج خارجي لطبقة خارج الشاشة بدون LayerOutsets (في الأعلى، تم اقتصاصه إلى حدود التنسيق المستطيل) مقابل LayerOutsets(24.dp) (في الأسفل، بدون اقتصاص)
الشكل 17: توهّج خارجي لطبقة خارج الشاشة بدون LayerOutsets (في الأعلى، تم اقتصاصه عند حدود التنسيق المستطيل للدالة المركّبة) مقارنةً باستخدام LayerOutsets(24.dp) (في الأسفل، بدون اقتصاص)

كتابة محتوى دالة مركّبة في صورة نقطية

من حالات الاستخدام الشائعة إنشاء 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()
)

معدِّل مخصّص مقلوب على النص
الشكل 17: معدِّل مخصّص لقلب النص

مراجع إضافية

للاطّلاع على المزيد من الأمثلة التي تستخدم graphicsLayer والرسم المخصّص، يُرجى مراجعة المراجع التالية: