اجزای نمای سفارشی را ایجاد کنید

روش نوشتن را امتحان کنید
Jetpack Compose ابزار رابط کاربری پیشنهادی برای اندروید است. یاد بگیرید که چگونه با طرح‌بندی‌ها در Compose کار کنید.

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

فهرستی ناقص از ویجت‌های موجود شامل Button ، TextView ، EditText ، ListView ، CheckBox ، RadioButton ، Gallery ، Spinner و موارد خاص‌تر AutoCompleteTextView ، ImageSwitcher و TextSwitcher می‌شود.

از جمله طرح‌بندی‌های موجود می‌توان به LinearLayout ، FrameLayout ، RelativeLayout و موارد دیگر اشاره کرد. برای مثال‌های بیشتر، به طرح‌بندی‌های رایج مراجعه کنید.

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

ایجاد زیرکلاس‌های View خودتان، کنترل دقیقی بر ظاهر و عملکرد یک عنصر صفحه نمایش به شما می‌دهد. برای اینکه ایده‌ای از کنترلی که با Viewهای سفارشی به دست می‌آورید، ارائه دهید، در اینجا چند مثال از کارهایی که می‌توانید با آنها انجام دهید، آورده شده است:

  • شما می‌توانید یک نوع View کاملاً سفارشی ایجاد کنید - برای مثال، یک دکمه "کنترل صدا"، که با استفاده از گرافیک دوبعدی رندر شده و شبیه یک کنترل الکترونیکی آنالوگ است.
  • شما می‌توانید گروهی از کامپوننت‌های View را در یک کامپوننت واحد جدید ترکیب کنید، مثلاً برای ساختن چیزی شبیه به یک کادر ترکیبی (ترکیبی از لیست بازشو و فیلد متنی ورودی آزاد)، یک کنترل انتخابگر دو قسمتی (یک پنل چپ و راست با لیستی در هر کدام که می‌توانید مشخص کنید کدام آیتم در کدام لیست باشد) و غیره.
  • شما می‌توانید نحوه‌ی رندر شدن یک کامپوننت EditText روی صفحه را تغییر دهید. برنامه‌ی نمونه‌ی NotePad از این قابلیت برای ایجاد یک صفحه‌ی Notepad خط‌دار به خوبی استفاده می‌کند.
  • شما می‌توانید رویدادهای دیگر - مانند فشردن کلیدها - را ضبط کرده و آنها را به روشی سفارشی، مانند یک بازی، مدیریت کنید.

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

رویکرد اساسی

در اینجا یک مرور کلی سطح بالا از آنچه برای ایجاد اجزای View خود باید بدانید، آورده شده است:

  1. یک کلاس یا زیرکلاس View موجود را با کلاس خودتان گسترش دهید.
  2. برخی از متدهای کلاس بالا را override کنید. متدهای کلاس بالا که باید override شوند با on شروع می‌شوند - برای مثال، onDraw() ، onMeasure() و onKeyDown() . این مشابه رویدادهای on در Activity یا ListActivity است که برای چرخه عمر و سایر قلاب‌های عملکردی override می‌کنید.
  3. از کلاس الحاقی جدید خود استفاده کنید. پس از تکمیل، می‌توانید از کلاس الحاقی جدید خود به جای نمایی که بر اساس آن ایجاد شده بود، استفاده کنید.

اجزای کاملاً سفارشی

شما می‌توانید اجزای گرافیکی کاملاً سفارشی ایجاد کنید که به هر شکلی که می‌خواهید ظاهر شوند. شاید یک VU متر گرافیکی بخواهید که شبیه یک گیج آنالوگ قدیمی باشد، یا یک نمای متنی برای آواز خواندن که در آن یک توپ جهنده در امتداد کلمات حرکت می‌کند، در حالی که شما با دستگاه کارائوکه آواز می‌خوانید. ممکن است چیزی بخواهید که اجزای داخلی نتوانند انجام دهند، مهم نیست که چگونه آنها را ترکیب کنید.

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

برای ایجاد یک کامپوننت کاملاً سفارشی، موارد زیر را در نظر بگیرید:

  • عمومی‌ترین نمایی که می‌توانید بسط دهید View است، بنابراین معمولاً برای ایجاد سوپر کامپوننت جدید خود، کار را با بسط دادن آن شروع می‌کنید.
  • شما می‌توانید یک سازنده ارائه دهید که می‌تواند ویژگی‌ها و پارامترها را از XML دریافت کند و شما می‌توانید از این ویژگی‌ها و پارامترهای خودتان، مانند رنگ و محدوده‌ی VU meter یا عرض و میرایی سوزن، استفاده کنید.
  • احتمالاً می‌خواهید شنونده‌های رویداد، دسترسی‌دهنده‌های ویژگی و اصلاح‌کننده‌های خودتان و همچنین رفتارهای پیچیده‌تر را در کلاس کامپوننت خود ایجاد کنید.
  • شما تقریباً مطمئناً می‌خواهید onMeasure() لغو کنید و همچنین اگر می‌خواهید کامپوننت چیزی را نشان دهد، احتمالاً نیاز به لغو onDraw() نیز خواهید داشت. در حالی که هر دو رفتار پیش‌فرض دارند، onDraw() پیش‌فرض هیچ کاری انجام نمی‌دهد و onMeasure() پیش‌فرض همیشه اندازه ۱۰۰x۱۰۰ را تعیین می‌کند، که احتمالاً شما آن را نمی‌خواهید.
  • همچنین می‌توانید در صورت نیاز، on دیگر را override کنید.

توابع onDraw() و onMeasure() را بسط دهید

متد onDraw() یک Canvas ارائه می‌دهد که می‌توانید هر چیزی را که می‌خواهید روی آن پیاده‌سازی کنید: گرافیک‌های دوبعدی، سایر اجزای استاندارد یا سفارشی، متن استایل‌دار یا هر چیز دیگری که می‌توانید به آن فکر کنید.

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

در سطح بالا، پیاده‌سازی onMeasure() چیزی شبیه به این است:

  • متد onMeasure() که بازنویسی شده است، با مشخصات عرض و ارتفاع فراخوانی می‌شود که به عنوان الزاماتی برای محدودیت‌های اندازه‌گیری‌های عرض و ارتفاعی که شما تولید می‌کنید، در نظر گرفته می‌شوند. پارامترهای widthMeasureSpec و heightMeasureSpec هر دو کدهای صحیحی هستند که نشان‌دهنده ابعاد می‌باشند. مرجع کامل به نوع محدودیت‌هایی که این مشخصات می‌توانند نیاز داشته باشند را می‌توانید در مستندات مرجع زیر View.onMeasure(int, int) بیابید. این مستندات مرجع همچنین کل عملیات اندازه‌گیری را توضیح می‌دهد.
  • متد onMeasure() کامپوننت شما، عرض و ارتفاع اندازه‌گیری شده را محاسبه می‌کند که برای رندر کردن کامپوننت مورد نیاز هستند. این کامپوننت باید سعی کند در محدوده مشخصات ارسالی باقی بماند، اگرچه می‌تواند از آنها فراتر رود. در این حالت، والد می‌تواند انتخاب کند که چه کاری انجام دهد، از جمله برش، پیمایش، ایجاد یک استثنا یا درخواست از onMeasure() برای تلاش مجدد، شاید با مشخصات اندازه‌گیری متفاوت.
  • وقتی عرض و ارتفاع محاسبه شدند، متد setMeasuredDimension(int width, int height) را به همراه اندازه‌های محاسبه‌شده فراخوانی کنید. عدم انجام این کار منجر به یک خطا می‌شود.

در اینجا خلاصه‌ای از سایر متدهای استانداردی که این چارچوب برای نماها فراخوانی می‌کند، آورده شده است:

دسته بندی روش‌ها توضیحات
خلقت سازنده‌ها یک فرم از سازنده وجود دارد که هنگام ایجاد نما از کد فراخوانی می‌شود و یک فرم که هنگام پر کردن نما از یک فایل طرح‌بندی فراخوانی می‌شود. فرم دوم ویژگی‌های تعریف شده در فایل طرح‌بندی را تجزیه و اعمال می‌کند.
onFinishInflate() بعد از یک view فراخوانی می‌شود و تمام فرزندان آن از XML مشتق می‌شوند.
طرح بندی onMeasure(int, int) برای تعیین الزامات اندازه برای این نما و تمام فرزندانش فراخوانی می‌شود.
onLayout(boolean, int, int, int, int) زمانی فراخوانی می‌شود که این view باید به همه فرزندانش اندازه و موقعیت اختصاص دهد.
onSizeChanged(int, int, int, int) زمانی فراخوانی می‌شود که اندازه این نما تغییر کند.
طراحی onDraw(Canvas) زمانی فراخوانی می‌شود که نما باید محتوای خود را رندر کند.
پردازش رویداد onKeyDown(int, KeyEvent) زمانی فراخوانی می‌شود که یک رویداد key down رخ دهد.
onKeyUp(int, KeyEvent) زمانی فراخوانی می‌شود که یک رویداد key up رخ دهد.
onTrackballEvent(MotionEvent) زمانی فراخوانی می‌شود که رویداد حرکت ترک‌بال رخ دهد.
onTouchEvent(MotionEvent) زمانی فراخوانی می‌شود که یک رویداد حرکت صفحه لمسی رخ دهد.
تمرکز onFocusChanged(boolean, int, Rect) زمانی فراخوانی می‌شود که نما فوکوس را به دست می‌آورد یا از دست می‌دهد.
onWindowFocusChanged(boolean) زمانی فراخوانی می‌شود که پنجره‌ی حاوی نما، فوکوس را به دست آورد یا از دست بدهد.
پیوست کردن onAttachedToWindow() زمانی فراخوانی می‌شود که نما به یک پنجره متصل باشد.
onDetachedFromWindow() زمانی فراخوانی می‌شود که نما از پنجره‌اش جدا شود.
onWindowVisibilityChanged(int) زمانی فراخوانی می‌شود که میزان دیده شدن پنجره‌ای که شامل نما است تغییر کند.

کنترل‌های مرکب

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

در اندروید، دو نمای دیگر برای انجام این کار به راحتی در دسترس هستند: Spinner و AutoCompleteTextView . صرف نظر از این، این مفهوم برای یک کادر ترکیبی مثال خوبی است.

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

  • درست مانند یک Activity ، از رویکرد اعلانی (مبتنی بر XML) برای ایجاد اجزای موجود استفاده کنید یا آنها را به صورت برنامه‌ای از کد خود تودرتو کنید. نقطه شروع معمول، نوعی Layout است، بنابراین کلاسی ایجاد کنید که از یک Layout ارث‌بری کند. در مورد یک جعبه ترکیبی، می‌توانید از LinearLayout با جهت افقی استفاده کنید. می‌توانید طرح‌بندی‌های دیگری را درون آن تودرتو کنید، بنابراین کامپوننت مرکب می‌تواند به طور دلخواه پیچیده و ساختاریافته باشد.
  • در سازنده‌ی کلاس جدید، هر پارامتری که کلاس پایه انتظار دارد را بگیرید و ابتدا آنها را به سازنده‌ی کلاس پایه منتقل کنید. سپس، می‌توانید نماهای دیگر را برای استفاده در کامپوننت جدید خود تنظیم کنید. اینجا جایی است که فیلد EditText و لیست بازشو را ایجاد می‌کنید. می‌توانید ویژگی‌ها و پارامترهای خود را در XML معرفی کنید که سازنده‌ی شما بتواند آنها را دریافت و استفاده کند.
  • به صورت اختیاری، می‌توانید برای رویدادهایی که نماهای محصور شده شما ممکن است ایجاد کنند، شنونده‌هایی ایجاد کنید. به عنوان مثال، یک متد شنونده برای شنونده کلیک آیتم لیست وجود دارد که در صورت انتخاب یک لیست، محتوای EditText را به‌روزرسانی می‌کند.
  • به صورت اختیاری، می‌توانید ویژگی‌های خودتان را با accessorها و modifierها ایجاد کنید. برای مثال، اجازه دهید مقدار EditText در ابتدا در کامپوننت تنظیم شود و در صورت نیاز، محتوای آن را جستجو کنید.
  • به صورت اختیاری، onDraw() و onMeasure() را نادیده بگیرید. این کار معمولاً هنگام گسترش یک Layout ضروری نیست، زیرا طرح‌بندی دارای رفتار پیش‌فرض است که احتمالاً به خوبی کار می‌کند.
  • به صورت اختیاری، متدهای دیگر on مانند onKeyDown() را override کنید، برای مثال برای انتخاب مقادیر پیش‌فرض خاص از لیست بازشو یک کادر ترکیبی هنگام ضربه زدن به یک کلید خاص.

استفاده از یک Layout به عنوان مبنای یک کنترل سفارشی مزایایی دارد، از جمله موارد زیر:

  • شما می‌توانید طرح‌بندی را با استفاده از فایل‌های XML اعلانی، درست مانند یک صفحه فعالیت، مشخص کنید، یا می‌توانید نماها را به صورت برنامه‌نویسی ایجاد کنید و آنها را از کد خود در طرح‌بندی قرار دهید.
  • متدهای onDraw() و onMeasure() ، به علاوه‌ی اکثر متدهای on دیگر، رفتار مناسبی دارند، بنابراین لازم نیست آنها را override کنید.
  • شما می‌توانید به سرعت نماهای مرکب دلخواه و پیچیده‌ای بسازید و آنها را طوری استفاده کنید که انگار یک کامپوننت واحد هستند.

تغییر نوع نمای موجود

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

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

اگر قبلاً این کار را نکرده‌اید، نمونه NotePad را به اندروید استودیو وارد کنید یا با استفاده از لینک ارائه شده، به منبع آن نگاه کنید. به طور خاص، تعریف LinedEditText را در فایل NoteEditor.java ببینید.

نکاتی که در این فایل باید به آنها توجه کنید:

  1. تعریف

    کلاس با خط زیر تعریف می‌شود:
    public static class LinedEditText extends EditText

    LinedEditText به عنوان یک کلاس داخلی درون اکتیویتی NoteEditor تعریف شده است، اما عمومی است تا بتوان از خارج از کلاس NoteEditor به عنوان NoteEditor.LinedEditText به آن دسترسی داشت.

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

    LinedEditText EditText ارث‌بری می‌کند، که در این مورد همان نمایی است که باید سفارشی‌سازی شود. پس از اتمام کار، کلاس جدید می‌تواند جایگزین یک نمای EditText معمولی شود.

  2. مقداردهی اولیه کلاس

    مثل همیشه، ابتدا super فراخوانی می‌شود. این یک سازنده پیش‌فرض نیست، اما یک سازنده پارامتری است. EditText با این پارامترها هنگام inflate شدن از یک فایل layout XML ایجاد می‌شود. بنابراین، سازنده باید آنها را گرفته و به سازنده superclass نیز منتقل کند.

  3. متدهای بازنویسی شده

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

    برای این نمونه، بازنویسی متد onDraw() به شما امکان می‌دهد خطوط آبی را روی بوم نمای EditText نقاشی کنید. بوم به متد onDraw() بازنویسی‌شده منتقل می‌شود. متد super.onDraw() قبل از پایان متد فراخوانی می‌شود. متد کلاس بالا باید فراخوانی شود. در این حالت، آن را در انتها و پس از نقاشی خطوطی که می‌خواهید بگنجانید، فراخوانی کنید.

  4. کامپوننت سفارشی

    حالا کامپوننت سفارشی خود را دارید، اما چگونه می‌توانید از آن استفاده کنید؟ در مثال NotePad، کامپوننت سفارشی مستقیماً از طرح‌بندی اعلانی استفاده می‌شود، بنابراین به note_editor.xml در پوشه res/layout نگاه کنید:

    <view xmlns:android="http://schemas.android.com/apk/res/android"
        class="com.example.android.notepad.NoteEditor$LinedEditText"
        android:id="@+id/note"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@android:color/transparent"
        android:padding="5dp"
        android:scrollbars="vertical"
        android:fadingEdge="vertical"
        android:gravity="top"
        android:textSize="22sp"
        android:capitalize="sentences"
    />

    کامپوننت سفارشی به عنوان یک نمای عمومی در XML ایجاد می‌شود و کلاس با استفاده از بسته کامل مشخص می‌شود. کلاس داخلی که تعریف می‌کنید با استفاده از نمادگذاری NoteEditor$LinedEditText ارجاع داده می‌شود، که یک روش استاندارد برای ارجاع به کلاس‌های داخلی در زبان برنامه‌نویسی جاوا است.

    اگر کامپوننت نمای سفارشی شما به عنوان یک کلاس داخلی تعریف نشده باشد، می‌توانید کامپوننت نمای را با نام عنصر XML اعلام کنید و ویژگی class را حذف کنید. برای مثال:

    <com.example.android.notepad.LinedEditText
      id="@+id/note"
      ... />

    توجه داشته باشید که کلاس LinedEditText اکنون یک فایل کلاس جداگانه است. وقتی این کلاس در کلاس NoteEditor تو در تو باشد، این تکنیک کار نمی‌کند.

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

ایجاد کامپوننت‌های سفارشی فقط به اندازه نیاز شما پیچیده است.

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