拍照——裁剪,或者是选择图片——裁剪,是我们设置头像或上传图片时经常需要的一组操作。上篇讲了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中
  • 裁剪框可以是任意长宽比的矩形
  • 裁剪框的左右外边距可以设置
  • 遮罩层颜色可以设置
  • 裁剪框下有提示文字(自己的产品需求)
  • 后面产品又加入了一条裁剪图片的最大大小

属性定义

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

  1. <declare-styleable name="ClipImageView">
  2. <attr name="civHeight" format="integer"/>
  3. <attr name="civWidth" format="integer"/>
  4. <attr name="civTipText" format="string"/>
  5. <attr name="civTipTextSize" format="dimension"/>
  6. <attr name="civMaskColor" format="color"/>
  7. <attr name="civClipPadding" format="dimension"/>
  8. </declare-styleable>

其中:

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

成员变量

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

  1. private final int mMaskColor;//遮罩层颜色
  2. private final Paint mPaint;//画笔
  3. private final int mWidth;//裁剪框宽的大小(从属性上读到的整型值)
  4. private final int mHeight;//裁剪框高的大小(同上)
  5. private final String mTipText;//提示文字
  6. private final int mClipPadding;//裁剪框相对于控件的内边距
  7. private float mScaleMax = 4.0f;//图片最大缩放大小
  8. private float mScaleMin = 2.0f;//图片最小缩放大小
  9. /**
  10. * 初始化时的缩放比例
  11. */
  12. private float mInitScale = 1.0f;
  13. /**
  14. * 用于存放矩阵
  15. */
  16. private final float[] mMatrixValues = new float[9];
  17. /**
  18. * 缩放的手势检查
  19. */
  20. private ScaleGestureDetector mScaleGestureDetector = null;
  21. private final Matrix mScaleMatrix = new Matrix();
  22. /**
  23. * 用于双击
  24. */
  25. private GestureDetector mGestureDetector;
  26. private boolean isAutoScale;
  27. private float mLastX;
  28. private float mLastY;
  29. private boolean isCanDrag;
  30. private int lastPointerCount;
  31. private Rect mClipBorder = new Rect();//裁剪框
  32. private int mMaxOutputWidth = 0;//裁剪后的图片的最大输出宽度

构造方法

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


  1. public ClipImageView(Context context) {
  2. this(context, null);
  3. }
  4. public ClipImageView(Context context, AttributeSet attrs) {
  5. super(context, attrs);
  6. setScaleType(ScaleType.MATRIX);
  7. mGestureDetector = new GestureDetector(context,
  8. new SimpleOnGestureListener() {
  9. @Override
  10. public boolean onDoubleTap(MotionEvent e) {
  11. if (isAutoScale)
  12. return true;
  13. float x = e.getX();
  14. float y = e.getY();
  15. if (getScale() < mScaleMin) {
  16. ClipImageView.this.postDelayed(new AutoScaleRunnable(mScaleMin, x, y), 16);
  17. } else {
  18. ClipImageView.this.postDelayed(new AutoScaleRunnable(mInitScale, x, y), 16);
  19. }
  20. isAutoScale = true;
  21. return true;
  22. }
  23. });
  24. mScaleGestureDetector = new ScaleGestureDetector(context, this);
  25. this.setOnTouchListener(this);
  26. mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
  27. mPaint.setColor(Color.WHITE);
  28. TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.ClipImageView);
  29. mWidth = ta.getInteger(R.styleable.ClipImageView_civWidth, 1);
  30. mHeight = ta.getInteger(R.styleable.ClipImageView_civHeight, 1);
  31. mClipPadding = ta.getDimensionPixelSize(R.styleable.ClipImageView_civClipPadding, 0);
  32. mTipText = ta.getString(R.styleable.ClipImageView_civTipText);
  33. mMaskColor = ta.getColor(R.styleable.ClipImageView_civMaskColor, 0xB2000000);
  34. final int textSize = ta.getDimensionPixelSize(R.styleable.ClipImageView_civTipTextSize, 24);
  35. mPaint.setTextSize(textSize);
  36. ta.recycle();
  37. mPaint.setDither(true);
  38. }

定义裁剪框

裁剪框的位置

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

  1. @Override
  2. protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
  3. super.onLayout(changed, left, top, right, bottom);
  4. final int width = getWidth();
  5. final int height = getHeight();
  6. mClipBorder.left = mClipPadding;
  7. mClipBorder.right = width - mClipPadding;
  8. final int borderHeight = mClipBorder.width() * mHeight / mWidth;
  9. mClipBorder.top = (height - borderHeight) / 2;
  10. mClipBorder.bottom = mClipBorder.top + borderHeight;
  11. }

绘制裁剪框

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



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

  1. @Override
  2. protected void onDraw(Canvas canvas) {
  3. super.onDraw(canvas);
  4. final int width = getWidth();
  5. final int height = getHeight();
  6. mPaint.setColor(mMaskColor);
  7. mPaint.setStyle(Paint.Style.FILL);
  8. canvas.drawRect(0, 0, width, mClipBorder.top, mPaint);
  9. canvas.drawRect(0, mClipBorder.bottom, width, height, mPaint);
  10. canvas.drawRect(0, mClipBorder.top, mClipBorder.left, mClipBorder.bottom, mPaint);
  11. canvas.drawRect(mClipBorder.right, mClipBorder.top, width, mClipBorder.bottom, mPaint);
  12. mPaint.setColor(Color.WHITE);
  13. mPaint.setStrokeWidth(1);
  14. mPaint.setStyle(Paint.Style.STROKE);
  15. canvas.drawRect(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom, mPaint);
  16. if (mTipText != null) {
  17. final float textWidth = mPaint.measureText(mTipText);
  18. final float startX = (width - textWidth) / 2;
  19. final Paint.FontMetrics fm = mPaint.getFontMetrics();
  20. final float startY = mClipBorder.bottom + mClipBorder.top / 2 - (fm.descent - fm.ascent) / 2;
  21. mPaint.setStyle(Paint.Style.FILL);
  22. canvas.drawText(mTipText, startX, startY, mPaint);
  23. }
  24. }

修改图片的初始显示

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

  1. @Override
  2. public void setImageDrawable(Drawable drawable) {
  3. super.setImageDrawable(drawable);
  4. postResetImageMatrix();
  5. }
  6. @Override
  7. public void setImageResource(int resId) {
  8. super.setImageResource(resId);
  9. postResetImageMatrix();
  10. }
  11. @Override
  12. public void setImageURI(Uri uri) {
  13. super.setImageURI(uri);
  14. postResetImageMatrix();
  15. }
  16. private void postResetImageMatrix() {
  17. post(new Runnable() {
  18. @Override
  19. public void run() {
  20. resetImageMatrix();
  21. }
  22. });
  23. }

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

  1. /**
  2. * 垂直方向与View的边矩
  3. */
  4. public void resetImageMatrix() {
  5. final Drawable d = getDrawable();
  6. if (d == null) {
  7. return;
  8. }
  9. final int dWidth = d.getIntrinsicWidth();
  10. final int dHeight = d.getIntrinsicHeight();
  11. final int cWidth = mClipBorder.width();
  12. final int cHeight = mClipBorder.height();
  13. final int vWidth = getWidth();
  14. final int vHeight = getHeight();
  15. final float scale;
  16. final float dx;
  17. final float dy;
  18. if (dWidth * cHeight > cWidth * dHeight) {
  19. scale = cHeight / (float) dHeight;
  20. } else {
  21. scale = cWidth / (float) dWidth;
  22. }
  23. dx = (vWidth - dWidth * scale) * 0.5f;
  24. dy = (vHeight - dHeight * scale) * 0.5f;
  25. mScaleMatrix.setScale(scale, scale);
  26. mScaleMatrix.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f));
  27. setImageMatrix(mScaleMatrix);
  28. mInitScale = scale;
  29. mScaleMin = mInitScale * 2;
  30. mScaleMax = mInitScale * 4;
  31. }

注意:这里有一个坑。把一个Bitmap设置到ImageView中,显示时要计算的是ImageView获取的Drawable对象以及这个对象的宽高,而不是Bitmap对象。Drawable对象可能由于对Bitmap的放大或缩小显示,导致它的宽或高与Bitmap的宽高不同。

还有一点小注意:获取控件宽高是要在控件被绘制出来之后才能获取得到的,所以上面我通过post一个Runnable对象到主线程的Looper中,保证它是在界面绘制完成之后被调用。

缩放及拖动

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

裁剪

这里是另外一个改造的重点了。

首先,鸿洋大神是通过创建一个空的Bitmap,并根据它创建出一个Canvas对象,然后通过draw方法把缩放后的图片给绘制到这个Bitmap中,再调用Bitmap.createBitmap得到属于裁剪框的内容。但是我们已经重写了onDraw方法画出裁剪框,所以这里就不考虑了。

另外,这种方法还有一个问题:它绘制的是Drawable对象。如果我们设置进去的是一个比较大的Bitmap,那么就可能被缩放了,这里裁剪的是缩放后的Bitmap,也就是它不是对原图进行裁剪的。

这里我参考了其他裁剪图片库,通过保存了缩放平移的Matrix成员变量进行计算,获取出裁剪框在其的对应范围,并根据最终所需(我们产品要限制一个最大大小),得到最终的图片,代码如下:

  1. public Bitmap clip() {
  2. final Drawable drawable = getDrawable();
  3. final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
  4. final float[] matrixValues = new float[9];
  5. mScaleMatrix.getValues(matrixValues);
  6. final float scale = matrixValues[Matrix.MSCALE_X] * drawable.getIntrinsicWidth() / originalBitmap.getWidth();
  7. final float transX = matrixValues[Matrix.MTRANS_X];
  8. final float transY = matrixValues[Matrix.MTRANS_Y];
  9. final float cropX = (-transX + mClipBorder.left) / scale;
  10. final float cropY = (-transY + mClipBorder.top) / scale;
  11. final float cropWidth = mClipBorder.width() / scale;
  12. final float cropHeight = mClipBorder.height() / scale;
  13. Matrix outputMatrix = null;
  14. if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
  15. final float outputScale = mMaxOutputWidth / cropWidth;
  16. outputMatrix = new Matrix();
  17. outputMatrix.setScale(outputScale, outputScale);
  18. }
  19. return Bitmap.createBitmap(originalBitmap,
  20. (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
  21. outputMatrix, false);
  22. }

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

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

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

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

比如,读X上的缩放值,代码为matrixValues[Matrix.MSCALE_X]

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

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

然后获取图片平移量:

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

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

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

上面就是我们所要裁剪出来的最终结果。

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

  1. Matrix outputMatrix = null;
  2. if (mMaxOutputWidth > 0 && cropWidth > mMaxOutputWidth) {
  3. final float outputScale = mMaxOutputWidth / cropWidth;
  4. outputMatrix = new Matrix();
  5. outputMatrix.setScale(outputScale, outputScale);
  6. }

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

  1. Bitmap.createBitmap(originalBitmap,
  2. (int) cropX, (int) cropY, (int) cropWidth, (int) cropHeight,
  3. 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开发技巧——定制仿微信图片裁剪控件的更多相关文章

  1. android 开发进阶 自定义控件-仿ios自动清除控件

    先上图: 开发中经常需要自定义view控件或者组合控件,某些控件可能需要一些额外的配置.比如自定义一个标题栏,你可能需要根据不同尺寸的手机定制不同长度的标题栏,或者更常见的你需要配置标题栏的背景,这时 ...

  2. Android开发之高仿微信图片选择器

    记得刚开始做Andriod项目那会,经常会碰到一些上传图片的功能需求,特别是社交类的app,比如用户头像,说说配图,商品配图等功能都需要让我们到系统相册去选取图片,但官方却没有提供可以选取多张图片的相 ...

  3. android之使用GridView+仿微信图片上传功能

    由于工作要求最近在使用GridView完成图片的批量上传功能,我的例子当中包含仿微信图片上传.拍照.本地选择.相片裁剪等功能,如果有需要的朋友可以看一下,希望我的实际经验能对您有所帮助. 直接上图,下 ...

  4. Android 自定义 HorizontalScrollView 打造再多图片(控件)也不怕 OOM 的横向滑动效果

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/38140505 自从Gallery被谷歌废弃以后,Google推荐使用ViewPa ...

  5. android开发 软键盘出现后 防止EditText控件遮挡 总体平移UI

    在EditText控件接近底部的情况下 软键盘弹出后会把获得焦点的EditText控件遮挡 无法看到输入信息  防止这种情况发生 就须要设置AndroidManifest.xml的属性 前面的xml信 ...

  6. Android开发:在布局里移动ImageView控件

    在做一个app时碰到需要移动一个图案的位置,查了一上午资料都没找到demo,自己写一个吧 RelativeLayout.LayoutParams lp = new RelativeLayout.Lay ...

  7. 学习笔记001之[Android开发视频教学].01_06_Android当中的常见控件

    文本框,按钮 菜单按钮(需复写两个方法) 后续需完成联系代码.

  8. eclipse android开发,文本编辑xml文件,给控件添加ID后,R.java,不自动的问题。

    直接编辑xml文件给控件添加id,不自动更新.原来的id写法:@id/et_tel 然后改写成这样:@+id/et_tel  然后就好了!操`1

  9. Android开发技巧——大图裁剪

    本篇内容是接上篇<Android开发技巧--定制仿微信图片裁剪控件> 的,先简单介绍对上篇所封装的裁剪控件的使用,再详细说明如何使用它进行大图裁剪,包括对旋转图片的裁剪. 裁剪控件的简单使 ...

随机推荐

  1. $rootscope说明

    scope是AngularJS中的作用域(其实就是存储数据的地方),很类似JavaScript的原型链 .搜索的时候,优先找自己的scope,如果没有找到就沿着作用域链向上搜索,直至到达根作用域roo ...

  2. jacascript DOM节点——元素节点、属性节点、文本节点

    前言:这是笔者学习之后自己的理解与整理.如果有错误或者疑问的地方,请大家指正,我会持续更新! DOM节点的三个种类:元素节点.文本节点.属性节点: 元素节点 元素节点就是 HTML 标签元素,元素节点 ...

  3. 使用控制台调试WinForm窗体程序

    .程序代码结构 .Win32DebuggerHelper.cs using System.Runtime.InteropServices; /* TODO:使用方法 Win32.AllocConsol ...

  4. java学习历程,一年三年五年计划

    学习这一部分其实也算是今天的重点,这一部分用来回答很多群里的朋友所问过的问题,那就是你是如何学习Java的,能不能给点建议?今天我是打算来点干货,因此咱们就不说一些学习方法和技巧了,直接来谈每个阶段要 ...

  5. Android重构篇——项目架构篇

    版权声明:本文为博主原创文章,未经博主允许不得转载. 转载请表明出处:http://www.cnblogs.com/cavalier-/p/6823777.html 前言 大家好,我是Cavalier ...

  6. 使用数据库乐观锁解决高并发秒杀问题,以及如何模拟高并发的场景,CyclicBarrier和CountDownLatch类的用法

    数据库:mysql 数据库的乐观锁:一般通过数据表加version来实现,相对于悲观锁的话,更能省数据库性能,废话不多说,直接看代码 第一步: 建立数据库表: CREATE TABLE `skill_ ...

  7. Centos常用命令之:ls和cd

    在使用centos这个linux系统的时候,我们总是免不了需要查看当前目录中的内容,需要切换到别的目录,新建删除等等一系列在window中非常普通的操作. 那在linux中这些操作是什么样的呢. 在l ...

  8. [JSOI2007]合金

    Description 某公司加工一种由铁.铝.锡组成的合金.他们的工作很简单.首先进口一些铁铝锡合金原材料,不同种类的 原材料中铁铝锡的比重不同.然后,将每种原材料取出一定量,经过融解.混合,得到新 ...

  9. [HNOI2016]树

    Description 小A想做一棵很大的树,但是他手上的材料有限,只好用点小技巧了.开始,小A只有一棵结点数为N的树,结 点的编号为1,2,…,N,其中结点1为根:我们称这颗树为模板树.小A决定通过 ...

  10. 4999: This Problem Is Too Simple!

    Description 给您一颗树,每个节点有个初始值. 现在支持以下两种操作: C i x(0<=x<2^31) 表示将i节点的值改为x. Q i j x(0<=x<2^31 ...