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