কাস্টম ড্রয়িং তৈরি করুন

Compose-এর মাধ্যমে চেষ্টা করুন
Jetpack Compose হল Android-এর জন্য সাজেস্ট করা UI টুলকিট। Compose-এ লেআউট নিয়ে কীভাবে কাজ করতে হয় তা জানুন।

কাস্টম ভিউয়ের সবচেয়ে গুরুত্বপূর্ণ অংশ হল এটি দেখতে কেমন। কাস্টম ড্রয়িং আপনার অ্যাপ্লিকেশনের প্রয়োজন অনুযায়ী সহজ বা জটিল হতে পারে। এই ডকুমেন্টে সবচেয়ে সাধারণ কিছু অপারেশন কভার করা হয়েছে।

আরও তথ্যের জন্য, Drawables ওভারভিউ দেখুন।

onDraw() ওভাররাইড করুন

কাস্টম ভিউ আঁকার সবচেয়ে গুরুত্বপূর্ণ ধাপ হল onDraw() মেথড ওভাররাইড করা। onDraw()-এর প্যারামিটার হল একটি Canvas অবজেক্ট যা ভিউ নিজেকে আঁকার জন্য ব্যবহার করতে পারে। Canvas ক্লাস টেক্সট, লাইন, বিটম্যাপ ও অন্যান্য অনেক গ্রাফিক্স প্রিমিটিভ আঁকার পদ্ধতি নির্ধারণ করে। আপনার কাস্টম ইউজার ইন্টারফেস (UI) তৈরি করতে, আপনি 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)
}

জাভা

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() পদ্ধতিতে ড্রয়িং অবজেক্ট তৈরি করলে পারফর্ম্যান্স উল্লেখযোগ্যভাবে কমে যায় এবং আপনার UI ধীরগতির হয়ে যেতে পারে।

লেআউট ইভেন্ট ম্যানেজ করুন

আপনার কাস্টম ভিউ সঠিকভাবে আঁকার জন্য, এটির সাইজ কত তা খুঁজে বের করুন। জটিল কাস্টম ভিউতে স্ক্রিনে তাদের অংশের সাইজ ও আকারের উপর নির্ভর করে প্রায়ই একাধিক লেআউট গণনা করতে হয়। স্ক্রিনে আপনার ভিউয়ের সাইজ সম্পর্কে কখনও অনুমান করবেন না। এমনকি শুধুমাত্র একটি অ্যাপ আপনার ভিউ ব্যবহার করলেও, সেই অ্যাপকে বিভিন্ন স্ক্রিন সাইজ, একাধিক স্ক্রিন ডেনসিটি এবং পোর্ট্রেট ও ল্যান্ডস্কেপ মোডে বিভিন্ন অ্যাস্পেক্ট রেশিও ম্যানেজ করতে হবে।

যদিও 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)
}

জাভা

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)
}

জাভা

@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() হেল্পার পদ্ধতি ব্যবহার করা হয়। এই হেল্পার onMeasure()-এ পাস করা ভ্যালুর সাথে ভিউয়ের প্রয়োজনীয় সাইজের তুলনা করে উপযুক্ত View.MeasureSpec ভ্যালু রিটার্ন করে।
  • onMeasure()-এর কোনও রিটার্ন ভ্যালু নেই। পরিবর্তে, পদ্ধতিটি setMeasuredDimension() কল করে তার ফলাফল জানায়। এই পদ্ধতি কল করা বাধ্যতামূলক। আপনি এই কলটি বাদ দিলে, View ক্লাস রানটাইম ব্যতিক্রম থ্রো করে।

ড্র

অবজেক্ট তৈরি ও পরিমাপ করার কোড নির্ধারণ করার পরে, আপনি onDraw() প্রয়োগ করতে পারবেন। প্রতিটি ভিউ onDraw()-কে আলাদাভাবে প্রয়োগ করে, তবে কিছু সাধারণ অপারেশন আছে যা বেশিরভাগ ভিউ শেয়ার করে:

  • drawText() ব্যবহার করে টেক্সট আঁকুন। setTypeface() কল করে টাইপফেস এবং setColor() কল করে টেক্সটের রঙ নির্দিষ্ট করুন।
  • drawRect(), drawOval() এবং drawArc() ব্যবহার করে প্রিমিটিভ আকার আঁকুন। setStyle() কল করে আকারগুলি ফিল করা, আউটলাইন করা বা উভয়ই করা হয়েছে কিনা তা পরিবর্তন করুন।
  • Path ক্লাস ব্যবহার করে আরও জটিল আকার আঁকা। Path অবজেক্টে লাইন ও কার্ভ যোগ করে একটি আকার নির্ধারণ করুন, তারপর drawPath() ব্যবহার করে আকারটি আঁকুন। প্রিমিটিভ শেপের মতো, পাথ আউটলাইন, ফিল বা দুটিই করা যেতে পারে, setStyle()-এর উপর নির্ভর করে।
  • LinearGradient অবজেক্ট তৈরি করে গ্রেডিয়েন্ট ফিল নির্ধারণ করুন। ভরাট করা আকারে আপনার LinearGradient ব্যবহার করতে setShader() কল করুন।
  • 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
)

জাভা

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 (API লেভেল 31) RenderEffect ক্লাস যোগ করে, যা View অবজেক্ট এবং রেন্ডারিং হায়ারার্কিতে ব্লার, কালার ফিল্টার, Android শেডার এফেক্ট এবং আরও অনেক কিছুর মতো সাধারণ গ্রাফিক্স এফেক্ট প্রয়োগ করে। আপনি চেন এফেক্ট হিসেবে এফেক্ট একত্রিত করতে পারেন, যার মধ্যে ইনার ও আউটার এফেক্ট বা ব্লেন্ডেড এফেক্ট থাকে। এই ফিচারের জন্য সহায়তা ডিভাইস প্রসেসিং পাওয়ারের উপর নির্ভর করে পরিবর্তিত হয়।

এছাড়াও, আপনি View.setRenderEffect(RenderEffect) কল করে View-এর জন্য RenderNode-এ এফেক্ট প্রয়োগ করতে পারবেন।

RenderEffect অবজেক্ট প্রয়োগ করতে, নিম্নলিখিত ধাপগুলি অনুসরণ করুন:

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

আপনি প্রোগ্রাম্যাটিক্যালি ভিউ তৈরি করতে পারেন অথবা XML লেআউট থেকে এটি ইনফ্লেট করতে পারেন এবং ভিউ বাইন্ডিং বা findViewById() ব্যবহার করে এটি ফিরিয়ে আনতে পারেন।