[Android多媒体一]调用系统相机拍照并存储到指定位置,适配安卓 7.0

项目里想添加几个小功能,拍照、录像、录音。都是调用系统的多媒体,拍摄、录制完毕也不能不管,应该将他们都存储到手机上,方便日后查看、使用。但是开发过程并不是一蹴而就,也是查阅了很多资料,向大佬请教了很多,才成功实现了功能,并且由于是Android 7.0的系统,相比以前的版本对文件的管理更加复杂。下面直接进入正题。

一、准备

创建项目后,还需要做什么准备?不是一顿敲就完事了吗?为了适配7.0的系统,确实需要准备一下:

创建项目,MultiMediaTest,包名com.my.example.multimediatest,其它默认,打开主配置文件,在Application标签下,修改并加入以下代码:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.my.example.multimediatest">

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

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="${applicationId}.provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/provider_paths"/>
        </provider>

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

需要读写权限,另外的就是这段代码:


适配7.0的关键,这还不够,因为指定了的资源文件还没有创建,直接对着provider_paths按Alt+Enter,在res下创建资源文件,对创建好的文件作以下修改:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <!--"."表示所有路径-->
    <external-path name="external_files" path="."/>
</paths>

Ok,准备完成。

二、开始编写

首先理清一下思路:

1、启动系统相机并保存文件涉及到读写权限,Android 6.0以上的系统都需要动态申请。启动相机前,检查权限,没有则申请,有则继续,拒绝权限则程序不能执行。

2、将文件存储到指定位置,需要在SD根目录下创建文件夹,把图片统一存进去。

3、因为我们不能在系统相机使用中的时候去读写文件,所以需要相机在拍摄后,返回一个结果,然后根据拍摄结果去执行读写操作。

4、即使存储了图片,系统图片库也是不会及时更新,所以还需要通知系统更新图库,让我们拍摄的照片及时的显示出来。

理清思路后开始写代码:

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="拍照并保存"/>

</RelativeLayout> 

MainActivity代码:

package com.my.example.multimediatest;

import android.Manifest;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.StrictMode;
import android.provider.MediaStore;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Calendar;

public class MainActivity extends AppCompatActivity {



    private static final String TAG = "MainActivity";

    private String mImagePath;                   //用于存储最终目录,即根目录 / 要操作(存储文件)的文件夹

    public static final String SD_APP_DIR_NAME = "TestDir"; //存储程序在外部SD卡上的根目录的名字
    public static final String PHOTO_DIR_NAME = "photo";    //存储照片在根目录下的文件夹名字
    public static final String VOICE_DIR_NAME = "voice";    //存储音频在根目录下的文件夹名字
    public static final String VIDEO_DIR_NAME = "video";    //存储视频在根目录下的文件夹名字

    public static final int PHOTO_RESULT_CODE = 100;        //标志符,图片的结果码,判断是哪一个Intent
    public static final int VOICE_RESULT_CODE = 101;        //标志符,音频的结果码,判断是哪一个Intent
    public static final int VIDEO_RESULT_CODE = 102;        //标志符,视频的结果码,判断是哪一个Intent

    private Uri mImageUri;                                  //指定的uri

    private String mImageName;                              //保存的图片的名字

    private File mImageFile;                                //图片文件

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Log.d(TAG, "开始...");

        // android 7.0系统解决拍照的问题
        StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
        StrictMode.setVmPolicy(builder.build());
        builder.detectFileUriExposure();

        //按钮的点击事件
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //检查是否获得写入权限,未获得则向用户请求
                if (ActivityCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                        != PackageManager.PERMISSION_GRANTED) {
                    //未获得,向用户请求
                    Log.d(TAG, "无读写权限,开始请求权限。");
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]
                            {Manifest.permission.WRITE_EXTERNAL_STORAGE}, 200);
                } else {
                    Log.d(TAG, "有读写权限,准备启动相机。");
                    //启动照相机
                    startCamera();
                }
            }
        });
    }

    /**
     * 返回用户是否允许权限的结果,并处理
     */
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResult) {
        if (requestCode == 200) {
            //用户允许权限
            if (grantResult[0] == PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG, "用户已允许权限,准备启动相机。");
                //启动照相机
                startCamera();
            } else {  //用户拒绝
                Log.d(TAG, "用户已拒绝权限,程序终止。");
                Toast.makeText(this, "程序需要写入权限才能运行", Toast.LENGTH_SHORT).show();
            }
        }
    }

    /**
     * 启动相机,创建文件,并要求返回uri
     */
    @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void startCamera() {
        Intent intent = new Intent();
        //指定动作,启动相机
        intent.setAction(MediaStore.ACTION_IMAGE_CAPTURE);
        intent.addCategory(Intent.CATEGORY_DEFAULT);
        Log.d(TAG, "指定启动相机动作,完成。");
        //创建文件
        createImageFile();
        Log.d(TAG, "创建图片文件结束。");
        //添加权限
        intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
        Log.d(TAG, "添加权限。");
        //获取uri
        mImageUri = FileProvider.getUriForFile(this, "com.my.example.multimediatest.provider", mImageFile);
        Log.d(TAG, "根据图片文件路径获取uri。");
        //将uri加入到额外数据
        intent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
        Log.d(TAG, "将uri加入启动相机的额外数据。");
        Log.d(TAG, "启动相机...");
        //启动相机并要求返回结果
        startActivityForResult(intent, PHOTO_RESULT_CODE);
        Log.d(TAG, "拍摄中...");
    }

    /**
     * 创建图片文件
     */
    private void createImageFile(){
        Log.d(TAG, "开始创建图片文件...");
        //设置图片文件名(含后缀),以当前时间的毫秒值为名称
        mImageName = Calendar.getInstance().getTimeInMillis() + ".jpg";
        Log.d(TAG, "设置图片文件的名称为:"+mImageName);
        //创建图片文件
        mImageFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath()
                + "/" + SD_APP_DIR_NAME + "/" + PHOTO_DIR_NAME + "/", mImageName);
        //将图片的绝对路径设置给mImagePath,后面会用到
        mImagePath = mImageFile.getAbsolutePath();
        //按设置好的目录层级创建
        mImageFile.getParentFile().mkdirs();
        Log.d(TAG, "按设置的目录层级创建图片文件,路径:"+mImagePath);
        //不加这句会报Read-only警告。且无法写入SD
        mImageFile.setWritable(true);
        Log.d(TAG, "将图片文件设置可写。");
    }

    /**
     * 处理返回结果。
     * 1、图片
     * 2、音频
     * 3、视频
     *
     * @param requestCode 请求码
     * @param resultCode  结果码 成功 -1 失败 0
     * @param data        返回的数据
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {

        super.onActivityResult(requestCode, resultCode, data);

        Log.d(TAG, "拍摄结束。");
        if (resultCode == Activity.RESULT_OK) {
            Log.d(TAG, "返回成功。");
            Log.d(TAG, "请求码:" + requestCode + "  结果码:" + resultCode + "  data:" + data);
            switch (requestCode) {
                case PHOTO_RESULT_CODE: {
                    Bitmap bitmap = null;
                    try {
                        //根据uri设置bitmap
                        bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), mImageUri);
                        Log.d(TAG, "根据uri设置bitmap。");
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                    //将图片保存到SD的指定位置
                    savePhotoToSD(bitmap);
                    //更新系统图库
                    updateSystemGallery();
                    Log.d(TAG, "结束。");
                    break;
                }
                case VOICE_RESULT_CODE: {
//                    saveVoiceToSD();
                    break;
                }
                case VIDEO_RESULT_CODE: {
//                    saveVideoTOSD();
                    break;
                }
            }
        }
    }

    /**
     * 保存照片到SD卡的指定位置
     */
    private void savePhotoToSD(Bitmap bitmap) {
        Log.d(TAG, "将图片保存到指定位置。");
        //创建输出流缓冲区
        BufferedOutputStream os = null;
        try {
            //设置输出流
            os = new BufferedOutputStream(new FileOutputStream(mImageFile));
            Log.d(TAG, "设置输出流。");
            //压缩图片,100表示不压缩
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os);
            Log.d(TAG, "保存照片完成。");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (os != null) {
                try {
                    //不管是否出现异常,都要关闭流
                    os.flush();
                    os.close();
                    Log.d(TAG, "刷新、关闭流");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    /**
     * 更新系统图库
     */
    private void updateSystemGallery() {
        //把文件插入到系统图库
        try {
            MediaStore.Images.Media.insertImage(this.getContentResolver(),
                    mImageFile.getAbsolutePath(), mImageName, null);
            Log.d(TAG, "将图片文件插入系统图库。");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        // 最后通知图库更新
        this.sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.parse("file://" + mImagePath)));
        Log.d(TAG, "通知系统图库更新。");
    }

    /**
     * 保存音频到SD卡的指定位置
     */
    private void saveVoiceToSD() {

    }

    /**
     * 保存视频到SD卡的指定位置
     */
    private void saveVideoTOSD() {

    }

}

按照思路,很容易理解上面的代码:程序启动,点击拍照并保存按钮,开始检查权限,获得权限后,使用隐式启动的方式启动相机,在启动相机前,就把图片文件创建好,并获取到该图片的uri,方便返回结果时的处理,拍照完成时,根据是否拍照成功进行处理,若成功,则将uri转为bitmap,用于存储。存储完毕再通知系统图库更新。

三、运行结果

1、先看log打印


完整的执行流程。

2、查看刚刚拍摄的图片在手机中的位置


3、查看系统图库是否加入了刚刚拍摄的照片


4、点进去查看图片


完美!~图片质量还是不错的。

四、注意事项

需要注意的问题有四点:

1、MainActivity的onCreate中,为适配7.0的系统还需要写一下三行代码:


不然会报错。

2、provider的路径,一定要使用项目最开始的包名,如果包名修改过,会直接中断程序,因为它找不到provider,开始在这个位置弄了很久,仔细看log提示才恍然大悟,包名不对。


3、以上代码为了适配7.0的系统,我也只有一个7.0系统的测试真机,可能在4.几的机器上报错,这时候只要针对获取uri的代码,加上版本判断就好了。

4、拍照并存储只是一个方面,还有一个重要的点是怎么去图库读取拍摄的图片,由于拍摄的图片质量很高,直接读取并设置到ImageView上是行不通的,极有可能OOM,所以读取图片时需要对图片进行压缩、旋转等操作,才能保证读取成功并显示正确。

5、补充:上面的代码中,没有申请照相机权限,开始测试的时候同意了,代码中忘记加了,使用的时候还需要检查相机权限,不然会崩溃。代码中要进行检查,主配置文件也要添加权限。

    <!-- 读写权限 -->
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <!-- 录音权限 -->
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <!-- 相机权限 -->
    <uses-permission android:name="android.permission.CAMERA"/>
    <!-- 录制视频权限 -->
    <uses-permission android:name="android.permission.RECORD_VIDEO"/>
直接都加上了,后续的多媒体都要用到。



原创文章,转载请注明出处:https://blog.csdn.net/Lone1yCode/article/details/79833842


©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页