Tối ưu hoá ứng dụng camera trên thiết bị có thể gập lại nhờ Jetpack WindowManager

1. Trước khi bắt đầu

Thiết bị có thể gập lại có điểm gì đặc biệt?

Các thiết bị có thể gập lại ở mỗi thế hệ lại có sự đổi mới. Các thiết bị này mang đến những trải nghiệm độc đáo và đi kèm theo đó là các cơ hội có một không hai để làm hài lòng người dùng của bạn bằng những tính năng khác biệt, chẳng hạn như giao diện người dùng khi đặt thiết bị trên mặt bàn để sử dụng mà không dùng tay.

Điều kiện tiên quyết

  • Kiến thức cơ bản về cách phát triển các ứng dụng Android
  • Kiến thức cơ bản về Khung chèn phần phụ thuộc Hilt

Sản phẩm bạn sẽ tạo ra

Trong lớp học lập trình này, bạn sẽ tạo một ứng dụng camera có bố cục được tối ưu hoá cho các thiết bị có thể gập lại.

c5e52933bcd81859.png

Bạn bắt đầu với một ứng dụng camera cơ bản không phản ứng với bất kỳ tư thế đặt thiết bị nào hoặc tận dụng camera sau có chất lượng tốt hơn để cho ra những bức ảnh chân dung tự chụp đẹp hơn. Bạn cập nhật mã nguồn để chuyển bản xem trước sang màn hình nhỏ hơn khi thiết bị ở trạng thái mở và để phản ứng với điện thoại đang được đặt ở chế độ trên mặt bàn.

Mặc dù ứng dụng camera là trường hợp sử dụng thuận tiện nhất cho API này, nhưng bạn có thể áp dụng cả hai tính năng đã tìm hiểu trong lớp học lập trình này cho mọi ứng dụng.

Kiến thức bạn sẽ học được

  • Cách sử dụng Jetpack Window Manager để phản ứng với việc thay đổi tư thế
  • Cách chuyển ứng dụng của bạn sang màn hình nhỏ hơn của thiết bị có thể gập lại

Bạn cần

  • Một phiên bản Android Studio gần đây
  • Một thiết bị có thể gập lại hoặc trình mô phỏng thiết bị có thể gập lại

2. Bắt đầu thiết lập

Lấy mã khởi đầu

  1. Nếu đã cài đặt Git, bạn có thể chỉ cần chạy lệnh bên dưới. Để kiểm tra xem Git đã được cài đặt hay chưa, hãy nhập git --version vào dòng lệnh hoặc cửa sổ dòng lệnh và xác minh rằng mã này được thực thi đúng cách.
git clone https://github.com/android/large-screen-codelabs.git
  1. Không bắt buộc: Nếu chưa có Git, bạn có thể nhấp vào nút sau để tải xuống tất cả mã cho lớp học lập trình này:

Mở mô-đun đầu tiên

  • Trong Android Studio, hãy mở mô-đun đầu tiên trong /step1.

Ảnh chụp màn hình Android Studio cho thấy đoạn mã liên quan đến lớp học lập trình này

Nếu bạn được yêu cầu phải sử dụng phiên bản Gradle mới nhất, hãy tiếp tục và cập nhật phiên bản đó.

3. Chạy và quan sát

  1. Chạy mã trên mô-đun step1.

Như bạn có thể thấy, đây là một ứng dụng camera đơn giản. Bạn có thể chuyển đổi giữa camera trước và sau cũng như điều chỉnh tỷ lệ khung hình. Tuy nhiên, nút đầu tiên ở bên trái hiện không có tác dụng gì ngoại trừ là điểm truy cập cho chế độ Tự chụp chân dung bằng camera sau.

149e3f9841af7726.png

  1. Bây giờ, hãy tìm cách đặt thiết bị ở vị trí mở một nửa, trong đó bản lề không hoàn toàn phẳng hoặc đóng nhưng tạo thành một góc 90 độ.

Như bạn có thể thấy, ứng dụng không phản hồi với các tư thế của thiết bị, do đó bố cục không thay đổi, khi để bản lề ở giữa kính ngắm.

4. Tìm hiểu về Jetpack WindowManager

Thư viện Jetpack WindowManager giúp các nhà phát triển ứng dụng tạo trải nghiệm được tối ưu hoá cho các thiết bị có thể gập lại. Thư viện này chứa lớp FoldingFeature mô tả đường ranh giới phần hiển thị trong một màn hình linh hoạt hoặc một bản lề nằm giữa hai màn hình thực. API tương ứng cấp quyền truy cập vào các thông tin quan trọng liên quan đến thiết bị:

Lớp FoldingFeature chứa thông tin bổ sung, chẳng hạn như occlusionType() hoặc isSeparating(), nhưng lớp học lập trình này không tìm hiểu sâu về những thông tin đó.

Kể từ phiên bản 1.2.0-beta01, thư viện này sẽ sử dụng WindowAreaController, một API cho phép Chế độ màn hình sau chuyển cửa sổ hiện tại đến màn hình được căn chỉnh với camera sau. Việc này rất tuyệt trong trường hợp tự chụp ảnh chân dung bằng camera sau và nhiều trường hợp sử dụng khác!

Thêm phần phụ thuộc

  • Để sử dụng Jetpack WindowManager trong ứng dụng, bạn cần thêm các phần phụ thuộc sau vào tệp build.gradle ở cấp mô-đun của mình:

step1/build.gradle

def work_version = '1.2.0'
implementation "androidx.window:window:$work_version"
implementation "androidx.window:window-java:$work_version"
implementation "androidx.window:window-core:$work_version"

Giờ bạn có thể truy cập vào cả hai lớp FoldingFeatureWindowAreaController trong ứng dụng của mình. Bạn sẽ sử dụng chúng để tạo nên trải nghiệm tối ưu cho camera trên thiết bị có thể gập lại!

5. Triển khai chế độ Tự chụp chân dung bằng camera sau

Bắt đầu với chế độ Màn hình sau.

API cho phép chế độ này là WindowAreaController. API này cung cấp thông tin và hành vi liên quan đến việc di chuyển cửa sổ giữa các màn hình hoặc khu vực hiển thị trên một thiết bị.

API này cho phép bạn truy vấn danh sách WindowAreaInfo hiện có sẵn để tương tác.

Bằng cách sử dụng WindowAreaInfo, bạn có thể truy cập vào WindowAreaSession, một giao diện thể hiện đặc điểm của một khu vực cửa sổ đang hoạt động và trạng thái có sẵn của một WindowAreaCapability. cụ thể

  1. Khai báo các biến này trong 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. Và khởi tạo chúng trong phương thức 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. Bây giờ, hãy triển khai hàm updateUI() để bật hoặc tắt nút tự chụp chân dung bằng camera sau, tuỳ vào trạng thái hiện tại:

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
            }
        }
    }
}

Bước cuối cùng này là không bắt buộc nhưng sẽ rất hữu ích để tìm hiểu tất cả các trạng thái có thể có của WindowAreaCapability.

  1. Bây giờ, hãy triển khai hàm toggleRearDisplayMode. Hàm này sẽ đóng phiên nếu chức năng này vốn đang hoạt động hoặc sẽ gọi hàm 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
            )
        }
    }
}

Hãy lưu ý cách sử dụng MainActivity làm WindowAreaSessionCallback.

API Màn hình sau hoạt động theo phương thức tiếp cận của trình nghe: khi yêu cầu chuyển nội dung sang màn hình khác, bạn bắt đầu một phiên được trả về thông qua phương thức onSessionStarted() của trình nghe. Khi muốn quay lại màn hình bên trong (và lớn hơn), bạn sẽ đóng phiên này và nhận được thông báo xác nhận trong phương thức onSessionEnded(). Để tạo một trình nghe như vậy, bạn cần triển khai giao diện WindowAreaSessionCallback.

  1. Sửa đổi nội dung khai báo MainActivity để triển khai giao diện WindowAreaSessionCallback:

step1/MainActivity.kt

class MainActivity : AppCompatActivity(), WindowAreaSessionCallback

Bây giờ, hãy triển khai các phương thức onSessionStartedonSessionEnded bên trong MainActivity. Các phương thức gọi lại này cực kỳ hữu ích trong việc nhận thông báo về trạng thái phiên và cập nhật ứng dụng theo đó.

Nhưng lần này, để đơn giản, bạn chỉ cần kiểm tra trong phần nội dung hàm xem có lỗi nào không rồi ghi lại trạng thái.

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. Tạo và chạy ứng dụng. Sau đó, nếu mở thiết bị và nhấn vào nút màn hình sau, bạn sẽ được nhắc bằng một thông báo như sau:

ba878f120b7c8d58.png

  1. Chọn "Switch screens now" (Chuyển đổi màn hình ngay) để xem nội dung đã được chuyển sang màn hình ngoài!

6. Triển khai Chế độ trên mặt bàn

Giờ là lúc giúp ứng dụng của bạn nhận biết chế độ gập: bạn chuyển nội dung của mình sang một bên hoặc phía trên bản lề của thiết bị dựa vào hướng gấp của chế độ gập. Để thực hiện việc này, bạn sẽ thao tác bên trong FoldingStateActor để mã được tách khỏi Activity sao cho dễ đọc hơn.

Phần cốt lõi của API này bao gồm giao diện WindowInfoTracker được tạo bằng phương thức tĩnh yêu cầu một Activity:

step1/CameraCodelabDependencies.kt

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

Bạn không cần phải viết mã này vì mã đã có sẵn nhưng sẽ rất hữu ích nếu bạn hiểu cách tạo WindowInfoTracker.

  1. Để theo dõi bất kỳ sự thay đổi nào về cửa sổ, hãy theo dõi những thay đổi này trong phương thức onResume() của Activity:

step1/MainActivity.kt

lifecycleScope.launch {
    foldingStateActor.checkFoldingState(
         this@MainActivity, 
         binding.viewFinder
    )
}
  1. Bây giờ, hãy mở tệp FoldingStateActor, vì đã đến lúc điền vào phương thức checkFoldingState().

Như bạn đã thấy, phương thức này chạy trong giai đoạn RESUMED của Activity và tận dụng WindowInfoTracker để theo dõi mọi thay đổi về bố cục.

step1/FoldingStateActor.kt

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

Khi sử dụng giao diện WindowInfoTracker, bạn có thể gọi windowLayoutInfo() để thu thập một Flow về WindowLayoutInfo chứa tất cả thông tin có trong DisplayFeature.

Bước cuối cùng là phản ứng với những thay đổi này và di chuyển nội dung theo đó. Bạn thực hiện việc này bên trong phương thức updateLayoutByFoldingState(), từng bước một.

  1. Đảm bảo activityLayoutInfo chứa một số thuộc tính DisplayFeature và ít nhất một trong số đó là FoldingFeature, nếu không, bạn không nên thực hiện bất kỳ thao tác nào:

step1/FoldingStateActor.kt

val foldingFeature = activeWindowLayoutInfo?.displayFeatures
            ?.firstOrNull { it is FoldingFeature } as FoldingFeature?
            ?: return
  1. Tính toán vị trí của nếp gấp để đảm bảo vị trí của thiết bị đang ảnh hưởng đến bố cục và không nằm ngoài các ranh giới hệ phân cấp của bạn:

step1/FoldingStateActor.kt

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

Giờ thì bạn đã chắc rằng mình có một FoldingFeature ảnh hưởng đến bố cục, vì vậy, bạn cần phải di chuyển nội dung của mình.

  1. Kiểm tra xem FoldingFeature có phải là HALF_OPEN hay không, nếu không thì bạn chỉ cần khôi phục vị trí của nội dung. Nếu giá trị là HALF_OPEN, bạn cần chạy một bước kiểm tra khác và hành động khác đi dựa trên hướng gập:

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()
}

Nếu hướng gập là VERTICAL, bạn sẽ chuyển nội dung sang phải, nếu không thì bạn sẽ chuyển nội dung lên đầu vị trí gập.

  1. Tạo và chạy ứng dụng của bạn, sau đó mở thiết bị ra và đặt ở chế độ trên mặt bàn để xem nội dung di chuyển theo đó!

7. Xin chúc mừng!

Trong lớp học lập trình này, bạn đã tìm hiểu một số chức năng dành riêng cho thiết bị có thể gập lại, chẳng hạn như Chế độ màn hình sau hoặc Chế độ trên mặt bàn và cách mở khoá những chức năng này bằng Jetpack WindowManager.

Bạn đã sẵn sàng triển khai những trải nghiệm người dùng thú vị cho ứng dụng camera rồi đó.

Tài liệu đọc thêm

Tài liệu tham khảo