Android 圖片保存,相冊刷新(版本兼容)

Android 常見功能保存圖片十分常用,近年來隨著Android版本更新,逐漸收緊了App的權限,導致App存儲圖片需要做的兼容性問題越來越多.

原因:

  • 廠商定制存儲方式
  • 不同版本存儲方式不一致
  • Android Q 沙盒機制

導致的問題:

  • 文件存儲異常
  • 相冊不展示下載的圖片
  • 相冊展示重復的下載圖片

Android Q (10) 新增了分區存儲
針對外部存儲的過濾視圖,可提供對特定于應用的文件和媒體集合的訪問權限,所以圖片保存的時候需要存儲到指定App文件夾下才能保存文件

兼容實現:

1. 處理Android Q 存儲地址問題

  /**
     * 根據 Android Q 區分地址
     *
     * @param context
     * @return
     */
    public static String getPath(Context context) {
        // equalsIgnoreCase() 忽略大小寫
        String fileName = "";
        if (Build.VERSION.SDK_INT >= 29) {
            fileName = context.getExternalFilesDir("").getAbsolutePath() + "/current/";
        } else {
            if ("Xiaomi".equalsIgnoreCase(Build.BRAND)) { // 小米手機
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else if ("HUAWEI".equalsIgnoreCase(Build.BRAND)) {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else if ("HONOR".equalsIgnoreCase(Build.BRAND)) {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else if ("OPPO".equalsIgnoreCase(Build.BRAND)) {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else if ("vivo".equalsIgnoreCase(Build.BRAND)) {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else if ("samsung".equalsIgnoreCase(Build.BRAND)) {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/Camera/";
            } else {
                fileName = Environment.getExternalStorageDirectory().getPath() + "/DCIM/";
            }
        }
        File file = new File(fileName);
        if (file.mkdirs()) {
            return fileName;
        }
        return fileName;
    }

2. 判斷Android Q 版本

/**
     * 判斷android Q  (10 ) 版本
     *
     * @return
     */
    public static boolean isAdndroidQ() {
        return Build.VERSION.SDK_INT >= 29;
    }

3. 復制文件

/**
     * 復制文件
     *
     * @param oldPathName
     * @param newPathName
     * @return
     */
    public static boolean copyFile(String oldPathName, String newPathName) {
        try {
            File oldFile = new File(oldPathName);
            if (!oldFile.exists()) {
                return false;
            } else if (!oldFile.isFile()) {
                return false;
            } else if (!oldFile.canRead()) {
                return false;
            }

            FileInputStream fileInputStream = new FileInputStream(oldPathName);
            FileOutputStream fileOutputStream = new FileOutputStream(newPathName);
            byte[] buffer = new byte[1024];
            int byteRead;
            while (-1 != (byteRead = fileInputStream.read(buffer))) {
                fileOutputStream.write(buffer, 0, byteRead);
            }
            fileInputStream.close();
            fileOutputStream.flush();
            fileOutputStream.close();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

4. 插入相冊


    /**
     * 插入相冊 部分機型適配(區分手機系統版本 Android Q)
     *
     * @param context
     * @param filePath
     * @return
     */
    public static boolean insertMediaPic(Context context, String filePath) {
        if (TextUtils.isEmpty(filePath)) return false;
        File file = new File(filePath);
        //判斷android Q  (10 ) 版本
        if (isAdndroidQ()) {
            if (file == null || !file.exists()) {
                return false;
            } else {
                try {
                    MediaStore.Images.Media.insertImage(context.getContentResolver(), file.getAbsolutePath(), file.getName(), null);
                    return true;
                } catch (Exception e) {
                    e.printStackTrace();
                    return false;
                }
            }
        } else {
            ContentValues values = new ContentValues();
            values.put(MediaStore.Images.Media.DATA, file.getAbsolutePath());
            values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
            values.put(MediaStore.Images.ImageColumns.DATE_TAKEN, System.currentTimeMillis() + "");
            context.getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
            context.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + file.getAbsolutePath())));
            return true;
        }

    }

項目實現

1.Android Q 圖片存儲適配

1.1 res/xml/文件夾下 創建 app_files.xml

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <paths xmlns:android="http://schemas.android.com/apk/res/android">
        <external-path
            name="external_files"
            path="" />
        <path>
            <root-path
                name="root_path"
                path="." />
        </path>

        <external-path
            name="camera_photos"
            path="" />

        <external-path
            name="external_storage_root"
            path="." />
        <grant-uri-permission
            android:path="string"
            android:pathPattern="string"
            android:pathPrefix="string" />
    </paths>
</resources>

1.2 AndroidManifest.xml 中 app_files文件配置


AndroidManifest.xml.png

2.圖片 下載 以及保存(Kotlin 攜程下載圖片)

圖片存儲.png
package com.wu.material.activity

import android.Manifest
import android.app.Activity
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.databinding.DataBindingUtil
import com.bumptech.glide.Glide
import com.wu.material.R
import com.wu.material.databinding.ActivityCoroutinesBinding
import com.wu.material.util.FileSaveUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*


/**
 * @author wkq
 *
 * @date 2022年03月03日 12:44
 *
 *@des
 *
 */

class CoroutinesActivity : AppCompatActivity() {

    var databinding: ActivityCoroutinesBinding? = null
    //權限Code
    var REQUEST_CODE_LAUNCH = 10011
    var permissionsREAD = arrayOf(
        Manifest.permission.READ_EXTERNAL_STORAGE,
        Manifest.permission.WRITE_EXTERNAL_STORAGE)

    var path = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Ffile02.16sucai.com%2Fd%2Ffile%2F2014%2F0829%2F372edfeb74c3119b666237bd4af92be5.jpg&refer=http%3A%2F%2Ffile02.16sucai.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1648708406&t=ca9d3a371ddad53fbc5fa074db2090cc"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        databinding = DataBindingUtil.setContentView<ActivityCoroutinesBinding>(
            this,
            R.layout.activity_coroutines
        )
        // 判斷權限
       var isHave= checkPermissions(this,permissionsREAD,REQUEST_CODE_LAUNCH)
        if (isHave){
            showView()
        }
    }

    private fun showView() {
        Glide.with(this).load(path).into(databinding!!.ivIcon)
        databinding!!.btSave.setOnClickListener {
            savePic(path)
        }
    }




    fun savePic(path: String) {

        //攜程
        GlobalScope.launch(Dispatchers.IO) {
            var file = Glide.with(this@CoroutinesActivity).asFile().load(path).submit().get()
            Log.e("",file.absolutePath)
            // 文件夾位置
           var parentPath= FileSaveUtil.getPath(this@CoroutinesActivity)
            //文件名
           var fileName= System.currentTimeMillis().toString()+".png"
            //新文件文件地址
            var filePath=parentPath+fileName
            //復制地址(部分機型 不復制到指定文件夾,相冊不更新)
            FileSaveUtil.copyFile(file.path,filePath)

            var isSave=FileSaveUtil.insertMediaPic(this@CoroutinesActivity,filePath)

            withContext(Dispatchers.Main) {
                //主線程里更新 UI
                if (isSave){
                    Toast.makeText(this@CoroutinesActivity,"成功了",Toast.LENGTH_SHORT).show()
                }else{
                    Toast.makeText(this@CoroutinesActivity,"失敗了",Toast.LENGTH_SHORT).show()
                }
            }

        }
    }

    /**
     * 判斷權限
     */
    fun onRequestPermissionsResult(
        activity: Activity?,
        requestCode: Int,
        permissions: Array<String?>,
        grantResults: IntArray
    ): BooleanArray? {
        var result = true
        var isNerverAsk = false
        val length = grantResults.size
        for (i in 0 until length) {
            val permission = permissions[i]
            val grandResult = grantResults[i]
            if (grandResult == PackageManager.PERMISSION_DENIED) {
                result = false
                if (!ActivityCompat.shouldShowRequestPermissionRationale(
                        activity!!,
                        permission!!
                    )
                ) isNerverAsk = true
            }
        }
        return booleanArrayOf(result, isNerverAsk)
    }

    /**
     * 授權結果回調
     */
    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String?>, grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if (requestCode == REQUEST_CODE_LAUNCH) {
            val hasPermissions = onRequestPermissionsResult(this, requestCode, permissions, grantResults)
            if (hasPermissions!![0]) {
                showView()
            } else {
                Toast.makeText(this@CoroutinesActivity,"沒權限",Toast.LENGTH_SHORT).show()

            }
        }
    }

    /**
     * 校驗權限
     */
    fun checkPermissions(
        activity: Activity?,
        permissions: Array<String>,
        requestCode: Int
    ): Boolean { //Android6.0以下默認有權限
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return true
        val needList: MutableList<String> = ArrayList()
        var needShowRationale = false
        val length = permissions.size
        for (i in 0 until length) {
            val permisson = permissions[i]
            if (TextUtils.isEmpty(permisson)) continue
            if (ActivityCompat.checkSelfPermission(activity!!, permisson)
                != PackageManager.PERMISSION_GRANTED
            ) {
                needList.add(permisson)
                if (ActivityCompat.shouldShowRequestPermissionRationale(
                        activity,
                        permisson
                    )
                ) needShowRationale = true
            }
        }
        return if (needList.size != 0) {
            if (needShowRationale) {
                //
                return false
            }
            ActivityCompat.requestPermissions(activity!!, needList.toTypedArray(), requestCode)
            false
        } else {
            true
        }
    }


}

注意:

  • 魅族手機個別版本下載到本地的圖片相冊刷新不出來
  • 個別手機相冊刷新會重復

總結

Android 系統隨著系統版本的更新,以及國內各大廠商各大魔改 導致圖片下載相冊更新出現問題,這里項目中做的兼容做了記錄,隨后,項目中逐漸出現的問題再更新

寫作不易,歡迎點贊

參考文獻

1.Android Q 行為變更

2.Demo源碼地址

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容