Android图片裁剪实现(EnjoyCrop)

Android裁剪功能实现

概述

从4月初到5月份 ,差不多一个多月,终于把裁剪图片的功能码出来了,期间,解决了一个又来一个问题,好吧,问题总是会有的。

这里大致介绍这个裁剪功能技术点、主要难点,实现原理。

技术点

  • 图片缩放、移动
  • 裁剪区域预览
  • 裁剪(包括越图片边界裁剪)
  • 边界限制

主要难点

  • 裁剪区域预览
  • 裁剪
  • 边界限制

实现原理

裁剪预览区域的实现

在我做过的项目中,就有使用过一些网络上开源的裁剪功能:半透明遮罩层的矩形预览框功能。它的实现原理是在裁剪预览区域外的地方填充了几个半透明的矩形框,进而实现了矩形裁剪预览框功能,如下图。

这种功能虽然可以实现预览功能,但是仅仅局限于当预览区外的地方可以通过规则的形状填充,如果是圆形的裁剪预览框,那么就没办法通过这种方式来实现了。

所以我们需要另外想过办法来实现圆形的预览框。在一开始的时候,我这边的思路是通过在半透明的遮罩层上镂空一个预览框。我们来试试在半透明的遮罩层上叠加一个透明的预览框。

    public void onDraw(Canvas canvas){
        //绘画半透明遮罩
        canvas.drawColor(Color.parseColor("#90000000"));
        Paint paint = new Paint();
        paint.setColor(Color.TRANSPARENT);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        int left = getWidth()/2;
        int top = getHeight()/2;
        //绘制透明预览框
        canvas.drawCircle(left, top, 300, paint);
    }

效果可见下图。

可以看出,虽然在中间白色的预览框是全透明的一个裁剪预览框,,下面还是会有一层半透明的遮罩层覆盖住图片,实现不了预览框全透明的效果。

看来这种简单的叠加方式是无法实现我们的需求,所以通过搜寻资料,最终,发现可以采用Xfermode方式来实现。通过设定Xfermode模式,可以将两个重叠的层通过一定的方式来显示,例如下层是半透明遮罩,上层是透明圆形框,那么可以通过设置相应的Xfermode模式来实现。我们改一下上面的代码:


 public void onDraw(Canvas canvas){
        //这里需要通过bitmap创建canvas才能对Xfermode生效果,具体原因这里也不大清楚
        Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
        Canvas xFerCanvas = new Canvas(bitmap);
        //绘画半透明遮罩
        xFerCanvas.drawColor(Color.parseColor("#90000000"));
        Paint paint = new Paint();
        paint.setColor(Color.TRANSPARENT);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        //设置当前画笔的Xfermode模式,不同的模式效果可以参照Google提供的Demo-ApiDemos/Graphics/XferModes
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
        int left = getWidth()/2;
        int top = getHeight()/2;
        //绘制透明预览框
        xFerCanvas.drawCircle(left, top, 300, paint);
        //最后将生成的bitmap绘制到我们的画布上
        canvas.drawBitmap(bitmap,0,0,null);
        bitmap.recycle();
        System.gc();
    }

效果可见下图

可以看出,实现了我们想要的效果。

对Xfermode更加详细的讲解可以阅读博文时之沙-Android 颜色渲染(九) PorterDuff及Xfermode详解,里面有详细的讲解不同的Xfermode对层叠加的不同效果。虽然这种方案可以实现效果,但是这种方案有一个很大的缺点,就是需要创建一个新的Bitmap,会导致内容占用率大量提高。所以这里通过了博文JianTao_Yang-Android ImageCropper 矩形 圆形 裁剪框找到了第二种方案。第二种方案的实现思路是:在绘画半透明遮罩之前,先将画布可以绘画位置限定在裁剪预览框之外,这样绘画的半透明遮罩自然就空下了中间的预览框,这样就实现了该功能。

    public void onDraw(Canvas canvas){

        Paint paint = new Paint();
        paint.setColor(Color.TRANSPARENT);
        paint.setStyle(Paint.Style.FILL);
        paint.setAntiAlias(true);
        int left = getWidth()/2;
        int top = getHeight()/2;
        //创建圆形预览框
        Path path = new Path();
        path.addCircle(left, top, 300, Path.Direction.CW);
        //保存当前canvas 状态
        canvas.save();
        //将当前画布可以绘画区域限制死为预览框外的区域
        canvas.clipPath(path, Region.Op.DIFFERENCE);
        //绘画半透明遮罩
        canvas.drawColor(Color.parseColor("#90000000"));
        //还原画布状态
        canvas.restore();
    }

这里就不贴图了,最终效果与采用Xfermode叠加方案是一样,并且不需要创建新的Bitmap,不会导致内存占用率大量提高。但是这种方案也有其局限性,由于我们只能通过Path来限制其在画布可绘画的区域,并且Path只支持一些几何形的图案,所以预览框形状被限死在几何形图案集合内。

这里总结一下上面两种方案和其应用场景:

  • 如果是几何形的预览框,那么首推限制绘画区域的方案,内存占用率低。
  • 如果是非几何形的预览框(例如卡通形状的预览框),那么在这里给出的方案里,你只能通过Xfermode方式来实现了,不过使用这种方式需要注意内存的占有率。

图片缩放&移动实现

这里的图片缩放、移动全部通过Matrix实现的。其实移动的实现方式可以采用两种方式:

  • View.scrollBy(int,int)或者View.scrollTo(int,int)方式实现.
  • 图片Matrix处理。

    但是这里由于需要实现缩放功能,所以干脆统一采用Matrix方式来实现。Matrix是一组参数集合,其中不同的参数对应着不同的功能处理(平移/缩放等),具体可以查看博文Qiengo-Android Matrix

    这里只讲述Matrix实现的缩放与移动,不对View.scroll**移动方式多做说明。

    在通过Matrix实现缩放、移动之前,需要调用ImageView.setScaleType(ImageView.ScaleType.MATRIX),将ImageView的缩放方式设置为MATRIX。

Matrix移动实现

Matrix移动的实现十分简单,通过记录最后移动点与当前移动点的距离就可以实现移动功能。

  //motionX,motionY为当前触摸的坐标
  public void drag(float motionX, float motionY) {
        //mLastY,mLastX 为上一次触摸的坐标
        float moveX = motionX -mLastX;
        float moveY = motionY - mLastY;
        //通过postTranslate方法就可以移动到相应的位置
        mView.getImageMatrix().postTranslate(moveX,moveY);
        //重画视图
        mView.invalidate();
    }

Matrix缩放实现

Martix缩放功能也是相对简单。通过ScaleGestureDetector方式实现了缩放功能。我们主要通过实现ScaleGestureDetector,重写onScale方法,当然由于与移动功能叠加,所以需要在缩放的时候,屏蔽掉移动功能,所以我们需要记录缩放开始与结束。

  @Override
    public boolean onScale(ScaleGestureDetector detector) {
        //px,py为缩放的中心点,以该点为中心点进行缩放
        float px = detector.getFocusX();
        float py = detector.getFocusY();
        //缩放的比例 大于1为放大, 小于1为缩小
        float scaleFactor= detector.getScaleFactor();
        //通过postScale方式来实现缩放效果
        mView.getImageMatrix().postScale(scaleFactor,scaleFactor,
                px,py);
        //重画视图
        mView.invalidate();
        return true;
    }
     @Override
    public boolean onScaleBegin(ScaleGestureDetector detector) {
        //设置缩放标志位
        isScale = true;
        return true;
    }

    @Override
    public void onScaleEnd(ScaleGestureDetector detector) {
        isScale = false;
    }

    @Override
    public boolean touch(MotionEvent event){
        //缩放手势处理
        mGestureDetector.onTouchEvent(event);
        //如果不在缩放中,则处理普通的触摸事件
        if(!isScale){
            }
        }
        return false;

    }

裁剪功能实现

如果没有移动与缩放功能,那么裁剪会是一个相当简单的功能,因为其裁剪的位置总是固定的,但是如果加入了移动与缩放,那么事情就变的复杂了。当移动后与缩放后,裁剪的位置与大小都发生了变化,另外,移动和缩放可能导致图片部分或者全部不在预览框内,这些情况我们都需要进行处理,下面我们看看怎么正确的裁剪出预览框显示的图片。由于为了照顾到图片不在预览框的情况,所以我们采用了以下方式来做最终的图片裁剪:

 public void crop() {
        //其中width与height是最终实际裁剪的图片大小,saveBitmap就是最终裁剪的图片
        Bitmap saveBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
        Canvas canvas = new Canvas(saveBitmap);
        //bitmap为原图,这里就是最终裁剪图片的实现方式,其中cropRect是裁剪区域,showRect是最终显示在画布的区域
        canvas.drawBitmap(bitmap, cropRect, showRect, new Paint());
    }

从上面代码段,我们可以清晰的知道影响裁剪的因素有:

- 最终裁剪的图片大小

- 实际裁剪的四个角的位置(相对于原图)

- 显示裁剪图片的四个角的位置(相对于画布)

注意以下的计算的前置条件是原图片中心点与预览框中心点均与屏幕中心点重叠

其中裁剪的图片大小我们很容易就可以根据裁剪预览框的大小与原图片缩放的倍数来获取。

我们通过裁剪的左上角起始坐标与最终裁剪的图片大小,来获取裁剪的四个位置。

//actuallyWidth与actuallyHeight为裁剪的实际长宽
//原图中心点x坐标--实际图片x坐标中心点-横坐标的实际偏移量,就可以得出裁剪的左上角位置
//,由于这里采用比较心点的方式去得到实际横坐标偏移量,所以这里可以不用理会缩放与移动产生的偏移量。
cropLeft = (int) (bitmap.getWidth()/2-mImageView.getActuallyScrollX()/scale-actuallyWidth/2);
//这里也是一样
cropTop = (int) (bitmap.getHeight()/2-mImageView.getActuallyScrollY()/scale - actuallyHeight/2);
 //这里会进行边界判断,默认右边点为左边点+宽度
 int cropRight = cropLeft+actuallyWidth;
 int cropBottom =  cropTop+actuallyHeight;
  //裁剪总宽度超出原图宽度,需要重新设置右边点位置为图片宽度
 if(cropRight>bitmap.getWidth()){
     cropRight = bitmap.getWidth();
 }
  //裁剪总高度超出原图高度,需要重新设置右边点位置为图片高度
 if(cropBottom>bitmap.getHeight()){
     cropBottom = bitmap.getHeight();
 }

而显示区域的四个角位置,获取就相对简单。其中左与上固定为0,剩下的就是右边与底部点了。

        //由于裁剪区域与显示区域长宽应该是一致的,所以这里默认右边与底部为最终裁剪大小
        int showRight = actuallyWidth;
        int showBottom = actuallyHeight;
        int cropRight = cropLeft+actuallyWidth;
        int cropBottom =  cropTop+actuallyHeight;
        //裁剪超出图片边界超出边界
        if(cropRight>bitmap.getWidth()){
            cropRight = bitmap.getWidth();
            //由于左固定为0,那么这里相应也要调整右边位置,让宽度与裁剪区域一致
            showRight = bitmap.getWidth()-cropLeft;
        }
        if(cropBottom>bitmap.getHeight()){
            cropBottom = bitmap.getHeight();
            //由于上位置固定为0,那么这里相应也要调整底部位置,让高度与裁剪区域一致
            showBottom = bitmap.getHeight()-cropTop;
        }

至此,裁剪所需要的参数全部计算完毕,这样就可以正确裁剪出预览框中的内容。


边界限制

为了提升用户体验,或者是实现需求,可能我们需要限制缩放&移动的边界,让裁剪预览框的区域可以完全在图片里面,换个意思就是说,裁剪最后的图片一定是图片上的某个区域,而不会出现只裁剪到一部分图片,另一部分是空白的。

边界的限制只是针对移动与缩放。下面我们分别看看怎么对两者做边界限制

移动边界限制

同样由于涉及到了缩放,移动的边界限制需要特别处理。具体的思路是获取当前移动的距离与当前图片在屏幕上实际的四个位置点(左右上下),例如我们需要判断是否会超过左边界,那么我们会判断横坐标移动的距离+图片当前左边位置是否大于限制框的左边横坐标,是的话,那么则视为出界,应当重新计算移动距离。其他三个位置亦是如此,我们还是看下下面的代码片。

    public void drag(float motionX, float motionY) {
        //移动距离
        float moveX = motionX -mLastX;
        float moveY = motionY - mLastY;
        //mRestrictRect为限制框,这个框实质就是预览框在屏幕上的坐标位置
        if(mRestrictRect!=null){
            //经过缩放与移动后,图片在屏幕上实际的位置
            RectF rectF = getCurrentRectF();
            //下面为四边边界的判断与重计算
            if(moveX>0){
                if(rectF.left+moveX>mRestrictRect.left){
                    moveX = mRestrictRect.left-rectF.left;
                }
            }else {
                if(rectF.right+moveX<mRestrictRect.right){
                    moveX = mRestrictRect.right- rectF.right;
                }
            }
            if(moveY>0){
                if(rectF.top+moveY>mRestrictRect.top){
                    moveY = mRestrictRect.top-rectF.top;
                }
            }else {
                if(rectF.bottom+moveY<mRestrictRect.bottom){
                    moveY = mRestrictRect.bottom- rectF.bottom;
                }
            }
        }
        mView.getImageMatrix().postTranslate(moveX,moveY);
        mView.invalidate();
    }

缩放边界限制

缩放边界的限制会相对复杂。因为当缩放出界时,需要根据多种情况重新计算缩放所需要的参数。

缩放边界限制的流程是:

1、按照缩放值,获取图片将会在屏幕出现的位置

2、判断四个位置是否会超出边界,并记录四个位置边界判断结果。如果缩放后的某个位置会超出限制框的边界位置,那么则限定该坐标为缩放中心点,保证该点位置不移动。例如左边将会出界,那么则以限制框的左边位置作为最终缩放中心点横坐标。

3、如果左右或者上下出界,则不用进行缩放,因为无法缩放了。如果不是这种情况,则进入4

4、根据Martrix缩放的计算公式推导出,什么缩放倍数下,会达到限制框的边界值。这里将会取到四个边界缩放值与当前的缩放值进行比对,取其中最大的缩放值作为最后的缩放值(因为缩小情况才会导致越界)

我们看看具体的代码片

    public boolean onScale(ScaleGestureDetector detector) {

        //初始化缩放值
        float px = detector.getFocusX();
        float py = detector.getFocusY();
        float scaleFactor= detector.getScaleFactor();

        if(mRestrictRect!=null){
            Matrix matrixAfter = new Matrix(mView.getImageMatrix());
                     matrixAfter.postScale(detector.getScaleFactor()
                     ,detector.getScaleFactor(),detector.ge tFocusX(),detector.getFocusY());
            final BitmapDrawable drawable = (BitmapDrawable) mView.getDrawable();
            final Bitmap bitmap  = drawable.getBitmap();
            RectF rectF = new RectF(0,0,bitmap.getWidth(),bitmap.getHeight());
            //上面的大段代码都是为了这里,这里将会获取按照当前缩放值缩放后的图片实际的坐标位置
            matrixAfter.mapRect(rectF);

            boolean isLeftLimit = false ,isRightLimit = false ,
            isTopLimit = false ,isBottomLimit =false;
            //判断缩放后的位置是否会超过边界
            if(rectF.left>mRestrictRect.left){
                //超过边界则将点最为最后的缩放中心点,让该边界的点固定下来,不被改变
                px = mRestrictRect.left;
                isLeftLimit = true;
            }
            if(rectF.right<mRestrictRect.right){
                px = mRestrictRect.right;
                isRightLimit = true;
            }
            if(rectF.top>mRestrictRect.top){
              py = mRestrictRect.top;
                isTopLimit = true;
            }
            if(rectF.bottom<mRestrictRect.bottom){
                py = mRestrictRect.bottom;
                isBottomLimit = true;
            }
            //左右两边或者上下两边都无法缩放,就不缩放了
            if((isRightLimit&&isLeftLimit)||(isTopLimit&&isBottomLimit)){
                return true;
            }
            //重新计算允许的最小缩放倍数,根据四条边界的缩放倍数与当前的缩放倍数,
            //获取最大缩放倍数,因为主要是缩小才会导致超越边界
            //计算公式是: 结果坐标(ResultX) = 缩放前坐标(Before X)*缩放倍数(scale)
            //+中心点坐标(center X)*(1-缩放倍数(scale))
            float maxScaleLeft = (mRestrictRect.left-px)/(getCurrentRectF().left-px);
            if(scaleFactor<maxScaleLeft){
                scaleFactor = maxScaleLeft;
            }
            float maxScaleRight = (mRestrictRect.right-px)/(getCurrentRectF().right-px);
            if(scaleFactor<maxScaleRight){
                scaleFactor = maxScaleRight;
            }
            float maxScaleTop = (mRestrictRect.top-py)/(getCurrentRectF().top-py);
            if(scaleFactor<maxScaleTop){
                scaleFactor = maxScaleTop;
            }
            float maxSacleBottom = (mRestrictRect.bottom-py)/(getCurrentRectF().bottom-py);
            if(scaleFactor<maxSacleBottom){
                scaleFactor = maxSacleBottom;
            }
        }
        //保存当前的缩放值
        mScale=mScale*scaleFactor;
        //执行缩放
        mView.getImageMatrix().postScale(scaleFactor,scaleFactor,
                px,py);
        mView.invalidate();
        return true;
    }

GitHub地址

最后附录上EnjoyCrop在GitHub上的地址–EnjoyCrop,希望这篇文档对你有帮助,谢谢!

时间: 2024-10-27 17:31:10

Android图片裁剪实现(EnjoyCrop)的相关文章

Android图片裁剪功能——调用系统裁剪

花了两天时间看了下android的图片裁剪功能的实现.其实刚开始做这个我挺虚的,以为整个功能都需要自己写出来,但查了些资料,发现android已经提供了裁剪功能,需要的话自己调用就成了.soga,这下轻松多了. 首先推荐几篇博客 Android大图片裁剪终极解决方案 要想弄明白裁剪功能,这系列博客非常重要,你可以不看我下面总结的,但你一定要看他这系列的几篇文章. Android 图片裁剪功能实现详解(类似QQ自定义头像裁剪) 这篇也不错,比较喜欢他的注释.虽然也有些误导,比如说他有一段对setD

Android图片裁剪之自由裁剪

我的博客http://blog.csdn.net/dawn_moon 客户的需求都是非常怪的.我有时候在给客户做项目的时候就想骂客户是sb.可是请你相信我,等你有需求,自己变成客户的时候,给你做项目的哥哥肯定也会骂你是sb. 是这种,客户须要做一个图片上传的功能,这个图片须要裁剪.一般而言,这东西用系统自带的裁剪就搞定了.但是客户不,他要能够自由裁剪,就是长宽比不固定,想裁成什么比例就裁成什么比例,我一听,蛋都碎了. 没有办法,客户sb归sb,需求还是得照做,不然不给钱要喝西北风了. 图片裁剪的

Android图片裁剪——自定义裁剪工具

上次弄完调用系统裁剪之后,我又试着做一个自定义的裁剪工具. 老习惯,文章开始前还是先把我参考的资料贴出来.您愿意节省点时间看别人的更好的就直接从下面链接跳走-愿意看看我怎么做的那就先谢谢了! GitHub上老外做的一个非常棒的demo,代码也很漂亮 android自定义view实现裁剪图片功能,不使用系统的 第一个链接代码写的太好了,不过很多我用不上,也不需要那么麻烦的文件结构:第二个代码比较简单,但有些地方还是有借鉴意义的. 下面是我的代码,时间紧,就先不写太详细了: 注意几点: 我是在平板上

Android图片裁剪解决方案 -- 从相册截图

在Android开发中,可以轻松调用一个Intent完成从相册中截图的工作: Intent intent = new Intent(Intent.ACTION_GET_CONTENT, null); intent.setType("image/*"); intent.putExtra("crop", "true"); 附加选项如下: 选项 数据类型 描述 crop String 发送裁剪信号 aspectX int X方向上的比例 aspectY

android图片裁剪截取中间正方形部分

在做相册应用的过程中,需要得到一个压缩过的缩略图但,同时我还希望得到的bitmap能够是正方形的,以适应正方形的imageView,传统设置inSampleSize压缩比率的方式只是压缩了整张图片,如果一个图片的长宽差距较大,则展示出来的时候会有拉伸的现象,因此正确的做法是在压缩之后,对bitmap进行裁剪. 代码如下: 给定图片维持宽高比缩放后,截取正中间的正方形部分 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

Android 图片裁剪

参考文章: http://www.linuxidc.com/Linux/2012-11/73939p2.htm http://www.linuxidc.com/Linux/2012-11/73940p3.htm http://blog.csdn.net/yzwlord/article/details/8274131 http://blog.csdn.net/floodingfire/article/details/8144617

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

拍照--裁剪,或者是选择图片--裁剪,是我们设置头像或上传图片时经常需要的一组操作.上篇讲了Camera的使用,这篇讲一下我对图片裁剪的实现. 背景 下面的需求都来自产品. 裁剪图片要像微信那样,拖动和放大的是图片,裁剪框不动. 裁剪框外的内容要有半透明黑色遮罩. 裁剪框下面要显示一行提示文字(这点我至今还是持保留意见的). 在Android中,裁剪图片的控件库还是挺多的,特别是github上比较流行的几个,都已经进化到比较稳定的阶段,但比较遗憾的是它们的裁剪过程是拖动或缩放裁剪框,于是只好自己

Android大图片裁剪终极解决方案 原理分析

约几个月前,我正为公司的APP在Android手机上实现拍照截图而烦恼不已. 上网搜索,确实有不少的例子,大多都是抄来抄去,而且水平多半处于demo的样子,可以用来讲解知识点,但是一碰到实际项目,就漏洞百出. 当时我用大众化的解决方案,暂时性的做了一个拍照截图的功能,似乎看起来很不错.问题随之而来,我用的是小米手机,在别的手机上都运行正常,小米这里却总是碰钉子.虽然我是个理性的米粉,但是也暗地里把小米的工程师问候了个遍.真是惭愧! 翻文档也找不出个答案来,我一直对com.android.came

腾讯大牛动态教学:Android 仿微信 QQ 图片裁剪,赶紧收藏起来!

前言 在平时开发中,经常需要实现这样的功能,拍照 - 裁剪,相册 - 裁剪.当然,系统也有裁剪的功能,但是由于机型,系统兼容性等问题,在实际开发当中,我们通常会自己进行实现.今天,就让我们一起来看看怎样实现. 这篇博客实现的功能主要有仿微信,QQ 上传图像裁剪功能,包括拍照,从相册选取.裁剪框的样式有圆形,正方形,九宫格. 主要讲解的功能点 使用说明 整体的实现思路 裁剪框的实现 图片缩放的实现,包括放大,缩小,移动,裁剪等 我们先来看看我们实现的效果图 使用说明 有两种调用方式 第一种 第一种