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

تجربة طريقة ComposeAllowed
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()