الرسومات في Compose

يحتاج العديد من التطبيقات إلى أن تكون قادرًا على التحكم بدقة في ما يتم رسمه على الشاشة. قد يكون هذا صغيرًا مثل وضع مربع أو دائرة على الشاشة في المكان المناسب فقط، أو قد يكون ترتيبًا متقنًا من العناصر الرسومية في العديد من الأنماط المختلفة.

رسم أساسي مع مفاتيح التعديل وDrawScope

إنّ الطريقة الأساسية لرسم عنصر مخصّص في Compose هي باستخدام مفاتيح التعديل، مثل Modifier.drawWithContent وModifier.drawBehind وModifier.drawWithCache.

على سبيل المثال، لرسم عنصر خلف العنصر القابل للإنشاء، يمكنك استخدام أداة التعديل drawBehind لبدء تنفيذ أوامر الرسم:

Spacer(
    modifier = Modifier
        .fillMaxSize()
        .drawBehind {
            // this = DrawScope
        }
)

إذا كان كل ما تحتاجه هو عنصر قابل للإنشاء وجذاب، يمكنك استخدام Canvas القابل للإنشاء. عنصر Canvas القابل للإنشاء هو برنامج ربط مريح حول Modifier.drawBehind. يمكنك وضع Canvas في التنسيق بالطريقة نفسها التي تضعها مع أي عنصر آخر في واجهة Compose. وضمن Canvas، يمكنك رسم عناصر مع إمكانية التحكّم الدقيق في نمطها وموقعها.

تعرض جميع معدِّلات الرسم DrawScope، وهي بيئة رسم مُفصَّل تحافظ على حالتها الخاصة. يتيح لك هذا تعيين المعاملات لمجموعة من العناصر الرسومية. توفّر DrawScope عدة حقول مفيدة، مثل size، وهي عنصر Size يحدّد الأبعاد الحالية للسمة DrawScope.

لرسم شيء، يمكنك استخدام إحدى دوال الرسم العديدة في DrawScope. على سبيل المثال، ترسم التعليمة البرمجية التالية مستطيلاً في الزاوية العلوية اليسرى من الشاشة:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    drawRect(
        color = Color.Magenta,
        size = canvasQuadrantSize
    )
}

مستطيل وردي مرسوم على خلفية بيضاء يشغل ربع الشاشة
الشكل 1. مستطيل مرسوم باستخدام "لوحة الرسم" في Compose

لمعرفة المزيد من المعلومات حول معدِّلات الرسم المختلفة، يمكنك الاطّلاع على مستندات معدِّلات الرسومات.

نظام الإحداثيات

لرسم عنصر على الشاشة، عليك معرفة الإزاحة (x وy) وحجم العنصر. في العديد من طرق الرسم على DrawScope، يتم توفير الموضع والحجم من خلال قيم المعلَمات التلقائية. تضع المعلمات التلقائية بشكل عام العنصر عند النقطة [0, 0] على لوحة الرسم وتوفر علامة size تلقائية تملأ مساحة الرسم بأكملها، كما في المثال أعلاه. يظهر المستطيل في أعلى يسار الصفحة. لضبط حجم العنصر وموضعه، تحتاج إلى فهم نظام الإحداثيات في Compose.

يقع أصل نظام الإحداثيات ([0,0]) في أعلى وحدات البكسل في أقصى اليسار في منطقة الرسم. تزيد قيمة x كلما تحركت إلى اليمين وتزداد قيمة y كلما تحركت لأسفل.

شبكة تعرض نظام الإحداثيات ويظهر أعلى اليسار [0، 0] وأسفل اليمين [العرض، الارتفاع]
الشكل 2. نظام إحداثيات الرسم / شبكة الرسم

على سبيل المثال، إذا أردت رسم خط قُطري من أعلى يسار مساحة لوحة الرسم إلى أسفل يسار الشاشة، يمكنك استخدام الدالة DrawScope.drawLine() وتحديد إزاحة البداية والنهاية باستخدام موضعَي x وy المقابلتَين:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasWidth = size.width
    val canvasHeight = size.height
    drawLine(
        start = Offset(x = canvasWidth, y = 0f),
        end = Offset(x = 0f, y = canvasHeight),
        color = Color.Blue
    )
}

التحويلات الأساسية

تقدّم DrawScope عمليات تحويل لتغيير مكان أو كيفية تنفيذ أوامر الرسم.

المقياس

استخدِم DrawScope.scale() لزيادة حجم عمليات الرسم بمقدار عامل. تنطبق عمليات مثل scale() على جميع عمليات الرسم داخل دالة lambda المقابلة. على سبيل المثال، يزيد الرمز التالي من scaleX 10 مرات وscaleY 15 مرة:

Canvas(modifier = Modifier.fillMaxSize()) {
    scale(scaleX = 10f, scaleY = 15f) {
        drawCircle(Color.Blue, radius = 20.dp.toPx())
    }
}

دائرة تم قياسها بشكل غير منتظم
الشكل 3. تطبيق عملية مقياس على دائرة على لوحة الرسم.

ترجمة

استخدِم رمز DrawScope.translate() لتحريك عمليات الرسم للأعلى أو للأسفل أو لليسار أو لليمين. على سبيل المثال، ينقل الرمز التالي الرسم 100 بكسل إلى اليمين و300 بكسل لأعلى:

Canvas(modifier = Modifier.fillMaxSize()) {
    translate(left = 100f, top = -300f) {
        drawCircle(Color.Blue, radius = 200.dp.toPx())
    }
}

تم نقل دائرة خارج المركز
الشكل 4. تطبيق عملية ترجمة على دائرة في لوحة الرسم.

تدوير

استخدِم DrawScope.rotate() لتدوير عمليات الرسم حول نقطة محورية. على سبيل المثال، تقوم التعليمة البرمجية التالية بتدوير مستطيل 45 درجة:

Canvas(modifier = Modifier.fillMaxSize()) {
    rotate(degrees = 45F) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

هاتف على شكل مستطيل تم تدويره بزاوية 45 درجة في وسط الشاشة
الشكل 5. نستخدم rotate() لتطبيق التدوير على نطاق الرسم الحالي، الذي يعمل على تدوير المستطيل بمقدار 45 درجة.

مساحة داخلية

يمكنك استخدام DrawScope.inset() لضبط المعلَمات التلقائية للمعلَمات الحالية DrawScope وتغيير حدود الرسم وترجمة الرسومات وفقًا لذلك:

Canvas(modifier = Modifier.fillMaxSize()) {
    val canvasQuadrantSize = size / 2F
    inset(horizontal = 50f, vertical = 30f) {
        drawRect(color = Color.Green, size = canvasQuadrantSize)
    }
}

تضيف هذه التعليمة البرمجية مساحة متروكة بشكل فعّال لأوامر الرسم:

مستطيل مبطّن من حوله
الشكل 6. تطبيق مساحة داخلية على أوامر الرسم

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

لتطبيق عمليات تحويل متعددة على رسوماتك، استخدِم الدالة DrawScope.withTransform() التي تنشئ وتطبّق عملية تحويل واحدة تجمع كل التغييرات المطلوبة. يُعد استخدام withTransform() أكثر فعالية من إجراء استدعاءات متداخلة لتحويلات فردية، لأنه يتم تنفيذ جميع التحويلات معًا في عملية واحدة، بدلًا من أن تحتاج ميزة Compose إلى حساب وحفظ كل تحويل من التحويلات المتداخلة.

على سبيل المثال، تطبق التعليمة البرمجية التالية كلاً من الترجمة والتدوير على المستطيل:

Canvas(modifier = Modifier.fillMaxSize()) {
    withTransform({
        translate(left = size.width / 5F)
        rotate(degrees = 45F)
    }) {
        drawRect(
            color = Color.Gray,
            topLeft = Offset(x = size.width / 3F, y = size.height / 3F),
            size = size / 3F
        )
    }
}

هاتف يحتوي على مستطيل مستدير تم نقله إلى جانب الشاشة
الشكل 7. استخدِم withTransform لتطبيق كل من التدوير والترجمة، وتدوير المستطيل وتحويله إلى اليسار.

عمليات الرسم الشائعة

رسم نص

لرسم نص في Compose، يمكنك عادةً استخدام عنصر Text القابل للإنشاء. مع ذلك، إذا كنت في DrawScope أو تريد رسم النص يدويًا باستخدام التخصيص، يمكنك استخدام الطريقة DrawScope.drawText().

لرسم نص، أنشئ TextMeasurer باستخدام rememberTextMeasurer واطلب drawText باستخدام أداة القياس:

val textMeasurer = rememberTextMeasurer()

Canvas(modifier = Modifier.fillMaxSize()) {
    drawText(textMeasurer, "Hello")
}

عرض رسالة ترحيب مرسومة على لوحة رسم
الشكل 8. رسم نص على "لوحة الرسم"

قياس النص

يعمل نص الرسم بشكل مختلف قليلاً عن أوامر الرسم الأخرى. عادةً، تعطي أمر الرسم الحجم (العرض والارتفاع) لرسم الشكل/الصورة به. تتوفّر في النص بعض المعلَمات التي تتحكّم في حجم النص المعروض، مثل حجم الخط والخط والأحرف المتصلة والمسافات بين الأحرف.

من خلال ميزة Compose، يمكنك استخدام TextMeasurer للوصول إلى حجم النص الذي تم قياسه، اعتمادًا على العوامل المذكورة أعلاه. إذا كنت ترغب في رسم خلفية خلف النص، يمكنك استخدام المعلومات التي تم قياسها للحصول على حجم المنطقة التي يشغلها النص:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixedWidth((size.width * 2f / 3f).toInt()),
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

ينتج مقتطف الرمز هذا خلفية وردية على النص:

نص متعدد الأسطر يشغل حجم 2⁄3 من المساحة الكاملة، مع مستطيل للخلفية
الشكل 9. نص متعدد الأسطر يشغل حجم 2⁄3 من المساحة الكاملة، مع مستطيل للخلفية.

يؤدي ضبط القيود أو حجم الخط أو أي خاصية تؤثر على الحجم الذي يتم قياسه إلى حجم جديد يتم الإبلاغ عنه. يمكنك ضبط حجم ثابت لكلٍّ من width وheight، ويتّبع النص المجموعة TextOverflow. على سبيل المثال، يعرض الرمز التالي النص 1⁄3 من الارتفاع و1⁄3 عرض المنطقة القابلة للإنشاء، ويضبط TextOverflow على TextOverflow.Ellipsis:

val textMeasurer = rememberTextMeasurer()

Spacer(
    modifier = Modifier
        .drawWithCache {
            val measuredText =
                textMeasurer.measure(
                    AnnotatedString(longTextSample),
                    constraints = Constraints.fixed(
                        width = (size.width / 3f).toInt(),
                        height = (size.height / 3f).toInt()
                    ),
                    overflow = TextOverflow.Ellipsis,
                    style = TextStyle(fontSize = 18.sp)
                )

            onDrawBehind {
                drawRect(pinkColor, size = measuredText.size.toSize())
                drawText(measuredText)
            }
        }
        .fillMaxSize()
)

يتم الآن رسم النص ضمن القيود مع علامة حذف في النهاية:

نص مرسوم على خلفية وردية، مع علامة حذف تقطع النص.
الشكل 10. TextOverflow.Ellipsis مع قيود ثابتة على قياس النص.

رسم صورة

لرسم ImageBitmap باستخدام DrawScope، يمكنك تحميل الصورة باستخدام ImageBitmap.imageResource() ثم الاتصال بـ drawImage:

val dogImage = ImageBitmap.imageResource(id = R.drawable.dog)

Canvas(modifier = Modifier.fillMaxSize(), onDraw = {
    drawImage(dogImage)
})

صورة كلب مرسوم على "لوحة الرسم"
الشكل 11. رسم "ImageBitmap" على لوحة رسم

رسم أشكال أساسية

هناك العديد من دوال رسم الأشكال في DrawScope. لرسم شكل، استخدم إحدى دوال الرسم المحددة مسبقًا، مثل drawCircle:

val purpleColor = Color(0xFFBA68C8)
Canvas(
    modifier = Modifier
        .fillMaxSize()
        .padding(16.dp),
    onDraw = {
        drawCircle(purpleColor)
    }
)

واجهة برمجة التطبيقات

ناتج

drawCircle()

رسم دائرة

drawRect()

رسم مستطيل

drawRoundedRect()

رسم مستطيل مستدير

drawLine()

خط الرسم

drawOval()

رسم شكل بيضاوي

drawArc()

رسم قوس

drawPoints()

تعادُل النقاط

رسم مسار

المسار هو سلسلة من التعليمات الرياضية التي تؤدي إلى رسم بمجرد تنفيذه. يمكن لـ DrawScope رسم مسار باستخدام الطريقة DrawScope.drawPath().

على سبيل المثال، لنفترض أنّك أردت رسم مثلث. ويمكنك إنشاء مسار بدوال مثل lineTo() وmoveTo() باستخدام حجم منطقة الرسم. بعد ذلك، يمكنك طلب drawPath() باستخدام هذا المسار الذي تم إنشاؤه حديثًا للحصول على مثلث.

Spacer(
    modifier = Modifier
        .drawWithCache {
            val path = Path()
            path.moveTo(0f, 0f)
            path.lineTo(size.width / 2f, size.height / 2f)
            path.lineTo(size.width, 0f)
            path.close()
            onDrawBehind {
                drawPath(path, Color.Magenta, style = Stroke(width = 10f))
            }
        }
        .fillMaxSize()
)

مثلث مسار أرجواني مقلوب مرسوم في علامة التبويب Compose
الشكل 12. إنشاء Path ورسمه في Compose

جارٍ الوصول إلى عنصر واحد (Canvas)

من خلال DrawScope، ليس لديك إمكانية الوصول المباشر إلى عنصر Canvas. يمكنك استخدام DrawScope.drawIntoCanvas() للوصول إلى كائن Canvas نفسه الذي يمكنك استدعاء الدوال عليه.

على سبيل المثال، إذا كان لديك عنصر Drawable مخصّص تريد رسمه على اللوحة، يمكنك الوصول إلى اللوحة واستدعاء Drawable#draw()، مع تمرير الكائن Canvas:

val drawable = ShapeDrawable(OvalShape())
Spacer(
    modifier = Modifier
        .drawWithContent {
            drawIntoCanvas { canvas ->
                drawable.setBounds(0, 0, size.width.toInt(), size.height.toInt())
                drawable.draw(canvas.nativeCanvas)
            }
        }
        .fillMaxSize()
)

شكل بيضاوي أسود قابل للرسم بحجم كامل
الشكل 13. جارٍ الوصول إلى لوحة الرسم لرسم Drawable.

مزيد من المعلومات

لمزيد من المعلومات حول "الرسم في Compose"، ألق نظرة على الموارد التالية: