android實現拍照、相簿選圖、裁剪功能,相容7.0以及小米

joy_we1發表於2018-05-10

現在一般的手機應用都會有上傳頭像的功能,我在實現這個功能的時候遇到很多問題,這裡專門記錄一下。

add 2018/5/10 21:05

先列舉一下我出現過的問題:

1.執行時許可權

2.呼叫系統相機拍照後crash,或者返回RESULT_CANCEL(0)

3.選擇相片後得到的Uri為空或者為Uri後半段為資源ID(%1234567這種)

4.呼叫系統裁剪後crash

5.小米手機的特別情況

還有許多小問題,大多都是上面問題引起的併發症,就不一一列舉了。


先上程式碼,慢慢講。


1.佈局


只關注頭像那一欄就可以了,點選頭像後會彈出選擇頁面。PopupWindow的實現如下:

1.1    新建layout檔案pop_item

<?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"
            />
        <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>

        三個Button分別對應三個按鈕,中間的View是兩個按鈕之間的線,colorMainGreen是

<color name="colorMainGreen">#40cab3</color>

1.2    可以看到三個按鈕分別是上圓角,下圓角,全圓角,在drawable中新建3個xml,繪製Button樣式

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>

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>

while_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>

        簡單解釋一下shape的用法,每一組< />中制定一個屬性(不知道是不是這麼叫,但是是這個意思),在每條屬性中可以指定更多的細節。solid指定填充,corners指定圓角,在corners中的radius指定了圓角的半徑,stroke用於描邊。

1.3    PhotoPopupWindow佈局都寫好了,現在我們要寫自己的PhotoPopupWindow類載入它,同時給他新增點選事件。新建一個package,命名為popup(這樣做的目的是使得程式碼結構清晰),在這個包下新建PhotoPopupWindow類,繼承PopupWindow

public class PhotoPopupWindow extends PopupWindow {
    private static final String TAG = "PhotoPopupWindow";
    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);
        assert inflater != null;
        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;
            }
        });
    }
}

        程式碼是很主流的寫法,沒什麼特別的。TAG常量會出現在每一個JAVA類中,即使我用不到他。這樣寫便於區別Log發生的位置。兩個OnClickListener需要在例項化的時候實現。

1.4    彈出框寫好了,接下來是頭像頁面的佈局檔案

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@color/backgroundWhite"
    tools:context=".activity.UserInfoActivity">

    <android.support.v7.widget.Toolbar
        android:id="@+id/toolbar_userinfo"
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:background="?attr/colorPrimary"
        app:title="使用者資訊"
        android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
        app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
    </android.support.v7.widget.Toolbar>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        >
        <RelativeLayout
            android:id="@+id/user_head"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:textSize="16sp"
                android:text="頭像 "/>
            <de.hdodenhof.circleimageview.CircleImageView
                android:id="@+id/user_head_iv"
                android:layout_width="40dp"
                android:layout_height="40dp"
                android:layout_alignParentEnd="true"
                android:layout_alignParentRight="true"
                android:layout_centerVertical="true"
                android:layout_marginRight="25dp"
                app:civ_border_color="#F3F3F3"
                app:civ_border_width="1dp"/>
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/user_nick_name"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:textSize="16sp"
                android:text="暱稱"/>
            <TextView
                android:id="@+id/user_nick_name_TV"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginRight="10dp"
                android:textSize="16sp"
                android:layout_toLeftOf="@id/user_nick_name_IV" />
            <ImageView
                android:id="@+id/user_nick_name_IV"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:src="@drawable/ic_chevron_right_black_24dp"
                android:layout_alignParentRight="true"
                android:layout_marginRight="15dp"
                />
        </RelativeLayout>

        <RelativeLayout
            android:id="@+id/user_gender"
            android:layout_width="match_parent"
            android:layout_height="60dp"
            android:layout_marginTop="1dp"
            android:background="@drawable/bg_info_rl">
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginLeft="15dp"
                android:textSize="16sp"
                android:text="性別"/>
            <TextView
                android:id="@+id/user_gender_TV"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:gravity="center_vertical"
                android:layout_marginRight="10dp"
                android:textSize="16sp"
                android:layout_toLeftOf="@id/user_gender_IV" />
            <ImageView
                android:id="@+id/user_gender_IV"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:src="@drawable/ic_chevron_right_black_24dp"
                android:layout_marginRight="15dp"
                android:layout_alignParentRight="true"
                />
        </RelativeLayout>

    </LinearLayout>
</LinearLayout>
        我解釋一下有疑問的地方。Toolbar是谷歌推薦的標題欄,比Actionbar具有更好的擴充性。CircleImageView是一個開源庫,自行百度GitHub地址,用於產生圓形圖片。這一部分的設計可以參考郭霖大神的《第一行程式碼》或者他的部落格,有關Material design的內容(在十一章好像),他講解的很詳細。backgroundWhite是
<color name="backgroundWhite">#EBEBEB</color>
tools:context指定了該layout將在哪個activiy展示,指定了 tools:context的layout可以動態預覽佈局,也就是說我在acticity的onCreate裡改變了某個控制元件,我可以在不run的情況下預覽這個改變的效果。tools還有一些別的功能,我懂的也不多,就不介紹了。bg_info_rl是可變背景,這裡我只是簡單設定了點選和鬆開時候的顏色,程式碼這段結束後貼,ic_chevron_right_black_24dp是Material design提供的小圖示,樣式是“>”,大家可以自行搜尋,這是谷歌官方提供的免費素材包,可以在谷歌MD的官網或者GitHub上獲取。
bg_info_rl
<?xml version="1.0" encoding="UTF-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
    <item android:state_window_focused="false" android:drawable="@drawable/bg_info_rl_normal" />
    <item android:state_pressed="true" android:drawable="@drawable/bg_info_rl_pressed" />
</selector>
bg_info_rl_normal
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/white" />
</shape>
bg_info_rl_pressed
<?xml version="1.0" encoding="UTF-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="@color/light_grey" />
</shape>
color
<color name="white">#FFFFFF</color>
<color name="light_grey">#EAEAEA</color> <!--淺灰色-->

2.執行時許可權的問題

add 2018/5/11 10:18

        android 6.0引入了執行時許可權,使用者不必在安裝時授予所有許可權,可以在使用到相關功能時再授權。普通許可權不需要執行時申請,危險許可權則必須,否則程式會crash掉。

        頭像功能需要以下三個許可權

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CAMERA" />

        讀寫SD卡,使用相機,他們都是危險許可權,接下來新建UserInfoAcitivity,繼承AppCompatActivity 繼承View.OnClickListener介面。

變數和常量

private static final String TAG = "UserInfoActivity";
    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_CHANGE_USER_NICK_NAME = 10;
    private static final String IMAGE_FILE_NAME = "user_head_icon.jpg";

    PhotoPopupWindow mPhotoPopupWindow;
    TextView textView_user_nick_name;
    TextView textView_user_gender;
    CircleImageView circleImageView_user_head;
初始化佈局
@Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_userinfo);

        textView_user_nick_name = findViewById(R.id.user_nick_name_TV);
        textView_user_gender = findViewById(R.id.user_gender_TV);
        circleImageView_user_head = findViewById(R.id.user_head_iv);
        //InfoPrefs自己封裝的一個 SharedPreferences 工具類
        //init()指定檔名,getData(String key)獲取key對應的字串,getIntData(int key)獲取key對應的int
        InfoPrefs.init("user_info");
        refresh();

        RelativeLayout relativeLayout_user_nick_name = findViewById(R.id.user_nick_name);
        relativeLayout_user_nick_name.setOnClickListener(this);

        RelativeLayout relativeLayout_user_gender = findViewById(R.id.user_gender);
        relativeLayout_user_gender.setOnClickListener(this);

        RelativeLayout relativeLayout_user_head = findViewById(R.id.user_head);
        relativeLayout_user_head.setOnClickListener(this);
        //初始化 toolbar
        Toolbar toolbar = findViewById(R.id.toolbar_userinfo);
        setSupportActionBar(toolbar);
        ActionBar actionBar = getSupportActionBar();

        if (actionBar != null) {
            //指定toolbar左上角的返回按鈕,這個按鈕的id是home(無法更改)
            actionBar.setDisplayHomeAsUpEnabled(true);
            //actionBar.setHomeAsUpIndicator();
        }
    }


public void refresh(){
        textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));
        textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));
        showHeadImage();
        //circleImageView_user_head.setImageURI();
    }

        為每一個RelativeLayout都新增了點選事件,這裡我們只關注頭像的點選事件。出現的工具類我只解釋功能,完整程式碼可以在文末我的GitHub獲取,這個專案是一個手機桌面寵物的demo,包括懸浮窗、藍芽、鬧鐘等,合作寫的,程式碼的風格不太一致,見諒。

@Override
    public void onClick(View v) {
        switch(v.getId()){
            case R.id.user_head:
                //建立存放頭像的資料夾
                PictureUtil.mkdirMyPetRootDirectory();
                mPhotoPopupWindow = new PhotoPopupWindow(UserInfoActivity.this, new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 檔案許可權申請
                        if (ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                                != PackageManager.PERMISSION_GRANTED) {
                            // 許可權還沒有授予,進行申請
                            ActivityCompat.requestPermissions(UserInfoActivity.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(UserInfoActivity.this, "未找到圖片檢視器", Toast.LENGTH_SHORT).show();
                            }
                        }
                    }
                }, new View.OnClickListener()
                {
                    @Override
                    public void onClick (View v){
                        // 拍照及檔案許可權申請
                        if (ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED
                                || ContextCompat.checkSelfPermission(UserInfoActivity.this,
                                Manifest.permission.WRITE_EXTERNAL_STORAGE)
                                != PackageManager.PERMISSION_GRANTED) {
                            // 許可權還沒有授予,進行申請
                            ActivityCompat.requestPermissions(UserInfoActivity.this,
                                    new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 300); // 申請的 requestCode 為 300
                        } else {
                            // 許可權已經申請,直接拍照
                            mPhotoPopupWindow.dismiss();
                            imageCapture();
                        }
                    }
                });
                View rootView = LayoutInflater.from(UserInfoActivity.this).inflate(R.layout.activity_userinfo, null);
                mPhotoPopupWindow.showAtLocation(rootView,
                        Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
                break;

            case R.id.user_nick_name:
                ChangeInfoBean bean = new ChangeInfoBean();
                bean.setTitle("修改暱稱");
                bean.setInfo(InfoPrefs.getData(Constants.UserInfo.NAME));
                Intent intent = new Intent(UserInfoActivity.this,ChangeInfoActivity.class);
                intent.putExtra("data", bean);
                startActivityForResult(intent,REQUEST_CHANGE_USER_NICK_NAME);
                break;

            case R.id.user_gender:
                new ItemsAlertDialogUtil(UserInfoActivity.this).setItems(Constants.GENDER_ITEMS).
                        setListener(new ItemsAlertDialogUtil.OnSelectFinishedListener() {
                            @Override
                            public void SelectFinished(int which) {
                                InfoPrefs.setData(Constants.UserInfo.GENDER,Constants.GENDER_ITEMS[which]);
                                textView_user_gender.setText(InfoPrefs.getData(Constants.UserInfo.GENDER));
                            }
                        }).showDialog();
                break;
            default:
        }
    }

        執行時許可權的邏輯很簡單,先判斷是否已經授權過,如果已經授權,則直接進行操作,否則請求授權,根據請求結果處理。請求結果在onRequestPermissonsResult裡處理。有一點要提一下,執行時許可權申請是按照組來處理的,也就是說同屬一個組的許可權的請求是一樣的,而使用者只要授權一個,同組的許可權也會同時被授權。SD卡的讀寫全是同屬STORAGE組,所以我在申請SD卡讀寫許可權的時候只申請讀許可權或者寫許可權就可以了。

@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(UserInfoActivity.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;
        }
        //super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }

        到這裡,執行時許可權就已經處理好了。

3.拍照和選圖

add 2018/5/11 11:30

        從這裡開始就可能會NullPointerException,SecurityException等問題。

3.1    拍照

private void imageCapture() {
        Intent intent;
        Uri pictureUri;
        //getMyPetRootDirectory()得到的是Environment.getExternalStorageDirectory() + File.separator+"MyPet"
        //也就是我之前建立的存放頭像的資料夾(目錄)
        File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), 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);
            //""中的內容是隨意的,但最好用package名.provider名的形式,清晰明瞭
            pictureUri = FileProvider.getUriForFile(this,
                    "com.example.mypet.fileprovider", pictureFile);
        } else {
            intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            pictureUri = Uri.fromFile(pictureFile);
        }
        // 去拍照,拍照的結果存到pictureUri對應的路徑中
        intent.putExtra(MediaStore.EXTRA_OUTPUT, pictureUri);
        Log.e(TAG,"before take photo"+pictureUri.toString());
        startActivityForResult(intent, REQUEST_IMAGE_CAPTURE);
    }

        從android7.0(SDK>=24)開始,直接使用本地真實路徑的Uri被認為是不安全的,會丟擲一個FileUri什麼什麼Exception(記不清了),必須使用FileProvider封裝過的Uri,感興趣可以自己看下,7.0y以上得到的uri是content開頭,以下以file開頭。那麼FileProvider怎麼使用呢?

        首先,在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="MyPet/" name="MyPetRoot" />
    <external-path name="sdcard_root" path="."/>
</paths>

        paths指定要用到的目錄,external-path是SD卡根目錄,所以我的第一條的路徑是SD卡根目錄下新建的MyPet資料夾,取名為MyPetRoot,這個虛擬目錄名可以隨意取,最終的Uri會是這樣 content://com.example.mypet.fileprovider/MyPetRoot/......。第二條是將SD卡共享,這是用於相簿選圖。

        接下來,在AndroidManifest.xml中註冊這個provider

<provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="com.example.mypet.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths" />
        </provider>

        有幾點要注意:authorities要與之前使用FileProvider時""中的內容相同(實際上的邏輯是使用FileProvider時要與宣告的authorites相同)。exported必須要並且只能設為false,否則會報錯,其實也很好理解,我們將sd卡共享了,如果再設定成可以被外界訪問,那麼許可權就沒有用了。grantUriPermissions要設定為true,這樣才能讓其他程式(系統相機等)臨時使用這個provider。最後在<meta-data />指定使用的resource,也就是我們剛才寫的xml。

        現在我們回到imageCapture,還有兩句沒有解釋。addflags的作用是給Intent新增一個標記(我不知道應該怎麼叫,看我後面的解釋),這裡我們新增的是Intent.FLAG_GRANT_READ_URI_PERMISSION,他的作用是臨時授權Intent啟動的Activity使用我們Rrovider封裝的Uri。最後一行啟動拍照Activity,請求碼是REQUEST_IMAGE_CAPTURE,這個值自己設定,用於區分回撥結果來自哪個請求。這樣,我們就完成了拍照請求,會跳轉到系統拍照介面,接下來就要處理拍照得到的照片了。

        重寫onActicityResult方法,我直接把所有請求先全貼了,拍照後的回撥是case REQUEST_IMAGE_CAPTURE下的內容,邏輯很簡單,拍照後需要裁剪,裁剪需要用到我們拍照得到的圖片的Uri。

@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // 回撥成功
        if (resultCode == RESULT_OK) {
            switch (requestCode) {

                // 切割
                case REQUEST_SMALL_IMAGE_CUTTING:
                    Log.e(TAG,"before show");
                    File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
                    Uri cropUri = Uri.fromFile(cropFile);
                    setPicToView(cropUri);
                    break;

                // 相簿選取
                case REQUEST_IMAGE_GET:
                    Uri uri= PictureUtil.getImageUri(this,data);
                    startPhotoZoom(uri);
                    break;

                // 拍照
                case REQUEST_IMAGE_CAPTURE:
                    File pictureFile = new File(PictureUtil.getMyPetRootDirectory(), IMAGE_FILE_NAME);
                    Uri pictureUri;
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                        pictureUri = FileProvider.getUriForFile(this,
                                "com.example.mypet.fileprovider", pictureFile);
                        Log.e(TAG,"picURI="+pictureUri.toString());
                    } else {
                        pictureUri = Uri.fromFile(pictureFile);
                    }
                    startPhotoZoom(pictureUri);
                    break;
                // 獲取changeinfo銷燬 後 回傳的資料
                case REQUEST_CHANGE_USER_NICK_NAME:
                    String returnData = data.getStringExtra("data_return");
                    InfoPrefs.setData(Constants.UserInfo.NAME,returnData);
                    textView_user_nick_name.setText(InfoPrefs.getData(Constants.UserInfo.NAME));
                    break;
                default:
            }
        }else{
            Log.e(TAG,"result = "+resultCode+",request = "+requestCode);
        }
    }
        到這裡,拍照的功能已經實現了,如果不想裁剪,可以直接將  sd卡根目錄/MyPet/user_head_icon.jpg這張圖片顯示,接下來講一下相簿選取的實現。

3.2    相簿選取

        其實相簿選取的程式碼我在上面已經完全貼過了,許可權申請的時候已經呼叫了相簿,OnActivityResult中處理回撥,這裡我要說一下我踩的一個大坑

Uri uri= PictureUtil.getImageUri(this,data);

        具體的各種問題我已經記不清了,沒辦法進行梳理,我只能籠統的說一下了(本來想說的很多,結果解決後把坑玩的差不多了,嚶嚶嚶,以後要一邊踩坑一邊寫)。總體上是因為android4.4開始相簿中返回的圖片不再是圖片的真是Uri了,而是封裝過的。我測試的時候返回的Uri五花八門,有的是可以正常處理的,有的需要進行解析,其中以小米最亂(不得不說MIUI對開發者各種不友好,為了使用者體驗各種不遵守規則)。最終在各種部落格(可能是我用法的原因,有不少部落格的方法我用了還是會報錯)的洗禮下終於解決了,我把他封裝在了PictureUtil裡面,過程是    亂七八糟的uri ->真實路徑 ->統一的uri 。

        部分 50% 來自《第一行程式碼》,部分 25% 來自他人部落格,部分 25% 是我自己寫的

public class PictureUtil {
    private static final String TAG = "PictureUtil";
    private static final String MyPetRootDirectory = Environment.getExternalStorageDirectory() + File.separator+"MyPet";

    public static String getMyPetRootDirectory(){
        return MyPetRootDirectory;
    }

    public static Uri getImageUri(Context context,Intent data){
        String imagePath = null;
        Uri uri = data.getData();
        if(Build.VERSION.SDK_INT >= 19){
            if(DocumentsContract.isDocumentUri(context,uri)){
                String docId = DocumentsContract.getDocumentId(uri);
                if("com.android.providers.media.documents".equals(uri.getAuthority())){
                    String id = docId.split(":")[1];
                    String selection = MediaStore.Images.Media._ID+"="+id;
                    imagePath = getImagePath(context,MediaStore.Images.Media.EXTERNAL_CONTENT_URI,selection);
                }else if("com.android.providers.downloads.documents".equals(uri.getAuthority())){
                    Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"),Long.valueOf(docId));
                    imagePath = getImagePath(context,contentUri,null);
                }
            }else if("content".equalsIgnoreCase(uri.getScheme())){
                imagePath = getImagePath(context,uri,null);
            }else if("file".equalsIgnoreCase(uri.getScheme())){
                imagePath = uri.getPath();
            }
        }else{
            uri= data.getData();
            imagePath = getImagePath(context,uri,null);
        }
        File file = new File(imagePath);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            uri = FileProvider.getUriForFile(context,
                  "com.example.mypet.fileprovider", file);
        } else {
            uri = Uri.fromFile(file);
        }

        return uri;
    }

    private static String getImagePath(Context context,Uri uri, String selection) {
        String path = null;
        Cursor cursor = context.getContentResolver().query(uri,null,selection,null,null);
        if(cursor != null){
            if(cursor.moveToFirst()){
                path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
            }
            cursor.close();
        }
        return path;
    }

    public static void mkdirMyPetRootDirectory(){
        boolean isSdCardExist = Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED);// 判斷sdcard是否存在
        if (isSdCardExist) {
            File MyPetRoot = new File(getMyPetRootDirectory());
            if (!MyPetRoot.exists()) {
                try {
                    MyPetRoot.mkdir();
                    Log.d(TAG, "mkdir success");
                } catch (Exception e) {
                    Log.e(TAG, "exception->" + e.toString());
                }
            }
        }
    }
}

        到這裡,有關圖片獲取的內容就都結束了

4.圖片裁剪

        前面傳入的Uri處理好了,一般裁剪不會出什麼問題,只有一個可能出現OOM(out of memory)問題,先上程式碼

private void startPhotoZoom(Uri uri) {
        Log.d(TAG,"Uri = "+uri.toString());
        //儲存裁剪後的圖片
        File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
        try{
            if(cropFile.exists()){
                cropFile.delete();
                Log.e(TAG,"delete");
            }
        }catch(Exception e){
            e.printStackTrace();
        }
        Uri cropUri;
        cropUri = Uri.fromFile(cropFile);

        Intent intent = new Intent("com.android.camera.action.CROP");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
            intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        }
        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", false);

        Log.e(TAG,"cropUri = "+cropUri.toString());

        intent.putExtra(MediaStore.EXTRA_OUTPUT, cropUri);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        intent.putExtra("noFaceDetection", true); // no face detection
        startActivityForResult(intent, REQUEST_SMALL_IMAGE_CUTTING);
    }

主要問題在這一句

intent.putExtra("return-data", false);

如果設定成true,那麼裁剪得到的圖片將會直接以Bitmap的形式快取到記憶體中,Bitmap是相當大的,如果手機記憶體不足,或者手機分配的記憶體不足會導致OOM問題而閃退。當然,實際上現在的手機300X300一般不會OOM,但為了保險起見和可能需要更大圖片的需求,最好將“return-data”置為false。

5.儲存並顯示圖片

        回撥的程式碼在上面已經貼過了,這裡可能會有一個疑問

case REQUEST_SMALL_IMAGE_CUTTING:
                    Log.e(TAG,"before show");
                    File cropFile=new File(PictureUtil.getMyPetRootDirectory(),"crop.jpg");
                    Uri cropUri = Uri.fromFile(cropFile);
                    setPicToView(cropUri);
                    break;

        按理說應該分版本獲取Uri,否則會丟擲異常。實際上並非如此,我之前沒有搞明白7.0的Uri保護到底是做什麼的,後來看了文件才明白 : 

一個應用提供自身檔案給其它應用使用時,如果給出一個file://格式的URI的話,應用會丟擲FileUriExposedException

        也就是說提供給外界時才需要fileprovider,也就是呼叫相機、相簿、裁剪的時候才需要,自己使用是不需要的。谷歌做出這個改動的原因是 

谷歌認為目標app可能不具有檔案許可權,會造成潛在的問題。所以讓這一行為快速失敗。

好了,基本上所有的坑都踩完了,下面補上其他的程式碼

public void setPicToView(Uri uri)  {
        if (uri != null) {
            Bitmap photo = null;
            try {
                photo = BitmapFactory.decodeStream(getContentResolver().openInputStream(uri));
            }catch (FileNotFoundException e){
                e.printStackTrace();
            }
            // 建立 Icon 資料夾
            if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
                //String storage = Environment.getExternalStorageDirectory().getPath();
                File dirFile = new File(PictureUtil.getMyPetRootDirectory(),  "Icon");
                if (!dirFile.exists()) {
                    if (!dirFile.mkdirs()) {
                        Log.d(TAG, "in setPicToView->資料夾建立失敗");
                    } else {
                        Log.d(TAG, "in setPicToView->資料夾建立成功");
                    }
                }
                File file = new File(dirFile, IMAGE_FILE_NAME);
                InfoPrefs.setData(Constants.UserInfo.HEAD_IMAGE,file.getPath());
                //Log.d("result",file.getPath());
                // Log.d("result",file.getAbsolutePath());
                // 儲存圖片
                FileOutputStream outputStream = null;
                try {
                    outputStream = new FileOutputStream(file);
                    photo.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
                    outputStream.flush();
                    outputStream.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            // 在檢視中顯示圖片
            showHeadImage();
            //circleImageView_user_head.setImageBitmap(InfoPrefs.getData(Constants.UserInfo.GEAD_IMAGE));
        }
    }
private void showHeadImage() {
        boolean isSdCardExist = Environment.getExternalStorageState().equals(
                Environment.MEDIA_MOUNTED);// 判斷sdcard是否存在
        if (isSdCardExist) {

            String path = InfoPrefs.getData(Constants.UserInfo.HEAD_IMAGE);// 獲取圖片路徑

            File file = new File(path);
            if (file.exists()) {
                Bitmap bm = BitmapFactory.decodeFile(path);
                // 將圖片顯示到ImageView中
                circleImageView_user_head.setImageBitmap(bm);
            }else{
                Log.e(TAG,"no file");
                circleImageView_user_head.setImageResource(R.drawable.huaji);
            }
        } else {
            Log.e(TAG,"no SD card");
            circleImageView_user_head.setImageResource(R.drawable.huaji);
        }
    }

huaji是一張在沒有頭像情況下的預設頭像

6.總結

        最後做一個總結,如果按照以上程式碼執行的話,會在SD卡根目錄下建立一下檔案和資料夾

        dir:MyPet

        file:user_head_icon.jpg

        file:crop.jpg

                dir:Icon

        file:user_head_icon.jpg


    MyPet/user_head_icon.jpg是拍照得到的原圖,MyPet/crop.jpg是裁剪得到的圖片,我們最後顯示的是MyPet/Icon/user_head_icon.jpg,所以前兩個如果不需要可以在最後刪掉,刪掉的程式碼我就不寫了。


        最後的最後,UserInfoActivity和整個專案的完整程式碼可在我的GitHub獲取:NeedKwok

       之後我也可能寫一下如何實現一個鬧鐘




相關文章