التقاط الصور

ملاحظة: تشير هذه الصفحة إلى الفئة Camera التي تم إيقافها نهائيًا. ننصح باستخدام CameraX أو Camera2 في حالات استخدام معيّنة. تتوافق كل من CameraX وCamera2 مع الإصدار 5.0 من نظام التشغيل Android (المستوى 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>

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

ومع ذلك، إذا أردت أن تظل الصور خاصة بتطبيقك فقط، يمكنك بدلاً من ذلك استخدام الدليل الذي يوفّره Context.getExternalFilesDir(). في الإصدار 4.3 من نظام التشغيل Android والإصدارات الأقدم، تتطلّب الكتابة في هذا الدليل أيضًا الحصول على إذن WRITE_EXTERNAL_STORAGE. بدءًا من الإصدار Android 4.4، لم يعُد الإذن مطلوبًا لأنّ الدليل لا يمكن للتطبيقات الأخرى الوصول إليه، لذا يمكنك الإعلان عن أنّه يجب طلب الإذن فقط في الإصدارات الأقدم من 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://. بالنسبة إلى التطبيقات الأحدث التي تستهدف الإصدار 7.0 من نظام التشغيل Android (مستوى واجهة برمجة التطبيقات 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);
}