我们平时在使用QQ或微信的时候经常要和别人分享图片,这些图片可以是用手机摄像头拍的,也可以是从相册中选取的。类似这样的功能实在是太常见了,几乎在每个应用程序中都会有,那么本节我们就学习一下调用摄像头和相册方面的知识。
8.3.1 调用摄像头拍照
先来看看摄像头方面的知识,现在很多的应用都会要求用户上传一张图片来作为头像,这时摄像头拍张照是最简单快捷的。下面就让我们通过一个例子来学习一下如何才能在应用程序里调用手机的摄像头进行拍照。
新建一个CameraAlbumTest项目,然后修改activity_main.xml中的代码,如下所示:
可以看到,布局文件中只有两个控件一个Button和一个ImageView。BUtton适用于打开摄像头进行拍照的,而ImageView则是用于将拍到的图片显示出来。
然后开始编写调用摄像头的具体逻辑,修改mainActivity中的代码,如下所示:
上述代码稍微有点复杂,我们来仔细分析一下。在MainActivity中要做的第一件事自然是分别获取到Button和ImageView的实例,并给Button注册上点击事件,然后在Button的点击事件里开始处理调用摄像头的逻辑,我们重点看一下这部代码。
首先这里创建了一个File,对象,用于存放摄像头拍下的图片,这里我们把图片命名为output_image.jpg,并将它存放在手机SD卡的应用关联缓存目录下。什么叫做应用关联缓存目录啦?就是指SD卡中专门存放当前应用缓存数据的位置,调用getExternalCacheDir()方法可以得到这个目录,具体的路径是/sdcard/Android/data/<package name>/cache。name为什么要使用应用关联目录存放图片啦?因为从Android6.0系统开始,读写SD卡被列为了危险权限,如果将图片存放在SD卡的任何其他目录,都要进行运行时权限处理才行,而使用应关联目录则可以跳过这一步。
接着会进行一个判断,如果运行设备的系统版本低于Android7.0就调用formFile()方法将File对象转换成Uri对象,这个Uri对象标识这output_image.jpg这张图片的本地真实路径。否则就调用FileProvider的getUriForFile()方法将File对象转换成一个封装过得Uri对象。getUriForFile()方法接收3各参数,第一个参数要求传入Context对象,第二个参数可以是任意唯一的字符串,第三个参数真是我们刚刚创建的File对象。之所以要进行这样一层转换,是因为从Android7.0系统开始,直接使用本地真实路劲Uri被认为是不安全的,会抛出一个FileUriExposedException异常。而FileProvider则是一种特殊的内容提供者,他是用了和内存提供器类似的机制来对数据进行保护,可以选择性地保护,可以选择性地将封装过得Uri共享给外部,从而提高了应用的安全性。
接下来构建出一个Intent对象,并将这个Intent的action指定为android.media.action.IMAGE_CAPTURE,再调用Inten的putExtra()方法来指定图片的输出地址,这里填入刚刚得到的Uri对象,最后调用startActivityForResult()来启动活动。由于我们使用的是一个隐士Intent,系统会找出能够响应这个Intent的活动启动,这样照相机程序就会被打开,拍下的照片将会输出到output_image.jpg中。
注意,刚才我们是使用startActivityForResult()来启动活动的,因此拍完照后会有结果返回到onActivityResult()方法中。如果发现拍照成功,就可以调用BitmapFactory的decodeStream()方法将output_image.jpg这张照片解析成Bitmap对象,然后把它设置到ImageView中显示出来。
不过现在还没结束,刚才提到了内容提供器,那么我们自然要在AndroidManifest.xml中内容提供器进行注册了,如下所示:(不多说,直接上代码)
其实,android:name属性的值是固定的,android:authorities属性的之必须要和刚才FileProvider.getUriForFile()方法中第二个参数一致。另外,这里还有<provider>标签的内部使用<meta-data>来指定Uri的共享路径,并引入了一个@xml/file_paths资源。当然,这个资源现在还是不存在的,下面我们就来创建它。
右击res目录---->NEW----->File,创建一个file_paths.xml文件。然后修改file_paths.xml文件中的内容,如下所示:
其中,external-path就是用来指定Uri共享的,name属性的值可以随便填,path属性的值表示共享的的具体路径。这里设置空值就表示将整个SD卡进行共享,当然你也可以仅共享我们存放output_image.jpg这张图片的路径。
另外还有一点要注意,在Android4.4系统之前,访问SD卡的应用关联目录也是要声明权限的,从4.4系统开始不再需要权限声明。那么我们为了能够兼容老版本系统的手机,还需要在AndroidManifest.xml中声明一下访问SD卡的权限
这样代码就编写完了,现在将程序运行运行到手机上。
8.3.2 从相册中选择照片
虽然调用摄像头拍照既方便又快捷,但我们并不是每次都需要去当场拍以后在那个照片的,因为每个人的手机相册里应该存在许许多多张照片,直接从相册里选取一张现在照片回避打开相机拍照更加常用。一个优秀的应用程序应该讲这两种选择方式都提供给用户,由用户决定使用哪一种。下面我们就来看一下,如何才能实现从相册中选择照片的功能。
还是在CameraAlbumTest项目的基础上进行修改,编辑activity_main.xml文件,在布局中添加一个按钮用于从相册选择照片,代码如图所示:
可以看到,在Choose_From_Album按钮的点击事件里我们现实进行了一个运行权限处理,动态申请WRITE_EXTERNAL_STORAGE这个危险权限。为什么需要申请证券西安啦?因为相册中的照片都是存在SD卡上,我们要从SD卡中读取照片就需要申请这个权限。WRITE_EXTERNAL_STORAGE表示同时授予程序对SD卡读写能力。
当用户授予了权限申请之后,会调用openAlbum()方法,这里我们先是构建出了一个Intent对象,并将它的action指定为android.intent.action.GET_CONTENT.接着给这个Intent对象设置一些必要参数,然后调用startActivityForResult()方法就可以打开相册程序选择照片了。注意在调用startActivityForResult()方法的时候,我们个诶第二个参数传入的值变成了CHOOSE_PHoto的case图片,接下来的逻辑就比较复杂了,首先为了兼容新老版本的手机,我们做了一个判断,如果是4.4及以上系统的手机就调用handleImageOnKiKat()方法来处理图片,否则就调用handleImageBeforeKitke()方法来处理图片。之所以要这样做,是因为Android系统从4.4版本开始,选取相册中的图片不再返回图片真实的Uri了,而是一个封装过的Uri,因此如果是4.4版本以上的手机就需要对这个Uri进行解析才行。
那么handleImageOnKiKat()方法中的逻辑基本是如何解析这个封装过的Uri了。这里有好几种判断情况,如果返回的Uri是document类型的话,那就取出document id进行处理,如果不是的话,那么就是用普通的方式处理。另外,如果Uri的authority是media格式的话,document id还需要再进行一次解析,要通过字符串分隔的方式取出后半部分才能得到真正的数字id。取出的id用于构建新的Uri和条件语句,然后把这些值作为参数传入到getImagePath()方法当中,就可以获取到图片的真实路径了。拿到图片的路径之后,在调用disPlayImage()方法将图片显示在界面上。
相比于handleImageOnKikAT()方法,handleImageBeforeKiKat()方法中的逻辑就要简单很多了,因为它的Uri是没有封装过的,不需要任何解析,直接将Uri传入到getImagePath()方法当中就能获取到图片的真实路径了,最后同样是调用displayImage()方法来让图片显示到界面上。
现在将程序重新运行到手机上,然后点击一下choose From Album按钮,首先会弹出权限申请对象画框,如图8.14所示。
点击允许之后就会打开手机相册,如图8.15所示:
然后随意选择一张照片,回到我们程序的界面,选中的照片应该就会显示出来了,如图8.16所示。
调用摄像头拍照以及从相册中选取照片很多Android应用都会带有的功能,现在你已经将这两种技术学会了,将来工作中需要开发类似的功能,相信你一定会轻松完成。不过目前我们现实还不算完美,因为有些照片经过裁剪之后体积仍然很大,直接加载到内存中又可能会导致程序崩溃。更好的做好是根据项目的需求先对照片进行适当的压缩,然后再加载到内存中。至于如何对照片进行压缩,就要开眼你查阅资料的能力了,这里就不再展开进行讲解了。