2015年4月23日 星期四

Hybrid Apps開發系列之4.4:多頁面App程式 (天氣app-非線上版)

前文(Hybrid Apps開發系列之4.1:多頁面App製作與頁籤(Tabs)簡介)介紹的範例有許多codes重複出現,例如頁籤<ion-tabs>便同時出現在兩個.html檔內:

    <ion-tabs class="tabs-icon-top">
        <ion-tab href="/home" icon="ion-home" title="首頁"></ion-tab>
        <ion-tab href="/order" icon="ion-compose" title="訂位"></ion-tab>
    </ion-tabs>
此外,<ion-nav-bar>等指令也同樣重複出現。為了讓程式碼共用,必須採用「共用states」的方式。

共用states

UI-Router可定義abstract state-也就是操作過程中不能直接瀏覽的頁面-讓多個states共用;而abstract state是parent state;其他共用abstract state的頁面則為child states。因此,4.1的頁籤範例修改的部分如下圖所示。

圖1.頁籤頁面的內容定義為abstract state(如tab state),便可讓其他state共用。
  1. 加入新的abstract state,該state和一般state不同,必須加入屬性"abstract : true"例如:
    ...[略]...
    .state('tab',{
        url: '/tab',
        abstract: true,
        templateUrl: 'templates/tabs.html'
     })
    ...[略]...
    
  2. 修改其他要共用abstract state的頁面定義,加入views屬性,並給予名稱:
    ...[略]...
    .state('tab.home',{
        url: '/home',
        views: {
             'tab-home': {
                  templateUrl: 'templates/home.html',
                  controller: 'homeCtrl'
             }
        }       
     })
    ...[略]...
    
    和原先state定義不同除了名稱(由home改為tab.home,tab為abstract state名稱)之外,還加入了views屬性,並給予名稱(例如tab-home),將頁面檔案、controller的設定放在其中。views的名稱相當重要,會出現在parent state的頁面檔的<ion-nav-view>之name屬性,以便將此名稱所代表的子頁面,帶入parent state中。
  3. 將頁籤共用的部份獨立出來另存新檔,注意每個<ion-tab></ion-tab>內都要加入<ion-nav-view>指令,並設定其name屬性為對應子頁面views屬性所定義的名稱,例如:
    <ion-tabs>
      <ion-tab href="‘/tab/home’">
        <ion-nav-view name="‘tab-home’">
        </ion-nav-view>
      </ion-tab>
      <ion-tab href="‘/tab/order’">
         <ion-nav-view name="‘tab-order’">
         </ion-nav-view>
      </ion-tab>
    </ion-tabs>
    
    此處'tab-home', 'tab-order'便分別定義於兩個state的views屬性內。而第3行第6行href的網址須加上parent state的url,例如/home變為 /tabs/home。此乃因為各頁面的url定義雖然不變,但state位階變為abstract state的children,故各頁面完整的url必須加上parent state的url。
  4. 其他如<ion-nav-bar>指令可直接移至index.html,做為app本身layout的一部分。例如:
    <ion-nav-bar>
    <ion-nav-back-button>
    </ion-nav-back-button>
    </ion-nav-bar>
    <ion-nav-view>
    </ion-nav-view>
    

天氣app(非線上版)

目前網路有許多開放資料,只要透過http連線便可取得JSON格式的資料。JSON是JavaScript object格式,故直接可用於ionic專案內。此處便以open weather api的天氣資料為例,先將回傳的JSON資料貼在專案內使用,製作一頁籤式的天氣app。下一篇文章再運用線上取得JSON資料重製同一專案。取得資料的方式可參考:http://openweathermap.org/current;而回傳JSON資料格式則參考:http://openweathermap.org/weather-data#current
圖2. 天氣App介面
點選上圖頁面項目,便會顯示該城市的天氣概況,此部份便是運用前文Hybrid Apps開發系列之4.3:多頁面App程式 (清單/細節瀏覽)介紹的方法。左邊兩張圖則是兩個頁籤。整個專案結構如下圖:
圖3.專案結構

檔案內容

app.js關於config設定的部份如下:
angular.module('starter', ['ionic', 'starter.controllers'])
        .config(function ($stateProvider, $urlRouterProvider, $ionicConfigProvider) {
            $ionicConfigProvider.tabs.position('bottom');
            $urlRouterProvider.otherwise('/tab/home');
            $stateProvider
                    .state('tab', {
                        url: '/tab',
                        abstract: true,
                        templateUrl: 'templates/tabs.html'
                    })
                    .state('tab.home', {
                        url: '/home',
                        views: {
                            'tab-home': {
                                templateUrl: 'templates/home.html',
                                controller: 'homeCtrl'
                            }
                        }
                    })
                    .state('tab.settings', {
                        url: '/settings',
                        views: {
                            'tab-settings': {
                                templateUrl: 'templates/settings.html',
                                controller: 'settingsCtrl'
                            }
                        }
                    })
                    .state('weather', {
                        url: '/tab/home/:id',
                        templateUrl: 'templates/weather.html',
                        controller: 'weatherCtrl'
                    });
        })

  • 第2-3行:為了讓頁籤在不同行動平台上都出現在頁面下方,使用$ionicConfigProvider服務,以.tab.position('bottom')設定頁籤固定於下方(Android預設位置在頁面上方)。
  • 第6-9行:共用的parent state定義,注意第8行"abstract:true"為必要屬性設定。
  • 第11-19行:圖2左邊頁面的定義。注意第14行views名稱設為'tab-home',此名稱要用於第9行tabs.html內。
  • 第20-28行:圖2中間頁面的定義。第21行views名稱設為'tab-settings',此名稱必須用於第9行parent state的html檔內。
  • 第30行:最後一個'weather' state設定與前幾個均不相同,因為weather並未與其他頁面共用'tab' state。此外,由於weather的用途是顯示某城市的天氣概況,城市序號會由'tab.home' state傳來,因此url必須設定為'/tab/home/:id',而非'/home/:id',因為tab.home的完整url須結合parent state的url,而成為'tab/home'。

templates/tabs.html檔案內容如下:
<ion-tabs class="tabs-icon-top">
    <ion-tab title="天氣" icon="ion-home" href="#/tab/home">
        <ion-nav-view name="tab-home"></ion-nav-view>
    </ion-tab>        
    <ion-tab title="設定" icon="ion-settings" href="#/tab/settings">
        <ion-nav-view name="tab-settings"></ion-nav-view>
    </ion-tab>
</ion-tabs>

  • 第2, 5行:href設定值必須包含parent state的部分-'#/tab'。
  • 第3, 6行:每個<ion-tab>各加入自己的<ion-nav-view>,name屬性值必須與state設定的views名稱相同。

starter.controllers定義於js/controllers.js,檔案內容如下:
angular.module('starter.controllers',[])
        .controller('homeCtrl',function($scope,$location){
            $scope.citys = citys;
            $scope.showWeather = function(index){                
                $location.path('/tab/home/'+index);
            }
        })
        .controller('settingsCtrl',function($scope){
            $scope.citys = citys;
        })
        .controller('weatherCtrl',function($scope,$stateParams){            
            $scope.weather = weather[$stateParams.id];
            $scope.img_base_url = image_base_url;
        })

var citys = [
    {name: "台北", q: "Taipei", on: true},
    {name: "台中", q: "Taichung", on: true},
    {name: "台南", q: "Tainan", on: true},
    {name: "高雄", q: "Kaohsiung", on: true},
    {name: "花蓮", q: "Hualian", on: true},
]
var image_base_url = "http://openweathermap.org/img/w/"; // open weather api icon
var weather = [
    {"coord": {"lon": 121.53, "lat": 25.05}, 
     "sys": {"message": 0.0418, "country": "TW","sunrise": 1429565168, "sunset": 1429611544},
     "weather": [{"id": 500, "main": "Rain", "description": "light rain", "icon": "10n"}], 
     "base": "stations", 
     "main": {"temp": 292.18, "temp_min": 292.18, "temp_max": 292.18, "pressure": 1022.92, 
            "sea_level": 1031.39, "grnd_level": 1022.92, "humidity": 100}, 
     "wind": {"speed": 3.75, "deg": 41.0005}, 
     "clouds": {"all": 92}, 
     "rain": {"3h": 0.13}, 
     "dt": 1429625933, 
     "id": 1668341, 
     "name": "Taipei", 
     "cod": 200},
     //...略...
]

  • 第4-6行:定義showWeather()事件處理器,給頁面使用。下一個檔案home.html便會說明因為捨棄之前用超連結<a href="指定url">的方式,改用ng-click,所以提供此事件處理器做轉址動作。Ionic/AngularJS之$location提供轉址服務,故第2行函式定義處加上$location參數,第5行再呼叫$location.path()進行轉址。
  • 第11-14行:和Hybrid Apps開發系列之4.3:多頁面App程式 (清單/細節瀏覽)範例相同,天氣概況頁面的城市參數也是透過$stateParams傳遞進來,$stateParams.id之'id'係因.config裡weather state定義了"'tab/home/:id"而得。
  • 第16-22:自定資料陣列。on屬性係用來記錄settings頁面各項目的開關(toggle)狀態。
  • 第24-37行:是從open weather api取得的台北市資料,其他城市資料則省略。

首頁templates/home.html檔案內容如下:
<ion-view title="目前天氣">
    <ion-content>
        <ion-list class="list">
            <ion-item class="item item-icon-left" ng-repeat="city in citys"
                      ng-click="showWeather($index)"
                      ng-show="city.on">
                <i class="icon ion-ios-home-outline"></i>
                {{city.name}}
            </ion-item>
        </ion-list>
    </ion-content>    
</ion-view>

  • 第4-9行:單一項目。第3行先使用<ion-list class="item">定義清單區塊,區塊內再用<ion-item class="item">定義每一個項目,如下所示:
    <ion-list class="item">
        <ion-item class="item"></ion-item>
        <ion-item class="item"></ion-item>
        ...
    </ion-list>
    此處以ng-repeat套用controller端$scope所設定的資料模型陣列,帶出城市資料,ng-click則取代之前的超連結做法,提供設定點選事件處理的函式呼叫。$index同樣是ng-repeat進行的次數,由0算起,故可代表城市資料陣列的序號。ng-show運用城市資料的on屬性值,決定是否顯示該項目。on屬性質透過下面settings.html頁面進行變更。
設定頁面templates/settings.html內容如下:
<ion-view title="城市列表">
    <ion-content>
        <ion-list>
            <ion-item class="item item-toggle" ng-repeat="city in citys">
                {{city.name}}
                <label class="toggle toggle-assertive">
                    <input type="checkbox" ng-model="city.on">
                    <div class="track">
                        <div class="handle"></div>
                    </div>
                </label>
            </ion-item>
        </ion-list>
    </ion-content>
</ion-view>

  • 第6-11行:開關(toggle)的CSS設定較為複雜,最外層以<label class="toggle">標示,僅跟著第7行設定input type為"checkbox",並以ng-model綁定controller端設定的變數;最後再以<div class="track">包住<div class="handle">顯示開關的外型。

天氣概況頁面templates/weather.html內容如下:
<ion-view title="天氣概況">
    <ion-content>
        <div class="list card">
            <div class="item">
                <h2>City of {{weather.name}}</h2>                
            </div>
            <div class="item item-avatar">
                <img src="{{img_base_url+weather.weather[0].icon+'.png'}}">
                <h2>{{weather.main['temp']-273.15 | number:1}}°C</h2>
                <h2>{{(weather.dt)*1000 | date:'yyyy-MM-dd HH:mm:ss'}}</h2>
            </div>
            <div class="item">
                <div class="row">
                    <div class="col">濕度</div>
                    <div class="col">{{weather.main.humidity}}%</div>
                </div>
                <div class="row">
                    <div class="col">風速</div>
                    <div class="col">{{weather.wind.speed}}哩/秒</div>
                </div>
            </div>
        </div>         
    </ion-content>    
</ion-view>


index.html檔定義如下:
[...略...]
<!-- your app's js -->
        <script src="js/app.js"></script>
        <script src="js/controllers.js"></script>
<body ng-app="starter">
    <ion-nav-bar>
        <ion-nav-back-button></ion-nav-back-button>
    </ion-nav-bar>
    <ion-nav-view>
    </ion-nav-view>   
</body>
</html>

沒有留言:

張貼留言