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

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

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

افزودن یک انیمیشن ظاهر شونده (Reveal Animation)

حالا می‌خواهیم که به کاربر این امکان را بدهیم که بتواند در مورد هریک از این مکان‌ها و تصاویر، یادداشت‌هایی را بنویسد. برای اینکار، در «activity_detail.xml» یک «edittext» داریم که به طور پیش‌فرض مخفی است.

وقتی کاربر برروی «FAB» (مخفف FloatingActionButton) کلیک می‌کند، این «edittext» به همراه یک انیمیشن مانند تصویر زیر نمایان می‌شود.

Reveal animation

«DetailActivity» را باز کنید. دو متد هستند که باید در آن پیاده‌سازی کنید:

  • ()revealtEditText
  • ()hideEditText

ابتدا، کدهای زیر را به «()revealEditText» اضافه کنید:

1val cx = view.right - 30
2val cy = view.bottom - 60
3val finalRadius = Math.max(view.width, view.height)
4val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, 0f, finalRadius.toFloat())
5view.visibility = View.VISIBLE
6isEditTextVisible = true
7anim.start()

دو مقدار «int» که داریم، مقادیر x و y موقعیت view را با کمی تغییر جزئی می‌گیرند. این تغییر جزئی به کاربر این تصور را می‌دهد که این انیمیشن دارد از سمت FAB صورت می‌گیرد. پس از آن، یک شعاع به آن داده‌ایم که حالت دایره‌ای شکل را مانند تصویر بالا ایجاد می‌کند. تمامی این مقادیر x، y و شعاع به نمونه انیمیشن ما ارسال می‌شوند. این انیمیشن از «ViewAnimationUtils» استفاده می‌کند که به ما اجازه ساخت این حالت دایره‌ای را می‌دهد.

از آنجایی که «EditText» به طور پیشفرض مخفی است، باید به آن مقدار «VISIBLE» را بدهیم و مقدار «isEditTextVisible» را نیز به «true» تغییر دهیم. در نهایت تابع «()start» را در انیمیشن صدا می‌کنیم. برای اینکه view را از بین ببریم، باید مقادیر زیر را به «()hideEditText» اضافه کنیم:

1val cx = view.right - 30
2val cy = view.bottom - 60
3val initialRadius = view.width
4val anim = ViewAnimationUtils.createCircularReveal(view, cx, cy, initialRadius.toFloat(), 0f)
5anim.addListener(object : AnimatorListenerAdapter() {
6override fun onAnimationEnd(animation: Animator) {
7super.onAnimationEnd(animation)
8view.visibility = View.INVISIBLE
9}
10})
11isEditTextVisible = false
12anim.start()

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

اگر کیبورد در صفحه نمایان شود، باید ابتدا آن را از صفحه خارج کنید تا انیمیشن بدون مشکل اجرا شود. برای اینکار، کد «inputManager.showSoftInput» را که در «DetailActivity» قرار دارد را کامنت کنید، البته فراموش نکنید که بعدا آن را از حالت کامنت خارج کنید. و البته نگران آیکون مثبت که در دکمه نیست نباشید، بزودی آن را هم درست می‌کنیم.

ایجاد انیمیشن برای Floating Action Button

حالا که انیمیشن ما به خوبی کار نمایش دادن و مخفی کردن «edittext» را انجام می‌دهد، می‌توانید FAB را نیز به گونه‌ای تنظیم کنیم که درست همانند تصویر زیر عمل کند:

morph preview

فایل شروع پروژه دارای هر دو تصاویر وکتور مثبت و تیک است. در ادامه یاد می‌گیرید که چگونه بین آن دو یک انیمیشن ایجاد کنید (یا اصطلاحا آن را مورف (morph) کنید.)

در پوشه «res/drawables» یک فایل «resource» جدید از طریق منوی «New\Drawable resource file» ایجاد کنید. نام آن را «icn_morph» بگذارید و عنصر اصلی (root element) آن را «animated-vector» انتخاب کنید:

1<?xml version="1.0" encoding="utf-8"?>
2<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
3android:drawable="@drawable/icn_add">
4</animated-vector>

«animated-vector» به یک «android:drawable» نیاز دارد که حتما وجود داشته باشد. در اپلیکیشن ما، «animated vector» به صورت یک علامت به علاوه آغاز می‌شود و در نهایت به یک علامت تیک تغییر می‌کند، برای همین «drawable» را برابر با «icn_add» قرار می‌دهیم. حالا برای اینکه کار تغییر صورت بگیرد، مقادیر زیر را در تگ «animated-vector» اضافه کنید:

1<target
2android:animation="@anim/path_morph"
3android:name="sm_vertical_line" />
4<target
5android:animation="@anim/path_morph_lg"
6android:name="lg_vertical_line" />
7<target
8android:animation="@anim/fade_out"
9android:name="horizontal_line" />

با کد بالا، درواقع داریم خط عمودی علامت مثبت را به یک علامت تیک تغییر می‌دهیم و خط افقی را محو می‌کنیم. تصویر زیر گویای این مساله است:

تبدیل به علاوه به تیک

در ادامه، خط عمودی از دو خط تشکیل شده است، یک خط عمودی کوچکتر و یک خط بزرگتر:

تبدیل به علاوه به تیک

از روی تصاویر بالا می‌توانید متوجه شوید که با تغییر دو خط اول، یعنی «sm_vertical_line» و «lg_vertical_line»، و رسم آن‌ها در زوایای متفاوت، می‌توان یک علامت تیک تشکیل داد. این دقیقا همان کاری است که در قطعه کد بالا انجام داده‌ایم، همچنین خط افقی را نیز مخفی کرده‌ایم.

پس از آن، باید انیمیشن را برگردانیم و علامت تیک را به یک علامت به علاوه تبدیل کنیم. یک فایل «drawable resource» دیگر ایجاد کنید. این‌بار آن را «icn_morph_reverse» بنامید و محتوای آن را با کد زیر جایگزین کنید:

1<?xml version="1.0" encoding="utf-8"?>
2<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
3android:drawable="@drawable/icn_add">
4<target
5android:animation="@anim/path_morph_reverse"
6android:name="sm_vertical_line"/>
7<target
8android:animation="@anim/path_morph_lg_reverse"
9android:name="lg_vertical_line" />
10<target
11android:animation="@anim/fade_in"
12android:name="horizontal_line" />
13</animated-vector>

اینبار دو خط عمودی به حالت اولیه خود برمی‌گردند و خط افقی نیز نمایان می‌شود که در مجموع یک انیمیشن روان را ایجاد می‌کنند. حالا باید این انیمیشن را تکمیل کنیم. فایل «DetailActivity.kt» را باز کنید و کد زیر را در متد «()onClick» در پایان دستور «if» و قبل از شروع «else» وارد کنید:

1addButton.setImageResource(R.drawable.icn_morph)
2val animatable = addButton.drawable as Animatable
3animatable.start()

در این کد ما منبع تصویر را «icn_morph» قرار می‌دهیم، انیمیشن را از آن استخراج، و در نهایت آن را اجرا می‌کنیم. در نهایت، کد زیر را در پایان دستور «else» قرار دهید:

1addButton.setImageResource(R.drawable.icn_morph_reverse)
2val animatable = addButton.drawable as Animatable
3animatable.start()

این کد نیز دقیقا کار کد بالا را انجام می‌دهد با این تفاوت که منبع تصویر را «icn_morph_reverse» قرار می‌دهد تا انیمیشن به جهت مخالف اجرا شود. در کنار تغییر آیکون، با کلیک کاربر، متنی که در «todoText» قرار دارد، به «todoAdapter» وارد می‌شود و لیست فعالیت‌های مربوط به آن مکان را بروزرسانی می‌کند. این مساله به دلیل سفید بودن رنگ متن، هنوز مشخص نیست، ولی در مراحل بعد با تغییر رنگ viewها، به نمایش متن کمک می‌کنیم. اپلیکیشن را بسازید و اجرا کنید تا تغییرات را ببینید! آیکون FAB بین تیک و به علاوه تغییر می‌کند.

morph

اضافه کردن رنگ‌های پویا به viewها توسط API پالت رنگ

حالا وقت آن است که رنگ این View را نیز توسط API پالت رنگ تنظیم کنیم. البته نه رنگ‌های ثابت، بلکه همانند قبل، از رنگ‌های پویا استفاده می‌کنیم!

در «DetailActivity» با اضافه کردن کد زیر به متد «()colorize» آن را تکمیل کنید:

1val palette = Palette.from(photo).generate()
2applyPalette(palette)

همانند قبل، یک رنگ از روی تصویر ساخته می‌شود (با این تفاوت که این دفعه اینکار به صورت همگام صورت می‌گیرد) و آن پالت رنگی را به «()applyPallete» ارسال می‌کنیم. متد «()applyPalette» را با کد زیر جایگزین کنید:

1private fun applyPalette(palette: Palette) {
2window.setBackgroundDrawable(ColorDrawable(palette.getDarkMutedColor(defaultColor)))
3placeNameHolder.setBackgroundColor(palette.getMutedColor(defaultColor))
4revealView.setBackgroundColor(palette.getLightVibrantColor(defaultColor))
5}

در این کد داریم از رنگ‌های تیره خفه (dark muted color)، رنگ‌های خفه (muted color) و رنگ‌های پرانرژی روشن (light vibrant color) به عنوان پس‌زمینه پنجره، محل قرارگیری تیتر و پنجره‌ای که با FAB نمایان می‌شود استفاده می‌کنیم. در نهایت، برای اعمال این زنجیره از تغییرات، کد زیر را به آخر متد «()getPhoto» اضافه کنید:

1colorize(photo)

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

palette API preview

جابه‌جایی بین Activityهایی که عناصر یکسان دارند

همه ما آن جابه‌جایی‌هایی که بین تصاویر و متن‌ها در اپلیکیشن‌های گوگل صورت می‌گیرد را دیده‌ایم و برایمان سوال شده که چگونه با متریال دیزاین می‌توانیم آن‌ها را بسازیم. در ادامه به یادگیری چگونگی ساخت این انیمیشن‌های روان و زیبا می‌پردازیم. جابه‌جایی بین اکتیویتی‌هایی با عناصر یکسان در میان اکتیویتی‌هایی صورت می‌گیرد که دارای چند view یکسان هستند. برای مثال می‌توان یک تصویر کوچک را به یک تصویر بزرگتر در یک اکتیویتی دیگر تبدیل کرد که باعث می‌شود آن تصویر همواره در محتوا حاضر باشد.

ما می‌خواهیم بین لیست که «MainActivity» است و قسمت جزئیات که «DetailActivity» است، عناصر زیر را جابه‌جا کنیم:

  • تصویر آن محل
  • تیتر آن محل
  • پس‌زمینه تیتر

فایل «row_places.xml» را باز کنید و تعریف زیر را در تگ «ImageView» که آی‌دی «placeImage» را دارد اضافه کنید:

1android:transitionName="tImage"

سپس کد زیر را به تگ «LinearLayout» که آی‌دی «placeNameHolder» را دارد اضافه کنید:

1android:transitionName="tNameHolder"

توجه کنید که «placeName» فاقد «transition name» است. دلیل آن این است که فرزند «placeNameHolder» است و «placeNameHolder» خودش تمامی فرزندانش را جابه‌جا می‌کند. در فایل «activity_detail.xml»، یک «transitionName» به تگ «ImageView» که آی دی «placeImage» را دارد اضافه کنید:

1android:transitionName="tImage"

سپس به همان ترتیب، یک «transitionName» به تگ «LinearLayout» که آی «placeNameHolder» را دارد اضافه کنید:

1android:transitionName="tNameHolder"

عناصر مشترکی که می‌خواهید بین اکیتیوتی‌ها جابه‌جا کنید باید دارای «android:transitionName» ثابت باشند، که این دقیقا همان کاری است که ما در حال انجام آن هستیم. همچنین توجه داشته باشید که اندازه تصویر و ارتفاع «placeNameHolder» در این اکتیویتی بسیار بزرگتر است. باید تغییرات هر سه قالب را به گونه‌ای اعمال کنید که یک تصویر دنباله‌دار زیبا را ایجاد کنند. در متد «()onItemClickListener» که در «MainActivity» قرار دارد، کد زیر را بروزرسانی کنید:

1override fun onItemClick(view: View, position: Int) {
2val intent = DetailActivity.newIntent(this@MainActivity, position)
3
4// 1
5val placeImage = view.findViewById<ImageView>(R.id.placeImage)
6val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
7
8// 2
9val imagePair = Pair.create(placeImage as View, "tImage")
10val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
11
12// 3
13val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
14imagePair, holderPair)
15ActivityCompat.startActivity(this@MainActivity, intent, options.toBundle())
16}

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

1import android.support.v4.util.Pair

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

  1. ما یک نمونه از «placeImage» و «placeImageHolder» با موقعیت آن‌ها در RecyclerView می‌گیریم. در این بخش به «Kotlin Android Extensions» تکیه نمی‌کنیم، چراکه به مقدار «placeImage» و «placeNameHolder» در آن view خاص نیاز داریم.
  2. ما یک «Pair» (به معنای جفت یا زوج) می‌سازیم که شامل خود view و «transitionName» که به تصویر و «text holder» نسبت داده‌ایم است. باز هم یادآوری می‌کنیم که باید پکیج را به صورت دستی اضافه کنید: android.support.v4.util.Pair.
  3. برای اینکه اکتیویتی را با عناصر مشترک جابه‌جا کنیم، باید نمونه‌ای که از «Pair» ساخته‌ایم را به اکتیویتی ارسال کنیم و اکتیویتی را توسط «options» آغاز کنیم.

نرم افزار را بسازید و اجرا کنید تا جابه‌جایی بین «main activity» و «detail activity» را ببینید:

transition preview

البته انیمیشن ما در دو بخش ایراد دارد:

  • دکمه شناور ما به صورت یکدفعه در «DetailActivity» ظاهر می‌شود.
  • اگر برروی یکی از سطرها که زیر «action bar» یا «navigation bar» قرار دارد ضربه بزنید، انیمیشن آن کمی می‌پرد.

اول مشکل دکمه را حل می‌کنیم. ابتدا «Detailactivity.kt» را باز کنید و کد زیر را به متد «()windowTransition» اضافه کنید:

1window.enterTransition.addListener(object : Transition.TransitionListener {
2override fun onTransitionEnd(transition: Transition) {
3addButton.animate().alpha(1.0f)
4window.enterTransition.removeListener(this)
5}
6
7override fun onTransitionResume(transition: Transition) { }
8override fun onTransitionPause(transition: Transition) { }
9override fun onTransitionCancel(transition: Transition) { }
10override fun onTransitionStart(transition: Transition) { }
11})

این شنونده که اضافه کردیم زمانی فعال می‌شود که عمل جابه‌جایی بین صفحات تمام شود، سپس با استفاده از این شنونده، دکمه FAB را در صفحه نمایان می‌کنیم. برای اینکه این کار موثر باشد، مقدار «alpha» که مربوط به «FAB» است را در «acitivty_detail.xml» به صفر تغییر دهید:

1android:alpha="0.0"

نرم‌افزار را بسازید و اجرا کنید. متوجه می‌شوید که FAB بسیار روان‌تر از قبل نمایان می‌شود:

transition preview

حالا به سراغ مشکلات «action bar» و «navigation bar» می‌رویم. کار را با بروزرسانی «styles.xml» آغاز می‌کنیم تا قالب نمایش اصلی را به «Theme.AppCompat.Light.NoActionBar» تغییر دهیم:

1<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">

از آنجایی که در «styles.xml» هیچ چیزی با عنوان «action bar» تعریف نشده‌است، باید آن را در XMLهای جداگانه اضافه کنید. فایل «activity_main.xml» را باز کنید و کد زیر را در تگ «LinearLayout»، درست در بالای تگ «RecyclerView» اضافه کنید:

1<include layout="@layout/toolbar" />

این کد یک قالب «toolbar» به قالب فعلی اضافه می‌کند (این قالب در پروژه شروع قرار دارد). حالا باید تغییرات مشابه را در قالب «detail activity» نیز ایجاد کنیم. فایل «activity_detail.xml» را باز کنید و کد زیر را در پایان اولین «FrameLayout»، درست قبل از بستن تگ «LinearLayout» داخل آن، اضافه کنید:

1<include layout="@layout/toolbar_detail"/>

سپس باید در «MainAcitivty» این نوار ابزار (toolbar) را راه‌اندازی کنید. کد زیر را در آخر متد «()onCreate» اضافه کنید:

1setUpActionBar()

در اینجا ما مقداری که از «findViewById» به دست آمده است را به بخش جدید نسبت می‌دهیم و سپس «()setUpActionBar» را صدا می‌زنیم. در حال حاضر این فقط یک متد خالی است. با اضافه کردن کد زیر به «()setUpActionBar» متد را تکمیل می‌کنیم:

1setSupportActionBar(toolbar)
2supportActionBar?.setDisplayHomeAsUpEnabled(false)
3supportActionBar?.setDisplayShowTitleEnabled(true)
4supportActionBar?.elevation = 7.0f

در اینجا ما «action bar» را به گونه‌ای تنظیم کردیم که یک نمونه از نوار ابزار شخصی‌سازی شده خودمان باشد. سپس وضعیت نمایان بودن تیتر را مشخص و دکمه بازگشت به خانه را غیر فعال کردیم. در نهایت یک سایه مناسب با استفاده از «elevation» به آن اضافه کردیم. نرم‌افزار را بسازید و اجرا کنید. متوجه می‌شوید که چیز زیادی تغییر نکرده است. ولی چیزی که اهمیت دارد این است که این تغییرات، پایه‌ای را که برای جابه‌جایی مناسب نیاز داشتیم فراهم کرده‌اند.

فایل «MainActivity» را باز کنید و «onitemClickListener» فعلی را با کد زیر جایگزین کنید:

1private val onItemClickListener = object : TravelListAdapter.OnItemClickListener {
2override fun onItemClick(view: View, position: Int) {
3// 1
4val transitionIntent = DetailActivity.newIntent(this@MainActivity, position)
5val placeImage = view.findViewById<ImageView>(R.id.placeImage)
6val placeNameHolder = view.findViewById<LinearLayout>(R.id.placeNameHolder)
7
8// 2
9val navigationBar = findViewById<View>(android.R.id.navigationBarBackground)
10val statusBar = findViewById<View>(android.R.id.statusBarBackground)
11
12val imagePair = Pair.create(placeImage as View, "tImage")
13val holderPair = Pair.create(placeNameHolder as View, "tNameHolder")
14
15// 3
16val navPair = Pair.create(navigationBar, Window.NAVIGATION_BAR_BACKGROUND_TRANSITION_NAME)
17val statusPair = Pair.create(statusBar, Window.STATUS_BAR_BACKGROUND_TRANSITION_NAME)
18val toolbarPair = Pair.create(toolbar as View, "tActionBar")
19
20// 4
21val pairs = mutableListOf(imagePair, holderPair, statusPair, toolbarPair)
22if (navigationBar != null && navPair != null) {
23pairs += navPair
24}
25
26// 5
27val options = ActivityOptionsCompat.makeSceneTransitionAnimation(this@MainActivity,
28*pairs.toTypedArray())
29ActivityCompat.startActivity(this@MainActivity, transitionIntent, options.toBundle())
30}
31}

تغییرات بین حالت قبلی و حالت فعلی به شرح زیر است:

  1. نام «intent» را تغییر دادیم تا مفهومی‌تر باشد.
  2. یک اشاره (reference) به هر دو «navigation bar» و «status bar» ایجاد کرده‌ایم.
  3. سه نمونه جدید از «Pair» ساخته‌ایم: یکی برای «navigation bar»، یکی برای «status bar»، و دیگری برای «toolbar».
  4. یک کد برای جلوگیری از وقوع خطای «IllegalArgumentException» نوشته‌ایم که در برخی از دستگاها نظیر «Galaxy Tab S2» ایجاد می‌شود که در آن مقدار «navPair» برابر با «null» است.
  5. در نهایت مقادیری که به اکتیویتی جدید ارسال می‌شوند را بروزرسانی کردیم تا شامل تمام اشاره‌ها به viewهای جدید شود.

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

transition preview

حالا اگر برروی یک سطر در زیر «action bar» (یا همان toolbar که خودمان ساختیم) یا «navigation bar» ضربه بزنید، دیگر در انیمیشن آن پرش وجود ندارد و جابه‌جایی همراه با سایر عناصر مشترک صورت می‌گیرد که در نتیجه انیمیشن آن زیباتر می‌شود. به حالت جدولی نیز بروید و متوجه می‌شوید که جابه‌جایی در آن حالت نیز به خوبی انجام می‌شود.

بعد از این چکار کنیم؟

به خودتان افتخار کنید، شما یک اپلیکیشن با متریال دیزاین کامل ساخته‌اید! حالا خودتان را به چالش بگیرید و کارهای زیر را امتحان کنید:

  • از «StaggeredLayoutManager» استفاده کنید تا حالت جدول را از 2 ستون به 3 ستون تغییر دهید.
  • با API پالت رنگی آزمایش انجام دهید و در هر دو «MainActivity» و «DetailActivity» حالات مختلف را امتحان کنید.
  • یک دکمه به لیست مکان‌ها اضافه کنید و آن را به عنوان یک عنصر مشترک به «detail view» منتقل کنید (مثلا یک دکمه علاقه‌مندی‌ها).
  • انیمیشن‌های جابه‌جایی که ساخته‌اید را بهتر کنید. نرم‌افزار «Newsstand» اندروید را بررسی کنید و ببینید چگونه از یک جدول به صفحه جزئیات جابه‌جا می‌شود. شما تمامی کدهایی که برای ساخت آن نیاز دارید را در این مقاله دارید.
  • سعی کنید تمام انیمیشن‌های مورف را که اینجا ساختید با استفاده از «animated-vectors» بازسازی کنید.

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

#

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

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