آموزش ساخت Custom View در اندروید

احتمالا بارها پیش اومده که در برنامه‌ای که می‌نویسید به المانی نیاز پیدا کنید که در میان ابزارها و Viewهای خود اندروید وجود نداشته باشه. در این مواقع به احتمال بسیار بسیار زیاد می‌تونید چیزی که می‌خواید رو در میان هزاران کتابخانه اوپن سورسی که در اینترنت موجود هست پیدا کنید.

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


در این مطلب ما قصد داریم از پایه ترین حالت ممکن یک Custom View بسازیم. ابتدا قبل از هر کاری ببینیم چیزی که قراره بسازیم چی خواهد بود.

همونطور که میبینید چیزی که قراره ساخته بشه یک دکمه ضبط با چهار حالت بیکار، آماده، در حال ضبط و در حال بارگزاری خواهد بود.

نکته: من در این آموزش از زبان Kotlin استفاده می‌کنم که اگر با کاتلین آشنا نیستید پیشنهاد می‌کنم هر چه سریع‌تر دست به کار بشید و یاد گرفتنش رو شروع کنید.

مرحله ۱: اکستند از ویو

اولین کاری که باید انجام بدیم ساختن کلاس Viewمون هست که خوب مشخصا باید از کلاس View اندروید اکستند بشه. اسم ویو رو همونطور که میبینید RecordButton گذاشتیم.

https://gist.github.com/2hamed/56fb6303ef9065d01ed7d985d5a8e4b6

در توضیح کد بالا باید بگم که ما در اینجا دو تا از Constructorهای کلاس View رو override می‌کنیم. این ۲ constructor معمولا پرکاربردترین حالت‌ها هستند که البته حالت‌های دیگری هم وجود داره که استفاده متفاوتی دارند.

مرحله ۲: محاسبه ابعاد و اندازه‌ها

در این مرحله ما ابتدا به سراغ ایجاد ساده‌ترین حالتمون میریم. یعنی مود بیکار یا Idle که از دو تا دایره تشکیل شده. دایره کوچکتر مرکز سفید رنگمون هست و دایره بزرگتر حاشیه خاکستری رنگ.

دایره سفید در مرکز و قوس خاکستری در اطراف
دایره سفید در مرکز و قوس خاکستری در اطراف

ابتدا قبل از شروع کشیدن دایره‌ها لازمه که بدونیم ویوی ما دارای چه ابعادی هست. اینکه عرض و ارتفاع چه مقداری هست که ما بتونیم مرکز دایره و شعاعش رو مشخص کنیم. برای اینکه بتونیم این محاسبات رو انجام بدیم باید بدونیم که ابعاد ویوی ما چیا هستن و برای اینکار متود onMeasure رو override می‌کنیم.

https://gist.github.com/2hamed/e3b5d5ff975bd95ec5163b621eef17d2

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

داخل شرط هم که مشخصه مرکز ویو رو با تقسیم کردن عرض و ارتفاع بر ۲ به دست میاریم. شعاع هم برابر میذاریم با ۹۵ درصد کوچکترین بعد. یعنی ۹۵ درصد ارتفاع یا عرض، هر کدوم که کوچکتر بود و همچنین مقدار padding رو هم ازش کم می‌کنیم.

تذکر: اینکه چرا من ۹۵ درصد رو انتخاب کردم کاملا به سلیقه شخصی بر می‌گرده و شما می‌تونید هر چیزی بذاریدش.

مرحله ۳: مشخص کردن رنگ و مشخصات اجزاء

الان نوبت مشخص کردنه رنگ و بقیه مشخصات دایره و قوسمون رسیده. برای اینکار از کلاس Paint استفاده می‌کنیم.

https://gist.github.com/2hamed/103cd73b7ca7c7d5fb4ed98fefc79d1e

آبجکت innerPaint مربوط به دایره سفید رنگ وسط و outerPaint مربوط به دایره بیرونی هستش. نظر شما رو به radiusDiff هم جلب می‌کنم که مشخص میکنه دایره بیرونی ما چقدر بزرگتر از داخلیه باشه. مشخصه‌ی isAntiAlias هم برای اینه که دایره‌های ما اون حالت روان خودشون رو داشته باشن و اصطلاحا پیکسلی (pixelated) نشن.


مرحله ۴: نقاشی روی بوم

مرحله کشیدن یا Draw میشه گفت مهمترین مرحله تو ساخت یک Custom View به حساب میاد که با override کردن متود onDraw انجام میشه. در اینجا با یک آبجکت Canvas سر و کار داریم که مثل یک بوم نقاشی کاملا خالی هستش و می‌تونیم هر چیزی رو داخلش نقاشی کنیم.این آبجکت Canvas چیزی هست که در نهایت به کاربر نمایش داده میشه.

در اینجا ما ابزارهایی داریم که با کمک اونها می‌تونیم نقاشی کنیم که در این مرحله ما فقط از ابزار کشیدن دایره استفاده می‌کنیم. متود Canvas.drawCircle با دریافت مختصات x و y مرکز و مقدار شعاع و یک آبجکت Paint روی بوم دایره می‌کشه.

https://gist.github.com/2hamed/e46f20d480590341d966a9ee71a35b40

تذکر: یک نکته مهم که اینجا لازمه یاداور بشم اینه که توی متود onDraw به هیچ وجه نباید allocation انجام بشه. به این معنی که هیچ آبجکت جدیدی نباید ساخته بشه و بهش حافظه تخصیص داده بشه. به طور کلی هیچ کار سنگینی نباید انجام بشه و این نکته به این دلیله که اندروید برای اینکه بتونه کیفیت انیمیشن‌ها و نمایش تصویرش رو حفظ کنه نیاز داره که در هر ثانیه، ۶۰ بار صفحه رو آپدیت کنه و این یعنی ما فقط فرصت داریم در فاصله یک فریم شکل مورد نظرمون رو بکشیم که میشه چیزی حدود ۱۶ میلی‌ثانیه. هر کاری بیش از این مقدار باعث ایجاد لگ میشه. پیشنهاد می‌کنم این مطلب رو در مورد این نکته بخونید.


مرحله ۵: افزودن حالت‌های آماده و در حال ضبط

در این مرحله ما نیاز داریم که حالت‌های مختلفی رو نمایش بدیم. بهترین ابزاری که می‌تونید برای ذخیره و نمایش چند حالت خاص استفاده کنید از نظر من enum class هست. اگر نمی‌دونید enum ها چی هستن پیشنهاد می‌کنم این مطلب رو در سکان آکادمی بخونید.

اگر خاطرتون باشه ما بطور کل ۴ حالت متفاوت داشتیم. حالت‌های بیکار، آماده، در حال ضبط و در حال بارگزاری که به ترتیب با Idle، Ready، Recording و Loading مشخص می‌شن. و کلاس enum به صورت زیر خواهد بود.

https://gist.github.com/2hamed/a8c413b95490ecba4706681badf4a4e3

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

https://gist.github.com/2hamed/926355b85b5e793796fa50b22154794a

توضیحات: در این قسمت به غیر از متغیرهای قبلی ما دو متغیر جدید recordRadius که برای اندازه دایره و مربع قرمز و recordRect برای مختصات مربع قرمز داریم. من در اینجا مقدار recordRadius رو برابر با یک چهارم دایره بزرگ قرار دادم و با کم و زیاد کردن recordRadius از مختصات مرکز، ابعاد recordRect رو نیز مشخص کردم. recordPaint هم که برای مشخص کردن رنگ قرمز استفاده شده.

حالا دوباره نوبت نقاشی روی بوم رسیده.

https://gist.github.com/2hamed/cb5fcb2580b410163ed1f5b0bb024069


مرحله ۶: در حال بارگزاری

برای حالت در حال بارگزاری یا Loading ما به ابزار جدیدی به نام Arc (قوس) احتیاج داریم. چون با توجه به درصد پیشرفت (progress) ممکنه لازم باشه فقط مقدار کمی از دایره بیرونی کشیده بشه. خوشبختانه کشیدن یک قوس بسیار راحته. یک قوس هم مثل یک مربع نیاز به یک آبجکت RectF داره که مشخص کنه در کجا این قوس کشیده بشه. پر واضحه که ابعاد این مربع، در متود onMeasure مشخص میشه.

https://gist.github.com/2hamed/d564b4c1dd4f83019590f9cbd96348f4

و سپس نقاشی...

https://gist.github.com/2hamed/e9cf81b00cbf69b87faad2d8242bf07b

توضیحات: برای کشیدن یک قوس ما علاوه بر داشتن RectF نیاز به زاویه هم داریم که خیلی راحت مقدار progress رو تبدیل به کسر ۱۰۰ کرده و ضربدر 360 درجه می‌کنیم. مثلا با داشتن مقدار ۵۰ برای progress ابتدا ۵۰ رو تقسیم بر ۱۰۰ می‌کنیم که به عدد ۰.۵ می‌رسیم و سپس با ضرب در ۳۶۰ به زاویه ۱۸۰ میرسیم.


مرحله ۷: جمع بندی و تمیز کاری

نکته دیگری که لازمه بهش اشاره کنیم به روزرسانی View در مرحله‌ای بعد از ایجاد اون هست. برای مثال ممکنه بخوایم از مود آماده به مود در حال ضبط بریم یا اینکه بخوایم درصد پیشرفت رو به روز کنیم. برای اینکه ما به ویو خبر بدیم که مقادیر تغییر کرده و نیازه که از ابتدا مرحله draw انجام بشه می‌تونیم از متود invalidate() استفاده کنیم. به این صورت که هر زمان هر کدوم از مقادیر ذکر شده تغییر کرد ما invalidate رو صدا بزنیم. به این صورت:

https://gist.github.com/2hamed/976c8e5df7d52f215cb3b103e7838e5e


یه مورد دیگه اینکه ما برای تغییر واحد dp به px از فانکشن dpToPx استفاده کردیم که تعریفش رو در ادامه میبینید.

fun dpToPx(dp: Int) = (dp * resources.displayMetrics.density).toInt()

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

برای اون دسته از برنامه‌نویسان علاقه مند هم به عنوان تمرین می‌تونید خودتون قسمت بعدی که نمایش درصد پیشرفت داخل ویو هست رو انجام بدین.

همونطور که می‌بینید در اکثر مواقع ساختن یک CustomView خلاصه میشه به استفاده و ترکیب همین ابزارهای ساده که دونستشون کمک می‌کنه کدهای بهتری و در نتیجه پروژه‌های بهتری رو بتونیم ایجاد کنیم.

مثل همیشه تمام سورس این آموزش در مخزن گیت‌هاب زیر موجود است.

https://github.com/2hamed/RecordButton