概述

在安卓开发过程中,使用手机拍照功能的核心代码其实非常简单,仅仅只是一句呼唤系统Intent而已:

Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");



要是软件开发真的这么简单就好了,而且核心代码也仅仅只是核心代码而已。为了完成整个过程:调用手机摄像头、拍照、剪切照片、获取照片、处理照片,还是需要围绕核心代码做很多事情的。

第一步:获取系统权限

如果要调用手机的摄像头进行拍照,我们需要以下权限:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera"/>
<uses-feature android:name="android.hardware.camera.front"/>
<uses-feature android:name="android.hardware.camera.autofocus"/>


注意:

需要注意的是,从Android6.x开始,摄像头权限被认为是危险权限,仅仅只是在AndroidManifest.xml文件中做声明可能并不能使摄像头正常打开。而危险权限在Android6.x版本之后是需要额外编写代码,在程序运行时进行动态申请的。

关于运行时动态申请系统权限,可以参考我的这篇文章:


如果你在完成拍照之后,需要把照片保存在手机的扩展存储卡中,然后可能在后续的程序处理中将图片再从存储卡读取出来进行处理,那么就需要对扩展存储的读写权限:

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



第二步:绘制或编写Activity界面


在我的应用场景中,我的界面是这个样子的:




android自动拍照 自动拍照手机_手机



我在界面上用TableLayout做了一个容器,每当拍摄一张照片后,这张照片就会被放置在这个TableLayout中。如果拍摄了多张照片,那么就会像上图那样形成一个列表。


这样的界面结构其实非常简单,不过我还是把代码粘帖在这里:


<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    tools:context="com.yumi.mibao.easyasset.activities.TakePhotoActivity"
    android:background="@color/metro_white">



    <Button
        android:id="@+id/BUTTON_COMPLETE"
        android:layout_width="0dp"
        android:layout_height="48dp"
        android:background="@color/yumi_red"
        android:textColor="@color/metro_white"
        android:text="@string/str_complete"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginBottom="8dp"
        app:layout_constraintLeft_toRightOf="@+id/BUTTON_TAKE_PHOTO"
        android:layout_marginLeft="8dp"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp" />

    <Button
        android:id="@+id/BUTTON_TAKE_PHOTO"
        android:layout_width="68dp"
        android:layout_height="48dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:background="@color/metro_grass"
        android:text="@string/str_take_photo"
        android:textColor="@color/metro_white"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="@+id/BUTTON_ALBUM"
        android:layout_marginStart="8dp"
        android:onClick="takePohto"/>

    <Button
        android:id="@+id/BUTTON_ALBUM"
        android:layout_width="68dp"
        android:layout_height="48dp"
        android:layout_marginBottom="8dp"
        android:layout_marginLeft="8dp"
        android:background="@color/metro_blue"
        android:text="@string/str_album"
        android:textColor="@color/metro_white"
        app:layout_constraintBottom_toBottomOf="parent"
        android:layout_marginStart="8dp"
        android:onClick="pickPhotoFromAlbum"
        app:layout_constraintLeft_toLeftOf="parent" />



    <ScrollView
        android:layout_width="0dp"
        android:layout_height="495dp"
        android:layout_marginRight="8dp"
        app:layout_constraintRight_toRightOf="parent"
        android:layout_marginLeft="8dp"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:layout_marginTop="8dp"
        app:layout_constraintHorizontal_bias="0.0"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toTopOf="@+id/BUTTON_COMPLETE">

        <TableLayout
            android:id="@+id/TABLE_LAYOUT_TAKE_PHOTO_LIST"
            android:layout_width="match_parent"
            android:layout_height="fill_parent"
            android:layout_gravity="top"
            android:layout_marginTop="8dp"
            android:gravity="top" />
    </ScrollView>
</android.support.constraint.ConstraintLayout>


在上面的代码中,可以看到id为“BUTTON_TAKE_PHOTO”的按钮就对应了图片中的“拍照”按钮,在这个按钮的onclick事件中,我触发了一个方法“takePhoto”,就是这个方法会调起手机的拍照功能。具体实现往下看。


第三步:编写事件触发方法

3.1定义用于Intent回调的请求码


由于呼出系统拍照功能的Intent是采用startActivityForResult的模式来进行,所以需要明确的请求码来标记操作的类型,这样才能够在Intent的回调方法中进行相应的数据处理:


private final int TAKE_PHOTO = 1;//拍照操作
private final int CROP_PHOTO = 2;//切图操作

可以看到我一共定义了两个请求吗:拍照和切图。因为现在的手机默认拍摄的照片尺寸都比较大,其实不太利于程序处理,比如说如果要上传到服务器的话,可能会花费掉很多流量和上传时间;如果程序处理不当的话,还可能会引起内存溢出问题。


3.2定义照片的保存路径和文件名

/*
 *  拍照所得到的图像的保存路径
 */
private Uri imageUri;

/*
 *  当前用户拍照或者从相册选择的照片的文件名
 */
private String fileName;

可以看到这两个变量仅仅只是定义而已,并没有赋值。对它们的赋值操作会在后续的代码中进行。


3.3编写方法,呼出系统摄像头,实现拍照


/**
 * 当用户点击按钮时,打开摄像头进行拍照
 * @param view
 */
public void takePohto(View view){
    /*
     *  用时间戳的方式来命名图片文件,这样可以避免文件名称重复
     */
    SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss");
    Date date = new Date(System.currentTimeMillis());
    this.fileName = "easyasset"+format.format(date);

    /*
     *  创建一个File对象,用于存放拍照所得到的照片
     */
    File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM);
    File outputImage = new File(path,this.fileName+".jpg");

    /*
     *  以防万一,看一下这个文件是不是存在,如果存在的话,先删除掉
     */
    if(outputImage.exists()){
        outputImage.delete();
    }

    try {
        outputImage.createNewFile();
    } catch (IOException e) {
        e.printStackTrace();
    }

    /*
     *  将File对象转换为Uri对象,以便拍照后保存
     */
    this.imageUri = Uri.fromFile(outputImage);

    /*
     *  启动系统的照相Intent
     */
    Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); //Android系统自带的照相intent
    intent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri); //指定图片输出地址
    startActivityForResult(intent,this.TAKE_PHOTO); //以forResult模式启动照相intent
}

代码中已经分步骤写了注释,应该比较清晰。可以看到,在方法的最后,调用了系统Intent,并且使用startActivityForResult的模式来启动,而在启动Intent的同时,将请求码“TAKE_PHOTO”作为参数传递了过去。



3.4编写Intent回调方法


由于在上一步中,我们是使用startActivityForResult的形式来调用系统Intent进行拍照的,所以我们必须编写Intent回调方法,来处理拍照后得到的照片:


/**
 * 因为启动拍照intent的时候是使用的forResult模式,因此需要onActivityResult方法来接受回调参数
 * @param requestCode
 * @param resultCode
 * @param data
 */
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
    super.onActivityResult(requestCode,resultCode,data);

    if (resultCode != RESULT_OK) {
        Toast.makeText(this, "获取图片出现错误", Toast.LENGTH_SHORT).show();
    }
    else{
        switch(requestCode) {
            /*
             *  case TAKE_PHOTO 代表从拍摄照片的intent返回之后
             *  完成拍摄照片之后,立刻打开系统自带的裁剪图片的intent
             */
            case TAKE_PHOTO:
                this.cropPhoto(this.imageUri);
                break;

            default:
                break;
        }
    }
}

这样以来,当我们完成拍照并关闭摄像头界面后,系统就会自动调用这个回调方法。并且可以看到,在这个方法内部的switch逻辑块中,我们捕获了请求码“TAKE_PHOTO”,然后调用了一个名为“cropPhoto()”的方法。


这个方法就是用来进行图片裁剪的。具体实现往下看。


3.5编写图片裁剪方法


/**
 * 打开裁剪图片的系统界面
 */
private void cropPhoto(Uri imageUri){
    /*
     *  准备打开系统自带的裁剪图片的intent
     */
    Intent intent = new Intent("com.android.camera.action.CROP"); //打开系统自带的裁剪图片的intent
    intent.setDataAndType(imageUri, "image/*");
    intent.putExtra("scale", true);

    /*
     *  设置裁剪区域的宽高比例
     */
    intent.putExtra("aspectX", 1);
    intent.putExtra("aspectY", 1);

    /*
     *  设置裁剪区域的宽度和高度
     */
    intent.putExtra("outputX", 340);
    intent.putExtra("outputY", 340);

    /*
     *  指定裁剪完成以后的图片所保存的位置
     */
    intent.putExtra(MediaStore.EXTRA_OUTPUT, this.imageUri);
    Toast.makeText(this, "剪裁图片", Toast.LENGTH_SHORT).show();

    /*
     *  以广播方式刷新系统相册,以便能够在相册中找到刚刚所拍摄和裁剪的照片
     */
    Intent intentBc = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
    intentBc.setData(this.imageUri);
    this.sendBroadcast(intentBc);

    /*
     *  以forResult模式启动系统自带的裁剪图片的intent
     */
    startActivityForResult(intent, CROP_PHOTO); //设置裁剪参数显示图片至ImageView
}

代码逻辑也比较易懂,而且可以看到大部分的逻辑都于之前的调用摄像头拍照的代码逻辑非常类似。在这个方法的最后,同样是调用了系统Intent,并且使用startActivityForResult的模式来启动,而在启动Intent的同时,将请求码“CROP_PHOTO”作为参数传递了过去。


在我的手机上,切图界面是这个样子的:

android自动拍照 自动拍照手机_android自动拍照_02



3.6在Intent回调方法中,增加对切图回调的处理逻辑


在上面第3.4步的Intent回调方法中,我们需要增加对切图回调的处理:


/*
 *  case CROP_PHOTO 代表从裁剪照片的intent返回之后
 *  完成裁剪照片后,就要将图片生成bitmap对象,然后显示在界面上面了
 */
case CROP_PHOTO:
    try {
        /*
         *  将图片转换成Bitmap对象
         */
        Bitmap bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(this.imageUri));
        Toast.makeText(this, this.imageUri.toString(), Toast.LENGTH_SHORT).show();

        /*
         *  在界面上显示图片
         */
        this.addPhotoToActivity(bitmap);
    } catch(FileNotFoundException e) {
        e.printStackTrace();
    }
    break;



/**
 * 因为启动拍照intent的时候是使用的forResult模式,因此需要onActivityResult方法来接受回调参数
 * @param requestCode
 * @param resultCode
 * @param data
 */
@Override
protected void onActivityResult(int requestCode,int resultCode,Intent data){
    super.onActivityResult(requestCode,resultCode,data);

    if (resultCode != RESULT_OK) {
        Toast.makeText(this, "获取图片出现错误", Toast.LENGTH_SHORT).show();
    }
    else{
        switch(requestCode) {
            /*
             *  case TAKE_PHOTO 代表从拍摄照片的intent返回之后
             *  完成拍摄照片之后,立刻打开系统自带的裁剪图片的intent
             */
            case TAKE_PHOTO:
                this.cropPhoto(this.imageUri);
                break;

            /*
             *  case CROP_PHOTO 代表从裁剪照片的intent返回之后
             *  完成裁剪照片后,就要将图片生成bitmap对象,然后显示在界面上面了
             */
            case CROP_PHOTO:
                try {
                    /*
                     *  将图片转换成Bitmap对象
                     */
                    Bitmap bitmap = BitmapFactory.decodeStream(this.getContentResolver().openInputStream(this.imageUri));
                    Toast.makeText(this, this.imageUri.toString(), Toast.LENGTH_SHORT).show();

                    /*
                     *  在界面上显示图片
                     */
                    this.addPhotoToActivity(bitmap);
                } catch(FileNotFoundException e) {
                    e.printStackTrace();
                }
                break;


            default:
                break;
        }
    }
}

可以看到,在完成了切图之后,调用了一个名为“addPhotoToActivity”的方法,来将经过裁剪的图片显示在界面上。这个方法的实现如下:


3.7显示图片


在显示图片之前,我对界面上的TableLayout容器做了一点小小的布局设计:


/*
 *  一组界面样式,分别是照片在TableRow中所占的宽度比重和照片上传状态的文字信息在TableRow中所占的宽度比重
 */
private TableRow.LayoutParams photoParams;
private TableRow.LayoutParams uploadStateMsgParam;

我专门写了一个简单的方法,来初始化这两个LayoutParams:

/**
 * 初始化一些界面组件的样式
 */
private void initLayoutParams(){
    /*
     *  拍照所得到的图片被放置在界面上时,其在TableRow所占的宽度占比
     */
    this.photoParams = new TableRow.LayoutParams(
        TableRow.LayoutParams.WRAP_CONTENT,
        268,
        0.1f
    );

    /*
     *  照片上传状态的文字信息被放置在界面上时,其在TableRow所占的宽度占比
     */
    this.uploadStateMsgParam = new TableRow.LayoutParams(
        TableRow.LayoutParams.WRAP_CONTENT,
        268,
        0.9f
    );
}

然后我在onCreate中调用了这个方法:

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


    /*
     *  调用方法,初始化界面组件的样式
     */
    this.initLayoutParams();
}

有了界面组件的基本样式之后,就可以将图片显示在界面上了:


/**
 * 将拍照和裁剪后所得到的照片,罗列在界面上
 */
private  void addPhotoToActivity(Bitmap bitMap){
    /*
     *  首先获取到用来显示照片的容器
     *  该容易是一个TableLayout
     */
    TableLayout tableLayout = (TableLayout)this.findViewById(R.id.TABLE_LAYOUT_TAKE_PHOTO_LIST);

    /*
     *  创建一个TableRow对象
     *  每一行TableRow对象都用来存放一张照片,以及该照片的上传情况信息
     *  将这个TableRow放入TableLayout中
     */
    TableRow tableRow = new TableRow(this);
    tableRow.setPadding(0,0,0,8);//设置每一行的下间距
    tableLayout.addView(tableRow);

    /*
     *  创建一个ImageView对象
     *  将这个对象放入TableRow中
     *  并在这个对象上显示刚刚拍照所得到的照片
     */
    ImageView imageView = new ImageView(this);
    imageView.setLayoutParams(this.photoParams);
    imageView.setImageBitmap(bitMap);
    tableRow.addView(imageView);

    /*
     *  创建一个TextView对象
     *  为这个对象设置一段“图片正在上传”的提示文字
     *  并将这个TextView对象放入TableRow中
     */
    TextView textView = new TextView(this);
    textView.setLayoutParams(this.uploadStateMsgParam);
    textView.setGravity(Gravity.CENTER_VERTICAL);
    textView.setText("正在上传照片...");
    textView.setTextColor(ContextCompat.getColor(this,R.color.metro_blue));
    tableRow.addView(textView);

}


整个思路到此为止,看起来一大堆,其实逻辑还是非常清晰和简单的。

附几张深夜工作时的状态:



android自动拍照 自动拍照手机_android_03