مهمترین بخش یک نمای سفارشی ظاهر آن است. طراحی سفارشی می تواند با توجه به نیازهای برنامه شما آسان یا پیچیده باشد. این سند برخی از رایج ترین عملیات را پوشش می دهد.
برای اطلاعات بیشتر، به نمای کلی 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()
آن را بازیابی کنید.