android基于gpuimage和photoview的图片编辑(滤镜,饱和度,裁剪)
此博客方便自己使用与他人交流,未经同意不允许他人转载
以前在项目中遇到图片处理的需求,滤镜、饱和度处理和裁剪功能,这里基于gpuimage实现图片滤镜和饱和度的功能,基于photoview图片放大,缩小,移动,实现裁剪功能。demo地址:ImageEdit
以下是两个开源项目的地址有兴趣的同学可以了解一下
android-gpuimage
PhotoView
图片有长图和宽图所以gpuimage的位置大小并不是固定的,得依据图片大小动态调整,同时图片内存分辨率必须做相应处理
1.gpuimage位置处理:以宽度为360dp的屏幕为基准,做gpuimage的缩放
private void setImageVieSize(View view, int width, int height) {
int fixSize = ViewSizeUtil.getCustomDimen(360f);
ViewGroup.LayoutParams layoutParams = view.getLayoutParams();
if (width == height) {
width = height = fixSize;
} else {
if (width > height) {
height = height * fixSize / width;
width = fixSize;
} else {
width = width * fixSize / height;
height = fixSize;
}
}
layoutParams.height = height;
layoutParams.width = width;
}
2.图片分辨率处理:以2048×2048为标准,(imageloader里面默认能支持的最大分辨率,图片太大加载不出来就会提示超过此分辨率,这个分辨率的选择得看手机和内存使用情况,有可能出现oom,imageloader里面是以屏幕分辨率或者控件的大小对图片进行压缩,但是我们这里原则上是尽可能让图片清晰),如果以2的倍数来压,你会发现图片会变的很模糊,所以压缩后的分辨率尽可能保证接近2048×2048,并且对图片进行lrucache内存管理,优化总结,目前对图片分辨率的处理:
public static Bitmap getSuitBitmap(String filePath) {
Bitmap bitmap;
int degree = ImageUtil.readPictureDegree(uploadFilePath);//对图片做旋转处理
if (uploadFilePath.startsWith("file://")) {
uploadFilePath = uploadFilePath.substring(7);
}
bitmap = decodeBitmap(uploadFilePath);
if (degree != 0) {
bitmap = ImageUtil.rotateBitmap(bitmap, degree);
}
return bitmap;
}
public static Bitmap decodeBitmap(String pathName) {
Bitmap bitmap = null;
try {
BitmapFactory.Options options = getBitmapOptions();
BitmapFactory.decodeFile(pathName, options);
getAfterBitmap(options);
String key = getKey(String.valueOf(options.outWidth), String.valueOf(options.outHeight), pathName);
bitmap = getMemoryCacheBitmap(key);
if (bitmap == null || bitmap.isRecycled()) {
bitmap = BitmapFactory.decodeFile(pathName, options);
putInMemoryCache(key, bitmap);
}
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
return bitmap;
}
public static void getAfterBitmap(BitmapFactory.Options options) {
float widthRate = options.outWidth * 1.0f / 2048 * 1.0f;
float heightRate = options.outHeight * 1.0f / 2048 * 1.0f;
options.inSampleSize = 1;
if (widthRate * heightRate > 1) {
options.inSampleSize = (int) (widthRate * heightRate) + 1;
}
options.inJustDecodeBounds = false;
}
private static String getKey(String width, String height, String path) {
return path + "_" + "width" + width + "_" + "height" + height;
}
效果图如下:
这个功能就是gpuimage的api调用而已,看完simple发现图片的处理大概就是以下几步:
GPUImageFilter filter = GPUImageFilterTools.createFilterForType(AppContext.context(), GPUImageFilterTools.FilterType.SATURATION);//.获取对应的GPUImageFilter
mFilterAdjuster = new GPUImageFilterTools.FilterAdjuster(filter);//.根据fliter获取FilterAdjuster
mGPUImageView.setImage(bitmap);//设置图片
mGPUImageView.setFilter(filter);//设置filter
mFilterAdjuster.adjust(progress);//设置进度
mGPUImageView.requestRender();//刷新view
这个自定义的进度条不准备详细赘述,后面会专门花点时间写篇博客讲这些年写过的自定义控件(先挖个坑)
效果图如下:
首先得根据原图生成各种滤镜效果,管理里面还有滤镜筛选和排序(这个排序功能第三方的很多,这里不做叙述),同时还要根据用户本地保存顺序和类型,处理内存问题。
首先增加滤镜数据,获取FilterType,GPUImageFilterTools类中定义了FilterType表示各种滤镜,选取了一部分滤镜,为了方便数据处理把enum,变成string
public static final String I_1977 = "48";//创新
public static final String I_AMARO = "49";//流年
public static final String I_BRANNAN = "50";//淡雅
public static final String I_EARLYBIRD = "51";//怡尚
public static final String I_HEFE = "52";//优格
public static final String I_HUDSON = "53";//胶片
public static final String I_INKWELL = "54";//黑白
public static final String I_LOMO = "55";//个性
public static final String I_LORDKELVIN = "56";//回忆
public static final String I_NASHVILLE = "57";//复古
public static final String I_RISE = "58";//森系
public static final String I_SIERRA = "59";//清新
public static final String I_SUTRO = "60";//摩登
public static final String I_TOASTER = "61";//绚丽
public static final String I_VALENCIA = "62";//优雅
public static final String I_WALDEN = "63";//日系
public static final String I_XPROII = "64";//新潮
public static final String DEFAULT = "65";//原图
private List filters = new ArrayList();
private void initImageFilterData() {
String imageFilters = SharedPreferenceHelper.getInstance().getImageFilters();//判断本地是否有滤镜
if (imageFilters.isEmpty()) {
StringBuffer stringBuffer = new StringBuffer();
int start = Integer.valueOf(GPUImageFilterTools.FilterType.I_1977);
int end = Integer.valueOf(GPUImageFilterTools.FilterType.I_HEFE);
for (int i = start; i < end; i++) {
filters.add(String.valueOf(i));
stringBuffer.append(filters.get(i - start));
stringBuffer.append(",");
}
if (stringBuffer.length() > 0) {
stringBuffer.deleteCharAt(stringBuffer.length() - 1);
}
SharedPreferenceHelper.getInstance().putImageFilters(stringBuffer.toString());
} else {
String[] split = imageFilters.split(",");
for (int i = 0; i < split.length; i++) {
if (!split[i].isEmpty()) {
filters.add(split[i]);
}
}
}
filters.add(0, GPUImageFilterTools.FilterType.DEFAULT);
}
//然后根据type获取filter,mList就是上文中的filters
public void addFilters() {
this.gpuImageFilters = new ArrayList<>();
for (int i = 0; i < mList.size(); i++) {
gpuImageFilters.add(GPUImageFilterTools.createFilterForType(AppContext.context(), mList.get(i)));
}
}
public static GPUImageFilter createFilterForType(final Context context, final String type) {
switch (type) {
case FilterType.DEFAULT:
return new GPUImageFilter();
case FilterType.CONTRAST:
return new GPUImageContrastFilter(2.0f);
case FilterType.GAMMA:
return new GPUImageGammaFilter(2.0f);
case FilterType.INVERT:
return new GPUImageColorInvertFilter();
case FilterType.PIXELATION:
return new GPUImagePixelationFilter();
case FilterType.HUE:
return new GPUImageHueFilter(90.0f);
............
default:
LogUtils.i("filter",type);
throw new IllegalStateException("No filter of that type!");
}
当选择滤镜时,同时更新原图的滤镜,这里绘制时会造成卡顿,所以得开个线程:
//处理滤镜选择和管理滤镜功能
onImageFilterSelectUpdateRecyclerListener.setOnImageFilterClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
int position = (int) v.getTag();
if (position == onImageFilterSelectUpdateRecyclerListener.getCount() - 1) {
ImageFilterManagerActivity.startActivityForResult(PostImageEditActivity.this, pImage.url, Constants.REQUEST_CODE_1001);
} else {
if (lastFilterPosition != position) {
currentFilter = onImageFilterSelectUpdateRecyclerListener.getGpuImageFilters().get(position);
setFilterBitmap(executorService, PostImageEditActivity.this, currentFilter, PostImageEditActivity.originalBitmap, mGPUImageView, position);
}
}
}
});
//更新原图滤镜
public void setFilterBitmap(ExecutorService executorService, final BaseActivity baseActivity, GPUImageFilter filter, final Bitmap bitmap, final ImageView imageView, final int tempPosition) {
baseActivity.showWaitDialog();
GPUImage.getBitmapForFilter(executorService, bitmap, filter, new GPUImage.ResponseListener() {
@Override
public void response(final Bitmap item) {
ImageEditApplication.getInstance().handler.post(new Runnable() {
@Override
public void run() {
baseActivity.hideWaitDialog();
imageView.setImageBitmap(item);
currentBitmap = item;
lastFilterPosition = tempPosition;
}
});
}
});
}
//增加线程 GPUImage类中
public static void getBitmapForFilter(ExecutorService executorService, final Bitmap bitmap, final GPUImageFilter filter, final ResponseListener listener) {
executorService.execute(new Runnable() {
@Override
public void run() {
GPUImageRenderer renderer = new GPUImageRenderer(filter);
renderer.setImageBitmap(bitmap, false);
PixelBuffer buffer = new PixelBuffer(bitmap.getWidth(), bitmap.getHeight());
buffer.setRenderer(renderer);
renderer.setFilter(filter);
if (listener != null) {
listener.response(buffer.getBitmap());
}
filter.destroy();
renderer.deleteImage();
buffer.destroy();
}
});
}
//recyclerView中内存处理
if (position == getCount() - 1) {
imageView.setTag(null);
name = R.string.timeline_manage;
imageView.setImageResource(R.drawable.icon_image_filter);
holder.setBackgroundRes(R.id.item_filter_image_container, R.drawable.rectangle_image_filter_setting);
imageLayoutParams.height = imageLayoutParams.width = ViewSizeUtil.getCustomDimen(29f);
} else {
imageLayoutParams.height = imageLayoutParams.width = ViewSizeUtil.getCustomDimen(94f);
name = context.getResources().getIdentifier("text_filter_" + mList.get(position), "string", AppContext.getContext().getPackageName());
holder.setBackgroundRes(R.id.item_filter_image_container, R.color.white);
Bitmap bitmap = mMCache.get(mList.get(position) + path);//利用imageloader的内存管理
imageView.setTag(mList.get(position) + path);
if (bitmap != null && !bitmap.isRecycled()) {
imageView.setImageBitmap(bitmap);
} else {
final GPUImageFilter filter = gpuImageFilters.get(position);
ImageFilterHandler.setFilterBitmap(executorService, filter, mList.get(position) + path, this.bitmap, imageView);//获取滤镜后的图片,并进行内存管理
}
}
// ImageFilterHandler类中
public static void setFilterBitmap(ExecutorService executorService, GPUImageFilter filter, final String key, Bitmap bitmap, final ImageView imageView) {
GPUImage.getBitmapForFilter(executorService, bitmap,filter, new GPUImage.ResponseListener() {
@Override
public void response(final Bitmap item) {
MemoryCache mMCache = ImageLoader.getInstance().getMemoryCache();
mMCache.put(key,item);
String tag = (String) imageView.getTag();
if (tag.equals(key)) {
AppContext.context().handler.post(new Runnable() {
@Override
public void run() {
imageView.setImageBitmap(item);
}
});
}
}
});
}
横图
1.横图初始时图片高和剪裁区域等高,可以左右滑动,不能上下滑动,
2.点击缩放按钮,图片宽和剪裁区域等宽,不能滑动,
3.双击能进行2次放大,超过裁剪区域能进行移动
长图
1.初始等宽,超过裁剪区域能移动
2.点击缩放按钮,图片高和剪裁区域等高,不能滑动
3.双击能进行2次放大,超过裁剪区域能进行移动
photoview已经把图片放大,缩小,移动的功能做的很强大了,我们只需要加个剪裁区域盖在上面,同时限制移动的区域,往下移动时图片顶部最低和裁剪区域顶部等高不得低于裁剪区域顶部,往上移动时图片底部和和裁剪区域底部登高不得高于裁剪区域底部,左右移动时规则一样(仿instagram),没办法photoview源码还得走起一波
1.得知道裁剪区域上下左右的位置
2.得知道photoview移动的方法
先解决第一个问题:除掉取消,还原,完成剩下的区域都是图片可以显示的区域,裁剪区域在剩下的空间居中:
private int setCustomBounds() {
//image_container是个相对布局,里面的view就是裁剪区域和imageview,dp_360以360dp屏幕为标准适配的
private int setCustomBounds() {
int deltHeight = (image_container.getHeight() - dp_360) / 2;
RectF rectF = new RectF();
rectF.left = 0;
rectF.right = dp_360;
rectF.top = deltHeight;
rectF.bottom = rectF.top + dp_360;
mAttacher.setCustomBounds(rectF);
return deltHeight;
}
//设置裁剪区域上边界和下边界的两条线
private void setMask(int deltHeight) {
View bottom = getView(R.id.clip_bounds_view_below);
bottom.setBackgroundResource(R.color.black_overlay);
View above = getView(R.id.clip_bounds_view_above);
above.setBackgroundResource(R.color.black_overlay);
above.getLayoutParams().height = bottom.getLayoutParams().height = deltHeight;
}
//photoview矩阵初始化时会调用setOnMatrixChangeListener,我们必须根据是长图还是宽图把图片进行缩放,达到我们之前说的初始状态,初始化完成之后这段逻辑就不需要走了,因此给imageview加个tag做标记,baseScaleRate记录初始化时的缩放倍率,方便还愿功能用到
imageView.setTag(true);
mAttacher.setOnMatrixChangeListener(new PhotoViewAttacher.OnMatrixChangedListener() {
@Override
public void onMatrixChanged(RectF rect) {//Rectf为当前显示区域
Object tag = imageView.getTag();
if (tag != null) {
boolean flag = (boolean) tag;
baseRectF = new RectF(rect);
if (bitmap.getHeight() >= bitmap.getWidth()) {
if (flag) {
float bitmapRate = bitmap.getHeight() * 1.0f / bitmap.getWidth() * 1.0f;
float viewRate = image_container.getHeight() * 1.0f / image_container.getWidth() * 1.0f;
//图片是否超过imageview的控件区域,因为imageview是充满剩余空间的,超过就需要缩放,未超过不需要
if (bitmapRate > viewRate) {
imageView.setTag(false);
baseScaleRate = dp_360 * 1.0f / rect.width() * 1.0f;
if (baseScaleRate > 3.0f) {
mAttacher.setMaximumScale(baseScaleRate + 2);
}
mAttacher.setScale(baseScaleRate);
} else {
imageView.setTag(null);
}
} else {
imageView.setTag(null);
}
float height = image_container.getHeight() * 1.0f > baseRectF.height() ? baseRectF.height() : image_container.getHeight() * 1.0f;
// mAttacher.setCustomMinScale(dp_360 * 1.0f / height);
mAttacher.setMinimumScale(dp_360 * 1.0f / height);
} else {
if (flag) {
imageView.setTag(false);
baseScaleRate = dp_360 * 1.0f / rect.height() * 1.0f;
if (baseScaleRate > 3.0f) {
mAttacher.setMaximumScale(baseScaleRate + 2);
}
mAttacher.setScale(baseScaleRate);
mAttacher.setMinimumScale(baseRectF.height() * 1.0f / dp_360 * 1.0f);//设置最小缩放比率
} else {
imageView.setTag(null);
}
}
}
}
});
//缩放按钮逻辑就是设置缩放倍率而已
setOnClickListener(R.id.photo_full_view, new View.OnClickListener() {
@Override
public void onClick(View v) {
if (bitmap.getHeight() >= bitmap.getWidth()) {
if (mAttacher.getScale() < baseScaleRate) {
mAttacher.setScale(baseScaleRate);
} else {
float height = image_container.getHeight() * 1.0f > baseRectF.height() ? baseRectF.height() : image_container.getHeight() * 1.0f;
mAttacher.setScale(dp_360 * 1.0f / height);
}
} else {
if (mAttacher.getScale() < baseScaleRate) {
mAttacher.setScale(baseScaleRate);
} else {
mAttacher.setScale(baseRectF.height() * 1.0f / dp_360 * 1.0f);
}
}
}
});
//ClipBoundsView宽高和屏幕等宽里面就画了4条线,边界的线放在外面还好处理一点,里面测量写死了,
限制photoview移动区域
//设置photoview矩阵移动边界
private int setCustomBounds() {
int deltHeight = (image_container.getHeight() - dp_360) / 2;
RectF rectF = new RectF();
rectF.left = 0;
rectF.right = dp_360;
rectF.top = deltHeight;
rectF.bottom = rectF.top + dp_360;
mAttacher.setCustomBounds(rectF);//增加我们的限制条件方法
return deltHeight;
}
//接下来设置photoview移动范围,在PhotoViewAttacher类中有fling方法处理滑动逻辑的
public void fling(int viewWidth, int viewHeight, int velocityX,
int velocityY) {
final RectF rect = getDisplayRect();
if (null == rect) {
return;
}
final int startX = Math.round(-rect.left);
final int minX, maxX, minY, maxY;
if (viewWidth < rect.width()) {
minX = 0;
maxX = Math.round(rect.width() - viewWidth);
} else {
minX = maxX = startX;
}
final int startY = Math.round(-rect.top);
if (customBounds != null) {
//此处加上我们的限制条件,scroller移动向上为正,向下为负,minY就是向下移动的距离,maxy就是向上移动的距离,向下能滚到裁剪区域的顶部就是在原来的基础上向下滑动 -(int) customBounds.top ,customBounds == null的情况就是原来的代码,向上在原来的基础上加上 customBounds.top,这样fling移动的过程搞定(实在不好理解就打断点,看看图片移动的过程就理解了)
if (customBounds.height() < rect.height()) {
minY = -(int) customBounds.top;//
maxY = (int) (Math.round(rect.height() - viewHeight) + customBounds.top);//
} else {
minY = maxY = startY;
}
} else {
if (viewHeight < rect.height()) {
minY = 0;
maxY = Math.round(rect.height() - viewHeight);
} else {
minY = maxY = startY;
}
}
mCurrentX = startX;
mCurrentY = startY;
if (DEBUG) {
LogManager.getLogger().d(
LOG_TAG,
"fling. StartX:" + startX + " StartY:" + startY
+ " MaxX:" + maxX + " MaxY:" + maxY);
}
// If we actually can move, fling the scroller
if (startX != maxX || startY != maxY) {
mScroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
}
}
//还有一个处理的地方是双手缩放的时候,必须设置矩阵的边界,ctr+F12查找方法找到checkMatrixBounds()这个就是处理矩阵边界的方法,依据源码的逻辑(customBounds == null的情况下的处理)依葫芦画瓢增加相应的逻辑
private boolean checkMatrixBounds() {
final ImageView imageView = getImageView();
if (null == imageView) {
return false;
}
final RectF rect = getDisplayRect(getDrawMatrix());
if (null == rect) {
return false;
}
final float height = rect.height(), width = rect.width();
float deltaX = 0, deltaY = 0;
final int viewHeight = getImageViewHeight(imageView);
if (customBounds != null) {
if (height <= customBounds.height()) {
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top;
break;
case FIT_END:
deltaY = viewHeight - height - rect.top;
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top;
break;
}
} else if (rect.top > customBounds.top) {
deltaY = -rect.top + customBounds.top;
} else if (rect.bottom < customBounds.bottom) {
deltaY = customBounds.bottom - rect.bottom;
}
final int viewWidth = getImageViewWidth(imageView);
if (width <= customBounds.width()) {
switch (mScaleType) {
case FIT_START:
deltaX = -rect.left;
break;
case FIT_END:
deltaX = viewWidth - width - rect.left;
break;
default:
deltaX = (viewWidth - width) / 2 - rect.left;
break;
}
mScrollEdge = EDGE_BOTH;
} else if (rect.left > customBounds.left) {
mScrollEdge = EDGE_LEFT;
deltaX = -rect.left + customBounds.left;
} else if (rect.right < customBounds.right) {
deltaX = customBounds.right - rect.right;
mScrollEdge = EDGE_RIGHT;
} else {
mScrollEdge = EDGE_NONE;
}
} else {
if (height <= viewHeight) {
switch (mScaleType) {
case FIT_START:
deltaY = -rect.top;
break;
case FIT_END:
deltaY = viewHeight - height - rect.top;
break;
default:
deltaY = (viewHeight - height) / 2 - rect.top;
break;
}
} else if (rect.top > 0) {
deltaY = -rect.top;
} else if (rect.bottom < viewHeight) {
deltaY = viewHeight - rect.bottom;
}
final int viewWidth = getImageViewWidth(imageView);
if (width <= viewWidth) {
switch (mScaleType) {
case FIT_START:
deltaX = -rect.left;
break;
case FIT_END:
deltaX = viewWidth - width - rect.left;
break;
default:
deltaX = (viewWidth - width) / 2 - rect.left;
break;
}
mScrollEdge = EDGE_BOTH;
} else if (rect.left > 0) {
mScrollEdge = EDGE_LEFT;
deltaX = -rect.left;
} else if (rect.right < viewWidth) {
deltaX = viewWidth - rect.right;
mScrollEdge = EDGE_RIGHT;
} else {
mScrollEdge = EDGE_NONE;
}
}
// Finally actually translate the matrix
mSuppMatrix.postTranslate(deltaX, deltaY);
return true;
}
图片裁剪保存
//思路,bitmap剪切问题不大,调用Bitmap.createBitmap方法,需要知道起始点位置,宽,高,只要计算出图片没有缩放的情况下,x,y的偏移,同时加上裁剪区域的位置。矩阵的资料时间隔的太久,忘了当时找了哪些资料了这篇不错android matrix 最全方法详解与进阶
private Bitmap clip() {
RectF mClipBorderRectF = getClipBorder();
final Drawable drawable = imageView.getDrawable();
final float[] matrixValues = new float[9];
Matrix displayMatrix = mAttacher.getDrawMatrix();
displayMatrix.getValues(matrixValues);
final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / bitmap.getWidth();
final float transX = matrixValues[Matrix.MTRANS_X];
final float transY = matrixValues[Matrix.MTRANS_Y];
final float cropX = (-transX + mClipBorderRectF.left) / scale;
final float cropY = (-transY + mClipBorderRectF.top) / scale;
final float cropWidth = mClipBorderRectF.width() / scale;
final float cropHeight = mClipBorderRectF.height() / scale;
return Bitmap.createBitmap(bitmap, (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight, null, false);
}
private RectF getClipBorder() {
//获取裁剪区域在image_container中的相对父控件位置
RectF mClipBorderViewRectF = new RectF(ViewSizeUtil.getViewRectInParent(clip_bounds_view, image_container));
//获取图片相对父控件的位置
RectF displayRect = mAttacher.getDisplayRect();
if (bitmap.getHeight() >= bitmap.getWidth()) {
//mAttacher.getScale() = baseScaleRate就是和裁剪区域等宽的情况
if (mAttacher.getScale() < baseScaleRate) {//如果是缩小的状态,图片的区域小于裁剪区域的,就是类似长图2的情况
mClipBorderViewRectF.left = displayRect.left;
mClipBorderViewRectF.right = displayRect.right;
}
} else {
//mAttacher.getScale() = baseScaleRate就是和裁剪区域等高的情况
if (mAttacher.getScale() < baseScaleRate) {//类似宽图1的情况
mClipBorderViewRectF.top = displayRect.top;
mClipBorderViewRectF.bottom = displayRect.bottom;
}
}
return mClipBorderViewRectF;
}
主要的难点差不多就是这些,最后附上demo地址:ImageEdit
此博客方便自己使用与他人交流,未经同意不允许他人转载