Android調(diào)取系統(tǒng)相機(jī)拍照獲取到拍攝照片或從相冊(cè)中直接選取照片后展示上傳是Android開發(fā)中很常見的一個(gè)功能,實(shí)現(xiàn)的思路主要是:
自Android 6.0以后對(duì)某些涉及用戶隱私權(quán)限的獲取需要?jiǎng)討B(tài)獲取指郁,所以首先是檢查權(quán)限蚓土,如沒有權(quán)限則動(dòng)態(tài)申請(qǐng)權(quán)限甩挫,這里我們需要用到的權(quán)限是WRITE_EXTERNAL_STORAGE和CAMERA。
自Android 7.0后系統(tǒng)禁止應(yīng)用向外部公開file://URI 棍弄,因此需要FileProvider來向外界傳遞URI脊阴。
獲取到拍照后的照片,按照現(xiàn)在的手機(jī)拍照文件大小來說不做處理直接展示很容易發(fā)生OOM责嚷,因此這一步需要對(duì)圖片做壓縮處理鸳兽。
一、動(dòng)態(tài)申請(qǐng)權(quán)限
首先在Mainfest.xml文件中聲明權(quán)限
<uses-permission android:name="android.permission.CAMERA"/>
<!-- 因?yàn)榕恼招枰獙懭胛募?所以需要申請(qǐng)讀取內(nèi)存的權(quán)限 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
接下來點(diǎn)擊Button按鈕模擬調(diào)取拍照
private static final int REQUEST_PERMISSION_CODE = 101;
mButtonTakePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {//大于Android 6.0
if (!checkPermission()) { //沒有或沒有全部授權(quán)
requestPermissions(); //請(qǐng)求權(quán)限
}
} else {
takePhoto();//拍照邏輯
}
}
});
//檢查權(quán)限
private boolean checkPermission() {
//是否有權(quán)限
boolean haveCameraPermission = ContextCompat.checkSelfPermission(mContext, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED;
boolean haveWritePermission = ContextCompat.checkSelfPermission(mContext,
Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED;
return haveCameraPermission && haveWritePermission;
}
// 請(qǐng)求所需權(quán)限
@RequiresApi(api = Build.VERSION_CODES.M)
private void requestPermissions() {
requestPermissions(new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, REQUEST_PERMISSION_CODE);
}
// 請(qǐng)求權(quán)限后會(huì)在這里回調(diào)
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
switch (requestCode) {
case REQUEST_PERMISSION_CODE:
boolean allowAllPermission = false;
for (int i = 0; i < grantResults.length; i++) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {//被拒絕授權(quán)
allowAllPermission = false;
break;
}
allowAllPermission = true;
}
if (allowAllPermission) {
takePhotoOrPickPhoto();//開始拍照或從相冊(cè)選取照片
} else {
Toast.makeText(mContext, "該功能需要授權(quán)方可使用", Toast.LENGTH_SHORT).show();
}
break;
}
}
在點(diǎn)擊拍照按鈕后罕拂,調(diào)用 ContextCompat.checkSelfPermission( )
方法檢查是否有權(quán)限贸铜,方法返回值為0說明已經(jīng)授權(quán)。沒授權(quán)的情況下聂受,調(diào)用requestPermissions( )
方法,該方法的第一個(gè)參數(shù)為一個(gè)數(shù)組烤镐,數(shù)組中的值為你要申請(qǐng)的一個(gè)或多個(gè)權(quán)限的值蛋济,第二個(gè)參數(shù)為請(qǐng)求碼。
調(diào)用requestPermission( )
方法后我們需要在Activity中重寫onRequestPermissionsResult()
方法炮叶,在該方法中會(huì)得到回調(diào)結(jié)果碗旅,方法中第一個(gè)參數(shù)是請(qǐng)求碼,第二個(gè)參數(shù)是我們申請(qǐng)的權(quán)限數(shù)組镜悉,第三個(gè)參數(shù)數(shù)組中每一個(gè)值對(duì)應(yīng)申請(qǐng)的每一個(gè)權(quán)限的返回值祟辟,值為0或-1,0代表授權(quán)侣肄,-1代表拒絕授權(quán)旧困。源碼如下
/**
* Permission check result: this is returned by {@link #checkPermission}
* if the permission has been granted to the given package.
*/
public static final int PERMISSION_GRANTED = 0;//授權(quán)成功
/**
* Permission check result: this is returned by {@link #checkPermission}
* if the permission has not been granted to the given package.
*/
public static final int PERMISSION_DENIED = -1;//拒絕授權(quán)
二、FileProvider
在獲取所有所需的權(quán)限后,我們調(diào)取系統(tǒng)相機(jī)拍照
private void takePhoto() {
// 步驟一:創(chuàng)建存儲(chǔ)照片的文件
String path = getFilesDir() + File.separator + "images" + File.separator;
File file = new File(path, "test.jpg");
if(!file.getParentFile().exists())
file.getParentFile().mkdirs();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
//步驟二:Android 7.0及以上獲取文件 Uri
mUri = FileProvider.getUriForFile(PickPicActivity.this, "com.example.admin.custmerviewapplication", file);
} else {
//步驟三:獲取文件Uri
mUri = Uri.fromFile(file);
}
//步驟四:調(diào)取系統(tǒng)拍照
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
startActivityForResult(intent, REQUEST_TAKE_PHOTO_CODE);
}
在Android 7.0之前我們只需要步驟一吼具、三僚纷、四即可調(diào)取系統(tǒng)相機(jī)拍照,在此之后的話直接這么調(diào)取會(huì)報(bào)android.os.FileUriExposedException
異常拗盒。所以我們需要對(duì)Android 7.0及以后的機(jī)型適配怖竭,采用FileProvider方式。
1. FileProvider是什么
FileProvider是ContentProvider的一個(gè)子類陡蝇,用于應(yīng)用程序之間私有文件的傳遞痊臭。自Android 7.0后系統(tǒng)禁止應(yīng)用向外部公開file://URI ,因此需要FileProvider來向外界傳遞URI登夫,傳遞的形式是content : //Uri广匙,使用時(shí)需要在清單文件中注冊(cè)。
2.注冊(cè)清單文件
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.admin.custmerviewapplication"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
...
</application>
</manifest>
解釋上面provider標(biāo)簽的意思:
name 因?yàn)槲覀兪褂玫氖荲4包下的FileProvider 悼嫉,所以name的值就是V4包下FileProvider的相對(duì)路徑值艇潭。當(dāng)然我們也可以自定義類繼承于FileProvider,這時(shí)候name的值就是我們自定義類的相對(duì)路徑了
authorities 可以理解為標(biāo)識(shí)符戏蔑,是我們自己自定義的蹋凝。我們代碼中調(diào)用getUriForFile方法獲取Uri時(shí)第二個(gè)參數(shù)就是這里我們定義的值。
exported 代表是否可以輸出被外部程序使用总棵,填false就行鳍寂。
android:grantUriPermissions 是否允許為文件授予臨時(shí)權(quán)限,必須為true
<meta-data>標(biāo)簽里配置的內(nèi)容是用來指定那個(gè)文件夾下的文件是可被共享的情龄。
name 為固定的值android.support.FILE_PROVIDER_PATHS迄汛。
path 是對(duì)應(yīng)的xml文件路徑,@xml/file_paths代表在xml文件下的file_paths文件骤视。
3.指定可共享的文件路徑
我們?cè)趓es目錄下新建一個(gè)xml文件夾鞍爱,在文件夾下創(chuàng)建一個(gè)名為file_paths的xml文件
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<!--files-path 相當(dāng)于 getFilesDir()-->
<files-path name="my_images" path="images"/>
<!--cache-path 相當(dāng)于 getCacheDir()-->
<cache-path name="lalala" path="cache_image"/>
<!--external-path 相當(dāng)于 Environment.getExternalStorageDirectory()-->
< external-path name="hahaha" path="comeOn"/>
<!--external-files-path 相當(dāng)于 getExternalFilesDir("") -->
<external-files-path name="paly" path="freeSoft"/>
<!--external-cache-path 相當(dāng)于 getExternalCacheDir() -->
<external-cache-path name="lei" path="."/>
...
</paths>
files-path所代表的路徑等于getFilesDir(),打印getFileDir( )它的路徑是 /data/user/0/包名/files专酗。什么意思呢睹逃,<files-path name="my_images" path="images"/>
的意思就是/data/user/0/包名/files + "/files-path標(biāo)簽中path的值/"
路徑下的文件是可共享的,在生成Uri時(shí)name的值my_images會(huì)替代上面的路徑/data/user/0/包名/files / images /
向外暴露祷肯。最終的Uri會(huì)是content : //com.example.admin.custmerviewapplication / my_images / test.jpg
我們?cè)诖a中獲取Uri的方法就是FileProvider.getUriForFile("上下文"沉填,"清單文件中authorities的值","共享的文件")佑笋;
三翼闹、圖片獲取并壓縮
我們調(diào)用startActivityForResult(intent, REQUEST_TAKE_PHOTO_CODE);
進(jìn)行拍照,拍照結(jié)束后會(huì)回調(diào)onActivityResult( )方法蒋纬。
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK && requestCode == REQUEST_TAKE_PHOTO_CODE) {//獲取系統(tǒng)照片上傳
Bitmap bm = null;
try {
bm = getBitmapFormUri(mUri);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
mImageView.setImageBitmap(bm);
}
}
通過Uri直接獲取圖片加載到內(nèi)存然后顯示在ImageView很容易發(fā)生OOM猎荠,所以還需做進(jìn)一步的圖片壓縮坚弱。
public Bitmap getBitmapFormUri(Uri uri) throws FileNotFoundException, IOException {
InputStream input = getContentResolver().openInputStream(uri);
//這一段代碼是不加載文件到內(nèi)存中也得到bitmap的真是寬高,主要是設(shè)置inJustDecodeBounds為true
BitmapFactory.Options onlyBoundsOptions = new BitmapFactory.Options();
onlyBoundsOptions.inJustDecodeBounds = true;//不加載到內(nèi)存
onlyBoundsOptions.inDither = true;//optional
onlyBoundsOptions.inPreferredConfig = Bitmap.Config.RGB_565;//optional
BitmapFactory.decodeStream(input, null, onlyBoundsOptions);
input.close();
int originalWidth = onlyBoundsOptions.outWidth;
int originalHeight = onlyBoundsOptions.outHeight;
if ((originalWidth == -1) || (originalHeight == -1))
return null;
//圖片分辨率以480x800為標(biāo)準(zhǔn)
float hh = 800f;//這里設(shè)置高度為800f
float ww = 480f;//這里設(shè)置寬度為480f
//縮放比法牲,由于是固定比例縮放史汗,只用高或者寬其中一個(gè)數(shù)據(jù)進(jìn)行計(jì)算即可
int be = 1;//be=1表示不縮放
if (originalWidth > originalHeight && originalWidth > ww) {//如果寬度大的話根據(jù)寬度固定大小縮放
be = (int) (originalWidth / ww);
} else if (originalWidth < originalHeight && originalHeight > hh) {//如果高度高的話根據(jù)寬度固定大小縮放
be = (int) (originalHeight / hh);
}
if (be <= 0)
be = 1;
//比例壓縮
BitmapFactory.Options bitmapOptions = new BitmapFactory.Options();
bitmapOptions.inSampleSize = be;//設(shè)置縮放比例
bitmapOptions.inDither = true;
bitmapOptions.inPreferredConfig = Bitmap.Config.RGB_565;
input = getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(input, null, bitmapOptions);
input.close();
return compressImage(bitmap);//再進(jìn)行質(zhì)量壓縮
}
public Bitmap compressImage(Bitmap image) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
image.compress(Bitmap.CompressFormat.JPEG, 100, baos);//質(zhì)量壓縮方法,這里100表示不壓縮拒垃,把壓縮后的數(shù)據(jù)存放到baos中
int options = 100;
while (baos.toByteArray().length / 1024 > 100) { //循環(huán)判斷如果壓縮后圖片是否大于100kb,大于繼續(xù)壓縮
baos.reset();//重置baos即清空baos
//第一個(gè)參數(shù) :圖片格式 停撞,第二個(gè)參數(shù): 圖片質(zhì)量,100為最高悼瓮,0為最差 戈毒,第三個(gè)參數(shù):保存壓縮后的數(shù)據(jù)的流
image.compress(Bitmap.CompressFormat.JPEG, options, baos);//這里壓縮options,把壓縮后的數(shù)據(jù)存放到baos中
options -= 10;//每次都減少10
if (options<=0)
break;
}
ByteArrayInputStream isBm = new ByteArrayInputStream(baos.toByteArray());//把壓縮后的數(shù)據(jù)baos存放到ByteArrayInputStream中
Bitmap bitmap = BitmapFactory.decodeStream(isBm, null, null);//把ByteArrayInputStream數(shù)據(jù)生成圖片
return bitmap;
}
壓縮的步驟分為兩步横堡,第一步是先得到bitmap的真實(shí)寬高計(jì)算壓縮比例埋市,得到壓縮比例后進(jìn)行初步壓縮。第二步將初步壓縮的bitmap進(jìn)行質(zhì)量壓縮得到最終的圖片命贴。
從相冊(cè)中選取圖片步驟和調(diào)取相機(jī)拍照的步驟一致道宅,只是創(chuàng)建的intent和在onActivtyResult回調(diào)時(shí)獲取的Uri不同。
//調(diào)用相冊(cè)
Intent intent = new Intent(Intent.ACTION_PICK,android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_UR);
startActivityForResult(intent, PICK_IMAGE_CODE);
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
//獲取圖片路徑
if (requestCode == PICK_IMAGE_CODE && resultCode == Activity.RESULT_OK && data != null) {
mUri = data.getData();//通過getData獲取到Uri
.
.
.
}
}