Android : Xây dựng bộ lọc hình ảnh như app Instagram
Bài đăng này đã không được cập nhật trong 5 năm
Ngày nay các bộ lọc hình ảnh khá phổ biến trong nhiều ứng dụng Android. Instagram nổi tiếng với tính năng bộ lọc phổ biến và có lẽ là ứng dụng đầu tiên giới thiệu bộ lọc hình ảnh cho thế giới Android. Có rất nhiều ứng dụng chỉnh sửa hình ảnh khác cung cấp các bộ lọc hình ảnh và các tính năng chỉnh sửa hình ảnh.
Trong bài viết này, chúng ta sẽ tìm hiểu cách xây dựng một ứng dụng bộ lọc hình ảnh như Instagram. Chúng ta không phát triển chính xác các bộ lọc hình ảnh trong Instagram mà chúng ta sẽ sử dụng các bộ lọc ma fthuw viện cung cấp.
1. Bộ lọc hình ảnh được xây dựng như thế nào
Thông thường các thao tác xử lý ảnh sẽ được thực hiện bằng ngôn ngữ native C / C ++ bản địa. Trong Android, bạn có thể viết thư viện của mình bằng C hoặc C ++ và sử dụng JNI (Java Native Interface) để làm cho các hàm có thể truy cập thông qua code java. Bạn cũng có thể xem xét sử dụng các thư viện xử lý ảnh phổ biến như openCV để tạo thư viện bộ lọc của riêng bạn.
Chúng tã sẽ sử dụng thư viện bộ lọc hình ảnh hiện có trong bài viết này.
2. Sử dụng thư viện AndroidPhotoFilters
Trong bài viết này, chúng ta sẽ sử dụng thư viện bộ lọc hình ảnh AndroidPhotoFilters được phát triển bởi Zomato. Thư viện này cung cấp các thao tác chỉnh sửa hình ảnh cơ bản như kiểm soát Độ sáng, Độ bão hòa, Độ tương phản và một vài bộ lọc hình ảnh.
Kết hợp tất cả các tính năng này với nhau, bạn có thể tạo ra một app chỉnh sửa hình ảnh cơ bản.
Cũng cần nhớ rằng thư viện rất cơ bản, bạn không thể đạt được các bộ lọc tuyệt vời như Instagram bằng cách này. Để xây dựng bộ lọc chính xác như Instagram, rất nhiều code Native C/C++ phải được thực hiện.
Nhưng chúng ta sẽ cố gắng đạt được các bộ lọc giống như Instagram.
Sử dụng thư viện như sau :
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// ...
implementation 'info.androidhive:imagefilters:1.0.7'
}
3. Xây dựng ứng dụng
- Tạo project mới File ⇒ New Project => Basic Activity
- Sử dụng các thư viện cần thiết sau trong app/build.gradle :
dependencies {
// ...
// image filters
implementation 'info.androidhive:imagefilters:1.0.7'
// butter knife
compile 'com.jakewharton:butterknife:8.8.1'
annotationProcessor 'com.jakewharton:butterknife-compiler:8.8.1'
// dexter M permissions
compile 'com.karumi:dexter:4.1.0'
compile 'com.android.support:recyclerview-v7:26.1.0'
}
- Thêm các resource trong strings.xml, colors.xml, dimens.xml, styles.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>
<?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>
<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>
- Thêm các Permission sau :
<?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>
- Thêm menu res => menu => 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>
- Tạo package uitls => BitmapUtils.java :
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;
/**
* Created by ravi on 06/11/17.
*/
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;
}
}
}
Trong pạkeage utiles , tạo **NonSwipeableViewPager.java **
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*/);
}
}
}
Tạo thêm class SpacesItemDecoration.java :
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;
}
}
}
Xây dựng layout cho item của RecyclerView : res => layout => 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>
ThumbnailsAdapter.java :
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);
}
}
Thêm Image Filters List Fragment như sau :
- Tạp fragment mới : **File ⇒ New ⇒ Fragment ⇒ Fragment (Blank) => FiltersListFragment.java. **
- Mở layout của fragment **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>
FiltersListFragment.java :
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);
}
}
Bây giờ chúng ta sẽ thêm fragment cung cấp thao tác chỉnh sửa hình ảnh cơ bản như kiểm soát Độ sáng, Độ bão hòa, Độ tương phản. Tạo EditImageFragment.java và layout của fragment này là fragment_edit_image.xml :
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>
**EditImageFragment.java **
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();
}
}
Hãy tạo các layout 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>
Quay trở lại với MainActivity.java :
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à kiểm tra một lần. Bạn sẽ thấy giao diện đẹp như trong bài viết. 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.
All rights reserved