۱. قبل از شروع
چه چیز خاصی در مورد گوشیهای تاشو وجود دارد؟
گوشیهای تاشو نوآوریهای بینظیری هستند که در هر نسل یک بار اتفاق میافتند. آنها تجربیات منحصر به فردی را ارائه میدهند و با ویژگیهای متمایزی مانند رابط کاربری رومیزی برای استفاده بدون دخالت دست، فرصتهای بینظیری را برای لذت بردن کاربران شما به ارمغان میآورند.
پیشنیازها
- دانش پایه در توسعه اپلیکیشنهای اندروید
- دانش پایه در مورد چارچوب تزریق وابستگی Hilt
آنچه خواهید ساخت
در این آزمایشگاه کد، شما یک اپلیکیشن دوربین با طرحبندیهای بهینه برای دستگاههای تاشو میسازید.

شما با یک برنامه دوربین ساده شروع میکنید که به هیچ یک از حالتهای دستگاه واکنش نشان نمیدهد و از دوربین عقب بهتر برای گرفتن سلفیهای بهتر استفاده نمیکند. شما کد منبع را بهروزرسانی میکنید تا پیشنمایش را به صفحه نمایش کوچکتر هنگام باز شدن دستگاه منتقل کند و به تنظیم گوشی در حالت تبلت واکنش نشان دهد.
اگرچه اپلیکیشن دوربین راحتترین مورد استفاده برای این API است، اما هر دو ویژگی که در این آزمایشگاه کد یاد میگیرید را میتوان در هر اپلیکیشنی اعمال کرد.
آنچه یاد خواهید گرفت
- نحوه استفاده از Jetpack Window Manager برای واکنش به تغییر وضعیت
- چگونه برنامه خود را به صفحه نمایش کوچکتر یک گوشی تاشو منتقل کنیم
آنچه نیاز دارید
- نسخه جدید اندروید استودیو
- یک دستگاه تاشو یا شبیهساز تاشو
۲. آماده شوید
دریافت کد شروع
- اگر گیت را نصب کردهاید، میتوانید به سادگی دستور زیر را اجرا کنید. برای بررسی نصب بودن گیت،
git --versionرا در ترمینال یا خط فرمان تایپ کنید و تأیید کنید که به درستی اجرا میشود.
git clone https://github.com/android/large-screen-codelabs.git
- اختیاری: اگر گیت ندارید، میتوانید روی دکمهی زیر کلیک کنید تا تمام کدهای این codelab را دانلود کنید:
ماژول اول را باز کنید
- در اندروید استودیو، اولین ماژول را در مسیر
/step1باز کنید.

اگر از شما خواسته شد که از آخرین نسخه Gradle استفاده کنید، آن را بهروزرسانی کنید.
۳. بدوید و مشاهده کنید
- کد را روی ماژول
step1اجرا کنید.
همانطور که میبینید، این یک برنامه دوربین ساده است. میتوانید بین دوربین جلو و عقب جابجا شوید و نسبت تصویر را تنظیم کنید. با این حال، اولین دکمه از سمت چپ در حال حاضر کاری انجام نمیدهد - اما قرار است نقطه ورود به حالت سلفی عقب باشد.

- حالا، سعی کنید دستگاه را در حالت نیمه باز قرار دهید، که در آن لولا کاملاً صاف یا بسته نیست بلکه زاویه ۹۰ درجه تشکیل میدهد.
همانطور که میبینید، برنامه به حالتهای مختلف دستگاه واکنش نشان نمیدهد و بنابراین طرحبندی تغییر نمیکند و لولا در وسط منظرهیاب قرار میگیرد.
۴. درباره Jetpack WindowManager اطلاعات کسب کنید
کتابخانه Jetpack WindowManager به توسعهدهندگان برنامه کمک میکند تا تجربیات بهینهای را برای دستگاههای تاشو ایجاد کنند. این کتابخانه شامل کلاس FoldingFeature است که نحوه تا شدن در یک صفحه نمایش انعطافپذیر یا لولا بین دو پنل صفحه نمایش فیزیکی را توصیف میکند. API آن دسترسی به اطلاعات مهم مربوط به دستگاه را فراهم میکند:
-
state()در صورتی که لولا ۱۸۰ درجه باز شده باشد،FLATو در غیر این صورتHALF_OPENEDرا برمیگرداند. - اگر عرض
FoldingFeatureاز ارتفاع آن بیشتر باشد،orientation()FoldingFeature.Orientation.HORIZONTALرا برمیگرداند؛ در غیر این صورت،FoldingFeature.Orientation.VERTICALرا برمیگرداند. -
bounds()مرزهایFoldingFeatureرا در قالبRectارائه میدهد.
کلاس FoldingFeature شامل اطلاعات اضافی مانند occlusionType() یا isSeparating() است، اما این مجموعه کد به طور عمیق به بررسی آنها نمیپردازد.
از نسخه ۱.۲.۰-بتا۰۱ به بعد، این کتابخانه از WindowAreaController استفاده میکند، یک API که حالت نمایش عقب را قادر میسازد تا پنجره فعلی را به نمایشگری که با دوربین عقب همتراز است منتقل کند، که برای گرفتن سلفی با دوربین عقب و بسیاری از موارد استفاده دیگر عالی است!
وابستگیها را اضافه کنید
- برای استفاده از Jetpack WindowManager در برنامه خود، باید وابستگیهای زیر را به فایل
build.gradleدر سطح ماژول خود اضافه کنید:
مرحله ۱/build.gradle
def work_version = '1.2.0-beta01'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"
حالا میتوانید به هر دو کلاس FoldingFeature و WindowAreaController در برنامه خود دسترسی داشته باشید. از آنها برای ساخت بهترین تجربه دوربین تاشو استفاده میکنید!
۵. حالت سلفی از پشت را فعال کنید
با حالت نمایش عقب شروع کنید.
رابط برنامهنویسی کاربردی (API) که این حالت را امکانپذیر میکند، WindowAreaController است که اطلاعات و رفتار مربوط به جابجایی پنجرهها بین نمایشگرها یا نواحی نمایشگر روی یک دستگاه را فراهم میکند.
این به شما امکان میدهد لیستی از WindowAreaInfo را که در حال حاضر برای تعامل در دسترس هستند، جستجو کنید.
با استفاده از WindowAreaInfo میتوانید به WindowAreaSession دسترسی پیدا کنید، رابطی برای نمایش یک ویژگی فعال ناحیه پنجره و وضعیت در دسترس بودن برای یک WindowAreaCapability.
- این متغیرها را در
MainActivityخود تعریف کنید:
مرحله ۱/فعالیت اصلی.kt
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var rearDisplaySession: WindowAreaSession? = null
private var rearDisplayWindowAreaInfo: WindowAreaInfo? = null
private var rearDisplayStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
- و آنها را در متد
onCreate()مقداردهی اولیه کنید:
مرحله ۱/فعالیت اصلی.kt
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowAreaController.windowAreaInfos
.map{info->info.firstOrNull{it.type==WindowAreaInfo.Type.TYPE_REAR_FACING}}
.onEach { info -> rearDisplayWindowAreaInfo = info }
.map{it?.getCapability(rearDisplayOperation)?.status?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
.distinctUntilChanged()
.collect {
rearDisplayStatus = it
updateUI()
}
}
}
- اکنون تابع
updateUI()را برای فعال یا غیرفعال کردن دکمه سلفی عقب، بسته به وضعیت فعلی، پیادهسازی کنید:
مرحله ۱/فعالیت اصلی.kt
private fun updateUI() {
if(rearDisplaySession != null) {
binding.rearDisplay.isEnabled = true
// A session is already active, clicking on the button will disable it
} else {
when(rearDisplayStatus) {
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not supported on this device"
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
binding.rearDisplay.isEnabled = false
// RearDisplay Mode is not currently available
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
binding.rearDisplay.isEnabled = true
// You can enable RearDisplay Mode
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
binding.rearDisplay.isEnabled = true
// You can disable RearDisplay Mode
}
else -> {
binding.rearDisplay.isEnabled = false
// RearDisplay status is unknown
}
}
}
}
این مرحله آخر اختیاری است، اما برای یادگیری تمام حالتهای ممکن یک WindowAreaCapability.
- اکنون تابع
toggleRearDisplayModeرا پیادهسازی کنید که در صورت فعال بودن قابلیت، جلسه را میبندد، یا تابعtransferActivityToWindowAreaرا فراخوانی کنید:
مرحله ۱/CameraViewModel.kt
private fun toggleRearDisplayMode() {
if(rearDisplayStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
if(rearDisplaySession == null) {
rearDisplaySession = rearDisplayWindowAreaInfo?.getActiveSession(rearDisplayOperation)
}
rearDisplaySession?.close()
} else {
rearDisplayWindowAreaInfo?.token?.let { token ->
windowAreaController.transferActivityToWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaSessionCallback = this
)
}
}
}
به استفاده از MainActivity به عنوان WindowAreaSessionCallback توجه کنید.
API نمایشگر پشتی با رویکرد شنونده کار میکند: وقتی درخواست انتقال محتوا به نمایشگر دیگر را میدهید، یک جلسه (session) را آغاز میکنید که از طریق متد onSessionStarted() شنونده بازگردانده میشود. وقتی میخواهید به نمایشگر داخلی (و بزرگتر) برگردید، جلسه را میبندید و در متد onSessionEnded() تأیید دریافت میکنید. برای ایجاد چنین شنوندهای، باید رابط WindowAreaSessionCallback را پیادهسازی کنید.
- تعریف
MainActivityرا طوری تغییر دهید که رابطWindowAreaSessionCallbackرا پیادهسازی کند:
مرحله ۱/فعالیت اصلی.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
حالا، متدهای onSessionStarted و onSessionEnded را درون MainActivity پیادهسازی کنید. این متدهای callback برای اطلاع از وضعیت session و بهروزرسانی برنامه بر اساس آن بسیار مفید هستند.
اما این بار، برای سادگی، فقط در بدنه تابع بررسی کنید که آیا خطایی وجود دارد یا خیر و وضعیت را ثبت کنید.
مرحله ۱/فعالیت اصلی.kt
override fun onSessionEnded(t: Throwable?) {
if(t != null) {
Log.d("Something was broken: ${t.message}")
}else{
Log.d("rear session ended")
}
}
override fun onSessionStarted(session: WindowAreaSession) {
Log.d("rear session started [session=$session]")
}
- برنامه را بسازید و اجرا کنید. اگر سپس دستگاه خود را باز کنید و روی دکمه نمایشگر پشتی ضربه بزنید، پیامی مانند این به شما نمایش داده میشود:

- برای مشاهدهی انتقال محتوای خود به نمایشگر بیرونی، گزینهی « تغییر صفحه نمایش» را انتخاب کنید!
۶. حالت رومیزی را پیادهسازی کنید
حالا وقت آن رسیده که برنامهتان را طوری بسازید که از تا شدن آگاه باشد: شما محتوای خود را بر اساس جهت تا شدن، در کنار یا بالای لولای دستگاه جابجا میکنید. برای انجام این کار، درون FoldingStateActor عمل خواهید کرد تا کد شما برای خوانایی آسانتر از Activity جدا شود.
بخش اصلی این API شامل رابط WindowInfoTracker است که با یک متد استاتیک ایجاد شده است که به یک Activity نیاز دارد:
مرحله ۱/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
لازم نیست این کد را بنویسید زیرا از قبل موجود است، اما برای درک نحوه ساخت WindowInfoTracker مفید است.
- برای گوش دادن به هرگونه تغییر پنجره، این تغییرات را در متد
onResume()ازActivityخود گوش دهید:
مرحله ۱/فعالیت اصلی.kt
lifecycleScope.launch {
foldingStateActor.checkFoldingState(
this@MainActivity,
binding.viewFinder
)
}
- حالا فایل
FoldingStateActorرا باز کنید، چون وقت آن رسیده که متدcheckFoldingState()را پر کنید.
همانطور که قبلاً دیدید، این تابع در فاز RESUMED از Activity شما اجرا میشود و از WindowInfoTracker برای گوش دادن به هرگونه تغییر در طرحبندی استفاده میکند.
step1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
با استفاده از رابط WindowInfoTracker ، میتوانید تابع windowLayoutInfo() را فراخوانی کنید تا Flow از WindowLayoutInfo را که شامل تمام اطلاعات موجود در DisplayFeature است، جمعآوری کنید.
آخرین مرحله، واکنش به این تغییرات و جابجایی محتوا بر اساس آن است. شما این کار را درون متد updateLayoutByFoldingState() ، گام به گام انجام میدهید.
- مطمئن شوید که
activityLayoutInfoشامل برخی ویژگیهایDisplayFeatureاست و حداقل یکی از آنهاFoldingFeatureاست، در غیر این صورت نمیخواهید کاری انجام دهید:
step1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- موقعیت تاشدگی را محاسبه کنید تا مطمئن شوید که موقعیت دستگاه بر طرحبندی شما تأثیر میگذارد و خارج از مرزهای سلسله مراتب شما نیست:
step1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
حالا مطمئن هستید که یک FoldingFeature دارید که روی طرحبندی شما تأثیر میگذارد، بنابراین باید محتوای خود را منتقل کنید.
- بررسی کنید که آیا
FoldingFeatureHALF_OPENاست یا خیر، در غیر این صورت فقط موقعیت محتوای خود را بازیابی میکنید. اگرHALF_OPENاست، باید بررسی دیگری انجام دهید و بر اساس جهت تاخوردگی، متفاوت عمل کنید:
step1/FoldingStateActor.kt
if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) {
when (foldingFeature.orientation) {
FoldingFeature.Orientation.VERTICAL -> {
cameraViewfinder.moveToRightOf(foldPosition)
}
FoldingFeature.Orientation.HORIZONTAL -> {
cameraViewfinder.moveToTopOf(foldPosition)
}
}
} else {
cameraViewfinder.restore()
}
اگر تاخوردگی VERTICAL باشد، محتوای خود را به سمت راست منتقل میکنید، در غیر این صورت آن را به بالای موقعیت تاخوردگی منتقل میکنید.
- برنامه خود را بسازید و اجرا کنید، و سپس دستگاه خود را باز کنید و آن را در حالت تبلت قرار دهید تا ببینید محتوا به طور متناسب حرکت میکند!
۷. تبریک میگویم!
در این آزمایشگاه کد، شما با برخی از قابلیتهای منحصر به فرد دستگاههای تاشو، مانند حالت نمایش از پشت یا حالت رومیزی، و نحوهی باز کردن قفل آنها با استفاده از Jetpack WindowManager آشنا شدید.
شما آمادهاید تا تجربههای کاربری عالی را برای برنامه دوربین خود پیادهسازی کنید.