Android 仿微信裁剪图片

在很多App 中,需要注册登录,那么就免不了 设置用户的头像。头像无非就是方形 或者 圆形,那么就诞生了这样一个需求:

  • 从相册中选择一张图片
  • 中间区域是圆形 或者 方形的透明裁剪框
  • 裁剪框周围是阴影
  • 图片可以移动、缩放

网上有很多,包括Github上,但是绝大多数都是 移动裁剪框,而不是移动图片。但是最后还是找到了一个可以参考模仿的例子《Android开发技巧——定制仿微信图片裁剪控件》。这篇博客详细讲述了自定义裁剪控件的过程。以前我也是直接拿过来用的。直到某一天,产品跑过来说:“裁剪中间这个矩形框能不能改成圆的,因为我们的头像是圆的”。 于是我就开始改这个代码了,说不上优化,只是换一种思路去解决问题。

有图有真相

          

原作者定制内容

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

我的修改:

  • 去掉了底部提示文字(有需要可以自己加
  • 画遮罩和中间的裁剪框是 用到了 PorterDuffXfermode 方法
  • 在最后裁剪成头像时 也用到了上述方法
  • 圆形框或者矩形框 可以自由选择
  • 图片的边框可以滑倒裁剪框内部,松开手指时,会有一个回弹效果

自定义属相修改

 <attr name="civHeight" format="integer" />
        <attr name="civWidth" format="integer" />
        <attr name="civMaskColor" format="color" />
        <attr name="civClipPadding" format="dimension" />
        <attr name="civClipCircle" format="boolean" />
  1. civHegiht 和 civWidth 是中间矩形裁剪框的比值
  2. civMaskColor  是裁剪框外围的颜色,也成遮罩颜色
  3. civPadding  是裁剪框距离我们控件的边距

其他内容像 参数变量,构造方法等等,原作者都做了详细的描述,我这里就不再一一赘述了,接下来我们就重点讲讲 绘制裁剪框和遮罩 图片拖动双击缩放 以及绘制头像

绘制裁剪框

基本思路:在一个新建的 Canvas 上画一个 遮罩(bitmap),再用一个透明的画笔 设置 PorterDuffXfermode 为PorterDuff.Mode.CLEAR,在遮罩上画一个 矩形或圆形,这样一个中间为空的裁剪框 的遮罩就形成了。将遮罩画在所选图片上之上,别忘了 还要画一个 圆形框或者矩形框

/**
     * 画中间的边框(方形或是圆形)
     */
    public void drawRectangleOrCircle(Canvas canvas) {
        float cx = mClipBorder.left + mClipBorder.width() / 2f;
        float cy = mClipBorder.top + mClipBorder.height() / 2f;
        float radius = mClipBorder.height() / 2f;
        RectF rectF = new RectF(mClipBorder.left, mClipBorder.top, mClipBorder.right, mClipBorder.bottom);
        Bitmap bitmap = Bitmap.createBitmap(canvas.getWidth(), canvas.getHeight(), Bitmap.Config.ARGB_8888);
        Canvas temp = new Canvas(bitmap);
        Paint transparentPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        PorterDuffXfermode porterDuffXfermode = new PorterDuffXfermode(PorterDuff.Mode.CLEAR);
        transparentPaint.setColor(Color.TRANSPARENT);
        temp.drawRect(0, 0, temp.getWidth(), temp.getHeight(), mPaint);
        transparentPaint.setXfermode(porterDuffXfermode);
        if (mDrawCircleFlag) { // 画圆

            temp.drawCircle(cx, cy, radius, transparentPaint);
        } else { // 画矩形(可以设置矩形的圆角)

            temp.drawRect(rectF, transparentPaint);
        }
        canvas.drawBitmap(bitmap, 0, 0, null);

        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(Color.WHITE);
        mPaint.setStrokeWidth(DeviceUtil.dip2px(2));
        if (mDrawCircleFlag) {
            canvas.drawCircle(cx, cy, radius, mPaint);
        } else {
            canvas.drawRect(rectF, mPaint);
        }
    }

底部的遮罩层是目标图像,上面的裁剪形状是源图像,PorterDuff.Mode.CLEAR 起到了清空源图像所在区域图像的作用。

双击缩放

说到双击,我们必要要检测到手指检测,当然啦,我们不会傻到自己写一塌逻辑去检测用户是否是双击了屏幕。这里我们用到了这个GestureDetector 来监听用户手势,如果是双击,自然会走双击的回掉方法 public boolean onDoubleTap(MotionEvent e)

手势监听

setScaleType(ScaleType.MATRIX);
        mGestureDetector = new GestureDetector(context,
                new SimpleOnGestureListener() {
                    @Override
                    public 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;
                    }
                });

大家有没有注意到 这个 setScaleType(ScaleType.MATRIX) ,该方法就是设置 imageview 可以根据 矩阵去缩放。

缩放操作

 private class AutoScaleRunnable implements Runnable {
        static final float BIGGER = 1.07f;
        static final float SMALLER = 0.93f;
        private final float mTargetScale;
        private final float tmpScale;

        /**
         * 缩放的中心
         */
        private final float x;
        private final float y;

        /**
         * 传入目标缩放值,根据目标值与当前值,判断应该放大还是缩小
         */
        AutoScaleRunnable(float targetScale, float x, float y) {
            this.mTargetScale = targetScale;
            this.x = x;
            this.y = y;
            if (getScale() < mTargetScale) {
                tmpScale = BIGGER;
            } else {
                tmpScale = SMALLER;
            }

        }

        @Override
        public void run() {
            // 进行缩放
            mScaleMatrix.postScale(tmpScale, tmpScale, x, y);
            checkBorder();
            setImageMatrix(mScaleMatrix);

            final float currentScale = getScale();
            // 如果值在合法范围内,继续缩放
            if (((tmpScale > 1f) && (currentScale < mTargetScale))
                    || ((tmpScale < 1f) && (mTargetScale < currentScale))) {
                ClipImageView.this.postDelayed(this, 16);
            } else {
                // 设置为目标的缩放比例
                final float deltaScale = mTargetScale / currentScale;
                mScaleMatrix.postScale(deltaScale, deltaScale, x, y);
                checkBorder();
                setImageMatrix(mScaleMatrix);
                isAutoScale = false;
            }

        }
    }

这边是根据 放大倍数(1.07) 和缩小倍数(0.93) 去缩放的,是慢慢缩放,而不是一下子缩放,这样会很突兀。注意缩放完了以后记得去检查边界,因为在缩放后,图片的边界在裁剪框里面,所以需要再去移动图片 以达到适配裁剪框的目的

两个手指缩放

通过多指操作 来检测缩放,我们用到了这个监听类 ScaleGestureDetector

 @Override
    public boolean onScale(ScaleGestureDetector detector) {
        float scale = getScale();
        float scaleFactor = detector.getScaleFactor();

        if (getDrawable() == null)
            return true;
        //缩放的范围控制
        if ((scale < mScaleMax && scaleFactor > 1.0f)
                || (scale > mInitScale && scaleFactor < 1.0f)) {
            //缩放阙值最小值判断
            if (scaleFactor * scale < mInitScale) {
                scaleFactor = mInitScale / scale;
            }
            if (scaleFactor * scale > mScaleMax) {
                scaleFactor = mScaleMax / scale;
            }
            //设置缩放比例
            mScaleMatrix.postScale(scaleFactor, scaleFactor,
                    detector.getFocusX(), detector.getFocusY());
            checkBorder();
            setImageMatrix(mScaleMatrix);
        }
        return true;
    }

我们在控制缩放的时候,还是需要计算一下在不在我们的缩放倍数范围里。

移动图片

这边的设计是:

  • 如果图片的宽 或 高正好等于裁剪框的宽 或高,那么久移动不了图片
  • 放大后,移动图片,图片的边界可以移动到裁剪框里面,手指松开后,图片自动校正适配裁剪框
 @Override
    public boolean onTouch(View v, MotionEvent event) {
        if (mGestureDetector.onTouchEvent(event))
            return true;
        mScaleGestureDetector.onTouchEvent(event);

        float x = 0, y = 0;
        // 拿到触摸点的个数
        final int pointerCount = event.getPointerCount();

        // 得到多个触摸点的x与y均值
        for (int i = 0; i < pointerCount; i++) {
            x += event.getX(i);
            y += event.getY(i);
        }
        x /= pointerCount;
        y /= pointerCount;

        //每当触摸点发生变化时,重置mLasX , mLastY
        if (pointerCount != lastPointerCount) {
            isCanDrag = false;
            mLastX = x;
            mLastY = y;
        }

        lastPointerCount = pointerCount;
        switch (event.getAction()) {
            case MotionEvent.ACTION_MOVE:
                float dx = x - mLastX;
                float dy = y - mLastY;

                if (!isCanDrag) {
                    isCanDrag = isCanDrag(dx, dy);
                }
                if (isCanDrag) {
                    if (getDrawable() != null) {

                        RectF rectF = getMatrixRectF();
                        // 如果宽度小于屏幕宽度,则禁止左右移动
                        if ((int) rectF.width() <= mClipBorder.width()) {
                            dx = 0;
                        }

                        // 如果高度小于屏幕高度,则禁止上下移动
                        if ((int) rectF.height() <= mClipBorder.height()) {
                            dy = 0;
                        }

                        // 这里主要是 当宽或者高 大于 裁剪框的高或宽时,移动到与裁剪框边重合时,可以继续移动
                        if (rectF.left > mClipBorder.left + 1 || rectF.top > mClipBorder.top + 1 || rectF.right < mClipBorder.right - 1 || rectF.bottom < mClipBorder.bottom - 1) {
                            dx = dx * 0.25f;
                            dy = dy * 0.25f;
                        }
                        mScaleMatrix.postTranslate(dx, dy);
                        setImageMatrix(mScaleMatrix);
                    }
                }
                mLastX = x;
                mLastY = y;
                break;

            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                lastPointerCount = 0;

                // 当抬起手指时,如果划过了,没有填满裁剪框,就要自动弹回
                checkBorder();
                setImageMatrix(mScaleMatrix);

                break;
        }

        return true;
    }

裁剪头像

原著作者的思路:在矩阵中获取 图片的 缩放倍数,移动距离,进行相除和相加减,最后在原图上进行裁剪,这样有一个不好的地方,float 和 int 相互转换,会有1 px 的误差,而这个误差很有可能在裁剪时 崩溃,因为超出了图片的宽度,这是在实践中验证过了。

作为新时代的青年,我们当然要换一种想法,那就是 PorterDuffXfermode,只不过这里的model 是PorterDuff.Mode.SRC_IN。看上去还不错哦,与开头画裁剪框和遮罩 形成了 首尾呼应。

先在 新建的Canvas 上 画一个 透明的 Bitmap,中间画一个 填充型矩形,设置画笔 的Xfermode ,接着将变化后的bitmap 画上去,这样两个Bitmap 一合成,就在画布上形成了 周围是透明,只有中间矩形里 是图片,最后将矩形图片截图出来。

/**
     * 截图操作
     */
    public Bitmap clip() {
        final Drawable drawable = getDrawable();
        final Bitmap originalBitmap = ((BitmapDrawable) drawable).getBitmap();
        Bitmap bottomBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_4444);
        Canvas canvas = new Canvas(bottomBitmap);
        Paint paint = new Paint();
        paint.setAntiAlias(true);
        canvas.drawRect(mClipBorder, paint);
        paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
        canvas.drawBitmap(originalBitmap, mScaleMatrix, paint);
        return Bitmap.createBitmap(bottomBitmap, (int) mClipBorder.left, (int) mClipBorder.top, (int) mClipBorder.width(), (int) mClipBorder.height());

    }

效果演示

截图控件

Android 仿微信 头像裁剪

  1. Android开发技巧——定制仿微信图片裁剪控件
发布了6 篇原创文章 · 获赞 2 · 访问量 413

猜你喜欢

转载自blog.csdn.net/tocong2015/article/details/103367238