מסכים גדולים במצב פתוח ומצבים ייחודיים במצב מקופל מאפשרים חוויית משתמש חדשה במכשירים מתקפלים. כדי שהאפליקציה תתאים למכשירים מתקפלים, צריך להשתמש בספריית Jetpack WindowManager, שמספקת ממשק API לתכונות של חלונות במכשירים מתקפלים, כמו קיפולים ומפרקים. כשהאפליקציה מודעת לקיפולים, היא יכולה להתאים את הפריסה שלה כדי להימנע מהצבת תוכן חשוב באזור הקיפולים או הצירים, ולהשתמש בקיפולים ובצירים כמפרידים טבעיים.
הבנה אם מכשיר תומך בתצורות כמו תצורת שולחן או תצורת ספר יכולה לעזור לכם להחליט אם לתמוך בפריסות שונות או לספק תכונות ספציפיות.
מידע על החלון
ממשק WindowInfoTracker ב-Jetpack WindowManager חושף מידע על פריסת החלון. השיטה windowLayoutInfo() של הממשק מחזירה זרם של נתוני WindowLayoutInfo שמספקים לאפליקציה מידע על מצב הקיפול של מכשיר מתקפל. השיטה WindowInfoTracker#getOrCreate() יוצרת מופע של WindowInfoTracker.
WindowManager מספק תמיכה באיסוף WindowLayoutInfo נתונים באמצעות Kotlin flows ו-Java callbacks.
תהליכים ב-Kotlin
כדי להתחיל ולאסוף נתונים של WindowLayoutInfo, אפשר להשתמש בקורוטינה עם מודעות למחזור החיים שאפשר להפעיל מחדש, שבה בלוק הקוד repeatOnLifecycle מופעל כשמחזור החיים הוא לפחות STARTED, ומופסק כשמחזור החיים הוא STOPPED. ההרצה של בלוק הקוד מופעלת מחדש באופן אוטומטי כשהמחזור הוא STARTED שוב. בדוגמה הבאה, בלוק הקוד אוסף נתוני WindowLayoutInfo ומשתמש בהם:
class DisplayFeaturesActivity : AppCompatActivity() {
private lateinit var binding: ActivityDisplayFeaturesBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
החזרות (callbacks) ב-Java
שכבת התאימות של הקריאה החוזרת שכלולה בתלות androidx.window:window-java מאפשרת לכם לאסוף עדכוני WindowLayoutInfo בלי להשתמש ב-Kotlin flow. הארטיפקט כולל את המחלקה WindowInfoTrackerCallbackAdapter, שמתאימה את WindowInfoTracker כדי לתמוך ברישום (ובביטול הרישום) של קריאות חוזרות (callback) לקבלת עדכונים של WindowLayoutInfo, למשל:
public class SplitLayoutActivity extends AppCompatActivity {
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private ActivitySplitLayoutBinding binding;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker
.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
SplitLayoutActivity.this.runOnUiThread( () -> {
// Use newLayoutInfo to update the layout.
});
}
}
}
תמיכה ב-RxJava
אם אתם כבר משתמשים ב-RxJava (גרסה 2 או 3), תוכלו להשתמש בארטיפקטים שמאפשרים לכם להשתמש ב-Observable או ב-Flowable כדי לאסוף עדכוני WindowLayoutInfo בלי להשתמש בזרימת Kotlin.
שכבת התאימות שמסופקת על ידי יחסי התלות androidx.window:window-rxjava2 ו-androidx.window:window-rxjava3 כוללת את השיטות WindowInfoTracker#windowLayoutInfoFlowable() ו-WindowInfoTracker#windowLayoutInfoObservable(), שמאפשרות לאפליקציה לקבל עדכונים של WindowLayoutInfo, לדוגמה:
class RxActivity: AppCompatActivity {
private lateinit var binding: ActivityRxBinding
private var disposable: Disposable? = null
private lateinit var observable: Observable<WindowLayoutInfo>
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySplitLayoutBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
// Create a new observable.
observable = WindowInfoTracker.getOrCreate(this@RxActivity)
.windowLayoutInfoObservable(this@RxActivity)
}
@Override
protected void onStart() {
super.onStart();
// Subscribe to receive WindowLayoutInfo updates.
disposable?.dispose()
disposable = observable
.observeOn(AndroidSchedulers.mainThread())
.subscribe { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
@Override
protected void onStop() {
super.onStop();
// Dispose of the WindowLayoutInfo observable.
disposable?.dispose()
}
}
תכונות של מסכים מתקפלים
המחלקות WindowLayoutInfo של Jetpack WindowManager מאפשרות להשתמש בתכונות של חלון תצוגה כרשימה של רכיבי DisplayFeature.
FoldingFeature הוא סוג של DisplayFeature שמספק מידע על מסכים מתקפלים, כולל המאפיינים הבאים:
state: מצב הקיפול של המכשיר,FLATאוHALF_OPENED
orientation: הכיוון של הקיפול או הציר,HORIZONTALאוVERTICAL
occlusionType: האם הקיפול או הציר מסתירים חלק מהמסך,NONEאוFULL
isSeparating: האם הקיפול או הציר יוצרים שני אזורי תצוגה לוגיים, true או false
מכשיר מתקפל שתמיד מדווח על HALF_OPENED כ-true, כי המסך מחולק לשני אזורי תצוגה.isSeparating בנוסף, הערך של isSeparating הוא תמיד True במכשיר עם שני מסכים כשהאפליקציה מוצגת בשני המסכים.
המאפיין FoldingFeature bounds (שעובר בירושה מ-DisplayFeature)
מייצג את המלבן התוחם של תכונת קיפול, כמו קיפול או ציר.
אפשר להשתמש בגבולות כדי למקם רכיבים במסך ביחס לתכונה:
Kotlin
override fun onCreate(savedInstanceState: Bundle?) {
// ...
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collects from WindowInfoTracker when the lifecycle is
// STARTED and stops collection when the lifecycle is STOPPED.
WindowInfoTracker.getOrCreate(this@MainActivity)
.windowLayoutInfo(this@MainActivity)
.collect { layoutInfo ->
// New posture information.
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
// Use information from the foldingFeature object.
}
}
}
}
Java
private WindowInfoTrackerCallbackAdapter windowInfoTracker;
private final LayoutStateChangeCallback layoutStateChangeCallback =
new LayoutStateChangeCallback();
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// ...
windowInfoTracker =
new WindowInfoTrackerCallbackAdapter(WindowInfoTracker.getOrCreate(this));
}
@Override
protected void onStart() {
super.onStart();
windowInfoTracker.addWindowLayoutInfoListener(
this, Runnable::run, layoutStateChangeCallback);
}
@Override
protected void onStop() {
super.onStop();
windowInfoTracker.removeWindowLayoutInfoListener(layoutStateChangeCallback);
}
class LayoutStateChangeCallback implements Consumer<WindowLayoutInfo> {
@Override
public void accept(WindowLayoutInfo newLayoutInfo) {
// Use newLayoutInfo to update the Layout.
List<DisplayFeature> displayFeatures = newLayoutInfo.getDisplayFeatures();
for (DisplayFeature feature : displayFeatures) {
if (feature instanceof FoldingFeature) {
// Use information from the feature object.
}
}
}
}
מצב שולחני
באמצעות המידע שכלול באובייקט FoldingFeature, האפליקציה יכולה לתמוך במצבים כמו מצב שולחן, שבו הטלפון מונח על משטח, הציר נמצא במצב אופקי והמסך המתקפל פתוח בחצי.
ההצבה על השולחן מאפשרת למשתמשים לתפעל את הטלפונים שלהם בלי להחזיק אותם בידיים. המיקום השולחני מצוין לצפייה במדיה, לצילום תמונות ולביצוע שיחות וידאו.
כדי לקבוע אם המכשיר נמצא במצב שולחני, משתמשים ב-FoldingFeature.State וב-FoldingFeature.Orientation:
Kotlin
fun isTableTopPosture(foldFeature : FoldingFeature?) : Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}
Java
boolean isTableTopPosture(FoldingFeature foldFeature) {
return (foldFeature != null) &&
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
(foldFeature.getOrientation() == FoldingFeature.Orientation.HORIZONTAL);
}
אחרי שקובעים שהמכשיר נמצא במצב שולחני, מעדכנים את פריסת האפליקציה בהתאם. באפליקציות מדיה, המשמעות היא בדרך כלל הצבת ההפעלה מעל הקו הדמיוני, ומיקום אמצעי הבקרה והתוכן הנוסף מיד אחריו, כדי לאפשר חוויית צפייה או האזנה ללא מגע.
ב-Android 15 (רמת API 35) ומעלה, אפשר להפעיל API סינכרוני כדי לזהות אם מכשיר תומך במצב שולחן, בלי קשר למצב הנוכחי של המכשיר.
ה-API מספק רשימה של תנוחות שהמכשיר תומך בהן. אם הרשימה כוללת תנוחה של מכשיר במצב שולחן, אפשר לפצל את פריסת האפליקציה כדי לתמוך בתנוחה ולהריץ בדיקות A/B בממשק המשתמש של האפליקציה עבור פריסות של מצב שולחן ומסך מלא.
Kotlin
if (WindowSdkExtensions.getInstance().extensionsVersion >= 6) {
val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
if (postures.contains(TABLE_TOP)) {
// Device supports tabletop posture.
}
}
Java
if (WindowSdkExtensions.getInstance().getExtensionVersion() >= 6) {
List<SupportedPosture> postures = WindowInfoTracker.getOrCreate(context).getSupportedPostures();
if (postures.contains(SupportedPosture.TABLETOP)) {
// Device supports tabletop posture.
}
}
דוגמאות
אפליקציית
MediaPlayerActivity: במאמר הזה מוסבר איך להשתמש ב-Media3 Exoplayer וב-WindowManager כדי ליצור נגן וידאו שמתאים למכשירים מתקפלים.אופטימיזציה של אפליקציית המצלמה במכשירים מתקפלים באמצעות Jetpack WindowManager סדנת קוד: איך מטמיעים מצב שולחן באפליקציות צילום. הצגת עינית המצלמה בחלק העליון של המסך (מעל הקו) והפקדים בחלק התחתון (מתחת לקו).
מצב הספר
תכונה ייחודית נוספת במכשירים מתקפלים היא מצב ספר, שבו המכשיר פתוח בחצי והציר אנכי. המיקום של הספר מצוין לקריאת ספרים דיגיטליים. פריסה של שני דפים במסך גדול של מכשיר מתקפל פתוח, כמו ספר כרוך, מאפשרת ליהנות מחוויה דומה לקריאת ספר אמיתי.
אפשר להשתמש בו גם לצילום אם רוצים לצלם תמונות ללא מגע יד ביחס גובה-רוחב שונה.
מטמיעים את מצב הספר באותן טכניקות שבהן משתמשים למצב השולחן. ההבדל היחיד הוא שבקוד צריך לבדוק שהכיוון של תכונת הקיפול הוא אנכי ולא אופקי:
Kotlin
fun isBookPosture(foldFeature : FoldingFeature?) : Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}
Java
boolean isBookPosture(FoldingFeature foldFeature) {
return (foldFeature != null) &&
(foldFeature.getState() == FoldingFeature.State.HALF_OPENED) &&
(foldFeature.getOrientation() == FoldingFeature.Orientation.VERTICAL);
}
שינויים בגודל החלון
אזור התצוגה של אפליקציה יכול להשתנות כתוצאה משינוי בהגדרת המכשיר. לדוגמה, כשהמכשיר מקופל או נפתח, מסובב או כשמשנים את הגודל של חלון במצב מרובה חלונות.
המחלקות Jetpack WindowManager WindowMetricsCalculator מאפשרות לאחזר את מדדי החלון הנוכחיים והמקסימליים. בדומה לפלטפורמה WindowMetrics שהוצגה ברמת API 30, המחלקה WindowManager
WindowMetrics מספקת את גבולות החלון, אבל ה-API תואם לאחור עד רמת API 14.
איך משתמשים בסיווגים של גודל החלון
מקורות מידע נוספים
טעימות
- Jetpack WindowManager: דוגמה לשימוש בספריית Jetpack WindowManager
- Jetcaster : הטמעה של תנוחת ישיבה נכונה באמצעות Compose
Codelabs
- תמיכה במכשירים מתקפלים ובמכשירים עם שני מסכים באמצעות Jetpack WindowManager
- אופטימיזציה של אפליקציית המצלמה במכשירים מתקפלים באמצעות Jetpack WindowManager