顯示具有 Ionic Framework 標籤的文章。 顯示所有文章
顯示具有 Ionic Framework 標籤的文章。 顯示所有文章

2015年11月25日

Ionic Framework 教學 - 11. App 上架

Android 和 iOS 分開上架,上架前要先修改參數,以 Android 為例,我們先移除 console log (注意如果你之後要繼續開發的話要把這個 plugin 加回來)。
cordova plugin rm cordova-plugin-console

然後可以修改 mysidemenu/platforms/android/AndroidManifest.xml 裡面的 android:versionName="0.0.1"
之後跑 release build 然後產生 my-release-key.keystore 來簽署我們的 apk ,再用 zipalign 打包好,就可以到 Google Play 上架了
cordova build --release android
keytool
cd platforms/android/build/outputs/apk/
keytool -genkey -v -keystore my-release-key.keystore -alias alias_name -keyalg RSA -keysize 2048 -validity 10000
ls
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-release-key.keystore android-release-unsigned.apk alias_name
~/android/android-sdk-macosx/build-tools/22.0.1/zipalign -v 4 android-release-unsigned.apk myapp.apk

上架填寫表單的時候要注意不要使用網路上抓來的圖片。











Ionic Framework 教學 - 10. 使用 Cordova Plugin




承上一章

我們來試著製作一個會用到相機的 App 來練習 Plugin 的使用,相機的功能是藉由 Cordova Plugin 來連接的。


首先 Shell 指令進入我們的 project,然後增加手機目標平台系統,ios 或是 android
cd mysidemenu
ionic platform add android
ionic platform add ios

然後安裝 Camera Plugin 和 Canvas2Image Plugin
npm update -g cordova
cordova platform update android@5.0.0
cordova plugin add cordova-plugin-camera
cordova plugin add https://github.com/devgeeks/Canvas2ImagePlugin.git
cordova plugin add cordova-plugin-whitelist

在 www/templates/menu.html 增加一個選單
<ion-item menu-close href="#/app/photo">
  Wanted
</ion-item>

然後增加 www/templates/photo.html
<ion-view>
  
  <!-- add a camera button to take a photo -->
  <ion-nav-buttons side="secondary">
    <button
      ng-click="capturePhoto()"
      class="button button-icon icon icon-small ion-camera">
    </button>
  </ion-nav-buttons>

  <ion-content>

    <div
      style="
        width:320px;
        height:453px;
        overflow: hidden;
        margin: 0 auto;
      ">
      <div
        style="
          width:320px;
          height: 1000px;
          background-image:url('{{photo.url}}');
          background-repeat: no-repeat;
          background-size: contain;
        ">
        <img ng-src="{{framePhoto}}"
          style="
            width:320px;
          ">
      </div>
    </div>
    <button class="button button-full" ng-click="save()">Save</button>
  </ion-content>

</ion-view>

這裡可以看到我們使用一個 background image ,圖片位置是一個變數 photo.url ,然後前面放一個圖片 img ,還有兩個按鈕對影兩個函式, capturePhoto 用來拍照, save() 用來儲存照片。
接下來我們定義一個 Controller 來處理這些邏輯。
.controller('PhotoCtrl', function($scope) {
  
  $scope.framePhoto = 'http://i.imgur.com/5WkxqFC.png?1';
  //photo to show in view page
  $scope.photo = {};
  
  //options to take a photo
  var photoOptions = {};
  if( typeof Camera !== 'undefined'){

    //https://github.com/apache/cordova-plugin-camera
    photoOptions = {
      quality: 100,
      destinationType: Camera.DestinationType.FILE_URL,
      sourceType: Camera.PictureSourceType.CAMERA, //[PHOTOLIBRARY, CAMERA, SAVEDPHOTOALBUM]
      encodingType: 1, //jpeg:0 png:1
      targetWidth: 320,
      targetHeight: 453
    };

  }

  /**
   * Capture a photo, when user click the camera icon
   */
  $scope.capturePhoto = function() {
    if(navigator.camera){
      navigator.camera.getPicture(
        onGetPictureSuccess, onGetPictureError, photoOptions);
    }
  };

  /**
   * Called when the photo is taken
   * @param  {string} result photo local url
   */
  function onGetPictureSuccess (result) {
    console.log('onGetPictureSuccess', result);
    $scope.photo.url = result;
    $scope.$apply();
  }

  /**
   * Called when the capture photo runs into an error
   * @param  {object} err [description]
   */
  function onGetPictureError (err) {
    console.error('error', err);
  }



  /**
   * Load an image from a file path, call 'onload' function when loaded.
   * @param  {string} src    image file path
   * @param  {function} onload callback function
   * @return {image}        
   */
  function loadImage(src, onload) {
    console.log('loadImage');
    var img = new Image();

    img.onload = onload;
    img.src = src;
    return img;
  }

  
  /**
   * Combines two images in a canvas, use the size of image 1.
   * @param  {string}   src1     file path of image 1
   * @param  {string}   src2     file path of image 2
   * @param  {Function} callback function when 2 images combined
   */
  function combineImages (src1, src2, callback) {
    console.log('combineImages', [src1, src2]);
    var canvas = document.createElement('canvas');
    var ctx = canvas.getContext('2d'), dataURL;
    var img1 = loadImage(src1, onload);
    var img2 = loadImage(src2, onload);
    var i = 0, base64Photo;
    function onload() {
      i += 1;
      if(i == 2){ //when 2 images are both loaded
        i = 0;
        canvas.width = img1.width;
        canvas.height = img1.height;
        //draw img2 according to img1's ratio
        ctx.drawImage(img2, 0, 0, img2.width, img2.height, 0, 0, img1.width, img1.width*img2.height/img2.width);
        ctx.drawImage(img1, 0, 0);
        callback(canvas);
      }
    }
  }

  /**
   * save image from canvas to gallery
   * @param  {canvas} canvas
   */
  function saveImageFromCanvas(canvas){
    window.canvas2ImagePlugin.saveImageDataToLibrary(
      function(msg){
          console.log('canvas2ImagePlugin', msg);
      },
      function(err){
          console.log('canvas2ImagePlugin', err);
      },
      canvas
    );
  }

  $scope.save = function() {
    //combine 2 images 
    combineImages($scope.framePhoto, $scope.photo.url, saveImageFromCanvas);
  };

})

理解程式內容請一邊閱讀程式註解。 最後我們在 www/js/app.js 增加一組 routes
.state('app.photo', {
  url: '/photo',
  views: {
    'menuContent': {
      templateUrl: 'templates/photo.html',
      controller: 'PhotoCtrl'
    }
  }
})

試著執行這個頁面,發現不能執行,因為電腦上面沒有這些 Plugin 對應的手機程式,我們要到手機上去測試執行。
有別於之前上傳的測試方式,這次我們用到 Plugin 裡面因為包含原生的程式,我們必須要直接編譯這個 App。
請到 App Store 安裝 Xcode 或是到 Android 官網下載 Android SDK,然後安裝好並且把 Android SDK 環境變數加到系統內。
接著到命令列 Shell 編譯你需要的對應的手機系統。
ionic build ios
ionic build android

編譯沒有問題之後,如果你是使用 Android 系統,你可以接上你的手機測試,請打開你手機的 developer mode ,ios 的使用者可以跑 simulator。
ionic run ios
ionic run android

遺憾的是,ios simulator 只能檢查你編譯成功,沒有支援相機功能。
iOS 的話我們進到 Xcode 打開 mysidemenu project , File > Open... > mysidemenu/platforms/ios/mysidemenu.xcodeproj
連接手機平板裝置後開始編譯。











2015年11月19日

Ionic Framework 教學 - 9. 簡易聊天室設定

承第八章

我們來做一個設定頁面,讓我們在聊天室講話的時候可以有一個暱稱。

請大家建立一個設定頁面 www/templates/settings.html

<ion-view view-title="設定">
  <ion-content>
    <ion-list>

      <ion-item class="item item-input-inset">
        <label class="item-input-wrapper">
          <input type="text" placeholder="Nickname" ng-model="currentUser.displayName">
        </label>
        <button class="button button-small" ng-click="saveName()">
          Save
        </button>
      </ion-item>

      <label class="item item-input item-select">
        <div class="input-label">
          Color
        </div>
        <select class="" 
          ng-model="settings.color" 
          ng-options="c.value as c.name for c in colors" 
          ng-change="showColor()">
      </label>

    </ion-list>
  </ion-content>
</ion-view>

這裡使用了 currentUser.displayName 來當做顯示名稱,saveName()函式來儲存名稱,使用 select 來選擇顏色,當顏色被選擇之後會觸發 ng-change ,就可以用 showColor()來看看顏色變數被改變了嗎。
然後我們來定義這些功能,請增加一個 SettingsCtrl

.controller('SettingsCtrl', function($scope, $rootScope) {

  //gloabl variable, local storage
  $scope.currentUser = JSON.parse(window.localStorage.currentUser) || {};

  //global variable, temporary
  $rootScope.settings = $rootScope.settings || {};

  //https://en.wikipedia.org/wiki/Web_colors
  $scope.colors = [
    {
      name: 'Navy',
      value: '#000080',
    },
    {
      name: 'Crimson',
      value: '#DC143C'
    },
    {
      name: 'DimGray',
      value: '#696969'
    }
  ];

  $scope.showColor = function() {
    console.log($rootScope.settings);
  };

  $scope.saveName = function() {
    alert($scope.currentUser.displayName + ' saved!');
    window.localStorage.currentUser = JSON.stringify($scope.currentUser);
  };

})

這裡我們有兩種全域變數的寫法,一種是存在 local storage 一種是存在 $rootScope 裡面,等下我們來看看這兩種用法的差別。
注意這裡,在 window.localStorage 中必須要用字串的形式儲存,所以我們用 JSON.stringify 來把物件轉換成字串。讀取的時候則用 JSON.parse 來把字串轉換成物件。
然後我們在 www/js/app.js 增加一個 route state 來處理 settings

.state('app.settings', {
  url: '/settings',
  views: {
    'menuContent': {
      templateUrl: 'templates/settings.html',
      controller: 'SettingsCtrl'
    }
  }
})
然後我們在 www/templates/messages.html 增加一個按鈕可以連到設定頁面,把這個按鈕增加到 ion-view 的裡面,ion-content的上面。

  <ion-nav-buttons side="secondary">
    <button ui-sref="app.settings"
      class="button button-icon icon ion-gear-a">
    </button>
  </ion-nav-buttons>

這是一個 header button , secondary 代表他在次要位置。
然後我們修改 www/templates/messages.html 的內容,增加了顯示名稱和顏色,完成後大概成為這樣。
<ion-view view-title="{{chatroom.name}}">
  <ion-nav-buttons side="secondary">
    <button ui-sref="app.settings"
      class="button button-icon icon ion-gear-a">
    </button>
  </ion-nav-buttons>
  <ion-content>
    <ion-list>
      <ion-item class="item-input-inset">
        <label class="item-input-wrapper">
          <input type="text" placeholder="New Message" ng-model="newMessage.content">
        </label>
        <button class="button button-small" ng-click="sendMessage()">
          Send
        </button>
      </ion-item>
      <ion-item class="item-text-wrap" ng-repeat="message in messages.slice().reverse()">
        <strong class="" ng-show="message.displayName">{{message.displayName}}:</strong>
        <span style="color:{{message.color || '#000'}}">
          {{message.content}}
        </span>
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

然後我們回頭修改 MessagesCtrl 來讓送出的 message 夾帶 displayName 和 color 的資訊。
.controller('MessagesCtrl', function($scope, $state, $rootScope, ChatroomService, MessageService) {

  console.log('Chatroom Id: ', $state.params.roomId);

  //get current chatroom from roomId
  $scope.chatroom = ChatroomService.get($state.params.roomId) || {};
  console.log('This is chatroom: ', $scope.chatroom.name);

  //get messages in this chatroom
  $scope.messages = MessageService.all($state.params.roomId);

  //global variable, localStorage
  var currentUser = JSON.parse(window.localStorage.currentUser) || {};
  $rootScope.settings = $rootScope.settings || {};
  console.log($rootScope.settings);

  $scope.newMessage = {};

  $scope.sendMessage = function () {
    $scope.newMessage.color = $rootScope.settings.color || '#000';
    $scope.newMessage.displayName = currentUser.displayName;
    console.log('New message: ', $scope.newMessage);

    //send message
    MessageService.send($scope.newMessage).then(function() {
      $scope.newMessage.content = '';
    });
  };

})

如果你重新載入程式,你會發現 color 的設定不見了,但是你的暱稱還保留著。

藉由這個練習,我們可以知道 localStorage 和 $rootScope 的身為全域變數的方便強大和危險,強大是在任何地方修改這個變數都有效,危險也是因為你無法確定這個變數會被什麼函式在任何什麼地方修改了,造成不是你預期的效果。

練習題:使用者設定顏色之後,要去聊天室打字才能知道是什麼顏色,這不是一個好的使用者體驗,如何在選擇完顏色的時候讓使用者知道是什麼顏色呢?






Ionic Framework 教學 - 8. 簡易聊天室

承第六章和第七章

設定好 Firebase 後,我們可以來做簡易聊天室,了解資料怎麼和後台連結,為了方便教學,這裡的聊天室我們只做匿名聊天室。
首先我們要做一個聊天室列表,我們增加一個 ChatroomsCtrl Controller,注意這裡 Chatrooms 是複數。

.controller('ChatroomsCtrl', function($scope) {

  $scope.chatrooms = [];
  
  $scope.newChatroom = {};

  $scope.createChatroom = function () {
    console.log($scope.newChatroom);
  };
})

記得 $scope 代表是 View 要使用的變數,這裡我們定義了 chatrooms 陣列, newChatroom 物件和 createChatroom 函式。
console.log 代表在瀏覽器 console 把變數內容印出來看看的意思。
然後建立 www/templates/chatrooms.html
<ion-view view-title="Chatrooms">
  <ion-content>
    <ion-list>
      <ion-item class="item-input-inset">
        <label class="item-input-wrapper">
          <input type="text" placeholder="New Chatroom Name" ng-model="newChatroom.name">
        </label>
        <button class="button button-small" ng-click="createChatroom()">
          Create
        </button>
      </ion-item>
      <ion-item ng-repeat="chatroom in chatrooms" href="#/app/chatrooms/{{chatroom.$id}}">
        {{chatroom.name}}
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>
其中 ng-model 代表使用變數,ng-repeat 代表使用陣列中的變數。
然後在 www/js/app.js 增加一個 route
  .state('app.chatrooms', {
    url: '/chatrooms',
    views: {
      'menuContent': {
        templateUrl: 'templates/chatrooms.html',
        controller: 'ChatroomsCtrl'
      }
    }
  })

我們順便把 www/js/app.js 最下面的 $urlRouterProvider.otherwise('/app/playlists'); 改掉,這是預設的顯示頁面,如果連到任何未定義的 url 就會改連到這裡,我們改成 /app/chatrooms。
$urlRouterProvider.otherwise('/app/chatrooms');

最後在 www/templates/menu.html 增加一個 Chatrooms 選項

<ion-item menu-close href="#/app/chatrooms">
  Chatrooms
</ion-item>

現在可以按按看按鈕,理解一下程式內容。
然後我們增加一個 www/js/services.js
var firebaseUrl = "https://your-project-id.firebaseio.com/";
angular.module('starter.services', [])
//Firebase datastore reference
.factory("FirebaseRef", function($firebaseAuth) {
  return new Firebase(firebaseUrl);
})
//for log in and sign up
.factory("Auth", function($firebaseAuth) {
  var usersRef = new Firebase(firebaseUrl+"users");
  return $firebaseAuth(usersRef);
})
.factory("ChatroomService", function($firebaseArray) {
  var roomsRef = new Firebase(firebaseUrl+"chatrooms");
  var rooms = $firebaseArray(roomsRef);
  return {
    all: function() {
      console.log(rooms);
      return rooms;
    },
    get: function(roomId) {
      return rooms.$getRecord(roomId);
    },
    create: function(chatroom) {
      console.log('Adding chatroom: ', chatroom.name);
      return rooms.$add(chatroom);
    }
  }
});

其中 your-project-id 請填入你在 Firebase 申請的 Project ID ,這裡我們在做一個和 Firebase 溝通的 Chatroom service 。
這裡我們定義了 ChatroomService 服務,他主要有三個功能 all 來取得所有的聊天室, get 來取得特定 ID 的聊天室, create 來創造一個聊天室。
我們在第六章有說到,在 www/index.html 中,需要引用 services.js 來讓這個服務可以載入到頁面中。
...
<script src="js/app.js"></script>
<script src="js/services.js"></script>
...

在第六章也有提到,在 app.js 最上面要加上 starter app 對 'firebase' 和 'starter.services' 的依賴關係,這樣 angular 才知道要引用這些程式和服務。

angular.module('starter', ['ionic', 'firebase', 'starter.services', 'starter.controllers'])

然後讓我們的 ChatroomsCtrl 來使用這個 service 。


.controller('ChatroomsCtrl', function($scope, ChatroomService) {

  $scope.chatrooms = ChatroomService.all();
  
  $scope.newChatroom = {};

  $scope.createChatroom = function () {
    console.log($scope.newChatroom);
    ChatroomService.create($scope.newChatroom);
  };
})

這樣就可以順利增加 Chatroom 了,如果你到 Firebase project console 也可以看到對應的資料被增加上去了,比如說我增加了三個聊天室,下面圖片亂數的部分就是 Firebase 給這三個聊天室的特殊 ID。











目前Chatroom bar 點下去是沒有用的,因為對應的路徑 #/app/chatrooms/{{chatroom.$id}} 還沒有定義。

現在我們來製作可以發話的聊天室。
首先我們增加這個聊天室訊息的 Service 。
在 www/js/services.js 下面增加 MessageService 。

.factory("MessageService", function($firebaseArray) {
  var messagesRef = new Firebase(firebaseUrl+"messages");
  var messages;
  return {
    all: function(roomId) {
      console.log('get messages from: ', roomId);
      var roomRef = new Firebase(firebaseUrl + "chatrooms/" + roomId + "/messages");
      messages = $firebaseArray(roomRef);
      return messages;
    },
    send: function(message) {
      console.log('Adding message: ', message);
      return messages.$add(message);
    }
  };
})

這裡我們定義了 MessageService 有兩個功能, all 是根據某個 chatroom 的 ID 找出所有的對話訊息的 messages ,send 是用來發送訊息,就是把新的訊息加入 Firebase 中,這個 chatroom 的 messages 陣列。

然後我們就可以在 Controller 中使用這個 Services 了,我們建立一個 MessagesCtrl

.controller('MessagesCtrl', function($scope, $state, ChatroomService, MessageService) {

  console.log('Chatroom Id: ', $state.params.roomId);

  //get current chatroom from roomId
  $scope.chatroom = ChatroomService.get($state.params.roomId) || {};
  console.log('This is chatroom: ', $scope.chatroom.name);

  //get messages in this chatroom
  $scope.messages = MessageService.all($state.params.roomId);

  $scope.newMessage = {};

  $scope.sendMessage = function () {
    console.log('New message: ', $scope.newMessage);

    //send message
    MessageService.send($scope.newMessage);
  };

})

然後我們來建立這個聊天室的訊息頁面,建立 www/templates/messages.html

<ion-view view-title="{{chatroom.name}}">
  <ion-content>
    <ion-list>
      <ion-item class="item-input-inset">
        <label class="item-input-wrapper">
          <input type="text" placeholder="New Message" ng-model="newMessage.content">
        </label>
        <button class="button button-small" ng-click="sendMessage()">
          Send
        </button>
      </ion-item>
      <ion-item class="item-text-wrap" ng-repeat="message in messages.slice().reverse()">
        {{message.content}}
      </ion-item>
    </ion-list>
  </ion-content>
</ion-view>

這裡可以看到 chatroom 代表當前這個聊天室,newMessage 代表 input 輸入框中新增的訊息, sendMessage 代表按鈕按下去後送出訊息,ng-repeat="message in messages" 代表列出所有在 messages 中的訊息。
然後我們來增加一個 route ,修改 www/js/app.js
.state('app.messages', {
  url: '/chatrooms/:roomId',
  views: {
    'menuContent': {
      templateUrl: 'templates/messages.html',
      controller: 'MessagesCtrl'
    }
  }
})
因為我們可以有很多個聊天室,每個聊天室的 url 不同,所以注意這裡的 :roomId ,代表這個 url 願意接受變數 roomId,並且把變數 roomId 傳給 MessageCtrl Controller 使用,請對照 chatrooms.html 的 href="#/app/chatrooms/{{chatroom.$id}}" 觀察,我們把 url 中的 chatroom.$id 對應到 state route 的 roomId , 使用者進到訊息頁面的時候 route 把 roomId 接受到的值給了新的 controller 中的 $state.params.roomId,我們是用這個介面來達成動態頁面的變數傳遞。

這裡編輯好之後就可以告一段落了。

發送訊息之後,各位也可以到 Firebase console 去看你的資料結構。












下一章,簡易聊天室設定



Ionic Framework 教學 - 7. 簡易登入和登出

繼續上一章的教學,我們來做 Firebase 的使用者登入。
這篇也會提到 LocalStorage 的基本用法。
首先我們建立一個使用者資訊頁面 www/templates/profile.html

<ion-view view-title="使用者資訊">
  <ion-content>
    <div class="list">
      <div class="item">
        Name: {{currentUser.displayName}}
      </div>
    </div>
  </ion-content>
</ion-view>

定義一個 Controller ProfileCtrl

...
.controller('ProfileCtrl', function() {
})
...

然後加入 app.js route
...
.state('app.profile', {
  url: '/profile',
  views: {
    'menuContent': {
      templateUrl: 'templates/profile.html',
      controller: 'ProfileCtrl'
    }
  }
})
...

修改 menu.html 增加 Profile 項目

<ion-item menu-close href="#/app/profile">
  Profile
</ion-item>

連到 http://localhost:8100/#/app/profile 會發現這個頁面目前沒有正確的內容,因為 currentUser 還沒有定義。
現在我們來實作登入功能。
第一步我們直接修改 www/templates/login.html

<ion-modal-view>
  <ion-header-bar>
    <h1 class="title">登入</h1>
    <div class="buttons">
      <button class="button button-clear" ng-click="closeLogin()">關閉</button>
    </div>
  </ion-header-bar>
  <ion-content>
    <form ng-submit="doLogin()">
      <div class="list">
        <label class="item item-input">
          <span class="input-label">Email</span>
          <input type="text" ng-model="loginData.email">
        </label>
        <label class="item item-input">
          <span class="input-label">Password</span>
          <input type="password" ng-model="loginData.password">
        </label>
        <label class="item">
          <button class="button button-block button-positive" type="submit">登入</button>
        </label>
      </div>
    </form>
  </ion-content>
</ion-modal-view>

注意這裡我們改用了 loginData 方便使用上了解。
然後我們在 controllers.js 定義 loginData 和 doLogin()

...
.controller('AppCtrl', function($scope, $ionicModal, $timeout, $rootScope, $state, $ionicLoading, 
  FirebaseRef, Auth) {

  // With the new view caching in Ionic, Controllers are only called
  // when they are recreated or on app start, instead of every page change.
  // To listen for when this page is active (for example, to refresh data),
  // listen for the $ionicView.enter event:
  //$scope.$on('$ionicView.enter', function(e) {
  //});

  // Form data for the login modal
  $scope.loginData = {};

  // Create the login modal that we will use later
  $ionicModal.fromTemplateUrl('templates/login.html', {
    scope: $scope
  }).then(function(modal) {
    $scope.modal = modal;
  });

  // Triggered in the login modal to close it
  $scope.closeLogin = function() {
    $scope.modal.hide();
  };

  // Open the login modal
  $scope.login = function() {
    $scope.modal.show();
  };

  $scope.loginData = {};

  // Perform the login action when the user submits the login form
  $scope.doLogin = function() {
    console.log($scope.loginData);
    var loginData = $scope.loginData;

    if (loginData && loginData.email && loginData.password) {
      $ionicLoading.show({
        template: '登入中...'
      });
      Auth.$authWithPassword({
        email: loginData.email,
        password: loginData.password
      }).then(function (authData) {
        console.log(authData);
        FirebaseRef.child("users").child(authData.uid).once('value', function (snapshot) {
          var val = snapshot.val();
          console.log(val);
          window.localStorage.currentUser = JSON.stringify(val);
          $scope.$apply(function () {
            $rootScope.currentUser = val;
          });
        }, function(err) {
          console.error(err);
        });
        $ionicLoading.hide();
        $scope.closeLogin();
        $state.go('app.profile');
      }).catch(function (error) {
        alert("登入失敗 " + error.message);
        $ionicLoading.hide();
      });
    } else {
      alert("輸入 Email 和 Password")
    }
  };
})
...

這裡可以看到使用者登入後,我們把使用者名稱儲存到 window.localStorage.currentUser 中,這個 window.localStorage 儲存內容必須要使用字串,所以我們把用 JSON.stringify 把使用者資訊從 object 轉換成字串。
window.localStorage 的內容可能被同樣機器上的其他程序看到和使用,所以不建議儲存使用者的秘密資訊,使用者資訊建議儲存在 server 。

我們修改 app.js 的起始執行內容,把 currentUser 的內容代入到 currentUser ,這樣各個 View 都可以使用了。
...
.run(function($ionicPlatform, $rootScope) {
  $ionicPlatform.ready(function() {
    // Hide the accessory bar by default (remove this to show the accessory bar above the keyboard
    // for form inputs)
    if (window.cordova && window.cordova.plugins.Keyboard) {
      cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true);
      cordova.plugins.Keyboard.disableScroll(true);

    }
    if (window.StatusBar) {
      // org.apache.cordova.statusbar required
      StatusBar.styleDefault();
    }
    
    console.log(JSON.parse(window.localStorage.currentUser));
    $rootScope.currentUser = JSON.parse(window.localStorage.currentUser) || null;

  });
})
...

接下來實作登出功能。
修改 profile.html ,增加登出按鈕。

<ion-view view-title="使用者資訊">
  <ion-content>
    <div class="list">
      <div class="item">
        Name: {{currentUser.displayName}}
      </div>
    </div>
    <button class="button button-full button-positive" ng-click="logout()">登出</button>
  </ion-content>
</ion-view>

然後修改 ProfileCtrl
...
.controller('ProfileCtrl', function($scope, $rootScope, Auth) {
  $scope.logout = function () {
    $rootScope.currentUser = null;
    window.localStorage.currentUser = null;
    Auth.$unauth();
  };
})
...

現在可以試試看登入和登出了。

下一章,簡易聊天室


Ionic Framework 教學 - 6. 簡易註冊

承續上一章的應用,我們想來試做一個簡易聊天室,在做聊天室之前我們要先做使用者登入,對於使用者註冊和登入沒興趣的朋友可以跳至第八章。
要做 instant messaging 的話,我們必須要有一個即時同步訊息的後台,因為我們這個教學主要是做手機端的開發,所以我們就選用一些現有的即時通訊後台服務吧,比如說:

1. Layer: https://layer.com/
2. Quickblox: http://quickblox.com/
3. Firebase: https://www.firebase.com/
...

就決定是你了,Firebase,因為我印象中有 Angular-Fire 這種已經成熟的轉接器,可以讓 Firebase 跟 Ionic 中的 AngularJS 做良好的溝通,比較方便我們這次的教學。

Firebase 提供的登入方式基本上有一種簡是易登入,就是用帳號密碼,和另一種是社群登入,像是用 Facebook 的方式來登入, 由於 Firebase 官網已經有一篇 Ionic + Facebook 登入的教學,所以這個部分就留給同學朋友們自己練習,我們來實作一個簡易的帳號密碼登入。

首先,我們要安裝 bower ,這是目前 Ionic 還在用的 web package management tool 。

$ npm install -g bower

然後安裝 firebase & angular-fire

$ bower install firebase angularfire

我們要在 www/index.html 中引用 app.js 之前的地方引入這個兩個 javascript library

...
<!-- Firebase -->
<script src="lib/firebase/firebase.js"></script>

<!-- OR Firebase from CDN -->
<script src="https://cdn.firebase.com/js/client/2.3.0/firebase.js"></script>

<!-- AngularFire -->
<script src="lib/angularfire/dist/angularfire.min.js"></script>

<!-- OR AngularFire from CDN -->
<script src="https://cdn.firebase.com/libs/angularfire/1.1.3/angularfire.min.js"></script>

<!-- your app's js -->
<script src="js/app.js"></script>
...

接下來到 Firebase 申請一個帳號,並且建立一個 Project,下面會用到你的 Project ID。
申請完之後請到進入你的 Project 管理頁面的 Login & Auth ,並且勾選 Enable Email & Password Authentication 。



然後我們新增一個 www/js/services.js

var firebaseUrl = "https://your-project-id.firebaseio.com/";
angular.module('starter.services', [])
.factory("FirebaseRef", function($firebaseAuth) {
  return new Firebase(firebaseUrl);
})
.factory("Auth", function($firebaseAuth) {
  var usersRef = new Firebase(firebaseUrl+"users");
  return $firebaseAuth(usersRef);
});
your-project-id 請使用你申請的 Project ID
這裡我們定義了 FirebaseRef 和 Auth 兩個工廠,他們分別指向 Firebase 的資料庫和使用者工具,等下會用到。
回到 www/index.html 引用 services.js

...
<script src="js/app.js"></script>
<script src="js/services.js"></script>
...

然後修改 app.js 中 starter app 的依賴關係,增加 firebase 和 starter.services

angular.module('starter', ['ionic', 'firebase', 'starter.services', 'starter.controllers'])

接下來我們做一個註冊頁面 www/templates/signup.html

<ion-view view-title="註冊">
  <ion-content class="padding">
    <div class="list">
      <label class="item item-input">
        <span class="input-label">Email</span>
        <input ng-model="signupData.email" type="text">
      </label>
      <label class="item item-input">
        <span class="input-label">密碼</span>
        <input ng-model="signupData.password" type="password">
      </label>
      <label class="item item-input">
        <span class="input-label">顯示名稱</span>
        <input ng-model="signupData.displayName" type="text">
      </label>
      <button class="button button-full button-positive" ng-click="createUser()">建立</button>
    </div>
  </ion-content>
</ion-view>

這裡看到我們使用了 signupData 和 createUser ,接下來我們來定義這兩個東西。
我們增加一個 Controller

...
.controller('SignupCtrl', function($scope, $ionicLoading, FirebaseRef, Auth) {

  $scope.signupData = {};

  $scope.createUser = function () {
    var signupData = $scope.signupData;

    if (signupData && signupData.email && signupData.password && signupData.displayName) {
      $ionicLoading.show({
        template: '註冊中...'
      });

      Auth.$createUser({
        email: signupData.email,
        password: signupData.password
      }).then(function (userData) {
        console.log(userData);
        alert("註冊成功!");
        // add user info
        FirebaseRef.child("users").child(userData.uid).set({
          displayName: signupData.displayName
        });
        $ionicLoading.hide();
      }).catch(function (error) {
        alert("Error: " + error);
        $ionicLoading.hide();
      });
    } else {
      alert("請填寫所有資料");
    }
  }
})
...

接著定義路徑,修改 app.js

...
.state('app.signup', {
  url: '/signup',
  views: {
    'menuContent': {
      templateUrl: 'templates/signup.html',
      controller: 'SignupCtrl'
    }
  }
})
...

然後加入 menu.html

...
<ion-item menu-close href="#/app/signup">
  Signup
</ion-item>
...

完成了,然後就可以測試看看註冊功能了,在 Firebase console 也可以看到註冊的使用者資料。










2015年11月18日

Ionic Framework 教學 - 5. 下載到手機上測試

承上一章

1. 在網站上註冊一個帳號 https://apps.ionic.io/signup
2. 在手機上下載 Ionic View App
3. 下指令上傳 project

$ ionic upload 

然後填入剛剛註冊的帳號,密碼在打的時候不會顯示是正常的。

4. 上傳後進入你在手機上下載下來就可以看到了,試試看。


下一章 簡易註冊



Ionic Framework 教學 - 4. 修改樣式

承上一章

我們之前都是用 Ionic 預設的樣式,但是這樣大家做出來的 App 豈不是都長得一樣?
我們要怎麼修改樣式呢?我們來修改剛剛做好的大師兄頁面 baseball.html 。
那個按鈕我們不喜歡,修改一下按鈕顏色和邊框,順便修改裡面的文字,我們把它做好做滿。

首先我們先啟用 sass

$ ionic setup sass

然後修改  baseball.html 增加 button-full 和 homerun 類

...
<button ui-sref="app.surprise"
    class="button button-positive button-full homerun icon-left ion-ios-baseball-outline">
    打擊
</button>
...

我們想要把這個按鈕的文字做的又粗又大
首先編輯 scss/ionic.app.scss

...

// Include all of Ionic
@import "www/lib/ionic/scss/ionic";

.homerun {
  font-size: 30px;
  font-weight: 900;
}

然後我們想要把所有按鈕的邊框加的很厚,是所有的按鈕喔,不是只有畫面中這一個,還包含了以後我們建立的按鈕,所以我們直接修改 Ionic 預設的 css
修改 www/lib/ionic/scss/_variables.scss

...
$button-border-width:             5px !default;
...

lib裡面的東西理論上很少修改,所以沒有自動更新,修改完 lib 裡面的東西我們要手動重新啟動 ionic

# ctrl + c to stop ionic server
$ ionic serve

重新整理頁面後就可以看到修改的東西都實現了。

下一章 手機測試





Ionic Framework 教學 - 3. 製作一個簡單的頁面

http://pic.ctitv.com/

承上一章
我們來做一個林智勝的打擊頁面。
首先在 www/templates/menu.html 頁面增加一個Baseball選項
<ion-side-menus enable-menu-with-back-views="false">
    <ion-side-menu-content>
      <ion-nav-bar class="bar-stable">
        <ion-nav-back-button>
        </ion-nav-back-button>

        <ion-nav-buttons side="left">
          <button class="button button-icon button-clear ion-navicon" menu-toggle="left">
          </button>
        </ion-nav-buttons>
      </ion-nav-bar>
      <ion-nav-view name="menuContent"></ion-nav-view>
    </ion-side-menu-content>

    <ion-side-menu side="left">
      <ion-header-bar class="bar-stable">
        <h1 class="title">Left</h1>
      </ion-header-bar>
      <ion-content>
        <ion-list>
          <ion-item menu-close ng-click="login()">
            Login
          </ion-item>
          <ion-item menu-close href="#/app/search">
            Search
          </ion-item>
          <ion-item menu-close href="#/app/browse">
            Browse
          </ion-item>
          <ion-item menu-close href="#/app/playlists">
            Playlists
          </ion-item>
          <ion-item menu-close href="#/app/baseball">
            Baseball
          </ion-item>
        </ion-list>
      </ion-content>
    </ion-side-menu>
  </ion-side-menus>

這個時候 Baseball 點下去是沒有用的,我們要增加對應的頁面。
首先在 www/templates 資料夾中增加一個 baseball.html ,這個我們等下會用到。
然後編輯 www/js/controllers.js ,增加一個 Controller

...

.controller('PlaylistCtrl', function($scope, $stateParams) {
})
.controller('BaseballCtrl', function() {
})
;

注意 "..." 是代表省略的意思,不是真的程式碼,因為我盡量只寫有變動的部份,有些同學直接照抄是不行的。
分號要放在最後面,這個 BaseballCtrl 是用來處理有關 Baseball 的事件處理的,目前內容是空白,我們等下會用到。
然後編輯 www/js/app.js ,在 routing 路線圖裡面,增加一組 route ,他是告訴 App 遇到什麼狀態 state ,或是什麼 url 的情況,要交給哪個 Controller 來處理,並且指定顯示什麼 template 頁面。

...

.config(function($stateProvider, $urlRouterProvider) {
  $stateProvider

  ...

  .state('app.single', {
    url: '/playlists/:playlistId',
    views: {
      'menuContent': {
        templateUrl: 'templates/playlist.html',
        controller: 'PlaylistCtrl'
      }
    }
  })
  .state('app.baseball', {
    url: '/baseball',
    views: {
      'menuContent': {
        templateUrl: 'templates/baseball.html',
        controller: 'BaseballCtrl'
      }
    }
  })
  ;
  // if none of the above states are matched, use this as the fallback
  $urlRouterProvider.otherwise('/app/playlists');
});

到這裡我們再試試看點擊 Baseball 頁籤,就會出現空白頁面,而且 header 的 title 是錯誤的,我們來修改他。
編輯 www/templates/baseball.html

  
<ion-view view-title="請大師兄翻譯翻譯">
  <ion-content>
    <img alt="" class="full-image" src="http://i.imgur.com/fnU7FHf.jpg?1" />
    <button class="button button-positive icon-left ion-ios-baseball-outline">
      打擊
    </button>
  </ion-content>
</ion-view>

我們修改了 header title, 增加了一張大師兄曼妙的身影,和一個打擊按鈕。
打擊下去會發生什麼事呢?
我們來增加下一個 View
首先增加 www/templates/surprise.html

<ion-view view-title="什麼叫驚喜">
  <ion-content>
    <img class="full-image" src="http://i.imgur.com/JpsC5xC.jpg?1" alt="">
  </ion-content>
</ion-view>

同樣的這個頁面也需要一個 Controller 和 route ,我們分別在 controllers.js 和 app.js 加上

  ...
  
  .controller('SurpriseCtrl', function() {
  })
  ;
  ...

  .state('app.surprise', {
    url: '/surprise',
    views: {
      'menuContent': {
        templateUrl: 'templates/surprise.html',
        controller: 'SurpriseCtrl'
      }
    }
  })
  ;

這樣完成之後,我們要怎麼讓打擊按鈕連到這個頁面呢?
第一種方法,我們剛剛已經知道了,在 menu.html 裡面,用 href 的方式連到對應的 url 。
我們來試試看第二種方式,用 ui-sref 連到對應的 state 。
我們修改 baseball.html

<ion-view view-title="請大師兄翻譯翻譯">
  <ion-content>
    <img class="full-image" src="http://i.imgur.com/fnU7FHf.jpg?1" alt="">
    <button ui-sref="app.surprise"
      class="button button-positive icon-left ion-ios-baseball-outline">
      打擊
    </button>
  </ion-content>
</ion-view>

現在試試看打擊按鈕了。
除了用 ur-sref 我們也可以在 Controller 中控制轉場,我們修改打擊按鈕
<button ng-click="hitTheBall()"
  class="homerun button button-positive button-full icon-left ion-ios-baseball-outline">
  打擊
</button>
讓他被點擊的時候呼叫 hitTheBall ,然後我們修改 Controller
.controller('BaseballCtrl', function($scope, $state) {
  $scope.hitTheBall = function() {
    $state.go('app.surprise');
  }
})

$scope 就是連接 View 中使用的變數,$state 就是用來使用 routing 的
可是這樣有什麼好處呢? 有時候按鈕按下去有很多事情需要處理,處理完才能到下一個 state ,假設我們加一個 loading 的畫面
增加 templates/loading.html

<img class="full-image" src="http://i.imgur.com/AkbN5f0.jpg?1" alt="">

然後我們修改 BaseballCtrl

.controller('BaseballCtrl', function($scope, $state, $ionicLoading, $timeout) {
  $scope.hitTheBall = function() {
    $ionicLoading.show({
      templateUrl: 'templates/loading.html'
    });

    //wait for 2 seconds
    $timeout(function() {
      $state.go('app.surprise');
      $ionicLoading.hide();
    }, 2000);
    
  };
})

這樣你就大概知道一個頁面要怎麼做了,更多 Ionic 預設的元件你可以參考
http://ionicframework.com/docs/components/
現在發揮你的創意,換你來練習一下,比如說古巴教練說了一句話,然後發生了什麼事?
熟悉靜態頁面對於後面的課程很重要!

下一章 樣式修改





Ionic Framework 教學 - 2. 建立一個測試App

承上一章

接著我們來試著建立一個測試  App
  ## create mysidemenu based on sidemenu project

  $ ionic start mysidemenu sidemenu

  ## go to mysidemenu folder

  $ cd mysidemenu

  ## list files

  $ ls  # on mac
  $ dir  # on windows

  ## open mysidemenu folder, take a look at the folder structure

  $ open .  # on mac
  $ explorer .  # on windows

  ## start running my app
  ## run this command in mysidemenu folder

  $ ionic serve

  ## (and choose 'localhost')

  ## Then, navigate your browser to http://localhost:8100
  ## At this moment, you may want to install Chrome if you don't have one.
  ## https://www.google.com/chrome/browser/desktop/index.html


這樣,我們就從 sidemenu 這個樣板建立了一個 mysidemenu 的 App,接著你就可以打開瀏覽器來測試看看,在 Chrome 裡面,你可以使用 Command + Option + i 來開啟開發人員工具(Windows 則使用 F12),在 Network 頁籤你可以看到載入訊息,在 Console 頁籤裡你可以看到錯誤訊息(如果有的話),並且你可以用 Command + Shift + m 來模擬手機螢幕,在這裡你可能會需要重新整理頁面來套用適當的版面。

如果你還沒有一個很好的程式編輯器,我們可以推薦幾個適合入門又強大的編輯器給你

1. Sublime Text
http://www.sublimetext.com/2
2. Atom
https://atom.io/
3. Visual Code
https://www.visualstudio.com/zh-tw/products/code-vs.aspx

前往下一章- 製作一個靜態頁面



2015年11月15日

Ionic Framework 製作 Hybrid App - 1.準備環境

blog.ionic.io

因為一些大學課程需要的關係製作了這一系列的教學,在你深入的看這個教學之前,我們希望你可以先上網了解 Native App, Web App, Hybrid App 等等開發方法的優點和缺點,選擇最適合你的 App 開發方式。

你至少需要具備一些基礎的  HTML 和 Javascript 的知識來跟著我們實作這一系列的教學。


準備環境


我們需要 Node.js, npm, Cordova, and Ionic

什麼是 Node.js 和 npm?


Node.js® is a JavaScript runtime built on Chrome's V8 JavaScript engine. Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient. Node.js' package ecosystem, npm, is the largest ecosystem of open source libraries in the world.

more: https://nodejs.org/en/

什麼是 Cordova?


Mobile apps with HTML, CSS & JS. Target multiple platforms with one code base. Free and open source.

more: https://cordova.apache.org/

https://blog.codecentric.de/en/2014/11/ionic-angularjs-framework-on-the-rise/


什麼是 Ionic Framework?

Create mobile apps with the web technologies you love. Free and open source, Ionic offers a library of mobile-optimized HTML, CSS and JS components, gestures, and tools for building highly interactive apps. Built with Sass and optimized for AngularJS.

more: http://ionicframework.com/


前往第二章 建立一個測試 App