Bài viết lần này sẽ tiếp tục hướng dẫn các bạn viết ứng dụng lọc ảnh, các bạn có thể xem lại phần 1 bài viết này tại đây

5. Tạo RecyclerView Adapter cho Thumbnails filter

Ở phàn 1 chúng ta đã có tất cả các class bắt buộc. Bây giờ hãy cùng tạo class adapter của RecyclerView trước khi chuyển sang giao diện người dùng thực tế.

Bước 10

Tạo một file layout mới có tên thumbnail_list_item.xml. Layout này chứa một TextView và ImageView để hiển thị tên bộ lọc và hình thumbnail.

thumbnail_list_item.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="match_parent"
    android:orientation="vertical">
 
    <TextView
        android:id="@+id/filter_name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginBottom="5dp"
        android:layout_marginTop="5dp"
        android:fontFamily="@string/roboto_medium" />
 
    <ImageView
        android:id="@+id/thumbnail"
        android:layout_width="@dimen/thumbnail_size"
        android:layout_height="@dimen/thumbnail_size"
        android:src="@mipmap/ic_launcher" />
 
</LinearLayout>

Bước 11

Tạo một class tên là ThumbnailsAdapter.java, class này hoạt động như bộ thu nhỏ của RecyclerView để hiển thị hình ảnh được lọc trong danh sách ngang.

ThumbnailsAdapter.java
package info.androidhive.imagefilters;
 
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
 
import com.zomato.photofilters.imageprocessors.Filter;
import com.zomato.photofilters.utils.ThumbnailItem;
 
import java.util.List;
 
import butterknife.BindView;
import butterknife.ButterKnife;
 
/**
 * Created by ravi on 23/10/17.
 */
 
public class ThumbnailsAdapter extends RecyclerView.Adapter<ThumbnailsAdapter.MyViewHolder> {
 
    private List<ThumbnailItem> thumbnailItemList;
    private ThumbnailsAdapterListener listener;
    private Context mContext;
    private int selectedIndex = 0;
 
    public class MyViewHolder extends RecyclerView.ViewHolder {
        @BindView(R.id.thumbnail)
        ImageView thumbnail;
 
        @BindView(R.id.filter_name)
        TextView filterName;
 
        public MyViewHolder(View view) {
            super(view);
 
            ButterKnife.bind(this, view);
        }
    }
 
 
    public ThumbnailsAdapter(Context context, List<ThumbnailItem> thumbnailItemList, ThumbnailsAdapterListener listener) {
        mContext = context;
        this.thumbnailItemList = thumbnailItemList;
        this.listener = listener;
    }
 
    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View itemView = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.thumbnail_list_item, parent, false);
 
        return new MyViewHolder(itemView);
    }
 
    @Override
    public void onBindViewHolder(MyViewHolder holder, final int position) {
        final ThumbnailItem thumbnailItem = thumbnailItemList.get(position);
 
        holder.thumbnail.setImageBitmap(thumbnailItem.image);
 
        holder.thumbnail.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                listener.onFilterSelected(thumbnailItem.filter);
                selectedIndex = position;
                notifyDataSetChanged();
            }
        });
 
        holder.filterName.setText(thumbnailItem.filterName);
 
        if (selectedIndex == position) {
            holder.filterName.setTextColor(ContextCompat.getColor(mContext, R.color.filter_label_selected));
        } else {
            holder.filterName.setTextColor(ContextCompat.getColor(mContext, R.color.filter_label_normal));
        }
    }
 
    @Override
    public int getItemCount() {
        return thumbnailItemList.size();
    }
 
    public interface ThumbnailsAdapterListener {
        void onFilterSelected(Filter filter);
    }
}

6. Thêm danh sách các bộ lọc ảnh trong list Fragment

Bây giờ chúng ta sẽ tạo một class Fragment để hiển thị hìnhthumbnails của bức ảnh được lọc trong danh sách nằm ngang. Để đạt được điều này, chúng ta cần một RecyclerView và cung cấp danh sách các hình ảnh thu nhỏ cho class adapter.

Bước 12

Tạo một Fragment mới bằng cách vào File ⇒ New ⇒ Fragment ⇒ Fragment (Blank) và đặt tên nó là FiltersListFragment.java.

Bước 13

Mở file layout của fragment_filters_list.xml fragment và thêm phần tử RecyclerView.

fragment_filters_list.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.androidhive.imagefilters.FiltersListFragment">
 
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recycler_view"
        android:layout_gravity="center_vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:clipChildren="false"
        android:padding="4dp"
        android:scrollbars="none" />
</FrameLayout>

Bước 14

Mở file FiltersListFragment.java và thực hiện các sửa đổi như được hiển thị bên dưới.

FilterPack.getFilterPack () cung cấp danh sách các bộ lọc có sẵn từ thư viện.

Trong phương thức prepareThumbnail () lọc qua và mỗi thumbnail item được thêm vào ThumbnailsManager để xử lý chúng. Các hình thumbnail được xử lý được thêm vào lại vào thumbnailItemList là tài nguyên dữ liệu cho RecyclerView.

Khi bộ dữ liệu hình thu nhỏ đã sẵn sàng, mAdapter.notifyDataSetChanged () được gọi để hiển thị danh sách. Tất cả điều này đã được thực hiện trong background thread như quá trình xử lý hình ảnh

Interfac FiltersListFragmentListener cung cấp phương thức gọi lại chomain activity bất cứ khi nào một bộ lọc mới được chọn.

Việc xử lý thực tế bộ lọc hình ảnh đã chọn sẽ được nói cụ thể khi nói về MainActivity.

FiltersListFragment.java
package info.androidhive.imagefilters;
 
import android.graphics.Bitmap;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.support.v7.widget.DefaultItemAnimator;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
 
import com.zomato.photofilters.FilterPack;
import com.zomato.photofilters.imageprocessors.Filter;
import com.zomato.photofilters.utils.ThumbnailItem;
import com.zomato.photofilters.utils.ThumbnailsManager;
 
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
import butterknife.BindView;
import butterknife.ButterKnife;
import info.androidhive.imagefilters.utils.BitmapUtils;
import info.androidhive.imagefilters.utils.SpacesItemDecoration;
 
 
public class FiltersListFragment extends Fragment implements ThumbnailsAdapter.ThumbnailsAdapterListener {
    @BindView(R.id.recycler_view)
    RecyclerView recyclerView;
 
    ThumbnailsAdapter mAdapter;
 
    List<ThumbnailItem> thumbnailItemList;
 
    FiltersListFragmentListener listener;
 
    public void setListener(FiltersListFragmentListener listener) {
        this.listener = listener;
    }
 
    public FiltersListFragment() {
        // Required empty public constructor
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        View view = inflater.inflate(R.layout.fragment_filters_list, container, false);
 
        ButterKnife.bind(this, view);
 
        thumbnailItemList = new ArrayList<>();
        mAdapter = new ThumbnailsAdapter(getActivity(), thumbnailItemList, this);
 
        RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getActivity(), LinearLayoutManager.HORIZONTAL, false);
        recyclerView.setLayoutManager(mLayoutManager);
        recyclerView.setItemAnimator(new DefaultItemAnimator());
        int space = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 8,
                getResources().getDisplayMetrics());
        recyclerView.addItemDecoration(new SpacesItemDecoration(space));
        recyclerView.setAdapter(mAdapter);
 
        prepareThumbnail(null);
 
        return view;
    }
 
    /**
     * Renders thumbnails in horizontal list
     * loads default image from Assets if passed param is null
     *
     * @param bitmap
     */
    public void prepareThumbnail(final Bitmap bitmap) {
        Runnable r = new Runnable() {
            public void run() {
                Bitmap thumbImage;
 
                if (bitmap == null) {
                    thumbImage = BitmapUtils.getBitmapFromAssets(getActivity(), MainActivity.IMAGE_NAME, 100, 100);
                } else {
                    thumbImage = Bitmap.createScaledBitmap(bitmap, 100, 100, false);
                }
 
                if (thumbImage == null)
                    return;
 
                ThumbnailsManager.clearThumbs();
                thumbnailItemList.clear();
 
                // add normal bitmap first
                ThumbnailItem thumbnailItem = new ThumbnailItem();
                thumbnailItem.image = thumbImage;
                thumbnailItem.filterName = getString(R.string.filter_normal);
                ThumbnailsManager.addThumb(thumbnailItem);
 
                List<Filter> filters = FilterPack.getFilterPack(getActivity());
 
                for (Filter filter : filters) {
                    ThumbnailItem tI = new ThumbnailItem();
                    tI.image = thumbImage;
                    tI.filter = filter;
                    tI.filterName = filter.getName();
                    ThumbnailsManager.addThumb(tI);
                }
 
                thumbnailItemList.addAll(ThumbnailsManager.processThumbs(getActivity()));
 
                getActivity().runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mAdapter.notifyDataSetChanged();
                    }
                });
            }
        };
 
        new Thread(r).start();
    }
 
    @Override
    public void onFilterSelected(Filter filter) {
        if (listener != null)
            listener.onFilterSelected(filter);
    }
 
    public interface FiltersListFragmentListener {
        void onFilterSelected(Filter filter);
    }
}

7. Thêm fragment chỉnh sửa, controls ảnh

Bây giờ chúng ta sẽ thêm phần controls ảnh để điều khiển độ sáng, độ tương phản và độ bão hòa.

Bước 15

Tạo thêm một fragment có tên EditImageFragment.java bằng cách thực hiện theo các bước. Mở file layout của fragment có tên là fragment_edit_image.xml này và thực hiện các sửa đổi như dưới đây.

Trong layoyt này chúng ta sẽ thêm ba view: SeekBar để điều khiển độ sáng, độ tương phản và độ bão hòa của hình ảnh.

fragment_edit_image.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center_vertical"
    android:orientation="vertical"
    android:paddingLeft="@dimen/margin_horizontal"
    android:paddingRight="@dimen/margin_horizontal"
    tools:context="info.androidhive.imagefilters.EditImageFragment">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingBottom="@dimen/padding_10"
        android:paddingTop="@dimen/padding_10">
 
 
        <TextView
            android:layout_width="@dimen/lbl_edit_image_control"
            android:layout_height="wrap_content"
            android:text="@string/lbl_brightness" />
 
        <SeekBar
            android:id="@+id/seekbar_brightness"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingBottom="@dimen/padding_10"
        android:paddingTop="@dimen/padding_10">
 
 
        <TextView
            android:layout_width="@dimen/lbl_edit_image_control"
            android:layout_height="wrap_content"
            android:text="@string/lbl_contrast" />
 
        <SeekBar
            android:id="@+id/seekbar_contrast"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingBottom="@dimen/padding_10"
        android:paddingTop="@dimen/padding_10">
 
        <TextView
            android:layout_width="@dimen/lbl_edit_image_control"
            android:layout_height="wrap_content"
            android:text="@string/lbl_saturation" />
 
        <SeekBar
            android:id="@+id/seekbar_saturation"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_weight="1" />
    </LinearLayout>
 
</LinearLayout>

Bước 16

Mở EditImageFragment.java và thực hiện các sửa đổi như dưới đây.

Trong phương thức onCreateView, các widgets Seekbar được khởi tạo với giá trị ban đầu và giá trị cực đại. Đối với độ sáng, giá trị có thể từ -100 / 100. Độ tương phản và độ bão hòa có giá trị nổi.

Interface EditImageFragmentListener cung cấp các phương thức gọi lại bất cứ khi nào khi các giá trị Seekbar thay đổi.

Việc xử lý độ sáng, độ tương phản và độ bão hòa một lần nữa được nói cụ thể trong MainActivity khi gọi lại.

EditImageFragment.java
package info.androidhive.imagefilters;
 
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.SeekBar;
 
import butterknife.BindView;
import butterknife.ButterKnife;
 
 
public class EditImageFragment extends Fragment implements SeekBar.OnSeekBarChangeListener {
 
    private EditImageFragmentListener listener;
 
    @BindView(R.id.seekbar_brightness)
    SeekBar seekBarBrightness;
 
    @BindView(R.id.seekbar_contrast)
    SeekBar seekBarContrast;
 
    @BindView(R.id.seekbar_saturation)
    SeekBar seekBarSaturation;
 
    public void setListener(EditImageFragmentListener listener) {
        this.listener = listener;
    }
 
    public EditImageFragment() {
        // Required empty public constructor
    }
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    }
 
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_edit_image, container, false);
 
        ButterKnife.bind(this, view);
 
        // keeping brightness value b/w -100 / +100
        seekBarBrightness.setMax(200);
        seekBarBrightness.setProgress(100);
 
        // keeping contrast value b/w 1.0 - 3.0
        seekBarContrast.setMax(20);
        seekBarContrast.setProgress(0);
 
        // keeping saturation value b/w 0.0 - 3.0
        seekBarSaturation.setMax(30);
        seekBarSaturation.setProgress(10);
 
        seekBarBrightness.setOnSeekBarChangeListener(this);
        seekBarContrast.setOnSeekBarChangeListener(this);
        seekBarSaturation.setOnSeekBarChangeListener(this);
 
        return view;
    }
 
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean b) {
        if (listener != null) {
 
            if (seekBar.getId() == R.id.seekbar_brightness) {
                // brightness values are b/w -100 to +100
                listener.onBrightnessChanged(progress - 100);
            }
 
            if (seekBar.getId() == R.id.seekbar_contrast) {
                // converting int value to float
                // contrast values are b/w 1.0f - 3.0f
                // progress = progress > 10 ? progress : 10;
                progress += 10;
                float floatVal = .10f * progress;
                listener.onContrastChanged(floatVal);
            }
 
            if (seekBar.getId() == R.id.seekbar_saturation) {
                // converting int value to float
                // saturation values are b/w 0.0f - 3.0f
                float floatVal = .10f * progress;
                listener.onSaturationChanged(floatVal);
            }
        }
    }
 
    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        if (listener != null)
            listener.onEditStarted();
    }
 
    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        if (listener != null)
            listener.onEditCompleted();
    }
 
    public void resetControls() {
        seekBarBrightness.setProgress(100);
        seekBarContrast.setProgress(0);
        seekBarSaturation.setProgress(10);
    }
 
    public interface EditImageFragmentListener {
        void onBrightnessChanged(int brightness);
 
        void onSaturationChanged(float saturation);
 
        void onContrastChanged(float contrast);
 
        void onEditStarted();
 
        void onEditCompleted();
    }
}

8. Thực hiện giao diện chính (kết hợp các fragment)

Bây giờ chúng ta đã có các fragments , chúng ta hãy cùng xem cách kết hợp chúng để đạt được kết quả cuối cùng

Bước 17

Mở file layout activity_main.xml và content_main.xml và NonSwipeableViewPager và TabLayout và thêm đoạn code sau

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="info.androidhive.imagefilters.MainActivity">
 
    <android.support.design.widget.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">
 
        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@android:color/white"
            app:popupTheme="@style/AppTheme.PopupOverlay" />
 
    </android.support.design.widget.AppBarLayout>
 
    <include layout="@layout/content_main" />
 
</android.support.design.widget.CoordinatorLayout>
content_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@android:color/white"
    android:orientation="vertical"
    app:layout_behavior="@string/appbar_scrolling_view_behavior"
    tools:context="info.androidhive.imagefilters.MainActivity"
    tools:showIn="@layout/activity_main">
 
    <ImageView
        android:id="@+id/image_preview"
        android:layout_width="match_parent"
        android:layout_height="360dp"
        android:scaleType="centerCrop" />
 
    <info.androidhive.imagefilters.utils.NonSwipeableViewPager
        android:id="@+id/viewpager"
        android:layout_width="match_parent"
        android:layout_height="120dp"
        android:layout_above="@+id/tabs"
        android:layout_below="@+id/image_preview"
        app:layout_behavior="@string/appbar_scrolling_view_behavior" />
 
    <android.support.design.widget.TabLayout
        android:id="@+id/tabs"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        app:tabGravity="fill"
        app:tabMode="fixed" />
 
</RelativeLayout>

Bước 18

Mở file MainActivity.java và thực hiện các thay đổi như dưới đây.

System.loadLibrary (“NativeImageProcessor”) được gọi để khởi tạo thư viện gốc.

FiltersListFragment và EditImageFragments được thêm vào ViewPager trong phương thức setupViewPager ().

onFilterSelected () sẽ được gọi khi bộ lọc được chọn trong FiltersListFragment. Bộ lọc đã chọn được xử lý và hình ảnh cuối cùng được hiển thị imagePreview.

onBrightnessChanged (), onSaturationChanged () và onContrastChanged () sẽ được gọi khi các giá trị Seekbar thay đổi trong EditImageFragments.

Hình ảnh cuối cùng sẽ được lưu vào Thư viện khi lựa chọn SAVE từ Thanh công cụ.

MainActivity.java
package info.androidhive.imagefilters;
 
import android.Manifest;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.Snackbar;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;
 
import com.karumi.dexter.Dexter;
import com.karumi.dexter.MultiplePermissionsReport;
import com.karumi.dexter.PermissionToken;
import com.karumi.dexter.listener.PermissionRequest;
import com.karumi.dexter.listener.multi.MultiplePermissionsListener;
import com.zomato.photofilters.imageprocessors.Filter;
import com.zomato.photofilters.imageprocessors.subfilters.BrightnessSubFilter;
import com.zomato.photofilters.imageprocessors.subfilters.ContrastSubFilter;
import com.zomato.photofilters.imageprocessors.subfilters.SaturationSubfilter;
 
import java.util.ArrayList;
import java.util.List;
 
import butterknife.BindView;
import butterknife.ButterKnife;
import info.androidhive.imagefilters.utils.BitmapUtils;
 
public class MainActivity extends AppCompatActivity implements FiltersListFragment.FiltersListFragmentListener, EditImageFragment.EditImageFragmentListener {
 
    private static final String TAG = MainActivity.class.getSimpleName();
 
    public static final String IMAGE_NAME = "dog.jpg";
 
    public static final int SELECT_GALLERY_IMAGE = 101;
 
    @BindView(R.id.image_preview)
    ImageView imagePreview;
 
    @BindView(R.id.tabs)
    TabLayout tabLayout;
 
    @BindView(R.id.viewpager)
    ViewPager viewPager;
 
    @BindView(R.id.coordinator_layout)
    CoordinatorLayout coordinatorLayout;
 
    Bitmap originalImage;
    // to backup image with filter applied
    Bitmap filteredImage;
 
    // the final image after applying
    // brightness, saturation, contrast
    Bitmap finalImage;
 
    FiltersListFragment filtersListFragment;
    EditImageFragment editImageFragment;
 
    // modified image values
    int brightnessFinal = 0;
    float saturationFinal = 1.0f;
    float contrastFinal = 1.0f;
 
    // load native image filters library
    static {
        System.loadLibrary("NativeImageProcessor");
    }
 
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ButterKnife.bind(this);
 
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);
        getSupportActionBar().setDisplayHomeAsUpEnabled(true);
        getSupportActionBar().setTitle(getString(R.string.activity_title_main));
 
        loadImage();
 
        setupViewPager(viewPager);
        tabLayout.setupWithViewPager(viewPager);
    }
 
    private void setupViewPager(ViewPager viewPager) {
        ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
 
        // adding filter list fragment
        filtersListFragment = new FiltersListFragment();
        filtersListFragment.setListener(this);
 
        // adding edit image fragment
        editImageFragment = new EditImageFragment();
        editImageFragment.setListener(this);
 
        adapter.addFragment(filtersListFragment, getString(R.string.tab_filters));
        adapter.addFragment(editImageFragment, getString(R.string.tab_edit));
 
        viewPager.setAdapter(adapter);
    }
 
    @Override
    public void onFilterSelected(Filter filter) {
        // reset image controls
        resetControls();
 
        // applying the selected filter
        filteredImage = originalImage.copy(Bitmap.Config.ARGB_8888, true);
        // preview filtered image
        imagePreview.setImageBitmap(filter.processFilter(filteredImage));
 
        finalImage = filteredImage.copy(Bitmap.Config.ARGB_8888, true);
    }
 
    @Override
    public void onBrightnessChanged(final int brightness) {
        brightnessFinal = brightness;
        Filter myFilter = new Filter();
        myFilter.addSubFilter(new BrightnessSubFilter(brightness));
        imagePreview.setImageBitmap(myFilter.processFilter(finalImage.copy(Bitmap.Config.ARGB_8888, true)));
    }
 
    @Override
    public void onSaturationChanged(final float saturation) {
        saturationFinal = saturation;
        Filter myFilter = new Filter();
        myFilter.addSubFilter(new SaturationSubfilter(saturation));
        imagePreview.setImageBitmap(myFilter.processFilter(finalImage.copy(Bitmap.Config.ARGB_8888, true)));
    }
 
    @Override
    public void onContrastChanged(final float contrast) {
        contrastFinal = contrast;
        Filter myFilter = new Filter();
        myFilter.addSubFilter(new ContrastSubFilter(contrast));
        imagePreview.setImageBitmap(myFilter.processFilter(finalImage.copy(Bitmap.Config.ARGB_8888, true)));
    }
 
    @Override
    public void onEditStarted() {
 
    }
 
    @Override
    public void onEditCompleted() {
        // once the editing is done i.e seekbar is drag is completed,
        // apply the values on to filtered image
        final Bitmap bitmap = filteredImage.copy(Bitmap.Config.ARGB_8888, true);
 
        Filter myFilter = new Filter();
        myFilter.addSubFilter(new BrightnessSubFilter(brightnessFinal));
        myFilter.addSubFilter(new ContrastSubFilter(contrastFinal));
        myFilter.addSubFilter(new SaturationSubfilter(saturationFinal));
        finalImage = myFilter.processFilter(bitmap);
    }
 
    /**
     * Resets image edit controls to normal when new filter
     * is selected
     */
    private void resetControls() {
        if (editImageFragment != null) {
            editImageFragment.resetControls();
        }
        brightnessFinal = 0;
        saturationFinal = 1.0f;
        contrastFinal = 1.0f;
    }
 
    class ViewPagerAdapter extends FragmentPagerAdapter {
        private final List<Fragment> mFragmentList = new ArrayList<>();
        private final List<String> mFragmentTitleList = new ArrayList<>();
 
        public ViewPagerAdapter(FragmentManager manager) {
            super(manager);
        }
 
        @Override
        public Fragment getItem(int position) {
            return mFragmentList.get(position);
        }
 
        @Override
        public int getCount() {
            return mFragmentList.size();
        }
 
        public void addFragment(Fragment fragment, String title) {
            mFragmentList.add(fragment);
            mFragmentTitleList.add(title);
        }
 
        @Override
        public CharSequence getPageTitle(int position) {
            return mFragmentTitleList.get(position);
        }
    }
 
    // load the default image from assets on app launch
    private void loadImage() {
        originalImage = BitmapUtils.getBitmapFromAssets(this, IMAGE_NAME, 300, 300);
        filteredImage = originalImage.copy(Bitmap.Config.ARGB_8888, true);
        finalImage = originalImage.copy(Bitmap.Config.ARGB_8888, true);
        imagePreview.setImageBitmap(originalImage);
    }
 
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }
 
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
 
        if (id == R.id.action_open) {
            openImageFromGallery();
            return true;
        }
 
        if (id == R.id.action_save) {
            saveImageToGallery();
            return true;
        }
 
        return super.onOptionsItemSelected(item);
    }
 
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (resultCode == RESULT_OK && requestCode == SELECT_GALLERY_IMAGE) {
            Bitmap bitmap = BitmapUtils.getBitmapFromGallery(this, data.getData(), 800, 800);
 
            // clear bitmap memory
            originalImage.recycle();
            finalImage.recycle();
            finalImage.recycle();
 
            originalImage = bitmap.copy(Bitmap.Config.ARGB_8888, true);
            filteredImage = originalImage.copy(Bitmap.Config.ARGB_8888, true);
            finalImage = originalImage.copy(Bitmap.Config.ARGB_8888, true);
            imagePreview.setImageBitmap(originalImage);
            bitmap.recycle();
 
            // render selected image thumbnails
            filtersListFragment.prepareThumbnail(originalImage);
        }
    }
 
    private void openImageFromGallery() {
        Dexter.withActivity(this).withPermissions(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        if (report.areAllPermissionsGranted()) {
                            Intent intent = new Intent(Intent.ACTION_PICK);
                            intent.setType("image/*");
                            startActivityForResult(intent, SELECT_GALLERY_IMAGE);
                        } else {
                            Toast.makeText(getApplicationContext(), "Permissions are not granted!", Toast.LENGTH_SHORT).show();
                        }
                    }
 
                    @Override
                    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
                        token.continuePermissionRequest();
                    }
                }).check();
    }
 
    /*
    * saves image to camera gallery
    * */
    private void saveImageToGallery() {
        Dexter.withActivity(this).withPermissions(Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                .withListener(new MultiplePermissionsListener() {
                    @Override
                    public void onPermissionsChecked(MultiplePermissionsReport report) {
                        if (report.areAllPermissionsGranted()) {
                            final String path = BitmapUtils.insertImage(getContentResolver(), finalImage, System.currentTimeMillis() + "_profile.jpg", null);
                            if (!TextUtils.isEmpty(path)) {
                                Snackbar snackbar = Snackbar
                                        .make(coordinatorLayout, "Image saved to gallery!", Snackbar.LENGTH_LONG)
                                        .setAction("OPEN", new View.OnClickListener() {
                                            @Override
                                            public void onClick(View view) {
                                                openImage(path);
                                            }
                                        });
 
                                snackbar.show();
                            } else {
                                Snackbar snackbar = Snackbar
                                        .make(coordinatorLayout, "Unable to save image!", Snackbar.LENGTH_LONG);
 
                                snackbar.show();
                            }
                        } else {
                            Toast.makeText(getApplicationContext(), "Permissions are not granted!", Toast.LENGTH_SHORT).show();
                        }
                    }
 
                    @Override
                    public void onPermissionRationaleShouldBeShown(List<PermissionRequest> permissions, PermissionToken token) {
                        token.continuePermissionRequest();
                    }
                }).check();
 
    }
 
    // opening image in default image viewer app
    private void openImage(String path) {
        Intent intent = new Intent();
        intent.setAction(Intent.ACTION_VIEW);
        intent.setDataAndType(Uri.parse(path), "image/*");
        startActivity(intent);
    }
}

Chạy ứng dụng và thử nghiệm một lần. Bạn sẽ thấy giao diện đẹp như trong bài báo. Bạn có thể áp dụng các bộ lọc khác nhau từ danh sách và có thể kiểm soát độ sáng, độ bão hòa và độ tương phản.

Source
Source code