Android - Lấy hình ảnh thu nhỏ và đầy đủ từ camera.

Xin chào tất cả các bạn, hôm nay mình xin chia sẽ với các bạn cách lấy hình ảnh thu nhỏ (thumbnail) và hình ảnh đầy đủ sử dụng camera của thiết bị Android.

1. Vấn đề.

  • Chúng ta thường sử dụng hai loại ảnh từ camera, đầu tiên là hình ảnh thu nhỏ (thumbnail) với độ phân giải thấp và dĩ nhiên dung lượng cũng thấp, ảnh này thường được sử dụng vào các view nhỏ để đại diện, nói có vẻ khó hiểu nhưng rất gần gũi với chúng ta thôi đó là thư viện ảnh (gallery) khi bạn mở lên thì sẽ có một danh sách các bức ảnh mà bạn có trong thiết bị, mỗi bức ảnh nhỏ trong danh sách đó chính là thumbnail image.

    Như thế này chắc cũng các bạn đã hình dung ra thumbnail image là gì rồi phải không ạ. Còn loại thứ hai chính là full image, đây chính là bức ảnh đầy đủ mà các bạn có, nó có độ phân giải cao hơn và dĩ nhiên dung lượng cũng lớn hơn rất nhiều so với thumbnail image. Tại sao mình lại đề cập đến vấn đề này vì nghe có vẻ xử lí và hiển thị ảnh khá là đơn giản tuy nhiên vấn đề không đơn giản như vậy, các bạn sẽ gặp rắc rối với nó đấy ạ, rất nhiều bạn sẽ gặp lỗi OutOfMemmory đơn giản vì dung lượng của ảnh là rất lớn và Android lại rất hạn chế trong việc xử lý các tệp lớn như thế đó là lí do tại sao chúng ta lại có hai loại ảnh như vậy.

2. Giải pháp

2.1: Đầu tiên chúng ta phải chụp được ảnh đã.

static final int REQUEST_IMAGE_CAPTURE = 1;
private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE);
    }
}

Ở đây các bạn thấy chúng ta kiểm tra điều kiện takePictureIntent.resolveActivity(getPackageManager()) != null để đảm bảo rằng app sẽ không bị crash khi bạn gọi startActivityForResult mà không có ứng dụng nào xử lí. Vậy là đã chụp được ảnh rồi, giờ ta đến bước tiếp theo.

2.2 Lấy hình ảnh thu nhỏ (thumbnail)

  • Khi bạn sử dụng Intent để chuoj ảnh như ở trên thì app camera sẽ trẻ về cho bạn một fioe Bitmap nhỏ trong key "data" mà bạn có thể lấy được ở trong onActivityResult.
@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");
        mImageView.setImageBitmap(imageBitmap);
    }
}
  • Mọi việc có vẻ khá là đơn giản, tuy nhiên đôi khi bạn sẽ cần ảnh lớn, ví dụ như khi bạn làm app có chức năng xem ảnh hoặc đơn giản là bạn cần up ảnh đó lên server chẳng hạn, thumbnail sẽ không đáp ứng được và bạn sẽ phải lấy ảnh đầy đủ (full-sized image).

2.3 Lấy hình ảnh đầy đủ (full-sized image).

  • Đầu tiên bạn cần phải xin quyền WRITE_EXTERNAL_STORAGE (bạn chỉ cần xin quyền ghi thôi vì khi bạn xin quyền này thì đồng thời bạn cũng có quyền đọc luôn rồi) , tại sao phải xin quyền này vì camera sẽ lưu cho bạn ảnh đầy đủ nếu bạn cung cấp cho nó một tệp để lưu vào, và đương nhiên kèm theo nó là một cái tên tệp hợp lệ.
<manifest ...>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    ...
</manifest>

Thông thường thì ảnh này sẽ được lưu trữ ở bộ nhớ ngoài chung (các app khác cũng có thể lấy) thư mục thích hợp cho việc này được cung cấp bởi getExternalStoragePublicDirectory(). Tuy nhiên, nếu bạn muốn ảnh chỉ ở chế độ riêng tư cho ứng dụng của mình bạn có thể sử dụng thư mục được cung cấp bởi getExternalFilesDir () khi bạn lưu trữ riêng tư như này thì khi bạn gỡ cài đặt ứng dụng các ảnh mà bạn lưu cũng sẽ bị xóa theo.

  • Đầu tiên ta tạo File để lưu ảnh lại.
 String mCurrentPhotoPath;
//Biến này để sau này bạn có thể tiện sử dụng ảnh.
private File createImageFile() throws IOException {
    // Tạo tên file dựa vào nhãn thời gian.
    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 */
    );

    // Lưu lại giá trị đường dẫn của ảnh vào biến mCurrentPhotoPath
    mCurrentPhotoPath = image.getAbsolutePath();
    return image;
}
  • Sau khi đã tạo được File để lưu ảnh ta sử dụng Intent để chụp ảnh.
static final int REQUEST_TAKE_PHOTO = 1;

private void dispatchTakePictureIntent() {
    Intent takePictureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    if (takePictureIntent.resolveActivity(getPackageManager()) != null) {
        // Create the File where the photo should go
        File photoFile = null;
        try {
            photoFile = createImageFile();
        } catch (IOException ex) {
            ...
        }
        // 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_TAKE_PHOTO);
        }
    }
}
  • Ở trên chắc các bạn đã thấy chúng ta lấy photoUri bằng cách sử dụng FileProvider, việc chúng ta lấy uri này và tạo file như ở trên để Android có thể biết được nơi để lưu ảnh.
  • Tiếp theo chúng ta sẽ tạo content provider. Ở trong file Manifest các bạn khai báo thẻ providder
<application>
  ...
  <provider
       android:name="android.support.v4.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>
  • Các bạn lưu ý cần set giá trị authorities giống với giá trị mà các bạn sử dụng trong phương thức getUriForFile ban nãy mà các bạn đã viết ở trên.
  • Trong thư mục res các bạn cần tạo một file resource res/xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" />
</paths>
  • Các bạn cần thay đổi com.example.package.name thành tên package của ứng dụng của bạn.
  • Rồi vậy là xong khá nhiều bước cầu kỳ, vậy thì tại sao chúng ta lại phải làm như vậy, đó là tại vì khi bạn muốn lấy hình ảnh đầy đủ Android sẽ không trả về cho bạn ảnh hoặc url của ảnh qua intent cho bạn đâu ạ, mà bạn sẽ phải tự lấy bằng chính đường dẫn File mà các bạn tạo để lưu file ảnh ở trên, đó là lí do chúng ta phải tự tạo file và lấy Filepath cho việc này.
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    super.onActivityResult(requestCode, resultCode, intent);
    try {
        switch (requestCode) {
            case 0: {
                if (resultCode == RESULT_OK) {
                    File file = new File(currentPhotoPath);
                    Bitmap bitmap = MediaStore.Images.Media
                            .getBitmap(context.getContentResolver(), Uri.fromFile(file));
                    if (bitmap != null) {
                        ...Với file path mà các bạn đã có ở trên các bạn có thể lấy file, bitmap hay các định dạng khác mà bạn có thể dễ dàng ép kiểu sang. Ở đây mình lấy bitmap, bitmap này chính là bitmap của ảnh đầy đủ.
                    }
                }
                break;
            }
        }

    } catch (Exception error) {
        error.printStackTrace();
    }
}

2.4 Xử lý ảnh đầy đủ (full-size image).

  • Ok vậy là sau khi cố gắng để lấy được ảnh đầy đủ giờ chúng ta lại chuẩn bị làm giảm chất lượng của ảnh đi ạ. Nghe có vẻ sai sai nhưng thực ra như ban đầu mình đã trình bày file ảnh đầy đủ này là quá lớn đối với android, và khi có nhiều ảnh thì xử lí sẽ là cả một vấn đề lớn.vì android có bộ nhớ hạn chế. Bạn có thể giảm thiểu việc này bằng cách chuyển file ảnh này thành một mảng các byte sao cho phù hợp, ở đây mình xin giới thiệu hai cách đó là nén trực tiêp Bitmap và Scale image.
  • Với cách nén Bitmap, các bạn sẽ nén bitmap của ảnh đầy đủ lại nhằm giảm kích cỡ.
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, bos)
  • Các bạn lưu ý ở đây JPEG là định dạng mà các bạn muốn nén (mặc định android đã cung cấp cho ta ảnh dạng jpeg để giữ cho ảnh dung lượng nhẹ nên nếu các bạn mà xài .PNG ở đây thì không những không giảm mà kích cỡ ảnh se tăng rất nhiều lần sau khi nén đấy ạ). Tham số thứ 2 là chất lượng sau khi nén ở đây mình để 50 (một nửa) còn bos chỉ đơn giản là mảng ByteArrayOutputStream() mà mình muốn nén bitmao thành. Khi sử dụng cách này các bạn nên lưu ý là file dữ liệu exif của ảnh sẽ bị mất thế nên nếu cần các bạn cần phải lưu thông tin exif này trước khi nén và ghi lại sau khi nén xong. Thông thường vấn đề hay gặp phải với tệp exif này đó là ảnh sẽ bị xoay sau khi nén bởi vì thông tin về hướng của ảnh được lưu trong tệp này. Ta có thể lấy exif trước khi nén bằng cách:
   // Ở đây mình ví dụ là lấy thông tin về hướng của ảnh.
    val oldExif = ExifInterface(path)
    val exifOrientation = oldExif.getAttribute(ExifInterface.TAG_ORIENTATION)
  • Sau đó sau khi nén ta sẽ ghi lại thông tin này vào file ảnh:
if (exifOrientation != null) {
        val newExif = ExifInterface(path)
        newExif.setAttribute(ExifInterface.TAG_ORIENTATION, exifOrientation)
        newExif.saveAttributes()
    }

- Cách thứ hai đó là scale kích cỡ của ảnh sao cho phù hợp với view mà chúng ta muốn hiển thi.
private void setPic() {
    // Lấy kích thước của view
    int targetW = mImageView.getWidth();
    int targetH = mImageView.getHeight();

    // Lấy kích thước của bitmap
    BitmapFactory.Options bmOptions = new BitmapFactory.Options();
    bmOptions.inJustDecodeBounds = true;
    BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    int photoW = bmOptions.outWidth;
    int photoH = bmOptions.outHeight;

    // Xác định mức độ giảm tỷ lệ hình ảnh.
    int scaleFactor = Math.min(photoW/targetW, photoH/targetH);

    // Decode file thành bitmap để set cho view
    bmOptions.inJustDecodeBounds = false;
    bmOptions.inSampleSize = scaleFactor;
    bmOptions.inPurgeable = true;

    Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
    mImageView.setImageBitmap(bitmap);
}
  • Các bạn cũng có thể thay đổi bitmap config để thay giảm dung lượng của ảnh nữa.

3. Tổng kết.

Trên đây là một vài chia sẻ của mình về việc lấy ảnh, xử lý ảnh trong android, bài viết có thể có còn chỗ sai sót mong các bạn góp ý để bài viết tốt hơn. Cảm ơn các bạn đã theo dõi bài viết của mình.