เปิดประสบการณ์การใช้งานกล้องของคุณ

1. ก่อนเริ่มต้น

โทรศัพท์แบบพับได้มีความพิเศษอย่างไร

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

ข้อกำหนดเบื้องต้น

  • ความรู้พื้นฐานเกี่ยวกับการพัฒนาแอป Android
  • มีความรู้พื้นฐานเกี่ยวกับเฟรมเวิร์กการขึ้นต่อกันของ Hilt

สิ่งที่คุณจะสร้าง

ในโค้ดแล็บนี้ คุณจะได้สร้างแอปกล้องที่มีเลย์เอาต์ที่เพิ่มประสิทธิภาพสำหรับอุปกรณ์แบบพับได้

6caebc2739522a1b.png

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

แม้ว่าแอปกล้องจะเป็นกรณีการใช้งานที่สะดวกที่สุดสำหรับ API นี้ แต่คุณสามารถนำฟีเจอร์ทั้ง 2 อย่างที่ได้เรียนรู้ในโค้ดแล็บนี้ไปใช้กับแอปใดก็ได้

สิ่งที่คุณจะได้เรียนรู้

  • วิธีใช้ Jetpack Window Manager เพื่อตอบสนองต่อการเปลี่ยนท่าทาง
  • วิธีย้ายแอปไปยังจอแสดงผลขนาดเล็กของอุปกรณ์พับได้

สิ่งที่คุณต้องมี

  • Android Studio เวอร์ชันล่าสุด
  • อุปกรณ์ที่พับได้หรือโปรแกรมจำลองที่พับได้

2. ตั้งค่า

รับรหัสเริ่มต้น

  1. หากติดตั้ง Git ไว้แล้ว คุณก็เรียกใช้คำสั่งด้านล่างได้เลย หากต้องการตรวจสอบว่าติดตั้ง Git แล้วหรือไม่ ให้พิมพ์ git --version ในเทอร์มินัลหรือบรรทัดคำสั่ง แล้วตรวจสอบว่าคำสั่งทำงานอย่างถูกต้อง
git clone https://github.com/android/large-screen-codelabs.git
  1. ไม่บังคับ: หากไม่มี Git ให้คลิกปุ่มต่อไปนี้เพื่อดาวน์โหลดโค้ดทั้งหมดสำหรับโค้ดแล็บนี้

เปิดโมดูลแรก

  • ใน Android Studio ให้เปิดโมดูลแรกในส่วน /step1

ภาพหน้าจอของ Android Studio ที่แสดงโค้ดที่เกี่ยวข้องกับ Codelab นี้

หากระบบขอให้คุณใช้ Gradle เวอร์ชันล่าสุด ให้อัปเดต

3. เรียกใช้และสังเกต

  1. เรียกใช้โค้ดในโมดูล step1

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

a34aca632d75aa09.png

  1. ตอนนี้ลองวางอุปกรณ์ในตำแหน่งที่เปิดครึ่งหนึ่ง ซึ่งบานพับไม่ได้แบนราบหรือปิดสนิท แต่ทำมุม 90 องศา

ดังที่เห็นได้ว่าแอปไม่ตอบสนองต่อท่าทางต่างๆ ของอุปกรณ์ จึงไม่มีการเปลี่ยนแปลงเลย์เอาต์ ทำให้บานพับอยู่ตรงกลางช่องมองภาพ

4. ดูข้อมูลเกี่ยวกับ Jetpack WindowManager

ไลบรารี Jetpack WindowManager ช่วยให้นักพัฒนาแอปสร้างประสบการณ์การใช้งานที่เพิ่มประสิทธิภาพสำหรับอุปกรณ์แบบพับได้ ประกอบด้วยคลาส FoldingFeature ที่อธิบายรอยพับในจอแสดงผลแบบยืดหยุ่นหรือบานพับระหว่างแผงจอแสดงผลจริง 2 แผง API ของอุปกรณ์จะให้สิทธิ์เข้าถึงข้อมูลสำคัญที่เกี่ยวข้องกับอุปกรณ์ ดังนี้

คลาส 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. ที่เฉพาะเจาะจง

  1. ประกาศตัวแปรเหล่านี้ใน 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
  1. และเริ่มต้นใช้งานในเมธอด 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()
      }
  }
}
  1. ตอนนี้ให้ใช้ฟังก์ชัน 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.

  1. ตอนนี้ ให้ใช้ฟังก์ชัน 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

  1. แก้ไข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]")
}
  1. สร้างและเรียกใช้แอป จากนั้นหากคุณกางอุปกรณ์ออกแล้วแตะปุ่มจอแสดงผลด้านหลัง ระบบจะแจ้งข้อความต่อไปนี้

3fa50cce0b0d4b8d.png

  1. เลือก "เปลี่ยนหน้าจอเลย" เพื่อดูเนื้อหาที่ย้ายไปยังจอแสดงผลด้านนอก

6. ใช้โหมดบนโต๊ะ

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

ส่วนหลักของ API นี้ประกอบด้วยอินเทอร์เฟซ WindowInfoTracker ซึ่งสร้างขึ้นด้วยเมธอดแบบคงที่ที่ต้องใช้ Activity

step1/CameraCodelabDependencies.kt

@Provides
fun provideWindowInfoTracker(activity: Activity) =
        WindowInfoTracker.getOrCreate(activity)

คุณไม่จำเป็นต้องเขียนโค้ดนี้เนื่องจากมีอยู่แล้ว แต่การทำความเข้าใจวิธีสร้าง WindowInfoTracker จะเป็นประโยชน์

  1. หากต้องการฟังการเปลี่ยนแปลงหน้าต่าง ให้ฟังการเปลี่ยนแปลงเหล่านี้ในเมธอด onResume() ของ Activity

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity, 
         binding.viewFinder
    )
}
  1. ตอนนี้ให้เปิดไฟล์ FoldingStateActor เนื่องจากถึงเวลาที่จะกรอกข้อมูลวิธีการ checkFoldingState() แล้ว

ดังที่คุณเห็นแล้วว่าฟังก์ชันนี้ทำงานในระยะRESUMEDของ Activity และใช้ประโยชน์จาก WindowInfoTracker เพื่อฟังการเปลี่ยนแปลงเลย์เอาต์

step1/FoldingStateActor.kt

windowInfoTracker.windowLayoutInfo(activity)
      .collect { newLayoutInfo ->
         activeWindowLayoutInfo = newLayoutInfo
         updateLayoutByFoldingState(cameraViewfinder)
      }

เมื่อใช้WindowInfoTracker อินเทอร์เฟซ คุณจะเรียกใช้ windowLayoutInfo() เพื่อรวบรวม Flow ของ WindowLayoutInfo ที่มีข้อมูลทั้งหมดที่พร้อมใช้งานใน DisplayFeature ได้

ขั้นตอนสุดท้ายคือการตอบสนองต่อการเปลี่ยนแปลงเหล่านี้และย้ายเนื้อหาตามนั้น คุณจะดำเนินการนี้ภายในเมธอด updateLayoutByFoldingState() ทีละขั้นตอน

  1. ตรวจสอบว่า activityLayoutInfo มีพร็อพเพอร์ตี้ DisplayFeature บางรายการ และมีพร็อพเพอร์ตี้อย่างน้อย 1 รายการเป็น FoldingFeature มิฉะนั้นคุณก็ไม่ต้องทำอะไร

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. คำนวณตำแหน่งของรอยพับเพื่อให้แน่ใจว่าตำแหน่งของอุปกรณ์ส่งผลต่อเลย์เอาต์และไม่ได้อยู่นอกขอบเขตของลำดับชั้น

step1/FoldingStateActor.kt

val foldPosition = FoldableUtils.getFeaturePositionInViewRect(
            foldingFeature,
            cameraViewfinder.parent as View
        ) ?: return

ตอนนี้คุณมั่นใจแล้วว่ามี FoldingFeature ที่ส่งผลต่อเลย์เอาต์ จึงต้องย้ายเนื้อหา

  1. ตรวจสอบว่า 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 ให้ย้ายเนื้อหาไปทางขวา ไม่เช่นนั้นให้ย้ายเนื้อหาไปไว้เหนือตำแหน่งพับ

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

7. ยินดีด้วย

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

คุณพร้อมที่จะมอบประสบการณ์การใช้งานที่ยอดเยี่ยมให้กับแอปกล้องแล้ว

อ่านเพิ่มเติม

Reference