إنشاء رسم مخصّص

تجربة طريقة "الكتابة"
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدم المقترَحة لنظام التشغيل Android. تعرَّف على كيفية استخدام التنسيقات في ميزة "الكتابة الذكية".

إنّ الجزء الأكثر أهمية في طريقة العرض المخصّصة هو مظهرها. يمكن أن يكون الرسم المخصّص سهلاً أو معقّدًا حسب احتياجات تطبيقك. يتناول هذا المستند بعض العمليات الأكثر شيوعًا.

لمزيد من المعلومات، اطّلِع على نظرة عامة على عناصر قابلة للرسم.

تجاوز onDraw()

أهم خطوة في رسم عرض مخصّص هي إلغاء طريقة onDraw(). المَعلمة التي يتم تمريرها إلى onDraw() هي Canvas كائن يمكن أن يستخدمه العرض لرسم نفسه. يحدّد الصف Canvas طرقًا لرسم النصوص والخطوط والصور النقطية والعديد من العناصر الرسومية الأساسية الأخرى. يمكنك استخدام هذه الطرق في onDraw() لإنشاء واجهة مستخدم مخصّصة.

ابدأ بإنشاء Paint. يناقش القسم التالي Paint بمزيد من التفصيل.

إنشاء عناصر رسومات

يقسّم إطار العمل android.graphics عملية الرسم إلى منطقتَين:

  • ما يجب رسمه، ويتم التعامل معه بواسطة Canvas
  • كيفية الرسم، تتم إدارتها من خلال Paint.

على سبيل المثال، توفّر Canvas طريقة لرسم خط، وتوفّر Paint طرقًا لتحديد لون هذا الخط. يحتوي Canvas على طريقة لرسم مستطيل، ويحدّد Paint ما إذا كان سيتم ملء هذا المستطيل بلون أو تركه فارغًا. يحدّد Canvas الأشكال التي يمكنك رسمها على الشاشة، بينما يحدّد Paint اللون والنمط والخط وما إلى ذلك لكل شكل ترسمه.

قبل رسم أي شيء، أنشئ Paint واحدًا أو أكثر. يوضّح المثال التالي كيفية إجراء ذلك في طريقة تُسمى init. يتم استدعاء هذه الطريقة من الدالة الإنشائية في Java، ولكن يمكن إعدادها بشكل مضمّن في Kotlin.

Kotlin

@ColorInt
private var textColor    // Obtained from style attributes.

@Dimension
private var textHeight   // Obtained from style attributes.

private val textPaint = Paint(ANTI_ALIAS_FLAG).apply {
    color = textColor
    if (textHeight == 0f) {
        textHeight = textSize
    } else {
        textSize = textHeight
    }
}

private val piePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
    style = Paint.Style.FILL
    textSize = textHeight
}

private val shadowPaint = Paint(0).apply {
    color = 0x101010
    maskFilter = BlurMaskFilter(8f, BlurMaskFilter.Blur.NORMAL)
}

Java

private Paint textPaint;
private Paint piePaint;
private Paint shadowPaint;

@ColorInt
private int textColor;       // Obtained from style attributes.

@Dimension
private float textHeight;    // Obtained from style attributes.

private void init() {
   textPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   textPaint.setColor(textColor);
   if (textHeight == 0) {
       textHeight = textPaint.getTextSize();
   } else {
       textPaint.setTextSize(textHeight);
   }

   piePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   piePaint.setStyle(Paint.Style.FILL);
   piePaint.setTextSize(textHeight);

   shadowPaint = new Paint(0);
   shadowPaint.setColor(0xff101010);
   shadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
}

يُعد إنشاء العناصر مسبقًا عملية تحسين مهمة. يتم إعادة رسم طرق العرض بشكل متكرر، ويتطلب العديد من عناصر الرسم عملية تهيئة مكلفة. يؤدي إنشاء عناصر رسم ضمن طريقة onDraw() إلى خفض الأداء بشكل كبير، وقد يؤدي إلى بطء واجهة المستخدم.

التعامل مع أحداث التنسيق

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

على الرغم من أنّ View تتضمّن العديد من الطرق للتعامل مع القياس، لا تحتاج معظمها إلى إعادة تعريف. إذا لم يكن العرض بحاجة إلى تحكّم خاص في حجمه، ما عليك سوى إلغاء طريقة واحدة: onSizeChanged().

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

عندما يتم تحديد حجم للعرض، يفترض مدير التنسيق أنّ الحجم يتضمّن مساحة العرض المتروكة. تعامَل مع قيم المساحة المتروكة عند حساب حجم العرض. إليك مقتطف من onSizeChanged() يوضّح كيفية إجراء ذلك:

Kotlin

private val showText    // Obtained from styled attributes.
private val textWidth   // Obtained from styled attributes.

override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
    super.onSizeChanged(w, h, oldw, oldh)
    // Account for padding.
    var xpad = (paddingLeft + paddingRight).toFloat()
    val ypad = (paddingTop + paddingBottom).toFloat()

    // Account for the label.
    if (showText) xpad += textWidth.toFloat()
    val ww = w.toFloat() - xpad
    val hh = h.toFloat() - ypad

    // Figure out how big you can make the pie.
    val diameter = Math.min(ww, hh)
}

Java

private Boolean showText;    // Obtained from styled attributes.
private int textWidth;       // Obtained from styled attributes.

@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    super.onSizeChanged(w, h, oldw, oldh);
    // Account for padding.
    float xpad = (float)(getPaddingLeft() + getPaddingRight());
    float ypad = (float)(getPaddingTop() + getPaddingBottom());

    // Account for the label.
    if (showText) xpad += textWidth;

    float ww = (float)w - xpad;
    float hh = (float)h - ypad;

    // Figure out how big you can make the pie.
    float diameter = Math.min(ww, hh);
}

إذا كنت بحاجة إلى تحكّم أدق في مَعلمات تصميم العرض، عليك تنفيذ onMeasure(). تتضمّن مَعلمات هذه الطريقة قيم View.MeasureSpec التي توضّح لك حجم العرض الذي يريده العنصر الرئيسي للعرض، وما إذا كان هذا الحجم هو الحد الأقصى أو مجرد اقتراح. وكتحسين، يتم تخزين هذه القيم كأعداد صحيحة مضغوطة، ويمكنك استخدام الطرق الثابتة في View.MeasureSpec لفك ضغط المعلومات المخزّنة في كل عدد صحيح.

في ما يلي مثال على تنفيذ onMeasure(). في هذا التنفيذ، يحاول جعل مساحته كبيرة بما يكفي لجعل الرسم البياني كبيرًا مثل تصنيفه:

Kotlin

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
    // Try for a width based on your minimum.
    val minw: Int = paddingLeft + paddingRight + suggestedMinimumWidth
    val w: Int = View.resolveSizeAndState(minw, widthMeasureSpec, 1)

    // Whatever the width is, ask for a height that lets the pie get as big as
    // it can.
    val minh: Int = View.MeasureSpec.getSize(w) - textWidth.toInt() + paddingBottom + paddingTop
    val h: Int = View.resolveSizeAndState(minh, heightMeasureSpec, 0)

    setMeasuredDimension(w, h)
}

Java

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on your minimum.
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);

   // Whatever the width is, ask for a height that lets the pie get as big as it
   // can.
   int minh = MeasureSpec.getSize(w) - (int)textWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(minh, heightMeasureSpec, 0);

   setMeasuredDimension(w, h);
}

هناك ثلاث نقاط مهمة يجب ملاحظتها في هذا الرمز:

  • تأخذ العمليات الحسابية في الاعتبار مساحة العرض المتروكة. كما ذكرنا سابقًا، تقع هذه المسؤولية على عاتق طريقة العرض.
  • يتم استخدام طريقة المساعد resolveSizeAndState() لإنشاء قيم العرض والارتفاع النهائية. تعرض هذه الدالة المساعدة قيمة View.MeasureSpec مناسبة من خلال مقارنة حجم العرض المطلوب بالقيمة التي تم تمريرها إلى onMeasure().
  • لا تعرض الدالة onMeasure() أي قيمة. بدلاً من ذلك، تعرض الطريقة نتائجها من خلال استدعاء setMeasuredDimension(). يجب استدعاء هذه الطريقة. إذا حذفت هذا الاستدعاء، سيؤدي ذلك إلى طرح استثناء وقت التشغيل في الفئة View.

الرسم

بعد تحديد رمز إنشاء العنصر وقياسه، يمكنك تنفيذ onDraw(). تنفِّذ كل طريقة عرض onDraw() بشكل مختلف، ولكن هناك بعض العمليات الشائعة التي تشترك فيها معظم طرق العرض:

  • رسم نص باستخدام drawText() حدِّد نوع الخط من خلال استدعاء setTypeface() ولون النص من خلال استدعاء setColor().
  • ارسم أشكالاً أساسية باستخدام drawRect() وdrawOval() وdrawArc(). يمكنك تغيير ما إذا كانت الأشكال معبأة أو مخططة أو كليهما من خلال استدعاء setStyle().
  • ارسم أشكالاً أكثر تعقيدًا باستخدام الفئة Path. حدِّد شكلاً بإضافة خطوط ومنحنيات إلى كائن Path، ثم ارسم الشكل باستخدام drawPath(). وكما هو الحال مع الأشكال الأساسية، يمكن تحديد خطوط خارجية للمسارات أو ملؤها أو كليهما، وذلك استنادًا إلى setStyle().
  • يمكنك تحديد التعبئات المتدرّجة الألوان من خلال إنشاء كائنات LinearGradient. اتّصِل setShader() لاستخدام LinearGradient على الأشكال المملوءة.
  • رسم صور نقطية باستخدام drawBitmap()

يرسم الرمز التالي مزيجًا من النص والخطوط والأشكال:

Kotlin

private val data = mutableListOf<Item>() // A list of items that are displayed.

private var shadowBounds = RectF()       // Calculated in onSizeChanged.
private var pointerRadius: Float = 2f    // Obtained from styled attributes.
private var pointerX: Float = 0f         // Calculated in onSizeChanged.
private var pointerY: Float = 0f         // Calculated in onSizeChanged.
private var textX: Float = 0f            // Calculated in onSizeChanged.
private var textY: Float = 0f            // Calculated in onSizeChanged.
private var bounds = RectF()             // Calculated in onSizeChanged.
private var currentItem: Int = 0         // The index of the currently selected item.

override fun onDraw(canvas: Canvas) {
    super.onDraw(canvas)

    canvas.apply {
        // Draw the shadow.
        drawOval(shadowBounds, shadowPaint)

        // Draw the label text.
        drawText(data[currentItem].label, textX, textY, textPaint)

        // Draw the pie slices.
        data.forEach {item ->
            piePaint.shader = item.shader
            drawArc(
                bounds,
                360 - item.endAngle,
                item.endAngle - item.startAngle,
                true,
                piePaint
            )
        }

        // Draw the pointer.
        drawLine(textX, pointerY, pointerX, pointerY, textPaint)
        drawCircle(pointerX, pointerY, pointerRadius, textPaint)
    }
}

// Maintains the state for a data item.
private data class Item(
    var label: String,      
    var value: Float = 0f,

    @ColorInt
    var color: Int = 0,

    // Computed values.
    var startAngle: Float = 0f,
    var endAngle: Float = 0f,

    var shader: Shader
)

Java

private List<Item> data = new ArrayList<Item>();  // A list of items that are displayed.

private RectF shadowBounds;                       // Calculated in onSizeChanged.
private float pointerRadius;                      // Obtained from styled attributes.
private float pointerX;                           // Calculated in onSizeChanged.
private float pointerY;                           // Calculated in onSizeChanged.
private float textX;                              // Calculated in onSizeChanged.
private float textY;                              // Calculated in onSizeChanged.
private RectF bounds;                             // Calculated in onSizeChanged.
private int currentItem = 0;                      // The index of the currently selected item.

protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    // Draw the shadow.
    canvas.drawOval(
            shadowBounds,
            shadowPaint
    );

    // Draw the label text.
    canvas.drawText(data.get(currentItem).label, textX, textY, textPaint);

    // Draw the pie slices.
    for (int i = 0; i < data.size(); ++i) {
        Item it = data.get(i);
        piePaint.setShader(it.shader);
        canvas.drawArc(
                bounds,
                360 - it.endAngle,
                it.endAngle - it.startAngle,
                true, 
                piePaint
        );
    }

    // Draw the pointer.
    canvas.drawLine(textX, pointerY, pointerX, pointerY, textPaint);
    canvas.drawCircle(pointerX, pointerY, pointerRadius, textPaint);
}

// Maintains the state for a data item.
private class Item {
    public String label;
    public float value;
    @ColorInt
    public int color;

    // Computed values.
    public int startAngle;
    public int endAngle;

    public Shader shader;
}    

تطبيق تأثيرات الرسومات

يضيف الإصدار Android 12 (المستوى 31 لواجهة برمجة التطبيقات) الفئة RenderEffect التي تطبّق تأثيرات رسومات شائعة، مثل التمويه وفلاتر الألوان وتأثيرات Android shader وغيرها على عناصر View وهياكل العرض. يمكنك الجمع بين التأثيرات كتأثيرات متسلسلة تتألف من تأثير داخلي وخارجي، أو تأثيرات مدمجة. يختلف توفّر هذه الميزة حسب قدرة معالجة الجهاز.

يمكنك أيضًا تطبيق تأثيرات على RenderNode الأساسي لإنشاء View من خلال استدعاء View.setRenderEffect(RenderEffect).

لتنفيذ عنصر RenderEffect، اتّبِع الخطوات التالية:

view.setRenderEffect(RenderEffect.createBlurEffect(radiusX, radiusY, SHADER_TILE_MODE))

يمكنك إنشاء العرض برمجيًا أو توسيعه من تنسيق XML واسترداده باستخدام ربط العرض أو findViewById().