بدء استخدام تطبيق XX

في هذا الدرس التطبيقي حول الترميز، سنتعلّم كيفية إنشاء تطبيق كاميرا يستخدم كاميرا X لعرض عدسة الكاميرا والتقاط صور وتحليل مصدر بيانات من الكاميرا.

ولتحقيق هذا الهدف، سنقدّم مفهوم حالات الاستخدام في تطبيق XX، والذي يمكن استخدامه لمجموعة متنوعة من عمليات الكاميرا من عرض عدسة الكاميرا إلى تحليل الإطارات في الوقت الفعلي.

ما سنتعلمه

  • كيفية إضافة تبعيات تطبيق XX
  • طريقة عرض معاينة الكاميرا في نشاط. (معاينة حالة الاستخدام)
  • كيفية التقاط صورة وحفظها في مساحة التخزين (حالة استخدام ImageCapture)
  • كيفية تحليل الإطارات من الكاميرا في الوقت الفعلي (حالة استخدام تحليل الصورة)

الأجهزة التي سنحتاج إليها

  • على الرغم من أن جهاز Android متوافق مع محاكي Android Studio، سيكون مناسبًا له. الحد الأدنى لمستوى واجهة برمجة التطبيقات المتوافق هو 21.

البرامج التي سنحتاج إليها

  • الإصدار Android 3.3 أو الإصدارات الأحدث.

باستخدام قائمة "استوديو Android"، ابدأ مشروعًا جديدًا واختَر نشاط فارغ عندما يُطلب منك ذلك.

بعد ذلك، نختار أي اسم نريده -- لقد اخترنا ببراعة &لمعرفة&تطبيق كاميراX يجب أن نتأكد من ضبط اللغة على Kotlin، أن يكون الحد الأدنى لمستوى واجهة برمجة التطبيقات هو 21 (وهو الحد الأدنى المطلوب لكاميرا XX) وأننا نستخدم عناصر AndroidX.

للبدء، أضِف تبعيات تطبيق XX إلى ملف Gradle الخاص بالتطبيق، داخل قسم الارتباطات:

// Use the most recent version of CameraX, currently that is alpha04
def camerax_version = "1.0.0-alpha04"
implementation "androidx.camera:camera-core:${camerax_version}"
implementation "androidx.camera:camera-camera2:${camerax_version}"

انقر على مزامنة الآن عندما يُطلب منك ذلك، وسنكون مستعدًا لاستخدام كاميرا X في تطبيقنا.

سنستخدم واجهة SurfaceTexture لعرض عدسة الكاميرا. في هذا الدرس التطبيقي حول الترميز، سنعرض عدسة الكاميرا بتنسيق مربّع الحجم الثابت. للاطّلاع على مثال أكثر شمولاً يعرض عدسة الكاميرا المتجاوبة، يمكنك مراجعة العيّنة الرسمية.

لنبدأ تعديل ملف activity_main ضمن تنسيق res >Layout > activity_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <TextureView
            android:id="@+id/view_finder"
            android:layout_width="640px"
            android:layout_height="640px"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

جزء مهم من إضافة أي وظيفة في المشروع الذي يستخدم الكاميرا هو طلب أذونات CAMERA المناسبة. أولاً، يجب الإعلان عنها في ملف البيان، قبل علامة التطبيق:

<uses-permission android:name="android.permission.CAMERA" />

وبعد ذلك، داخل MainActivity، نحتاج إلى طلب الأذونات في وقت التشغيل. سنجري التغييرات في ملف MainActivity ضمن java > com.example.cameraxapp > MainActivity.kt:

في أعلى الملف، خارج تعريف فئة MainActivity، دعنا نضيف الثوابت وعمليات الاستيراد التالية:

// Your IDE likely can auto-import these classes, but there are several
// different implementations so we list them here to disambiguate
import android.Manifest
import android.util.Size
import android.graphics.Matrix
import java.util.concurrent.TimeUnit

// This is an arbitrary number we are using to keep tab of the permission
// request. Where an app has multiple context for requesting permission,
// this can help differentiate the different contexts
private const val REQUEST_CODE_PERMISSIONS = 10

// This is an array of all the permission specified in the manifest
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)

ضمن صف MainActivity، أضِف الحقول التالية وطرق المساعدة التي تُستخدم لطلب الأذونات وتشغيل الرمز بعد أن نتأكد من أنه تم منح جميع الأذونات:

class MainActivity : AppCompatActivity(), LifecycleOwner {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
    }

    // Add this after onCreate

    private lateinit var viewFinder: TextureView

    private fun startCamera() {
        // TODO: Implement CameraX operations
    }

    private fun updateTransform() {
        // TODO: Implement camera viewfinder transformations
    }

    /**
     * Process result from permission request dialog box, has the request
     * been granted? If yes, start Camera. Otherwise display a toast
     */
    override fun onRequestPermissionsResult(
        requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
        if (requestCode == REQUEST_CODE_PERMISSIONS) {
            if (allPermissionsGranted()) {
                viewFinder.post { startCamera() }
            } else {
                Toast.makeText(this,
                    "Permissions not granted by the user.", 
                    Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }

    /**
     * Check if all permission specified in the manifest have been granted
     */
    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(
               baseContext, it) == PackageManager.PERMISSION_GRANTED
    }
}

وأخيرًا، نجمع كل العناصر معًا في onCreate لإنشاء طلب الإذن عندما يكون ذلك مناسبًا:

override fun onCreate(savedInstanceState: Bundle?) {
    ...

    // Add this at the end of onCreate function

    viewFinder = findViewById(R.id.view_finder)

    // Request camera permissions
    if (allPermissionsGranted()) {
        viewFinder.post { startCamera() }
    } else {
        ActivityCompat.requestPermissions(
            this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
    }

    // Every time the provided texture view changes, recompute layout
    viewFinder.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
        updateTransform()
    }
}

الآن، عند بدء تشغيل التطبيق، سيتم التحقق من وجود أذونات الكاميرا المناسبة. وفي حال حدوث ذلك، سيتم استدعاء `startCamera()` مباشرةً. وبخلاف ذلك، سيطلب منك الأذونات فور اتّخاذ الإجراء المطلوب، ويستدعي `startالكاميرا()`.

بالنسبة إلى معظم تطبيقات الكاميرا، يُعدّ عرض عدسة الكاميرا للمستخدمين أمرًا في غاية الأهمية، وإلّا سيكون من الصعب جدًا على المستخدمين توجيه الكاميرا إلى المكان الصحيح. يمكن تنفيذ عدسة الكاميرا باستخدام الفئة "معاينة" مع "XX".

لاستخدام المعاينة، نحتاج أولاً إلى تحديد تهيئة يتم استخدامها بعد ذلك لإنشاء مثيل لحالة الاستخدام. المثيل الناتج هو ما نحتاج إلى ربطه بدورة حياة كاميرا X. وسننفّذ ذلك ضمن طريقة `startCamera()`، وما عليك سوى ملء حقل التنفيذ بهذا الرمز:

private fun startCamera() {

    // Create configuration object for the viewfinder use case
    val previewConfig = PreviewConfig.Builder().apply {
        setTargetAspectRatio(Rational(1, 1))
        setTargetResolution(Size(640, 640))
    }.build()

    // Build the viewfinder use case
    val preview = Preview(previewConfig)

    // Every time the viewfinder is updated, recompute layout
    preview.setOnPreviewOutputUpdateListener {

        // To update the SurfaceTexture, we have to remove it and re-add it
        val parent = viewFinder.parent as ViewGroup
        parent.removeView(viewFinder)
        parent.addView(viewFinder, 0)

        viewFinder.surfaceTexture = it.surfaceTexture
        updateTransform()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

في هذه المرحلة، علينا تنفيذ الطريقة الغامضة "updateUpdate()". داخل `updateTransform()`، يتمثل الهدف في تعويض التغييرات في اتجاه الجهاز لعرض عدسة الكاميرا في اتجاه عمودي:

private fun updateTransform() {
    val matrix = Matrix()

    // Compute the center of the view finder
    val centerX = viewFinder.width / 2f
    val centerY = viewFinder.height / 2f

    // Correct preview output to account for display rotation
    val rotationDegrees = when(viewFinder.display.rotation) {
        Surface.ROTATION_0 -> 0
        Surface.ROTATION_90 -> 90
        Surface.ROTATION_180 -> 180
        Surface.ROTATION_270 -> 270
        else -> return
    }
    matrix.postRotate(-rotationDegrees.toFloat(), centerX, centerY)

    // Finally, apply transformations to our TextureView
    viewFinder.setTransform(matrix)
}

لتنفيذ تطبيق متوافق مع الإنتاج، يمكنك الاطّلاع على النموذج الرسمي لمعرفة ما يجب معالجته. وللحفاظ على استمرار هذا الدرس التطبيقي حول الترميز، سنأخذ بعض الاختصارات. على سبيل المثال، لا نتتبّع بعض تغييرات الإعداد، مثل عمليات تدوير الأجهزة بزاوية 180 درجة التي لا تؤدي إلى تشغيل أداة معالجة تغييرات التنسيق. تحتاج أيضًا كاميرات العرض غير المربّعة إلى تعويض تغيير نسبة العرض إلى الارتفاع عند تدوير الجهاز.

إذا أنشأنا التطبيق وشغّلناه، من المفترض أن نشاهد الآن معاينة للحياة. ممتاز.

للسماح للمستخدمين بالتقاط الصور، سنوفر زرًا كجزء من التصميم بعد عرض الزخرفة في res >Layout > activity_main.xml:

<ImageButton
        android:id="@+id/capture_button"
        android:layout_width="72dp"
        android:layout_height="72dp"
        android:layout_margin="24dp"
        app:srcCompat="@android:drawable/ic_menu_camera"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent" />

وتعمل حالات الاستخدام الأخرى بطريقة مشابهة جدًا للمعاينة. أولاً، يجب تحديد كائن الإعداد الذي يتم استخدامه لإنشاء مثيل لكائن حالة الاستخدام الفعلي. لالتقاط الصور، عند الضغط على زر الالتقاط، علينا تعديل طريقة `startCamera()` وإضافة بضعة أسطر أخرى من الرمز في النهاية، وذلك قبل الطلب إلى CameraX.bindToLifecycle:

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Create configuration object for the image capture use case
    val imageCaptureConfig = ImageCaptureConfig.Builder()
        .apply {
            setTargetAspectRatio(Rational(1, 1))
            // We don't set a resolution for image capture; instead, we
            // select a capture mode which will infer the appropriate
            // resolution based on aspect ration and requested mode
            setCaptureMode(ImageCapture.CaptureMode.MIN_LATENCY)
    }.build()

    // Build the image capture use case and attach button click listener
    val imageCapture = ImageCapture(imageCaptureConfig)
    findViewById<ImageButton>(R.id.capture_button).setOnClickListener {
        val file = File(externalMediaDirs.first(),
            "${System.currentTimeMillis()}.jpg")
        imageCapture.takePicture(file,
            object : ImageCapture.OnImageSavedListener {
            override fun onError(error: ImageCapture.UseCaseError,
                                 message: String, exc: Throwable?) {
                val msg = "Photo capture failed: $message"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.e("CameraXApp", msg)
                exc?.printStackTrace()
            }

            override fun onImageSaved(file: File) {
                val msg = "Photo capture succeeded: ${file.absolutePath}"
                Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show()
                Log.d("CameraXApp", msg)
            }
        })
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview)
}

بعد ذلك، عدِّل المكالمة إلى CameraX.bindToLifecycle لتضمين حالة الاستخدام الجديدة:

CameraX.bindToLifecycle(this, preview, imageCapture)

وباستخدام هذه الطريقة، أضفنا زرًا عمليًا لالتقاط الصور.

وتُعدّ كاميرا Analysis إحدى الميزات الرائعة في تطبيق XX. تسمح لنا هذه السياسة بتحديد فئة مخصّصة من خلال تنفيذ واجهة ImageAnalysis.محلل ليتم استدعاءها مع إطارات الكاميرا الواردة. تماشيًا مع الرؤية الأساسية لاستخدام كاميرا X، لن داعي للقلق بشأن إدارة حالة جلسة الكاميرا أو حتى التخلص من الصور، لأنّ الالتزام بمراحل النشاط في التطبيق يكفي مثل المكوّنات الأخرى التي تعيّن دورة حياة.

أولاً، سننفّذ أداة تحليل صور مخصصة. أداة تحليلنا بسيطة جدًا، فهي فقط تسجّل متوسط السطوع (إضاءة) في الصورة، ولكنها تقدم أمثلة على ما يجب إجراؤه لحالات الاستخدام المعقدة بشكل عشوائي. كل ما علينا فعله هو إلغاء الدالة `analyze` في فئة تنفّذ واجهة ImageAnalysis.محلل. يمكننا تعريف تنفيذنا كفئة داخلية ضمن MainActivity:

private class LuminosityAnalyzer : ImageAnalysis.Analyzer {
    private var lastAnalyzedTimestamp = 0L

    /**
     * Helper extension function used to extract a byte array from an
     * image plane buffer
     */
    private fun ByteBuffer.toByteArray(): ByteArray {
        rewind()    // Rewind the buffer to zero
        val data = ByteArray(remaining())
        get(data)   // Copy the buffer into a byte array
        return data // Return the byte array
    }

    override fun analyze(image: ImageProxy, rotationDegrees: Int) {
        val currentTimestamp = System.currentTimeMillis()
        // Calculate the average luma no more often than every second
        if (currentTimestamp - lastAnalyzedTimestamp >=
            TimeUnit.SECONDS.toMillis(1)) {
            // Since format in ImageAnalysis is YUV, image.planes[0]
            // contains the Y (luminance) plane
            val buffer = image.planes[0].buffer
            // Extract image data from callback object
            val data = buffer.toByteArray()
            // Convert the data into an array of pixel values
            val pixels = data.map { it.toInt() and 0xFF }
            // Compute average luminance for the image
            val luma = pixels.average()
            // Log the new luma value
            Log.d("CameraXApp", "Average luminosity: $luma")
            // Update timestamp of last analyzed frame
            lastAnalyzedTimestamp = currentTimestamp
        }
    }
}

مع تنفيذ صف الصورة لواجهة Analytics Analysis، ما علينا سوى إنشاء مثيل ImageAnalysis تمامًا كأي حالات استخدام أخرى وتحديث وظيفة `startCamera()` مرة أخرى، وذلك قبل الطلب الخاص باستدعاء CameraX.bindToLifecycle:

private fun startCamera() {

    ...

    // Add this before CameraX.bindToLifecycle

    // Setup image analysis pipeline that computes average pixel luminance
    val analyzerConfig = ImageAnalysisConfig.Builder().apply {
        // Use a worker thread for image analysis to prevent glitches
        val analyzerThread = HandlerThread(
            "LuminosityAnalysis").apply { start() }
        setCallbackHandler(Handler(analyzerThread.looper))
        // In our analysis, we care more about the latest image than
        // analyzing *every* image
        setImageReaderMode(
            ImageAnalysis.ImageReaderMode.ACQUIRE_LATEST_IMAGE)
    }.build()

    // Build the image analysis use case and instantiate our analyzer
    val analyzerUseCase = ImageAnalysis(analyzerConfig).apply {
        analyzer = LuminosityAnalyzer()
    }

    // Bind use cases to lifecycle
    // If Android Studio complains about "this" being not a LifecycleOwner
    // try rebuilding the project or updating the appcompat dependency to
    // version 1.1.0 or higher.
    CameraX.bindToLifecycle(this, preview, imageCapture)
}

كما نحدِّث أيضًا الاستدعاء الخاص بـ CameraX.bindtoLifecycle لربط حالة الاستخدام الجديدة:

CameraX.bindToLifecycle(
    this, preview, imageCapture, analyzerUseCase)

سيؤدي تشغيل التطبيق الآن إلى ظهور رسالة مشابهة لهذه في Logcat كل ثانية تقريبًا:

D/CameraXApp: Average luminosity: ...

لاختبار التطبيق، ما عليك سوى النقر على الزر تشغيل في "استوديو Android" وسيتم إنشاء مشروعنا ونشره وتشغيله في الجهاز أو المحاكي المحدد. بعد تحميل التطبيق، من المفترض أن نرى عدسة الكاميرا التي ستظل في وضع عمودي حتى بعد تدوير الجهاز، وذلك بفضل رمز تعامل الاتجاه الذي أضفناه سابقًا، ويجب أن يتمكن أيضًا من التقاط الصور باستخدام الزر:

لقد أكملت الدرس التطبيقي حول الترميز وتم بنجاح. بالنظر إلى ما سبق، نفذت ما يلي على تطبيق Android جديد من البداية:

  • يتم تضمين تبعيات CameraX في مشروعك.
  • تم عرض عدسة الكاميرا (باستخدام حالة استخدام المعاينة)
  • تنفيذ الصور، حفظ الصور في مساحة التخزين (باستخدام حالة استخدام ImageCapture)
  • تم تطبيق تحليل للإطارات من الكاميرا في الوقت الفعلي (باستخدام حالة استخدام ImageAnalysis)

إذا كنت مهتمًا بالاطّلاع على المزيد من المعلومات عن تطبيق XX والمهام التي يمكنك تنفيذها باستخدامه، يمكنك الاطّلاع على المستندات أو استنساخ النموذج الرسمي.