یک طراحی سفارشی ایجاد کنید

روش Compose را امتحان کنید
Jetpack Compose جعبه ابزار UI توصیه شده برای اندروید است. نحوه کار با طرح‌بندی‌ها در Compose را بیاموزید.

مهمترین بخش یک نمای سفارشی ظاهر آن است. طراحی سفارشی می تواند با توجه به نیازهای برنامه شما آسان یا پیچیده باشد. این سند برخی از رایج ترین عملیات را پوشش می دهد.

برای اطلاعات بیشتر، به نمای کلی Drawables مراجعه کنید.

Override onDraw()

مهمترین مرحله در ترسیم نمای سفارشی، نادیده گرفتن متد onDraw() است. پارامتر onDraw() یک شی Canvas است که view می تواند از آن برای ترسیم خود استفاده کند. کلاس Canvas متدهایی را برای ترسیم متن، خطوط، بیت مپ و بسیاری دیگر از گرافیک های اولیه تعریف می کند. می توانید از این روش ها در onDraw() برای ایجاد رابط کاربری سفارشی خود (UI) استفاده کنید.

با ایجاد یک شی Paint شروع کنید. بخش بعدی Paint با جزئیات بیشتری مورد بحث قرار می دهد.

ایجاد اشیاء نقاشی

چارچوب android.graphics طراحی را به دو بخش تقسیم می کند:

  • چه چیزی را بکشیم، توسط Canvas اداره می شود.
  • نحوه ترسیم توسط Paint .

به عنوان مثال، Canvas روشی را برای کشیدن یک خط و Paint روش هایی را برای تعیین رنگ آن خط ارائه می دهد. Canvas روشی برای رسم مستطیل دارد و Paint تعیین می کند که آیا آن مستطیل را با یک رنگ پر کنید یا آن را خالی بگذارید. Canvas اشکالی را تعریف می کند که می توانید روی صفحه بکشید و Paint رنگ، سبک، فونت و غیره را از هر شکلی که می کشید را مشخص می کند.

قبل از کشیدن هر چیزی، یک یا چند شیء Paint ایجاد کنید. مثال زیر این کار را در متدی به نام init انجام می دهد. این متد از سازنده جاوا فراخوانی می شود، اما می توان آن را به صورت درون خطی در 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() به طور قابل توجهی عملکرد را کاهش می دهد و می تواند رابط کاربری شما را کند کند.

رویدادهای طرح بندی را مدیریت کنید

برای ترسیم درست نمای سفارشی خود، اندازه آن را بیابید. نماهای سفارشی پیچیده اغلب باید بسته به اندازه و شکل ناحیه خود روی صفحه، محاسبات طرح‌بندی متعددی را انجام دهند. هرگز در مورد اندازه نمای خود روی صفحه فرضیات نکنید. حتی اگر فقط یک برنامه از نمای شما استفاده کند، آن برنامه باید اندازه‌های مختلف صفحه، چگالی صفحه‌نمایش چندگانه و نسبت‌های مختلف را در حالت عمودی و افقی مدیریت کند.

اگرچه View روش‌های زیادی برای مدیریت اندازه‌گیری دارد، اما اکثر آنها نیازی به نادیده گرفتن ندارند. اگر نمای شما به کنترل خاصی روی اندازه خود نیاز ندارد، فقط یک روش را لغو کنید: onSizeChanged() .

onSizeChanged() زمانی فراخوانی می شود که برای اولین بار به نمای شما یک اندازه اختصاص داده شود، و اگر اندازه نمای شما به هر دلیلی تغییر کند، دوباره. موقعیت ها، ابعاد و هر مقدار دیگر مربوط به اندازه نمای خود را در onSizeChanged() محاسبه کنید، به جای اینکه هر بار که ترسیم می کنید دوباره آنها را محاسبه کنید. در مثال زیر، onSizeChanged() جایی است که view مستطیل مرزی نمودار و موقعیت نسبی برچسب متن و سایر عناصر بصری را محاسبه می کند.

هنگامی که به نمای شما یک اندازه اختصاص داده می شود، مدیر طرح بندی فرض می کند که اندازه شامل صفحه نمایش است. هنگام محاسبه اندازه نمای خود، مقادیر padding را مدیریت کنید. در اینجا یک قطعه از onSizeChanged() است که نحوه انجام این کار را نشان می دهد:

کاتلین

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

اگر به کنترل دقیق تری روی پارامترهای طرح بندی view خود نیاز دارید، onMeasure() را پیاده سازی کنید. پارامترهای این روش مقادیر View.MeasureSpec هستند که به شما می‌گویند والدین دیدگاه شما می‌خواهند نمای شما چقدر بزرگ باشد و آیا این اندازه حداکثر سخت است یا فقط یک پیشنهاد. به عنوان یک بهینه سازی، این مقادیر به عنوان اعداد صحیح بسته بندی شده ذخیره می شوند و شما از روش های استاتیک View.MeasureSpec برای باز کردن اطلاعات ذخیره شده در هر عدد صحیح استفاده می کنید.

در اینجا نمونه ای از پیاده سازی onMeasure() است. در این پیاده سازی، سعی می کند مساحت خود را به اندازه ای بزرگ کند که نمودار را به اندازه برچسب آن بزرگ کند:

کاتلین

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() استفاده می شود. این کمک کننده یک مقدار View.MeasureSpec مناسب را با مقایسه اندازه مورد نیاز view با مقدار ارسال شده به onMeasure() برمی گرداند.
  • onMeasure() هیچ مقدار بازگشتی ندارد. در عوض، متد نتایج خود را با فراخوانی setMeasuredDimension() ارتباط می دهد. فراخوانی این روش الزامی است. اگر این فراخوانی را حذف کنید، کلاس View یک استثنا در زمان اجرا ایجاد می کند.

قرعه کشی کنید

بعد از اینکه کد ایجاد و اندازه گیری شی خود را تعریف کردید، می توانید onDraw() را پیاده سازی کنید. هر view به طور متفاوتی onDraw() را پیاده سازی می کند، اما برخی از عملیات های رایج وجود دارد که اکثر view ها به اشتراک می گذارند:

  • با استفاده از drawText() متن را ترسیم کنید. با فراخوانی setTypeface() تایپ و با فراخوانی setColor() رنگ متن را مشخص کنید.
  • اشکال ابتدایی را با استفاده از drawRect() ، drawOval() و drawArc() رسم کنید. با فراخوانی setStyle() پر شده، مشخص شده یا هر دو شکل را تغییر دهید.
  • با استفاده از کلاس Path اشکال پیچیده تری رسم کنید. با افزودن خطوط و منحنی ها به یک شی Path یک شکل را تعریف کنید، سپس با استفاده از drawPath() شکل را ترسیم کنید. مانند اشکال ابتدایی، بسته به setStyle() می‌توان مسیرها را مشخص، پر کرد یا هر دو را مشخص کرد.
  • با ایجاد اشیاء LinearGradient ، پرهای گرادیان را تعریف کنید. برای استفاده از LinearGradient روی اشکال پر شده setShader() را فراخوانی کنید.
  • نقشه های بیت را با استفاده از drawBitmap() بکشید.

کد زیر ترکیبی از متن، خطوط و اشکال را ترسیم می کند:

کاتلین

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 اشیاء و سلسله مراتب رندر استفاده می کند. می‌توانید افکت‌ها را به‌عنوان جلوه‌های زنجیره‌ای، که از یک اثر درونی و بیرونی، یا جلوه‌های ترکیبی تشکیل شده‌اند، ترکیب کنید. پشتیبانی از این ویژگی بسته به قدرت پردازش دستگاه متفاوت است.

همچنین می‌توانید با فراخوانی View.setRenderEffect(RenderEffect) روی RenderNode زیرین برای یک View افکت‌ها اعمال کنید.

برای پیاده سازی یک شی RenderEffect ، موارد زیر را انجام دهید:

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

می‌توانید View را به‌صورت برنامه‌نویسی ایجاد کنید یا آن را از یک طرح XML پر کنید و با استفاده از View binding یا findViewById() آن را بازیابی کنید.