รองรับโหมดการแสดงผลแบบพับได้

อุปกรณ์แบบพับได้มอบประสบการณ์การรับชมที่ไม่เหมือนใคร โหมดการแสดงผลด้านหลังและโหมดหน้าจอคู่ช่วยให้คุณสร้างฟีเจอร์การแสดงผลพิเศษสำหรับอุปกรณ์แบบพับได้ เช่น การแสดงตัวอย่างเซลฟีด้วยกล้องหลังและโหมดหน้าจอทั้งด้านในและด้านนอกพร้อมกัน แต่จอแสดงผลจะไม่เหมือนกัน

โหมดจอแสดงผลด้านหลัง

โดยปกติแล้ว เมื่อกางอุปกรณ์แบบพับได้ออก จะมีเพียงหน้าจอด้านในเท่านั้นที่ทำงาน โหมดจอแสดงผลด้านหลังช่วยให้คุณย้ายกิจกรรมไปยังหน้าจอด้านนอกของอุปกรณ์แบบพับได้ ซึ่งมักจะหันออกจากผู้ใช้ขณะที่กางอุปกรณ์ออก จอแสดงผลด้านในจะปิดโดยอัตโนมัติ

แอปพลิเคชันนวนิยายอย่างหนึ่งคือการแสดงตัวอย่างของกล้องบนหน้าจอด้านนอกเพื่อให้ผู้ใช้สามารถถ่ายเซลฟีด้วยกล้องหลังซึ่งมักจะให้ประสิทธิภาพในการถ่ายภาพที่ดีกว่ากล้องหน้าเป็นอย่างมาก

ในการเปิดใช้งานโหมดการแสดงผลด้านหลัง ผู้ใช้จะตอบสนองต่อกล่องโต้ตอบเพื่ออนุญาตให้แอปสลับหน้าจอได้ เช่น

รูปที่ 1 กล่องโต้ตอบของระบบเพื่ออนุญาตให้เริ่มโหมดการแสดงผลด้านหลัง

ระบบจะสร้างกล่องโต้ตอบ ดังนั้นคุณจึงไม่จำเป็นต้องพัฒนาอะไรอีก กล่องโต้ตอบต่างๆ จะปรากฏขึ้นตามสถานะของอุปกรณ์ เช่น ระบบจะนำผู้ใช้ไปยังกางอุปกรณ์หากอุปกรณ์ปิดอยู่ คุณปรับแต่งกล่องโต้ตอบไม่ได้ และกล่องโต้ตอบอาจแตกต่างกันไปตามอุปกรณ์ซึ่งมาจาก OEM ต่างๆ

คุณสามารถลองใช้โหมดจอแสดงผลด้านหลังกับแอปกล้อง Pixel Fold ดูได้ ดูตัวอย่างการใช้งานได้ในโค้ดแล็บเพิ่มประสิทธิภาพแอปกล้องในอุปกรณ์แบบพับได้ด้วย WindowManager ของ Jetpack

โหมด 2 หน้าจอ

โหมด 2 หน้าจอช่วยให้คุณแสดงเนื้อหาบนจอแสดงผลทั้ง 2 จอพร้อมกันได้ โหมดหน้าจอคู่พร้อมใช้งานใน Pixel Fold ที่ใช้ Android 14 (API ระดับ 34) ขึ้นไป

ตัวอย่าง Use Case คือโปรแกรมแปลภาษาแบบ Dual Screen

รูปที่ 2 ล่ามแบบ 2 หน้าจอที่แสดงเนื้อหาที่แตกต่างกันบนจอแสดงผลด้านหน้าและด้านหลัง

เปิดใช้โหมดต่างๆ แบบเป็นโปรแกรม

คุณสามารถเข้าถึงโหมดจอแสดงผลด้านหลังและโหมดหน้าจอคู่ผ่าน Jetpack WindowManager API ได้ โดยเริ่มจากไลบรารีเวอร์ชัน 1.2.0-beta03

เพิ่มทรัพยากร Dependency ของ WindowManager ลงในไฟล์โมดูล build.gradle ของแอปดังนี้

ดึงดูดKotlin
dependencies {
    implementation
"androidx.window:window:1.2.0-beta03"
}
dependencies {
    implementation
("androidx.window:window:1.2.0-beta03")
}

จุดแรกเข้าคือ WindowAreaController ที่ให้ข้อมูลและลักษณะการทำงานที่เกี่ยวข้องกับการย้ายหน้าต่างระหว่างจอแสดงผลหรือระหว่างพื้นที่แสดงผลในอุปกรณ์ WindowAreaController ให้คุณค้นหารายการออบเจ็กต์ WindowAreaInfo ที่ใช้ได้

ใช้ WindowAreaInfo เพื่อเข้าถึง WindowAreaSession ซึ่งเป็นอินเทอร์เฟซที่แสดงถึงฟีเจอร์พื้นที่หน้าต่างที่ใช้งานอยู่ ใช้ WindowAreaSession เพื่อระบุความพร้อมจำหน่ายสินค้าของ WindowAreaCapability ที่เฉพาะเจาะจง

ซึ่งความสามารถแต่ละอย่างจะเกี่ยวข้องกับ WindowAreaCapability.Operation นั้นๆ ในเวอร์ชัน 1.2.0-beta03 Jetpack WindowManager รองรับการดำเนินการ 2 ประเภท ได้แก่

ต่อไปนี้คือตัวอย่างวิธีประกาศตัวแปรสำหรับโหมดจอแสดงผลด้านหลังและโหมดหน้าจอคู่ในกิจกรรมหลักของแอป

KotlinJava
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
private WindowAreaControllerCallbackAdapter windowAreaController = null;
private Executor displayExecutor = null;
private WindowAreaSessionPresenter windowAreaSession = null;
private WindowAreaInfo windowAreaInfo = null;
private WindowAreaCapability.Status capabilityStatus  =
       
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED;

private WindowAreaCapability.Operation dualScreenOperation =
       
WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA;
private WindowAreaCapability.Operation rearDisplayOperation =
       
WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA;

วิธีเริ่มต้นตัวแปรในเมธอด onCreate() ของกิจกรรมมีดังนี้

KotlinJava
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 -> windowAreaInfo = info }
           
.map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
           
.distinctUntilChanged()
           
.collect {
                capabilityStatus
= it
           
}
   
}
}
displayExecutor = ContextCompat.getMainExecutor(this);
windowAreaController
= new WindowAreaControllerCallbackAdapter(WindowAreaController.getOrCreate());
windowAreaController
.addWindowAreaInfoListListener(displayExecutor, this);

windowAreaController
.addWindowAreaInfoListListener(displayExecutor,
  windowAreaInfos
-> {
   
for(WindowAreaInfo newInfo : windowAreaInfos){
       
if(newInfo.getType().equals(WindowAreaInfo.Type.TYPE_REAR_FACING)){
            windowAreaInfo
= newInfo;
            capabilityStatus
= newInfo.getCapability(presentOperation).getStatus();
           
break;
       
}
   
}
});

ก่อนเริ่มการดำเนินการ ให้ตรวจสอบความพร้อมใช้งานของความสามารถที่ต้องการ ดังนี้

KotlinJava
when (capabilityStatus) {
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
     
// The selected display mode is not supported on this device.
   
}
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
     
// The selected display mode is not available.
   
}
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
     
// The selected display mode is available and can be enabled.
   
}
   
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
     
// The selected display mode is already active.
   
}
   
else -> {
     
// The selected display mode status is unknown.
   
}
}
if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED)) {
 
// The selected display mode is not supported on this device.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE)) {
 
// The selected display mode is not available.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE)) {
 
// The selected display mode is available and can be enabled.
}
else if (capabilityStatus.equals(WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE)) {
 
// The selected display mode is already active.
}
else {
 
// The selected display mode status is unknown.
}

โหมดหน้าจอคู่

ตัวอย่างต่อไปนี้จะปิดเซสชันหากความสามารถดังกล่าวทำงานอยู่ หรือเรียกใช้ฟังก์ชัน presentContentOnWindowArea()

KotlinJava
fun toggleDualScreenMode() {
   
if (windowAreaSession != null) {
        windowAreaSession
?.close()
   
}
   
else {
        windowAreaInfo
?.token?.let { token ->
            windowAreaController
.presentContentOnWindowArea(
                token
= token,
                activity
= this,
                executor
= displayExecutor,
                windowAreaPresentationSessionCallback
= this
           
)
       
}
   
}
}
private void toggleDualScreenMode() {
   
if(windowAreaSession != null) {
        windowAreaSession
.close();
   
}
   
else {
       
Binder token = windowAreaInfo.getToken();
        windowAreaController
.presentContentOnWindowArea( token, this, displayExecutor, this);
   
}
}

โปรดสังเกตการใช้กิจกรรมหลักของแอปเป็นอาร์กิวเมนต์ WindowAreaPresentationSessionCallback

API ใช้วิธีการ Listener กล่าวคือเมื่อคุณส่งคำขอเพื่อนำเสนอเนื้อหาไปยังหน้าจออีกเครื่องหนึ่งของอุปกรณ์แบบพับได้ คุณจะเริ่มต้นเซสชันที่ส่งกลับผ่านเมธอด onSessionStarted() ของผู้ฟัง เมื่อปิดเซสชัน คุณจะได้รับการยืนยันในเมธอด onSessionEnded()

ในการสร้าง Listener ให้ใช้อินเทอร์เฟซ WindowAreaPresentationSessionCallback ดังนี้

KotlinJava
class MainActivity : AppCompatActivity(), windowAreaPresentationSessionCallback
public class MainActivity extends AppCompatActivity implements WindowAreaPresentationSessionCallback

ผู้ฟังต้องใช้เมธอด onSessionStarted(), onSessionEnded(), และ onContainerVisibilityChanged() เมธอด Callback จะแจ้งให้ คุณทราบสถานะเซสชันและช่วยให้คุณอัปเดตแอปได้ตามความเหมาะสม

ฟังก์ชัน Callback ของ onSessionStarted() จะได้รับ WindowAreaSessionPresenter เป็นอาร์กิวเมนต์ โดยอาร์กิวเมนต์คือคอนเทนเนอร์ที่ช่วยให้คุณเข้าถึงพื้นที่หน้าต่างและแสดงเนื้อหาได้ ระบบอาจปิดงานนำเสนอโดยอัตโนมัติเมื่อผู้ใช้ออกจากหน้าต่างแอปพลิเคชันหลัก หรือปิดงานนำเสนอโดยเรียกใช้ WindowAreaSessionPresenter#close()

สําหรับการเรียกกลับอื่นๆ เพื่อความสะดวก ให้ตรวจสอบข้อผิดพลาดในเนื้อหาของฟังก์ชันและบันทึกสถานะ

KotlinJava
override fun onSessionStarted(session: WindowAreaSessionPresenter) {
    windowAreaSession
= session
   
val view = TextView(session.context)
    view
.text = "Hello world!"
    session
.setContentView(view)
}

override fun onSessionEnded(t: Throwable?) {
   
if(t != null) {
       
Log.e(logTag, "Something was broken: ${t.message}")
   
}
}

override fun onContainerVisibilityChanged(isVisible: Boolean) {
   
Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}
@Override
public void onSessionStarted(@NonNull WindowAreaSessionPresenter session) {
    windowAreaSession
= session;
   
TextView view = new TextView(session.getContext());
    view
.setText("Hello world, from the other screen!");
    session
.setContentView(view);
}

@Override public void onSessionEnded(@Nullable Throwable t) {
   
if(t != null) {
       
Log.e(logTag, "Something was broken: ${t.message}");
   
}
}

@Override public void onContainerVisibilityChanged(boolean isVisible) {
   
Log.d(logTag, "onContainerVisibilityChanged. isVisible = " + isVisible);
}

ใช้ไอคอนทางการของ Dual Screen เพื่อระบุให้ผู้ใช้เห็นถึงวิธีเปิดหรือปิดใช้โหมด Dual Screen เพื่อรักษาความสอดคล้องในระบบนิเวศ

ดูตัวอย่างที่ใช้งานได้ได้ที่ DualScreenActivity.kt

โหมดจอแสดงผลด้านหลัง

ตัวอย่างฟังก์ชัน toggleRearDisplayMode() ต่อไปนี้จะปิดเซสชันหากความสามารถดังกล่าวเปิดใช้งานอยู่แล้ว หรือจะเรียกใช้ฟังก์ชัน transferActivityToWindowArea() ก็ได้ ซึ่งคล้ายกับตัวอย่างโหมดหน้าจอคู่

KotlinJava
fun toggleRearDisplayMode() {
   
if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
       
if(windowAreaSession == null) {
            windowAreaSession
= windowAreaInfo?.getActiveSession(
                operation
           
)
       
}
        windowAreaSession
?.close()
   
} else {
        windowAreaInfo
?.token?.let { token ->
            windowAreaController
.transferActivityToWindowArea(
                token
= token,
                activity
= this,
                executor
= displayExecutor,
                windowAreaSessionCallback
= this
           
)
       
}
   
}
}
void toggleDualScreenMode() {
   
if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
       
if(windowAreaSession == null) {
            windowAreaSession
= windowAreaInfo.getActiveSession(
                operation
           
)
       
}
        windowAreaSession
.close()
   
}
   
else {
       
Binder token = windowAreaInfo.getToken();
        windowAreaController
.transferActivityToWindowArea(token, this, displayExecutor, this);
   
}
}

ในกรณีนี้ ระบบจะใช้กิจกรรมที่แสดงเป็น WindowAreaSessionCallback ซึ่งติดตั้งใช้งานได้ง่ายกว่า เนื่องจากคอลแบ็กไม่ได้รับตัวนําเสนอที่อนุญาตให้แสดงเนื้อหาในพื้นที่หน้าต่าง แต่โอนกิจกรรมทั้งหมดไปยังอีกพื้นที่หนึ่งแทน

KotlinJava
override fun onSessionStarted() {
   
Log.d(logTag, "onSessionStarted")
}

override fun onSessionEnded(t: Throwable?) {
   
if(t != null) {
       
Log.e(logTag, "Something was broken: ${t.message}")
   
}
}
@Override public void onSessionStarted(){
   
Log.d(logTag, "onSessionStarted");
}

@Override public void onSessionEnded(@Nullable Throwable t) {
   
if(t != null) {
       
Log.e(logTag, "Something was broken: ${t.message}");
   
}
}

ใช้ไอคอนอย่างเป็นทางการของกล้องหลังเพื่อระบุให้ผู้ใช้เห็นถึงวิธีเปิดหรือปิดใช้โหมดจอแสดงผลด้านหลัง เพื่อความสอดคล้องในระบบนิเวศ

แหล่งข้อมูลเพิ่มเติม