1. לפני שמתחילים
מה מיוחד במכשירים מתקפלים?
מכשירים מתקפלים הם חידושים שקורים פעם בדור. הן מספקות חוויות ייחודיות, ויש בהן הזדמנויות ייחודיות לשמח את המשתמשים באמצעות תכונות מובחנות כמו ממשק משתמש שמוצב על השולחן לשימוש ללא מגע.
דרישות מוקדמות
- ידע בסיסי בפיתוח אפליקציות ל-Android
- ידע בסיסי ב-Hilt Dependency Injection framework
מה תפַתחו
ב-codelab הזה, תבנו אפליקציית מצלמה עם פריסות מותאמות למכשירים מתקפלים.

מתחילים עם אפליקציית מצלמה בסיסית שלא מגיבה למצב המכשיר או למצלמה האחורית הטובה יותר כדי לשפר את תמונות הסלפי. מעדכנים את קוד המקור כדי להעביר את התצוגה המקדימה למסך הקטן יותר כשהמכשיר נפתח, וכדי להגיב להגדרת הטלפון במצב 'על משטח, מסך למעלה'.
אפליקציית המצלמה היא תרחיש השימוש הכי נוח ל-API הזה, אבל אפשר להשתמש בשתי התכונות שמוסברות ב-codelab הזה בכל אפליקציה.
מה תלמדו
- איך משתמשים ב-Jetpack Window Manager כדי להגיב לשינוי בתנוחה
- איך מעבירים את האפליקציה למסך הקטן יותר של מכשיר מתקפל
מה תצטרכו
- גרסה עדכנית של Android Studio
- מכשיר מתקפל או אמולטור מתקפל
2. להגדרה
קבלת קוד ההתחלה
- אם Git מותקן, אפשר פשוט להריץ את הפקודה שלמטה. כדי לבדוק אם Git מותקן, מקלידים
git --versionבמסוף או בשורת הפקודה ומוודאים שהפקודה מופעלת בצורה תקינה.
git clone https://github.com/android/large-screen-codelabs.git
פתיחת המודול הראשון
- ב-Android Studio, פותחים את המודול הראשון בקטע
/step1.

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

- עכשיו, מנסים להעמיד את המכשיר במצב חצי פתוח, שבו הציר לא שטוח או סגור לגמרי, אלא יוצר זווית של 90 מעלות.
כפי שאפשר לראות, האפליקציה לא מגיבה למצבי מכשיר שונים, ולכן הפריסה לא משתנה והציר נשאר באמצע של עינית המצלמה.
4. מידע על Jetpack WindowManager
ספריית Jetpack WindowManager עוזרת למפתחי אפליקציות ליצור חוויות אופטימליות למכשירים מתקפלים. הוא מכיל את המחלקה FoldingFeature שמתארת קיפול במסך גמיש או ציר בין שני לוחות מסך פיזיים. ממשק ה-API שלו מספק גישה למידע חשוב שקשור למכשיר:
-
state()מחזירהFLATאם הציר פתוח ב-180 מעלות אוHALF_OPENEDאחרת. - הפונקציה
orientation()מחזירה את הערךFoldingFeature.Orientation.HORIZONTALאם הרוחב שלFoldingFeatureגדול מהגובה, אחרת היא מחזירה את הערךFoldingFeature.Orientation.VERTICAL. -
bounds()מציין את הגבולות שלFoldingFeatureבפורמטRect.
הכיתה 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. ספציפי.
- צריך להצהיר על המשתנים האלה בקובץ
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
- ומאתחלים אותם ב-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()
}
}
}
- עכשיו מטמיעים את הפונקציה
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.
- עכשיו מטמיעים את הפונקציה
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.
- משנים את ההצהרה
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]")
}
- מבצעים Build ומריצים את האפליקציה. אם לאחר מכן פותחים את המכשיר ומקישים על הלחצן של המסך האחורי, מוצגת הודעה כמו זו:

- בוחרים באפשרות החלפת המסכים עכשיו כדי לראות את התוכן עובר למסך החיצוני.
6. הטמעה של מצב 'על משטח'
עכשיו צריך להגדיר את האפליקציה כך שתתאים למכשיר מתקפל: מעבירים את התוכן לצד או מעל הציר של המכשיר בהתאם לכיוון הקיפול. כדי לעשות זאת, תפעלו בתוך FoldingStateActor כדי שהקוד יהיה מופרד מ-Activity, וכך יהיה קל יותר לקרוא אותו.
החלק המרכזי של ה-API הזה הוא הממשק WindowInfoTracker, שנוצר באמצעות שיטה סטטית שדורשת Activity:
step1/CameraCodelabDependencies.kt
@Provides
fun provideWindowInfoTracker(activity: Activity) =
WindowInfoTracker.getOrCreate(activity)
אין צורך לכתוב את הקוד הזה כי הוא כבר קיים, אבל כדאי להבין איך בנוי התג WindowInfoTracker.
- כדי להאזין לשינויים בחלון, צריך להאזין לשינויים האלה בשיטה
onResume()שלActivity:
step1/MainActivity.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.
השלב האחרון הוא להגיב לשינויים האלה ולהעביר את התוכן בהתאם. אתם עושים את זה בתוך ה-method 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 שמשפיע על הפריסה, ולכן אתם צריכים להעביר את התוכן.
- בודקים אם
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, מעבירים את התוכן ימינה, אחרת מעבירים אותו מעל קו הקיפול.
- מריצים את האפליקציה, פותחים את המכשיר ומניחים אותו במצב שולחני כדי לראות את התוכן זז בהתאם.
7. מעולה!
ב-Codelab הזה למדתם על כמה יכולות שייחודיות למכשירים מתקפלים, כמו מצב מסך אחורי או מצב על משטח, מסך למעלה, ואיך להפעיל אותן באמצעות Jetpack WindowManager.
אתם מוכנים להטמיע חוויות משתמש מעולות באפליקציית המצלמה שלכם.