美文网首页Android开发Android开发经验谈Android技术知识
腾讯大牛动态教学:Android 仿微信 QQ 图片裁剪,赶紧收

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

作者: jett老师 | 来源:发表于2020-04-14 14:45 被阅读0次

    前言

    在平时开发中,经常需要实现这样的功能,拍照 - 裁剪,相册 - 裁剪。当然,系统也有裁剪的功能,但是由于机型,系统兼容性等问题,在实际开发当中,我们通常会自己进行实现。今天,就让我们一起来看看怎样实现。

    这篇博客实现的功能主要有仿微信,QQ 上传图像裁剪功能,包括拍照,从相册选取。裁剪框的样式有圆形,正方形,九宫格。

    主要讲解的功能点

    1. 使用说明
    2. 整体的实现思路
    3. 裁剪框的实现
    4. 图片缩放的实现,包括放大,缩小,移动,裁剪等

    我们先来看看我们实现的效果图

    使用说明

    有两种调用方式

    第一种

    第一种,使用普通的 startActivityForResult 进行调用,并重写 onActivityResult 方法,在里面根据 requestCode 进行处理

     1ClipImageActivity.goToClipActivity(this, uri);
     2@Override
     3protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
     4    switch (requestCode) {
     5        case REQ_CLIP_AVATAR:  //剪切图片返回
     6            if (resultCode == RESULT_OK) {
     7                final Uri uri = intent.getData();
     8                if (uri == null) {
     9                    return;
    10                }
    11                String cropImagePath = FileUtil.getRealFilePathFromUri(getApplicationContext(), uri);
    12
    13
    14    ----
    15
    16}
    

    第二种

    第二种调用 ClipImageActivity.goToClipActivity 方法,结果以 callBack 回调的方式返回回来,这种看起来比较直观点,个人也比较喜欢这种方法。它的实现原理是通过空白的 fragment 处理实现的,有兴趣的可以看我这一篇博客 Android Fragment 的妙用 - 优雅地申请权限和处理 onActivityResult

    1ClipImageActivity.goToClipActivity(this, uri, new ActivityResultHelper.Callback() {
    2    @Override
    3    public void onActivityResult(int resultCode, Intent data) {
    4
    5    }
    6});
    

    整体实现思路

    从上面的效果图我们可以看到,裁剪功能主要包括两大块

    1. 裁剪框
    2. 图片的缩放,移动,裁剪等

    因此,为了方便日后的修改,我们将裁剪框的功能单独提取出来,图片缩放功能提出出来。即裁剪框单独一个 View。

    下面,让我们一起来看看裁剪框功能的实现。

    裁剪框功能的实现

    裁剪框主要有两层,第一层,裁剪框的实现(包括圆形,长方形,九宫格形状),第二层,在裁剪区域上面盖上一层蒙层。

    蒙层

    蒙层的实现我们是通过 Xfermode 实现的

    1xfermode = new PorterDuffXfermode(PorterDuff.Mode.DST_OUT);
    2
    3
    4//通过Xfermode的DST_OUT来产生中间的透明裁剪区域,一定要另起一个Layer(层)
    5canvas.saveLayer(0, 0, this.getWidth(), this.getHeight(), null, Canvas.ALL_SAVE_FLAG);
    6
    7//设置背景
    8canvas.drawColor(Color.parseColor("#a8000000"));
    9paint.setXfermode(xfermode);
    

    圆形裁剪框的实现

    绘制圆形裁剪框很容易实现,主要确定圆心和半径即可

    1//中间的透明的圆
    2canvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, clipRadiusWidth, paint);
    3//白色的圆边框
    4canvas.drawCircle(this.getWidth() / 2, this.getHeight() / 2, clipRadiusWidth, borderPaint);
    

    正方形裁剪框的实现

    绘制长方形的话主要要确定四个点的坐标 left ,top, right, botom。很简单

    1left = mHorizontalPadding;
    2top = this.getHeight() / 2 - clipWidth / 2;
    3right =   this.getWidth() - mHorizontalPadding;
    4botom = this.getHeight() / 2 + clipWidth / 2;
    
    1//绘制中间白色的矩形蒙层
    2canvas.drawRect(mHorizontalPadding, this.getHeight() / 2 - clipWidth / 2,
    3        this.getWidth() - mHorizontalPadding, this.getHeight() / 2 + clipWidth / 2, paint);
    4//绘制白色的矩形边框
    5canvas.drawRect(mHorizontalPadding, this.getHeight() / 2 - clipWidth / 2,
    6        this.getWidth() - mHorizontalPadding, this.getHeight() / 2 + clipWidth / 2, borderPaint);
    

    九宫格的

    九宫格的绘制稍微繁琐一点,分三个步骤

    1. 绘制长方形边框
    2. 绘制九宫格引导线
    3. 绘制裁剪边框的是个直角

    绘制长方形边框的这里就不说了,比较简单。

    我们来看一下绘制九宫格引导线的

    • 绘制竖直方向两条线
    • 绘制水平方向两条线
     1private void drawGuidelines(@NonNull Canvas canvas, Rect clipRect) {
     2
     3    final float left = clipRect.left;
     4    final float top = clipRect.top;
     5    final float right = clipRect.right;
     6    final float bottom = clipRect.bottom;
     7
     8    final float oneThirdCropWidth = (right - left) / 3;
     9
    10    final float x1 = left + oneThirdCropWidth;
    11    //引导线竖直方向第一条线
    12    canvas.drawLine(x1, top, x1, bottom, mGuidelinePaint);
    13    final float x2 = right - oneThirdCropWidth;
    14    //引导线竖直方向第二条线
    15    canvas.drawLine(x2, top, x2, bottom, mGuidelinePaint);
    16
    17    final float oneThirdCropHeight = (bottom - top) / 3;
    18
    19    final float y1 = top + oneThirdCropHeight;
    20    //引导线水平方向第一条线
    21    canvas.drawLine(left, y1, right, y1, mGuidelinePaint);
    22    final float y2 = bottom - oneThirdCropHeight;
    23    //引导线水平方向第二条线
    24    can
    

    绘制四个直角的

     1private void drawCorners(@NonNull Canvas canvas, Rect clipRect) {
     2
     3    final float left = clipRect.left;
     4    final float top = clipRect.top;
     5    final float right = clipRect.right;
     6    final float bottom = clipRect.bottom;
     7
     8    //简单的数学计算
     9    final float lateralOffset = (mCornerThickness - mBorderThickness) / 2f;
    10    final float startOffset = mCornerThickness - (mBorderThickness / 2f);
    11
    12    //左上角左面的短线
    13    canvas.drawLine(left - lateralOffset, top - startOffset, left - lateralOffset, top + mCornerLength, mCornerPaint);
    14    //左上角上面的短线
    15    canvas.drawLine(left - startOffset, top - lateralOffset, left + mCornerLength, top - lateralOffset, mCornerPaint);
    16
    17    //右上角右面的短线
    18    canvas.drawLine(right + lateralOffset, top - startOffset, right + lateralOffset, top + mCornerLength, mCornerPaint);
    19    //右上角上面的短线
    20    canvas.drawLine(right + startOffset, top - lateralOffset, right - mCornerLength, top - lateralOffset, mCornerPaint);
    21
    22    //左下角左面的短线
    23    canvas.drawLine(left - lateralOffset, bottom + startOffset, left - lateralOffset, bottom - mCornerLength, mCornerPaint);
    24    //左下角底部的短线
    25    canvas.drawLine(left - startOffset, bottom + lateralOffset, left + mCornerLength, bottom + lateralOffset, mCornerPaint);
    26
    27    //右下角左面的短线
    28    canvas.drawLine(right + lateralOffset, bottom + startOffset, right + lateralOffset, bottom - mCornerLength, mCornerPaint);
    29    //右下角底部的短线
    30    canvas.drawLine(right + startOffset, bottom + lateralOffset, right - mCornerLength, bottom + lateralOffset, mCornerPaint);
    31}
    

    图片裁剪框的实现到此讲解完毕,更多细节请参考 ClipView

    图片的缩放,移动

    实现原理简述

    这里我们是通过 ClipViewLayout 实现的,它是 RelativeLayout 的子类,里面含有 ImageView 和 ClipView(裁剪框)。我们通过监听 ClipViewLayout 的 onTouchEvent 事件,设置 imageView 的图片矩阵。

    我们先来了解一下,主要有三种模式,NONE,DRAG, ZOOM。NONE 表示初始模式,DRAG 表示拖拽模式,ZOOM 表示缩放模式

    1private static final int NONE = 0;
    2//动作标志:拖动
    3private static final int DRAG = 1;
    4//动作标志:缩放
    5private static final int ZOOM = 2;
    

    当我们多个手指按下的时候,加入两个手指之间的距离超过 10,此时我们认为进入 ZOOM 模式。DRAG 模式的话当我们手指按下的时候进入。NONE 模式,当我们手机抬起的时候,进入复位模式。

     1switch (event.getAction() & MotionEvent.ACTION_MASK) {
     2    case MotionEvent.ACTION_DOWN:
     3        Log.d(TAG, "onTouchEvent: ACTION_DOWN");
     4        mSavedMatrix.set(mMatrix);
     5        //设置开始点位置
     6        mStart.set(event.getX(), event.getY());
     7        mode = DRAG;
     8        break;
     9    case MotionEvent.ACTION_POINTER_DOWN:
    10        //开始放下时候两手指间的距离
    11        mOldDist = spacing(event);
    12        if (mOldDist > 10f) {
    13            mSavedMatrix.set(mMatrix);
    14            midPoint(mMid, event);
    15            mode = ZOOM;
    16        }
    17        break;
    18    case MotionEvent.ACTION_UP:
    19    case MotionEvent.ACTION_POINTER_UP:
    20        mode = NONE;
    21        break;
    

    接下来我们一起来看一下,我们 action_move 的时候,我们是怎样进行移动和缩放的。

    移动的话相对比较简单,首先它会计算出我们这一次 event 事件相对我们 action_down 时候 event 事件的偏移量 dx, dy。接着调用 mMatrix.postTranslate(dx, dy),进行矩阵的移动。最后,再检测是否超出边界。

     1case MotionEvent.ACTION_MOVE:
     2    Log.d(TAG, "onTouchEvent: mode =" + mode);
     3    if (mode == DRAG) { //拖动
     4        mMatrix.set(mSavedMatrix);
     5        float dx = event.getX() - mStart.x;
     6        float dy = event.getY() - mStart.y;
     7
     8        mVerticalPadding = mClipView.getClipRect().top;
     9        mMatrix.postTranslate(dx, dy);
    10        //检查边界
    11        checkBorder();
    12    } 
    13    ---
    14    mImageView.setImageMatrix(mMatrix);
    

    边界检测 主要是检查缩放,移动后的图片矩阵的 left, top,right, bottom 是否在图片裁剪框之内,如果在的话,需要对图片矩阵进行移动。确保边界不在裁剪框之内。

     1/**
     2 * 边界检测
     3 */
     4private void checkBorder() {
     5    RectF rect = getMatrixRectF(mMatrix);
     6    float deltaX = 0;
     7    float deltaY = 0;
     8    int width = mImageView.getWidth();
     9    int height = mImageView.getHeight();
    10    // 如果宽或高大于屏幕,则控制范围 ; 这里的0.001是因为精度丢失会产生问题,但是误差一般很小,所以我们直接加了一个0.01
    11    if (rect.width() + 0.01 >= width - 2 * mHorizontalPadding) {
    12        // 图片矩阵的最左边 > 裁剪边框的左边
    13        if (rect.left > mHorizontalPadding) {
    14            deltaX = -rect.left + mHorizontalPadding;
    15        }
    16        // 图片矩阵的最右边 < 裁剪边框的右边
    17        if (rect.right < width - mHorizontalPadding) {
    18            deltaX = width - mHorizontalPadding - rect.right;
    19        }
    20    }
    21    if (rect.height() + 0.01 >= height - 2 * mVerticalPadding) {
    22        // 图片矩阵的 top > 裁剪边框的 top
    23        if (rect.top > mVerticalPadding) {
    24            deltaY = -rect.top + mVerticalPadding;
    25        }
    26        // 图片矩阵的 bottom < 裁剪边框的 bottom
    27        if (rect.bottom < height - mVerticalPadding) {
    28            deltaY = height - mVerticalPadding - rect.bottom;
    29        }
    30    }
    31
    32    Log.i(TAG, "checkBorder: deltaX=" + deltaX + " deltaY = " + deltaY);
    33
    34    mMatrix.postTranslate(deltaX, deltaY);
    35}
    

    裁剪功能的实现

     1/**
     2 * 获取剪切图
     3 */
     4public Bitmap clip() {
     5    mImageView.setDrawingCacheEnabled(true);
     6    mImageView.buildDrawingCache();
     7    Rect rect = mClipView.getClipRect();
     8    Bitmap cropBitmap = null;
     9    Bitmap zoomedCropBitmap = null;
    10    try {
    11        cropBitmap = Bitmap.createBitmap(mImageView.getDrawingCache(), rect.left, rect.top, rect.width(), rect.height());
    12        // 对图片进行压缩,这里因为 mPreViewW 与宽高是相等的,所以压缩比例是 1:1,可以根据需要自己调整
    13        zoomedCropBitmap = BitmapUtil.zoomBitmap(cropBitmap, mPreViewW, mPreViewW);
    14    } catch (Exception e) {
    15        e.printStackTrace();
    16    }
    17    if (cropBitmap != null) {
    18        cropBitmap.recycle();
    19    }
    20    // 释放资源
    21    mImageView.destroyDrawingCache();
    22    return zoomedCropBitmap;
    23}
    

    题外话

    这个 Demo 涉及到的 Android 技术点其实是蛮多的,可以说是麻雀虽小,五脏俱全。Android 7.0 图片拍照适配,6.0 动态权限申请,Android 使用空白 fragment 处理 onActivityResult,动态权限申请,自定义 View,View 的事件分发机制等等。

    这篇博客主要是介绍个人认为比较重要的技术点,其他的可以自行取了解。最后,提供一下 demo 下载地址:https://github.com/gdutxiaoxu/clipimagea

    文章不易,如果大家喜欢这篇文章,或者对你有帮助希望大家多多,点赞转发关注。文章会持续更新的。绝对干货!!!

    相关文章

      网友评论

        本文标题:腾讯大牛动态教学:Android 仿微信 QQ 图片裁剪,赶紧收

        本文链接:https://www.haomeiwen.com/subject/axftvhtx.html