کار با متریال دیزاین در برنامه نویسی اندروید — بخش اول

۳۳۱ بازدید
آخرین به‌روزرسانی: ۲۷ تیر ۱۴۰۲
زمان مطالعه: ۹ دقیقه
کار با متریال دیزاین در برنامه نویسی اندروید — بخش اول

متریال دیزاین (Material Design) یک زبان طراحی است که با خود امکانات جذابی برای طراحی یک رابط کاربری زیبا آورده است. در این آموزش از ریشه به بررسی متریال دیزاین می‌پردازیم تا ببینیم که اصلا متریال دیزاین چیست؟

گوگل، متریال دیزاین را این‌گونه تعریف می‌کند:

"متریال دیزاین امکان طراحی رابط کاربری با گرافیک خیره کننده و انیمیشن‌های جذاب را ارائه می‌دهد تا بتوانید به کمک آن یک تجربه بصری و زیبا خلق کنید."

در این آموزش، متریال دیزاین را در یک اپلیکیشن به نام «Travel Wishlist» (به معنای اهداف و خواسته‌های سفر) پیاده‌سازی می‌کنیم. در هنگام ساخت این اپلیکیشن، موضوعات زیر را فرا خواهید گرفت:

  • پیاده‌سازی یک قالب نمایش متریال
  • ساخت «view»های پویا به کمک «RecyclerView» و «CardView»
  • استفاده از API پالت رنگی برای ساخت طرح‌های رنگی جهت استفاده در متون و ‌زمینه viewها
  • ساخت انیمیشن‌های دلپذیر توسط APIهای انیمیشن اندروید

در این آموزش فرض شده‌است که شما آشنایی پایه با برنامه‌نویسی اندروید، از جمله «Kotlin»، «XML»، «Android Studio» و «Gradle» را دارید. اگر کاملا در زمینه برنامه‌نویسی اندروید تازه کار هستید، توصیه می‌کنیم ابتدا «آموزش برنامه‌نویسی اندروید (Android) - مقدماتی» را مشاهده کنید و کمی اطلاعات پایه راجع به Kotlin نیز کسب کنید.

برای اینکه بتوانید از این آموزش نهایت استفاده را ببرید، باید «Android Studio 3.0 Beta 2» و یا نسخه جدیدتر، و «Kotlin 1.1.4-2» یا نسخه جدیدتر را نصب کنید. با این مقدمه، آموزش را شروع می‌کنیم.

شروع کار با متریال دیزاین

ابتدا فایل شروع پروژه را از این لینک(+) دانلود کنید و سپس اندروید استودیو را راه اندازی کنید.

برای وارد کردن فایل شروع پروژه، گزینه «Open and existing Android Studio project» را از صفحه خوش‌آمد گویی انتخاب کنید:

صفحه‌ی خوش‌آمد گویی اندروید استودیو

سپس فایل دانلود شده پروژه را انتخاب، و گزینه «Open» را بزنید.

انتخاب پروژه

اپلیکیشن «Travel Wishlist» قرار است یک نرم‌افزار خیلی ساده باشد. در این اپلیکیشن کاربران می‌توانند تصاویری از نقاط مختلف دنیا را ببینند و برای هر کدام یادداشت‌هایی اضافه کنند که در آنجا چه کاری می‌خواهند بکنند، یا از چه مکانی می‌خواهند دیدن کنند. پروژه را ساخته و اجرا (Build and Run) کنید. باید یک صفحه‌ با یک طراحی خیلی مقدماتی را ببینید:

پروژه‌ی آغازین

فعلا دنیای ما خالی است. در ادامه اجزای متریالی از جمله viewهای پویا، طرح‌های رنگی و انیمیشن‌ها را اضافه می‌کنیم که زیبایی حقیقی تصاویرتان را به نمایش بگذارند. فایل «build.gradle» را برای تغییر در ماژول اپلیکیشن باز کنید و «RecyclerView»، «CardView»، «Palette» و «Picasso» را به کتابخانه‌های مورد استفاده خود (dependencies) اضافه کنید:

1dependencies {
2implementation fileTree(dir: 'libs', include: ['*.jar'])
3implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
4implementation 'com.android.support:appcompat-v7:26.0.1'
5implementation 'com.android.support:recyclerview-v7:26.0.1'
6implementation 'com.android.support:cardview-v7:26.0.1'
7implementation 'com.android.support:palette-v7:26.0.1'
8implementation 'com.squareup.picasso:picasso:2.5.2'
9testImplementation 'junit:junit:4.12'
10androidTestImplementation 'com.android.support.test:runner:1.0.0'
11androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.0'
12}

در اینجا شما دارید ابزارهایی را که در طول این آموزش از آن‌ها استفاده می‌کنید را مشخص می‌کنید. چند مورد اول، APIهای ارائه‌شده توسط گوگل هستند، ولی گزینه آخر یعنی «Picasso» یک کتابخانه فوق‌العاده برای دانلود و دریافت تصاویر است که توسط وبسایت «Square» ارائه شده‌است. حالا که ابزارهای مورد استفاده خودمان را تعریف کرده‌ایم، زمانش رسیده است که متریال دیزاین را وارد اپلیکیشن کنیم!

آماده‌سازی قالب نمایش

قبل از اینکه کار دیگری را شروع کنیم، ابتدا باید قالب نمایش اپلیکیشن را آماده کنیم. فایل «style.xml» را که در پوشه «res/values» قرار دارد، باز کنید.

قالب نمایشی که به طور پیشفرض تعریف شده‌است، «Theme.AppCompat.Light.DarkActionBar» است. موارد زیر را در داخل تگ «theme» اضافه کنید:

1<item name="android:navigationBarColor">@color/primary_dark</item>
2<item name="android:displayOptions">disableHome</item>

با اضافه کردن این گزینه‌ها، اندروید به طور خودکار مقدار «colorPrimary» را به «Action bar»، مقدار «colorPrimaryDark» را به «status bar» و مقدار «colorAccent» را به ویجت‌های (widget) رابط کاربری نظیر «text field»ها و «checkbox»ها می‌دهد. همچنین در کد بالا رنگ «navigation bar» نیز تغییر می‌کند. برای «android:displayOptions» مقدار «disableHome» را قرار داده‌ایم تا رنگ آن را با ظاهر خود اپلیکیشن تطبیق دهد. نرم‌افزار را ساخته و اجرا کنید. طرح رنگی جدید را در اپلیکیشن می‌بینید.

تنظیمات اولیه‌ی تم

تغییرات خیلی جزئی هستند، ولی همانند هر سفری، گسترش دادن این رابط کاربری نیز با برداشتن یک قدم آغاز می‌شود.

استفاده از RecyclerView و CardView

برای اینکه بتوانید به کاربران خود پنجره‌ای ارائه دهید که از طریق آن بتوانند هرکجا دوست دارند سفر کنند، باید یک «view» بسازید. برای اینکار می‌توانید به جای «ListView» که قبلا استفاده می‌شد، از «RecyclerView» استفاده کنید.

گوگل، RecyclerView را اینگونه تعریف می‌کند:

«یک view انعطاف‌پذیر است که می‌تواند پنجره‌ای محدود را برای مجموعه داده‌های بسیار ارائه دهد.»

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

پیاده‌سازی یک Recycler View در XML

ابتدا فایل «activity_main.xml» را باز کنید و قطعه کد زیر را در تگ «LinearLayout» اضافه کنید:

1<android.support.v7.widget.RecyclerView
2android:id="@+id/list"
3android:layout_width="match_parent"
4android:layout_height="match_parent"
5android:background="@color/light_gray"/>

در این بخش ما یک RecyclerView را به قالب «Activity» خود اضافه کردیم و آن را به گونه‌ای تنظیم کردیم که تمام view خود را پر کند.

ساخت یک Recycler View و اعمال آن به یک Layout Manager

قبل از اینکه کدی به زبان «Kotlin» بنویسید، ابتدا اندروید استودیو را به گونه‌ای تنظیم کنید که به طور خودکار تعاریف لازم را اضافه کند تا دیگر نیاز نداشته باشید هر بار به طور دستی آن‌ها را وارد کنید. برای این کار، از منوهای اندروید استودیو به مسیر «Preferences\Editor\General\Auto Import» بروید و گزینه «Add unambiguous imports on the fly» را انتخاب کنید.

فایل «MainActivity.kt» را باز کنید و در بالای کلاس، کد زیر را اضافه کنید:

1lateinit private var staggeredLayoutManager: StaggeredGridLayoutManager

در این کد شما فقط دارید یک خصوصیت تعریف می‌کنید که به «LayoutManager» اشاره دارد. سپس، کد زیر را به آخر تابع «()onCreate» اضافه کنید:

1staggeredLayoutManager = StaggeredGridLayoutManager(1, StaggeredGridLayoutManager.VERTICAL)
2list.layoutManager = staggeredLayoutManager

در کد بالا، شما «layout manager» که مسئول مدیریت «RecyclerView» است را برابر با «StaggeredGridLayoutManager» قرار می‌دهید که از آن برای ساخت دو جدول عمودی استفاده می‌کنیم. در این کد شما مقدار یک را برای تعداد خطوط جدول، و مقدار «StaggeredGridLayoutManager.Vertical» را برای جهت قرارگیری آن مشخص می‌کنید. با دادن مقدار یک برای تعداد خطوط جدول، درواقع داریم یک لیست می‌سازیم که بزودی مشاهده می‌کنید. بعدا یک جدول مشخص با دو ستون اضافه می‌کنیم. توجه داشته باشید که دارید از «Kotlin Android Extensions» برای پیدا کردن لیست استفاده می‌کنید، در نتیجه نیازی به صدا کردن «()findViewById» نیست. بررسی کنید که کد زیر در قسمت importهای کلاستان وجود داشته باشد. این کد باید به طور خودکار، هنگام نوشتن کلمه «list» اضافه شده باشد:

1import kotlinx.android.synthetic.main.activity_main.*

ساخت سطرها و سلول‌ها توسط CardView

یک «CardView» همانند یک پس‌زمینه ثابت برای view می‌ماند که با گوشه‌های گرد و سایه‌ها تکمیل می‌شود. از «CardView» برای پیاده‌سازی سطرها و سلول‌های قالب «RecyclerView» استفاده می‌کنیم. به طور پیشفرض، یک «CardView» امکانات «FrameLayout» را ارث‌بری می‌کند، به همین دلیل امکان داشتن Viewهای زیرشاخه و فرزند را نیز دارد.

از آدرس «res\layout» یک فایل «Layout resource» جدید بسازید و آن را «row_places.xml» نام‌گذاری کنید. از گزینه «OK» برای ساخت آن استفاده کنید.

new resource file

برای اینکه به قالب مد نظرتان برسید، تمام محتوای این فایل را با کد زیر جایگزین کنید:

1<?xml version="1.0" encoding="utf-8"?>
2<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
3xmlns:card_view="http://schemas.android.com/apk/res-auto"
4android:id="@+id/placeCard"
5android:layout_width="match_parent"
6android:layout_height="wrap_content"
7android:layout_margin="8dp"
8card_view:cardCornerRadius="@dimen/card_corner_radius"
9card_view:cardElevation="@dimen/card_elevation">
10
11<ImageView
12android:id="@+id/placeImage"
13android:layout_width="match_parent"
14android:layout_height="200dp"
15android:scaleType="centerCrop" />
16
17<!-- Used for the ripple effect on touch -->
18<LinearLayout
19android:id="@+id/placeHolder"
20android:layout_width="match_parent"
21android:layout_height="match_parent"
22android:background="?android:selectableItemBackground"
23android:orientation="horizontal" />
24
25<LinearLayout
26android:id="@+id/placeNameHolder"
27android:layout_width="match_parent"
28android:layout_height="45dp"
29android:layout_gravity="bottom"
30android:orientation="horizontal">
31
32<TextView
33android:id="@+id/placeName"
34android:layout_width="match_parent"
35android:layout_height="wrap_content"
36android:layout_gravity="center_vertical"
37android:gravity="start"
38android:paddingStart="10dp"
39android:paddingEnd="10dp"
40android:textAppearance="?android:attr/textAppearanceLarge"
41android:textColor="@android:color/white" />
42
43</LinearLayout>
44
45</android.support.v7.widget.CardView>

با اضافه کردن کد «xmlns:card_view=”http://schemas.android.com/apk/res-auto”» به دستورات، شما در واقع دارید خصوصیاتی نظیر «card_view:cardCornerRadius» و «card_view:cardElevation» را که مسئولیت طراحی ظاهری متریال دیزاین را بر عهده دارند، تعریف می‌کنید.

توجه کنید که در بخش «mainHolder» مقدار «?android:selectableItemBackground» را برای پس‌زمینه انتخاب کرده‌ایم. این کد انیمیشن موج را هنگامی که کاربر بر روی یک سلول کلیک می‌کند، فعال می‌کند. این امکان در بسیاری از اپلیکیشن‌های اندروید در دسترس است و به زودی در اپلیکیشن خودتان نیز آن را خواهید دید.

پیاده‌سازی یک آداپتور برای RecyclerView

برای اینکه اطلاعات را در RecyclerView مدیریت کنید، به یک آداپتور نیاز دارید. در پوشه «main/java»، برروی گزینه «package com.raywenderlich.android.travelwishlist » راست کلیک کنید و از منوی «new» گزینه «Kotline file\Class» را انتخاب کنید.

اسم این کلاس را «TraveltListAdapter» بگذارید. کد زیر را به کلاس اضافه کنید. توجه داشته باشید که کدهای مربوط به پکیج‌ها را از بالای فایل حذف نکنید:

1// 1
2class TravelListAdapter(private var context: Context) : RecyclerView.Adapter<TravelListAdapter.ViewHolder>() {
3
4override fun getItemCount(): Int {
5}
6
7override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
8}
9
10override fun onBindViewHolder(holder: ViewHolder?, position: Int) {
11}
12
13// 2
14inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
15}
16}

در قطعه کد بالا چند اتفاق می‌افتد:

  1. کلاس «TravelListAdapter» را به گونه‌ای ساختیم که از «Recycler.Adapter» ارث‌بری کند که بعدا بتوانیم متدهای مورد نیاز را «override» کنیم. همچنین کلاس سازنده (constructor) را با یک «Context» ساختیم که بعدا در هنگام ساخت شیء از روی «TravelListAdapter» در «MainActivity»، آن را ارسال کنیم. این کار را جلوتر در این آموزش انجام خواهیم داد.
  2. یک کلاس «ViewHolder» ساختیم. درست است که استفاده از «ViewHolder» در «ListView» اختیاری است، ولی در «RecyclerView» مجبور به استفاده از آن هستید. اینکار با دوری کردن از فراخوانی «()findViewById» برای هر سلول، عملکرد و Scroll کردن را بهبود می‌بخشد.

متدهای «RecyclerView.Adapter» را در کلاس «TravelListAdapter» همانند زیر بروزرسانی کنید:

1// 1
2override fun getItemCount() = PlaceData.placeList().size
3
4// 2
5override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
6val itemView = LayoutInflater.from(parent.context).inflate(R.layout.row_places, parent, false)
7return ViewHolder(itemView)
8}
9
10// 3
11override fun onBindViewHolder(holder: ViewHolder, position: Int) {
12val place = PlaceData.placeList()[position]
13holder.itemView.placeName.text = place.name
14Picasso.with(context).load(place.getImageResourceId(context)).into(holder.itemView.placeImage)
15}

توضیح این کدها به شرح زیر است:

  1. «()getItemCount» تعداد آیتم‌هایی که در آرایه شما است را برمی‌گرداند. در این کد تعداد آرایه برابر است با مقداری که در «PlaceData.PlaceList().size» قرار دارد.
  2. تابع «onCreateViewHolder(…)» یک شیء جدید از «ViewHolder» با مقدار «row_places» خالی را برمی‌گرداند.
  3. «onBindViewHolder(…)» شیء «Place» را به عناصر رابط کاربری در «ViewHolder» متصل می‌کند. از «Picasso» برای ذخیره‌سازی تصاویر در لیست استفاده می‌کنیم.

یک مقدار جدید در «MainActivity» اضافه کنید که به آداپتور اشاره داشته باشد:

1lateinit private var adapter: TravelListAdapter

سپس یک شیء جدید از روی آداپتورتان بسازید و آنرا به «RecyclerView» در آخر «()onCreate»، قبل از تنظیم کردن «layout manager»، پاس دهید.

1adapter = TravelListAdapter(this)
2list.adapter = adapter

حالا نرم‌افزار را بسازید و اجرا کنید. یک سری لیست از مناطق گوناگون را خواهید دید.List preview

 

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

پیاده‌سازی رابط کلیکی (Click Interface) برای هر سلول

برخلاف ListView، RecyclerView به طور پیشفرض رابط «onItemClick» را ندارد و باید یک رابط در آداپتور برای آن تعریف کنید. در کلاس «TravelListAdapter» یک خصوصیت با ارث‌بری از «onItemClickListener» بسازید.

کد زیر را به بالای «TravelListAdapter» اضافه کنید:

1lateinit var itemClickListener: OnItemClickListener

حالا با اضافه کردن این رابط در یک «inner class» در «ViewHolder»، «View.OnClickListener» را پیاده‌سازی کنید:

1inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), View.OnClickListener {

سپس متد زیر را درون کلاس «inner ViewHolder» اضافه کنید:

1override fun onClick(view: View) {
2
3}

با اضافه کردن کد زیر در بالای «ViewHolder»، این دو را به هم متصل کنید:

1init {
2itemView.placeHolder.setOnClickListener(this)
3}

در کد بالا ما مقدار «setOnClickListener» را برای «placeHolder» فعال‌سازی، و متد «onClick» را نیز «override» کردیم.

برای اینکه رابط «onClick» را در «RecyclerView» پیاده‌سازی کنید، چند کار دیگر نیز باید انجام دهید. ابتدا، پس از تعریف کلاس «inner ViewHolder»، کد زیر را اضافه کنید:

1interface OnItemClickListener {
2fun onItemClick(view: View, position: Int)
3}

سپس، متد تنظیم کننده «sette onClickListener» را به «TravelListAdapter» اضافه کنید:

1fun setOnItemClickListener(itemClickListener: OnItemClickListener) {
2this.itemClickListener = itemClickListener
3}

حالا آن را به متد خالی «onClick» که درون کلاس «inner ViewHolder» قرار داشت اضافه کنید:

1override fun onClick(view: View) = itemClickListener.onItemClick(itemView, adapterPosition)

در کلاس «MainActivity» یک نمونه از «OnItemClickListener» در بالای «()onCreate» بسازید:

1private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
2override fun onItemClick(view: View, position: Int) {
3Toast.makeText(this@MainActivity, "Clicked " + position, Toast.LENGTH_SHORT).show()
4}
5}

در نهایت، با اضافه کردن کد زیر در پایین «()onCreate»، درست جایی که آداپتور را تنظیم کردید، شنونده (listener) خود را به آداپتور وصل کنید:

1adapter.setOnItemClickListener(onItemClickListener)

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

click interface

رفتن از لیست به جدول و بالعکس

«StaggeredLayoutManager» به شما اجازه می‌دهد به قالب خودتان امکان تطبیق‌پذیری اضافه کنید. برای اینکه لیستی که اکنون دارید را به یک جدول دو ستونه فشرده‌تر تبدیل کنید، تنها کافی‌است که مقدار «spanCount» را از «StaggeredLayoutManager» در «MainActivity» تغییر دهید.

در تابع «()toggle»، مقدار زیر را در بالای «()showGridView» اضافه کنید:

1staggeredLayoutManager.spanCount = 2

سپس مقدار زیر را نیز به بالای «()showListView» اضافه کنید:

1staggeredLayoutManager.spanCount = 1

در این بخش شما فقط دارید مقدار «spanCount» را بین یک و دو جابه‌جا می‌کنید که در نتیجه منجر به نمایش تک یا دو ستونه اپلیکیشن می‌شود. اپلیکیشن را بسازید و اجرا کنید، سپس از دکمه‌ای که در «ActionBar» قرار دارد برای تغییر بین حالت لیست و جدولی استفاده کنید.

list to grid

استفاده از API پالت رنگی در لیست

حالا می‌توانیم چند امکان جذاب متریال دیزاین را در اپلیکیشن اضافه کنیم. با API پالت رنگ شروع می‌کنیم. برگردید به «TravelListAdapter» که در آن یک رنگ پس‌زمینه برای «PlaceNameHolder» مشخص کردیم. می‌خواهیم رنگ آن را بر اساس رنگ تصویر به صورت پویا مشخص کنیم.

کد زیر را به آخر «(...)onBindViewHolder» اضافه کنید:

1val photo = BitmapFactory.decodeResource(context.resources, place.getImageResourceId(context))
2Palette.from(photo).generate { palette ->
3val bgColor = palette.getMutedColor(ContextCompat.getColor(context, android.R.color.black))
4holder.itemView.placeNameHolder.setBackgroundColor(bgColor)
5}

متد «(...)generate» یک پالت رنگی را در پس‌زمینه می‌سازد و در هنگام ساخت مقدار لامبدا (λ) را در برمی‌گیرد. در این بخش شما می‌توانید به این پالت رنگی دسترسی داشته باشید و به کمک آن رنگ پس‌زمینه «holder.itemView.placeNameHolder» را انتخاب کنید. اگر رنگی در آن وجود نداشته باشد، این متد یک رنگ پیشفرض را برمی‌گرداند که در کد ما این رنگ «android.R.color.black» است.

نرم‌افزار را بسازید و اجرا کنید تا عملکرد API پالت رنگی را ببینید.

List preview

توجه داشته باشید که این API می‌تواند رنگ‌های مختلفی را برگرداند. می‌توانید به جای «(...)palette.getMutecColor» از palette.getVibrantColor(…)» «palette.getDarkVibrantColor و سایر موارد استفاده کنید و تفاوت‌های آن‌ها را مشاهده کنید.

استفاده از APIهای جدید متریال

در این بخش، با استفاده از «DetailActivity» و قالب آن که «activity_detail» نام دارد، یک سری API جدید متریال دیزاین را وارد برنامه می‌کنیم تا جذابیت بیشتری به آن بدهد.

ابتدا باید ببینیم که «detail view» که در حال حاضر داریم چگونه است. برای اینکار، کد زیر را به «DetailActivity» اضافه کنید:

1fun newIntent(context: Context, position: Int): Intent {
2val intent = Intent(context, DetailActivity::class.java)
3intent.putExtra(EXTRA_PARAM_ID, position)
4return intent
5}

سپس وارد «MainActivity» شوید و «Toast» را که در «(...)onItemClick» قرار دارد با کد زیر جایگزین کنید:

1startActivity(DetailActivity.newIntent(this@MainActivity, position))

می‌توانید موقعیت آبجکت‌ها را توسط یک «intent» ارسال کنید تا «DetailActivity» بتواند اطلاعات مربوطه را دریافت، و قالب صفحه را براساس آن تنظیم کند.

اپلیکیشن را بسازید و اجرا کنید.detail preview

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

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

اگر مطلب بالا برای شما مفید بوده است و تمایل دارید در رابطه با مباحث آن مطالعه بیشتری داشته باشید، شاید آموزش‌هایی که در زیر آمده‌اند برای‌تان مفید باشند.

#

بر اساس رای ۵ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
raywenderlich
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *