6. Android - 更换头像及图片裁剪(适配Android7.0)

6. Android - 更换头像及图片裁剪(适配Android7.0)

原创  2017年06月26日 17:35:09

一、概述

相信大家都用过 Android 应用中更换头像的功能,在这个功能中,用户可以拍照或者选择相册图片,然后裁剪出头像所需要的图案。 
那么你们有没有考虑过这个功能怎么实现的呢?今天就让我们一步步搞定这个功能,先看运行效果,这里选择了相册图片并设置头像。 
选择相册图片并裁剪.gif

点这里下载 Demo 的 src 文件 
下载之后,把里面的文件复制到你自己的项目中即可。

二、PopUpWindow 设计及弹出效果

1. 布局

优雅简洁的用户界面是吸引用户的开端,那先让我们设计一个漂亮的 PopUpWindow,如下所示: 
PopUpWindow 样式.png 
这个 PopUpWindow 里共有3个按钮,分别为“拍照”,“从相册选择”,以及“取消”。上面两个按钮连接在了一起,下方的“取消”与它们分开,那么这里就需要3种按钮样式:“拍照”按钮只有上方是圆角,“从相册选择”按钮只有下方是圆角,“取消”按钮四个角都是圆角。 
在 drawable 文件夹中新建下方3个 shape 文件。

1. white_btn

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white"/>
    <corners android:radius="10dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2. white_btn_top

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white" />
    <corners android:topLeftRadius="10dp"
             android:topRightRadius="10dp"
             android:bottomRightRadius="0dp"
             android:bottomLeftRadius="0dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3. white_btn_bottom

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white" />
    <corners android:topLeftRadius="0dp"
             android:topRightRadius="0dp"
             android:bottomRightRadius="10dp"
             android:bottomLeftRadius="10dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

有了这3个按钮样式,就可以写出 PopUpWindow 的布局了。 
在 colors.xml 中添加字体颜色 <color name="colorMainGreen">#40cab3</color> 
在 layout 中新建 pop_item.xml 布局,因为 PopUpWindow 弹出时,屏幕的背景会变灰,因此需要将布局的背景颜色设置为半透明灰色,颜色代码 #66000000

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#66000000">

    <LinearLayout
        android:id="@+id/ll_pop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:orientation="vertical"
        android:layout_alignParentBottom="true">
        <Button
            android:id="@+id/icon_btn_camera"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/white_btn_top"
            android:textColor="@color/colorMainGreen"
            android:text="拍照"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/background_gray"/>
        <Button
            android:id="@+id/icon_btn_select"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/white_btn_bottom"
            android:textColor="@color/colorMainGreen"
            android:text="从相册选择"/>
        <Button
            android:id="@+id/icon_btn_cancel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="15dp"
            android:background="@drawable/white_btn"
            android:textColor="@color/colorMainGreen"
            android:text="取消"/>
    </LinearLayout>

</RelativeLayout>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2. 动画效果

这里 PopUpWindow 的出现和消失使用淡入淡出的动画效果。 
在 res 中新建 anim 文件夹,在其中新建两个动画效果。

popup_show.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="300"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
</set>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

popup_gone.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="200"
        android:fromAlpha="1.0"
        android:toAlpha="0.0" />
</set>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在 styles.xml 中设置 PopUpWindow 整体的动画效果。

<style name="popwindow_anim_style">
        <item name="android:windowEnterAnimation">@anim/popup_show</item>
        <item name="android:windowExitAnimation">@anim/popup_gone</item>
</style>
    
    
  • 1
  • 2
  • 3
  • 4

3. PopupWindow 类

有了布局和动画效果,接下来就可以写 PopUpWindow 的工具类了,这个工具类负责接收外部的点击监听器,并设置点击弹窗外关闭弹窗。 
新建 PhotoPopupWindow 类,它的构造函数需要传入 “拍照” 和 “相册” 两个按钮的点击监听,具体代码如下:

public class PhotoPopupWindow extends PopupWindow {

    private View mView; // PopupWindow 菜单布局
    private Context mContext; // 上下文参数
    private View.OnClickListener mSelectListener; // 相册选取的点击监听器
    private View.OnClickListener mCaptureListener; // 拍照的点击监听器

    public PhotoPopupWindow(Activity context, View.OnClickListener selectListener, View.OnClickListener captureListener) {
        super(context);
        this.mContext = context;
        this.mSelectListener = selectListener;
        this.mCaptureListener = captureListener;
        Init();
    }

    /**
     * 设置布局以及点击事件
     */
    private void Init() {
        LayoutInflater inflater = (LayoutInflater) mContext
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mView = inflater.inflate(R.layout.pop_item, null);
        Button btn_camera = (Button) mView.findViewById(R.id.icon_btn_camera);
        Button btn_select = (Button) mView.findViewById(R.id.icon_btn_select);
        Button btn_cancel = (Button) mView.findViewById(R.id.icon_btn_cancel);

        btn_select.setOnClickListener(mSelectListener);
        btn_camera.setOnClickListener(mCaptureListener);
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });

        // 导入布局
        this.setContentView(mView);
        // 设置动画效果
        this.setAnimationStyle(R.style.popwindow_anim_style);
        this.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
        this.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
        // 设置可触
        this.setFocusable(true);
        ColorDrawable dw = new ColorDrawable(0x0000000);
        this.setBackgroundDrawable(dw);
        // 单击弹出窗以外处 关闭弹出窗
        mView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int height = mView.findViewById(R.id.ll_pop).getTop();
                int y = (int) event.getY();
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    if (y < height) {
                        dismiss();
                    }
                }
                return true;
            }
        });
    }
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

三、在 MainActivity 中设置头像

1. activity_main 布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.chen.lister.testchangeicon.MainActivity">

    <LinearLayout
        android:id="@+id/main_ll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal">
        <ImageView
            android:id="@+id/main_icon"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_marginTop="20dp"
            android:src="@mipmap/ic_launcher"/>
        <Button
            android:id="@+id/main_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:text="更换头像"/>
    </LinearLayout>
</LinearLayout>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

2. 弹出 PopUpWindow

回顾之前的 PopUpWindow 工具类,它的构造方法需要上下文以及两个点击事件的监听器,新建 PopUpWindow 之后就可以让它显示在屏幕下方中间。

main_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 进入相册选择
                    }
                }, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 拍照
                    }
                });
                View rootView = LayoutInflater.from(MainActivity.this).inflate(R.layout.activity_main, null);
                mPhotoPopupWindow.showAtLocation(rootView,
                        Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
            }
        });
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3. 拍照或选择图片并切割

在 MainActivity 中添加如下常量:

private static final int REQUEST_IMAGE_GET = 0;
private static final int REQUEST_IMAGE_CAPTURE = 1;
private static final int REQUEST_SMALL_IMAGE_CUTTING = 2;
private static final int REQUEST_BIG_IMAGE_CUTTING = 3;
private static final String IMAGE_FILE_NAME = "icon.jpg";
    
    
  • 1
  • 2
  • 3
  • 4
  • 5

先看在相册中选择图片,点击进入相册选择图片的按钮后,系统应该使用 startActivityForResult() 调用选择图片的 intent 并返回一个结果。

mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判断系统中是否有处理该 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivityForResult(intent, REQUEST_IMAGE_GET);
    } else {
    Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

返回的结果在 onActivityResult() 中处理,先通过 data.getData() 获取选择到的图片的 Uri,再通过 startSmallPhotoZoom() 对该图片进行裁剪。

/**
 * 处理回调结果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回调成功
    if (resultCode == RESULT_OK) {
        switch (requestCode) {

            // 小图切割
            case REQUEST_SMALL_IMAGE_CUTTING:
                if (data != null) {
                    setPicToView(data);
                }
                break;

            // 相册选取
            case REQUEST_IMAGE_GET:
                try {
                    startSmallPhotoZoom(data.getData());
                } catch (NullPointerException e) {
                    e.printStackTrace();
                }
                break;

            //......    
        }
    }
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

startSmallPhotoZoom() 方法如下,它会启动系统的裁剪界面进行裁剪并返回结果。它启动的 intent 中 “return-data” 为 true,意味着它裁剪完图片会直接将图片作为 bitmap 在内存中返回。 
如果你够细心,你就会发现它返回的结果也在上面 onActivityResult() 中调用 setPicToView() 方法处理了。

/**
 * 小图模式切割图片
 * 此方式直接返回截图后的 bitmap,由于内存的限制,返回的图片会比较小
 */
public void startSmallPhotoZoom(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 300); // 输出图片大小
    intent.putExtra("outputY", 300);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", true);
    startActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

setPicToView() 方法如下,它将裁剪后的图片保存到指定文件夹并设置到 ImageView 中。

/**
 * 小图模式中,保存图片后,设置到视图中
 */
private void setPicToView(Intent data) {
    Bundle extras = data.getExtras();
    if (extras != null) {
        Bitmap photo = extras.getParcelable("data"); // 直接获得内存中保存的 bitmap
        // 创建 smallIcon 文件夹
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String storage = Environment.getExternalStorageDirectory().getPath();
            File dirFile = new File(storage + "/smallIcon");
            if (!dirFile.exists()) {
                if (!dirFile.mkdirs()) {
                    Log.e("TAG", "文件夹创建失败");
                } else {
                    Log.e("TAG", "文件夹创建成功");
                }
            }
            File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
            // 保存图片
            FileOutputStream outputStream = null;
            try {
                outputStream = new FileOutputStream(file);
                photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                outputStream.flush();
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 在视图中显示图片
        main_icon.setImageBitmap(photo);
    }
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

解决了相册选图,再来看拍照。 
点击拍照的按钮,即调用系统的拍照功能。

mPhotoPopupWindow.dismiss();
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
          Uri.fromFile(new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME)));
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
    
    
  • 1
  • 2
  • 3
  • 4
  • 5

修改 onActivityResult() 函数,增加拍照返回的处理,最后同样调用 startSmallPhotoZoom() 函数进行裁剪。

/**
 * 处理回调结果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回调成功
    if (resultCode == RESULT_OK) {
        switch (requestCode) {

        // 小图切割
        case REQUEST_SMALL_IMAGE_CUTTING:
            if (data != null) {
                setPicToView(data);
            }
            break;

        // 相册选取
        case REQUEST_IMAGE_GET:
            try {
                startSmallPhotoZoom(data.getData());
            } catch (NullPointerException e) {
                e.printStackTrace();
            }
            break;

        // 拍照
        case REQUEST_IMAGE_CAPTURE:
            File temp = new File(Environment.getExternalStorageDirectory() + "/" + IMAGE_FILE_NAME);
            startSmallPhotoZoom(Uri.fromFile(temp));
            break;
        }
    }
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

四、大图片裁剪

设置完头像,再看之前保存的图片,你会发现它们都很模糊,那如果想裁剪出清晰的图片,该怎么做呢? 
还记得裁剪图片 Intent 中的这两个参数吗,它们就代表了输出图片的大小。

intent.putExtra("outputX", 300); // 输出图片大小
intent.putExtra("outputY", 300);
    
    
  • 1
  • 2

那么想提高图片的质量,是不是把这两个值加大就可以了呢? 
在回答这个问题之前,让我们先来了解一下裁剪后的图片是怎么返回的。 
假设现在有一张图片尺寸为 3200*2400px。也许你觉得返回这张图没什么问题,大不了耗1-2M的内存。不错,这个尺寸的图片确实只有1.8M左右的大小。但是你想不到的是,这个尺寸对应的 Bitmap 会耗光你应用程序的所有内存。Android出于安全性考虑,只会给你一个寒碜的缩略图。 
Android 中,默认 Bitmap 为 32 位,也就是说,一个像素点占用 4 个字节,那么之前我们说的图片需要占用多大的内存呢?3200*2400*4 bytes = 30M。 
整整30M!即使你想为一张只会存在几秒钟的图片消耗这么大的内存,Android 也不会答应的。

所以如果我们想提高裁剪图片的质量,可不是只加大输出的图片像素大小就可以的。那我们还应该做什么呢?先来看看裁剪图片的 Intent 可附带的参数,看看它们为我们提供了什么信息。

附带参数 数据类型 描述
crop String 发送裁剪信号
aspectX int X方向上的比例
aspectY int Y方向上的比例
outputX int 裁剪区的宽
outputY int 裁剪区的高
scale boolean 是否保留比例
return-data boolean 是否将数据保留在Bitmap中返回
data Parcelable 相应的Bitmap数据
circleCrop String 圆形裁剪区域?
MediaStore.EXTRA_OUTPUT (“output”) Uri 将URI指向相应的file:///…

在小图返回模式中,我们将 return-data 设置为了“true”,因此会在内存中直接返回一个 Bitmap,由于内存的原因,它将会是一个模糊的缩略图。 
如果将 return-data 设置为“false”,那么在 onActivityResult() 的 Intent 数据中你将不会接收到任何 Bitmap,相反,我们需要将 MediaStore.EXTRA_OUTPUT 关联到一个 Uri,此 Uri 是用来存放 Bitmap 的,那么裁剪后的图片就会保存到 sd 卡中。 
具体代码如下:

/**
 * 大图模式切割图片
 * 直接创建一个文件将切割后的图片写入
 */
public void startBigPhotoZoom(Uri uri) {
    // 创建大图文件夹
    Uri imageUri = null;
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
        String storage = Environment.getExternalStorageDirectory().getPath();
        File dirFile = new File(storage + "/bigIcon");
        if (!dirFile.exists()) {
            if (!dirFile.mkdirs()) {
                Log.e("TAG", "文件夹创建失败");
            } else {
                Log.e("TAG", "文件夹创建成功");
            }
        }
        File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
        imageUri = Uri.fromFile(file);
    }
    // 开始切割
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 600); // 输出图片大小
    intent.putExtra("outputY", 600);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", false); // 不直接返回数据
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一个文件
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

之后根据需求将图片设置到 ImageView 中或者上传服务器即可,这里不再赘述。

五、Android 新版本适配

上面的程序在 Android5.X 及以下可以正常运行,但是在 Android6.0 和 Android7.0 下运行时会崩溃。这是因为 Android6.0 需要程序动态申请权限,而 Android7.0 对 Uri 添加了保护。

1. 动态权限

在 Android6.0 之后,拍照和读取本地文件都需要在运行时动态申请权限。 
申请之前需要检查用户之前是否已经同意该权限。如果已经同意,则直接进行下一步操作。如果没有,则进行申请,成功后在回调方法 onRequestPermissionsResult() 中进行后续处理。

修改头像按钮的点击事件

main_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 拍照及文件权限申请
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                        || ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 权限还没有授予,进行申请
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申请的 requestCode 为 300
                } else {
                    // 权限已经申请,直接拍照
                    mPhotoPopupWindow.dismiss();
                    imageCapture();
                }
            }
        }, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 文件权限申请
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 权限还没有授予,进行申请
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申请的 requestCode 为 200
                } else {
                    // 如果权限已经申请过,直接进行图片选择
                    mPhotoPopupWindow.dismiss();
                    Intent intent = new Intent(Intent.ACTION_PICK);
                    intent.setType("image/*");
                    // 判断系统中是否有处理该 Intent 的 Activity
                    if (intent.resolveActivity(getPackageManager()) != null) {
                        startActivityForResult(intent, REQUEST_IMAGE_GET);
                    } else {
                        Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
        View rootView = LayoutInflater.from(MainActivity.this)
                .inflate(R.layout.activity_main, null);
        mPhotoPopupWindow.showAtLocation(rootView,
                Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
    }
});
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

权限申请的回调

/**
 * 处理权限回调结果
 */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case 200:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mPhotoPopupWindow.dismiss();
                Intent intent = new Intent(Intent.ACTION_PICK);
                intent.setType("image/*");
                // 判断系统中是否有处理该 Intent 的 Activity
                if (intent.resolveActivity(getPackageManager()) != null) {
                    startActivityForResult(intent, REQUEST_IMAGE_GET);
                } else {
                    Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                }
            } else {
                mPhotoPopupWindow.dismiss();
            }
            break;
        case 300:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mPhotoPopupWindow.dismiss();
                imageCapture();
            } else {
                mPhotoPopupWindow.dismiss();
            }
            break;
    }
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

2. Uri 保护

Android7.0 的官方文档是这么说的:

Passing file://URIs outside the package domain may leave the receiver with an unaccessible path. Therefore, attempts to pass a file:// URI trigger a FileUriExposedException. The recommended way to share the content of a private file is using the FileProvider.

什么意思呢?就是说,file:// 这样的 Uri 不能附着在 Intent 上,否则会引发 FileUriExposedException,官方建议使用 FileProvider 改变 Uri 的传递方式。 
在这个应用中,我们在调用相机并把拍摄的照片保存到手机本地时,如果传入 file:// 这样的 Uri 就会造成应用崩溃,因此需要使用 FileProvider,步骤如下。 
注:选择图片不会崩溃,因为选择图片后传入的 Uri 本身就是 Content Uri。

1. 在 res 下新建 xml 文件夹,其中新建 provider_paths.xml,代码如下

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 注意包名 -->
    <external-path path="Android/data/com.chen.lister.testchangeicon/" name="files_root" />
</paths>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5

2. 在 manifest 中进行声明

<!-- 注意包名 -->
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.chen.lister.testchangeicon.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3. 将拍照行为封装成 imageCapture() 方法

/**
 * 判断系统及拍照
 */
private void imageCapture() {
    Intent intent;
    Uri pictureUri;
    File pictureFile = new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME);
    // 判断当前系统
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        pictureUri = FileProvider.getUriForFile(this,
                "com.chen.lister.testchangeicon.fileProvider", pictureFile);
    } else {
        intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        pictureUri = Uri.fromFile(pictureFile);
    }
    // 去拍照
    intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
    startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

4. 图片裁剪

因为裁剪图片时也会用到 Intent,所以也要对 Uri 记性处理,我们也可以使用上面的方法进行处理。具体如下:

// 开始切割
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(FileProvider.getUriForFile(this,
            "com.chen.lister.testchangeicon.fileProvider", file), "image/*");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// ......
intent.putExtra("return-data", false); // 不直接返回数据
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一个文件
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
    
    
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这样程序就可以在 Android7.0 下正常运行。

一、概述

相信大家都用过 Android 应用中更换头像的功能,在这个功能中,用户可以拍照或者选择相册图片,然后裁剪出头像所需要的图案。 
那么你们有没有考虑过这个功能怎么实现的呢?今天就让我们一步步搞定这个功能,先看运行效果,这里选择了相册图片并设置头像。 
选择相册图片并裁剪.gif

点这里下载 Demo 的 src 文件 
下载之后,把里面的文件复制到你自己的项目中即可。

二、PopUpWindow 设计及弹出效果

1. 布局

优雅简洁的用户界面是吸引用户的开端,那先让我们设计一个漂亮的 PopUpWindow,如下所示: 
PopUpWindow 样式.png 
这个 PopUpWindow 里共有3个按钮,分别为“拍照”,“从相册选择”,以及“取消”。上面两个按钮连接在了一起,下方的“取消”与它们分开,那么这里就需要3种按钮样式:“拍照”按钮只有上方是圆角,“从相册选择”按钮只有下方是圆角,“取消”按钮四个角都是圆角。 
在 drawable 文件夹中新建下方3个 shape 文件。

1. white_btn

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white"/>
    <corners android:radius="10dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

2. white_btn_top

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white" />
    <corners android:topLeftRadius="10dp"
             android:topRightRadius="10dp"
             android:bottomRightRadius="0dp"
             android:bottomLeftRadius="0dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

3. white_btn_bottom

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@android:color/white" />
    <corners android:topLeftRadius="0dp"
             android:topRightRadius="0dp"
             android:bottomRightRadius="10dp"
             android:bottomLeftRadius="10dp"/>
    <stroke android:width="0dp" android:color="@android:color/white" />
</shape>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

有了这3个按钮样式,就可以写出 PopUpWindow 的布局了。 
在 colors.xml 中添加字体颜色 <color name="colorMainGreen">#40cab3</color> 
在 layout 中新建 pop_item.xml 布局,因为 PopUpWindow 弹出时,屏幕的背景会变灰,因此需要将布局的背景颜色设置为半透明灰色,颜色代码 #66000000

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="#66000000">

    <LinearLayout
        android:id="@+id/ll_pop"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginLeft="15dp"
        android:layout_marginRight="15dp"
        android:orientation="vertical"
        android:layout_alignParentBottom="true">
        <Button
            android:id="@+id/icon_btn_camera"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/white_btn_top"
            android:textColor="@color/colorMainGreen"
            android:text="拍照"/>
        <View
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:background="@color/background_gray"/>
        <Button
            android:id="@+id/icon_btn_select"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="@drawable/white_btn_bottom"
            android:textColor="@color/colorMainGreen"
            android:text="从相册选择"/>
        <Button
            android:id="@+id/icon_btn_cancel"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginTop="10dp"
            android:layout_marginBottom="15dp"
            android:background="@drawable/white_btn"
            android:textColor="@color/colorMainGreen"
            android:text="取消"/>
    </LinearLayout>

</RelativeLayout>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

2. 动画效果

这里 PopUpWindow 的出现和消失使用淡入淡出的动画效果。 
在 res 中新建 anim 文件夹,在其中新建两个动画效果。

popup_show.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="300"
        android:fromAlpha="0.0"
        android:toAlpha="1.0" />
</set>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

popup_gone.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <alpha
        android:duration="200"
        android:fromAlpha="1.0"
        android:toAlpha="0.0" />
</set>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在 styles.xml 中设置 PopUpWindow 整体的动画效果。

<style name="popwindow_anim_style">
        <item name="android:windowEnterAnimation">@anim/popup_show</item>
        <item name="android:windowExitAnimation">@anim/popup_gone</item>
</style>
  
  
  • 1
  • 2
  • 3
  • 4

3. PopupWindow 类

有了布局和动画效果,接下来就可以写 PopUpWindow 的工具类了,这个工具类负责接收外部的点击监听器,并设置点击弹窗外关闭弹窗。 
新建 PhotoPopupWindow 类,它的构造函数需要传入 “拍照” 和 “相册” 两个按钮的点击监听,具体代码如下:

public class PhotoPopupWindow extends PopupWindow {

    private View mView; // PopupWindow 菜单布局
    private Context mContext; // 上下文参数
    private View.OnClickListener mSelectListener; // 相册选取的点击监听器
    private View.OnClickListener mCaptureListener; // 拍照的点击监听器

    public PhotoPopupWindow(Activity context, View.OnClickListener selectListener, View.OnClickListener captureListener) {
        super(context);
        this.mContext = context;
        this.mSelectListener = selectListener;
        this.mCaptureListener = captureListener;
        Init();
    }

    /**
     * 设置布局以及点击事件
     */
    private void Init() {
        LayoutInflater inflater = (LayoutInflater) mContext
                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        mView = inflater.inflate(R.layout.pop_item, null);
        Button btn_camera = (Button) mView.findViewById(R.id.icon_btn_camera);
        Button btn_select = (Button) mView.findViewById(R.id.icon_btn_select);
        Button btn_cancel = (Button) mView.findViewById(R.id.icon_btn_cancel);

        btn_select.setOnClickListener(mSelectListener);
        btn_camera.setOnClickListener(mCaptureListener);
        btn_cancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                dismiss();
            }
        });

        // 导入布局
        this.setContentView(mView);
        // 设置动画效果
        this.setAnimationStyle(R.style.popwindow_anim_style);
        this.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
        this.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
        // 设置可触
        this.setFocusable(true);
        ColorDrawable dw = new ColorDrawable(0x0000000);
        this.setBackgroundDrawable(dw);
        // 单击弹出窗以外处 关闭弹出窗
        mView.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                int height = mView.findViewById(R.id.ll_pop).getTop();
                int y = (int) event.getY();
                if (event.getAction() == MotionEvent.ACTION_UP) {
                    if (y < height) {
                        dismiss();
                    }
                }
                return true;
            }
        });
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61

三、在 MainActivity 中设置头像

1. activity_main 布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.chen.lister.testchangeicon.MainActivity">

    <LinearLayout
        android:id="@+id/main_ll"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:gravity="center_horizontal">
        <ImageView
            android:id="@+id/main_icon"
            android:layout_width="120dp"
            android:layout_height="120dp"
            android:layout_marginTop="20dp"
            android:src="@mipmap/ic_launcher"/>
        <Button
            android:id="@+id/main_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="20dp"
            android:text="更换头像"/>
    </LinearLayout>
</LinearLayout>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

2. 弹出 PopUpWindow

回顾之前的 PopUpWindow 工具类,它的构造方法需要上下文以及两个点击事件的监听器,新建 PopUpWindow 之后就可以让它显示在屏幕下方中间。

main_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 进入相册选择
                    }
                }, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 拍照
                    }
                });
                View rootView = LayoutInflater.from(MainActivity.this).inflate(R.layout.activity_main, null);
                mPhotoPopupWindow.showAtLocation(rootView,
                        Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
            }
        });
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

3. 拍照或选择图片并切割

在 MainActivity 中添加如下常量:

private static final int REQUEST_IMAGE_GET = 0;
private static final int REQUEST_IMAGE_CAPTURE = 1;
private static final int REQUEST_SMALL_IMAGE_CUTTING = 2;
private static final int REQUEST_BIG_IMAGE_CUTTING = 3;
private static final String IMAGE_FILE_NAME = "icon.jpg";
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

先看在相册中选择图片,点击进入相册选择图片的按钮后,系统应该使用 startActivityForResult() 调用选择图片的 intent 并返回一个结果。

mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判断系统中是否有处理该 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivityForResult(intent, REQUEST_IMAGE_GET);
    } else {
    Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

返回的结果在 onActivityResult() 中处理,先通过 data.getData() 获取选择到的图片的 Uri,再通过 startSmallPhotoZoom() 对该图片进行裁剪。

/**
 * 处理回调结果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回调成功
    if (resultCode == RESULT_OK) {
        switch (requestCode) {

            // 小图切割
            case REQUEST_SMALL_IMAGE_CUTTING:
                if (data != null) {
                    setPicToView(data);
                }
                break;

            // 相册选取
            case REQUEST_IMAGE_GET:
                try {
                    startSmallPhotoZoom(data.getData());
                } catch (NullPointerException e) {
                    e.printStackTrace();
                }
                break;

            //......    
        }
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29

startSmallPhotoZoom() 方法如下,它会启动系统的裁剪界面进行裁剪并返回结果。它启动的 intent 中 “return-data” 为 true,意味着它裁剪完图片会直接将图片作为 bitmap 在内存中返回。 
如果你够细心,你就会发现它返回的结果也在上面 onActivityResult() 中调用 setPicToView() 方法处理了。

/**
 * 小图模式切割图片
 * 此方式直接返回截图后的 bitmap,由于内存的限制,返回的图片会比较小
 */
public void startSmallPhotoZoom(Uri uri) {
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 300); // 输出图片大小
    intent.putExtra("outputY", 300);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", true);
    startActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

setPicToView() 方法如下,它将裁剪后的图片保存到指定文件夹并设置到 ImageView 中。

/**
 * 小图模式中,保存图片后,设置到视图中
 */
private void setPicToView(Intent data) {
    Bundle extras = data.getExtras();
    if (extras != null) {
        Bitmap photo = extras.getParcelable("data"); // 直接获得内存中保存的 bitmap
        // 创建 smallIcon 文件夹
        if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            String storage = Environment.getExternalStorageDirectory().getPath();
            File dirFile = new File(storage + "/smallIcon");
            if (!dirFile.exists()) {
                if (!dirFile.mkdirs()) {
                    Log.e("TAG", "文件夹创建失败");
                } else {
                    Log.e("TAG", "文件夹创建成功");
                }
            }
            File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
            // 保存图片
            FileOutputStream outputStream = null;
            try {
                outputStream = new FileOutputStream(file);
                photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                outputStream.flush();
                outputStream.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 在视图中显示图片
        main_icon.setImageBitmap(photo);
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

解决了相册选图,再来看拍照。 
点击拍照的按钮,即调用系统的拍照功能。

mPhotoPopupWindow.dismiss();
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,
          Uri.fromFile(new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME)));
startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

修改 onActivityResult() 函数,增加拍照返回的处理,最后同样调用 startSmallPhotoZoom() 函数进行裁剪。

/**
 * 处理回调结果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回调成功
    if (resultCode == RESULT_OK) {
        switch (requestCode) {

        // 小图切割
        case REQUEST_SMALL_IMAGE_CUTTING:
            if (data != null) {
                setPicToView(data);
            }
            break;

        // 相册选取
        case REQUEST_IMAGE_GET:
            try {
                startSmallPhotoZoom(data.getData());
            } catch (NullPointerException e) {
                e.printStackTrace();
            }
            break;

        // 拍照
        case REQUEST_IMAGE_CAPTURE:
            File temp = new File(Environment.getExternalStorageDirectory() + "/" + IMAGE_FILE_NAME);
            startSmallPhotoZoom(Uri.fromFile(temp));
            break;
        }
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

四、大图片裁剪

设置完头像,再看之前保存的图片,你会发现它们都很模糊,那如果想裁剪出清晰的图片,该怎么做呢? 
还记得裁剪图片 Intent 中的这两个参数吗,它们就代表了输出图片的大小。

intent.putExtra("outputX", 300); // 输出图片大小
intent.putExtra("outputY", 300);
  
  
  • 1
  • 2

那么想提高图片的质量,是不是把这两个值加大就可以了呢? 
在回答这个问题之前,让我们先来了解一下裁剪后的图片是怎么返回的。 
假设现在有一张图片尺寸为 3200*2400px。也许你觉得返回这张图没什么问题,大不了耗1-2M的内存。不错,这个尺寸的图片确实只有1.8M左右的大小。但是你想不到的是,这个尺寸对应的 Bitmap 会耗光你应用程序的所有内存。Android出于安全性考虑,只会给你一个寒碜的缩略图。 
Android 中,默认 Bitmap 为 32 位,也就是说,一个像素点占用 4 个字节,那么之前我们说的图片需要占用多大的内存呢?3200*2400*4 bytes = 30M。 
整整30M!即使你想为一张只会存在几秒钟的图片消耗这么大的内存,Android 也不会答应的。

所以如果我们想提高裁剪图片的质量,可不是只加大输出的图片像素大小就可以的。那我们还应该做什么呢?先来看看裁剪图片的 Intent 可附带的参数,看看它们为我们提供了什么信息。

附带参数 数据类型 描述
crop String 发送裁剪信号
aspectX int X方向上的比例
aspectY int Y方向上的比例
outputX int 裁剪区的宽
outputY int 裁剪区的高
scale boolean 是否保留比例
return-data boolean 是否将数据保留在Bitmap中返回
data Parcelable 相应的Bitmap数据
circleCrop String 圆形裁剪区域?
MediaStore.EXTRA_OUTPUT (“output”) Uri 将URI指向相应的file:///…

在小图返回模式中,我们将 return-data 设置为了“true”,因此会在内存中直接返回一个 Bitmap,由于内存的原因,它将会是一个模糊的缩略图。 
如果将 return-data 设置为“false”,那么在 onActivityResult() 的 Intent 数据中你将不会接收到任何 Bitmap,相反,我们需要将 MediaStore.EXTRA_OUTPUT 关联到一个 Uri,此 Uri 是用来存放 Bitmap 的,那么裁剪后的图片就会保存到 sd 卡中。 
具体代码如下:

/**
 * 大图模式切割图片
 * 直接创建一个文件将切割后的图片写入
 */
public void startBigPhotoZoom(Uri uri) {
    // 创建大图文件夹
    Uri imageUri = null;
    if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
        String storage = Environment.getExternalStorageDirectory().getPath();
        File dirFile = new File(storage + "/bigIcon");
        if (!dirFile.exists()) {
            if (!dirFile.mkdirs()) {
                Log.e("TAG", "文件夹创建失败");
            } else {
                Log.e("TAG", "文件夹创建成功");
            }
        }
        File file = new File(dirFile, System.currentTimeMillis() + ".jpg");
        imageUri = Uri.fromFile(file);
    }
    // 开始切割
    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(uri, "image/*");
    intent.putExtra("crop", "true");
    intent.putExtra("aspectX", 1); // 裁剪框比例
    intent.putExtra("aspectY", 1);
    intent.putExtra("outputX", 600); // 输出图片大小
    intent.putExtra("outputY", 600);
    intent.putExtra("scale", true);
    intent.putExtra("return-data", false); // 不直接返回数据
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一个文件
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

之后根据需求将图片设置到 ImageView 中或者上传服务器即可,这里不再赘述。

五、Android 新版本适配

上面的程序在 Android5.X 及以下可以正常运行,但是在 Android6.0 和 Android7.0 下运行时会崩溃。这是因为 Android6.0 需要程序动态申请权限,而 Android7.0 对 Uri 添加了保护。

1. 动态权限

在 Android6.0 之后,拍照和读取本地文件都需要在运行时动态申请权限。 
申请之前需要检查用户之前是否已经同意该权限。如果已经同意,则直接进行下一步操作。如果没有,则进行申请,成功后在回调方法 onRequestPermissionsResult() 中进行后续处理。

修改头像按钮的点击事件

main_btn.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        mPhotoPopupWindow = new PhotoPopupWindow(MainActivity.this, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 拍照及文件权限申请
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                        || ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 权限还没有授予,进行申请
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申请的 requestCode 为 300
                } else {
                    // 权限已经申请,直接拍照
                    mPhotoPopupWindow.dismiss();
                    imageCapture();
                }
            }
        }, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 文件权限申请
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 权限还没有授予,进行申请
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申请的 requestCode 为 200
                } else {
                    // 如果权限已经申请过,直接进行图片选择
                    mPhotoPopupWindow.dismiss();
                    Intent intent = new Intent(Intent.ACTION_PICK);
                    intent.setType("image/*");
                    // 判断系统中是否有处理该 Intent 的 Activity
                    if (intent.resolveActivity(getPackageManager()) != null) {
                        startActivityForResult(intent, REQUEST_IMAGE_GET);
                    } else {
                        Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                    }
                }
            }
        });
        View rootView = LayoutInflater.from(MainActivity.this)
                .inflate(R.layout.activity_main, null);
        mPhotoPopupWindow.showAtLocation(rootView,
                Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
    }
});
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51

权限申请的回调

/**
 * 处理权限回调结果
 */
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    switch (requestCode) {
        case 200:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mPhotoPopupWindow.dismiss();
                Intent intent = new Intent(Intent.ACTION_PICK);
                intent.setType("image/*");
                // 判断系统中是否有处理该 Intent 的 Activity
                if (intent.resolveActivity(getPackageManager()) != null) {
                    startActivityForResult(intent, REQUEST_IMAGE_GET);
                } else {
                    Toast.makeText(MainActivity.this, "未找到图片查看器", Toast.LENGTH_SHORT).show();
                }
            } else {
                mPhotoPopupWindow.dismiss();
            }
            break;
        case 300:
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                mPhotoPopupWindow.dismiss();
                imageCapture();
            } else {
                mPhotoPopupWindow.dismiss();
            }
            break;
    }
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

2. Uri 保护

Android7.0 的官方文档是这么说的:

Passing file://URIs outside the package domain may leave the receiver with an unaccessible path. Therefore, attempts to pass a file:// URI trigger a FileUriExposedException. The recommended way to share the content of a private file is using the FileProvider.

什么意思呢?就是说,file:// 这样的 Uri 不能附着在 Intent 上,否则会引发 FileUriExposedException,官方建议使用 FileProvider 改变 Uri 的传递方式。 
在这个应用中,我们在调用相机并把拍摄的照片保存到手机本地时,如果传入 file:// 这样的 Uri 就会造成应用崩溃,因此需要使用 FileProvider,步骤如下。 
注:选择图片不会崩溃,因为选择图片后传入的 Uri 本身就是 Content Uri。

1. 在 res 下新建 xml 文件夹,其中新建 provider_paths.xml,代码如下

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- 注意包名 -->
    <external-path path="Android/data/com.chen.lister.testchangeicon/" name="files_root" />
</paths>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5

2. 在 manifest 中进行声明

<!-- 注意包名 -->
<provider
    android:name="android.support.v4.content.FileProvider"
    android:authorities="com.chen.lister.testchangeicon.fileProvider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/provider_paths"/>
</provider>
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

3. 将拍照行为封装成 imageCapture() 方法

/**
 * 判断系统及拍照
 */
private void imageCapture() {
    Intent intent;
    Uri pictureUri;
    File pictureFile = new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME);
    // 判断当前系统
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        pictureUri = FileProvider.getUriForFile(this,
                "com.chen.lister.testchangeicon.fileProvider", pictureFile);
    } else {
        intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        pictureUri = Uri.fromFile(pictureFile);
    }
    // 去拍照
    intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
    startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
}
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

4. 图片裁剪

因为裁剪图片时也会用到 Intent,所以也要对 Uri 记性处理,我们也可以使用上面的方法进行处理。具体如下:

// 开始切割
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(FileProvider.getUriForFile(this,
            "com.chen.lister.testchangeicon.fileProvider", file), "image/*");
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// ......
intent.putExtra("return-data", false); // 不直接返回数据
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一个文件
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
  
  
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

这样程序就可以在 Android7.0 下正常运行。

猜你喜欢

转载自blog.csdn.net/qq_34979546/article/details/78710919