本文 主要写一个本人没事的时候写的一款防微信多图选择器 实现了包括微信的样式 剪切 涂鸦能功能
代码纯原生实现,将会介绍基础的图片压缩 基础的自定义view的处理方法 以及一些动画和矩阵处理相关
1. 系统图库的筛选
首先 初始化一个线程
mService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
读取图库 并且按照日期 从近到远排序
Cursor cursor = getContentResolver().
query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media.DATA,
MediaStore.Images.Media.DATE_ADDED},
null, null,
MediaStore.Images.Media.DATE_ADDED + " desc");
mAllDirs.add(new Folder("", null, null));
while (cursor.moveToNext()) {
int date = cursor.getInt(1);
String path = cursor.getString(0);
if (!Utils.isPicture(path)) {
continue;
}
ImageInfo info = new ImageInfo(date, path);
mAllPictures.add(info);
String dir = new File(path).getParentFile().getAbsolutePath();
Folder f = getFolder(dir);
if (f == null) {
List<ImageInfo> folderImage = new ArrayList<>();
folderImage.add(info);
Folder folder = new Folder(dir, info, folderImage);
mAllDirs.add(folder);
} else {
f.getImageInfos().add(info);
}
}
cursor.close();
将得到的路径 保存 并且获取其父目录 保存为目录(后续需要按照目录显示 ) 即 一个目录对应多个图片,因为需要一个所有目录 我们将在数组中添加一个空目录
2.将得到的图片压缩
按照微信 每行显示4张图片 ,这是我们的目标尺寸,因为压缩比较耗时 ,我们需要使用刚才创建的线程池。为此 我们创一个任务 用于显示图片的压缩 (下次任务 后续 也将用到),gif 图 使用movie 实现
@Override
public void run() {
Bitmap bitmap = mCache.get(mPath);
boolean isSize = false;
if (bitmap != null) {
int widthDif = bitmap.getWidth() / mWidth;
int heightDif = bitmap.getHeight() / mHeight;
//如果 宽高的相差在0.5倍 到2倍之间 我们就认为 不需要再次压缩
isSize = widthDif > 0.5f && widthDif < 2f || heightDif > 0.5f && heightDif < 2f;
}
if (bitmap == null || !isSize) {
bitmap = Utils.compress(mPath, mWidth, mHeight);
mCache.put(mPath, bitmap);
}
final Bitmap finalBitmap = bitmap;
mHost.runOnUiThread(new Runnable() {
@Override
public void run() {
mTarget.setImageBitmap(finalBitmap);
mTarget.setScaleX(1);
}
});
}
关于压缩方法 此处为根据目标图片的宽高进行采样
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(path, options);
int sample = Math.max(options.outWidth / width, options.outHeight / height);
options.inSampleSize = sample;
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFile(path, options);
至此 所有的 图片已经显示在列表中
3.目录的选择与隐藏
在初始化图片的时候 已经将目录列表初始化 并且 隐藏
mDirList.setLayoutManager(new LinearLayoutManager(MultiPicturesSelectorActivity.this));
mDirList.setAdapter(new DirsAdapter());
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mDirList.getLayoutParams();
int mTranslateSize = mContainer.getHeight() * 7 / 8;
params.height = mTranslateSize;
mDirList.setLayoutParams(params);
mDirList.setTranslationY(mTranslateSize);
如上 将目录高度出示为 屏幕高度的7/8 然后将其向下偏移 来达到隐藏的效果 当用户点击查看目录 的时候 将其显示 或者隐藏
private void animEnter() {
mShadow.setVisibility(View.VISIBLE);
mDirList.animate().translationYBy(-mDirList.getHeight()).setDuration(300).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
}
}).start();
}
private void animHide() {
mDirList.animate().translationYBy(mDirList.getHeight()).setDuration(300).setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mShadow.setVisibility(View.GONE);
}
}).start();
}
上述中 阴影 为 遮盖下面的图片的一个半透明view
4 图片的预览
点击图片将会跳转到预览界面 首先拿到路径 并且将其放入viewpage中 开始进入默认全屏 当用户单击图片 显示状态栏
int statusBarHeight = -1;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
//根据资源ID获取响应的尺寸值
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) mPreviewVp.getLayoutParams();
if (isShow)
params.topMargin = -statusBarHeight;
else
params.topMargin = 0;
mPreviewVp.setLayoutParams(params);
解释下 需要实现一个-的margin 否则 进入全屏状态 将会因为 上边距 而出现 左右的间隙
private class HandleSingleTap implements ScaleImageView.OnGestureListener {
@Override
public void onSingleTapUp() {
if (isShow) {
isShow = false;
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
mTopBar.animate().translationYBy(-mTopBar.getHeight()).setDuration(200).start();
//mBottomBar.animate().alpha(0).setDuration(200).start();
mBottomBar.setAlpha(0);
mBottomBar.setVisibility(View.GONE);
} else {
isShow = true;
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
mTopBar.animate().translationYBy(mTopBar.getHeight()).setDuration(200).start();
mBottomBar.setVisibility(View.VISIBLE);
mBottomBar.animate().alpha(0.8f).setDuration(200).start();
}
initTop();
}
}
同时控制下边距的透明状态 此单击事件 从自定义的imageView 传入
5.缩放ImageView
1)实现基础缩放功能
基础配置请参考代码 哈 关于 缩放 我们主要介绍的是手势实现
首先初始化图片的初始位置 (需要在onMeasure中获取宽高)
private void init() {
if (isInitScale) return;
Drawable d = getDrawable();
if (d == null) return;
int width = getMeasuredWidth();
int height = getMeasuredHeight();
int dw = d.getIntrinsicWidth();
int dh = d.getIntrinsicHeight();
Log.e("main", "dw " + dw + "dh " + dh);
// if (height > dh && width > dw) {
// float scaleW = (float) width / (float) dw;
// float scaleH = (float) height / (float) dh;
// mInitScale = Math.min(scaleH, scaleW);
// }
float scaleW = (float) width / (float) dw;
float scaleH = (float) height / (float) dh;
if (dh > height && dw > width) {
}
mInitScale = Math.min(scaleH, scaleW);
mMaxScale = mInitScale * 4;
mCenterScale = mInitScale * 2;
matrix.postTranslate((width - dw) / 2, (height - dh) / 2);
matrix.postScale(mInitScale, mInitScale, width / 2, height / 2);
setImageMatrix(matrix);
isInitScale = true;
mScaleFocus[0] = width / 2;
mScaleFocus[1] = height / 2;
}
解释下 首先 拿到图片本身的宽高 和控件的宽高 做比较 然后 移动 并且缩放到 图片的中间 然后 初始化一个缩放中心 和双击的 宽高 和最大宽高
2)手势控制
mGestureDetector = new GestureDetector(context, new TapCallback());
mScaleGestureDetector = new ScaleGestureDetector(context, new ScaleCallback());
@Override
public boolean onTouchEvent(MotionEvent event) {
mGestureDetector.onTouchEvent(event);
mScaleGestureDetector.onTouchEvent(event);
if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) {
if (checkScrollBorder) {
checkBorder();
checkScrollBorder = false;
}
}
return true;
}
如上 初始化 手势工具类 在缩放的 过程中 如果 我们将要缩放的scale 小于原来的宽高的一半 直接跳过 否则 将图片的matrix 缩放 并设置
public boolean onScale(ScaleGestureDetector detector) {
float factor = detector.getScaleFactor();
if (getCurScale() * factor < 0.5f * mInitScale) {
return true;
}
if (!isScale)
isScale = true;
mScaleFocus[0] = detector.getFocusX();
mScaleFocus[1] = detector.getFocusY();
matrix.postScale(factor, factor, detector.getFocusX(), detector.getFocusY());
setImageMatrix(matrix);
return true;
}
然后是缩放结束
@Override
public void onScaleEnd(ScaleGestureDetector detector) {
isScale = false;
super.onScaleEnd(detector);
if (getCurScale() > mMaxScale) {
slowScale(mMaxScale);
}
if (getCurScale() < mInitScale) {
resetScale();
}
}
如果 当前的scale 大于最大的scale 将会 重置为最大 如果小于 我的初始scale 将会重置为初始scale
这里介绍下慢动作缩放
ValueAnimator animator = ValueAnimator.ofFloat(getCurScale(), target);
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float curTarget = (float) animation.getAnimatedValue();
float factor = curTarget / getCurScale();
matrix.postScale(factor, factor, mScaleFocus[0], mScaleFocus[1]);
setImageMatrix(matrix);
if (animation.getAnimatedFraction() == 1) {
checkBorder();
if (mScaleEndListener != null) mScaleEndListener.onScaleEnd();
mScaleEndListener = null;
}
}
});
animator.setDuration(200).start();
如上 传入一个目标的scale 先初始化一个valueAnimator 在动画执行的过程中 一直设置缩放 来达到一个渐进的缩放
接下来是双击
@Override
public boolean onDoubleTap(MotionEvent e) {
float curScale = getCurScale();
if (curScale < mCenterScale) {
mScaleFocus[0] = e.getX();
mScaleFocus[1] = e.getY();
slowScale(mCenterScale);
} else {
resetScale();
}
return true;
}
同样的调用slowScale方法
点击回调
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
if (mGestureListener != null) {
mGestureListener.onSingleTapUp();
}
return true;
}
滑动
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
if (Math.abs(distanceX) < mTouchSlop && Math.abs(distanceY) < mTouchSlop) {
return true;
}
if (isScale) return true;//滑动的时候 如果在缩放 则 无操作
ScaleImageView.this.onScroll(distanceX, distanceY);
return false;
}
protected void onScroll(float distanceX, float distanceY) {
checkIntercept(distanceX);
if (getCurScale() == mInitScale) {
distanceY = 0;
}
float[] target = checkScroll(distanceX, distanceY);
matrix.postTranslate(-target[0], -target[1]);
setImageMatrix(matrix);
checkScrollBorder = true;
}
首先 我们看下checkIntercept 这个方法 是用来 阻止viewPage 拦截我的滑动的 因为 我可能处于放大的情况 此时 我需要 滑动我自己
private void checkIntercept(float dx) {
RectF rectF = getMatrixRectF();
float width = getWidth();
float height = getHeight();
if (rectF.height() > height || rectF.width() >= width) {
if (rectF.right >= width && dx > 0) {
getParent().requestDisallowInterceptTouchEvent(true);
} else if (rectF.left <= 0 && dx < 0) {
getParent().requestDisallowInterceptTouchEvent(true);
} else {
getParent().requestDisallowInterceptTouchEvent(false);
}
}
}
如上 首先 拿到 图片的矩形 (此矩阵为当前图片的内容范围 讲matrix 映射到一个rectF 即可)如果还没有滑过我显示的位置 则不允许父类拦截 。细心的你可能发现 还有个checkScroll 方法 我们来看下 这个方法
protected float[] checkScroll(float distanceX, float distanceY) {
RectF rectF = getMatrixRectF();
float width = getWidth();
float height = getHeight();
float damp = 4;
if (rectF.width() >= width) {
if (rectF.left > 0) {
distanceX = distanceX / damp;
}
if (rectF.right < width) {
distanceX = distanceX / damp;
}
}
if (rectF.height() >= height) {
if (rectF.top > 0) {
distanceY = distanceY / damp;
}
if (rectF.bottom < height) {
distanceY = distanceY / damp;
}
}
if (rectF.width() < width) {
distanceX = distanceX / damp;
}
if (rectF.height() < height) {
distanceY = distanceY / damp;
}
float[] result = new float[2];
result[0] = distanceX;
result[1] = distanceY;
return result;
}
这个是个阻尼 当滑动的位置 已经 脱离了 图片的边界 则给他一个阻尼
最后就是边界检测了 我们开看下 这个方法
protected void checkBorder() {
RectF rectF = getMatrixRectF();
// Log.e("main", rectF.toString());
// Log.e("main", "width " + rectF.width());
float dx = 0;
float dy = 0;
float width = getWidth();
float height = getHeight();
if (rectF.width() >= width) {
if (rectF.left > 0) {
dx = -rectF.left;
}
if (rectF.right < width) {
dx = width - rectF.right;
}
}
if (rectF.height() >= height) {
if (rectF.top > 0) {
dy = -rectF.top;
}
if (rectF.bottom < height) {
dy = height - rectF.bottom;
}
}
if (rectF.width() < width) {
dx = width / 2f + rectF.width() / 2f - rectF.right;
}
if (rectF.height() < height) {
dy = height / 2f + rectF.height() / 2f - rectF.bottom;
}
matrix.postTranslate(dx, dy);
setImageMatrix(matrix);
//slowTranslate(dx,dy);
}
如上 拿到当前的图片范围 与图片宽高做比较 这里 画了一张当图片小于空间的宽高 的时候的范围 希望能帮助理解
3)增加对长图的支持
主要支持为长图截屏 ,主要使用 BitmapRegionDecoder 类 一次解析一个矩形的范围,主要代码 为
mImageRect.offset((int) distanceX, (int) distanceY);
checkSelf();
setImageBitmap(mDecoder.decodeRegion(mImageRect, mOptions));
如上 此类 继承自ScaleImageView 主要在onScroll的时候 移动矩形 并且不断的设置。
至此 图片缩放分析完毕,当然 各种很多细节 还需要仔细参考代码。
6 、图片剪切
1)绘制剪切范围
如图 最我们应该以图片的范围 为基准 这是图片的初始 rect 那么 我们需要绘制的矩形应该为left=范围.left+线宽/2,其它边界也是一样的方法 算出来
然后是边角 边角为矩形的范围 偏移线宽的一半 (参考代码)
然后是4条横线的绘制
private void drawProfile(Canvas canvas) {
mPaint.setStrokeWidth(mProfileWidth);
//移动四角线宽
float offset = mProfileWidth / 2;
float left = mLeft - offset;
float top = mTop - offset;
float right = mRight + offset;
float bottom = mBottom + offset;
mProfile = new RectF(left, top, right, bottom);
canvas.drawRect(mProfile, mPaint);
}
如上 先是 绘制矩形
Path path = new Path();
int offset = mProfileWidth + mCornerWidth / 2;
canvas.drawPath(path, mPaint);
float left = mLeft - offset;
float top = mTop - offset;
float right = mRight + offset;
float bottom = mBottom + offset;
path.moveTo(left, top + mCornerSize);
path.lineTo(left, top);
path.lineTo(left + mCornerSize, top);
canvas.drawPath(path, mPaint);
然后是绘制一条边框 其它3个边框以此类推
将会得到如上图所示的矩形框(当前 已经对 图片进行了0.8f的缩放 )
当然 还有矩形的最小边界 和最大边界的控制 都是放在滑动中检测的限于篇幅限制,这里点到为止
首先带down 事件 确认我是不是应该移动我的矩形 ,即按下的位置落在我的四个边框
private boolean shouldMove(MotionEvent e) {
float x = e.getX();
float y = e.getY();
if (x > mProfile.right || x < mProfile.left || y > mProfile.bottom || y < mProfile.top) {
//在矩形的外面
return false;
}
if (x < mProfile.left + mCornerSize && y < mProfile.top + mCornerSize) {
mCurScrollRange = LEFT_TOP;
return true;
}
if (x > mProfile.right - mCornerSize && y < mProfile.top + mCornerSize) {
mCurScrollRange = RIGHT_TOP;
return true;
}
if (x < mProfile.left + mCornerSize && y > mProfile.bottom - mCornerSize) {
mCurScrollRange = LEFT_BOTTOM;
return true;
}
if (x > mProfile.right - mCornerSize && y > mProfile.bottom - mCornerSize) {
mCurScrollRange = RIGHT_BOTTOM;
return true;
}
return false;
}
如下 改变矩形的范围 重回,过程中 检测边界 不应该超出初始边界,最小不应该超过边框合并的 位置
switch (mCurScrollRange) {
case LEFT_TOP:
mLeft -= distanceX;
mTop -= distanceY;
break;
case LEFT_BOTTOM:
mLeft -= distanceX;
mBottom -= distanceY;
break;
case RIGHT_TOP:
mRight -= distanceX;
mTop -= distanceY;
break;
case RIGHT_BOTTOM:
mRight -= distanceX;
mBottom -= distanceY;
break;
}
checkBorder(distanceX, distanceY);
invalidate();
2)剪切
传入当前矩形的拖动范围
public Bitmap clipImage(Rect rect) {
Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
draw(canvas);
return Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
}
如上 调用draw 方法 就把 当前图片 draw到了我的画布上了!
7. 图片涂鸦
1)原型颜色绘制
首先放置一排颜色提供选择,其实很简单 画一个圈就好了 选中的时候圈大 未选中的时候圈小 然后 一个大圈套一个小圈
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int centerX = getMeasuredWidth() / 2;
mPaint.setColor(Color.WHITE);
canvas.drawCircle(centerX, centerX, centerX, mPaint);
mPaint.setColor(mColor);
canvas.drawCircle(centerX, centerX, centerX - offset, mPaint);
}
2) 涂写
很简单 沿线绘制即可
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (isInEditStatus) {
for (PathInfo pathInfo : mPaths) {
Path path = pathInfo.getPath();
if (pathInfo == getLastPath()) {
path.lineTo(mCurX, mCurY);
}
mPaint.setColor(pathInfo.getColor());
canvas.drawPath(path, mPaint);
}
}
}
然后同样的方法 ,画出当前的 bitmap 就大功告成了!
其他的细节
1 缓存池 使用静态哦 这样不用每次 都创建图片的缓存池
2. 注意刷新viewpager的方法
if (resultCode == RESULT_OK) {
String path = data.getStringExtra("path");
paths.set(mPreviewVp.getCurrentItem(), path);
ScaleImageView image = (ScaleImageView) mPreviewVp.getChildAt(mPreviewVp.getCurrentItem());
Bitmap result = Utils.compress(path, mPreviewVp.getWidth(), mPreviewVp.getHeight());
image.setImageBitmap(result);
//mPreViewImages.set(mPreviewVp.getCurrentItem(), image);
mPreviewVp.getAdapter().notifyDataSetChanged();
mThumbList.getAdapter().notifyItemChanged(mPreviewVp.getCurrentItem());
}
然后惯性滑动的代码有些bug 在手势的fling 方法中 已将其注释,待后续解决
大功告成
github 地址 https://github.com/fanyaopeng/MultiPicturesSelector 欢迎大家提出bug!!