رسم نماهای سفارشی در اندروید — از صفر تا صد

ما به عنوان توسعهدهندگان اندروید انواع مختلفی از UI را هر روز با استفاده از XML طراحی میکنیم، اما یادگیری رسم نماهای سفارشی در اندروید کاری آسان و مهارتی عالی محسوب میشود. با بهرهگیری از مزیت «نماهای سفارشی» (Custom Views) میتوانید از تکرار کدهای قالبی خودداری کنید.
اندروید مجموعهای عالی از ویجتها یا لیآوتها برای ساخت UI ارائه کرده است، اما این موارد نمیتوانند همه الزامات اپلیکیشنهای ما را برآورده سازند. این همان جایی است که نماهای سفارشی وارد بازی میشوند. به این منظور به یک زیرکلاس از یک نما نیاز داریم. به دست آوردن یک زیرکلاس از یک نما موجب میشود که کنترل دقیقی روی ظاهر و کارکرد آن عنصر روی صفحه داشته باشید. پیشنهاد میکنیم پیش از مطالعه این مقاله در صورت نیاز در مورد «چرخههای عمر نما» (View Lifecycle) اطلاعات بیشتری کسب نمایید.
چرا باید از نمای سفارشی استفاده کرد؟
از آنجا که ساخت اغلب نماهای سفارشی نسبت به نماهای معمولی به زمان بیشتر نیاز دارد، تنها باید زمانی از آنها استفاده کنیم که روش آسانتری برای پیادهسازی یک قابلیت وجود نداشته باشد و یا با مشکلات زیر مواجه باشیم که تنها از طریق نمای سفارشی قابل حل است:
- اگر نماهای زیادی در لیآوت خود دارید، میتوانید آنها را با رسم یک نمای سفارشی واحد و سبکتر کردن لیآوت بهینهتر سازید.
- در صورتی که کاربری و پشتیبانی از سلسلهمراتب نمای پیچیده دشوار باشد.
- به نمای سفارشی کامل نیاز دارید که باید به صورت دستی رسم شود.
مرور مقدماتی
برای آغاز ایجاد کامپوننتهای نمای سفارشی ابتدا باید مراحل زیر را طی کنیم:
- یک کلاس ایجاد کنید و کلاس نمای مبنا (Base View) یا زیرکلاس آن را بسط (Extend) دهید.
- سازندههایی برای مصرف خصوصیتها از XML طراحی نمایید.
- برخی از متدهای سوپرکلاس را Override کنید. متدهایی مانند ()onDraw() ،onMeasure و غیره بسته به نیاز باید باطل شوند.
- پس از تکمیل مراحل فوق، کلاس بسطیافته جدید میتواند به جای نمایی که بر مبنای آن ساخته شده مورد استفاده قرار گیرد.
مثال
فرض کنید در پروژهای لازم شده است که یک «نمای متنی» (TextView) مدور برای نمایش تعداد اعلانها بسازیم. در این حالت به یک زیرکلاس از TextView نیاز خواهیم داشت.
- گام 1: یک کلاس با نام CircularTextView میسازیم.
- گام 2: کلاس ویجت TextView را بسط میدهیم. اکنون IDE خطایی روی TextView نمایش میدهد که بیان میکند نوع سازنده باید ابتدا مقداردهی شود.
- گام 3: سازندهها را به کلاس اضافه میکنیم.
این کار به دو روش قابل اجرا است. یک روش این است که سازندهها را به مانند تصویر زیر به کلاس اضافه کنیم:
روش دیگر آن است که JvmOverloads@ را به فراخوانی سازنده مانند تصویر زیر اضافه کنیم:
به طور معمول، در مورد علت این که چرا انواع متفاوتی از سازندهها برای یک نما وجود دارند دچار سردرگمی میشویم.
View(Context context): این یک سازنده ساده است که در موارد ایجاد نما از کد به صورت دینامیکی مورد استفاده قرار میگیرد. در اینجا پارامتر context، ساختاری است که نما در آن اجرا میشود و از طریق آن به قالب جاری، منابع و غیره دسترسی مییابیم.
View(Context context, @Nullable AttributeSet attrs): این سازندهای که است که در زمان ساخت یک نما از روی XML فراخوانی میشود. این نما زمانی فراخوانی میشود که از فایل XML ساخته شود و خصوصیتهایی که در این فایل تعیینشده را ارائه میکند. این نسخه از یک استایل پیشفرض 0 استفاده میکند، بنابراین تنها مقدار خصوصیت اعمال شده آنهایی هستند که در قالب Context و در AttributeSet ارائه شده قرار دارند.
- گام 4: مهمترین گام در زمان رسم نمای سفارشی، باطل کردن متد ()onDraw و پیادهسازی منطق خودمان درون آن است.
OnDraw(canvas: Canvas?) یک پارامتر «بوم» (Canvas) دارد که کامپوننت نما به وسیله آن خودش را رسم میکند. برای رسم بوم لازم است که یک شیء Paint ایجاد کنیم.
رسم به طور عمده به دو ناحیه تقسیم میشود:
- آن چه باید رسم شود که از سوی Canvas مدیریت میشود.
- رسم چگونه باید انجام شود که از سوی Paint مدیریت میشود.
Canvas متدی برای رسم یک خط ارائه میکند، در حالی که Paint متدهایی برای تعریف رنگ خط معرفی میکند. در مثال CircularTextView، بوم متدی برای رسم یک دایره ارائه میکند، در حالی که شیء Paint مشخصههایی مانند رنگ، استایل، فونت و غیره را تعریف میکند که شکل کلی رسم را تعیین میکنند.
این نوبت به کدنویسی میرسد یک شیء Paint ایجاد میکنیم که برخی مشخصهها دارد و سپس شکل را با استفاده از آن شیء Paint روی بوم رسم میکنیم. متد ()onDraw مانند تصویر زیر است:
IDE یک هشدار نمایش میدهد که از تخصیص اشیا در طی مدت عملیات رسم/طرحبندی جلوگیری میکند. از آنجا که متد ()onDraw به طور مکرر فراخوانی میشود، در زمان رندر کردن یک نما هر بار اشیای غیرضروری تولید میشوند. بنابراین برای جلوگیری از ایجاد شیء غیرضروری باید این بخش را به صورت زیر به متد ()onDraw منتقل کنیم:
در زمان اجرای رسم، همواره به خاطر داشته باشید که به جای خلق اشیای جدید باید از شیء موجود مجدداً استفاده کنید. در این مورد بر IDE که یک مشکل احتمالی را هایلایت میکند، تکیه نکنید، زیرا IDE نمیتواند ببیند که شما نمیتوانید اشیای جدیدی درون متدهای فراخوانی شده از onDraw() ایجاد کنید.
- گام 5: اکنون آماده رسم هستیم و میتوانیم این کلاس نما را در XML تعریف کنیم:
این لیآوت XML را به اکتیویتی خود اضافه کرده و اپلیکیشن را اجرا کنید. خروجی به صورت زیر خواهد بود:
اینک این رنگ این نمای circlePaint را به صورت یک مشخصه دینامیک تغییر میدهیم که از درون اکتیویتی به همراه یک Stroke تخصیص مییابد. به این منظور باید متدهای setter دیگری در کلاس CircularTextView خود بسازیم که بتوانیم آن را برای تعیین دینامیک مشخصهها فراخوانی کنیم. در ابتدا یک رنگ Paint پیکربندی میکنیم. به این منظور یک متد setter به صورت زیر ایجاد میکنیم:
fun setSolidColor(color: String) { solidColor = Color.parseColor(color) circlePaint?.color = solidColor }
اکنون از درون اکتیویتی میتوانیم رنگ را به صورت دینامیکی با فراخوانی این متد تعیین کنیم:
circularTextView.setSolidColor("#FF0000")
در ادامه نوعی حاشیه نیز به دایره اضافه میکنیم. در مورد این Stroke دو نوع ورودی مورد نیاز است که یکی عرض آن و دیگری رنگش است. برای رنگ Stroke نیازمند ایجاد یک شیء Paint هستیم که همانند آن چیزی که قبلاً در مورد خود دایره دیدیم. برای عرض Stroke یک متغیر ایجاد کرده و مقدار آن را تعیین میکنیم و از این مقدار در متد ()onDraw استفاده میکنیم. کد کامل به صورت زیر خواهد بود:
class CircularTextView @JvmOverloads constructor( context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : TextView(context, attrs, defStyleAttr) { private var strokeWidth: Float = 0.toFloat() private var strokeColor: Int = 0 internal var solidColor: Int = 0 private var strokePaint: Paint? = null private var circlePaint: Paint? = null init { circlePaint = Paint() circlePaint?.color = Color.YELLOW circlePaint?.flags = Paint.ANTI_ALIAS_FLAG strokePaint = Paint() strokePaint?.color = Color.BLUE strokePaint?.flags = Paint.ANTI_ALIAS_FLAG } fun setSolidColor(color: String) { solidColor = Color.parseColor(color) circlePaint?.color = solidColor } fun setStrokeWidth(dp: Int) { val scale = context.resources.displayMetrics.density strokeWidth = dp * scale } fun setStrokeColor(color: String) { strokeColor = Color.parseColor(color) strokePaint?.color = strokeColor } override fun onDraw(canvas: Canvas?) { val h = this.height val w = this.width val diameter = if (h > w) h else w val radius = diameter / 2 this.height = diameter this.width = diameter canvas?.drawCircle((diameter / 2).toFloat(), (diameter / 2).toFloat(), radius.toFloat(), strokePaint!!) canvas?.drawCircle((diameter / 2).toFloat(), (diameter / 2).toFloat(), radius - strokeWidth, circlePaint!!) super.onDraw(canvas) } }
بدین ترتیب میتوانیم خصوصیتها را از درون اکتیویتی تنظیم کرده و آن را سفارشیسازی کنیم.
اکنون اپلیکیشن را با تنظیمات رنگی متفاوتی اجرا میکنیم:
اینک ایدهای از چگونگی تنظیم دینامیک مشخصهها از طریق اکتیویتی به دست آوردهایم، اما این سؤال پابرجاست که چگونه میتوانیم خصوصیتها را از XML تنظیم کنیم. در ادامه این موضوع را نیز توضیح خواهیم داد.
به این منظور ابتدا یک فایل جدید در پوشه values ایجاد میکنیم که نام آن attrs.xml باشد. این فایل شامل خصوصیتهای نماهای سفارشی مختلف است. در مثال زیر یک نما به نام CircularTextView یا خصوصیتی به نام ct_circle_fill_color داریم که ورودی رنگی میگیرد. به طور مشابه، میتوانیم خصوصیتهای دیگری نیز اضافه کنیم:
سپس در وهله دوم باید این مشخصهها را در کلاس نمای سفارشی فراخوانی کنیم. در بلوک init خصوصیتهای تعیینشده را به صورت زیر میخوانیم:
val typedArray = getContext().obtainStyledAttributes(attrs, R.styleable.CircularTextView) circlePaint?.color = typedArray.getColor(R.styleable.CircularTextView_ct_circle_fill_color,Color.BLUE) typedArray.recycle()
اکنون کافی است به فایل xml برویم و مقدار مشخصه را روی رنگی که میخواهیم تعیین کرده و اپلیکیشن را اجرا کنیم. بدین ترتیب خروجی مطلوب مشاهده میشود:
app:ct_circle_fill_color="@color/green"
در این مورد خروجی به صورت زیر است:
نکته: هرگز نباید اندازه نما را در زمان رسم کردن به صورت هاردکد تعیین کنید، زیرا توسعهدهندگان ممکن است همان نما را در اندازههای متفاوتی ببینند. بنابراین نما را بسته به اندازهای که دارد رسم کنید.
بهروزرسانی نمای سفارشی
اکنون که با روش رسم نماهای سفارشی آشنا شدیم، نوبت آن رسیده است که با روش بهروزرسانی این نماها در مواردی از قبیل بروز یک تغییر نیز آشنا شویم. به طور عمده دو روش برای این منظور وجود دارد که در ادامه آنها را توضیح میدهیم.
()invalidate: که یک متد است و موجب redraw الزامی یک نمای خاص برای نمایش تغییرات میشود. کافی است در مواردی که میخواهیم تغییری در نما ظاهر شود، invalidate را فراخوانی کنیم.
requestLayout: برخی اوقات تغییراتی در حالت ما ایجاد میشود، در این حالت میتوانیم با فراخوانی ()requestLayout اعلام کنیم که فازهای Measure و Layout نما باید از نو محاسبه شود. در این حالت در صورت بروز تغییری در کرانهای نما، کافی است متد Measure and Layout را فراخوانی کنیم.
بدین ترتیب به پایان این مقاله میرسیم. در این راهنما مروری مقدماتی بر شیوه ایجاد نمای سفارشی داشتیم. برای ایجاد نماهای سفارشی با بهترین عملکرد باید با همه مواردی که در این مقاله مطرح شدند آشنایی داشته باشید.
اگر این مطلب برای شما مفید بوده است، آموزشهای زیر نیز به شما پیشنهاد میشوند:
- مجموعه آموزشهای برنامهنویسی اندروید
- مجموعه آموزشهای برنامهنویسی
- گنجینه برنامهنویسی اندروید (Android)
- نوتیفیکیشن اندروید — اصول مقدماتی
- ۵ گام ضروری برای یادگیری برنامهنویسی اندروید — راهنمای جامع
==