الرسومات في 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 مستطيل مرسوم باستخدام Canvas في 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 تطبيق عملية تغيير الحجم على دائرة في Canvas

ترجمة

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

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

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

تدوير

استخدِم 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")
}

يعرض رسم "مرحبًا"" على‫ Canvas
الشكل 8 رسم نص على Canvas

قياس النص

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

باستخدام 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()
)

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

نص متعدّد الأسطر يشغل ثلثَي حجم المساحة الكاملة، مع مستطيل في الخلفية
الشكل 9 نص متعدّد الأسطر يشغل ثلثَي المساحة الكاملة، مع مستطيل في الخلفية

يؤدي تعديل القيود أو حجم الخط أو أي سمة تؤثر في الحجم المقاس إلى عرض حجم جديد. يمكنك ضبط حجم ثابت لكل من width وheight، ثم يتبع النص TextOverflow الذي تم ضبطه. على سبيل المثال، يعرض الرمز التالي النص في ثلث الارتفاع وثلث العرض للمساحة المركّبة، ويضبط 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)
})

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

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

تتوفّر العديد من دوال رسم الأشكال على 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()
)

عنصر‫ ShapeDrawable أسود بيضاوي يشغل الحجم الكامل
الشكل 13 جارٍ الوصول إلى لوحة الرسم لرسم Drawable.

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

لمزيد من المعلومات حول الرسم في Compose، يمكنك الاطّلاع على المراجع التالية: