1. לפני שמתחילים
מה מיוחד במכשירים מתקפלים?
מכשירים מתקפלים הם חידוש של פעם בדור. הן מספקות חוויות ייחודיות, וביניהן הזדמנויות ייחודיות להפיק הנאה למשתמשים שלכם בעזרת תכונות ייחודיות, כמו ממשק משתמש שולחני, לשימוש בהפעלה קולית.
דרישות מוקדמות
- ידע בסיסי בפיתוח אפליקציות ל-Android
- ידע בסיסי במסגרת הזרקת תלות של Hilt
מה תפַתחו
בשיעור ה-Codelab הזה תיצרו אפליקציית מצלמה עם פריסות שמותאמות למכשירים מתקפלים.
מתחילים עם אפליקציית מצלמה בסיסית שלא מגיבה לשום מצב של המכשיר, או מנצלים את המצלמה האחורית הטובה יותר לתמונות סלפי משופרות. צריך לעדכן את קוד המקור כדי להעביר את התצוגה המקדימה למסך הקטן יותר כשהמכשיר פתוח ומגיב כאשר הטלפון מוגדר במצב שולחני.
אפליקציית המצלמה היא התרחיש לדוגמה הנוח ביותר ל-API הזה, אבל אפשר ליישם את שתי התכונות שנלמדו ב-Codelab הזה בכל אפליקציה.
מה תלמדו
- איך להשתמש במנהל החלונות של Jetpack כדי להגיב לשינוי יציבה
- איך להעביר את האפליקציה לתצוגה קטנה של מכשיר מתקפל
מה צריך להכין
- גרסה עדכנית של 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()
, אבל ה-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.
ספציפי
- צריך להצהיר על המשתנים האלה ב
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
- ומאתחלים אותם בשיטה
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
.
Rear Display API פועל בגישת האזנה: כשמבקשים להעביר את התוכן למסך האחר, מתחילים סשן שמוחזר באמצעות שיטת ה-onSessionStarted()
של המאזינים. אם רוצים לחזור לתצוגה הפנימית (וגדולה יותר), סוגרים את הסשן ומקבלים אישור בשיטה onSessionEnded()
. כדי ליצור אוזן כזה, צריך להטמיע את הממשק של WindowAreaSessionCallback
.
- צריך לשנות את ההצהרה
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]")
}
- לפתח ולהפעיל את האפליקציה. אם לאחר מכן פותחים את המכשיר ומקישים על לחצן המסך האחורי, מוצגת הודעה כזו:
- בוחרים באפשרות אני רוצה לעבור בין מסכים כדי לראות את התוכן שהועבר למסך החיצוני.
6. הטמעת המצב 'על משטח, מסך למעלה'
עכשיו הזמן לשלב באפליקציה את המודעות לקיפול: אפשר להזיז את התוכן לצד או מעל ציר המכשיר בהתאם לכיוון החלק המקופל. לשם כך, צריך לפעול בתוך FoldingStateActor
כדי להפריד את הקוד מה-Activity
כדי שיהיה קל יותר לקרוא אותו.
החלק העיקרי של ה-API הזה כולל את הממשק WindowInfoTracker
, שנוצר באמצעות method סטטית שמחייבת 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.
עכשיו אפשר להטמיע חוויות משתמש מעולות באפליקציית המצלמה.