第四课:使用相机拍照

本课的目标是整合Camera这样我们可以用他来拍照了,但是要达到这个目标需要下点功夫。
除了要出发相机拍照之外,我们还要:

  • 将照片移动到手机上的持久储存
  • 在模板中展示这些照片
  • 显示其他的照片
  • 允许删除照片
    这样看来本课程还是蛮大的。在本应用的开始部分我们设置了Ionic Native,也就是基础部分里面的内容,Ionic Native是Ionic包装好的Cordova插件,使用更简单。
    本课程中将用到Ionic Native中的Camera插件和File插件。
    让我们愉快的开始吧!

创建一个Photo数据模型

在我们添加相片功能之前,我们需要创建一个数据模型来代表照片对象。当我们想要存放照片存储路径,照片拍摄日期的时候。数据模型就能够很轻松的处理这样的事情,像这样:

  1. let photo = new PhotoModel('path/to/image', new Date());
  2. rather than this:
  3. let photo = {
  4. image: 'path/to/image',
  5. date: newDate
  6. };

区别不是太明显,也不是所有的原因都需要用到,但是是一个很好的模式,特别是当数据越来越复杂的时候。举个复杂数据模型的例子,去看看Quick List课程,如果你还没看过的话。
> 修改 src/models/photo-model.ts 为如下:

  1. export class PhotoModel {
  2. constructor(public image: string, public date: Date){
  3. }
  4. }

这个模型很直白,我们传入了图片和日期。如果想要在home.ts中使用他的话,得先导入。
> 修改 src/pages/home/home.ts 为如下:

  1. import { Component } from '@angular/core';
  2. import { NavController } from 'ionic-angular';
  3. import { PhotoModel } from '../../models/photo-model';
  4. @Component({
  5. selector: 'page-home',
  6. templateUrl: 'home.html'
  7. })
  8. export class HomePage {
  9. constructor(public navCtrl: NavController) {
  10. }
  11. }

制作一个简单的Alert服务

Surprise!(译者:惊喜!好像不大好听)在进入开心环节之前,我们还有一个要做的事情。由于很多地方可能会有错误提示(拍照,移动照片),所以我们出发大量的警告框,当然,事情处理好了之后我们也要告诉用户一声不是。
创建警告提示的语法是这样的:

  1. let alert = this.alertCtrl.create({
  2. title: "My Title",
  3. message: "My Message",
  4. buttons: [
  5. {
  6. text: 'Ok'
  7. }
  8. ]
  9. });
  10. alert.present();

代码量还是有点的,如果我们在同一个文件里多次调用这个代码,看起来会很乱。所以我们新建另一个服务来处理这样的事情,就像数据模型一样。创建完之后,我们就可以像下面这样去弹出警告框:

  1. let alert = this.simpleAlert.createAlert('Oops!', 'Something went wrong.');
  2. alert.present();

> 修改 src/providers/simple-alert.ts 为如下:

  1. import { Injectable } from '@angular/core';
  2. import { AlertController } from 'ionic-angular';
  3. @Injectable()
  4. export class SimpleAlert {
  5. constructor(public alertCtrl: AlertController){
  6. }
  7. createAlert(title: string, message: string): any {
  8. return this.alertCtrl.create({
  9. title: title,
  10. message: message,
  11. buttons: [
  12. {
  13. text: 'Ok'
  14. }
  15. ]
  16. });
  17. }
  18. }

这里我们导入了AlertController服务用来创建警告框,然后创建了一个函数,结束一个title和一个message,然后返回一个用这些信息组装的的警告框。这虽然给我们创建了警告提示框,但是展示还是要为我们手动去展示出来。
> 修改 src/pages/home/home.ts 如下:

  1. import { Component } from '@angular/core';
  2. import { NavController } from 'ionic-angular';
  3. import { PhotoModel } from '../../models/photo-model';
  4. import { SimpleAlert } from '../../providers/simple-alert/simple-alert';
  5. @Component({
  6. selector: 'page-home',
  7. templateUrl: 'home.html'
  8. })
  9. export class HomePage {
  10. constructor(public navCtrl: NavController, public simpleAlert: SimpleAlert)
  11. {
  12. }
  13. }

注意我们在构造器里面注入了SimpleAlert和NavController,但是没有注入照片数据模型。这是因为每次我们要用到PhotoModel的时候,我们都是通过new新建一个实例的,但是对于SimpleAlert而言我们都是一次一次调用他统一实例的同一函数的。

使用Camera照相

好了,完成了照片模型和弹窗服务之后 — 我们终于来到了有趣的部分了,也就是拍照。我们现在要使用Ionic Native的Camera插件,我们得先进行导入:
> 在 src/pages/home/home.ts 中加入以下导入语句:

  1. import { Camera, File } from 'ionic-native';

现在,我们可以通过Camera对象来访问这个功能了。我们将一下如何使用这个插件,但是记住所有Ionic Native里面的插件都可以在这里找到文档:http://ionicframework.com/docs/native/Camera/
在写拍照相关的代码之前,我们将要到构造器里面添加一些变量,这样后面就不要老是导入这个导入那个,我们把应用后面会用到的全部都导入进来好了。我们将要加入一些新的函数,一些引用了其他函数的函数。这样我们就不会遇到函数undefined的问题,我们全部定义好,但是都留空。我们后续会详细实现他们(不全部是在本课中)。
> 修改 src/pages/home/home.ts 为如下:

  1. import { Component } from '@angular/core';
  2. import { ModalController, AlertController, Platform } from 'ionic-angular';
  3. import { PhotoModel } from '../../models/photo-model';
  4. import { SimpleAlert } from '../../providers/simple-alert';
  5. import { SlideshowPage } from '../slideshow/slideshow';
  6. import { Data } from '../../providers/data'
  7. import { Camera, File } from 'ionic-native';
  8. declare var cordova;
  9. @Component({
  10. selector: 'page-home',
  11. templateUrl: 'home.html'
  12. })
  13. export class HomePage {
  14. loaded: boolean = false;
  15. photoTaken: boolean = false;
  16. photos: PhotoModel[] = [];
  17. constructor(public dataService: Data, public platform: Platform, public simpleAlert: SimpleAlert, public modalCtrl: ModalController, public alertCtrl: AlertController) {
  18. }
  19. ionViewDidLoad(){
  20. // Uncomment to use test data
  21. /*this.photos = [
  22. new PhotoModel('http://placehold.it/100x100', new Date()),
  23. new PhotoModel('http://placehold.it/100x100', new Date()),
  24. new PhotoModel('http://placehold.it/100x100', new Date())
  25. ]*/
  26. this.platform.ready().then(() => {
  27. this.loadPhotos();
  28. });
  29. document.addEventListener('resume', () => {
  30. if(this.photos.length > 0){
  31. let today = new Date();
  32. if(this.photos[0].date.setHours(0,0,0,0) === today.setHours(0,0,0,0)){
  33. this.photoTaken = true;
  34. } else {
  35. this.photoTaken = false;
  36. }
  37. }
  38. }, false);
  39. }
  40. loadPhotos(): void {
  41. }
  42. takePhoto(): any {
  43. }
  44. createPhoto(photo): void {
  45. }
  46. removePhoto(photo): void {
  47. }
  48. playSlideshow(): void {
  49. }
  50. sharePhoto(image): void {
  51. }
  52. save(): void {
  53. }
  54. }

上面的代码中,我们新增了一个成员变量loaded用于保持追踪照片是否都从存储中加载完成(下节课要做的),photoTaken用于标记今天是否有拍照,photos用于持有所有的照片数据。由于照片只会在应用运行在真实设备的情况下才会加载,所以我们在ionViewDidLoad中加入了一些测试数据,我们在此处给this.photos添加了一个测试数据这样就可以在浏览器中进行测试。如果你想在测试的时候看到相片呈现,解除其中的注释即可。(记得后面删掉!)
我们已经设置好了所有需要用到的服务的引用,包括稍后要用到的数据服务和平台服务。
在这里我们也添加了一个奇怪的resume监听器。resume事件的触发时机是用户把应用发配到后台然后重新使用的时候将会触发。例如,当用户打开了你的应用,然后去玩Facebook,然后有回到我们应用的时候。我们这么做的原因是因为我们的photoTaken变量有一些极端的特例。想象一下,某人今天拍照了,但是当他们关闭应用但是没有全部关闭的时候,他只是被发配后台。第二天我们使用这个应用的时候,我们在loadPhoto函数中运行的用于判断当日是否拍照的逻辑将不会执行,因为只是恢复而不是重新加载。所以当应用恢复的时候,我们都要检查最后照片拍摄日期是否跟今日日期椅子,然后根据他去设置photoTaken变量。
注意:我们这里加上来declare var cordova,这样TypeScript不会抱怨:我根本不知道cordova是啥。
现在我们将实现takePhoto函数,这个函数是用来拍照的,所以我们先来个基础版。
> 修改 takePhoto 函数为如下:

  1. takePhoto(): any {
  2. if(!this.loaded || this.photoTaken){
  3. return false;
  4. }
  5. if(!this.platform.is('cordova')){
  6. console.log("You can only take photos on a device!");
  7. return false;
  8. }
  9. let options = {
  10. quality: 100,
  11. destinationType: 1, //return a path to the image on the device
  12. sourceType: 1, //use the camera to grab the image
  13. encodingType: 0, //return the image in jpeg format
  14. cameraDirection: 1, //front facing camera
  15. saveToPhotoAlbum: true //save a copy to the users photo album as well
  16. };
  17. Camera.getPicture(options).then((imagePath) => {
  18. console.log(imagePath);
  19. },
  20. (err) => {
  21. let alert = this.simpleAlert.createAlert('Oops!', 'Something went wrong.');
  22. alert.present();
  23. });
  24. }

执行Camera代码之前,我们检查了一系列的条件检查。如果之前创建的变量显示数据没有加载完成的话,或者今日已拍照的话,函数内后面的代码就不会执行了。我们也检查了我们是否是运行在‘cordova’平台上,也就是真机上,如果没有的话就直接退出函数。因为啊,你只能在真实设备上访问这些插件,如果不是在真实设备上运行的话,那么除了报错就只能报错了。
接着我们设置了一些选项传入Camera插件,这些配置了我们要干啥,我们要返回啥。这些值可以配置的东西包括,是否要用摄像头或者用户的照片库,返回图片的格式,用前置摄像头呢,还是用后置摄像头等等。我在每个选项后面都添加了注释,来解释他们分别是干啥的。
创建好了选项对象之后,我们调用了Camera对象的getPicture函数然后传入其中。这个函数讲会返回一个Promise,在执行完成之后,这个Promise将会给我们返回一个图片在设备上的存储路径。现在我们只是用日志输出这个值,但是这个值后续会好好用上的。如果返回的是一个错误值的话,我们就用SimpleAlert来展示错误信息给用户了。
照片照好了,存放路径也返回了,临时文件夹里。这样图片临时显示出来了,照片不会长久的保存,因为临时目录会被随时清理掉。为解决此问题,我们需要把照片移动其他地方去,然后再次存储这个照片的新路径。我们可以用File插件来完成这个。

将照片移动到永久存储中

为了用上File插件,我们需要对takePhoto函数进行一些修改:

  • 通过返回的 imagePath 来找到临时存储里面的照片
  • 重命名,然后移动到设备上的‘snapaday’文件夹(也就是永久存储)
  • 基于这个新的位置新建一个照片对象

看来takePhoto函数会变得复杂了好多。因为要写这么多代码。我会对代码详细添加注释,当然也会一步步的去讲解。
> 修改 takePhoto里的 getPicture 调用如下:

  1. Camera.getPicture(options).then((imagePath) => {
  2. //Grab the file name
  3. let currentName = imagePath.replace(/^.*[\\\/]/, '');
  4. //Create a new file name
  5. let d = new Date(),
  6. n = d.getTime(),
  7. newFileName = n + ".jpg";
  8. if(this.platform.is('ios')){
  9. //Move the file to permanent storage
  10. File.moveFile(cordova.file.tempDirectory, currentName,cordova.file.dataDirectory, newFileName).then((success: any)=> {
  11. this.photoTaken = true;
  12. this.createPhoto(success.nativeURL);
  13. this.sharePhoto(success.nativeURL);
  14. }, (err) => {
  15. console.log(err);
  16. let alert = this.simpleAlert.createAlert('Oops!', 'Something went wrong.');
  17. alert.present();
  18. });
  19. } else {
  20. this.photoTaken = true;
  21. this.createPhoto(imagePath);
  22. this.sharePhoto(imagePath);
  23. }
  24. },(err) => {
  25. let alert = this.simpleAlert.createAlert('Oops!', 'Something went wrong.');
  26. alert.present();
  27. });

希望上面添加的注释可以让后面将要进行的东西变得简单些,这里有一个高级别的手把手的讲解,从camera返回imagePath后发生了什么:

  1. 移除 imagePath 最后一个 / 前面的所有内容得到当前文件名
  2. 使用日期新建一个唯一的文件名,这样我们不会覆盖任何东西
  3. 如果我们是在iOS上运行的话,我们将照片从临时存储移动到持续存储中
  4. photoTaken 设为 true,将新路径传给图片然后传到 createPhotosharePhoto

做完这些的话咱们的相片就存储到永久存储空间去了。记住,咱们现在还没有定义createPhotosharePhoto函数。我们现在来创建createPhoto函数,对于sharePhoto的话,我们留到整合社交分享插件的时候再来实现。
createPhoto函数接受一个路径参数(nativeURL本地路径)并在应用中持有他的引用。
> 修改 createPhoto 函数为如下:

  1. createPhoto(photo): void {
  2. let newPhoto = new PhotoModel(photo, new Date());
  3. this.photos.unshift(newPhoto);
  4. this.save();
  5. }

你也看到了好简单的说。我们在使用Photo Model新建相片实例的时候传入了一个相片的本地路径和创建日期,然后将新建的相片对象加入到this.photos数组。我们没有用push方法(因为是将相片添加到数组尾部),我们通过unshift将他添加到数组头部。我们这部做的原因是我们需要以反向的方式来展示图片(最新的最先显示),这么做我们的生活就更轻松。
同时我们也调用了save函数,他会将我们的数据保存到数据存储中,但是我们目前还没有实现这个函数。

更新模板

我们现在将相片添加到了this.photos数组,我们现在可以将模板更新为循环显示他们了。由于你可能是想通过浏览器测试,不能通过照相机来添加照片,那么请随意打开测试数据的注释,这样就可以看到本部分代码的效果。
我们现在来更新模板。
> 修改 src/pages/home/home.html 为如下:

  1. <ion-header>
  2. <ion-navbar color="danger">
  3. <ion-title>
  4. <img src="assets/images/logo.png" />
  5. </ion-title>
  6. <ion-buttons end>
  7. <button ion-button icon-only (click)="playSlideshow()"><ion-icon name="play"></ion-icon></button>
  8. </ion-buttons>
  9. </ion-navbar>
  10. </ion-header>
  11. <ion-content>
  12. <ion-list>
  13. <button ion-item *ngIf="!photoTaken" detail-none (click)="takePhoto()">
  14. <img src="assets/images/smile.png" />
  15. </button>
  16. <ion-item-sliding *ngFor="let photo of photos">
  17. <ion-item>
  18. <img [src]="photo.image" />
  19. <ion-badge item-right light>0 days ago</ion-badge>
  20. </ion-item>
  21. <ion-item-options>
  22. <button ion-button icon-only color="light" (click)="removePhoto(photo)"><ion-icon name="trash"></ion-icon></button>
  23. </ion-item-options>
  24. </ion-item-sliding>
  25. </ion-list>
  26. </ion-content>

对模板主要有两大变更。首先,我们用上了ngFor来村换相片数组,然后为每个项创建了一个入口。通过在let photo of photos中的let photo,我们可以得到当前渲染的照片的应用,即:photo.image访问照片的存储路径。
由于我们可以访问到每张照片的路径,我们就可以设置img元素来展示他了。注意我们用来方括号[src],他允许我们将photo.image设置到image元素上来。这意味着会先评估photo.image,然后蛇之都奥src。如果我们不用方括号的话,src将是字符串”photo.image”。
我们也有一个删除按钮,他会将相片(也就是let photo)传入到removePhoto函数。我们现在就来实现这个方法吧。
> 修改 src/pages/home/home.ts 里的 removePhoto 函数:*

  1. removePhoto(photo): void {
  2. let today = new Date();
  3. if(photo.date.setHours(0,0,0,0) === today.setHours(0,0,0,0)){
  4. this.photoTaken = false;
  5. }
  6. let index = this.photos.indexOf(photo);
  7. if(index > -1){
  8. this.photos.splice(index, 1);
  9. this.save();
  10. }
  11. }

这个函数的第二部分够直白了,我们在photos数组中找到photo,移除他,触发save。第一部分看起来有点迷。这里搞事的是,如果用户拍勒个照,然后删除这个照片,他们就可以另外拍一张了(因为当日无照哇)。但是当他们删除了较早的照片的时候,他们就没法拍照。
所以啦,我们就获取被删除的照片的拍摄日期,然后跟今天日期进行对比。如果删除的是今日的照片,那么我们就把this.photoTaken设置为false这样用户今天有可以开心的拍另一张照片了。

总结

这节课实在是…!!!这节课设计了相当复杂的东西,但是我们在这节课中实现了应用的核心功能。还剩下一点点东西,但是我们现在可以拍照了,可以在将他们在一个列表中展示出来了,酷不酷!如果你想看看效果的话,到真机上去跑一下吧。如果你不知道我们做了些啥,可以直接跳到本书的 测试与调试部分去了解如何在设备上安装应用,然后回来完成应用剩余部分。
重要:如果现在想要在真机上测试拍照,那么你就需要将loadPhotos更新一下:

  1. loadPhotos(): void {
  2. this.loaded = true;
  3. }

你不能在数据加载完成之前拍照,所以我们得先伪造一下。在接下来的课程中,我们将学习如何创建一个数据服务来永久存储我们的照片,这样在用户后面回到应用的时候可以获取他们。