Android
iOS
Cordova
ionic
qnoteDay 19

Ionic2で写真にお絵描きできるアプリを作ってみた

More than 1 year has passed since last update.

概要

Cordovaを使ってアプリを作ってみようと思ったものの、UIを自分で一から実装するのは時間もかかるので、UI Frameworkを導入して作ることにした。
さくっと調べたところ、Ionicが良さそう。
ユーザー数が多いフレームワークを選択したら、困ったときに情報がみつかりやすいと思ったので。

参考:
Top 7 Mobile application HTML5 frameworks
React/Angular2時代のUIフレームワーク考──Ionic2、Onsen UI2を語る

Onsen UIもとても気になった。
他のフレームワークに依存しないという方針、とても良いと思った。

Ionicは独自CLI(CordovaのCLIをラップしている)なので、またCLIの操作を覚えないといけないのが難点といえば難点か。

先に断っておくが、今回はほぼ実際の試行錯誤に則って記事を書いている。
プラグインの使い方を誤って実装し直しという事態にも遭遇した(完全に自分の落ち度)。
そのような後から間違いに気づいて書き直した場合は、気づいた時点で書き直したコードを再度掲載している。
紛らわしくて申し訳ないが、ご容赦を。

こんなアプリを作る

自分の作るサンプルアプリがリストビューでなにかを表示するといったものが多くて、自分でこいつ頭悪いんじゃないかと思ったので、お絵描きアプリを作ることにした。
写真の上に自分でメッセージ書いてSNSにシェアすることを目指す。
そんなアプリ、ストアにたくさんあるんだろうけど、リストビューよりは楽しいだろう。

開発環境

  • Mac
  • OS 10.11.5
  • Xcode8.1、Android Studio2.1導入済み(SDK含む)
  • Node.js v6.9.1
  • Cordova v6.4.0
  • bower v1.8.0

Ionic導入

ググったらチュートリアル記事を見つけた。
Build a Mobile App with Angular 2 and Ionic 2

私の環境はすでにCordovaがインストール済なので、下記をインストール。

コマンド
npm install -g ionic
npm install -g typescript

あ、ionic info という状態を確認するコマンドで、ワーニングが出ていたので、ios-sim もインストールした。

コマンド
npm install -g ios-sim

Ionicのバージョンは下記。

コマンド
$ ionic --version
2.1.13

チュートリアルをやってみた

アプリを作る前にチュートリアルをやっておこうと思って、適当に探して見つけたチュートリアルをやってみた感想。

自分のtypoで動かなかったことをのぞけば、順調だった(当たり前か)。

JavaScriptというよりは、TypeScriptなんだね、Ionic。
TypeScriptは、なんか不思議な文法。
書き慣れたらスラスラ書けるのだろうか。

あと、Cordovaも同じなんだろうけど、

コマンド
ionic serve

で、ブラウザでアプリをエミュレートしているときに、ファイルを更新したらビルドし直さなくても更新内容が反映されるの、とても良い。
ネイティブだと更新のたびにそこそこ長いビルド時間がかかるので、テンポよく開発することができる。
ネイティブでもCIで自動ビルド→自動配信みたいなことをしていれば、テンポよくできるんだろうな。

お絵描きアプリを作ってみた

なんとなく感じはつかめた気がしたので、お絵描きアプリ制作に入る。

機能としては下記のようなものを想定。

お絵描き機能

  • 指でなぞって絵が描くことができる
  • 写真に落書きすることができる
    • 写真を端末のギャラリーから選択することができる

付随機能

  • 描いた絵を保存することができる
  • 保存した絵の一覧をみることができる
  • 一覧で選択した絵を全画面表示することができる
  • 描いた絵をSNSにシェアすることができる
  • 描いた絵を削除することができる

ソースコードは段階的に載せていくので、面倒な人は読み飛ばしてgithub見るといいよ。

プロジェクトを作る

適当にアプリ名を決めてプロジェクトを作る。
アプリ名決めるの、難しいよね。

新規プロジェクトは

コマンド
ionic start MyApp --v2

でできる。

私はテンプレート使ってみたかったので

コマンド
ionic start scribbling sidemenu --v2

とした。

参照:
ionic2-starter-sidemenu

--v2 は、ionic2を使うよということらしい。

--id com.sample.appname でiOSでいうところのバンドルIDも指定できるんだけど、今回はストア公開の予定もないので、指定しなかった。
あとでも変えられるはず。

テンプレートやテーマは公式のストアにいろいろあるので、眺めてみると楽しい。
テンプレート:Starters
テーマ   :Themes

デバッグ用にブラウザでアプリを起動しておく。

コマンド
ionic serve

これでファイルを保存するたびにビルドされて、最新のアプリの状態を確認できる。
Chromeのデベロッパーツールでコンソールを表示しておくと、さらに便利。

お絵描き用のプラグインを選定

HTML5のCanvas使えばお絵描きできる気がする!ということで、ググる。

最初に見つけた下記のモジュールが良さそう。
signature_pad

Ionicでも使えるようだ。
Building a Signature Drawpad using Ionic 2

このモジュールを使えば、お絵描き機能については、ほぼ実現できそうだ。

必要なものをいろいろとインストール。

コマンド
npm install angular2-signaturepad --save
npm install @ionic/storage --save
ionic plugin add cordova-sqlite-storage --save

npm install-g をつけるとプロジェクト以外でも使えるようにインストールされるんだけど、ちゃんと考えて使わないといけないんだろうなー、とかいまさら思った。

必要な画面を作成

テンプレートで自動生成された /scribbling/src/pages/ の中身は削除して新しく画面を作る。

コマンド
ionic g page canvas
ionic g page gallery
ionic g page details

pagesディレクトリはこんな構造になる。

コマンド
.
├── canvas
│   ├── canvas.html
│   ├── canvas.scss
│   └── canvas.ts
├── details
│   ├── details.html
│   ├── details.scss
│   └── details.ts
└── gallery
    ├── gallery.html
    ├── gallery.scss
    └── gallery.ts

ファイル名をがんばって変えるより新規で作ったほうが早いね。

作成したページを app.component.tsapp.module.ts に登録する

登録?
紐付ける?
とにかくそんな感じのことをする。

ソースを省略して掲載したかったんだけど、むずいので、全部載せ。

app.component.ts
import { Component, ViewChild } from '@angular/core';
import { Nav, Platform } from 'ionic-angular';
import { StatusBar, Splashscreen } from 'ionic-native';

// 使用する画面を追加
import { CanvasPage } from '../pages/canvas/canvas';
import { GalleryPage } from '../pages/gallery/gallery';


@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  @ViewChild(Nav) nav: Nav;

  // アプリ起動時に表示する画面
  rootPage: any = CanvasPage;

  pages: Array<{title: string, component: any}>;

  constructor(public platform: Platform) {
    this.initializeApp();

    // メニューに表示される画面を登録
    this.pages = [
      { title: 'Canvas', component: CanvasPage },
      { title: 'Gallery', component: GalleryPage },
    ];
  }

  initializeApp() {
    this.platform.ready().then(() => {
      StatusBar.styleDefault();
      Splashscreen.hide();
    });
  }

  openPage(page) {
    this.nav.setRoot(page.component);
  }
}
app.module.ts
import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';

// 使用する画面を追加
import { CanvasPage } from '../pages/canvas/canvas';
import { GalleryPage } from '../pages/gallery/gallery';
import { DetailsPage } from '../pages/details/details';


@NgModule({
  declarations: [
    MyApp,
    // 使用する画面を登録
    CanvasPage,
    GalleryPage,
    DetailsPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    // 使用する画面を登録
    CanvasPage,
    GalleryPage,
    DetailsPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}]
})
export class AppModule {}

detailsをメニューに登録していないのは、gallaryからのみ遷移させるから。

作成した画面でメニューが使えるようにする

作成しただけではメニューが使えないので、使えるようにする。

canvas.html
<ion-header>
  <ion-navbar>
    <button ion-button icon-only menuToggle>
      <ion-icon name="menu"></ion-icon> 
    </button>
    <ion-title>
      canvas
    </ion-title>
  </ion-navbar>
</ion-header>


<ion-content padding>

</ion-content>

<ion-navbar>〜</ion-navbar> の間にメニューの処理を追加した。
他の画面も同様に。

canvas画面でお絵描きできるようにする

参考記事を真似て、お絵描き機能を仮実装。

まず、Signature Pad、Storageをモジュールとして登録。

app.module.ts
// モジュールを追加
import { SignaturePadModule } from 'angular2-signaturepad';
import { Storage } from '@ionic/storage';

・・・

@NgModule({

  ・・・

  imports: [
    IonicModule.forRoot(MyApp),
    SignaturePadModule //追加
  ],

  ・・・

  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, Storage] //Storage追加
})
export class AppModule {}

続いて、canvas.ts を修正。

canvas.ts
import { Component, ViewChild } from '@angular/core';
import { NavController } from 'ionic-angular';

import { SignaturePad } from 'angular2-signaturepad/signature-pad';
import { Storage } from '@ionic/storage';
import { ToastController } from 'ionic-angular';

@Component({
  selector: 'page-canvas',
  templateUrl: 'canvas.html'
})
export class CanvasPage {
  signature = '';
  isDrawing = false;

  @ViewChild(SignaturePad) signaturePad: SignaturePad;
  signaturePadOptions: Object = {
    'minWidth': 2,
    'canvasWidth': 400,
    'canvasHeight': 200,
    'backgroundColor': '#f6fbff',
    'penColor': '#666a73'
  };

  constructor(public navCtrl: NavController, public storage: Storage, public toastCtrl: ToastController) {}

  ionViewDidEnter() {
    this.signaturePad.clear()
    this.storage.get('savedSignature').then((data) => {
      this.signature = data;
    });
  }

  drawComplete() {
    this.isDrawing = false;
  }

  drawStart() {
    this.isDrawing = true;
  }

  savePad() {
    this.signature = this.signaturePad.toDataURL();
    this.storage.set('savedSignature', this.signature);
    this.signaturePad.clear();
    let toast = this.toastCtrl.create({
      message: 'New Signature saved.',
      duration: 3000
    });
    toast.present();
  }

  clearPad() {
    this.signaturePad.clear();
  }
}

続いて、canvas.html を修正。

canvas.html
・・・

<ion-content>
  <div class="title-label">Please draw your Signature</div>
  <ion-row [ngClass]="{'drawing-active': isDrawing}">
    <ion-col></ion-col>
    <ion-col>
      <signature-pad [options]="signaturePadOptions" (onBeginEvent)="drawStart()" (onEndEvent)="drawComplete()"></signature-pad>
    </ion-col>
    <ion-col></ion-col>

  </ion-row>
  <button ion-button full color="danger" on-tap="clearPad()">Clear</button>
  <button ion-button full color="secondary" on-tap="savePad()">Save</button>

  <ion-row>
    <ion-col></ion-col>
    <ion-col width-80>
      <img [src]="signature"/>
    </ion-col>
    <ion-col></ion-col>
  </ion-row>
</ion-content>

続いて、canvas.scss を修正。

canvas.scss
page-canvas {
  .drawing-active {
    background: #d6fffc;
  }
  .title-label {
    text-align: center;
    font-weight: bold;
    padding: 10px;
  }
}

これでブラウザでお絵描きしてSAVEボタンをタップすると、お絵描きしたものが画面下部に表示される。
いいね!

お絵描き機能として実装したいものが残っているけど、先に他の画面の実装をすることにした。
お絵描き機能で詰まったときに、すべてのやる気がなくなる予感がしたので。

gallery画面を実装

gallery画面は、iOSのUICollectionViewを使用してCell内に画像を配置して、グリッド状に並べたデザインをイメージしている。

ググったら参考になりそうな記事を見つけた。
Layout the Cool Way: Using the Ionic 2 Grid Component

記事内にあったGithubのサンプルコードをダウンロードして早速試す。
あ、Ionicのサンプルコードはダウンロードしたあとに、npm install すると環境を作ってくれる。
足りないものがあればログで教えてくれる。

上記のサンプルを動かすために必要なのは下記のコマンド。

コマンド
cd ionic-imagepicker-sample-master
npm install
ionic plugin add cordova-plugin-image-picker
ionic serve

動かしてみた結果、gallery画面の基本デザインは期待していたものだったので、参考にさせていただく。

続いて、端末に保存した画像の一覧を表示する機能を作らねば。
iOSネイティブだったらRealmで簡単にできそうなんだけど。

さて、お絵描き機能で cordova-sqlite-storage をすでに追加済なので、それをキーワードに検索する。

Using Ionic 2 SqlStorage For a Simple Evernote Clone(この記事は削除されたメソッド使っているので、参考程度)
Build a Todo App from Scratch with Ionic 2
Use SQLite In Ionic 2 Instead Of Local Storage

SQLiteで画像を扱う方法も調べておく。

How to take picture using cordova camera and save into sqlite

画像の取得/保存に関するサービスを作るので、providerを作成。

コマンド
ionic g provider storage-service

作成した storage-service.tsapp.module.ts に追加。

app.module.ts
・・・

// providerを追加
import { StorageService } from '../providers/storage-service'

@NgModule({

・・・

  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, Storage, StorageService]
})
export class AppModule {}

続いて、storage-service.ts に処理を書いていく。
Storageへの保存/取得処理も canvas.ts からこっちに移す。

storage-service.ts
import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage';

@Injectable()
export class StorageService {
  public scribbles: string[] = [];

  constructor(public storage: Storage){
    this.getData().then((pictures) => {
      if (pictures) {
        this.scribbles = pictures;
      }
    });
  }

  getData() {
    return this.storage.get('scribbles');  
  }

  addScribble(dataURI) {
    // dataURIからBase64を取り出して配列に追加
    this.scribbles.push(dataURI.split(',')[1]);
    this.save();
  }

  save() {
    this.storage.set('scribbles', this.scribbles);
  }

  // base64にmimetypeを足して返す
  convertDataURI(data: string) {
    return "data:image/png;base64," + data;
  }

  // 1pxのgifを返す
  getEmptyImage() {
    return "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
  }
}

続いて、canvas.ts を修正。

canvas.ts
・・・

// providers追加
import { StorageService } from '../../providers/storage-service'

・・・

  constructor(public navCtrl: NavController, public storage: Storage, public toastCtrl: ToastController, private storageService: StorageService) {}

  ionViewDidEnter() {
    this.signaturePad.clear()
  }

・・・

  saveScribble() {
    this.signature = this.signaturePad.toDataURL();
    this.storageService.addScribble(this.signature);

    let toast = this.toastCtrl.create({
      message: 'New scribble saved.',
      duration: 3000
    });
    toast.present();
  }

  clearScribble() {
    this.signaturePad.clear();
  }
}

続いて、gallery.ts を実装。

gallery.ts
import { Component } from '@angular/core';
import { NavController } from 'ionic-angular';

import { StorageService} from '../../providers/storage-service'

@Component({
  selector: 'page-gallery',
  templateUrl: 'gallery.html'
})
export class GalleryPage {
  scribbles: string[];
  grid: string[][];

  constructor(public navCtrl: NavController, private storageService: StorageService) {
    this.storageService.getData().then((scribbles) => {
      if (scribbles) {
        this.scribbles = scribbles;
        this.grid = Array(Math.ceil(this.scribbles.length/2));
        this.load();
      }
    });
  }

  load() {
    let rowNum = 0;

    for (let i = 0; i < this.scribbles.length; i+=2) {
      this.grid[rowNum] = Array(2);

      for (var j = 0; j < this.grid[rowNum].length; j++) {
        if (this.scribbles[i+j]) {
          this.grid[rowNum][j] = this.storageService.convertDataURI(this.scribbles[i+j]);
        } else {
          // 画像が存在しない場合は1pxのgifを表示
          this.grid[rowNum][j] = this.storageService.getEmptyImage();
        }
      }
      rowNum++;
    }
  }
}

続いて、gallery.html を実装。

gallery.html
<ion-header>
  <ion-navbar>
    <button ion-button icon-only menuToggle>
       <ion-icon name="menu"></ion-icon> 
    </button>
    <ion-title>
      gallery
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-grid>
    <ion-row *ngFor="let row of grid">
      <ion-col width-50 *ngFor="let scribble of row">
      <img [src]="scribble" />
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>

これで、canvas画面で保存した画像の一覧を、gallery画面で表示するところまでできた。
iOSとAndroidの実機でも問題なく動いている。

課題として、画像の上書き保存ができない問題があるが、先送り。

details画面を実装

gallery画面で画像をタップしたら、details画面に遷移して、SNSにシェアできるようにしたい。
あと、画像を削除できるようにしたい。

ひとまず、画像をタップしたら、details画面に遷移するようにする。
details画面を登録して、goToDetails() メソッドを追加。

galley.ts
・・・

import { DetailsPage } from '../details/details'

・・・

export class GalleryPage {
  scribbles: string[];
  grid: string[][];

  constructor(public navCtrl: NavController, private storageService: StorageService) {
    this.storageService.getData().then((scribbles) => {
      if (scribbles) {
        this.scribbles = scribbles;
        this.grid = Array(Math.ceil(this.scribbles.length/2));
        this.load();
      }
    });
  }

・・・

  goToDetails(data: string) {
    this.navCtrl.push(DetailsPage, {data});
  }
}

続いて、galley.html に、タップしたら goToDetails() メソッドを実行する処理を書く。
imgタグに on-tap="goToDetails(scribble)" を追加する。

galley.html
・・・

<ion-content>
  <ion-grid>
    <ion-row *ngFor="let row of grid">
      <ion-col width-50 *ngFor="let scribble of row" >
        <img [src]="scribble" on-tap="goToDetails(scribble)"/>
      </ion-col>
    </ion-row>
  </ion-grid>
</ion-content>

続いて、details.tsdetails.html に渡された画像を表示する処理を書く。

details.ts
import { Component } from '@angular/core';
import { NavController, NavParams } from 'ionic-angular';

@Component({
  selector: 'page-details',
  templateUrl: 'details.html'
})
export class DetailsPage {
  scribble: string;

  constructor(public navCtrl: NavController, private navParams: NavParams) {
    this.scribble = navParams.get('data');
  }

}
details.html
<ion-header>
  <ion-navbar>
    <button ion-button icon-only menuToggle>
      <ion-icon name="menu"></ion-icon> 
    </button>
    <ion-title>
      details
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <div>
    <img [src]="scribble" />
  </div>
</ion-content>

これで、galley画面で画像をタップすると、details画面に遷移して、画像が表示されるようになった。

次に、シェア機能の実装。
これにはプラグインが必要なので、追加する。

コマンド
ionic plugin add cordova-plugin-x-socialsharing

続いて、シェア・画像削除に必要なUIと機能の実装をする。
ボタンをタップした際にアラートも表示するようにした。

details.ts
import { Component } from '@angular/core';
import { NavController, NavParams, AlertController } from 'ionic-angular';

import { SocialSharing } from 'ionic-native';

@Component({
  selector: 'page-details',
  templateUrl: 'details.html'
})
export class DetailsPage {
  scribble: string;

  constructor(public navCtrl: NavController, private navParams: NavParams, private alertCtrl: AlertController) {
    this.scribble = navParams.get('data');
  }

  twitterShare() {
    SocialSharing.shareViaTwitter("絵を描きました", this.scribble, null).then(() => {
        this.showBasicAlert('', '投稿しました');
      }, () => {
        this.showBasicAlert('', '投稿に失敗しました');
      })
  }

  facebookShare() {
    SocialSharing.shareViaFacebook("絵を描きました", this.scribble, null).then(() => {
        this.showBasicAlert('', '投稿しました');
      }, () => {
        this.showBasicAlert('', '投稿に失敗しました');
      })
  }

  removeScribble() {
    var okButtonHandler = () => {
      console.log('ok');
    };
    var cancelButtonHandler = () => {
      console.log('cancel');
    };
    var buttonTitles = {"okButton": "OK", "cancelButton": "Cancel"};
    this.showConfirmAlert('確認', '削除しますか?', buttonTitles, okButtonHandler, cancelButtonHandler);
  }

  showBasicAlert(title: string, message: string) {
      let alert = this.alertCtrl.create({
      title: title,
      subTitle: message,
      buttons: ['OK']
    });
    alert.present();
  }

  showConfirmAlert(title: string, message: string, buttonTitles: {[key: string]: string}, okButtonHandler: ()=>void, cancelButtonHandler: ()=>void) {
    let confirm = this.alertCtrl.create({
      title: title,
      message: message,
      buttons: [
        {
          text: buttonTitles["cancelButton"],
          handler: () => {
            cancelButtonHandler();
          }
        },
        {
          text: buttonTitles["okButton"],
          handler: () => {
            okButtonHandler();
          }
        }
      ]
    });
    confirm.present();
  }
}

適当にCSSでデコレーション。
求ム、CSS力。

details.scss
page-details {
    .share-toolbar {
        margin: 0;
        position: relative;
    }
    .button {
        margin-left: 5px;
        border-radius: 0.5em;
        border-radius: 0.5em;
    }
    #twitter {
        background: #1DA1F2;
    }
    #facebook {
        background: #3b5998;
    }
    #trash {
        background: #333;
        position: absolute; 
        right: 5px;
    }
    .icons {
        margin: 5px;
        color: #FFF;
        font-size: 30px;
    }
}

続いて、htmlを修正。
<ion-footer> で画面下部にフッターが作られて、<ion-toolbar> でiOSでいうところのナビゲーションバーのようなデザインのバーが作られる。
ボタンのアイコンはioniconsのアイコンを使用。
いろいろあって便利。
注意点は、リストのname列の名称と、<ion-icon name=""> のnameに入る名称が異なる場合があること。
公式サイトでアイコンをクリックすると使い方が表示されるので確認してから使用すると幸せ。

details.html
<ion-header>
  <ion-navbar>
    <button ion-button icon-only menuToggle>
      <ion-icon name="menu"></ion-icon> 
    </button>
    <ion-title>
      details
    </ion-title>
  </ion-navbar>
</ion-header>

<ion-content padding>
  <ion-row>
    <ion-col>
      <img [src]="scribble" />
    </ion-col>
  </ion-row>
</ion-content>

<ion-footer>
  <ion-toolbar class="share-toolbar">
    <button class="button" id="twitter" on-tap="twitterShare()">
      <ion-icon class="icons" name="logo-twitter"></ion-icon>
    </button>
    <button class="button" id="facebook" on-tap="facebookShare()">
      <ion-icon class="icons" name="logo-facebook"></ion-icon>
    </button>
    <button class="button" id="trash" on-tap="removeScribble()">
      <ion-icon class="icons" name="trash"></ion-icon>
    </button>
  </ion-toolbar>
</ion-footer>

続いて、削除機能。
配列で画像のBASE64文字列を保持しているので、削除対象画像を配列から削除して、その配列をStorageにSetし直せばいけそう。

storage-services.ts に削除機能を追加して、あわせてDataURIをBase64文字列に変換する処理をメソッドに書き直す。

storage-services.ts
・・・

@Injectable()
export class StorageService {
  public scribbles: string[] = [];

・・・

  addScribble(dataURI) {
    // dataURIからBase64を取り出して配列に追加
    this.scribbles.push(this.convertBase64(dataURI));
    this.save();
  }

・・・

  // mimetypeを外して、base64を返す
  convertBase64(dataURI: string) {
    return dataURI.split(',')[1];
  }

・・・

  removeScribble(data: string) {
    var index = this.scribbles.indexOf(data);
    if (index > -1) {
      this.scribbles.splice(index, 1);
      this.save();
    }
  }
}

続いて、details画面で削除ボタンを押した際の処理を書く。
画像削除後は画面を閉じるようにした。

details.ts
・・・

// providers追加
import { StorageService } from '../../providers/storage-service'

・・・

export class DetailsPage {
  scribble: string;

・・・

  removeScribble() {
    var okButtonHandler = () => {
      this.storageService.removeScribble(this.storageService.convertBase64(this.scribble));
      this.navCtrl.pop();
    };
    var cancelButtonHandler = () => {

    };
    var buttonTitles = {"okButton": "OK", "cancelButton": "Cancel"};
    this.showConfirmAlert('確認', '削除しますか?', buttonTitles, okButtonHandler, cancelButtonHandler);
  }

  showBasicAlert(title: string, message: string) {
      let alert = this.alertCtrl.create({
      title: title,
      subTitle: message,
      buttons: ['OK']
    });
    alert.present();
  }

・・・

}

続いて、galley画面で一覧を更新する処理を書く。
constoractorに書いていたDBからの取得処理をメソッドに移して、ViewDidEnterでも呼ぶようにした。

galley.ts
・・・

export class GalleryPage {
  scribbles: string[];
  grid: string[][];

  constructor(public navCtrl: NavController, private storageService: StorageService) {
    this.refresh();
  }

  ionViewDidEnter() {
    this.refresh();
  }

・・・

  refresh() {
    this.storageService.getData().then((scribbles) => {
      if (scribbles) {
        this.scribbles = scribbles;
        this.grid = Array(Math.ceil(this.scribbles.length/2));
        this.load();
      }
    });
  }
}

ここまでで、付随機能として冒頭にあげた内容は実装できた。
なかなか難しかった。

写真にお絵描きできるようにする

ようやく写真を扱うところまできた。
飽きずに進めていこう。

Ionicで端末のギャラリーを使用するには、カメラ用のプラグインを追加すればよい。

コマンド
ionic plugin add cordova-plugin-camera --save

続いて、canvas.ts にギャラリーから画像を選択して取得する機能を実装する。
写真に落書きする方法をいろいろ検索しまくったんだけど、fromDataURL() で画像を読み込めばSignaturePadに反映された。
ただし、読み込んだだけだとSignaturePadのcanvas全面には貼られないので、resize() メソッドでリサイズする。
今度はcanvasのサイズがおかしくなるので、canvasのサイズを初期設定に戻してあげる。
これらをふまえたものが下記。

canvas.ts
・・・

import { Camera } from 'ionic-native';

・・・

  selectPicture() {
    Camera.getPicture({
        destinationType: Camera.DestinationType.DATA_URL,
        sourceType: Camera.PictureSourceType.PHOTOLIBRARY,
        targetWidth: 1000,
        targetHeight: 1000
    }).then((imageData) => {
        // imageDataにBase64文字列が返る
        this.setBackgroundImage("data:image/jpeg;base64," + imageData);
    }, (err) => {
        console.log(err);
    });
  }

  setBackgroundImage(data: string) {
    if (data) {
      // リサイズしないと画像がfitされない
      this.signaturePad.resizeCanvas(); 
      // 画像をSignaturePadに読み込む
      this.signaturePad.fromDataURL(data); 
      // SignaturePadのCanvasサイズを初期設定に戻す
      this.signaturePad.set('canvasWidth', this.signaturePadOptions["canvasWidth"]);
      this.signaturePad.set('canvasHeight', this.signaturePadOptions["canvasHeight"]);
    }
  }


・・・

あとは、selectPicture() を呼ぶ口を用意するだけ。
ついでに各種ボタンのデザインを変えた。

canvas.htmlsave-icon.png という画像ファイルを使用しているが、Ionic2では画像ファイルの格納すべきディレクトリが決まっているので注意。
そのディレクトリ以外に格納しても、iOS、Androidにビルドした際にワーニングが出てしまう。
下記はAndroidの実機ビルドで失敗した際のワーニングを抜粋。

ワーニング
Error: /Users/yokota/Documents/study/Ionic/scribbling/platforms/android/gradlew: Command failed with exit code 1 Error output:
FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':packageDebug'.
> java.io.FileNotFoundException: /Users/yokota/Documents/study/Ionic/scribbling/platforms/android/build/intermediates/assets/debug/www/assets/image (Is a directory)

では、格納すべきディレクトリがどこかというと (プロジェクト名)/www/images/ 直下に格納する。
ただ、このディレクトリは .gitignore でGitリポジトリに含めないようになっているので、必要に応じてGitリポジトリに含めるように修正する必要がある。

.gitignore
www/*$
!www/images/$

前置きが長くなったが、canvas.htmlcanvas.scss は下記のようになった。

canvas.html
・・・

<ion-content>
  <ion-row [ngClass]="{'drawing-active': isDrawing}">
    <ion-col>
      <div id="wrapper">
        <signature-pad id="signature-pad" [options]="signaturePadOptions" (onBeginEvent)="drawStart()" (onEndEvent)="drawComplete()"></signature-pad>
      </div>
    </ion-col>
  </ion-row>
</ion-content>

<ion-footer>
  <ion-toolbar class="canvas-toolbar">
    <button class="button" id="images" on-tap="selectPicture()">
      <ion-icon class="icons" name="images"></ion-icon>
    </button>
    <button class="button" id="redo" on-tap="clearScribble()">
      <ion-icon class="icons" name="redo"></ion-icon>
    </button>
    <button class="button" id="save" on-tap="saveScribble()">
      <img class="icons-photo" src="images/save-icon.png"/>
    </button>
  </ion-toolbar>
</ion-footer>
canvas.scss
page-canvas {
  #wrapper {
    border: solid 1px #000;
    width: 400px;
    height: 300px;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    margin: auto;
    position: absolute;
  }
  .canvas-toolbar {
    margin: 0;
    position: relative;
  }
  .button {
    margin-left: 5px;
    border-radius: 0.5em;
    border-radius: 0.5em;
  }
  #images {
    background: #333;
  }
  #redo {
    background: #333;
  }
  #save {
    background: #333;
    width: 44px;
    height: 44px;
    padding: 8px;
  }
  .icons {
    margin: 5px;
    color: #FFF;
    font-size: 30px;
  }
}

これで、写真にお絵描きできるようになった。
この記事の目的は達成!

もう一歩踏み込んだ機能

お絵描き機能がほんとに線を描くだけなので、SignaturePadの機能を使ってできるもう一歩踏み込んだ機能を設けるなら下記かな。

  • 線の色を選択することができる
    • signaturePad.set('penColor': '#666a73');
  • 線の太さを選択することができる
    • signaturePad.set('minWidth': '10');

機能自体の追加よりUIを作り込むほうが難しそう。
興味のある方はどうぞ。

ソースコード

https://github.com/macneko-ayu/Ionic2-sample-scribbling

Ionic2でアプリを作ってみた感想

良いと思ったこと

意外といけるじゃんっていうのが本音。
やる前は「WebViewで動くアプリなんてできることもたかがしれてるんでしょ?」って思ってたけど、できることの多さにびっくりした。
ネイティブの機能をゴリゴリ使って、端末あわせこんで、みたいなのはちょっと苦手かもしれないけど、簡単なUIで少機能なアプリならIonic2で全然いけると思う。

まったく同じコードでiOS、Androidで動くというのもいいね。
学習コストをのぞけば、一つのアプリを作る時間で、2つのOSに対応してものができる。
これはとても良いことだ。

チュートリアル記事もいろいろあるのも良かったな。
公式サイトもいろいろ整備されていて、iconを公式が提供しているのも良かった。
使い道ないだろうというものもたくさんあるので一度見てみることをおすすめ。
ionicons

公式のフォーラムも活発だった。

つらいなと思ったこと

AngularJS、TypeScript、Ionicと覚えることが多い。
自分で選んでおきながら…と思われるかもしれないけど、ここまで情報が錯綜しているとは思わなかった。
これはしかたないことではあるんだけど、ネットの情報ではIonic1/AngularJS1の情報と、Ionic2/AngularJS2の情報が混在していて、精査するのがつらかった。
検索する際は1年以内という絞込が必須条件。
あとWeb界隈で使われている技術用語がもりもり出てくる。
そしてそれを調べると、「その前にAという根底技術があってだな」となって、つらい。
Web界隈の人、追いつくの、つらくないですか?と尊敬の念を抱くレベル。

Nexus7(4.4.2)でアプリの動きがちょっと遅い。
描画処理が重いのか、線を書いていると描画が追いつかないことがあった。
これはチューニングでどうにかできるのかなぁ。

まとめ

感想にも書いたけど、思ったより全然良いものだった。
実際に触らずに妄想で否定するのはよくないなと自戒。
調べないといけない技術が多くてつらいと書いたものの、だからこその楽しみもあったし。
食わず嫌いの人も一度触ってみることをおすすめします。