1. ก่อนเริ่มต้น
โทรศัพท์แบบพับได้มีความพิเศษอย่างไร
อุปกรณ์แบบพับได้เป็นนวัตกรรมที่เกิดขึ้นเพียงครั้งเดียวในยุคนี้ อุปกรณ์เหล่านี้มอบประสบการณ์ที่ไม่เหมือนใคร และมาพร้อมโอกาสที่ไม่เหมือนใครในการสร้างความพึงพอใจให้แก่ผู้ใช้ด้วยฟีเจอร์ที่แตกต่าง เช่น UI บนโต๊ะสำหรับการใช้งานแบบแฮนด์ฟรี
ข้อกำหนดเบื้องต้น
- ความรู้พื้นฐานเกี่ยวกับการพัฒนาแอป Android
- มีความรู้พื้นฐานเกี่ยวกับเฟรมเวิร์กการขึ้นต่อกันของ Hilt
สิ่งที่คุณจะสร้าง
ในโค้ดแล็บนี้ คุณจะได้สร้างแอปกล้องที่มีเลย์เอาต์ที่เพิ่มประสิทธิภาพสำหรับอุปกรณ์แบบพับได้

คุณเริ่มต้นด้วยแอปกล้องพื้นฐานที่ไม่ตอบสนองต่อท่าทางของอุปกรณ์หรือใช้ประโยชน์จากกล้องหลังที่ดีกว่าเพื่อถ่ายเซลฟีที่ดียิ่งขึ้น คุณอัปเดตซอร์สโค้ดเพื่อย้ายตัวอย่างไปยังจอแสดงผลขนาดเล็กเมื่ออุปกรณ์กางออก และตอบสนองต่อการตั้งค่าโทรศัพท์ในโหมดบนโต๊ะ
แม้ว่าแอปกล้องจะเป็นกรณีการใช้งานที่สะดวกที่สุดสำหรับ API นี้ แต่คุณสามารถนำฟีเจอร์ทั้ง 2 อย่างที่ได้เรียนรู้ในโค้ดแล็บนี้ไปใช้กับแอปใดก็ได้
สิ่งที่คุณจะได้เรียนรู้
- วิธีใช้ Jetpack Window Manager เพื่อตอบสนองต่อการเปลี่ยนท่าทาง
- วิธีย้ายแอปไปยังจอแสดงผลขนาดเล็กของอุปกรณ์พับได้
สิ่งที่คุณต้องมี
- 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 ที่อธิบายรอยพับในจอแสดงผลแบบยืดหยุ่นหรือบานพับระหว่างแผงจอแสดงผลจริง 2 แผง 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 ในแอป คุณต้องเพิ่มทรัพยากร Dependency ต่อไปนี้ลงในไฟล์
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 ทำงานด้วยแนวทางของ Listener กล่าวคือ เมื่อคุณขอย้ายเนื้อหาไปยังจอแสดงผลอื่น คุณจะเริ่มเซสชันที่ส่งคืนผ่านonSessionStarted()เมธอดของ Listener หากต้องการกลับไปที่จอแสดงผลด้านใน (และใหญ่กว่า) ให้ปิดเซสชัน แล้วคุณจะได้รับการยืนยันในonSessionEnded()วิธี หากต้องการสร้าง Listener ดังกล่าว คุณต้องใช้อินเทอร์เฟซ WindowAreaSessionCallback
- แก้ไข
MainActivityประกาศเพื่อให้ใช้WindowAreaSessionCallbackอินเทอร์เฟซ
step1/MainActivity.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
ตอนนี้ ให้ใช้เมธอด onSessionStarted และ onSessionEnded ภายใน MainActivity เมธอด Callback เหล่านี้มีประโยชน์อย่างยิ่งในการรับการแจ้งเตือนเกี่ยวกับสถานะเซสชันและอัปเดตแอปตามนั้น
แต่ครั้งนี้เพื่อความง่าย ให้ตรวจสอบในส่วนเนื้อหาของฟังก์ชันว่ามีข้อผิดพลาดหรือไม่ และบันทึกสถานะ
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 ซึ่งสร้างขึ้นด้วยเมธอดแบบคงที่ที่ต้องใช้ 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 ได้
ขั้นตอนสุดท้ายคือการตอบสนองต่อการเปลี่ยนแปลงเหล่านี้และย้ายเนื้อหาตามนั้น คุณจะดำเนินการนี้ภายในเมธอด updateLayoutByFoldingState() ทีละขั้นตอน
- ตรวจสอบว่า
activityLayoutInfoมีพร็อพเพอร์ตี้DisplayFeatureบางรายการ และมีพร็อพเพอร์ตี้อย่างน้อย 1 รายการเป็น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. ยินดีด้วย
ในโค้ดแล็บนี้ คุณได้เรียนรู้เกี่ยวกับความสามารถบางอย่างที่มีเฉพาะในอุปกรณ์พับได้ เช่น โหมดจอแสดงผลด้านหลังหรือโหมดบนโต๊ะ และวิธีปลดล็อกความสามารถเหล่านี้โดยใช้ Jetpack WindowManager
คุณพร้อมที่จะมอบประสบการณ์การใช้งานที่ยอดเยี่ยมให้กับแอปกล้องแล้ว