Hello, hôm nay mình sẽ trình bày nốt về các tính năng cuối cùng trong loạt bài về ứng dụng filter ảnh này. Các tính năng sẽ xây dựng sau khi hoàn thành bài viết:
- Load ảnh đã chụp từ camera
- Load ảnh từ bộ nhớ
- Chỉnh sửa ảnh
- Cắt ảnh
- Chia sẻ ảnh
[toc]
CameraResultActivity
Yeah, ở bài trước, mình đã tạo 1 CameraActivity trống để xử lý các dữ liệu truyền sang từ màn hình chụp ảnh (MainActivity). Sẽ có 2 trường hợp:
- Xử lý ảnh chụp từ camera
@OnClick({R.id.bt_take_picture}) public void onTakePictureClick() { showToast("Đang chụp ảnh..."); cameraView.takeShot(new CameraRecordGLSurfaceView.TakePictureCallback() { @Override public void takePictureOK(Bitmap bmp) { //Xử lý lưu file, truyền dữ liệu sang màn hình chỉnh ảnh ..... } }); }
- Chọn ảnh từ thư viện
@OnClick(R.id.iv_pick_image) public void pickImage() { Bundle bundle = new Bundle(); showActivity(CameraResultActivity.class, bundle); }
Với trường hợp này, ta đơn giản chỉ hiển thị CameraResultActivity và truyền cho nó 1 bundle rỗng. Ok, và nhiệm vụ của ta là sẽ phải xử lý các logic tương ứng với dữ liệu từ MainActivity truyền sang.
Xây dựng layout
Ta sẽ xây dựng layout cho màn hình chỉnh sửa ảnh. Mình tạo 1 file activity_camera_result.xml như sau:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:id="@+id/tb_camera" android:layout_width="match_parent" android:layout_height="45dp" android:background="@color/toolbar_color"> <ImageView android:id="@+id/bt_close" android:layout_width="@dimen/_30sdp" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/_4sdp" android:adjustViewBounds="true" android:padding="@dimen/_8sdp" android:src="@drawable/ic_close_camera" /> <ImageView android:id="@+id/bt_done" android:layout_width="@dimen/_30sdp" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/_4sdp" android:layout_marginRight="@dimen/_4sdp" android:adjustViewBounds="true" android:padding="@dimen/_6sdp" android:src="@drawable/ic_done_camera" /> <TextView style="@style/UTM_AvoBold" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="CAMERA" android:textColor="#3c3837" android:textSize="14sp" /> </RelativeLayout> <RelativeLayout android:id="@+id/rl_image" android:layout_width="match_parent" android:layout_height="@dimen/_100sdp" android:layout_below="@+id/tb_camera"> <com.isseiaoki.simplecropview.CropImageView android:id="@+id/iv_crop" android:layout_width="match_parent" android:layout_height="@dimen/_100sdp" /> <TextView android:id="@+id/tv_loading_image" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="Đang tải ảnh..." android:textSize="17sp" android:textStyle="bold" /> <!--<ProgressBar android:visibility="gone" android:id="@+id/pb_loading_image" android:layout_width="@dimen/_50sdp" android:layout_height="@dimen/_50sdp" android:layout_centerInParent="true" android:indeterminate="true" android:indeterminateTint="@color/colorPrimary" android:indeterminateTintMode="src_atop" android:progressDrawable="@drawable/progress_color_media" />--> </RelativeLayout> <RelativeLayout android:id="@+id/rl_action" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:layout_alignParentStart="true" android:background="#e2e2e2"> <TextView android:id="@+id/bt_back" style="@style/Roboto_Regular" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_margin="@dimen/_8sdp" android:text="@string/back" android:textColor="#3c3837" android:textSize="14sp" /> <TextView android:id="@+id/bt_continue" style="@style/Roboto_Regular" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_margin="@dimen/_8sdp" android:text="@string/next" android:textColor="#3c3837" android:textSize="14sp" /> </RelativeLayout> <android.support.v7.widget.RecyclerView android:id="@+id/rv_filter" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/rl_action" android:background="@color/toolbar_color"> </android.support.v7.widget.RecyclerView> </RelativeLayout>
Giao diện này cũng tương tự như giao diện màn hình chụp ảnh (MainActivity), khác mỗi cái là ta thay thế CameraView bằng 1 đối tượng CropImageView. Đây là 1 đối tượng ImageView đặc biệt, cho phép chúng ta lựa chọn vùng ảnh và cắt (crop). Màn hình này cũng chứa 1 list các hiệu ứng cho ta filter lên ImageView. Ok, đơn giản vậy thôi. Hãy tập trung vào việc xây dựng logic!
Xử lý ảnh chụp từ camera
Trước tiên, mình sẽ khai báo 1 biến String để lưu đường dẫn ảnh được truyền sang từ MainActivity (nếu có). 2 biến Bitmap để lưu bitmap của ảnh trước và sau khi filter. Sau đó mình viết 1 hàm get dữ liệu từ MainActivity truyền sang:
String imagePath; Bitmap resource; Bitmap finalResource;
private void getData() { Bundle bundle = getIntent().getBundleExtra(Constant.KEY_EXTRA); if (bundle != null) { imagePath = bundle.getString(Constant.KEY_IMAGE_PATH); targetScreen = bundle.getString(Constant.KEY_TARGET_SCREEN, ""); } }
Ta đồng thời cũng bind các view từ layout sang activity (sử dụng Butter Knife):
@Bind(R.id.rv_filter) RecyclerView rvFilter; @Bind(R.id.iv_crop) CropImageView ivCrop; @Bind(R.id.rl_image) RelativeLayout rlImage; @Bind(R.id.tv_loading_image) TextView tvLoadingImage;
Tiếp theo sẽ gọi hàm getData vừa viết trong hàm onCreate, đồng thời ta sẽ chỉnh sửa lại kích thước các view 1 chút cho đẹp mắt:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getData(); screenWidth = DeviceUtils.getScreenWidth(this); rlImage.getLayoutParams().height = screenWidth; ivCrop.getLayoutParams().height = screenWidth; }
Cuối cùng là viết logic xử lý ảnh chụp từ Camera (thực chất là hiển thị file ảnh có đường dẫn lưu trong biến imagePath:
if (imagePath != null && !imagePath.isEmpty()) { Glide.with(this).load(new File(imagePath)).asBitmap().format(DecodeFormat.PREFER_ARGB_8888).into(new SimpleTarget<Bitmap>() { @Override public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { CameraResultActivity.this.resource = resource; ivCrop.setImageBitmap(resource); ivCrop.setCustomRatio(screenWidth * 2 / 3, screenWidth * 2 / 3); tvLoadingImage.setVisibility(View.GONE); } }); }
Vậy là khi imagePath có giá trị, ta sẽ hiển thị ảnh lên ivCrop, đồng thời tạo sẵn 1 vùng crop mặc định (là toàn bộ ảnh). Logic này khá đơn giản đúng không.
Xử lý logic chọn ảnh từ thư viện
Trường hợp này được nhận biết bởi việc imagePath không có giá trị (MainActivity truyền 1 bundle rỗng sang). Nên ta sẽ xử lý như sau:
if (imagePath != null && !imagePath.isEmpty()) { //Xử lý ảnh chụp từ camera ..... } else { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); startActivityForResult(photoPickerIntent, RC_PICK_IMAGE); }
Sau đó, viết lại hàm onActivityResult như thế này:
@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == RC_PICK_IMAGE && resultCode == RESULT_OK) { Uri selectedImageUri = data.getData(); Glide.with(this).load(new File(getRealPathFromURI(selectedImageUri))).asBitmap().format(DecodeFormat.PREFER_ARGB_8888).into(new SimpleTarget<Bitmap>() { @Override public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) { CameraResultActivity.this.resource = resource; ivCrop.setImageBitmap(resource); ivCrop.setCustomRatio(screenWidth * 2 / 3, screenWidth * 2 / 3); tvLoadingImage.setVisibility(View.GONE); } }); } else { finish(); } }
Trong trường hợp người dùng có chọn ảnh từ thư viện, ta load ảnh đó lên ivvCrop (xử lý tương tự như trường hợp load ảnh chụp từ Camera). Nếu người dùng không chọn ảnh, ta finish activity này liền. Logic cũng khá đơn giản phải không. Ở đây, mình có viết thêm 1 hàm getRealPathFromURI để lấy đường dẫn ảnh mà người dùng chọn:
private String getRealPathFromURI(Uri contentURI) { String result; Cursor cursor = getContentResolver().query(contentURI, null, null, null, null); if (cursor == null) { // Source is Dropbox or other similar local file path result = contentURI.getPath(); } else { cursor.moveToFirst(); int idx = cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA); result = cursor.getString(idx); cursor.close(); } return result; }
Vậy là mình đã trình bày xong logic hiển thị ảnh. Giờ sẽ chuyển qua phần xử lý filter cho ảnh.
Chỉnh sửa ảnh
Chức năng này hoàn toàn tương tự như chức năng filter camera mình đã nói ở bài trước. Ta cũng sẽ tạo 1 hàm để hiển thị danh sách các filter lên như sau:
private void setUpListFilterEffect() { rvFilter.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false)); //create list filter List<FilterData> listFilter = new ArrayList<>(); int[] imageFilterId = {R.drawable.original_1, R.drawable.natural_1, R.drawable.natural_2 , R.drawable.pure_1, R.drawable.pure_2, R.drawable.pinky_1 , R.drawable.pinky_2, R.drawable.pinky_3, R.drawable.pinky_4 , R.drawable.warm_1, R.drawable.warm_2, R.drawable.cool_1 , R.drawable.cool_2, R.drawable.mood, R.drawable.bw}; for (int i = 0; i < EFFECT_CONFIGS.length; i++) { //listFilter.add(new FilterData(EFFECT_CONFIGS[i], imageFilterId[i ])); if (i == 0) { listFilter.add(new FilterData("Original", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 1) { listFilter.add(new FilterData("Natural 1", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 2) { listFilter.add(new FilterData("Natural 2", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 3) { listFilter.add(new FilterData("Pure 1", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 4) { listFilter.add(new FilterData("Pure 2", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 5) { listFilter.add(new FilterData("Pinky 1", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 6) { listFilter.add(new FilterData("Pinky 2", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 7) { listFilter.add(new FilterData("Pinky 3", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 8) { listFilter.add(new FilterData("Pinky 4", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 9) { listFilter.add(new FilterData("Warm 1", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 10) { listFilter.add(new FilterData("Warm 2", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 11) { listFilter.add(new FilterData("Cool 1", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 12) { listFilter.add(new FilterData("Cool 2", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 13) { listFilter.add(new FilterData("Mood", EFFECT_CONFIGS[i], imageFilterId[i])); } else if (i == 14) { listFilter.add(new FilterData("B&W", EFFECT_CONFIGS[i], imageFilterId[i])); } } ListFilterAdapter filterAdapter = new ListFilterAdapter(listFilter); rvFilter.setAdapter(filterAdapter); }
Ta tiếp theo ta sẽ viết 1 asynctask thực hiện công việc apply filter lên ivCrop:
private class FilterBitmapTask extends AsyncTask<String, Void, Bitmap> { @Override protected void onPreExecute() { super.onPreExecute(); showProgressDialog("filtering"); } @Override protected void onPostExecute(Bitmap resultBitmap) { super.onPostExecute(resultBitmap); ivCrop.setImageBitmap(resultBitmap); String logData = "FilterBitmapTask " + " bimap null:" + (resultBitmap == null); if (resultBitmap != null) { logData = logData + "bitmap width = " + resultBitmap.getWidth() + " - bitmap height = " + resultBitmap.getHeight(); } hideProgressDialog(); } @Override protected Bitmap doInBackground(String... params) { finalResource = CGENativeLibrary.cgeFilterImage_MultipleEffects(resource, params[0], 1f); return finalResource; } }
Cuối cùng, trong hàm setupListFilterEffect, ta thêm đoạn code set sự kiện listener cho item của recycler view:
filterAdapter.setOnFilterSelect(new ListFilterAdapter.OnFilterSelect() { @Override public void onSelect(FilterData filterData) { new FilterBitmapTask().execute(filterData.getRule()); } });
Vậy là đã hoàn tất việc filter cho ảnh.
Crop ảnh
Giờ ta sẽ làm đến chức năng crop ảnh. Bản thân CropImageView đã cho phép ta chọn vùng ảnh để crop, công việc của ta bây giờ chỉ là xử lý vùng ảnh được chọn đó thôi. Mình sẽ viết hàm xử lý sự kiện xác nhận crop ảnh (khi ấn vào nút continue hoặc done):
@OnClick({R.id.bt_continue, R.id.bt_done}) public void cropImage() { ivCrop.startCrop(null, new CropCallback() { @Override public void onSuccess(Bitmap cropped) { File file = new File(Environment.getExternalStorageDirectory() + "/FeedyPhoto/Filtered"); if (!file.exists()) { file.mkdirs(); } String imagePath = ImageUtil.saveBitmap(cropped, file.getAbsolutePath() + "/" + System.currentTimeMillis() + ".jpg"); Bundle bundle = new Bundle(); if (imagePath != null) { bundle.putString(Constant.KEY_IMAGE_PATH, imagePath); showActivity(FinalImageActivity.class, bundle); } } @Override public void onError(Throwable e) { } }, new SaveCallback() { @Override public void onSuccess(Uri uri) { } @Override public void onError(Throwable e) { } }); }
Đơn giản là sử dụng hàm startCrop của CropImageView để cắt ảnh, sau đó lưu vào thư mục FeedyPhoto/Filtered. Cuối cùng là truyền đường dẫn ảnh đã lưu sang FinalImageActivity. Đây sẽ là activity có chức năng hiển thị ảnh cuối cùng sau khi chỉnh sửa (filter, crop) và chia sẻ ảnh.
FinalImageActivity
Trước hết mình sẽ tạo 1 activity trống như thế này:
public class FinalImageActivity extends BaseActivity { @Override protected int getLayoutId() { return 0; } @Override protected void createView() { } }
Sau đó khai báo nó trong AndroidManifest.xml
<activity android:name=".FinalImageActivity" android:screenOrientation="portrait"> </activity>
Xây dựng layout
Mình tạo 1 file layout đặt tên là activity_final_image.xml như sau:
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <RelativeLayout android:id="@+id/tb_camera" android:layout_width="match_parent" android:layout_height="45dp" android:background="@color/toolbar_color"> <ImageView android:id="@+id/bt_close" android:layout_width="@dimen/_30sdp" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/_4sdp" android:adjustViewBounds="true" android:padding="@dimen/_8sdp" android:src="@drawable/ic_close_camera" /> <ImageView android:id="@+id/bt_post" android:layout_width="@dimen/_30sdp" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_centerVertical="true" android:layout_marginLeft="@dimen/_4sdp" android:layout_marginRight="@dimen/_4sdp" android:adjustViewBounds="true" android:padding="@dimen/_6sdp" android:src="@drawable/ic_done_camera" /> <TextView style="@style/UTM_AvoBold" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:text="CAMERA" android:textColor="#3c3837" android:textSize="14sp" /> </RelativeLayout> <EditText android:id="@+id/et_caption" android:layout_width="match_parent" android:layout_height="@dimen/_100sdp" android:layout_margin="@dimen/_12sdp" android:background="@drawable/bg_et_image_caption" android:gravity="top|left" android:hint="Nhập chú thích cho bức hình..." android:inputType="textMultiLine" android:lines="100" android:maxLines="100" android:paddingBottom="@dimen/_25sdp" android:paddingLeft="@dimen/_5sdp" android:paddingRight="@dimen/_5sdp" android:paddingTop="@dimen/_5sdp" android:textColorHint="#B0B0B0" /> <ImageView android:id="@+id/iv_picture" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> </RelativeLayout>
Activity này cơ bản là có 1 EditText để viết caption, 1 ImageView để hiển thị ảnh (sau khi chỉnh sửa), và 1 button để chia sẻ ảnh.
Hiển thị ảnh sau khi chỉnh sửa
Đầu tiên mình sẽ khai báo các view sử dụng trong activity trước:
@Bind(R.id.iv_picture) ImageView ivPicture; @Bind(R.id.et_caption) EditText etCaption; String imagePath;
Đơn giản là 1 ImageView và 1 EditText thôi. Thêm vào đó là 1 biến String để chứa đường dẫn ảnh truyền sang từ màn hình CameraResultActivity.
Mình viết 1 hàm getData để lấy dữ liệu truyền sang từ CameraResultActivity:
private void getData() { Bundle bundle = getIntent().getBundleExtra(Constant.KEY_EXTRA); if (bundle != null) { imagePath = bundle.getString(Constant.KEY_IMAGE_PATH); } }
Hàm này sẽ get được đường dẫn ảnh cần hiển thị. Sau khi lấy được đường dẫn rồi thì hiển thị ra thôi:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getData(); if (imagePath != null && !imagePath.isEmpty()) { Glide.with(this).load(new File(imagePath)).diskCacheStrategy(DiskCacheStrategy.ALL).into(ivPicture); } }
Vậy là xong, cùng đi sang chức năng chia sẻ nào.
Chia sẻ ảnh
Mình sẽ bắt sự kiện click cho button có id bt_post:
@OnClick(R.id.bt_post) public void post() { //share final Intent intent = new Intent(android.content.Intent.ACTION_SEND); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(new File(imagePath))); intent.putExtra(Intent.EXTRA_TEXT, etCaption.getText().toString()); intent.setType("image/png"); startActivity(Intent.createChooser(intent, "Share image via")); }
Tổng kết
Vậy là mình đã trình bày xong toàn bộ các chức năng của app filter ảnh và camera. Toàn bộ source code các bạn lấy tại https://github.com/nanashi1111/ImageFilterDemo. Hãy đọc code và chỉnh sửa cũng như thêm mới các chức năng để hiểu rõ hơn nữa nhé. Nếu có vấn đề gì chưa rõ hãy comment, mình sẽ giải thích.