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

بالإضافة إلى الدالة المركّبة 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). يمكن إعادة إصدار تعليمات الرسم التي تم التقاطها كجزء من طبقة بكفاءة من خلال مسار العرض بدون إعادة تنفيذ الرمز البرمجي للتطبيق.
  • التحويلات التي تنطبق على جميع تعليمات الرسم الواردة في طبقة.
  • تنقيط إمكانيات التركيب عند تحويل طبقة إلى صورة نقطية، يتم تنفيذ تعليمات الرسم الخاصة بها ويتم تسجيل الناتج في مخزن مؤقت خارج الشاشة. Compositing such a buffer for subsequent frames is faster than executing the individual instructions, but it will behave as a bitmap when transforms like scaling or rotation are applied.

عمليات التحويل

توفّر 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") ليناسب شكل الدائرة:

تم تطبيق المقطع على الدالة البرمجية القابلة للإنشاء Box
الشكل 8: تطبيق Clip على عنصر Box القابل للإنشاء

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

تم تطبيق المقطع مع translationY، وحدود حمراء للمخطط التفصيلي
الشكل 9: مقطع تم تطبيق الترجمةY عليه، وحد أحمر للمخطط التفصيلي

لاقتصاص الدالة المركّبة إلى المنطقة التي يتم رسمها فيها، يمكنك إضافة 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: تطبيق المقطع في أعلى عملية تحويل 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، يتم إنشاء مادة خارج الشاشة بحجم مساحة الرسم وعرضها مرة أخرى على الشاشة. Any drawing commands that are done with this strategy, are by default be clipped to this region. يوضّح مقتطف الرمز التالي الاختلافات عند التبديل إلى استخدام زخارف خارج الشاشة:

@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: يتم قص المقاطع خارج الشاشة إلى المنطقة، بينما لا يتم ذلك في الوضع التلقائي.
الشكل 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 والرسم المخصّص، يمكنك مراجعة المراجع التالية: