Màn hình lớn, gập lại được và các trạng thái gập độc đáo mang đến trải nghiệm người dùng mới mẻ trên các thiết bị có thể gập lại. Để giúp ứng dụng của bạn nhận biết được màn hình đầu tiên, hãy sử dụng thư viện Jetpack WindowManager, thư viện này cung cấp bề mặt API cho các tính năng có thể gập lại của cửa sổ thiết bị như màn hình đầu tiên và bản lề. Khi nhận biết được ứng dụng của bạn trong màn hình đầu tiên, ứng dụng có thể điều chỉnh bố cục để tránh đặt nội dung quan trọng trong màn hình đầu tiên hoặc bản lề, đồng thời sử dụng đường gấp và bản lề làm dấu phân tách tự nhiên.
Việc hiểu rõ liệu một thiết bị có hỗ trợ các cấu hình như tư thế trên mặt bàn hay tư thế sách có thể giúp bạn đưa ra quyết định về việc hỗ trợ nhiều bố cục hoặc cung cấp các tính năng cụ thể.
Thông tin về cửa sổ
Giao diện WindowInfoTracker trong Jetpack WindowManager hiển thị thông tin về bố cục cửa sổ. Phương thức windowLayoutInfo() của giao diện sẽ trả về
một luồng dữ liệu WindowLayoutInfo để thông báo cho ứng dụng của bạn về trạng thái của thiết bị có thể gập lại. Phương thức WindowInfoTracker#getOrCreate() tạo một bản sao của WindowInfoTracker.
WindowManager hỗ trợ thu thập dữ liệu WindowLayoutInfo bằng cách sử dụng luồng Kotlin và lệnh gọi lại Java.
Flow Kotlin
Để bắt đầu và dừng quá trình thu thập dữ liệu WindowLayoutInfo, bạn có thể sử dụng coroutine nhận biết vòng đời có thể bắt đầu lại trong đó khối mã repeatOnLifecycle được thực thi khi vòng đời tối thiểu là STARTED và đã dừng khi vòng đời là STOPPED. Việc thực thi khối mã sẽ tự động bắt đầu lại khi vòng đời là STARTED trở lại. Trong ví dụ sau, khối mã thu thập và sử dụng dữ liệu 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.
}
}
}
}
}
Các lệnh gọi lại Java
Lớp khả năng tương thích của lệnh gọi lại có trong phần phụ thuộc
androidx.window:window-java cho phép bạn thu thập nội dung cập nhật của
WindowLayoutInfo mà không cần dùng luồng Kotlin. Cấu phần phần mềm bao gồm
lớp WindowInfoTrackerCallbackAdapter, điều chỉnh
WindowInfoTracker để hỗ trợ lệnh gọi lại (và hủy đăng ký) để
nhận các bản cập nhật WindowLayoutInfo, ví dụ:
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.
});
}
}
}
Hỗ trợ RxJava
Nếu đã sử dụng RxJava (phiên bản 2 hoặc 3),
bạn có thể tận dụng các cấu phần phần mềm cho phép bạn sử dụng
Observable hoặc Flowable
để thu thập nội dung cập nhật của WindowLayoutInfo mà không cần dùng luồng Kotlin.
Lớp tương thích do androidx.window:window-rxjava2 và
androidx.window:window-rxjava3 cung cấp bao gồm phương thức
WindowInfoTracker#windowLayoutInfoFlowable() và
WindowInfoTracker#windowLayoutInfoObservable(), cho phép ứng dụng của bạn nhận được thông tin cập nhật WindowLayoutInfo, ví dụ:
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()
}
}
Tính năng của màn hình có thể gập
Lớp WindowLayoutInfo của Jetpack WindowsManager cung cấp các tính năng của cửa sổ hiển thị dưới dạng danh sách các phần tử DisplayFeature.
A FoldingFeature là một loại DisplayFeature cung cấp thông tin
về màn hình có thể gập lại, bao gồm các thuộc tính sau:
state: Trạng thái gập của thiết bị,FLAThoặcHALF_OPENEDorientation: Hướng của màn hình đầu tiên hoặc bản lề,HORIZONTALhoặcVERTICALocclusionType: Liệu màn hình gập hay bản lề có che khuất một phần màn hình hay không,NONEhoặcFULLisSeparating: Đường ranh giới phần hiển thị hoặc bản lề tạo ra hai khu vực hiển thị hợp lý, true hay false
Thiết bị có thể gập lại HALF_OPENED luôn báo cáo isSeparating là true vì màn hình được chia thành hai khu vực hiển thị. Ngoài ra, isSeparating luôn đúng trên thiết bị màn hình đôi khi ứng dụng kéo dài trên cả hai màn hình.
Thuộc tính FoldingFeature bounds (kế thừa từ DisplayFeature)
đại diện cho hình chữ nhật ranh giới của một tính năng gập lại, chẳng hạn như đường gập hoặc bản lề.
Bạn có thể dùng các giới hạn để định vị các thành phần trên màn hình tương ứng với tính năng này:
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.
}
}
}
}
Tư thế trên mặt bàn
Khi sử dụng thông tin có trong đối tượng FoldingFeature, ứng dụng của bạn có thể hỗ trợ các tư thế như tư thế trên mặt bàn, khi điện thoại nằm trên một bề mặt, bản lề ở vị trí nằm ngang và màn hình có thể gập lại đang mở một nửa.
Tư thế trên mặt bàn giúp người dùng thao tác thuận tiện với điện thoại mà không cần cầm điện thoại trên tay. Tư thế trên mặt bàn rất phù hợp để xem nội dung nghe nhìn, chụp ảnh và gọi video.
Sử dụng FoldingFeature.State và FoldingFeature.Orientation để xác định
xem thiết bị có đang ở tư thế trên mặt bàn hay không:
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);
}
Sau khi bạn biết thiết bị đang ở tư thế trên mặt bàn, hãy cập nhật bố cục ứng dụng cho phù hợp. Đối với các ứng dụng đa phương tiện, điều đó thường có nghĩa là đặt chế độ phát trong màn hình đầu tiên và đặt các nút điều khiển cũng như nội dung bổ sung ngay bên dưới để mang đến trải nghiệm xem hoặc nghe ở chế độ rảnh tay.
Trên Android 15 (cấp độ API 35) trở lên, bạn có thể gọi một API đồng bộ để phát hiện xem thiết bị có hỗ trợ tư thế trên mặt bàn hay không, bất kể trạng thái hiện tại của thiết bị.
API này cung cấp danh sách các tư thế mà thiết bị hỗ trợ. Nếu danh sách chứa tư thế trên mặt bàn, bạn có thể chia bố cục ứng dụng để hỗ trợ tư thế này và chạy thử nghiệm A/B trên giao diện người dùng của ứng dụng cho bố cục trên mặt bàn và toàn màn hình.
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.
}
}
Ví dụ
MediaPlayerActivityứng dụng: Xem cách sử dụng Media3 Exoplayer và WindowManager để tạo một trình phát video có tính năng nhận biết chế độ gập.Tối ưu hoá ứng dụng máy ảnh trên thiết bị có thể gập lại nhờ Jetpack WindowManager lớp học lập trình: Tìm hiểu cách triển khai tư thế trên mặt bàn cho các ứng dụng chụp ảnh. Hiển thị kính ngắm ở nửa trên của màn hình (trong màn hình đầu tiên) và các nút điều khiển ở nửa dưới cùng (dưới màn hình đầu tiên).
Tư thế sách
Một tính năng độc đáo khác của thiết bị có thể gập lại là tư thế sách, trong đó thiết bị được mở một nửa và bản lề thẳng đứng. Tư thế sách rất phù hợp để đọc sách điện tử. Với bố cục 2 trang trên một màn hình lớn có thể gập lại và mở ra như một cuốn sách đóng bìa, tư thế sách sẽ mang lại trải nghiệm đọc như khi bạn đọc một cuốn sách thực.
Bạn cũng có thể dùng chế độ này để chụp ảnh nếu muốn chụp ảnh ở một tỷ lệ khung hình khác mà không cần dùng tay.
Triển khai tư thế sách bằng các kỹ thuật dùng để triển khai tư thế trên mặt bàn. Điểm khác biệt duy nhất là mã phải kiểm tra để đảm bảo rằng hướng của tính năng gập là hướng dọc thay vì hướng ngang:
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);
}
Thay đổi kích thước cửa sổ
Khu vực hiển thị của ứng dụng có thể thay đổi do thay đổi cấu hình thiết bị, ví dụ: khi thiết bị được gập lại hoặc gập lại, xoay hoặc cửa sổ được thay đổi kích thước ở chế độ nhiều cửa sổ.
Lớp WindowMetricsCalculator của Jetpack WindowManager cho phép bạn
truy xuất các chỉ số thời lượng hiện tại và tối đa. Giống như nền tảng
WindowMetrics được giới thiệu trong cấp độ API 30, WindowManager
WindowMetrics cung cấp các giới hạn cửa sổ, nhưng API tương thích ngược
với cấp độ API 14.
Xem bài viết Sử dụng các lớp kích thước cửa sổ.
Tài nguyên khác
Mẫu
- Jetpack WindowManager: Ví dụ về cách sử dụng thư viện Jetpack WindowsManager
- Jetcaster : Triển khai tư thế trên mặt bàn bằng Compose
Lớp học lập trình
- Hỗ trợ thiết bị gập được và thiết bị màn hình đôi nhờ Jetpack WindowManager
- Tối ưu hoá ứng dụng máy ảnh trên thiết bị có thể gập lại nhờ Jetpack WindowManager