Mendukung platform yang dapat diubah ukurannya di aplikasi kamera Anda

1. Pengantar

Terakhir diperbarui: 27 Oktober 2022

Mengapa perlu menggunakan platform yang dapat diubah ukurannya?

Secara historis, aplikasi Anda bisa saja berada di periode yang sama selama siklus prosesnya.

Namun, dengan adanya faktor bentuk baru, seperti perangkat foldable, dan mode display baru seperti multi-aplikasi dan multi-display, Anda tidak dapat lagi berasumsi bahwa hal ini benar.

Secara khusus, mari kita lihat beberapa pertimbangan yang paling penting saat mengembangkan aplikasi yang menargetkan perangkat berlayar besar dan perangkat foldable:

  • Jangan berasumsi bahwa aplikasi Anda akan tetap berada di jendela berbentuk potret.. Meminta orientasi tetap masih didukung di Android 12L, tetapi kini kami memberikan produsen perangkat opsi untuk mengganti permintaan aplikasi untuk orientasi pilihan.
  • Jangan mengasumsikan rasio aspek atau dimensi tetap untuk aplikasi Anda. Meskipun Anda menetapkan resizeableActivity = "false", aplikasi Anda dapat digunakan dalam mode multi-aplikasi di perangkat layar besar (>=600dp) dengan API level 31 dan yang lebih tinggi.
  • Jangan mengasumsikan hubungan tetap antara orientasi layar dan kamera. Android Compatibility Definition Document menetapkan bahwa sensor gambar kamera "HARUS diorientasikan sehingga dimensi panjang kamera sejajar dengan dimensi panjang layar". Mulai API level 32, klien kamera yang membuat kueri orientasi pada perangkat foldable dapat menerima nilai yang dapat berubah secara dinamis, bergantung pada status perangkat/lipatan.
  • Jangan berasumsi bahwa ukuran inset tidak dapat berubah.. Taskbar baru dilaporkan ke aplikasi sebagai inset, dan jika digunakan dengan navigasi gestur, taskbar dapat disembunyikan dan ditampilkan secara dinamis.
  • Jangan berasumsi bahwa aplikasi Anda memiliki akses eksklusif ke kamera. Saat berada dalam mode multi-aplikasi, aplikasi lain dapat memperoleh akses eksklusif ke resource bersama seperti kamera dan mikrofon.

Sekarang saatnya memastikan bahwa aplikasi kamera Anda berfungsi dengan baik di setiap skenario dengan mempelajari cara mengubah output kamera agar sesuai dengan platform yang dapat diubah ukurannya dan mempelajari cara menggunakan API yang ditawarkan Android untuk menangani berbagai kasus penggunaan.

Yang akan Anda build

Dalam codelab ini, Anda akan membuat aplikasi sederhana yang menampilkan pratinjau kamera. Anda akan memulai dengan aplikasi kamera naif yang mengunci orientasi dan mendeklarasikan dirinya sendiri sebagai tidak dapat diubah ukurannya, dan Anda akan melihat bagaimana perilakunya di Android 12L.

Kemudian, Anda akan mengupdate kode sumber untuk memastikan pratinjau selalu ditampilkan dengan baik dalam setiap skenario. Hasilnya adalah aplikasi kamera yang menangani perubahan konfigurasi dengan benar dan mengubah platform secara otomatis agar sesuai dengan pratinjau.

1df0acf495b0a05a.png

Yang akan Anda pelajari

  • Cara pratinjau Camera2 ditampilkan di platform Android
  • Hubungan antara orientasi sensor, rotasi layar, dan rasio aspek
  • Cara mengubah platform agar sesuai dengan rasio aspek pratinjau kamera dan rotasi tampilan

Yang akan Anda butuhkan

  • Versi terbaru Android Studio
  • Pengetahuan dasar tentang pengembangan aplikasi Android
  • Pengetahuan dasar tentang API Camera2
  • Perangkat atau emulator yang menjalankan Android 12L

2. Penyiapan

Mendapatkan kode awal

Untuk memahami perilaku Android 12L, Anda akan memulai dengan aplikasi kamera yang mengunci orientasi dan menyatakan dirinya sebagai tidak dapat diubah ukurannya.

Jika sudah menginstal Git, Anda cukup menjalankan perintah di bawah ini. Untuk memeriksa apakah Git sudah diinstal, ketik git --version di terminal atau command line dan pastikan Git dijalankan dengan benar.

git clone https://github.com/googlecodelabs/android-camera2-preview.git

Jika tidak memiliki Git, Anda dapat mengklik tombol berikut untuk mendownload semua kode untuk codelab ini:

Membuka modul pertama

Di Android Studio, buka modul pertama yang berada di /step1.

Android Studio akan meminta Anda menyetel jalur SDK. Anda mungkin ingin mengikuti rekomendasi untuk mengupdate IDE dan SDK Tools jika mengalami masalah.

302f1fb5070208c7.png

Jika Anda diminta untuk menggunakan versi Gradle terbaru, lanjutkan lalu update.

Menyiapkan perangkat

Mulai tanggal publikasi codelab ini, ada serangkaian perangkat fisik terbatas yang dapat menjalankan Android 12L.

Anda dapat menemukan daftar perangkat dan petunjuk untuk menginstal 12L di sini: https://developer.android.com/about/versions/12/12L/get

Jika memungkinkan, gunakan perangkat fisik untuk menguji aplikasi kamera, tetapi jika Anda ingin menggunakan emulator, pastikan untuk membuatnya dengan layar besar (misalnya, Pixel C) dan dengan API level 32.

Menyiapkan subjek untuk frame

Saat menggunakan kamera, saya ingin memiliki subjek standar yang dapat saya tunjuk untuk menghargai perbedaan dalam setelan, orientasi, dan penskalaan.

Untuk codelab ini, saya akan menggunakan versi cetak dari gambar berbentuk persegi ini. 66e5d83317364e67.png

Jika bagaimanapun tanda panah tidak mengarah ke atas atau persegi menjadi gambar geometri lain . . . ada yang perlu diperbaiki!

3. Jalankan dan amati

Posisikan perangkat dalam mode potret dan jalankan kode pada modul 1. Pastikan Anda mengizinkan aplikasi Codelab Camera2 untuk mengambil gambar dan merekam video saat menggunakan aplikasi. Seperti yang dapat Anda lihat, pratinjau ditampilkan dengan benar dan menggunakan ruang layar secara efisien.

Sekarang, putar perangkat ke lanskap:

46f2d86b060dc15a.png

Ini jelas tidak bagus. Sekarang klik tombol refresh di sudut kanan bawah.

b8fbd7a793cb6259.png

Pratinjau akan sedikit lebih baik, tetapi tetap belum optimal.

Yang Anda lihat adalah perilaku mode kompatibilitas Android 12L. Aplikasi yang mengunci orientasinya dalam mode potret dapat dijadikan tampilan lebar saat perangkat diputar ke lanskap dan kepadatan layar lebih tinggi dari 600 dp.

Meskipun mempertahankan rasio aspek asli, mode ini juga memberikan pengalaman pengguna yang kurang optimal karena sebagian besar ruang layar tidak digunakan.

Selain itu, dalam hal ini, pratinjau salah diputar sebesar 90 derajat.

Sekarang kembalikan perangkat ke mode potret, dan mulai mode layar terpisah.

Anda dapat mengubah ukuran jendela dengan menarik pembagi tengah.

Lihat pengaruh perubahan ukuran terhadap pratinjau kamera. Apakah pratinjau terdistorsi? Apakah pratinjau mempertahankan rasio aspek yang sama?

4. Perbaikan cepat

Karena mode kompatibilitas hanya dipicu untuk aplikasi yang mengunci orientasi dan tidak dapat diubah ukurannya, Anda mungkin tergoda untuk hanya mengupdate tanda di manifes untuk menghindarinya.

Coba saja:

step1/AndroidManifest.xml

<activity
    android:name=".CameraActivity"
    android:exported="true"
    android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Sekarang, build aplikasi dan jalankan lagi dalam orientasi lanskap. Anda akan melihat yang seperti ini:

f5753af5a9e44d2f.png

Panah tidak mengarah ke atas, dan itu bukan persegi!

Karena aplikasi tidak dirancang untuk berfungsi dalam mode multi-aplikasi atau dalam orientasi berbeda, aplikasi tidak akan mengalami perubahan apa pun dalam ukuran jendela, sehingga menyebabkan masalah yang baru saja Anda alami.

5. Menangani perubahan konfigurasi

Mari kita mulai dengan memberi tahu sistem bahwa kita ingin menangani perubahan konfigurasi sendiri. Buka step1/AndroidManifest.xml dan tambahkan baris berikut:

step1/AndroidManifest.xml

<activity
    android:name=".CameraActivity"
    android:exported="true"
    android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
    android:resizeableActivity="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

Sekarang Anda juga harus mengupdate step1/CameraActivity.kt untuk membuat ulang CameraCaptureSession setiap kali ukuran platform berubah.

Buka baris 232 dan panggil fungsi createCaptureSession():

step1/CameraActivity.kt

override fun onSurfaceTextureSizeChanged(
    surface: SurfaceTexture,
    width: Int,
    height: Int
) {
    createCaptureSession()
}

Ada satu peringatan di sini: onSurfaceTextureSizeChanged tidak dipanggil setelah rotasi 180 derajat (ukuran tidak berubah!). Kode ini juga tidak memicu onConfigurationChanged, jadi satu-satunya opsi yang kita miliki adalah membuat instance DisplayListener dan memeriksa untuk rotasi 180 derajat. Karena perangkat memiliki empat orientasi (potret, lanskap, potret terbalik, dan lanskap terbalik) yang ditentukan oleh bilangan bulat 0, 1, 2, dan 3, kita perlu memeriksa perbedaan rotasi 2.

Tambahkan kode berikut:

step1/CameraActivity.kt

/** DisplayManager to listen to display changes */
private val displayManager: DisplayManager by lazy {
    applicationContext.getSystemService(DISPLAY_SERVICE) as DisplayManager
}

/** Keeps track of display rotations */
private var displayRotation = 0

...

override fun onAttachedToWindow() {
    super.onAttachedToWindow()
    displayManager.registerDisplayListener(displayListener, mainLooperHandler)
}

override fun onDetachedFromWindow() {
    super.onDetachedFromWindow()
    displayManager.unregisterDisplayListener(displayListener)
}

private val displayListener = object : DisplayManager.DisplayListener {
    override fun onDisplayAdded(displayId: Int) {}
    override fun onDisplayRemoved(displayId: Int) {}
    override fun onDisplayChanged(displayId: Int) {
        val difference = displayManager.getDisplay(displayId).rotation - displayRotation
        displayRotation = displayManager.getDisplay(displayId).rotation

        if (difference == 2 || difference == -2) {
            createCaptureSession()
        }
    }
}

Sekarang kita yakin bahwa sesi tangkapan akan dibuat ulang dalam kasus apa pun. Kini saatnya mempelajari hubungan tersembunyi antara orientasi kamera dan rotasi layar.

6. Orientasi sensor dan rotasi layar

Kami menyebut orientasi natural sebagai orientasi di mana pengguna cenderung menggunakan perangkat secara "alami". Misalnya, orientasi alami adalah lanskap untuk laptop dan potret untuk ponsel. Untuk tablet, orientasi alami bisa salah satu dari keduanya.

Berdasarkan definisi ini, kita dapat menentukan dua konsep lainnya.

1f9cf3248b95e534.png

Kita menyebut orientasi kamera sebagai sudut antara sensor kamera dan orientasi alami perangkat. Hal ini mungkin bergantung pada cara kamera dipasang secara fisik pada perangkat dan sensor seharusnya selalu disejajarkan dengan sisi panjang layar (lihat CDD).

Mengingat mungkin sulit untuk menentukan sisi panjang perangkat foldable—karena geometrinya secara fisik dapat berubah—mulai dari API level 32, kolom ini tidak lagi bersifat statis, namun dapat diambil secara dinamis dari objek CameraCharacteristics.

Konsep lainnya adalah rotasi perangkat, yang mengukur seberapa banyak perangkat diputar secara fisik dari orientasi naturalnya.

Karena kita biasanya hanya ingin menangani empat orientasi yang berbeda, kita hanya dapat mempertimbangkan sudut yang merupakan kelipatan 90 dan mendapatkan informasi ini dengan mengalikan nilai yang ditampilkan dari Display.getRotation() dengan 90.

Secara default, TextureView sudah memberi kompensasi pada orientasi kamera, tetapi tidak menangani rotasi tampilan, sehingga pratinjau salah diputar.

Hal ini dapat diatasi, hanya dengan memutar SurfaceTexture target. Mari kita update fungsi CameraUtils.buildTargetTexture untuk menerima parameter surfaceRotation: Int dan untuk menerapkan transformasi ke platform:

step1/CameraUtils.kt

fun buildTargetTexture(
    containerView: TextureView,
    characteristics: CameraCharacteristics,
    surfaceRotation: Int
): SurfaceTexture? {

    val previewSize = findBestPreviewSize(Size(containerView.width, containerView.height), characteristics)

    val surfaceRotationDegrees = surfaceRotation * 90

    val halfWidth = containerView.width / 2f
    val halfHeight = containerView.height / 2f

    val matrix = Matrix()

    // Rotate to compensate display rotation
    matrix.postRotate(
        -surfaceRotationDegrees.toFloat(),
        halfWidth,
        halfHeight
    )

    containerView.setTransform(matrix)

    return containerView.surfaceTexture?.apply {
        setDefaultBufferSize(previewSize.width, previewSize.height)
    }
}

Kemudian, Anda dapat memanggilnya dengan mengubah baris 138 CameraActivity dengan cara berikut:

step1/CameraActivity.kt

val targetTexture = CameraUtils.buildTargetTexture(
textureView, cameraManager.getCameraCharacteristics(cameraID))

Menjalankan aplikasi kini akan menghasilkan pratinjau seperti ini:

1566c3f9e5089a35.png

Panah sekarang mengarah ke atas, tetapi penampung masih belum berbentuk persegi. Mari kita lihat cara memperbaikinya di langkah terakhir.

Menskalakan jendela bidik

Langkah terakhir adalah melakukan penskalaan untuk menyesuaikan rasio aspek output kamera.

Masalah pada langkah sebelumnya terjadi karena secara default TextureView menskalakan kontennya agar sesuai dengan seluruh jendela. Jendela ini dapat memiliki rasio aspek yang berbeda dari pratinjau kamera, sehingga dapat direntangkan atau terdistorsi.

Kita dapat memperbaikinya dalam dua langkah:

  • Menghitung faktor penskalaan yang diterapkan TextureView ke dirinya sendiri secara default, dan membalik transformasi tersebut
  • Menghitung dan menerapkan faktor penskalaan yang tepat (yang harus sama untuk sumbu x dan y)

Untuk menghitung faktor penskalaan yang benar, kita harus memperhitungkan perbedaan antara orientasi kamera dan rotasi layar. Buka step1/CameraUtils.kt dan tambahkan fungsi berikut untuk menghitung rotasi relatif antara orientasi sensor dan rotasi layar:

step1/CameraUtils.kt

/**
 * Computes the relative rotation between the sensor orientation and the display rotation.
 */
private fun computeRelativeRotation(
    characteristics: CameraCharacteristics,
    deviceOrientationDegrees: Int
): Int {
    val sensorOrientationDegrees =
        characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0

    // Reverse device orientation for front-facing cameras
    val sign = if (characteristics.get(CameraCharacteristics.LENS_FACING) ==
        CameraCharacteristics.LENS_FACING_FRONT
    ) 1 else -1

    return (sensorOrientationDegrees - (deviceOrientationDegrees * sign) + 360) % 360
}

Mengetahui nilai yang ditampilkan dari computeRelativeRotation sangat penting karena fungsi ini membantu kita memahami apakah pratinjau asli telah diputar sebelum diskalakan.

Misalnya, untuk ponsel dalam orientasi alaminya, output kamera berbentuk lanskap, dan diputar 90 derajat sebelum ditampilkan di layar.

Di sisi lain, untuk Chromebook dalam orientasi alami, output kamera ditampilkan langsung di layar tanpa rotasi tambahan.

Lihat lagi kasus berikut:

4e3a61ea9796a914.png Dalam kasus kedua (tengah), sumbu x output kamera ditampilkan di atas sumbu y layar, begitu juga sebaliknya, artinya lebar dan tinggi output kamera dibalik selama transformasi. Dalam kasus lain, konfigurasinya tetap sama, meskipun rotasi masih diperlukan dalam skenario ketiga.

Kita dapat menggeneralisasi kasus tersebut dengan formula:

val isRotationRequired =
        computeRelativeRotation(characteristics, surfaceRotationDegrees) % 180 != 0

Dengan informasi ini, sekarang kita dapat mengupdate fungsi untuk menskalakan platform:

step1/CameraUtils.kt

fun buildTargetTexture(
        containerView: TextureView,
        characteristics: CameraCharacteristics,
        surfaceRotation: Int
    ): SurfaceTexture? {

        val surfaceRotationDegrees = surfaceRotation * 90
        val windowSize = Size(containerView.width, containerView.height)
        val previewSize = findBestPreviewSize(windowSize, characteristics)
        val sensorOrientation =
            characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
        val isRotationRequired =
            computeRelativeRotation(characteristics, surfaceRotationDegrees) % 180 != 0

        /* Scale factor required to scale the preview to its original size on the x-axis */
        var scaleX = 1f
        /* Scale factor required to scale the preview to its original size on the y-axis */
        var scaleY = 1f

        if (sensorOrientation == 0) {
            scaleX =
                if (!isRotationRequired) {
                    windowSize.width.toFloat() / previewSize.height
                } else {
                    windowSize.width.toFloat() / previewSize.width
                }

            scaleY =
                if (!isRotationRequired) {
                    windowSize.height.toFloat() / previewSize.width
                } else {
                    windowSize.height.toFloat() / previewSize.height
                }
        } else {
            scaleX =
                if (isRotationRequired) {
                    windowSize.width.toFloat() / previewSize.height
                } else {
                    windowSize.width.toFloat() / previewSize.width
                }

            scaleY =
                if (isRotationRequired) {
                    windowSize.height.toFloat() / previewSize.width
                } else {
                    windowSize.height.toFloat() / previewSize.height
                }
        }

        /* Scale factor required to fit the preview to the TextureView size */
        val finalScale = max(scaleX, scaleY)
        val halfWidth = windowSize.width / 2f
        val halfHeight = windowSize.height / 2f

        val matrix = Matrix()

        if (isRotationRequired) {
            matrix.setScale(
                1 / scaleX * finalScale,
                1 / scaleY * finalScale,
                halfWidth,
                halfHeight
            )
        } else {
            matrix.setScale(
                windowSize.height / windowSize.width.toFloat() / scaleY * finalScale,
                windowSize.width / windowSize.height.toFloat() / scaleX * finalScale,
                halfWidth,
                halfHeight
            )
        }

        // Rotate to compensate display rotation
        matrix.postRotate(
            -surfaceRotationDegrees.toFloat(),
            halfWidth,
            halfHeight
        )

        containerView.setTransform(matrix)

        return containerView.surfaceTexture?.apply {
            setDefaultBufferSize(previewSize.width, previewSize.height)
        }
    }

Build aplikasi, jalankan, dan nikmati pratinjau kamera yang keren!

Bonus: mengubah animasi default

Jika Anda ingin menghindari animasi default pada rotasi, yang mungkin tampak tidak biasa untuk aplikasi kamera, Anda dapat mengubahnya dengan animasi jumpcut untuk transisi yang lebih lancar dengan menambahkan kode berikut ke metode onCreate() aktivitas:

val windowParams: WindowManager.LayoutParams = window.attributes
windowParams.rotationAnimation = WindowManager.LayoutParams.ROTATION_ANIMATION_JUMPCUT
window.attributes = windowParams

7. Selamat

Yang telah Anda pelajari:

  • Cara aplikasi yang tidak dioptimalkan berperilaku di Android 12L dalam mode kompatibilitas
  • Cara menangani perubahan konfigurasi
  • Perbedaan antara konsep seperti orientasi kamera, rotasi layar, dan orientasi alami perangkat
  • Perilaku default TextureView
  • Cara menskalakan dan memutar platform untuk menampilkan pratinjau kamera dengan benar dalam setiap skenario.

Bacaan lebih lanjut

Dokumen referensi