前段时间做用户头像设置与上次,由于设计需要,将用户头像裁剪成圆形并设置上传.感觉裁剪成圆形图片的功能,以后很可能会用到,加之网上这一类的demo总结注释的不好,于是自己就做了demo并写好注释上传.

废话少说,先上图:

ios开发 图片裁剪 圆形 app裁剪圆形_android圆形图片裁剪

ios开发 图片裁剪 圆形 app裁剪圆形_ide_02



关键代码:

一:调用系统摄像头拍照后获取图片然后裁剪流程

1:调用系统摄像头拍照

/**
 * 打开系统摄像头拍照获取图片
 */
private void openCamera() {
    String state = Environment.getExternalStorageState();
    if (state.equals(Environment.MEDIA_MOUNTED)) {
        Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);//设置Action为拍照

        if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) {
            imageUri = Uri.fromFile(headIconFile);
        } else {
            //FileProvider为7.0新增应用间共享文件,在7.0上暴露文件路径会报FileUriExposedException
            //为了适配7.0,所以需要使用FileProvider,具体使用百度一下即可
            imageUri = FileProvider.getUriForFile(this,
                    "com.channelst.headimgclip.fileprovider", headIconFile);//通过FileProvider创建一个content类型的Uri
        }
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); //添加这一句表示对目标应用临时授权该Uri所代表的文件
        intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
        startActivityForResult(intent, TAKE_PHOTO_REQUEST_CODE);
        Log.e(TAG, "openCamera()---intent" + intent);
    }
}

2.调用系统摄像头返回,进入case TAKE_PHOTO_REQUEST_CODE

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    Log.e(TAG, "onActivityResult()---requestCode" + requestCode
            + ", resultCode : " + resultCode);
    switch (requestCode) {
        case CLIP_PHOTO_BY_SYSTEM_REQUEST_CODE:
            Log.d(TAG,"调用系统剪辑照片后返回.........");
            if (resultCode == RESULT_OK) {
                Bitmap bm = BitmapFactory.decodeFile(headClipFile.getAbsolutePath());

                headImg.setImageBitmap(bm);
                Log.e(TAG, "onActivityResult()---bm : " + bm);
            } else {
                Log.e(TAG, "onActivityResult()---resultCode : " + resultCode);
            }
            break;
        case TAKE_PHOTO_REQUEST_CODE:
            Log.i(TAG,"拍照后返回.........");
            if (resultCode == RESULT_OK) {
                //拍照后返回,调用系统裁剪,系统裁剪无法裁剪成圆形
                //clipPhotoBySystem(imageUri);
                //调用自定义裁剪
                clipPhotoBySelf(headIconFile.getAbsolutePath());
            }
            break;
        case CHOOSE_PHOTO_REQUEST_CODE:
            Log.i(TAG, "从相册选取照片后返回....");
            if (resultCode == RESULT_OK) {

                if (data != null) {
                    String filePath = "";
                    Uri originalUri = data.getData(); // 获得图片的uri
                    Log.i(TAG, "originalUri : " + originalUri);
                    if (originalUri != null) {
                        filePath = GetImagePath.getPath(this,originalUri);
                    }
                    Log.i(TAG, "filePath : " + filePath);

                    if (filePath != null && filePath.length() > 0) {
                        //clipPhotoBySystem(originalUri);
                        //调用自定义裁剪
                        clipPhotoBySelf(filePath);
                    }
                }

            }
            break;
        case CLIP_PHOTO_BY_SELF_REQUEST_CODE:
            Log.i(TAG, "从自定义切图返回..........");
            if (resultCode == RESULT_OK) {

                Bitmap bm = BitmapFactory.decodeFile(headClipFile.getAbsolutePath());
                headImg.setImageBitmap(bm);
                Log.i(TAG, "onActivityResult()---bm : " + bm);

            } else {
                Log.i(TAG, "onActivityResult()---resultCode : " + resultCode);
            }
            break;
    }
}

3.调用自定义裁剪方法,进入ClipPitctureActivity,如图一界面

/**
 * 调用自定义切图方法
 *
 * @param filePath
 */
protected void clipPhotoBySelf(String filePath) {
    Log.i(TAG, "通过自定义方式去剪辑这个照片");
    //进入裁剪页面,此处用的是自定义的裁剪页面而不是调用系统裁剪
    Intent intent = new Intent(this, ClipPictureActivity.class);
    intent.putExtra(ClipPictureActivity.IMAGE_PATH_ORIGINAL, filePath);
    intent.putExtra(ClipPictureActivity.IMAGE_PATH_AFTER_CROP,
            headClipFile.getAbsolutePath());

    startActivityForResult(intent, CLIP_PHOTO_BY_SELF_REQUEST_CODE);

}

4.在ClipPitctureActivity里,在主要布局加载完成后,加入一个自定义的ClipView,该View主要是在图片上方覆盖一层幕布,然后从中间抠出一个圆形

我们先来看clipView的初始化过程:

/**
 * 初始化截图区域,并将源图按裁剪框比例缩放
 *
 */
private void initClipView() {

    Intent intent = getIntent();
    final String originalImgPath = intent.getStringExtra(IMAGE_PATH_ORIGINAL);
    croppedImagePath = intent.getStringExtra(IMAGE_PATH_AFTER_CROP);

    // 首先判断源文件是否存在--防止垃圾数据的影响:
    // 一张图片在SD卡上已经被删除,但是媒体库中还有该数据。
    File file = new File(originalImgPath);
    if (!file.exists()) {
        Toast.makeText(this, "源文件在SD卡上不存在", Toast.LENGTH_SHORT).show();
        finish();
        return;
    }

    ......

    //初始化截图区域自定义view
    clipview = new ClipView(ClipPictureActivity.this);
    clipview.addOnDrawCompleteListener(new ClipView.OnDrawListenerComplete() {

        public void onDrawComplete() {
            clipview.removeOnDrawCompleteListener();
            int radius = (int) clipview.getRadius();
            int midX = (int) clipview.getCircleCenterPX();
            int midY = (int) clipview.getCircleCenterPY();

            int imageWidth = bitmap.getWidth();
            int imageHeight = bitmap.getHeight();
            // 按裁剪框求缩放比例
            float scale = (radius * 3.0f) / imageWidth;

            // 起始中心点
            float imageMidX = imageWidth * scale / 2;
            float imageMidY = imageHeight * scale / 2;
            srcPic.setScaleType(ImageView.ScaleType.MATRIX);

            // 缩放
            matrix.postScale(scale, scale);
            // 平移
            matrix.postTranslate(midX - imageMidX, midY - imageMidY);

            srcPic.setImageMatrix(matrix);
            srcPic.setImageBitmap(bitmap);
        }
    });

    matrix.reset();
    srcLayout.addView(clipview, new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));

}

再来看clipView的onDraw(),如下,onDraw里是先画一个矩形幕布,然后抠出一个圆来,最后画一个白色的边框

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);

    int width = this.getWidth();
    int height = this.getHeight();

    //画矩形region1
    canvas.clipRect(0, 0, width, height);

    //竖屏的时候width<height,取width的1/3作为半径
    // 横屏的时候width>height,取height的1/3作为半径
    int shortWidth = width<height?width:height;

    //画圆形region2
    Path path = new Path();
    circleCenterPX = (float) width/2.0f;
    circleCenterPY = (float) height/2.0f;

    radius = shortWidth/3.0f;
    path.addCircle(circleCenterPX, circleCenterPY, radius, Path.Direction.CCW);
    Log.i("ClipView", "onDraw()--circleCenterPX : " + circleCenterPX
            + ", circleCenterPY : " + circleCenterPY + ", radius : " + radius);
    Log.i("ClipView", "onDraw()--width : " + width + ", height : " + height);
    //path.addCircle(150,150,100, Path.Direction.CCW);
    //XOP表示补集就是全集的减去交集剩余部分,这剩余部分不用遮罩
    //也就相当于从遮罩里抠出一个圆形来
    canvas.clipPath(path, Region.Op.XOR);
    //canvas.clipRect(0,0,400,400);
    paint.setAlpha(((int)(255*0.4f)));
    canvas.drawRect(0, 0, width, height,paint);
    canvas.save();
    canvas.restore();

    // 画圆形边框
    borderPaint.setStyle(Paint.Style.STROKE);
    borderPaint.setAntiAlias(true);
    borderPaint.setColor(Color.WHITE);
    borderPaint.setStrokeWidth(clipBorderWidth);
    canvas.drawCircle(circleCenterPX, circleCenterPY, radius,borderPaint);

    clipWidth = clipHeight = (int) (radius*2);

    if (listenerComplete != null) {
        listenerComplete.onDrawComplete();
    }
}

5.接下来我们看保存图片的过程.

1)获取图片中的包含圆的矩形,2)把获取的矩形选取中间的圆形,也就是getCircleBitmap()方法

/**
 * @return 裁剪后的图片
 * @description 获取裁剪框内截图
 */
private Bitmap getBitmap() {

    try {
        // srcPic.getDrawingCache()获取View截图在某些情况下报错了。
        // 现在用一种新的获取view中图像的方法取代getDrawingCache()方法.
        // 另:在使用createBitmap()增加try..catch..以防止不断生成bitmap可能导致的oom
        int startX = (int) (clipview.getCircleCenterPX() - clipview.getRadius());
        int startY = (int) (clipview.getCircleCenterPY() - clipview.getRadius());
        Log.i(TAG, "getBitmap():startX=" + startX
                + ",startY=" + startY
                + ",clipview.getClipWidth()=" + clipview.getClipWidth()
                + ",clipview.getWidth()=" + clipview.getWidth()
                + ",clipview.getCircleCenterPX()=" + clipview.getCircleCenterPX()
                + ",clipview.getRadius()=" + clipview.getRadius()
                + ",clipview.getCircleCenterPY()=" + clipview.getCircleCenterPY());
        Bitmap finalBitmap = Bitmap.createBitmap(
                loadBitmapFromView(srcPic),
                startX, startY, clipview.getClipWidth(),
                clipview.getClipHeight());
        // 释放资源
        srcPic.destroyDrawingCache();
        Log.i(TAG, "getBitmap()  finalBitmap=" + finalBitmap);
        return getCircleBitmap(finalBitmap);
    } catch (OutOfMemoryError err) {
        Toast.makeText(this, "保存头像失败", Toast.LENGTH_SHORT).show();
        Log.e(TAG, err.getMessage());
        return null;
    } catch (Exception e) {
        Toast.makeText(this, "保存头像失败!", Toast.LENGTH_SHORT).show();
        Log.e(TAG, e.getMessage());
        return null;
    }

}


/**
 * @description 获取圆形裁剪框内截图
 * @param bitmap src图片
 * @return
 */
public static Bitmap getCircleBitmap(Bitmap bitmap) {
    Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(),
            Bitmap.Config.ARGB_8888);
    Canvas canvas = new Canvas(output);

    final int color = 0xff424242;
    final Paint paint = new Paint();
    final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());

    paint.setAntiAlias(true);
    paint.setFilterBitmap(true);
    paint.setDither(true);
    canvas.drawARGB(0, 0, 0, 0);
    paint.setColor(color);
    //在画布上绘制一个圆 -1是为了去掉白色的边框
    canvas.drawCircle(bitmap.getWidth() / 2, bitmap.getHeight() / 2,
            bitmap.getWidth() / 2 - 1, paint);
    paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
    canvas.drawBitmap(bitmap, rect, rect, paint);
    Log.i(TAG, "getCircleBitmap()     output=" + output);
    return output;
}

6.将圆形bitmap通过io流写入到图片文件中,如下


/**
 * @return void
 * @Title: saveMyBitmap
 * @Description: 保存bitmap对象到裁剪后的文件中
 */
public static void saveMyBitmap(File file, Bitmap mBitmap) {
    try {
        if (!file.getParentFile().exists()) {
            file.getParentFile().mkdirs();
        }
        if (!file.exists()) {
            file.createNewFile();
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    FileOutputStream fOut = null;
    try {
        fOut = new FileOutputStream(file);
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    }
    mBitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
    try {
        fOut.flush();
    } catch (IOException e) {
        e.printStackTrace();
    }
    try {
        fOut.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}




7.通过setResutl,返回ok

/**
 * 保存图片
 * 首先获取裁剪框里的图片
 * 然后保存到裁剪文件croppedImagePath中
 */
private void saveBitmap() {
    if (bitmap != null) {
        // 取出裁剪图片
        Bitmap clipBitmap = getBitmap();
        Log.i(TAG, "saveBitmap()  clipBitmap=" + clipBitmap);
        File file = new File(croppedImagePath);
        saveMyBitmap(file, clipBitmap);
    }

    Intent intent = new Intent();
    ClipPictureActivity.this.setResult(RESULT_OK, intent);
    finish();
}

8.这时候Mainactivity就收到CLIP_PHOTO_BY_SELF_REQUEST_CODE这个返回,在OnActivityResult即会进入该case

然后headImg这个imageView就将其显示出来.如图二


二:从图库中选择图片然后裁剪流程

从图库中选择图片然后裁剪流程和拍照获取图片裁剪流程差不多,差异只在于图片的获取方式

这里只介绍一下调用系统图库的代码:

点击"从相册选择"这个button即触发如下方法,启动系统相册


/**
 * 从系统图库中选择图片
 */
private void choosePhoto() {
    String state = Environment.getExternalStorageState();
    if (state.equals(Environment.MEDIA_MOUNTED)) {

        Intent openAlbumIntent = new Intent(Intent.ACTION_GET_CONTENT);
        openAlbumIntent.setType(IMAGE_TYPE);
        startActivityForResult(openAlbumIntent, CHOOSE_PHOTO_REQUEST_CODE);

    }
}

选择图片后返回界面即进入CHOOSE_PHOTO_REQUEST_CODE这个case,然后调用自定义裁剪,之后的过程与上面一致了.


最后,把源码上传一下,希望对别人对自己也有帮助.

demo链接



                                                                           @author 北京青牛软件南方基地_码农crst_luo 2017-11-06