Android实践:《简易照相机》的详细实现步骤及知识梳理

本文实现的简易的照相机,包括拍照、选图、分享三个功能,主要涉及到Intent隐式启动、Intent动作、Uri、危险权限读取、图像处理、手机旋转等知识。

首先,设计程序布局,仅有一个活动界面,如下所示:


设计程序的详细步骤、所涉知识整理如下:

/**
 * 项目目标:实现简易照相机
 * 实现方式:启动系统照相机
 * 学习目标:Intent隐式启动、简单模仿相机的图片处理
 *
 * 实现步骤:
 * 1、设计程序布局
 * 2、使用Intent启动系统照相机
 * 3、图片处理:
 *   (1)、权限获取
 *   (2)、保存图片
 *   (3)、显示图片
 *   (4)、手机旋转,图像的处理
 * 4、使用Intent浏览并选取照片
 * 5、分享照片
 *
 * 详细实现过程及知识整理:
 * 一、使用Intent启动系统照相机
 * Intent it = new Intent("android.media.action.IMAGE_CAPTURE");
 * startActivityForResult(it,100);
 * 涉及动作:android.media.action.IMAGE_CAPTURE,意为拍照。
 * 启动方式:要求动作完成后返回数据。
 * 100作为自定义标志符,用于识别。即处理多个返回的数据时,可以确定是哪一个Intent。
 *
 * 二、图片处理
 * 1、权限获取
 *  若要保存图片,必然涉及写操作,需要写权限才可以,这在android系统中是一个危险权限,
 *  实现读写操作,需要在AndroidManifest.xml中加入以下权限:
 *  <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
 *  <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 *  同时,还需要向用户请求获取这些权限,因为上述两个权限属于STORAGE权限组,所以只要其中一个
 *  权限被允许,其他同类权限也会被自动允许,故程序中可以只添加其中一个权限。
 *
 * 以写权限为例:
 * if (ActivityCompat.checkSelfPermission(this,
 * Manifest.permission.WRITE_EXTERNAL_STORAGE) !=
 * PackageManager.PERMISSION_GRANTED) {         //检查是否已获得写入权限
 *     ActivityCompat.requestPermissions(this,  //向用户要求允许写入权限
 *     new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},200);
 * }
 * 在向用户请求权限的代码中,方法的第二个参数必须为数组形式,可以一次请求多个权限,第三个参数200
 * 代表自定义的识别码,用户无论是否同意权限,程序都可以通过onRequestPermissionsResult方法接收
 * 结果,此时可以作为识别之用。如下代码所示:
 * public void onRequestPermissionsResult(int requestCode, String[] permissions,
 * int[] grantResults){
 *     if (requestCode == 200){
 *         if (grantResults[0] == PackageManager.PERMISSION_GRANTED){ //用户允许权限
 *             savePhoto();  //获取权限后才可以进行保存图片操作
 *         }else{       //用户拒绝权限
 *             Toast.makeText(this,"程序需要写入权限才能运行",Toast.LENGTH_SHORT).show();
 *         }
 *     }
 * }
 * 其中grantResults数组中存放的就是用户允许还是拒绝权限的结果,可以同时有多个,与要求权限时的
 * 权限排列顺序、数量相同。
 *
 * 2、保存图片
 * 因为资源的存取只能通过内容提供者,利用URI的传递,以便同意管控权限而避免不当的存取。
 * 首先使用getContentResolver()获得系统的内容提供者,然后通过它在手机共享图像文件路径里新增
 * 一个文件,并传回该文件的Uri对象,然后将该对象保存。
 * 综上所述,获取权限后,启动系统照相机,启动时将Uri对象加入额外数据中,如下代码所示:
 * private void savePhoto(){
 *     imgUri = getContentResolver().insert(
 *              MediaStore.Images.Media.EXTERNAL_CONTENT_URI,new ContentValues());
 *     Intent it = new Intent("android.media.action.IMAGE_CAPTURE");
 *     it.putExtra(MediaStore.EXTRA_OUTPUT, imgUri);
 *     startActivityForResult(it, 100);
 * }
 * 其中putExtra方法中,通过内容提供者获取的对象就是imgUri,名字为MediaStore.EXTRA_OUTPUT,
 * 相机程序会用这个名字读取未来拍照存档的URI。
 *
 * 3、显示图片
 * (1)、用BitmapFactory来读取图像。
 * Bitmap bmp = BitmapFactory.decodeStream(getContentResolver().
 *              openInputStream(imgUri), null, null);//读取图像内容并存储为Bitmap对象
 * imv.setImageBitmap(bmp);         //将Bitmap对象显示在ImageView中
 * 如此一来就会把图片显示出来了,但是这样做有一个弊端,当图片过大的时候,很有可能会因为内存不
 * 足而导致程序崩溃,所以需要对显示的图片进行约束,合理显示,避免过大。
 * (2)、用BitmapFactory.Options设置加载图像文件的选项
 * BitmapFactory.Options option = new BitmapFactory.Options(); //新建选项对象
 * option.inJustDecodeBounds = true;     //设置选项:只读取图片文件信息而不加载图片文件
 * BitmapFactory.decodeStream(imgUri.getPath(),option);//读取图像文件信息存入option中
 * iw = option.outWidth();      //从option中读取图像文件的宽
 * ih = option.outHeight();     //从option中读取图像文件的高
 * 按比例缩小图像:
 * vw = imv.getWidth();    //获取 ImageView 的宽度
 * vh = imv.getHeight();   //获取 ImageView 的高度
 * int scaleFactor = Math.min(iw/vw, ih/vh);   // 计算缩小比例
 * option.inJustDecodeBounds = false;  //关闭只加载图片文件信息的选项
 * option.inSampleSize = scaleFactor;  //设置缩小比例,例如2则长宽都将缩小为原来的1/2
 * 这时图像的处理之后的信息都在Options对象中,载入图片:
 * Bitmap bmp = BitmapFactory.decodeStream(
 *              getContentResolver().openInputStream(imgUri), null, option);
 * 显示图片:imv.setImageBitmap(bmp); //显示图片
 *
 * 4、手机旋转,图像的处理。
 * 如果手机开启自动旋转屏幕,则刚才拍摄的照片在显示中,会因为屏幕的旋转而消失。
 * 解决方式有两种:
 * 方式一:在发生旋转时,立即将所显示照片的Uri存储起来,等旋转完并重新启动Activity后,在按
 * 照存储的Uri将照片显示出来,另外,还可以按照旋转的方向载入不用的界面Layout文件来显示。
 * 方式二:关闭手机界面的自动旋转功能。
 * 用户在拍摄时会有横拍或竖拍,所以最好由程序决定要不要旋转屏幕,而不是随手机的旋转而旋转。
 * (1)、关闭自动旋转功能并设置屏幕为直向显示
 * //设置屏幕不随手机旋转
 * setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
 * /设置屏幕直向显示/
 * setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
 * (2)、处理横拍和竖拍时,图像宽高的缩小比例
 * Boolean needRotate;                         //用来存储是否需要旋转
 * int scaleFactor;
 * if(iw < ih) {    //如果图片的宽度小于高度
 *     needRotate = false;                     //不需要旋转
 *     scaleFactor = Math.min(iw/vw, ih/vh);   // 计算缩小比例
 * }
 * else {
 *     needRotate = true;                      //需要旋转
 *     scaleFactor = Math.min(iw/vh, ih/vw);// 将ImageView的宽高互换来计算缩小比例
 * }
 * (3)、用Matrix(android.graphics.Matrix)对象旋转图片
 * Matrix是一个旋转矩阵,用Bitmap.createBitmap()创建新的Bitmap时,可以用它来旋转图片。
 * createBitmap(Bitmap src,int x,int y,int width,int height,Matrix m,boolean filter)
 * src为要复制的来源Bitmap对象。
 * x、y指定要由来源Bitmap的那个位置开始复制(从左上角算起)。
 * width、height为新的Bitmap的宽高。
 * m是旋转矩阵,而最后一个参数filter设置了旋转时需要传入true。
 * 注:生成新的Bitmap后,原来的Bitmap对象就会被系统回收。
 * 具体代码如下:
 * if(needRotate) { //如果需要旋转
 *     Matrix matrix = new Matrix();   //新建 Matrix 对象
 *     matrix.postRotate(90);          //设置旋转角度
 *     bmp = Bitmap.createBitmap(bmp , //用原来的 Bitmap 创建一个新的 Bitmap
 *           0, 0, bmp.getWidth(), bmp.getHeight(), matrix, true);
 * }
 *
 * 做了这么多的图像处理后,为了更直观的了解图片从拍照到显示的变化,可以加入以下代码,用一个
 * 弹窗显示出图片的详细信息。
 * new AlertDialog.Builder(this)
 *     .setTitle("图像文件信息")
 *     .setMessage("图像文件URI:" + imgUri.toString() +
 *         "\n原始尺寸:" + iw + "x" + ih +
 *         "\n载入尺寸:" + bmp.getWidth() + "x" + bmp.getHeight() +
 *         "\n显示尺寸:" + vw + "x" + vh + (needRotate?"(旋转)":""))
 *     .setNeutralButton("关闭",null)
 *     .show();
 * 窗口中详细的展示了图像的URI路径、拍照尺寸、Bitmap载入的尺寸、ImageView显示的尺寸,还
 * 包括是否做了旋转操作。
 *
 *三、使用Intent浏览选择图片
 * Android系统中内建了一个内容提供者,存储着各种数据和多媒体文件等共享数据的相关信息。
 * Android内建的图库程序就是从这个数据库中读取可共享的图片文件信息,然后列出来供用户选取。
 * 所以将拍摄的照片设为系统共享文件,并用广播Intent的方式通知系统。
 * //将imgUri所指的文件设为系统共享媒体文件
 * Intent it = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, imgUri);
 * sendBroadcast(it);   //通知系统
 * 选取图片:
 * Intent it = new Intent(Intent.ACTION_GET_CONTENT);   //动作设为 "选取内容"
 * it.setType("image/*");            //设置要选取的媒体类型为:所有类型的图片
 * startActivityForResult(it, 101);  //启动意图, 并要求返回选取的图片
 * 101是自定义的识别码。上面的代码中要求返回选取的图片,所以在onActivityResult方法中可以
 * 根据识别码判断是哪一个Intent的动作,并获取这个图片的Uri信息,然后将其显示。
 *
 * 四、分享图片
 * 只要图片不为空,就可以使用动作ACTION_SEND分享图片,代码如下:
 * if(imgUri != null){
 *     Intent it = new Intent(Intent.ACTION_SEND);
 *     it.setType("image/*");
 *     it.putExtra(Intent.EXTRA_STREAM,imgUri);
 *     startActivity(it);
 * }
 *
 * */

程序具体代码如下所示:

package com.my.camera;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.net.Uri;
import android.os.Bundle;
import android.provider.MediaStore;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.ImageView;
import android.widget.Toast;

import java.io.IOException;

public class MainActivity extends AppCompatActivity {

    Uri imgUri;         //记录图片信息
    ImageView imv;      //视图组件

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

        //设置屏幕不随手机旋转
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_NOSENSOR);
        //设置屏幕直向显示
        setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);

        //关联视图组件
        imv = (ImageView)findViewById(R.id.imageView);
    }

    /**
     * 按钮“拍照”的点击事件
     * */
    public void onGet(View v){

        //检查是否获得写入权限,未获得则向用户请求
        if(ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
                != PackageManager.PERMISSION_GRANTED){
            //未获得,向用户请求
            ActivityCompat.requestPermissions(this,new String[]
                    {Manifest.permission.WRITE_EXTERNAL_STORAGE},200);
        }else{
            //已经获得权限
            savePhoto();
        }

    }

    /**
     * 按钮“图库”的点击事件
     * */
    public void onPick(View v){
        Intent it = new Intent(Intent.ACTION_GET_CONTENT);  //设置动作为选取内容
        it.setType("image/*");                              //设置要选取的媒体类型:所有类型图片
        startActivityForResult(it,101);                     //启动Intent,并要求返回选取的图像文件
    }

    /**
     * 按钮“分享”的点击事件
     * */
    public void onShare(View v){
        if(imgUri != null){
            Intent it = new Intent(Intent.ACTION_SEND);
            it.setType("image/*");
            it.putExtra(Intent.EXTRA_STREAM,imgUri);
            startActivity(it);
        }
    }

    /**
     * 返回用户是否允许权限的结果,并处理
     * */
    public void onRequestPermissionsResult(int requestCode,String[] permissions, int[] grantResult){
        if(requestCode == 200){
            //用户允许权限
            if(grantResult[0] == PackageManager.PERMISSION_GRANTED){
                savePhoto();    //允许写权限才可以保存图片
            }else{              //用户拒绝
                Toast.makeText(this,"程序需要写入权限才能运行",Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void savePhoto(){
        //通过内容提供者新增一个图像文件
        imgUri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                new ContentValues());
        Intent it = new Intent("android.media.action.IMAGE_CAPTURE");   //指定动作,拍照
        it.putExtra(MediaStore.EXTRA_OUTPUT,imgUri);        //将uri加到拍照Intent的额外数据

        startActivityForResult(it,100);             //设置100为识别码,启动活动
    }

    /**
     * 处理通过Intent启动的活动的返回结果
     * 识别码100,拍照活动返回的数据,将拍摄的图片设为共享,并通知系统
     * 识别码101,选取图片活动返回的数据,直接读取数据并存储到imgUri中
     * 调用showImg方法显示图片
     * 根据识别码来显示不同的出错Toast信息
     * */
    protected void onActivityResult(int requestCode,int resultCode,Intent data){

        super.onActivityResult(requestCode,resultCode,data);

        if(resultCode == Activity.RESULT_OK){

            switch(requestCode){
                case 100:
                    //设为系统共享媒体文件
                    Intent it = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,imgUri);
                    sendBroadcast(it);
                    break;
                case 101:
                    //选取图片,直接获取该图片的数据
                    imgUri = data.getData();
                    break;
            }
            showImg();     //显示图片

        }else{  //根据识别码的不同,显示不同的没有获取到图片的信息
            Toast.makeText(this,requestCode==100?"没有拍到照片":"没有选取照片",
                    Toast.LENGTH_SHORT).show();
        }

    }

    /**
     * 显示图片
     * 避免图像过大,进行处理后显示
     * 判断图片是横拍还是直拍决定是否旋转
     * 显示图片处理过程的详细信息
     * */
    public void showImg(){

        int iw,ih,vw,vh;    //iw,ih为图片宽高,vw,vh为ImageView组件的宽高
        boolean needRotate; //用来存储是否需要旋转

        Bitmap bmp = null;  //创建Bitmap对象

        BitmapFactory.Options option = new BitmapFactory.Options();  //创建选项对象

        option.inJustDecodeBounds = true;   //设置选项:只读取图像文件信息而不载入图像文件

        try{
            //读取图像文件信息存入option中
            BitmapFactory.decodeStream(getContentResolver().openInputStream(imgUri),null,option);
        }catch(IOException e){
            Toast.makeText(this,"读取照片信息时发生错误",Toast.LENGTH_SHORT).show();
            return;
        }

        iw = option.outWidth;   //从option中读取图像文件的宽度
        ih = option.outHeight;  //从option中读取图像文件的高度
        vw = imv.getWidth();    //获取ImageView的宽度
        vh = imv.getHeight();   //获取ImageView的高度

        int scaleFactor;
        if(iw < ih){
            needRotate = false; //不需要旋转
            scaleFactor = Math.min(iw/vw,ih/vh);    //计算缩小比例
        }else{
            needRotate = true;
            scaleFactor = Math.min(ih/vw,iw/vh);    //改用旋转后的图像宽、高计算缩小比例
        }

        option.inJustDecodeBounds = false;  //关闭之加载图像文件信息的选项
        option.inSampleSize = scaleFactor;  //设置缩小比例,若为3,则长宽将缩小为原来的1/3

        try{
            //加载图片
            bmp = BitmapFactory.decodeStream(getContentResolver().openInputStream(imgUri),
                    null,option);
        }catch(IOException e){
            Toast.makeText(this,"无法取得照片",Toast.LENGTH_SHORT).show();
        }

        if(needRotate){
            Matrix matrix = new Matrix();   //创建Matrix对象
            matrix.postRotate(90);          //旋转90°,顺时针
            //用原来的图像产生一个新的图片
            bmp = Bitmap.createBitmap(bmp,0,0,bmp.getWidth(),bmp.getHeight(),matrix,true);
        }

        imv.setImageBitmap(bmp);            //显示图片

        //弹窗显示图片的信息
        new AlertDialog.Builder(this)
                .setTitle("图像文件信息")
                .setMessage("图像文件URI:" + imgUri.toString() +
                        "\n原始尺寸:" + iw + "x" + ih +
                        "\n载入尺寸:" + bmp.getWidth() + "x" + bmp.getHeight() +
                        "\n显示尺寸:" + vw + "x" + vh + (needRotate?"(旋转)":""))
                .setNeutralButton("关闭",null)
                .show();

    }

}
//                     总结参考:《Android App开发入门 第2版》 机械工业出版社 施威铭 著 2017.8 

程序的部分运行结果如下所示:


(哇,我在编辑的时候发现这个图好大好大啊,不知道发布出去之后会不会还这么大。)


这个简易照相机非常的简单,使用起来非常的呆板,还需要不断的完善。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值