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

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

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

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

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

  • ידע בסיסי בפיתוח אפליקציות ל-Android
  • ידע בסיסי במסגרת הזרקת תלות של Hilt

מה תפַתחו

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

6caebc2739522a1b.png

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

אפליקציית המצלמה היא התרחיש לדוגמה הנוח ביותר ל-API הזה, אבל אפשר ליישם את שתי התכונות שנלמדו ב-Codelab הזה בכל אפליקציה.

מה תלמדו

  • איך להשתמש במנהל החלונות של Jetpack כדי להגיב לשינוי יציבה
  • איך להעביר את האפליקציה לתצוגה קטנה של מכשיר מתקפל

מה צריך להכין

  • גרסה עדכנית של 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(), אבל ה-codelab הזה לא בוחן את המידע הזה לעומק.

החל מגרסה 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. ומאתחלים אותם בשיטה 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.

Rear Display API פועל בגישת האזנה: כשמבקשים להעביר את התוכן למסך האחר, מתחילים סשן שמוחזר באמצעות שיטת ה-onSessionStarted() של המאזינים. אם רוצים לחזור לתצוגה הפנימית (וגדולה יותר), סוגרים את הסשן ומקבלים אישור בשיטה onSessionEnded(). כדי ליצור אוזן כזה, צריך להטמיע את הממשק של WindowAreaSessionCallback.

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

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

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

3fa50cce0b0d4b8d.png

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

6. הטמעת המצב 'על משטח, מסך למעלה'

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

החלק העיקרי של ה-API הזה כולל את הממשק WindowInfoTracker, שנוצר באמצעות method סטטית שמחייבת 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.

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

קריאה נוספת

חומרי עזר