נהנים מחוויית שימוש במצלמה מתקפלת

1. לפני שמתחילים

מה מיוחד במכשירים מתקפלים?

מכשירים מתקפלים הם חידושים שקורים פעם בדור. הן מספקות חוויות ייחודיות, ויש בהן הזדמנויות ייחודיות לשמח את המשתמשים באמצעות תכונות מובחנות כמו ממשק משתמש שמוצב על השולחן לשימוש ללא מגע.

דרישות מוקדמות

  • ידע בסיסי בפיתוח אפליקציות ל-Android
  • ידע בסיסי ב-Hilt Dependency Injection framework

מה תפַתחו

ב-codelab הזה, תבנו אפליקציית מצלמה עם פריסות מותאמות למכשירים מתקפלים.

6caebc2739522a1b.png

מתחילים עם אפליקציית מצלמה בסיסית שלא מגיבה למצב המכשיר או למצלמה האחורית הטובה יותר כדי לשפר את תמונות הסלפי. מעדכנים את קוד המקור כדי להעביר את התצוגה המקדימה למסך הקטן יותר כשהמכשיר נפתח, וכדי להגיב להגדרת הטלפון במצב 'על משטח, מסך למעלה'.

אפליקציית המצלמה היא תרחיש השימוש הכי נוח ל-API הזה, אבל אפשר להשתמש בשתי התכונות שמוסברות ב-codelab הזה בכל אפליקציה.

מה תלמדו

  • איך משתמשים ב-Jetpack Window Manager כדי להגיב לשינוי בתנוחה
  • איך מעבירים את האפליקציה למסך הקטן יותר של מכשיר מתקפל

מה תצטרכו

  • גרסה עדכנית של Android Studio
  • מכשיר מתקפל או אמולטור מתקפל

2. להגדרה

קבלת קוד ההתחלה

  1. אם Git מותקן, אפשר פשוט להריץ את הפקודה שלמטה. כדי לבדוק אם Git מותקן, מקלידים git --version במסוף או בשורת הפקודה ומוודאים שהפקודה מופעלת בצורה תקינה.
git clone https://github.com/android/large-screen-codelabs.git
  1. אופציונלי: אם אין לכם Git, אתם יכולים ללחוץ על הלחצן הבא כדי להוריד את כל הקוד של ה-codelab הזה:

פתיחת המודול הראשון

  • ב-Android Studio, פותחים את המודול הראשון בקטע /step1.

צילום מסך של Android Studio שבו מוצג הקוד שקשור ל-Codelab הזה

אם תתבקשו להשתמש בגרסת Gradle העדכנית, תצטרכו לעדכן אותה.

3. הפעלה והתבוננות

  1. מריצים את הקוד במודול step1.

כפי שאתם רואים, זו אפליקציית מצלמה פשוטה. אתם יכולים לעבור בין המצלמה הקדמית למצלמה האחורית ולשנות את יחס הגובה-רוחב. עם זאת, הכפתור הראשון משמאל לא עושה כרגע כלום, אבל הוא יהיה נקודת הכניסה למצב סלפי עם המצלמה האחורית.

a34aca632d75aa09.png

  1. עכשיו, מנסים להעמיד את המכשיר במצב חצי פתוח, שבו הציר לא שטוח או סגור לגמרי, אלא יוצר זווית של 90 מעלות.

כפי שאפשר לראות, האפליקציה לא מגיבה למצבי מכשיר שונים, ולכן הפריסה לא משתנה והציר נשאר באמצע של עינית המצלמה.

4. מידע על Jetpack WindowManager

ספריית Jetpack WindowManager עוזרת למפתחי אפליקציות ליצור חוויות אופטימליות למכשירים מתקפלים. הוא מכיל את המחלקה FoldingFeature שמתארת קיפול במסך גמיש או ציר בין שני לוחות מסך פיזיים. ממשק ה-API שלו מספק גישה למידע חשוב שקשור למכשיר:

הכיתה FoldingFeature מכילה מידע נוסף, כמו occlusionType() או isSeparating(), אבל במעבדת התכנות הזו לא נבדוק את המידע הזה לעומק.

החל מגרסה 1.2.0-beta01, הספרייה משתמשת ב-WindowAreaController, ממשק API שמאפשר להעביר את החלון הנוכחי למסך שמותאם למצלמה האחורית במצב 'מסך אחורי'. זה מצוין לצילום סלפי עם המצלמה האחורית ולתרחישי שימוש רבים אחרים.

הוספת יחסי תלות

  • כדי להשתמש ב-Jetpack WindowManager באפליקציה, צריך להוסיף את התלויות הבאות לקובץ build.gradle ברמת המודול:

step1/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 באפליקציה. אתם יכולים להשתמש בהן כדי ליצור את חוויית המצלמה האולטימטיבית במכשיר מתקפל.

5. הטמעה של מצב סלפי במצלמה האחורית

מתחילים עם מצב המסך האחורי.

‫API שמאפשר את המצב הזה הוא WindowAreaController, שמספק את המידע וההתנהגות לגבי העברת חלונות בין מסכים או אזורים במסך במכשיר.

היא מאפשרת לשלוח שאילתה לרשימת WindowAreaInfo שזמינים כרגע לאינטראקציה.

באמצעות WindowAreaInfo אפשר לגשת אל WindowAreaSession, ממשק שמציג את האזור של חלון פעיל ואת סטטוס הזמינות של WindowAreaCapability. ספציפי.

  1. צריך להצהיר על המשתנים האלה בקובץ MainActivity:

step1/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. ומאתחלים אותם ב-method‏ onCreate():

step1/MainActivity.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() כדי להפעיל או להשבית את לחצן הסלפי של המצלמה האחורית, בהתאם לסטטוס הנוכחי:

step1/MainActivity.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:

step1/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 של התצוגה האחורית פועל בגישה של listener: כשמבקשים להעביר את התוכן לתצוגה השנייה, מתחילים סשן שמוחזר דרך השיטה onSessionStarted() של ה-listener. אם רוצים לחזור למסך הפנימי (והגדול יותר), סוגרים את הסשן ומקבלים אישור בשיטה onSessionEnded(). כדי ליצור מאזין כזה, צריך להטמיע את הממשק WindowAreaSessionCallback.

  1. משנים את ההצהרה MainActivity כך שהיא תטמיע את הממשק WindowAreaSessionCallback:

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

עכשיו מטמיעים את ה-methods‏ onSessionStarted ו-onSessionEnded בתוך MainActivity. שיטות הקריאה החוזרת האלה שימושיות מאוד לקבלת התראות על סטטוס הסשן ולעדכון האפליקציה בהתאם.

אבל הפעם, כדי לפשט את העניינים, נבדוק בגוף הפונקציה אם יש שגיאות ונרשום את הסטטוס ביומן.

step1/MainActivity.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. מבצעים Build ומריצים את האפליקציה. אם לאחר מכן פותחים את המכשיר ומקישים על הלחצן של המסך האחורי, מוצגת הודעה כמו זו:

3fa50cce0b0d4b8d.png

  1. בוחרים באפשרות החלפת המסכים עכשיו כדי לראות את התוכן עובר למסך החיצוני.

6. הטמעה של מצב 'על משטח'

עכשיו צריך להגדיר את האפליקציה כך שתתאים למכשיר מתקפל: מעבירים את התוכן לצד או מעל הציר של המכשיר בהתאם לכיוון הקיפול. כדי לעשות זאת, תפעלו בתוך FoldingStateActor כדי שהקוד יהיה מופרד מ-Activity, וכך יהיה קל יותר לקרוא אותו.

החלק המרכזי של ה-API הזה הוא הממשק WindowInfoTracker, שנוצר באמצעות שיטה סטטית שדורשת Activity:

step1/CameraCodelabDependencies.kt

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

אין צורך לכתוב את הקוד הזה כי הוא כבר קיים, אבל כדאי להבין איך בנוי התג WindowInfoTracker.

  1. כדי להאזין לשינויים בחלון, צריך להאזין לשינויים האלה בשיטה onResume() של Activity:

step1/MainActivity.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.

השלב האחרון הוא להגיב לשינויים האלה ולהעביר את התוכן בהתאם. אתם עושים את זה בתוך ה-method‏ 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. מריצים את האפליקציה, פותחים את המכשיר ומניחים אותו במצב שולחני כדי לראות את התוכן זז בהתאם.

7. מעולה!

ב-Codelab הזה למדתם על כמה יכולות שייחודיות למכשירים מתקפלים, כמו מצב מסך אחורי או מצב על משטח, מסך למעלה, ואיך להפעיל אותן באמצעות Jetpack WindowManager.

אתם מוכנים להטמיע חוויות משתמש מעולות באפליקציית המצלמה שלכם.

קריאה נוספת

חומרי עזר