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

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

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

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

إلغاء 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 وغيرها على عناصر View والعروض الهرمية للعرض. يمكنك الجمع بين التأثيرات كتأثيرات سلسلة، والتي تتكون من تأثير داخلي وخارجي، أو تأثيرات ممزوجة. يختلف دعم هذه الميزة بناءً على قوة معالجة الجهاز.

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

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

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

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