تجربه دوربین خود را آشکار کنید

۱. قبل از شروع

چه چیز خاصی در مورد گوشی‌های تاشو وجود دارد؟

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

پیش‌نیازها

  • دانش پایه در توسعه اپلیکیشن‌های اندروید
  • دانش پایه در مورد چارچوب تزریق وابستگی Hilt

آنچه خواهید ساخت

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

6caebc2739522a1b.png

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

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

آنچه یاد خواهید گرفت

  • نحوه استفاده از Jetpack Window Manager برای واکنش به تغییر وضعیت
  • چگونه برنامه خود را به صفحه نمایش کوچکتر یک گوشی تاشو منتقل کنیم

آنچه نیاز دارید

  • نسخه جدید اندروید استودیو
  • یک دستگاه تاشو یا شبیه‌ساز تاشو

۲. آماده شوید

دریافت کد شروع

  1. اگر گیت را نصب کرده‌اید، می‌توانید به سادگی دستور زیر را اجرا کنید. برای بررسی نصب بودن گیت، git --version را در ترمینال یا خط فرمان تایپ کنید و تأیید کنید که به درستی اجرا می‌شود.
git clone https://github.com/android/large-screen-codelabs.git
  1. اختیاری: اگر گیت ندارید، می‌توانید روی دکمه‌ی زیر کلیک کنید تا تمام کدهای این codelab را دانلود کنید:

ماژول اول را باز کنید

  • در اندروید استودیو، اولین ماژول را در مسیر /step1 باز کنید.

تصویری از اندروید استودیو که کد مربوط به این آزمایشگاه کد را نشان می‌دهد

اگر از شما خواسته شد که از آخرین نسخه Gradle استفاده کنید، آن را به‌روزرسانی کنید.

۳. بدوید و مشاهده کنید

  1. کد را روی ماژول step1 اجرا کنید.

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

a34aca632d75aa09.png

  1. حالا، سعی کنید دستگاه را در حالت نیمه باز قرار دهید، که در آن لولا کاملاً صاف یا بسته نیست بلکه زاویه ۹۰ درجه تشکیل می‌دهد.

همانطور که می‌بینید، برنامه به حالت‌های مختلف دستگاه واکنش نشان نمی‌دهد و بنابراین طرح‌بندی تغییر نمی‌کند و لولا در وسط منظره‌یاب قرار می‌گیرد.

۴. درباره Jetpack WindowManager اطلاعات کسب کنید

کتابخانه Jetpack WindowManager به توسعه‌دهندگان برنامه کمک می‌کند تا تجربیات بهینه‌ای را برای دستگاه‌های تاشو ایجاد کنند. این کتابخانه شامل کلاس FoldingFeature است که نحوه تا شدن در یک صفحه نمایش انعطاف‌پذیر یا لولا بین دو پنل صفحه نمایش فیزیکی را توصیف می‌کند. API آن دسترسی به اطلاعات مهم مربوط به دستگاه را فراهم می‌کند:

کلاس 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.

  1. این متغیرها را در 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
  1. و آنها را در متد 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()
      }
  }
}
  1. اکنون تابع 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.

  1. اکنون تابع 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 را پیاده‌سازی کنید.

  1. تعریف 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]")
}
  1. برنامه را بسازید و اجرا کنید. اگر سپس دستگاه خود را باز کنید و روی دکمه نمایشگر پشتی ضربه بزنید، پیامی مانند این به شما نمایش داده می‌شود:

3fa50cce0b0d4b8d.png

  1. برای مشاهده‌ی انتقال محتوای خود به نمایشگر بیرونی، گزینه‌ی « تغییر صفحه نمایش» را انتخاب کنید!

۶. حالت رومیزی را پیاده‌سازی کنید

حالا وقت آن رسیده که برنامه‌تان را طوری بسازید که از تا شدن آگاه باشد: شما محتوای خود را بر اساس جهت تا شدن، در کنار یا بالای لولای دستگاه جابجا می‌کنید. برای انجام این کار، درون FoldingStateActor عمل خواهید کرد تا کد شما برای خوانایی آسان‌تر از Activity جدا شود.

بخش اصلی این API شامل رابط WindowInfoTracker است که با یک متد استاتیک ایجاد شده است که به یک Activity نیاز دارد:

مرحله ۱/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

لازم نیست این کد را بنویسید زیرا از قبل موجود است، اما برای درک نحوه ساخت WindowInfoTracker مفید است.

  1. برای گوش دادن به هرگونه تغییر پنجره، این تغییرات را در متد onResume() از Activity خود گوش دهید:

مرحله ۱/فعالیت اصلی.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity, 
         binding.viewFinder
    )
}
  1. حالا فایل FoldingStateActor را باز کنید، چون وقت آن رسیده که متد checkFoldingState() را پر کنید.

همانطور که قبلاً دیدید، این تابع در فاز RESUMED از Activity شما اجرا می‌شود و از WindowInfoTracker برای گوش دادن به هرگونه تغییر در طرح‌بندی استفاده می‌کند.

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

با استفاده از رابط WindowInfoTracker ، می‌توانید تابع windowLayoutInfo() را فراخوانی کنید تا Flow از WindowLayoutInfo را که شامل تمام اطلاعات موجود در DisplayFeature است، جمع‌آوری کنید.

آخرین مرحله، واکنش به این تغییرات و جابجایی محتوا بر اساس آن است. شما این کار را درون متد updateLayoutByFoldingState() ، گام به گام انجام می‌دهید.

  1. مطمئن شوید که activityLayoutInfo شامل برخی ویژگی‌های DisplayFeature است و حداقل یکی از آنها FoldingFeature است، در غیر این صورت نمی‌خواهید کاری انجام دهید:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. موقعیت تاشدگی را محاسبه کنید تا مطمئن شوید که موقعیت دستگاه بر طرح‌بندی شما تأثیر می‌گذارد و خارج از مرزهای سلسله مراتب شما نیست:

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

حالا مطمئن هستید که یک FoldingFeature دارید که روی طرح‌بندی شما تأثیر می‌گذارد، بنابراین باید محتوای خود را منتقل کنید.

  1. بررسی کنید که آیا FoldingFeature HALF_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 باشد، محتوای خود را به سمت راست منتقل می‌کنید، در غیر این صورت آن را به بالای موقعیت تاخوردگی منتقل می‌کنید.

  1. برنامه خود را بسازید و اجرا کنید، و سپس دستگاه خود را باز کنید و آن را در حالت تبلت قرار دهید تا ببینید محتوا به طور متناسب حرکت می‌کند!

۷. تبریک می‌گویم!

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

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

مطالعه بیشتر

مرجع