Android7.0的適配

weixin_34194087發表於2017-05-25

關於Android7.0的適配

最近在軟體的維護和更新過程中,瞭解到一些關於Android7.0的適配,在這裡和大家分享一下,據我所知,需要對Notification、拍照、圖片的裁剪進行適配

一、Notification

關於Android7.0 Notication增加的特性,在此我就不詳細說明了,因為關於這類介紹的文章,早有一些大牛已經發布過了。我主要講的是我在應用更新功能中使用Notification踩到的坑。可以這麼說,應用更新功能對於每個上線App都必不少,因為App的需求或者功能,都是會在不斷的變化和完善的。

我遇到的情況是:在Android7.0以下,以下程式碼是顯示下載App新版本成功後的通知欄,點選可以跳轉到安裝App的頁面。

    NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);

    // 建立一個開啟安裝App介面的意圖
    Intent installIntent = new Intent();
    installIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    installIntent.setAction(Intent.ACTION_VIEW);
    installIntent.setDataAndType(Uri.fromFile(file),
                "application/vnd.android.package-archive");


    // 建立一個Notification並設定相關屬性         
    NotificationCompat.Builder builder = new NotificationCompat.Builder(context);
    builder.setAutoCancel(false)//通知設定不會自動顯示
            .setShowWhen(true)//顯示時間
            .setSmallIcon(notificationIconResId)//設定通知的小圖示
            .setContentTitle("通知的標題")
            .setContentText("下載完成,點選安裝");//設定通知的內容

    //建立PendingIntent,用於點選通知欄後實現的意圖操作
    PendingIntent pendingIntent = getActivity(context, 0, installIntent, PendingIntent.FLAG_UPDATE_CURRENT);
    builder.setContentIntent(pendingIntent);

    Notification notification = builder.build();
    notification.defaults = Notification.DEFAULT_SOUND;// 設定為預設的聲音
    notification.flags = isCanClear ? Notification.FLAG_ONLY_ALERT_ONCE : Notification.FLAG_ONLY_ALERT_ONCE | Notification.FLAG_NO_CLEAR;
    manager.notify(0, notification);// 顯示通知

以上程式碼,在Android7.0以下,可以實現點選通知欄攔跳轉到安裝App介面的功能,但是在安卓7.0或以上,點選事件就出現問題了,點選通知欄沒有任何反應,通知欄也不會顯示,但是會有error等級的log輸出,出現FileUriExposedException這樣的異常,原因是Andorid7.0的“私有目錄被限制訪問”,“StrictMode API 政策”。由於從Android7.0開始,直接使用真實的路徑的Uri會被認為是不安全的,會丟擲一個FileUriExposedException這樣的異常。需要使用FileProvider,選擇性地將封裝過的Uri共享到外部。於是,需要對上面的程式碼進行修改。

         ......

     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 
        //判斷版本是否在7.0以上
        Uri apkUri =
                    FileProvider.getUriForFile(context, 
                    "com.chaychan.demo" + ".fileprovider", 
                    file);
        //新增這一句表示對目標應用臨時授權該Uri所代表的檔案
        installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        installIntent.setDataAndType(apkUri, "application/vnd.android.package-archive");
    } else {
        installIntent.setDataAndType(Uri.fromFile(file),
                "application/vnd.android.package-archive");
    }
         ......

以上程式碼增加了對系統版本的判斷,如果是Andorid7.0或以上,則不再使用Uri.fromFile()方法獲取檔案的Uri,而是通過使用FileProvider(support.v4提供的類)的getUriForFile()。同時要新增多這麼一行程式碼 installIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

由於FileProvider是繼承ContentProvider,屬於四大元件之一,需要在AndroidManifest.xml中配置,配置如下:

    <!--版本更新所要用到的 fileProvider 用於相容7.0通知欄的安裝-->
    <provider
        android:name="android.support.v4.content.FileProvider"
        android:authorities="${applicationId}.fileprovider"
        android:exported="false"
        android:grantUriPermissions="true">
        <!--後設資料-->
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/file_provider_paths"/>
    </provider>

配置中的authorities按照江湖規矩一般加上包名,${applicationId}是獲取當前專案的包名,前提是defaultConfig{}閉包中要有applicationId屬性。

 defaultConfig {
    applicationId "com.chaychan.demo"
   
}

<meta/>標籤中的resource填寫配置fileprovider的配置檔案,在res資源目錄下新建xml檔案下,在該資料夾下建立file_provider_paths.xml檔案,這個xml檔名並不是一定要這麼起,只要和清單檔案中配置的檔名一致就行。

6194067-6707dc17a3b97711.png
image

file_provider_paths.xml的內容如下

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths>
        <external-path path="" name="myFile"></external-path>
    </paths>
</resources>

上述程式碼中path="",是有特殊意義的,它程式碼根目錄,也就是說你可以向其它的應用共享根目錄及其子目錄下任何一個檔案了,如果你將path設為path="pictures", 那麼它代表著根目錄下的pictures目錄(eg:/storage/emulated/0/pictures),如果你向其它應用分享pictures目錄範圍之外的檔案是不行的。

完成上述的程式碼修改和FileProvider的配置後,就可以相容Android7.0或以上系統了,點選通知欄可以跳轉到安裝App的介面了。到此,關於Notification在Android7.0的相容就完成了。

拍照

在Andorid7.0以下,以下程式碼可以實現跳轉到拍照介面的功能,拍完照會在對應開啟拍照介面的Activity中的onActivityResult()方法中回撥。

// 指定呼叫相機拍照後照片的儲存路徑
File imgFile = new File(imgPath);
Uri imgUri = null;
imgUri = Uri.fromFile(imgFile);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);
startActivityForResult(intent, takePhotoRequestCode);

但是在Android7.0或者以上,以上程式碼在呼叫拍照功能的時候,會導致應用Crash,會報FileUriExposedException異常,需要對以上程式碼進行修改,對使用App的系統版本進行判斷,修改後程式碼如下:

// 指定呼叫相機拍照後照片的儲存路徑
File imgFile = new File(imgPath);
Uri imgUri = null;
if (Build.VERSION.SDK_INT >= 24){
    //如果是7.0或以上,使用getUriForFile()獲取檔案的Uri
    imgUri = FileProvider.getUriForFile(this, "com.chaychan.demo" + ".fileprovider",imgFile);
}else {
    imgUri = Uri.fromFile(imgFile);
}

Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);
startActivityForResult(intent, REQ_TAKE_PHOTO);

修改完成後,在Android7.0或以上的手機呼叫就可以呼叫拍照功能了,拍照完後,在onActivityResult()回撥中,imgFile就是儲存拍照後圖片的檔案物件,就可以進行相應的處理,比如說對圖片進行裁剪。

三、圖片的裁剪

在Android7.0以下,以下程式碼可以呼叫手機自帶的圖片裁剪功能:

/**
 * 發起剪裁圖片的請求
 * @param activity 上下文
 * @param srcFile 原檔案的File
 * @param output 輸出檔案的File
 * @param requestCode 請求碼
 */
public static void startPhotoZoom(Activity activity, File srcFile, File output,int requestCode) {

    Intent intent = new Intent("com.android.camera.action.CROP");
    intent.setDataAndType(Uri.fromFile(srcFile), "image/*");
    // crop為true是設定在開啟的intent中設定顯示的view可以剪裁
    intent.putExtra("crop", "true");

    // aspectX aspectY 是寬高的比例
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);

    // outputX,outputY 是剪裁圖片的寬高
    intent.putExtra("outputX", 800);
    intent.putExtra("outputY", 480);

    intent.putExtra("return-data", false);// true:不返回uri,false:返回uri
    intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(output));
    intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());

    activity.startActivityForResult(intent, requestCode);
}

但是在Android7.0或以上,以上程式碼就需要進行修改,修改如下:

/**
 * 發起剪裁圖片的請求
 * @param activity 上下文
 * @param srcFile 原檔案的File
 * @param output 輸出檔案的File
 * @param requestCode 請求碼
 */
public static void startPhotoZoom(Activity activity, File srcFile, File output,int requestCode) {

    ......

    //主要修改這行程式碼,不再使用Uri.fromFile()方法獲取檔案的Uri
    intent.setDataAndType(getImageContentUri(activity,srcFile), "image/*");

    ......
}

getImageContentUri()方法具體如下:

/**安卓7.0裁剪根據檔案路徑獲取uri*/
public static Uri getImageContentUri(Context context, File imageFile) {
    String filePath = imageFile.getAbsolutePath();
    Cursor cursor = context.getContentResolver().query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            new String[] { MediaStore.Images.Media._ID },
            MediaStore.Images.Media.DATA + "=? ",
            new String[] { filePath }, null);

    if (cursor != null && cursor.moveToFirst()) {
        int id = cursor.getInt(cursor
                .getColumnIndex(MediaStore.MediaColumns._ID));
        Uri baseUri = Uri.parse("content://media/external/images/media");
        return Uri.withAppendedPath(baseUri, "" + id);
    } else {
        if (imageFile.exists()) {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DATA, filePath);
            return context.getContentResolver().insert(
                    MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
        } else {
            return null;
        }
    }
}

由於自己將發起裁剪請求的方法進行封裝,所以在onActivityResult()中,拍照完成後,如果需要對圖片進行裁剪,則可以這麼操作:

public void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (resultCode) {
        case RESULT_OK://呼叫圖片選擇處理成功
            String zoomImgPath = "";
            Bitmap bm = null;
            File temFile = null;
            File srcFile = null;
            File outPutFile = null;
            switch (requestCode) {
                case REQ_TAKE_PHOTO:// 拍照後在這裡回撥
                    srcFile = new File(imgPath);
                    outPutFile = new File(outputPath);
                    outputUri = Uri.fromFile(outPutFile);
                    FileUtils.startPhotoZoom(this, srcFile, outPutFile, REQ_ZOOM);// 發起裁剪請求
                    break;

                case REQ_ZOOM://裁剪後回撥
                    if (data != null) {
                        if (outputUri != null) {
                            bm = ImageTools.decodeUriAsBitmap(this,outputUri);
                            
                            String scaleImgPath = FileUtils.saveBitmapByQuality(bm, 80);//複製並壓縮到自己的目錄並壓縮

                            //bm可以用於顯示在對應的ImageView中,scaleImgPath是剪裁併壓縮後的圖片的路徑,可以用於上傳操作
                            ...... //實現自己的業務邏輯
                            
                        }
                    } else {
                        UIUtils.showToast("選擇圖片發生錯誤,圖片可能已經移位或刪除");
                    }
                    break;
            }
    }
}

ImageTools的decodeUriAsBitmap()方法,是將Uri轉換為Bitmap物件,具體的程式碼如下:

public static Bitmap decodeUriAsBitmap(Context context,Uri uri) {
    Bitmap bitmap = null;
    try {
        // 先通過getContentResolver方法獲得一個ContentResolver例項,
        // 呼叫openInputStream(Uri)方法獲得uri關聯的資料流stream
        // 把上一步獲得的資料流解析成為bitmap
        bitmap = BitmapFactory.decodeStream(context.getContentResolver().openInputStream(uri));
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        return null;
    }
    return bitmap;
}

FileUtils.saveBitmapByQuality()方法,是對圖片進行壓縮,第一個引數傳入的是圖片的Bitmap物件,第二個引數是壓縮的保留率,比如上面使用的是80,即壓縮後為原來的80%,則是對其壓縮了20%,具體的程式碼如下:

/**
 * 按質量壓縮bm
 * @param bm
 * @param quality 壓縮儲存率
 * @return
 */
public static String saveBitmapByQuality(Bitmap bm,int quality) {
    String croppath="";
    try {
        File f = new File(FileUtils.generateImgePath());
        //得到相機圖片存到本地的圖片
        croppath=f.getPath();
        if (f.exists()) {
            f.delete();
        }
        FileOutputStream out = new FileOutputStream(f);
        bm.compress(Bitmap.CompressFormat.JPEG,quality, out);
        out.flush();
        out.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
    return croppath;
}

上述程式碼可以實現和相容Android7.0或以上系統的拍照+裁剪圖片的功能了。在這裡順便把呼叫相簿功能寫貼出來吧,畢竟實際開發中需要上傳圖片的時候,通常會讓使用者選擇是拍照或者從相簿中獲取。

Intent intent = new Intent(Intent.ACTION_PICK, null);
intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        "image/*");
startActivityForResult(intent, REQ_ALBUM);

如果需要在選擇完相簿圖片後對圖片進行裁剪,則可以像上面拍照程式碼那樣,需要在onActivityResult()回撥中,發起裁剪請求。這裡一次性貼出onActivityResult的處理:

public void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (resultCode) {
        case RESULT_OK://呼叫圖片選擇處理成功
            String zoomImgPath = "";
            Bitmap bm = null;
            File temFile = null;
            File srcFile = null;
            File outPutFile = null;
            switch (requestCode) {
                case REQ_TAKE_PHOTO:// 拍照後在這裡回撥
                    srcFile = new File(imgPath);
                    outPutFile = new File(outputPath);
                    outputUri = Uri.fromFile(outPutFile);
                    FileUtils.startPhotoZoom(this, srcFile, outPutFile, REQ_ZOOM);// 發起裁剪請求
                    break;

                 case REQ_ALBUM:// 選擇相簿中的圖片
                    if (data != null) {
                        Uri sourceUri = data.getData();
                        String[] proj = {MediaStore.Images.Media.DATA};

                        // 好像是android多媒體資料庫的封裝介面,具體的看Android文件
                        Cursor cursor = managedQuery(sourceUri, proj, null, null, null);

                        // 按我個人理解 這個是獲得使用者選擇的圖片的索引值
                        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                        // 將游標移至開頭 ,這個很重要,不小心很容易引起越界
                        cursor.moveToFirst();
                        // 最後根據索引值獲取圖片路徑
                        String imgPath = cursor.getString(column_index);

                        srcFile = new File(imgPath);
                        outPutFile = new File(FileUtils.generateImgePath());
                        outputUri = Uri.fromFile(outPutFile);
                        FileUtils.startPhotoZoom(this, srcFile, outPutFile, REQ_ZOOM);// 發起裁剪請求
                    }
                    break;


                case REQ_ZOOM://裁剪後回撥
                    if (data != null) {
                        if (outputUri != null) {
                            bm = ImageTools.decodeUriAsBitmap(this,outputUri);
                            
                            String scaleImgPath = FileUtils.saveBitmapByQuality(bm, 80);//複製並壓縮到自己的目錄並壓縮

                            //bm可以用於顯示在對應的ImageView中,scaleImgPath是剪裁併壓縮後的圖片的路徑,可以用於上傳操作
                            ...... //實現自己的業務邏輯
                            
                        }
                    } else {
                        UIUtils.showToast("選擇圖片發生錯誤,圖片可能已經移位或刪除");
                    }
                    break;
            }
    }
}

好了,寫到這裡,我的第一篇部落格終於完成了,花了接近四個小時,因為這是屬於技術性的部落格,文字要求嚴謹,所以不像寫作文那樣信手拈來。不過我儘量將文章寫得通俗易懂,希望可以幫助到更多的人,之前雖然在做專案的時候,有寫過不少筆記,但是從來沒有寫過部落格,要是有哪些地方寫得不夠好,還請各位大牛提出意見,彼此交流和學習。

我之所以萌發寫部落格的念頭,也是因為在開發過程中查詢問題的時候,無意間看到郭霖(人稱郭神)的部落格,於是一篇篇的看了他的部落格,也逐漸瞭解他,對他非常敬佩,昨天問了他寫部落格對提升能力有沒有幫助,他也推薦我寫部落格,所以今天我寫了第一篇部落格,希望可以一直堅持下去,畢竟我對於安卓開發,一直都很熱衷。

相關文章