Ngày nay bộ lọc ảnh khá phổ biến ở rất nhiều ứng dụng Android. Instagram nổi tiếng với tính năng bộ lọc màu và có lẽ là ứng dụng đầu tiên giới thiệu bộ lọc ảnh trong thế giới android. Ngoài ra cũng có rất nhiều ứng dụng chỉnh sửa hình ảnh khác cung cấp bộ lọc hình ảnh và các tính năng chỉnh sửa hình ảnh.

Trong bài này chúng ta sẽ học cách xây dựng một ứng dụng bộ lọc hình ảnh như Instagram.

Trong bài viết lần này, tôi sẽ hướng dẫn các bạn bắt đầu làm việc với các thư viện để sử dụng cho việc viết ứng dụng lọc ảnh

1. Bộ lọc màu ảnh được tạo ra như thế nào

Thông thường thao tác xử lý ảnh sẽ được thực hiện bằng các ngôn ngữ native C / C ++. Trong Android, bạn cũng có thể viết thư viện của mình trong C hoặc C ++ và sử dụng JNI (Java Native Interface) để làm cho các hàm có thể truy cập qua code java. Bạn cũng có thể xem xét sử dụng thư viện xử lý hình ảnh phổ biến như openCV để tạo thư viện bộ lọc của riêng bạn.

Khi viết các native modules, cấu hình JNI là một chủ đề tạch biệt với nhau. Bây giờ chúng ta sẽ xem xét việc sử dụng thư viện bộ lọc ảnh hiện có trong bài viết này.

2. Sử dụng thư viện (Zomato, Androidhive)

Đối với bài viết này, sẽ sử dụng thư viện bộ lọc ảnh AndroidPhotoFilters do Zomato phát triển. Thư viện này cung cấp các thao tác với hình ảnh cơ bản như kiểm soát độ sáng, bão hòa, tương phản và vài bộ lọc hình ảnh khác. Kết hợp tất cả các tính năng này với nhau, bạn có thể tạo các bộ lọc của mình.

Cũng nhớ thư viện là rất cơ bản, bạn không thể đạt được các bộ lọc tuyệt vời như Instagram sử dụng. Để xây dựng bộ lọc chính xác như Instagram, rất nhiều công việc cầnt phải được thực hiện ở cấp độ native. Nhưng chúng tôi sẽ cố gắng có được các bộ lọc giống với Instagram.

Để sử dụng thư viện, thêm info.androidhive: imagefilters: 1.0.7 trong dependencies của dự án.

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    // ...
 
    implementation 'info.androidhive:imagefilters:1.0.7'
}

Tài liệu hướng dẫn chi tiết về thư viện có thể được tìm thấy trên Github.

3. Gói bộ lọc ảnh

Đã có vài bộ lọc ảnh và gom chúng trong thư viện. Bạn có thể lấy bộ lọc sử dụng đoạn mã dưới đây. Bạn có thể lặp lại chúng và hiển thị phiên bản thu nhỏ của mỗi bộ lọc. Bạn cũng có thể truy cập trực tiếp vào bộ lọc cá nhân.

// get the filter pack
List<filter> filters = FilterPack.getFilterPack(getActivity());
 
for (Filter filter : filters) {
        ThumbnailItem item = new ThumbnailItem();
        item.image = thumbImage;
        item.filter = filter;
        item.filterName = filter.getName();
        ThumbnailsManager.addThumb(tI);
}
 
// Accessing single filter...
Bitmap bitmap = your_bitmap_;
Filter clarendon = FilterPack.getClarendon();
// apply filter
imagePreview.setImageBitmap(filter.processFilter(bitmap));

Dưới đây là bản preview của mỗi bộ lọc cùng với tên của nó.



4. Xây dựng giao diện giống như Instagram

Ý tưởng ở đây là xây dựng giao diện giống như Instagram với hai tab dưới cùng. Một tab là áp dụng các bộ lọc khác nhau và tab khác là kiểm soát các điều chỉnh hình ảnh như độ sáng, độ tương phản và độ bão hòa.

Để làm được layout này, chúng ta cần phải sử dụng ViewPager kết hợp với TabLayout. Để hiển thị các hình ảnh thu nhỏ trong một danh sách có thể cuộn, cần phải có RecyclerView. Chúng ta cũng cần hai class Fragment, một để hiển thị hình ảnh thu nhỏ theo chiều ngang để xem trước hiệu ứng lọc. Fragment khác là để hiển thị việc control hình ảnh.

ImageFiltersFragment.java được sử dụng để hiển thị hình ảnh thu nhỏ theo chiều ngang. EditImageFrament.java được sử dụng để render các control hình ảnh. Cả 2 fragment sẽ cung cấp các phương pháp gọi lại bất cứ khi nào bộ lọc được áp dụng hoặc control hình ảnh được thay đổi. Trong MainActivity thích hợp sẽ được thực hiện khi gọi lại.

Bây giờ, hãy bắt đầu với một dự án mới trong Android Studio.

Bước 1

Tạo một dự án mới trong Android Studio từ File ⇒ New Project và chọn Basic Activity

Bước 2

Mở file build.gradle nằm trong thư mục ứng dụng và thêm vào dependencies. Ngoài ra cũng thêm các dependencies cần thiết khác như ButterKnife, Dexter và RecyclerView.

// get the filter pack
List<filter> filters = FilterPack.getFilterPack(getActivity());
 
for (Filter filter : filters) {
        ThumbnailItem item = new ThumbnailItem();
        item.image = thumbImage;
        item.filter = filter;
        item.filterName = filter.getName();
        ThumbnailsManager.addThumb(tI);
}
 
// Accessing single filter...
Bitmap bitmap = your_bitmap_;
Filter clarendon = FilterPack.getClarendon();
// apply filter
imagePreview.setImageBitmap(filter.processFilter(bitmap));

Bước 3

Thêm resources dưới đây vào file strings.xml, colors.xml, dimens.xml và styles.xml tương ứng.

strings.xml
<resources>
    <string name="app_name">Image Filters</string>
    <string name="activity_title_main">Filters</string>
    <string name="action_settings">Settings</string>
    <string name="filters">FILTERS</string>
    <string name="edit">EDIT</string>
 
    <string name="lbl_brightness">BRIGHTNESS</string>
    <string name="lbl_contrast">CONTRAST</string>
    <string name="lbl_saturation">SATURATION</string>
    <string name="tab_filters">FILTERS</string>
    <string name="tab_edit">EDIT</string>
 
 
    <string name="roboto_medium">sans-serif-medium</string>
    <string name="filter_normal">Normal</string>
    <string name="action_save">SAVE</string>
    <string name="action_open">OPEN</string>
</resources>
colors.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="colorPrimary">#3F51B5</color>
    <color name="colorPrimaryDark">#303F9F</color>
    <color name="colorAccent">#009688</color>
    <color name="color_option_menu">#FF3990</color>
    <color name="filter_label_normal">#8A8889</color>
    <color name="filter_label_selected">#221F20</color>
</resources>
dimens.xml
<resources>
    <dimen name="fab_margin">16dp</dimen>
    <dimen name="thumbnail_size">80dp</dimen>
    <dimen name="recycler_size">100dp</dimen>
    <dimen name="thumbnail_horizontal_padding">8dp</dimen>
    <dimen name="thumbnail_vertical_padding">10dp</dimen>
    <dimen name="padding_10">10dp</dimen>
    <dimen name="margin_horizontal">16dp</dimen>
    <dimen name="lbl_edit_image_control">100dp</dimen>
</resources>
styles.xml
<resources>
 
    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>
 
    <style name="AppTheme.NoActionBar">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
    </style>
 
    <style name="AppTheme.NoActionBar.Fullscreen">
        <item name="windowActionBar">false</item>
        <item name="windowNoTitle">true</item>
        <item name="android:windowFullscreen">true</item>
        <item name="android:actionMenuTextColor">@color/color_option_menu</item>
    </style>
 
    <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Light" />
 
    <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
 
</resources>

Bước 4

Mở AndroidManifest.xml và thêm quyền READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE. Thêm chủ đề AppTheme.NoActionBar.Full vào main activity để hoạt động toàn màn hình.

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="info.androidhive.imagefilters">
 
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
 
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar.Fullscreen">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
 
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
 
</manifest>

Bước 5

Mở menu_main.xml nằm trong thư mục menu trong thư mục res and và sửa đổi menu items như dưới đây. Menu này cung cấpviệc OPENSAVE trên thanh công cụ để mở và lưu hình ảnh.

menu_main.xml
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    tools:context="info.androidhive.imagefilters.MainActivity">
    <item
        android:id="@+id/action_open"
        android:orderInCategory="100"
        android:title="@string/action_open"
        app:showAsAction="always" />
 
    <item
        android:id="@+id/action_save"
        android:orderInCategory="101"
        android:title="@string/action_save"
        app:showAsAction="always" />
</menu>

Bước 6

Tạo một package mới mang tên utils. Ở đây, chúng ta sẽ tiếp tục giữ vài helper class cho ứng dụng này.

Bước 7

Trong package utils, tạo một lớp có tên BitmapUtils.java. Class này sẽ có các phương pháp để tải hình ảnh từ thư viện, nén hình ảnh, lưu hình ảnh vào thư viện.

BitmapUtils.java
package info.androidhive.imagefilters.utils;
 
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
 
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
 
public class BitmapUtils {
 
    private static final String TAG = BitmapUtils.class.getSimpleName();
 
    /**
     * Getting bitmap from Assets folder
     *
     * @return
     */
    public static Bitmap getBitmapFromAssets(Context context, String fileName, int width, int height) {
        AssetManager assetManager = context.getAssets();
 
        InputStream istr;
        Bitmap bitmap = null;
        try {
            final BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
 
            istr = assetManager.open(fileName);
 
            // Calculate inSampleSize
            options.inSampleSize = calculateInSampleSize(options, width, height);
 
            // Decode bitmap with inSampleSize set
            options.inJustDecodeBounds = false;
            return BitmapFactory.decodeStream(istr, null, options);
        } catch (IOException e) {
            Log.e(TAG, "Exception: " + e.getMessage());
        }
 
        return null;
    }
 
    /**
     * Getting bitmap from Gallery
     *
     * @return
     */
    public static Bitmap getBitmapFromGallery(Context context, Uri path, int width, int height) {
        String filePathColumn = {MediaStore.Images.Media.DATA};
        Cursor cursor = context.getContentResolver().query(path, filePathColumn, null, null, null);
        cursor.moveToFirst();
        int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
        String picturePath = cursor.getString(columnIndex);
        cursor.close();
 
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFile(picturePath, options);
 
        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, width, height);
 
        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFile(picturePath, options);
    }
 
    private static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // Raw height and width of image
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
 
        if (height > reqHeight || width > reqWidth) {
 
            final int halfHeight = height / 2;
            final int halfWidth = width / 2;
 
            // Calculate the largest inSampleSize value that is a power of 2 and keeps both
            // height and width larger than the requested height and width.
            while ((halfHeight / inSampleSize) >= reqHeight
                    && (halfWidth / inSampleSize) >= reqWidth) {
                inSampleSize *= 2;
            }
        }
 
        return inSampleSize;
    }
 
    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
                                                         int reqWidth, int reqHeight) {
 
        // First decode with inJustDecodeBounds=true to check dimensions
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
 
        // Calculate inSampleSize
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
 
        // Decode bitmap with inSampleSize set
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }
 
    /**
     * Storing image to device gallery
     * @param cr
     * @param source
     * @param title
     * @param description
     * @return
     */
    public static final String insertImage(ContentResolver cr,
                                           Bitmap source,
                                           String title,
                                           String description) {
 
        ContentValues values = new ContentValues();
        values.put(MediaStore.Images.Media.TITLE, title);
        values.put(MediaStore.Images.Media.DISPLAY_NAME, title);
        values.put(MediaStore.Images.Media.DESCRIPTION, description);
        values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
        // Add the date meta data to ensure the image is added at the front of the gallery
        values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
        values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
 
        Uri url = null;
        String stringUrl = null;    /* value to be returned */
 
        try {
            url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
 
            if (source != null) {
                OutputStream imageOut = cr.openOutputStream(url);
                try {
                    source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
                } finally {
                    imageOut.close();
                }
 
                long id = ContentUris.parseId(url);
                // Wait until MINI_KIND thumbnail is generated.
                Bitmap miniThumb = MediaStore.Images.Thumbnails.getThumbnail(cr, id, MediaStore.Images.Thumbnails.MINI_KIND, null);
                // This is for backward compatibility.
                storeThumbnail(cr, miniThumb, id, 50F, 50F, MediaStore.Images.Thumbnails.MICRO_KIND);
            } else {
                cr.delete(url, null, null);
                url = null;
            }
        } catch (Exception e) {
            if (url != null) {
                cr.delete(url, null, null);
                url = null;
            }
        }
 
        if (url != null) {
            stringUrl = url.toString();
        }
 
        return stringUrl;
    }
 
    /**
     * A copy of the Android internals StoreThumbnail method, it used with the insertImage to
     * populate the android.provider.MediaStore.Images.Media#insertImage with all the correct
     * meta data. The StoreThumbnail method is private so it must be duplicated here.
     *
     * @see android.provider.MediaStore.Images.Media (StoreThumbnail private method)
     */
    private static final Bitmap storeThumbnail(
            ContentResolver cr,
            Bitmap source,
            long id,
            float width,
            float height,
            int kind) {
 
        // create the matrix to scale it
        Matrix matrix = new Matrix();
 
        float scaleX = width / source.getWidth();
        float scaleY = height / source.getHeight();
 
        matrix.setScale(scaleX, scaleY);
 
        Bitmap thumb = Bitmap.createBitmap(source, 0, 0,
                source.getWidth(),
                source.getHeight(), matrix,
                true
        );
 
        ContentValues values = new ContentValues(4);
        values.put(MediaStore.Images.Thumbnails.KIND, kind);
        values.put(MediaStore.Images.Thumbnails.IMAGE_ID, (int) id);
        values.put(MediaStore.Images.Thumbnails.HEIGHT, thumb.getHeight());
        values.put(MediaStore.Images.Thumbnails.WIDTH, thumb.getWidth());
 
        Uri url = cr.insert(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, values);
 
        try {
            OutputStream thumbOut = cr.openOutputStream(url);
            thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut);
            thumbOut.close();
            return thumb;
        } catch (FileNotFoundException ex) {
            return null;
        } catch (IOException ex) {
            return null;
        }
    }
}

Bước 8

Trong package utils, tạo một class có tên NonSwipeableViewPager.java. Đây là thành phần custom từ ViewPager vô hiệu hóa chức năng vuốt giữa các trang.

NonSwipeableViewPager.java
package info.androidhive.imagefilters.utils;
 
import android.content.Context;
import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.animation.DecelerateInterpolator;
import android.widget.Scroller;
 
import java.lang.reflect.Field;
 
/**
 * Created by ravi on 24/10/17.
 * Custom viewpager disabling the swipe
 * https://stackoverflow.com/questions/9650265/how-do-disable-paging-by-swiping-with-finger-in-viewpager-but-still-be-able-to-s
 */
 
public class NonSwipeableViewPager extends ViewPager {
 
    public NonSwipeableViewPager(Context context) {
        super(context);
        setMyScroller();
    }
 
    public NonSwipeableViewPager(Context context, AttributeSet attrs) {
        super(context, attrs);
        setMyScroller();
    }
 
    @Override
    public boolean onInterceptTouchEvent(MotionEvent event) {
        // Never allow swiping to switch between pages
        return false;
    }
 
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        // Never allow swiping to switch between pages
        return false;
    }
 
    //down one is added for smooth scrolling
 
    private void setMyScroller() {
        try {
            Class<?> viewpager = ViewPager.class;
            Field scroller = viewpager.getDeclaredField("mScroller");
            scroller.setAccessible(true);
            scroller.set(this, new MyScroller(getContext()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    public class MyScroller extends Scroller {
        public MyScroller(Context context) {
            super(context, new DecelerateInterpolator());
        }
 
        @Override
        public void startScroll(int startX, int startY, int dx, int dy, int duration) {
            super.startScroll(startX, startY, dx, dy, 350 /*1 secs*/);
        }
    }
}

Bước 9

Dưới utils tạo một lớp khác có tên SpacesItemDecoration.java. Class này là để thêm padding xung quanh hình ảnh thumbnail. padding-right sẽ được thêm vào tất cả các hình ảnh thumbnail nhưng nó không phải là item cuối cùng trong danh sách.

SpacesItemDecoration.java
package info.androidhive.imagefilters.utils;
 
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.view.View;
 
/**
 * Created by ravi on 23/10/17.
 */
 
public class SpacesItemDecoration extends RecyclerView.ItemDecoration {
    private int space;
 
    public SpacesItemDecoration(int space) {
        this.space = space;
    }
 
    @Override
    public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
        if (parent.getChildAdapterPosition(view) == state.getItemCount() - 1) {
            outRect.left = space;
            outRect.right = 0;
        }else{
            outRect.right = space;
            outRect.left = 0;
        }
    }
}

Ở bài viết tiếp theo, tôi sẽ hướng dẫn các bạn tiếp tục thực hiện việc viết ứng dụng lọc ảnh và có kèm theo source code để các bạn tham khảo

Source