拍照——裁剪,或者是选择图片——裁剪,是我们设置头像或上传图片时经常需要的一组操作。上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现。

背景

  1. 下面的需求都来自产品。
  2. 裁剪图片要像微信那样,拖动和放大的是图片,裁剪框不动。
  3. 裁剪框外的内容要有半透明黑色遮罩。
  4. 裁剪框下面要显示一行提示文字(这点我至今还是持保留意见的)。

在Android中,裁剪图片的控件库还是挺多的,特别是github上比较流行的几个,都已经进化到比较稳定的阶段,但比较遗憾的是它们的裁剪过程是拖动或缩放裁剪框,于是只好自己再找,看有没有现成或半成品的轮子,可以不必从零开始。
踏破铁鞋无觅处,皇天不负苦心人。我终于找到了两篇博客:《Android高仿微信头像裁剪》和《Android 高仿微信头像截取 打造不一样的自定义控件》,以及csdn上找到的前面博客所对应的一份代码,并最终实现了自己的裁剪控件。

大神的实现过程

首先先了解一下上面的高仿微信裁剪控件的实现过程。说起来也不难,主要是下面几点:
1,重写ImageView,并监听手势事件,包括双点,两点缩放,拖动,使它成为一个实现缩放拖动图片功能的控件。
2,定义一个Matrix成员变量,对于维护该图片的缩放、平移等矩阵数据。
3,拖动或缩放时,图片与裁剪框的相交面积一定与裁剪框相等。即图片不能拖离裁剪框。
3,在设置图片时,先根据图片的大小进行初始化的缩放平移操作,使得上面第三条的条件下图片尽可能的小。
4,每次接收到相对应的手势事件,都进行对应的矩阵计算,并将计算结果通过ImageViewsetImageMatrix方法应用到图片上。
5,裁剪框是一个单独的控件,与ImageView同样大,叠加到它上面显示出来。
6,用一个XXXLayout把裁剪框和缩放封装起来。
7,裁剪时,先创建一个空的Bitmap并用其创建一个Canvas,把缩放平移后的图片画到这个Bitmap上,并创建在裁剪框内的Bitmap(通过调用Bitmap.createBitmap方法)。

我的定制内容

我拿到的代码是鸿洋大神版本之后再被改动的,代码上有点乱(虽然功能上是实现的裁剪)。在原有的功能上,我希望进行的改动有:

  • 合并裁剪框的内容到ImageView中
  • 裁剪框可以是任意长宽比的矩形
  • 裁剪框的左右外边距可以设置
  • 遮罩层颜色可以设置
  • 裁剪框下有提示文字(自己的产品需求)
  • 后面产品又加入了一条裁剪图片的最大大小

属性定义

在上面的功能需求中,我定义了以下属性:

<declare-styleable name="ClipImageView"><attr name="civHeight" format="integer"/><attr name="civWidth" format="integer"/><attr name="civTipText" format="string"/><attr name="civTipTextSize" format="dimension"/><attr name="civMaskColor" format="color"/><attr name="civClipPadding" format="dimension"/>
</declare-styleable>

其中:

  • civHeightcivWidth是裁剪框的宽高比例。
  • civTipText提示文字的内容
  • civTipTextSize提示文字的大小
  • civMaskColor遮罩层的颜色值
  • civClipPadding裁剪内边距。由于裁剪框是在控件内部的,最终我选择使用padding来说明裁剪框与我们控件边缘的距离。

成员变量

成员变量我进行了一些改动,把原本用于定义裁剪框的水平边距变量及其他没什么用的变量等给去掉了,并加入了自己的一些成员变量,最终如下:

    private final int mMaskColor;//遮罩层颜色private final Paint mPaint;//画笔private final int mWidth;//裁剪框宽的大小(从属性上读到的整型值)private final int mHeight;//裁剪框高的大小(同上)private final String mTipText;//提示文字private final int mClipPadding;//裁剪框相对于控件的内边距private float mScaleMax = 4.0f;//图片最大缩放大小private float mScaleMin = 2.0f;//图片最小缩放大小/*** 初始化时的缩放比例*/private float mInitScale = 1.0f;/*** 用于存放矩阵*/private final float[] mMatrixValues = new float[9];/*** 缩放的手势检查*/private ScaleGestureDetector mScaleGestureDetector = null;private final Matrix mScaleMatrix = new Matrix();/*** 用于双击*/private GestureDetector mGestureDetector;private boolean isAutoScale;private float mLastX;private float mLastY;private boolean isCanDrag;private int lastPointerCount;private Rect mClipBorder = new Rect();//裁剪框private int mMaxOutputWidth = 0;//裁剪后的图片的最大输出宽度

构造方法

构造方法里主要是多了一些我们自定义属性的读取:

public ClipImageView(Context context) {this(context, null);}public ClipImageView(Context context, AttributeSet attrs) {super(context, attrs);setScaleType(ScaleType.MATRIX);mGestureDetector = new GestureDetector(context,new SimpleOnGestureListener() {@Overridepublic boolean onDoubleTap(MotionEvent e) {if (isAutoScale)return true;float x = e.getX();float y = e.getY();if (getScale() < mScaleMin) {ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);} else {ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);}isAutoScale = true;return true;}});mScaleGestureDetector = new ScaleGestureDetector(context, this);this.setOnTouchListener(this);mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);mPaint.setColor(Color.WHITE);TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);mTipText = ta.getString(R.styleable.ClipImageView_civTipText);mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);mPaint.setTextSize(textSize);ta.recycle();mPaint.setDither(true);}

定义裁剪框

裁剪框的位置

裁剪框是在控件正中间的,首先我们从属性中读取到的是宽高的比例,以及左右边距,但是在构造方法中,由于控件还没有绘制出来,无法获取到控件的宽高,所以并不能计算裁剪框的大小和位置。所以我重写了onLayout方法,在这里计算裁剪框的位置:

    @Overrideprotected void onLayout(boolean changed, int left, int top, int right, int bottom) {super.onLayout(changed, left, top, right, bottom);final int width = getWidth();final int height = getHeight();mClipBorder.left = mClipPadding;mClipBorder.right = width - mClipPadding;final int borderHeight = mClipBorder.width() * mHeight / mWidth;mClipBorder.top = (height - borderHeight) / 2;mClipBorder.bottom = mClipBorder.top + borderHeight;}

绘制裁剪框

这里我顺便把绘制提示文字的代码也一并给出,都是在同一个方法里的。很简单,重写onDraw方法即可。绘制裁剪框有两种方法,一是绘制一个满屏的遮罩层,然后从中间抠出一个长方形出来,但是我用的时候发现抠不出来,所以我采用的是下面这一种:

先画上下两个矩形,再画左右两个矩形,中间所围起来的没有画的部分就是我们的裁剪框。

    @Overrideprotected void onDraw(Canvas canvas) {super.onDraw(canvas);final int width = getWidth();final int height = getHeight();mPaint.setColor(mMaskColor);mPaint.setStyle(Paint.Style.FILL);canvas.drawRect(0, 0, width, mClipBorder.top, mPaint);canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint);canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint);canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint);mPaint.setColor(Color.WHITE);mPaint.setStrokeWidth(1);mPaint.setStyle(Paint.Style.STROKE);canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint);if (mTipText != null) {final float textWidth = mPaint.measureText(mTipText);final float startX = (width - textWidth) / 2;final Paint.FontMetrics fm = mPaint.getFontMetrics();final float startY = mClipBorder.bottom + mClipBorder.top / 2 - (fm.descent - fm.ascent) / 2;mPaint.setStyle(Paint.Style.FILL);canvas.drawText(mTipText, startX, startY, mPaint);}}

修改图片的初始显示

这里我不使用全局布局的监听(通过getViewTreeObserver加入回调),而是直接重写几个设置图片的方法,在设置图片后进行初始显示的设置:

    @Overridepublic void setImageDrawable(Drawable drawable) {super.setImageDrawable(drawable);postResetImageMatrix();}@Overridepublic void setImageResource(int resId) {super.setImageResource(resId);postResetImageMatrix();}@Overridepublic void setImageURI(Uri uri) {super.setImageURI(uri);postResetImageMatrix();}private void postResetImageMatrix() {post(new Runnable() {@Overridepublic void run() {resetImageMatrix();}});}

resetImageMatrix()方法设置图片的初始缩放及平移,参考图片大小,控件本身大小,以及裁剪框的大小进行计算:

    /*** 垂直方向与View的边矩*/public void resetImageMatrix() {final Drawable d = getDrawable();if (d == null) {return;}final int dWidth = d.getIntrinsicWidth();final int dHeight = d.getIntrinsicHeight();final int cWidth = mClipBorder.width();final int cHeight = mClipBorder.height();final int vWidth = getWidth();final int vHeight = getHeight();final float scale;final float dx;final float dy;if (dWidth * cHeight > cWidth * dHeight) {scale = cHeight / (float) dHeight;} else {scale = cWidth / (float) dWidth;}dx = (vWidth - dWidth * scale) * 0.5f;dy = (vHeight - dHeight * scale) * 0.5f;mScaleMatrix.setScale(scale, scale);mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));setImageMatrix(mScaleMatrix);mInitScale = scale;mScaleMin = mInitScale * 2;mScaleMax = mInitScale * 4;}

注意:这里有一个坑。把一个Bitmap设置到ImageView中,显示时要计算的是ImageView获取的Drawable对象以及这个对象的宽高,而不是Bitmap对象。Drawable对象可能由于对Bitmap的放大或缩小显示,导致它的宽或高与Bitmap的宽高不同。
还有一点小注意:获取控件宽高是要在控件被绘制出来之后才能获取得到的,所以上面我通过post一个Runnable对象到主线程的Looper中,保证它是在界面绘制完成之后被调用。

缩放及拖动

缩放及拖动时都需求判断是否超出边界,如果超出,则取允许的最终值。这里的代码我没怎么动,稍后可直接参考源码,暂不赘述。

裁剪

这里是另外一个改造的重点了。
首先,鸿洋大神是通过创建一个空的Bitmap,并根据它创建出一个Canvas对象,然后通过draw方法把缩放后的图片给绘制到这个Bitmap中,再调用Bitmap.createBitmap得到属于裁剪框的内容。但是我们已经重写了onDraw方法画出裁剪框,所以这里就不考虑了。
另外,这种方法还有一个问题:它绘制的是Drawable对象。如果我们设置进去的是一个比较大的Bitmap,那么就可能被缩放了,这里裁剪的是缩放后的Bitmap,也就是它不是对原图进行裁剪的。
这里我参考了其他裁剪图片库,通过保存了缩放平移的Matrix成员变量进行计算,获取出裁剪框在其的对应范围,并根据最终所需(我们产品要限制一个最大大小),得到最终的图片,代码如下:

    public Bitmap clip() {final Drawable drawable = getDrawable();final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();final float[] matrixValues = new float[9];mScaleMatrix.getValues(matrixValues);final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();final float transX = matrixValues[Matrix.MTRANS_X];final float transY = matrixValues[Matrix.MTRANS_Y];final float cropX = (-transX + mClipBorder.left) / scale;final float cropY = (-transY + mClipBorder.top) / scale;final float cropWidth = mClipBorder.width() / scale;final float cropHeight = mClipBorder.height() / scale;Matrix outputMatrix = null;if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {final float outputScale = mMaxOutputWidth / cropWidth;outputMatrix = new Matrix();outputMatrix.setScale(outputScale, outputScale);}return Bitmap.createBitmap(originalBitmap,(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,outputMatrix, false);}

由于我们是对Bitmap进行裁剪,所以首先获取这个Bitmap

        final Drawable drawable = getDrawable();final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();

然后,我们的矩阵值可以通过一个包含9个元素的float数组读出:

        final float[] matrixValues = new float[9];mScaleMatrix.getValues(matrixValues);

比如,读X上的缩放值,代码为matrixValues[Matrix.MSCALE_X]
要特别注意一点,在前文也有提到,这里缩放的是Drawable对象,但是我们裁剪时用的Bitmap,如果图片太大的话是可能在Drawable上进行缩放的,所以缩放大小的计算应该为:

        final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();

然后获取图片平移量:

        final float transX = matrixValues[Matrix.MTRANS_X];final float transY = matrixValues[Matrix.MTRANS_Y];

计算裁剪框对应在图片上的起点及宽高:

        final float cropX = (-transX + mClipBorder.left) / scale;final float cropY = (-transY + mClipBorder.top) / scale;final float cropWidth = mClipBorder.width() / scale;final float cropHeight = mClipBorder.height() / scale;

上面就是我们所要裁剪出来的最终结果。
但是,我前面也说的,应产品需求,要限制最大输出大小。由于我们裁剪出来的图片宽高比是3:2,我这里只取宽度(你要取高度也可以)进行限制,所以又加上了如下代码,当裁剪出来的宽度超出我们最大宽度时,进行缩放。

        Matrix outputMatrix = null;if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {final float outputScale = mMaxOutputWidth / cropWidth;outputMatrix = new Matrix();outputMatrix.setScale(outputScale, outputScale);}

最终根据上面计算出来的值,创建裁剪出来的Bitmap:

Bitmap.createBitmap(originalBitmap,(int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,outputMatrix, false);

这样,图片裁剪控件就算全部完成。

实现效果

后述

  1. 全部代码见:https://github.com/msdx/clip-image,有demo。
  2. 我在控件中还增加了一个接口getClipMatrixValues,获取裁剪时图片的矩阵值,它可用于做大图的裁剪。
  3. 有关大图的裁剪,我后续会再写一篇。
  4. 大图裁剪的代码,也在上面的demo里。
  5. 使用时可以设置裁剪框的宽高比来决定是正方形的裁剪框还是有其他比例要求的裁剪框

本文原创,转载请注明CSDN博客出处:
http://blog.csdn.net/maosidiaoxian/article/details/50828664

参考资料:

  • 《Android高仿微信头像裁剪》
  • 《Android 高仿微信头像截取 打造不一样的自定义控件》
  • cropper

Android开发技巧——定制仿微信图片裁剪控件相关推荐

  1. android 仿照ios 图片选择,GitHub - wildma/PictureSelector: Android 图片选择器(仿 IOS 图片选择控件)...

    PictureSelector Android 图片选择器(仿 IOS 图片选择控件) 效果图 功能特点 支持通过拍照获取图片 支持通过相册获取图片 支持图片是否裁剪两种场景 支持仿 IOS 底部弹出 ...

  2. CropImageView android上的一个图片裁剪控件

    CropImageView **文前:**本文非常容易让读者看的云里雾里,建议直接看效果图,觉得有用就去看源码吧. CropImageView的原型来自Cropimage_demo,是android上 ...

  3. 第一站仿小红书图片裁剪控件,深度解析大厂炫酷控件

    先来看两张效果图: 哈哈,就是这样了.效果差了一些,感兴趣的小伙伴们可以运行代码感受丝滑与弹性.前段时间在竞品小红书上看到了这样的效果:图片可以跟随手指移动,双指可以(无限)放大,缩小,还可以挤压,手 ...

  4. 第一站小红书图片裁剪控件,深度解析大厂炫酷控件

    先来看两张效果图: 哈哈,就是这样了.效果差了一些,感兴趣的小伙伴们可以运行代码感受丝滑与弹性.前段时间在竞品小红书上看到了这样的效果:图片可以跟随手指移动,双指可以(无限)放大,缩小,还可以挤压,手 ...

  5. 第一站小红书图片裁剪控件之二,自定义CoordinatorLayout联动效果

    本篇续: 第一站小红书图片裁剪控件,深度解析大厂炫酷控件 先来看看几张效果图: emmmm,想感受高清丝滑的动画效果,有以下两种方式: https://github.com/HpWens/MeiWid ...

  6. 像小红书一样的图片裁剪控件联动效果

    今日科技快讯 据CNBC报道,美国法官已经要求特斯拉首席执行官埃隆·马斯克(Elon Musk)在未来两周内设法与美国证券交易委员会(SEC)达成和解协议.否则,法院将决定是否判马斯克犯有藐视法庭罪. ...

  7. android禁止下拉刷新,Android开发之无痕过渡下拉刷新控件的实现思路详解

    相信大家已经对下拉刷新熟悉得不能再熟悉了,市面上的下拉刷新琳琅满目,然而有很多在我看来略有缺陷,接下来我将说明一下存在的缺陷问题,然后提供一种思路来解决这一缺陷,废话不多说!往下看嘞! 1.市面一些下 ...

  8. html5图片裁剪控件原型【含缩放,旋转,拖动功能】---2、核心代码

    推荐 这一篇文章是早年为了解决图片裁剪的探索性文章,现在已经开放出了falsh版及html5版本的图片裁剪插件,各位有时间可以看看: 浮士德html5图片裁剪器2016开源版 浮士德头像裁剪flash ...

  9. Android使用Retrofit技术仿微信图片上传,可以选择多张图片拍照上传

    Android 仿照微信发说说,既能实现拍照,选图库,多图案上传 使用Retrofit技术. 使用方法:详见博客 http://blog.csdn.net/u010046908/article/det ...

最新文章

  1. C++文件输入和输出
  2. 深入WPF中的图像画刷(ImageBrush)之1——ImageBrush使用举例
  3. android 并排按钮,简单布局:右边三个按钮并排靠右,左边一个输入框填满其他空间,多谢...
  4. POJ 1087 -- A Plug for UNIX(最大流,建图)(文末有极限数据)
  5. JavaScript中encodeURI,encodeURIComponent与escape的注意
  6. 腾讯云AI应用产品总监王磊:AI 在传统产业的最佳实践
  7. jQuery动态设置样式List item
  8. MFC中如何给对话框添加背景图片
  9. [转]Eclipse+pydev 常用快捷键
  10. 配置Windows Server 2008群集
  11. 如何在macOS中得到“另存为”快捷方式
  12. android launcher主要功能_Android 或有新变化,语音搜索进一步强化
  13. 飞机大战(Java)
  14. HarmonyOS移动应用学习笔记——2.HarmonyOS开发工具DevEco Studio安装
  15. 中间件系列六 RabbitMQ之Topic exchange 用法
  16. python input隐藏输入_python输入input
  17. day 05 random time sys os pickle json re模块 爬取dytt
  18. NPOI读取Word模板并保存
  19. [SSL_CHX][2021-08-19]前缀和
  20. 共享办公室,推送企业紧密合作

热门文章

  1. 根据下图实现类。在CylinderTest类中创建Cylinder类的对象,设置圆柱的底面半径和高,并输出圆柱的体积,继承性
  2. 大学生初涉职场十一大病毒
  3. Python环境的安装和配置
  4. 计算机网络基础中国石油大学,2017中国石油大学继续教育计算机网络基础答案...
  5. 隐私计算与区块链的融合思考
  6. UNIAPP中H5微信登录
  7. 神经网络为什么要使用激活函数,为什么relu要比sigmoid要好
  8. 苹果台式机怎么设置我的电脑计算机,苹果电脑一体机怎么开机_苹果台式一体机开机在哪-win7之家...
  9. html5 css3 jquery 画板
  10. E2类 MCR-WPT系统的搭建