1. ก่อนเริ่มต้น
อุปกรณ์แบบพับได้พิเศษอย่างไร
อุปกรณ์แบบพับได้เป็นนวัตกรรมที่ไม่เคยมีมาก่อน เครื่องมือเหล่านี้มอบประสบการณ์การใช้งานที่ไม่เหมือนใคร และมาพร้อมกับโอกาสพิเศษในการสร้างความประทับใจให้ผู้ใช้ด้วยฟีเจอร์ต่างๆ ที่แตกต่าง เช่น UI แบบตั้งโต๊ะเพื่อการใช้งานแบบแฮนด์ฟรี
ข้อกำหนดเบื้องต้น
- ความรู้พื้นฐานเกี่ยวกับการพัฒนาแอป Android
- ความรู้พื้นฐานเกี่ยวกับเฟรมเวิร์ก Hilt Dependency Injection
สิ่งที่คุณจะสร้าง
ใน Codelab นี้ คุณจะได้สร้างแอปกล้องที่มีเลย์เอาต์ที่เพิ่มประสิทธิภาพสำหรับอุปกรณ์แบบพับได้
คุณเริ่มต้นด้วยแอปกล้องพื้นฐานที่ไม่ตอบสนองต่อท่าทางใดๆ ของอุปกรณ์ หรือใช้ประโยชน์จากกล้องหลังที่ดีกว่าเพื่อให้ได้ภาพเซลฟีที่ดียิ่งขึ้น คุณอัปเดตซอร์สโค้ดเพื่อย้ายตัวอย่างไปยังจอแสดงผลขนาดเล็กเมื่อกางอุปกรณ์ออกและโต้ตอบกับโทรศัพท์ที่ตั้งค่าในโหมดบนโต๊ะ
แม้ว่าแอปกล้องถ่ายรูปจะเป็นกรณีการใช้งานที่สะดวกที่สุดสำหรับ API นี้ แต่ฟีเจอร์ทั้ง 2 อย่างที่คุณได้เรียนรู้ใน Codelab นี้สามารถนำไปใช้กับแอปใดก็ได้
สิ่งที่คุณจะได้เรียนรู้
- วิธีใช้ Jetpack Window Manager เพื่อตอบสนองต่อการเปลี่ยนท่า
- วิธีย้ายแอปไปยังจอแสดงผลขนาดเล็กของอุปกรณ์แบบพับได้
สิ่งที่คุณต้องมี
- Android Studio เวอร์ชันล่าสุด
- อุปกรณ์แบบพับได้หรือโปรแกรมจำลองแบบพับได้
2. ตั้งค่า
รับโค้ดเริ่มต้น
- หากคุณติดตั้ง Git ไว้ คุณสามารถเรียกใช้คำสั่งด้านล่างนี้ หากต้องการตรวจสอบว่ามีการติดตั้ง Git หรือไม่ ให้พิมพ์
git --version
ในเทอร์มินัลหรือบรรทัดคำสั่งและตรวจสอบว่า Git ทำงานได้อย่างถูกต้อง
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 ที่ช่วยให้โหมดจอแสดงผลด้านหลังเลื่อนหน้าต่างปัจจุบันไปยังจอแสดงผลที่สอดคล้องกับกล้องหลังได้ ซึ่งเหมาะสำหรับการถ่ายเซลฟีด้วยกล้องหลังและกรณีการใช้งานอื่นๆ อีกมากมาย
เพิ่มทรัพยากร Dependency
- หากต้องการใช้ 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()
ของผู้ฟัง หากต้องการกลับไปที่จอแสดงผลด้านใน (และใหญ่กว่า) ให้ปิดเซสชันและได้รับการยืนยันในเมธอด onSessionEnded()
หากต้องการสร้าง Listener ดังกล่าว คุณต้องติดตั้งอินเทอร์เฟซ WindowAreaSessionCallback
- แก้ไขการประกาศ
MainActivity
เพื่อใช้อินเทอร์เฟซWindowAreaSessionCallback
ดังนี้
step1/MainActivity.kt
class MainActivity : AppCompatActivity(), WindowAreaSessionCallback
ต่อไป ให้ใช้เมธอด onSessionStarted
และ onSessionEnded
ภายใน MainActivity
วิธีการติดต่อกลับดังกล่าวมีประโยชน์อย่างยิ่งในการรับการแจ้งเตือนสถานะเซสชันและอัปเดตแอปตามนั้น
แต่คราวนี้เพื่อความง่าย ให้ตรวจสอบในส่วนเนื้อหาของฟังก์ชันว่ามีข้อผิดพลาดอะไรไหม แล้วบันทึกสถานะไว้
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/cameralabDependencies.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
เพื่อฟังการเปลี่ยนแปลงเลย์เอาต์
ขั้นตอนที่ 1/FoldingStateActor.kt
windowInfoTracker.windowLayoutInfo(activity)
.collect { newLayoutInfo ->
activeWindowLayoutInfo = newLayoutInfo
updateLayoutByFoldingState(cameraViewfinder)
}
เมื่อใช้อินเทอร์เฟซ WindowInfoTracker
คุณจะสามารถเรียกใช้ windowLayoutInfo()
เพื่อรวบรวม Flow
จาก WindowLayoutInfo
ที่มีข้อมูลทั้งหมดที่มีใน DisplayFeature
ขั้นตอนสุดท้ายคือตอบสนองต่อการเปลี่ยนแปลงเหล่านี้และย้ายเนื้อหาตามนั้น โดยทำในเมธอด updateLayoutByFoldingState()
ทีละรายการ
- ตรวจสอบว่า
activityLayoutInfo
มีพร็อพเพอร์ตี้DisplayFeature
บางรายการ และมีพร็อพเพอร์ตี้อย่างน้อย 1 รายการเป็นFoldingFeature
มิเช่นนั้นคุณไม่ต้องดำเนินการใดๆ ดังนี้
ขั้นตอนที่ 1/FoldingStateActor.kt
val foldingFeature = activeWindowLayoutInfo?.displayFeatures
?.firstOrNull { it is FoldingFeature } as FoldingFeature?
?: return
- คำนวณตำแหน่งของเส้นแบ่งครึ่งจอเพื่อให้ตำแหน่งอุปกรณ์ส่งผลต่อเลย์เอาต์และไม่ได้อยู่นอกขอบเขตของลำดับชั้น
ขั้นตอนที่ 1/FoldingStateActor.kt
val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
foldingFeature,
cameraViewfinder.parent as View
) ?: return
ตอนนี้คุณก็แน่ใจแล้วว่าตัวเองมี FoldingFeature
ที่ส่งผลต่อเลย์เอาต์ คุณจึงต้องย้ายเนื้อหา
- ตรวจสอบว่า
FoldingFeature
เป็นHALF_OPEN
หรือเป็นเพียงการคืนค่าตำแหน่งเนื้อหา หากเป็นHALF_OPEN
คุณต้องดำเนินการตรวจสอบอีกครั้งและดำเนินการแตกต่างกันไปตามการวางแนวของแนวเส้นแบ่งครึ่งหน้า ดังนี้
ขั้นตอนที่ 1/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. ยินดีด้วย
Codelab นี้ได้เรียนรู้เกี่ยวกับความสามารถบางอย่างที่ไม่เหมือนใครของอุปกรณ์แบบพับได้ เช่น โหมดจอแสดงผลด้านหลังหรือโหมดบนโต๊ะ และวิธีปลดล็อกสิ่งเหล่านี้โดยใช้ Jetpack WindowManager
คุณพร้อมที่จะนำประสบการณ์ที่ยอดเยี่ยมของผู้ใช้ไปใช้กับแอปกล้องแล้ว