Android—更換頭像及圖片裁剪(適配Android7.0)

一对省、概述

相信大家都用過 Android 應(yīng)用中更換頭像的功能簸呈,在這個功能中诫龙,用戶可以拍照或者選擇相冊圖片,然后裁剪出頭像所需要的圖案谎懦。
那么你們有沒有考慮過這個功能怎么實現(xiàn)的呢肚豺?今天就讓我們一步步搞定這個功能,先看運行效果界拦,這里選擇了相冊圖片并設(shè)置頭像吸申。

選擇相冊圖片并裁剪.gif

點這里下載 Demo

二、PopUpWindow 設(shè)計及彈出效果

1. 布局

優(yōu)雅簡潔的用戶界面是吸引用戶的開端享甸,那先讓我們設(shè)計一個漂亮的 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>

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>

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>

有了這3個按鈕樣式对妄,就可以寫出 PopUpWindow 的布局了湘今。
在 colors.xml 中添加字體顏色 <color name="colorMainGreen">#40cab3</color>
在 layout 中新建 pop_item.xml 布局,因為 PopUpWindow 彈出時剪菱,屏幕的背景會變灰摩瞎,因此需要將布局的背景顏色設(shè)置為半透明灰色拴签,顏色代碼 #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>

2. 動畫效果

這里 PopUpWindow 的出現(xiàn)和消失使用淡入淡出的動畫效果。
在 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>

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>

在 styles.xml 中設(shè)置 PopUpWindow 整體的動畫效果。

<style name="popwindow_anim_style">
        <item name="android:windowEnterAnimation">@anim/popup_show</item>
        <item name="android:windowExitAnimation">@anim/popup_gone</item>
</style>

3. PopupWindow 類

有了布局和動畫效果上渴,接下來就可以寫 PopUpWindow 的工具類了岸梨,這個工具類負(fù)責(zé)接收外部的點擊監(jiān)聽器,并設(shè)置點擊彈窗外關(guān)閉彈窗稠氮。
新建 PhotoPopupWindow 類曹阔,它的構(gòu)造函數(shù)需要傳入 “拍照” 和 “相冊” 兩個按鈕的點擊監(jiān)聽,具體代碼如下:

public class PhotoPopupWindow extends PopupWindow {

    private View mView; // PopupWindow 菜單布局
    private Context mContext; // 上下文參數(shù)
    private View.OnClickListener mSelectListener; // 相冊選取的點擊監(jiān)聽器
    private View.OnClickListener mCaptureListener; // 拍照的點擊監(jiān)聽器

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

    /**
     * 設(shè)置布局以及點擊事件
     */
    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();
            }
        });

        // 導(dǎo)入布局
        this.setContentView(mView);
        // 設(shè)置動畫效果
        this.setAnimationStyle(R.style.popwindow_anim_style);
        this.setWidth(WindowManager.LayoutParams.MATCH_PARENT);
        this.setHeight(WindowManager.LayoutParams.WRAP_CONTENT);
        // 設(shè)置可觸
        this.setFocusable(true);
        ColorDrawable dw = new ColorDrawable(0x0000000);
        this.setBackgroundDrawable(dw);
        // 單擊彈出窗以外處 關(guān)閉彈出窗
        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;
            }
        });
    }
}

三隔披、在 MainActivity 中設(shè)置頭像

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>

2. 彈出 PopUpWindow

回顧之前的 PopUpWindow 工具類赃份,它的構(gòu)造方法需要上下文以及兩個點擊事件的監(jiān)聽器,新建 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) {
                        // 進(jìn)入相冊選擇
                    }
                }, 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);
            }
        });

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";

先看在相冊中選擇圖片,點擊進(jìn)入相冊選擇圖片的按鈕后漓库,系統(tǒng)應(yīng)該使用 startActivityForResult() 調(diào)用選擇圖片的 intent 并返回一個結(jié)果恃慧。

mPhotoPopupWindow.dismiss();
Intent intent = new Intent(Intent.ACTION_PICK);
intent.setType("image/*");
// 判斷系統(tǒng)中是否有處理該 Intent 的 Activity
if (intent.resolveActivity(getPackageManager()) != null) {
    startActivityForResult(intent, REQUEST_IMAGE_GET);
    } else {
    Toast.makeText(MainActivity.this, "未找到圖片查看器", Toast.LENGTH_SHORT).show();
}

返回的結(jié)果在 onActivityResult() 中處理,先通過 data.getData() 獲取選擇到的圖片的 Uri渺蒿,再通過 startSmallPhotoZoom() 對該圖片進(jìn)行裁剪痢士。

/**
 * 處理回調(diào)結(jié)果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回調(diào)成功
    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;

            //......    
        }
    }
}

startSmallPhotoZoom() 方法如下,它會啟動系統(tǒng)的裁剪界面進(jìn)行裁剪并返回結(jié)果茂装。它啟動的 intent 中 "return-data" 為 true怠蹂,意味著它裁剪完圖片會直接將圖片作為 bitmap 在內(nèi)存中返回。
如果你夠細(xì)心少态,你就會發(fā)現(xiàn)它返回的結(jié)果也在上面 onActivityResult() 中調(diào)用 setPicToView() 方法處理了城侧。

/**
 * 小圖模式切割圖片
 * 此方式直接返回截圖后的 bitmap,由于內(nèi)存的限制彼妻,返回的圖片會比較小
 */
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);
}

setPicToView() 方法如下嫌佑,它將裁剪后的圖片保存到指定文件夾并設(shè)置到 ImageView 中。

/**
 * 小圖模式中侨歉,保存圖片后屋摇,設(shè)置到視圖中
 */
private void setPicToView(Intent data) {
    Bundle extras = data.getExtras();
    if (extras != null) {
        Bitmap photo = extras.getParcelable("data"); // 直接獲得內(nèi)存中保存的 bitmap
        // 創(chuàng)建 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", "文件夾創(chuàng)建失敗");
                } else {
                    Log.e("TAG", "文件夾創(chuàng)建成功");
                }
            }
            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);
    }
}

解決了相冊選圖,再來看拍照幽邓。
點擊拍照的按鈕炮温,即調(diào)用系統(tǒng)的拍照功能。

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);

修改 onActivityResult() 函數(shù)牵舵,增加拍照返回的處理柒啤,最后同樣調(diào)用 startSmallPhotoZoom() 函數(shù)進(jìn)行裁剪倦挂。

/**
 * 處理回調(diào)結(jié)果
 */
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // 回調(diào)成功
    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;
        }
    }
}

四、大圖片裁剪

設(shè)置完頭像白修,再看之前保存的圖片妒峦,你會發(fā)現(xiàn)它們都很模糊,那如果想裁剪出清晰的圖片兵睛,該怎么做呢肯骇?
還記得裁剪圖片 Intent 中的這兩個參數(shù)嗎,它們就代表了輸出圖片的大小祖很。

intent.putExtra("outputX", 300); // 輸出圖片大小
intent.putExtra("outputY", 300);

那么想提高圖片的質(zhì)量笛丙,是不是把這兩個值加大就可以了呢?
在回答這個問題之前假颇,讓我們先來了解一下裁剪后的圖片是怎么返回的胚鸯。
假設(shè)現(xiàn)在有一張圖片尺寸為 3200*2400px。也許你覺得返回這張圖沒什么問題笨鸡,大不了耗1-2M的內(nèi)存姜钳。不錯,這個尺寸的圖片確實只有1.8M左右的大小形耗。但是你想不到的是哥桥,這個尺寸對應(yīng)的 Bitmap 會耗光你應(yīng)用程序的所有內(nèi)存。Android出于安全性考慮激涤,只會給你一個寒磣的縮略圖拟糕。
Android 中,默認(rèn) Bitmap 為 32 位倦踢,也就是說送滞,一個像素點占用 4 個字節(jié),那么之前我們說的圖片需要占用多大的內(nèi)存呢辱挥?3200*2400*4 bytes = 30M犁嗅。
整整30M!即使你想為一張只會存在幾秒鐘的圖片消耗這么大的內(nèi)存晤碘,Android 也不會答應(yīng)的愧哟。

所以如果我們想提高裁剪圖片的質(zhì)量,可不是只加大輸出的圖片像素大小就可以的哼蛆。那我們還應(yīng)該做什么呢蕊梧?先來看看裁剪圖片的 Intent 可附帶的參數(shù),看看它們?yōu)槲覀兲峁┝耸裁葱畔ⅰ?/p>

附帶參數(shù) 數(shù)據(jù)類型 描述
crop String 發(fā)送裁剪信號
aspectX int X方向上的比例
aspectY int Y方向上的比例
outputX int 裁剪區(qū)的寬
outputY int 裁剪區(qū)的高
scale boolean 是否保留比例
return-data boolean 是否將數(shù)據(jù)保留在Bitmap中返回
data Parcelable 相應(yīng)的Bitmap數(shù)據(jù)
circleCrop String 圓形裁剪區(qū)域腮介?
MediaStore.EXTRA_OUTPUT ("output") Uri 將URI指向相應(yīng)的file:///...

在小圖返回模式中肥矢,我們將 return-data 設(shè)置為了“true”,因此會在內(nèi)存中直接返回一個 Bitmap,由于內(nèi)存的原因甘改,它將會是一個模糊的縮略圖旅东。
如果將 return-data 設(shè)置為“false”,那么在 onActivityResult() 的 Intent 數(shù)據(jù)中你將不會接收到任何 Bitmap十艾,相反抵代,我們需要將 MediaStore.EXTRA_OUTPUT 關(guān)聯(lián)到一個 Uri,此 Uri 是用來存放 Bitmap 的忘嫉,那么裁剪后的圖片就會保存到 sd 卡中荤牍。
具體代碼如下:

/**
 * 大圖模式切割圖片
 * 直接創(chuàng)建一個文件將切割后的圖片寫入
 */
public void startBigPhotoZoom(Uri uri) {
    // 創(chuàng)建大圖文件夾
    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", "文件夾創(chuàng)建失敗");
            } else {
                Log.e("TAG", "文件夾創(chuàng)建成功");
            }
        }
        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); // 不直接返回數(shù)據(jù)
    intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一個文件
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
    startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);
}

之后根據(jù)需求將圖片設(shè)置到 ImageView 中或者上傳服務(wù)器即可,這里不再贅述庆冕。

五康吵、Android 新版本適配

上面的程序在 Android5.X 及以下可以正常運行,但是在 Android6.0 和 Android7.0 下運行時會崩潰访递。這是因為 Android6.0 需要程序動態(tài)申請權(quán)限晦嵌,而 Android7.0 對 Uri 添加了保護。

1. 動態(tài)權(quán)限

在 Android6.0 之后拷姿,拍照和讀取本地文件都需要在運行時動態(tài)申請權(quán)限惭载。
申請之前需要檢查用戶之前是否已經(jīng)同意該權(quán)限。如果已經(jīng)同意响巢,則直接進(jìn)行下一步操作描滔。如果沒有,則進(jìn)行申請抵乓,成功后在回調(diào)方法 onRequestPermissionsResult() 中進(jìn)行后續(xù)處理伴挚。

修改頭像按鈕的點擊事件

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) {
                // 拍照及文件權(quán)限申請
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                        || ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 權(quán)限還沒有授予靶衍,進(jìn)行申請
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申請的 requestCode 為 300
                } else {
                    // 權(quán)限已經(jīng)申請灾炭,直接拍照
                    mPhotoPopupWindow.dismiss();
                    imageCapture();
                }
            }
        }, new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 文件權(quán)限申請
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    // 權(quán)限還沒有授予,進(jìn)行申請
                    ActivityCompat.requestPermissions(MainActivity.this,
                            new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200); // 申請的 requestCode 為 200
                } else {
                    // 如果權(quán)限已經(jīng)申請過颅眶,直接進(jìn)行圖片選擇
                    mPhotoPopupWindow.dismiss();
                    Intent intent = new Intent(Intent.ACTION_PICK);
                    intent.setType("image/*");
                    // 判斷系統(tǒng)中是否有處理該 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);
    }
});

權(quán)限申請的回調(diào)

/**
 * 處理權(quán)限回調(diào)結(jié)果
 */
@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/*");
                // 判斷系統(tǒng)中是否有處理該 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;
    }
}

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 上涛酗,否則會引發(fā) FileUriExposedException铡原,官方建議使用 FileProvider 改變 Uri 的傳遞方式。
在這個應(yīng)用中商叹,我們在調(diào)用相機并把拍攝的照片保存到手機本地時燕刻,如果傳入 file:// 這樣的 Uri 就會造成應(yīng)用崩潰,因此需要使用 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>

2. 在 manifest 中進(jìn)行聲明

<!-- 注意包名 -->
<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>

3. 將拍照行為封裝成 imageCapture() 方法

/**
 * 判斷系統(tǒng)及拍照
 */
private void imageCapture() {
    Intent intent;
    Uri pictureUri;
    File pictureFile = new File(Environment.getExternalStorageDirectory(), IMAGE_FILE_NAME);
    // 判斷當(dāng)前系統(tǒng)
    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);
}

4. 圖片裁剪

因為裁剪圖片時也會用到 Intent,所以也要對 Uri 進(jìn)行處理酷勺,我們也可以使用上面的方法進(jìn)行處理本橙。具體如下:

// 開始切割
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); // 不直接返回數(shù)據(jù)
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); // 返回一個文件
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
startActivityForResult(intent, REQUEST_BIG_IMAGE_CUTTING);

這樣程序就可以在 Android7.0 下正常運行。

2020.05.06補充:

當(dāng)前程序在Android10下讀取文件再decode為Bitmap時為null脆诉,報錯為open failed: EACCES (Permission denied)甚亭。這是因為Android10下新增分區(qū)儲存功能,在外部存儲設(shè)備中為每個應(yīng)用提供了一個“隔離存儲沙盒”库说,我們選擇將其停用狂鞋,在AndroidManifest的application節(jié)點中添加android:requestLegacyExternalStorage="true"即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末潜的,一起剝皮案震驚了整個濱河市骚揍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌啰挪,老刑警劉巖滤否,帶你破解...
    沈念sama閱讀 222,000評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異姐扮,居然都是意外死亡组橄,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,745評論 3 399
  • 文/潘曉璐 我一進(jìn)店門锰什,熙熙樓的掌柜王于貴愁眉苦臉地迎上來下硕,“玉大人,你說我怎么就攤上這事汁胆∷笮眨” “怎么了?”我有些...
    開封第一講書人閱讀 168,561評論 0 360
  • 文/不壞的土叔 我叫張陵嫩码,是天一觀的道長誉尖。 經(jīng)常有香客問我,道長铸题,這世上最難降的妖魔是什么铡恕? 我笑而不...
    開封第一講書人閱讀 59,782評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮丢间,結(jié)果婚禮上探熔,老公的妹妹穿的比我還像新娘。我一直安慰自己烘挫,他們只是感情好诀艰,可當(dāng)我...
    茶點故事閱讀 68,798評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般涡驮。 火紅的嫁衣襯著肌膚如雪暗甥。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,394評論 1 310
  • 那天捉捅,我揣著相機與錄音撤防,去河邊找鬼。 笑死棒口,一個胖子當(dāng)著我的面吹牛寄月,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播无牵,決...
    沈念sama閱讀 40,952評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼漾肮,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了茎毁?” 一聲冷哼從身側(cè)響起克懊,我...
    開封第一講書人閱讀 39,852評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎七蜘,沒想到半個月后谭溉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,409評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡橡卤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,483評論 3 341
  • 正文 我和宋清朗相戀三年扮念,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碧库。...
    茶點故事閱讀 40,615評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡柜与,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出嵌灰,到底是詐尸還是另有隱情弄匕,我是刑警寧澤,帶...
    沈念sama閱讀 36,303評論 5 350
  • 正文 年R本政府宣布伞鲫,位于F島的核電站粘茄,受9級特大地震影響签舞,放射性物質(zhì)發(fā)生泄漏秕脓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,979評論 3 334
  • 文/蒙蒙 一儒搭、第九天 我趴在偏房一處隱蔽的房頂上張望吠架。 院中可真熱鬧,春花似錦搂鲫、人聲如沸傍药。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,470評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拐辽。三九已至拣挪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間俱诸,已是汗流浹背菠劝。 一陣腳步聲響...
    開封第一講書人閱讀 33,571評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留睁搭,地道東北人赶诊。 一個月前我還...
    沈念sama閱讀 49,041評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像园骆,于是被迫代替她去往敵國和親舔痪。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,630評論 2 359

推薦閱讀更多精彩內(nèi)容