التقاط الصور

ملاحظة: تشير هذه الصفحة إلى فئة الكاميرا التي تم إيقافها. وننصح باستخدام CameraX أو camera2 في حالات استخدام معيّنة. يتوافق كل من CameraX و Camera2 مع الإصدار Android 5.0 (المستوى 21 من واجهة برمجة التطبيقات) والإصدارات الأحدث.

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

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

طلب ميزة الكاميرا

إذا كان التقاط الصور من الوظائف الأساسية لتطبيقك، يجب حصر مستوى ظهوره على Google Play على الأجهزة المزوّدة بكاميرا. للإعلان عن أنّ تطبيقك يعتمد على توفُّر كاميرا، ضَع علامة <uses-feature> في ملف البيان:

<manifest ... >
    <uses-feature android:name="android.hardware.camera"
                  android:required="true" />
    ...
</manifest>

إذا كان تطبيقك يستخدم كاميرا ولا يتطلّب استخدامه ليعمل، اضبط السمة android:required على السمة false. عند إجراء ذلك، سيسمح Google Play للأجهزة التي لا تحتوي على كاميرا بتنزيل تطبيقك. تقع على عاتقك بعد ذلك مسؤولية التحقق من مدى توفّر الكاميرا في وقت التشغيل من خلال الاتصال برقم hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY). وإذا لم تكن الكاميرا متاحة، يجب إيقاف ميزاتها.

الحصول على الصورة المصغّرة

إذا لم يكن إنجاز التقاط صورة بسيط هو تتويج طموح تطبيقك، أنت على الأرجح تريد استعادة الصورة من تطبيق الكاميرا لتنفيذ إجراء بشأنها.

يشفّر تطبيق "كاميرا Android" الصورة في القيمة Intent التي يتم إرسالها إلى onActivityResult() على شكل Bitmap صغير في العناصر الإضافية، ضمن المفتاح "data". يسترد الرمز التالي هذه الصورة ويعرضها في ImageView.

Kotlin

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        val imageBitmap = data.extras.get("data") as Bitmap
        imageView.setImageBitmap(imageBitmap)
    }
}

Java

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
        Bundle extras = data.getExtras();
        Bitmap imageBitmap = (Bitmap) extras.get("data");
        imageView.setImageBitmap(imageBitmap);
    }
}

ملاحظة: قد تكون هذه الصورة المصغّرة من "data" مناسبة لرمز، ولكنها لا تصلح أكثر من ذلك بكثير. يتطلب التعامل مع صورة بحجم كامل جهدًا أكثر قليلاً.

حفظ الصورة بالحجم الكامل

يحفظ تطبيق "كاميرا Android" صورة بالحجم الكامل إذا منحتها ملفًا لحفظها فيه. يجب تقديم اسم ملف مؤهل بالكامل حيث يجب أن يحفظ تطبيق الكاميرا الصورة.

بشكل عام، يجب حفظ أي صور يلتقطها المستخدم باستخدام كاميرا الجهاز على الجهاز في وحدة التخزين الخارجية العامة حتى يتمكن جميع التطبيقات من الوصول إليها. يتم توفير الدليل المناسب للصور المشتركة من قِبل getExternalStoragePublicDirectory()، مع الوسيطة DIRECTORY_PICTURES. إنّ الدليل الذي توفّره هذه الطريقة مشترك بين جميع التطبيقات. في نظام التشغيل Android 9 (المستوى 28 لواجهة برمجة التطبيقات) والإصدارات الأقدم، تتطلّب القراءة والكتابة في هذا الدليل الإذنَين READ_EXTERNAL_STORAGE وWRITE_EXTERNAL_STORAGE على التوالي:

<manifest ...>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

في نظام التشغيل Android 10 (المستوى 29 لواجهة برمجة التطبيقات) والإصدارات الأحدث، يشكّل جدول MediaStore.Images الدليل المناسب لمشاركة الصور. ليس عليك تقديم بيان بشأن أي أذونات لمساحة التخزين، طالما أنّ تطبيقك يحتاج فقط إلى الوصول إلى الصور التي التقطها المستخدم باستخدام تطبيقك.

أمّا إذا أردت أن تبقى الصور خاصة بتطبيقك فقط، فيمكنك بدلاً من ذلك استخدام الدليل المتوفّر من Context.getExternalFilesDir(). في نظام التشغيل Android 4.3 والإصدارات الأقدم، تتطلب الكتابة في هذا الدليل أيضًا إذن WRITE_EXTERNAL_STORAGE. وبدءًا من الإصدار 4.4 من نظام التشغيل Android، لم يعُد الإذن مطلوبًا لأنّ الدليل لا يمكن الوصول إليه من خلال تطبيقات أخرى، وبالتالي يمكنك توضيح أنه يجب طلب الإذن فقط في إصدارات Android الأقدم عن طريق إضافة السمة maxSdkVersion:

<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                     android:maxSdkVersion="28" />
    ...
</manifest>

ملاحظة: عند إلغاء تثبيت التطبيق، يتم حذف الملفات التي تحفظها في الأدلة المتوفّرة من خلال getExternalFilesDir() أو getFilesDir().

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

Kotlin

lateinit var currentPhotoPath: String

@Throws(IOException::class)
private fun createImageFile(): File {
    // Create an image file name
    val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
    val storageDir: File = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
    return File.createTempFile(
            "JPEG_${timeStamp}_", /* prefix */
            ".jpg", /* suffix */
            storageDir /* directory */
    ).apply {
        // Save a file: path for use with ACTION_VIEW intents
        currentPhotoPath = absolutePath
    }
}

Java

String currentPhotoPath;

private File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    String imageFileName = "JPEG_" + timeStamp + "_";
    File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
    File image = File.createTempFile(
        imageFileName,  /* prefix */
        ".jpg",         /* suffix */
        storageDir      /* directory */
    );

    // Save a file: path for use with ACTION_VIEW intents
    currentPhotoPath = image.getAbsolutePath();
    return image;
}

باستخدام هذه الطريقة لإنشاء ملف للصورة، يمكنك الآن إنشاء واستدعاء Intent على النحو التالي:

Kotlin

private fun dispatchTakePictureIntent() {
    Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
        // Ensure that there's a camera activity to handle the intent
        takePictureIntent.resolveActivity(packageManager)?.also {
            // Create the File where the photo should go
            val photoFile: File? = try {
                createImageFile()
            } catch (ex: IOException) {
                // Error occurred while creating the File
                ...
                null
            }
            // Continue only if the File was successfully created
            photoFile?.also {
                val photoURI: Uri = FileProvider.getUriForFile(
                        this,
                        "com.example.android.fileprovider",
                        it
                )
                takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
                startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
            }
        }
    }
}

Java

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Ensure that there's a camera activity to handle the intent
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        // Create the File where the photo should go
        File photoFile = null;
        try {
            photoFile = createImageFile();
        } catch (IOException ex) {
            // Error occurred while creating the File
            ...
        }
        // Continue only if the File was successfully created
        if (photoFile != null) {
            Uri photoURI = FileProvider.getUriForFile(this,
                                                  "com.example.android.fileprovider",
                                                  photoFile);
            takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
            startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
        }
    }
}

ملاحظة: نستخدم السمة getUriForFile(Context, String, File) التي تعرض معرّف موارد منتظم (URI) content://. بالنسبة إلى التطبيقات الأحدث التي تستهدف الإصدار Android 7.0 (المستوى 24 لواجهة برمجة التطبيقات) والإصدارات الأحدث، يؤدي ضبط معرّف موارد منتظم (URI) file:// عبر حدود الحزمة إلى ظهور FileUriExposedException. لذلك، نقدّم الآن طريقة أكثر عمومية لتخزين الصور باستخدام FileProvider.

عليك الآن ضبط FileProvider. في ملف البيان الخاص بالتطبيق، عليك إضافة موفّر خدمة إلى تطبيقك:

<application>
   ...
   <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.example.android.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_paths"></meta-data>
    </provider>
    ...
</application>

تأكَّد من أنّ سلسلة المراجع تتطابق مع الوسيطة الثانية مع getUriForFile(Context, String, File). في قسم البيانات الوصفية ضمن تعريف الموفّر، يمكنك معرفة أنّ مقدّم الخدمة يتوقّع ضبط المسارات المؤهّلة في ملف موارد مخصّص، وهو res/xml/file_paths.xml. إليك المحتوى المطلوب لهذا المثال المحدّد:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="my_images" path="Pictures" />
</paths>

ويتطابق مكوِّن المسار مع المسار الذي يعرضه getExternalFilesDir() عند استدعاءه باستخدام Environment.DIRECTORY_PICTURES. تأكَّد من استبدال com.example.package.name باسم الحزمة الفعلي لتطبيقك. ويمكنك أيضًا مراجعة مستندات FileProvider للحصول على وصف شامل لمحددات المسارات التي يمكنك استخدامها إلى جانب external-path.

إضافة الصورة إلى معرض

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

ملاحظة: إذا حفظت صورتك في الدليل الذي يقدّمه تطبيق getExternalFilesDir()، لن يتمكّن ماسح الوسائط من الوصول إلى الملفات لأنّها خاصة بتطبيقك.

توضِّح الطريقة التالية في المثال التالي كيفية طلب الماسح الضوئي للوسائط في النظام لإضافة صورتك إلى قاعدة بيانات موفِّر الوسائط، ما يجعلها متاحة في تطبيق معرض Android والتطبيقات الأخرى.

Kotlin

private fun galleryAddPic() {
    Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE).also { mediaScanIntent ->
        val f = File(currentPhotoPath)
        mediaScanIntent.data = Uri.fromFile(f)
        sendBroadcast(mediaScanIntent)
    }
}

Java

private void galleryAddPic() {
    Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    File f = new File(currentPhotoPath);
    Uri contentUri = Uri.fromFile(f);
    mediaScanIntent.setData(contentUri);
    this.sendBroadcast(mediaScanIntent);
}

فك ترميز صورة تم تغيير حجمها

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

Kotlin

private fun setPic() {
    // Get the dimensions of the View
    val targetW: Int = imageView.width
    val targetH: Int = imageView.height

    val bmOptions = BitmapFactory.Options().apply {
        // Get the dimensions of the bitmap
        inJustDecodeBounds = true

        BitmapFactory.decodeFile(currentPhotoPath, bmOptions)

        val photoW: Int = outWidth
        val photoH: Int = outHeight

        // Determine how much to scale down the image
        val scaleFactor: Int = Math.max(1, Math.min(photoW / targetW, photoH / targetH))

        // Decode the image file into a Bitmap sized to fill the View
        inJustDecodeBounds = false
        inSampleSize = scaleFactor
        inPurgeable = true
    }
    BitmapFactory.decodeFile(currentPhotoPath, bmOptions)?.also { bitmap ->
        imageView.setImageBitmap(bitmap)
    }
}

Java

private void setPic() {
    // Get the dimensions of the View
    int targetW = imageView.getWidth();
    int targetH = imageView.getHeight();

    // Get the dimensions of the bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;

    BitmapFactory.decodeFile(currentPhotoPath, bmOptions);

    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Determine how much to scale down the image
    int scaleFactor = Math.max(1, Math.min(photoW/targetW, photoH/targetH));

    // Decode the image file into a Bitmap sized to fill the View
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(currentPhotoPath, bmOptions);
    imageView.setImageBitmap(bitmap);
}