Ngày nay, tính năng chụp và chỉnh sửa ảnh đã trở nên cực kỳ phổ biến trong các ứng dụng smart phone. Hôm qua nhân dịp vọc vạch nấu nướng mình có download và sử dụng app Feedy để thực hành theo. Chợt nhận ra Feedy cũng có 1 module cho phép user chụp ảnh (có filter camera) và chỉnh sửa ảnh. Mình thấy rất thú vị nên mình cũng clone luôn để làm 1 loạt bài tutorial về filter trên Android. Sau đây là demo ứng dụng của mình:
Các chức năng chính trong app mình xây dựng sẽ bao gồm:
- Chụp ảnh (có filter camera, sử dụng camera trước hoặc camera sau)
- Chỉnh sửa ảnh (lựa chọn filter)
- Cắt ảnh, viết caption cho ảnh và chia sẻ
- Đơn giản thôi đúng không nào. Giờ mình sẽ bắt tay vào làm nhé.
[toc]
Các thư viện sử dụng trong dự án
Lưu ý: Link source code ở cuối bài viết 😉
Mình sẽ dùng các thư viện sau:
- Android – GPU Image Plus: Đây là 1 thư viện filter khá mạnh với nhiều tính năng. Nó cung cấp cho ta các tính năng cơ bản như điều khiển độ sáng, độ phân giản, độ tương phản, … Ngoài ra thư viện này cung cấp cho ta tính năng sử dụng những file Lookup table (LUT) để áp dụng filter lên bitmap. Ngoài ra còn rất nhiều tính năng về xử lý ảnh khác nữa. Phần core của thư viện này viết bằng C/C++.
- Glide: Thư viện load ảnh.
- ButterKnife: Thư viện inject view, đỡ phải sử dụng findViewById rất dài dòng và nhàm chán.
- SimpleCropView: Thư viện cho phép ta cắt 1 vùng của bitmap.
- RoundedImageView: Thư viện bo góc cho imageview, do mình lười custom nên mình cũng dùng thư viện luôn cho nhanh.
Đó là những thư viện cơ bản mình dùng trong project.
Sơ lược về luồng đi của ứng dụng
Ứng dụng của chúng ta gồm 4 màn hình:
- Splash screen hiển thị logo của ứng dụng đồng thời xin quyền đọc ghi trên bộ nhớ máy (để lưu ảnh), quyền sử dụng camera .
- Camera screen: Chụp ảnh, lựa chọn hiệu ứng
- Result screen: Sau khi chụp ảnh, hiển thị ảnh đầu ra, cắt ảnh, chỉnh sửa ảnh
- Share screen: Hiển thị ảnh cuối cùng sau khi chỉnh sửa, viết caption và share ảnh.
Bắt đầu
Việc đầu tiên sẽ là… tạo project rồi. Tất nhiên, các bạn hãy tạo 1 project để code nhé 😀
Import thư viện
Mình sẽ import các thư viện kể trên vào dự án, thêm các lệnh này vào file build.gradle (level app):
//Thư viện ảnh bo góc compile 'com.makeramen:roundedimageview:2.3.0' //Thư viện filter compile 'org.wysaid:gpuimage-plus:2.4.9' //Thư viện cắt ảnh compile 'com.isseiaoki:simplecropview:1.1.6' //Thư viện dimension compile 'com.intuit.sdp:sdp-android:1.0.4' //Thư viện font chữ compile 'uk.co.chrisjenx:calligraphy:2.3.0' //Thư viện load ảnh compile 'com.github.bumptech.glide:glide:3.7.0'
Tạo base cho dự án
Permission
Ứng dụng cần quyền sử dụng camera và quyền đọc ghi file trên thiết bị Vì vậy ta cần thêm các dòng sau vào file AndroidManifest.xml:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.CAMERA" />
Font chữ
Mình sử dụng font Roboto cho dự án này. Trong thư mục main, mình sẽ tạo 1 thư mục assets chứa thư mục con Fonts (là nơi mình đặt các file Font chữ)
Gói image filter
Trong dự án này mình sẽ sử dụng các file Lookup table (LUT) – là các file chứa các thông số filter để chỉnh sửa màu sắc cho ảnh. Các file này chứa trong thư mục assets mình đã tạo bên trên:
Đây là các file chứa các thông số cho các filter mình sử dụng trong app.
BaseActivity
Như ở phần trên mình đã phân tích, luồng đi của ứng dụng qua 4 màn hình. Mình sẽ sử dụng các Activity tương ứng với mỗi màn, vậy ta có tổng cộng 4 Activity. Để code được ngắn gọn và tường minh, mình sẽ viết 1 lớp BaseActivity.java. Lớp này chứa 1 số phương thức chung có thể sử dụng cho tất cả các Activity trong dự án. Mọi Activity sẽ kế thừa từ lớp BaseActivity này:
public abstract class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(getLayoutId()); ButterKnife.bind(this); createView(); } //Hàm abstract trả về layout của activity protected abstract int getLayoutId(); //Hàm set sự kiện cho các view trong activity protected abstract void createView(); //Hàm show thông báo toast public void showToast(String msg) { if (msg != null) { Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "null", Toast.LENGTH_SHORT).show(); } } //Hàm chuyển activity public void showActivity(Class t) { Intent intent = new Intent(this, t); startActivity(intent); } ////Hàm chuyển activity kèm theo bundle public void showActivity(Class t, Bundle bundle) { Intent intent = new Intent(this, t); intent.putExtra(Constant.KEY_EXTRA, bundle); startActivity(intent); } }
Utils
Trong ứng dụng, có những chức năng ta cần đến kích thước màn hình, hoặc cần mở App Setting để user cấp quyền cho app. Những chức năng này mình viết trong 1 lớp DeviceUtils.java. Mình sẽ tạo 1 package đặt tên là utils để chưa file DeviceUtils.java.
package fbphoto.thou.com.utils; import android.app.Activity; import android.content.Intent; import android.net.Uri; import android.util.DisplayMetrics; /** * Created by Computer on 2/8/2018. */ public class DeviceUtils { //Lấy chiều rộng màn hình public static int getScreenWidth(Activity activity) { DisplayMetrics displayMetrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); return displayMetrics.widthPixels; } //Lấy chiều cao màn hình public static int getScreenHeight(Activity activity) { DisplayMetrics displayMetrics = new DisplayMetrics(); activity.getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); return displayMetrics.heightPixels; } //Mở app setting public static void openSettingsApp(Activity context) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.GINGERBREAD) { Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); intent.setData(Uri.parse("package:" + context.getPackageName())); context.startActivity(intent); } } }
Màn hình splash
Khai báo layout
Sau khi tạo xong base dự án ta sẽ bắt tay vào màn hình đầu tiên, là màn hình splash. Đây là màn hình có chức năng hiển thị ra 1 dòng text giới thiệu dưới dạng animation, đồng thời xin cấp các quyền mà ứng dụng sẽ sử dụng. Đây là file layout activity_spalsh.xml:
<?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"> <TextView android:id="@+id/tv_splash" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" android:layout_margin="@dimen/_10sdp" android:gravity="center" android:text="@string/intro" android:textColor="@android:color/black" android:textSize="40sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_centerHorizontal="true" android:layout_margin="@dimen/_10sdp" android:text="@string/website" android:textColor="@android:color/holo_red_dark" android:textSize="16sp" /> </RelativeLayout>
Đơn giản chỉ là 2 TextView giới thiệu về app thôi.
Khai báo SplashActivity trong AndroidManifest
Giờ ta sẽ tạo 1 file đặt tên là SplashActivity để viết nội dung cho màn hình splash. Class này sẽ kế thừa BaseActivity. Đồng thời trong AndroidManifest, ta khai báo SplashActivity là màn hình đầu tiên của ứng dụng (thay vì MainActivity mặc định):
<activity android:name=".SplashActivity" android:screenOrientation="portrait"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity>
Lớp SplashActivity:
public class SplashActivity extends BaseActivity { @Bind(R.id.tv_splash) TextView tvSplash; @Override protected int getLayoutId() { return R.layout.activity_splash; } @Override protected void createView() { } }
Đơn giản là khai báo layoutId và khai báo 1 TextView thôi đúng không. Giờ ta muốn cho TextView này 1 animation. Khi kết thúc animation, ta sẽ kiểm tra các quyền đọc ghi file, camera được cấp chưa. Nếu chưa được cấp thì sẽ bật ra dialog xin cấp quyền. Ta sẽ viết hàm animate để làm điều này.
Animation
private void animate() { AlphaAnimation animation = new AlphaAnimation(0f, 1f); animation.setDuration(2000); animation.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { } @Override public void onAnimationEnd(Animation animation) { askForPermission(); } @Override public void onAnimationRepeat(Animation animation) { } }); tvSplash.startAnimation(animation); }
Như các bạn thấy mình sử dụng AnimationListener để lắng nghe sự kiện animation kết thúc. Giờ ta sẽ viết hàm askForPermission để thực hiện bật dialog xin quyền từ user trong trường hợp các quyền ta sử dụng chưa được cấp phép. Nếu được cấp rồi thì nhảy thẳng đến màn hình Camera.
Phương thức xin cấp permission
Quay lại với BaseActivity 1 chút. Trong app này, mình chỉ có 1 chỗ cần phải xin quyền từ user, đó là ở SplashScreen. Tuy nhiên trong thực tế, 1 app có thể có nhiều lớp cần implement tính năng xin cấp quyền. Vì vậy mình sẽ viết hàm askCompactPermissions thực hiện tính năng này trong lớp BaseActivity. Các lớp kế thừa lớp BaseActivity sẽ đều có thể sử dụng hàm askCompactPermissions này để xin cấp quyền.
Đầu tiên ta viết 1 lớp PermissionUtils trong package utils:
package fbphoto.thou.com.utils; import android.Manifest; /** * Created by Computer on 2/8/2018. */ public class PermissionUtils { public static final String Manifest_READ_EXTERNAL_STORAGE = Manifest.permission.READ_EXTERNAL_STORAGE; public static final String Manifest_WRITE_EXTERNAL_STORAGE = Manifest.permission.WRITE_EXTERNAL_STORAGE; public static final String Manifest_CAMERA = Manifest.permission.CAMERA; public interface PermissionResult{ void permissionGranted(); void permissionDenied(); void permissionForeverDienid(); } }
Lớp này chứa các quyền ta cần xin (đọc ghi file, camera) và 1 interface PermissionResult xử lý các hành động của người dùng.
Trong BaseActivity, mình khởi tạo các biến sử dụng cho việc xin cấp quyền:
private final int KEY_PERMISSION = 200; private PermissionUtils.PermissionResult permissionResult; private String permissionsAsk[];
Và viết hàm askCompactPermissions như sau:
public void askCompactPermissions(String permissions[], PermissionUtils.PermissionResult permissionResult) { permissionsAsk = permissions; this.permissionResult = permissionResult; internalRequestPermission(permissionsAsk); } private void internalRequestPermission(String[] permissionAsk) { String arrayPermissionNotGranted[]; ArrayList<String> permissionsNotGranted = new ArrayList<>(); for (int i = 0; i < permissionAsk.length; i++) { if (!isPermissionGranted(permissionAsk[i])) { permissionsNotGranted.add(permissionAsk[i]); } } if (permissionsNotGranted.isEmpty()) { if (permissionResult != null) permissionResult.permissionGranted(); } else { arrayPermissionNotGranted = new String[permissionsNotGranted.size()]; arrayPermissionNotGranted = permissionsNotGranted.toArray(arrayPermissionNotGranted); ActivityCompat.requestPermissions(this, arrayPermissionNotGranted, KEY_PERMISSION); } } @Override public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { if (requestCode != KEY_PERMISSION) { return; } List<String> permissionDienid = new LinkedList<>(); boolean granted = true; for (int i = 0; i < grantResults.length; i++) { if (!(grantResults[i] == PackageManager.PERMISSION_GRANTED)) { granted = false; permissionDienid.add(permissions[i]); } } if (permissionResult != null) { if (granted) { permissionResult.permissionGranted(); } else { for (String s : permissionDienid) { if (!ActivityCompat.shouldShowRequestPermissionRationale(this, s)) { permissionResult.permissionForeverDienid(); return; } } permissionResult.permissionDenied(); } } }
Các hàm trên đảm nhiệm chức năng kiểm tra và hiển thị hộp thoại cấp quyền trong trường hợp người dùng chưa cấp quyền cho ứng dụng. Do đây ko phải trọng tâm ứng dụng nên mình sẽ ko nói quá kỹ phần này. Các bạn có thể tìm hiểu thêm hoặc sử dụng các thư viện hỗ trợ.
Sử dụng phương thức cấp permission
OK, đã viết hàm xin permission xong ở BaseActivity, giờ quay trở lại SplashScreen. Ta sẽ gọi hàm xin permission trong askForPermission:
private void askForPermission() { askCompactPermissions(new String[]{PermissionUtils.Manifest_CAMERA, PermissionUtils.Manifest_READ_EXTERNAL_STORAGE, PermissionUtils.Manifest_WRITE_EXTERNAL_STORAGE} , new PermissionUtils.PermissionResult() { @Override public void permissionGranted() { showActivity(MainActivity.class); finish(); } @Override public void permissionDenied() { showToast("Bạn cần cung cấp quyền để sử dụng ứng dụng"); finish(); } @Override public void permissionForeverDienid() { new AlertDialog.Builder(SplashActivity.this).setTitle("Cung cấp quyền") .setMessage("Bạn có muốn cấp quyền sử dụng camera, đọc file, ghi file để chạy demo này không?") .setPositiveButton("Có", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { DeviceUtils.openSettingsApp(SplashActivity.this); } }) .setNegativeButton("Không", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialogInterface, int i) { finish(); } }).show(); } }); }
Nội dung hàm này là: trong trường hợp permission được cấp, ta nhảy đến MainActivity (sẽ định nghĩa sau). Nếu không được cấp, thoát app. Nếu người dùng chọn option vĩnh viễn không cấp, ta sẽ hiện dialog yêu cầu người dùng bật App Setting và cấp lại quyền, nếu không sẽ thoát app.
Ngon rồi, giờ ta sẽ gọi hàm animate trong onResume của SplashActivity để thực hiện animation cho TextView:
@Override protected void onResume() { super.onResume(); animate(); }
Như vậy là mỗi khi vào app, hàm animate sẽ được gọi. Nó tạo ra 1 animation cho textview và khi animation kết thúc, kiểm tra trạng thái cấp quyền của ứng dụng.
Tổng kết
Ở phần đầu tiên của loạt bài hướng dẫn xây dựng app filter ảnh, mình đã giới thiệu tới các bạn các thư viện sử dụng trong app, cách tạo base project và xây dựng màn hình splash cho ứng dụng. Bài sau mình sẽ hướng dẫn xây dựng module chọn ảnh với các chức năng filter camera, và chức năng chọn ảnh trực tiếp từ bộ nhớ. Hãy theo dõi nhé.
One thought on “Xây dựng ứng dụng chụp ảnh và filter camera trên Android (p1)”