Trong hướng dẫn này, chúng ta sẽ xây dựng một ứng dụng cho phép người dùng chọn hình ảnh từ camera hoặc thư viện và hiển thị hình đó trong một ImageView.
Lưu ý: đoạn mã bên dưới hoạt động tốt với các phiên bản Android trước Nougat. (Google đã từ bỏ quy ước đặt tên theo bảng chữ cái. Android Q đã được đổi tên thành Android 10. Vì hướng dẫn này được viết trước khi Google đưa ra quyết định này, nên bạn sẽ thấy Android Q được nhắc đến ở một số chỗ trong bài.)
Tổng quan về tính năng chụp ảnh trong Android
Kể từ Android Marshmallow, quyền truy cập cần được cấp vào thời điểm chạy ứng dụng (runtime permissions). Hãy thêm các quyền sau vào tệp AndroidManifest.xml, phía trên thẻ application.
<uses-feature
android:name="android.hardware.camera"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.autofocus"
android:required="false" />
<uses-feature
android:name="android.hardware.camera.flash"
android:required="false" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="ANDROID.PERMISSION.READ_EXTERNAL_STORAGE"/>
Bằng cách thêm android.hardware.camera, Play Store sẽ nhận biết và ngăn người dùng cài đặt ứng dụng trên các thiết bị không có camera. Intent là cách tiêu chuẩn để ủy quyền hành động cho một ứng dụng khác. Để khởi động ứng dụng camera gốc, Intent cần sử dụng hằng số android.provider.MediaStore.ACTION_IMAGE_CAPTURE. Còn để chọn ảnh từ thư viện, Intent cần đối số sau: Intent.ACTION_GET_CONTENT.
Trong hướng dẫn này, chúng ta sẽ gọi trình chọn ảnh (image picker), cho phép người dùng chọn ảnh từ camera hoặc thư viện, sau đó hiển thị ảnh trong circular ImageView (hình tròn)và một ImageView thông thường. Thêm dòng dependency (phụ thuộc) sau vào tệp build.gradle:compile 'de.hdodenhof:circleimageview:2.1.0'
Cấu trúc dự án Android Image Capture
Mã nguồn Android Capture Image
Giao diện trong tệp activity_main.xml vẫn giữ nguyên, chỉ thay đổi biểu tượng của nút FAB thành @android:drawable/ic_menu_camera
. Nội dung của tệp content_main.xml
được trình bày bên dưới:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="<https://schemas.android.com/apk/res/android>"
xmlns:app="<https://schemas.android.com/apk/res-auto>"
xmlns:tools="<https://schemas.android.com/tools>"
android:id="@+id/content_main"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:background="#000000"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context="com.journaldev.imagepicker.MainActivity"
tools:showIn="@layout/activity_main">
<RelativeLayout
android:layout_width="250dp"
android:layout_height="250dp"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:background="@drawable/image_border"
android:clickable="true"
android:orientation="vertical">
<ImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:scaleType="centerCrop" />
</RelativeLayout>
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/img_profile"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_gravity="center_horizontal"
android:src="@drawable/profile"
app:civ_border_width="5dp"
app:civ_border_color="#FFFFFF"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true" />
</RelativeLayout>
Mã nguồn cho tệp MainActivity.java
được trình bày như bên dưới:
public class MainActivity extends AppCompatActivity {
Bitmap myBitmap;
Uri picUri;
private ArrayList permissionsToRequest;
private ArrayList permissionsRejected = new ArrayList();
private ArrayList permissions = new ArrayList();
private final static int ALL_PERMISSIONS_RESULT = 107;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
startActivityForResult(getPickImageChooserIntent(), 200);
}
});
permissions.add(CAMERA);
permissionsToRequest = findUnAskedPermissions(permissions);
//get the permissions we have asked for before but are not granted..
//we will store this in a global list to access later.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (permissionsToRequest.size() > 0)
requestPermissions(permissionsToRequest.toArray(new String[permissionsToRequest.size()]), ALL_PERMISSIONS_RESULT);
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
/**
* Create a chooser intent to select the source to get image from.<br />
* The source can be camera's (ACTION_IMAGE_CAPTURE) or gallery's (ACTION_GET_CONTENT).<br />
* All possible sources are added to the intent chooser.
*/
public Intent getPickImageChooserIntent() {
// Determine Uri of camera image to save.
Uri outputFileUri = getCaptureImageOutputUri();
List allIntents = new ArrayList();
PackageManager packageManager = getPackageManager();
// collect all camera intents
Intent captureIntent = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE);
List listCam = packageManager.queryIntentActivities(captureIntent, 0);
for (ResolveInfo res : listCam) {
Intent intent = new Intent(captureIntent);
intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
intent.setPackage(res.activityInfo.packageName);
if (outputFileUri != null) {
intent.putExtra(MediaStore.EXTRA_OUTPUT, outputFileUri);
}
allIntents.add(intent);
}
// collect all gallery intents
Intent galleryIntent = new Intent(Intent.ACTION_GET_CONTENT);
galleryIntent.setType("image/*");
List listGallery = packageManager.queryIntentActivities(galleryIntent, 0);
for (ResolveInfo res : listGallery) {
Intent intent = new Intent(galleryIntent);
intent.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
intent.setPackage(res.activityInfo.packageName);
allIntents.add(intent);
}
// the main intent is the last in the list (fucking android) so pickup the useless one
Intent mainIntent = allIntents.get(allIntents.size() - 1);
for (Intent intent : allIntents) {
if (intent.getComponent().getClassName().equals("com.android.documentsui.DocumentsActivity")) {
mainIntent = intent;
break;
}
}
allIntents.remove(mainIntent);
// Create a chooser from the main intent
Intent chooserIntent = Intent.createChooser(mainIntent, "Select source");
// Add all other intents
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, allIntents.toArray(new Parcelable[allIntents.size()]));
return chooserIntent;
}
/**
* Get URI to image received from capture by camera.
*/
private Uri getCaptureImageOutputUri() {
Uri outputFileUri = null;
File getImage = getExternalCacheDir();
if (getImage != null) {
outputFileUri = Uri.fromFile(new File(getImage.getPath(), "profile.png"));
}
return outputFileUri;
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
Bitmap bitmap;
if (resultCode == Activity.RESULT_OK) {
ImageView imageView = (ImageView) findViewById(R.id.imageView);
if (getPickImageResultUri(data) != null) {
picUri = getPickImageResultUri(data);
try {
myBitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), picUri);
myBitmap = rotateImageIfRequired(myBitmap, picUri);
myBitmap = getResizedBitmap(myBitmap, 500);
CircleImageView croppedImageView = (CircleImageView) findViewById(R.id.img_profile);
croppedImageView.setImageBitmap(myBitmap);
imageView.setImageBitmap(myBitmap);
} catch (IOException e) {
e.printStackTrace();
}
} else {
bitmap = (Bitmap) data.getExtras().get("data");
myBitmap = bitmap;
CircleImageView croppedImageView = (CircleImageView) findViewById(R.id.img_profile);
if (croppedImageView != null) {
croppedImageView.setImageBitmap(myBitmap);
}
imageView.setImageBitmap(myBitmap);
}
}
}
private static Bitmap rotateImageIfRequired(Bitmap img, Uri selectedImage) throws IOException {
ExifInterface ei = new ExifInterface(selectedImage.getPath());
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
switch (orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return rotateImage(img, 90);
case ExifInterface.ORIENTATION_ROTATE_180:
return rotateImage(img, 180);
case ExifInterface.ORIENTATION_ROTATE_270:
return rotateImage(img, 270);
default:
return img;
}
}
private static Bitmap rotateImage(Bitmap img, int degree) {
Matrix matrix = new Matrix();
matrix.postRotate(degree);
Bitmap rotatedImg = Bitmap.createBitmap(img, 0, 0, img.getWidth(), img.getHeight(), matrix, true);
img.recycle();
return rotatedImg;
}
public Bitmap getResizedBitmap(Bitmap image, int maxSize) {
int width = image.getWidth();
int height = image.getHeight();
float bitmapRatio = (float) width / (float) height;
if (bitmapRatio > 0) {
width = maxSize;
height = (int) (width / bitmapRatio);
} else {
height = maxSize;
width = (int) (height * bitmapRatio);
}
return Bitmap.createScaledBitmap(image, width, height, true);
}
/**
* Get the URI of the selected image from {@link #getPickImageChooserIntent()}.<br />
* Will return the correct URI for camera and gallery image.
*
* @param data the returned data of the activity result
*/
public Uri getPickImageResultUri(Intent data) {
boolean isCamera = true;
if (data != null) {
String action = data.getAction();
isCamera = action != null && action.equals(MediaStore.ACTION_IMAGE_CAPTURE);
}
return isCamera ? getCaptureImageOutputUri() : data.getData();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// save file url in bundle as it will be null on scren orientation
// changes
outState.putParcelable("pic_uri", picUri);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// get the file url
picUri = savedInstanceState.getParcelable("pic_uri");
}
private ArrayList findUnAskedPermissions(ArrayList wanted) {
ArrayList result = new ArrayList();
for (String perm : wanted) {
if (!hasPermission(perm)) {
result.add(perm);
}
}
return result;
}
private boolean hasPermission(String permission) {
if (canMakeSmores()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED);
}
}
return true;
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
new AlertDialog.Builder(this)
.setMessage(message)
.setPositiveButton("OK", okListener)
.setNegativeButton("Cancel", null)
.create()
.show();
}
private boolean canMakeSmores() {
return (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1);
}
@TargetApi(Build.VERSION_CODES.M)
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case ALL_PERMISSIONS_RESULT:
for (String perms : permissionsToRequest) {
if (hasPermission(perms)) {
} else {
permissionsRejected.add(perms);
}
}
if (permissionsRejected.size() > 0) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (shouldShowRequestPermissionRationale(permissionsRejected.get(0))) {
showMessageOKCancel("These permissions are mandatory for the application. Please allow access.",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
//Log.d("API123", "permisionrejected " + permissionsRejected.size());
requestPermissions(permissionsRejected.toArray(new String[permissionsRejected.size()]), ALL_PERMISSIONS_RESULT);
}
}
});
return;
}
}
}
break;
}
}
}
Có rất nhiều điểm có thể suy ra từ đoạn mã ở trên:
- Chúng ta cần yêu cầu quyền truy cập Camera khi người dùng bắt đầu Activity.
- Vì ta khởi chạy Intent để nhận kết quả trả về, nên cần gọi startActivityForResult với các đối số phù hợp.
- Thay vì dùng một hộp thoại để gọi riêng lẻ các Intent cho Camera và Thư viện, ta sử dụng phương thức getPickImageChooserIntent(), phương thức này tạo một chooser intent duy nhất bao gồm cả các intent cho camera và thư viện ảnh (lưu ý có thêm cả intent tài liệu). Intent.EXTRA_INITIAL_INTENTS được dùng để thêm nhiều intent ứng dụng tại cùng một chỗ.
- Với intent cho Camera, MediaStore.EXTRA_OUTPUT được truyền vào như một extra để xác định đường dẫn lưu trữ hình ảnh. Nếu không có dòng này, ảnh trả về chỉ là bản kích thước nhỏ.
- Đường dẫn URI cho hình ảnh được Camera trả về được lấy trong phương thức
getCaptureImageOutputUri()
. - Phương thức onActivityResult chủ yếu trả về một URI đến hình ảnh. Một số thiết bị có thể trả về bitmap bằng
data.getExtras().get("data")
. - Khi ảnh được chụp, màn hình Camera có thể khởi động lại Activity khi quay lại, làm cho biến URI lưu từ phương thức
getCaptureImageOutputUri()
bị null. Do đó, ta cần lưu và khôi phục giá trị này bằngonSaveInstanceState()
vàonRestoreInstanceState()
. - Bitmap được lấy từ URI bằng dòng mã:
myBitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), picUri);
- Một số thiết bị như Samsung Galaxy thường lưu ảnh theo chiều ngang. Nếu hiển thị ảnh nguyên bản, ảnh có thể bị sai chiều. Vì vậy, ta cần gọi phương thức
rotateImageIfRequired(myBitmap, picUri);
- ExifInterface là một lớp dùng để đọc và ghi các thẻ Exif trong tệp JPEG hoặc tệp ảnh RAW.
- Cuối cùng, phương thức getResizedBitmap() được gọi để điều chỉnh kích thước bitmap theo chiều rộng hoặc chiều cao (tùy bên nào lớn hơn), sau đó ảnh được hiển thị lên ImageView bằng setImageBitmap.
Kết quả đầu ra của ứng dụng được hiển thị ngay bên dưới. Lưu ý: Để chụp và hiển thị ảnh từ camera, bạn cần chạy ứng dụng trên một thiết bị điện thoại thực tế.