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

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

อุปกรณ์แบบพับได้พิเศษอย่างไร

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

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

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

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

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

6caebc2739522a1b.png

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

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

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

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

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

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

2. ตั้งค่า

รับโค้ดเริ่มต้น

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

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

  • ใน 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 ที่ช่วยให้โหมดจอแสดงผลด้านหลังเลื่อนหน้าต่างปัจจุบันไปยังจอแสดงผลที่สอดคล้องกับกล้องหลังได้ ซึ่งเหมาะสำหรับการถ่ายเซลฟีด้วยกล้องหลังและกรณีการใช้งานอื่นๆ อีกมากมาย

เพิ่มทรัพยากร 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. ที่ต้องการ

  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() ของผู้ฟัง หากต้องการกลับไปที่จอแสดงผลด้านใน (และใหญ่กว่า) ให้ปิดเซสชันและได้รับการยืนยันในเมธอด onSessionEnded() หากต้องการสร้าง Listener ดังกล่าว คุณต้องติดตั้งอินเทอร์เฟซ WindowAreaSessionCallback

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

3fa50cce0b0d4b8d.png

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

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

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

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

step1/cameralabDependencies.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 เพื่อฟังการเปลี่ยนแปลงเลย์เอาต์

ขั้นตอนที่ 1/FoldingStateActor.kt

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

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

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

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

ขั้นตอนที่ 1/FoldingStateActor.kt

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

ขั้นตอนที่ 1/FoldingStateActor.kt

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

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

  1. ตรวจสอบว่า 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 แสดงว่าคุณย้ายเนื้อหาไปทางขวา ไม่เช่นนั้นคุณจะย้ายเนื้อหาไปอยู่ตำแหน่งครึ่งหน้าบน

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

7. ยินดีด้วย

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

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

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

Reference