先上结果:

图1

Android 实现相机功能【完整源码】_android
图2:
Android 实现相机功能【完整源码】_相机_02
图3:
Android 实现相机功能【完整源码】_相机_03

activity_main 布局代码:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_photo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="25dp"
        android:text="拍照"/>

    <ImageView
        android:id="@+id/img_icon"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"/>


</LinearLayout>

MainActivity 代码

package ccv.turbosnail.photo_demo;

import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.support.v4.content.FileProvider;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class MainActivity extends AppCompatActivity {

    public static final int TAKE_PHOTO =1;  //  TAKE_PHOTO来作为case处理图片的标识
    private ImageView imgIcon;  //  显示拍照后的图片
    private Button btnPhoto;    //  拍照
    private Uri imageUri;   //  通用资源标志符

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView(); //初始化控件
        btnPhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                /**
                 *      问:   首先为什么这里要创建时间对象?
                 *      答:   因为,如果我们不用当前本地的时间作为图片的名字,当然这里图片的名字可以设为一个死值
                 *      例如:Monday.jpg    —  那么对于这个死值作为图片的名字,当我们需要拍多张照片时,就会一直覆盖
                 *      这张照片,也就是说,我们就无法实现多张照片的保存,之前的照片就会被新的照片覆盖掉。
                 *      所以说,这里用本地的当前时间作为照片的名字,就解决了这个问题
                 *      简单的来说就是,利用本地时间 — 解决图片名冲突问题
                 */


                SimpleDateFormat format = new SimpleDateFormat("yyyy年MM月dd日HH:mm:ss");
                Date curDate = new Date(System.currentTimeMillis());
                String str = format.format(curDate);

                /**
                 *          创建File对象,用于存储拍照后的照片
                 *          第一个参数:  是这张照片存放在手机SD卡的对应关联缓存应用
                 *          第二个参数:  这张图片的命名
                 */
                File outputImage = new File(getExternalCacheDir(),str+".jpg");
                try {
                    if (outputImage.exists()){          //  检查与File对象相连接的文件和目录是否存在于磁盘中
                        outputImage.delete();           //  删除与File对象相连接的文件和目录
                    }
                    outputImage.createNewFile();        //  如果与File对象相连接的文件不存在,则创建一个空文件
                } catch (IOException e) {
                    e.printStackTrace();
                }
                if (Build.VERSION.SDK_INT >= 24){       //  如果运行设备的系统版本高于 Android7.0
                    /**
                     *          将File对象转换成一个封装过的Uri对象
                     *          第一个参数:  要求传入Context参数
                     *          第二个参数:  可以是任意唯一的字符串
                     *          第三个参数:  我们刚刚创建的File对象
                     */
                    imageUri = FileProvider.getUriForFile(MainActivity.this,"ccv.turbosnail.photo_demo.fileprovider",outputImage);
                }else{                  //  如果运行设备的系统版本低于 Android7.0
                    //  将File对象转换成Uri对象,这个Uri对象表示着 str + ".jpg" 这张图片的本地真实路径
                    imageUri = Uri.fromFile(outputImage);
                }
                /**
                 *      启动相机程序
                 */
                //  将Intent的action指定为 拍照到指定目录 —— android.media.action.IMAGE_CAPTURE
                Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
                //  指定图片的输出地址
                intent.putExtra(MediaStore.EXTRA_OUTPUT,imageUri);
                /**
                 *      在通过startActivityForResult(),来启动活动,因此拍完照后会有结果返回到 onActivityResult()方法中
                 */
                startActivityForResult(intent,TAKE_PHOTO);
            }
        });

    }

    /**
     *
     * @param requestCode       第一个是请求码,可以进行传递数据前的一些操作,比如根据不同的请求码,设置不同的传递内容。
     * @param resultCode        第二个是返回码,也就是在B中设置的int的数值,这个是得到返回的内容的标识。
     * @param data              第三个是Intent的数据,比如在B中的setResult方法中传递了一些数据,在A中就可以通过解析Intent的内容来获得传递过来的数据。
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
        switch (requestCode){
            case TAKE_PHOTO:
                if (resultCode == RESULT_OK){       //  当拍照成功后,会返回一个返回码,这个值为 -1 — RESULT_OK
                    try{
                        //  根据Uri找到这张照片的资源位置,将它解析成Bitmap对象,然后将把它设置到imageView中显示出来
                        Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
                        imgIcon.setImageBitmap(bitmap);
                    } catch (FileNotFoundException e) {
                        e.printStackTrace();
                    }
                }
                break;
        }
    }

    private void initView() {
        btnPhoto = findViewById(R.id.btn_photo);
        imgIcon = findViewById(R.id.img_icon);
    }
}

AndroidManifest 代码:

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

    <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">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

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

        <provider
            android:name="android.support.v4.content.FileProvider"
            android:authorities="ccv.turbosnail.photo_demo.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths" />
        </provider>

    </application>

</manifest>

接着:

Android 实现相机功能【完整源码】_android_04

最后,仔细看下注释,代码在上面,但是我还是希望大家能认真理解一下它们的意思

Android 实现相机功能【完整源码】_拍照_05

点击运行即可!

总结:
相机的逻辑还是比较简单的,我想仔细阅读过注释的你一定不会觉得有什么困难,但我还是要总结一下我认为比较重要的三点:

  1. 应用缓存目录:
    自从 Android6.0 系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应用关联缓存目录可以跳过这一步 ,那么我们直接在创建File对象时,第一个参数填入 getExternalCacheDir()方法就可以得到这个目录

  2. 内容提供器的注册:
    自从 Android7.0系统开始,直接使用本地真实路径被认为是不安全的,会抛出一个FileUriExPosedException 异常,而FileProvider 是一种特殊的内容提供其,它使用了内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的Uri 共享给外部,从而提高了应用的安全性。

  3. 版本问题:
    如果运行设备的系统版本低于 Android7.0,就调用Uri的 fromFile()方法将File 对象转换成Uri对象,这个对象标识着拍照的图片的本地真实路径。如果低于7.0版本,就调用FileProvider的getUriForFile() 方法将File对象转换成一个封装过的Uri 对象。