android實現拍照、相簿選圖、裁剪功能,相容7.0以及小米
現在一般的手機應用都會有上傳頭像的功能,我在實現這個功能的時候遇到很多問題,這裡專門記錄一下。
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是tools:context指定了該layout將在哪個activiy展示,指定了 tools:context的layout可以動態預覽佈局,也就是說我在acticity的onCreate裡改變了某個控制元件,我可以在不run的情況下預覽這個改變的效果。tools還有一些別的功能,我懂的也不多,就不介紹了。bg_info_rl是可變背景,這裡我只是簡單設定了點選和鬆開時候的顏色,程式碼這段結束後貼,ic_chevron_right_black_24dp是Material design提供的小圖示,樣式是“>”,大家可以自行搜尋,這是谷歌官方提供的免費素材包,可以在谷歌MD的官網或者GitHub上獲取。<color name="backgroundWhite">#EBEBEB</color>
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_normalbg_info_rl_pressed<?xml version="1.0" encoding="UTF-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="@color/white" /> </shape>
color<?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 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。
到這裡,拍照的功能已經實現了,如果不想裁剪,可以直接將 sd卡根目錄/MyPet/user_head_icon.jpg這張圖片顯示,接下來講一下相簿選取的實現。@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); } }
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
之後我也可能寫一下如何實現一個鬧鐘
相關文章
- Android 拍照及相簿選取圖片功能,已適配Android6.0、7.0、8.0Android
- Android 圓形頭像 相簿和拍照裁剪選取Android
- Android:呼叫系統相機實現拍照+裁切(相容7.0以上系統)Android
- Android本地圖片上傳(拍照+相簿)Android地圖
- 手機拍照,調取相簿 裁剪,上傳
- Android Studio 呼叫Camera實現拍照功能Android
- Android 呼叫系統相機拍照 . 選取本地相簿Android
- Android呼叫相簿、相機(相容6.0、7.0、8.0)所需新增的許可權Android
- android圖片裁剪拼接實現(二):觸控實現Android
- 直播平臺搭建,Android手機拍照和手機相簿選取圖片的工具Android
- 直播平臺軟體開發,Android 10 拍照和相簿選擇Android
- Android中呼叫攝像頭拍照儲存,並在相簿中選擇圖片顯示Android
- 小程式–儲存圖片到相簿功能實現
- Android自定義拍照實現Android
- (H5)canvas實現裁剪圖片和馬賽克功能,以及又拍雲上傳圖片H5Canvas
- 處理input file限制只能拍照不能選相簿
- iOS自定義拍照框拍照&裁剪(一)iOS
- Android WebView 實現檔案選擇、拍照、錄製視訊、錄音AndroidWebView
- 直播原始碼開發,實現相簿的上傳和縮放裁剪原始碼
- 短視訊程式開發,Android:呼叫系統拍照和相簿Android
- u3d 呼叫android相機和相簿裁剪成圓形3DAndroid
- Android生成圖片並放入相簿Android
- Qt for Android (三) 開啟Android相簿並選一個圖片進行顯示QTAndroid
- python opencv如何實現目標區域裁剪功能PythonOpenCV
- 專案需求討論 - WebView下拍照及圖片選擇功能WebView
- 鴻蒙無許可權實現圖片選擇拍照和錄影片鴻蒙
- 基於React Hook實現圖片的裁剪ReactHook
- 【Android】【opencv】實現攝像頭拍照和錄影AndroidOpenCV
- Android 載入網路圖片 以及實現圓角圖片效果Android
- android7.0以上呼叫系統相機拍照並顯示到ImageView上AndroidView
- android 拍照Android
- Android 7.0新特性——桌面長按圖示出現快捷方式Android
- 手機端上傳照片實現 壓縮、拖放、縮放、裁剪、合成拼圖等功能
- php+html5相容手機端的圖片選取裁剪上傳例項PHPHTML
- 織夢點選圖片實現下一頁功能
- .NetCore實現圖片縮放與裁剪 - 基於ImageSharpNetCore
- Android Camera——拍照Android
- 適配Android4.4~Android11,呼叫系統相機,系統相簿,系統圖片裁剪,轉換檔案(對圖片進行上傳等操作)Android