现在在你的工具箱中有很多工具,让我们来谈谈开发一个真正的手机应用。Instagram是一个非常受欢迎的照片分享应用。使这些照片更加有趣并且展示一些Ionic功能,我们将会涉及到一个我们开发应用,它叫 Trendicity。这个应用以多种方式获取和展示Instagram的照片。
一种方式就是根据照片的位置在地图上进行展示。另一种方式就是将照片展示在可以滑动的卡片上,并且用户可以标示出喜欢的照片。最后一种方式,我们以列表的形式展示照片和更多有关照片信息。
这个完整的Trendicity应用可以在Github上找到。你可以下载它并在你的浏览器或者设备或仿真器上运行。
Trendicity应用是作为一个side menu 应用建成的,可是,它也很好的结合了tabs方式。我们将会从几方面去讨论这个应用。Side Menu和用户的操作选项,搜索功能,和加载服务的使用和(地图示图,卡片视图和列表视图)tabs。
我们通过构建这个应用来深入了解代码。
##Side menu
侧边菜单由以下菜单项构成:
- HOME
- Favorites
- About
- Login/Logout
下面是Trendicity侧边菜单的截图。关于怎么样实现侧边菜单和路由时一直带着它的细节,请看第四章:实现侧边菜单和配置路由。
##Home
HOME菜单选项点击时会调用HomtCtrl和展示地图Tab。地图上将会展示附近的一些图片。
##Favorites
大体上,Trendicity应用的收藏功的实现有三部分组成:FavoritesService
,FavoritesCtrl
和favorites.html
模版。
收藏功能有两种类型的照片会进行收存放:用户生成的和自己收藏的。前者是通过add-favorite模态窗口添加的,后者是通过地址位置直接关联到Instagram相片的。
这一部分,我们将会实际操作创建和删除收藏,看一下收藏功能是如何实现及FavoritesCtrl
和FavoritesServices
之间是如何交互的。
###收藏列表
收藏数据在一个列表中展现。当用户进入这个视图,会触发$ionicView.enter
事件,这个视图中的数据将自动刷新:
$scope.$on('$ionicView.enter',function(){ |
列表中的每一条收藏条目点击时都会在此相片的地理位置为中心定位到地图上。下面是收藏视图中展示收藏数据的代码:
<ion-list> |
添加一些友好的提示信息,当没有收藏信息要展示时,做如下操作:
<!-- Display message when no favorites exist --> |
###添加收藏
为了保持代码的可维护性,我们决定将开发添加收藏功能的的代码进行解藕合。因此,我们将它拆分成了 FavoritesCtrl,FavoritesService,add-favorite.html模版,和add-favorite-form指令,让它们组合到一起完成这个功能。
添加收藏的动作,从点击右上角的’+’号打开一个添加收藏的模态窗口。
<ion-nav-buttons side="right"> |
点击这个按钮会触发FavoriteCtrl的showModalAddFavorite()方法,它会打开添加收藏的模态窗口。
$scope.showModalAddFavorite = function(){ |
模态窗口中的内容都是有add-favorite-form指令负责生成。
<ion-content> |
![添加收藏](/images/ionic/add_favorite.png =300x)
addFavoriteForm指令会在其内部处理onSubmit。当这个表单填写的没有问题提交时,会调用addFavoriteForm中的submit方法。也就是说,一旦表单没有问题,指令会调用submit方法,通过调用他的on-submit属性。
现在,指今如何知道去哪里找到addFavorite(favorite)方法呢?
当我们初始化模态时,会将FavoriteCtrl的作用域一并传入。因此,这个模态的作用域是继承自它的父作用域(即收藏视图的作用域);
$ionicModal.fromTemplateUrl('templates/modals/add-favorite.html', { |
这样,addFavorite()方法已经与控制器的作用域相关联:
// Add a new favorite using the service |
当调用FavoritesService’s add()方法后,将会更新收藏列表并具隐藏add-favorite模态。
###添加收藏表单指令
addFavoriteForm指令使用templates/directives/add-favorite-form.html作为它的模版。这个视图包含了表单的基础验证:
<form name="formAddFavorite" no-validate> |
“Add”按钮会调用命令的submit()方法,会添加一个$attempt字段到表单中,标示用户已经最近已经提交了一次这个表单。这个字段被用来高亮不合法和没有输入的输入选项。
最后,当这个表单验证通过后,指令会通过模态中的一个属性调用onSubmit()方法:
$scope.submit = function() { |
![合法的表单](/images/ionic/valid_form.png =300x)
而且,在这个模态关闭时,其中添加收藏的表单会自动清空。它是通过监听modal.hidden事件来进行实现的:
$scope.$on('modal.hidden', function() { |
###删除收藏
当你滑动收藏条目时就可以进行删除收藏操作。
![删除收藏](/images/ionic/remove_operate.png =300x)
点击删除按钮,将会调用FavoritesCtrl的deleteFavorite()方法删除当前收藏的引用。在控制器这边,这个方法会调用FavoritesService的delete方法,将收藏列表中的当前操作的收藏项删除。
// Delete a favorite using the service and update scope var |
###收藏服务
FavoritesService是用标准方式来处理Trendicity中与收藏相关的服务。它将收藏相关逻辑与Trendicity其它功能隔离开。用这种方式的另一个好处就是FavoritesService服务可以在任何一个 Controller中使用。像我们在列表视图中实现的那样,这个服务被用来收藏自己喜欢的地理位置。
这个服务提供了三个方法:add(),getFavorites()和delete()
正如它们的名字叫的那样,getFavorites()会直接返回当前本地的所有收藏。
添加收藏时的处理方式根据收藏的类型而定。对于人们经常收藏的城市或区域,这个类型的收藏会用GeolocationService的addressToPosition()方法获取地理坐标。这个实现我们在稍后章节中会进行讨论。当获取到了地址经纬度,这个收藏会统一格式化一种结构存储到本地的收藏数组中。
var address = favorite.city + ', ' + favorite.region; |
针对具体的收藏,收藏会在列表视图的控制器中进行合适的格式化处理,和用户添加的收藏的处理过程相信,存放到本地收藏数据中。
var newLocation = { |
为了确保准确定位,这个服务为第一个收藏生成了一个对应的ID。每一个id都是根据当前系统的时间生成的:
this.add = function (favorite) { |
删除收藏的工作通过使用lodash的remove()方法实现。删除了收藏后,更新本地收藏并将最新的收藏列表返回。
// DELETE |
##About
‘About’菜单选项点击时会滑出界面,包含一些应用的介绍信息。这个页面会在应用程序第一次加载时显示。
##Login/Logout
当用户还没有登陆要登陆时,点击菜单的’Login’时会显示登陆界面。一旦用户登陆后,再点开菜单,’Logout’选项将出展示。
下面是www/templates/menu.html部分代码片断,展示了如何用ng-show和ng-hide实现上述功能。
<ion-item menu-close class="item-icon-left" ng-click="login()" ng-hide="isLoggedIn()"> |
你可以在www/controllers/app.js文件中找到isLoggedIn,login和logout方法:
// Determine if the user is logged into Instagram |
isLoggedIn方法会调用InstagramService的isLoggedIn()方法来获取当前登陆状态。退出方法会调用InstagramService的logout()方法来进行登记退出动作。我们会在第八章讲解更多关于InstagramService相关的内容。
为了做更多和Instagram相关并有趣的事情,我们需要用户登陆到Instagram。为了实现这个,我们使用一个$ionicModal显示为什么让用户登陆的简要的描述。当选择了登陆选项,login方法被调用,用户会看到一个登陆模态。
![登陆截图](/images/ionic/login.png =300x)
// Create the login modal that we will use later |
通常,登陆页面像下面截图中展示一样。但是由于我们要与Instagram集成,我们需要OAuth的方式进行登陆。
<ion-modal-view id="login"> |
用Oauth代替传统的登陆方式,让用户知道一旦登陆到了Instagram,他们能做哪些事情。一旦用户确定登陆到Instagram,我们会调用www/controllers/app.js中的loginToInstagram方法:
// Perform the OAuth login to Instagram |
这个方法将会用InAppBrowser Cordova插件执行OAuth授权。我们会使用这个插件打开一个登陆到Instagram窗口。Trendicity应用没有对用户名和密码进行处理。
![Instagram登陆](/images/ionic/instagram_login.png =300x)
##Search
搜索图标就是在列表或卡片视图的右上角的放大镜图标。用户有以下查询选项:
- Trending - 按当前在Instagram上帖子的受欢迎程序进行排序。
- Nearby - 当前用户所在位置1KM范围内发布的帖子。
- My Feed - 当前用户提供给Instagram的信息。
- My Liked Posts -当前用户喜欢的帖子。
![search](/images/ionic/search.png =300x)
在HomeCtrl中,我们设置了查询功能。为了关注搜索功能,下面代码进行稍微的压缩。
首先,我们定义了一些作用域变量来保存帖子和查询变量。我们使用了两个javascript对象来存放,所以当值发生变化时,对象不会丢失。
接下来,我们设置$scope.getPosts方法,它将会根据查询值决定哪些查询方法会被调用。
$scope.getPosts = function(value) { |
通过$watch监听ionicPopover中查询变量值的变化来及时调用$scope.getPosts方法来进行查询。
$scope.updatePosts = function (searchValue) {
$scope.getPosts(searchValue);
$scope.closePopover();
$ionicScrollDelegate.scrollTop();
};
$scope.$watch('search.value', function(newValue) {
// Triggered when user changes search value
$scope.updatePosts(newValue);
});
为了实现ionicPopover,我们首先需要通过 fromTemplateUrl 方法加载 html 模版。我们在这里将其作用域设置为当前作用域。当template加载时,我们将popover设置到这个作用域中,方便我们后面进行调用。
$ionicPopover.fromTemplateUrl('templates/search.html', { |
下面这段 search.html 模版片段。我们使用了ion-popover-view 组件定义这个视图。我们使用 ion-header-bar 组件展示弹出窗口的标题。我们将 ion-radio 组件列表放到 ion-content 组件中。ion-radio组件选择后,会作为搜索参考值。
<ion-popover-view> |
当用户点击在templates/home.html中定义的导航按钮时 这个 ionicPopover 将会弹出。下面地这段代码:
<ion-nav-buttons side="right"> |
由上面代码可以看到,当用户点击Search图标时,会调用openPopover方法。我们要做的就是让popover显示。当选择了一个值时,将会调用closePopover方法,在这个方法中,我们要确认popover是已经定义并是打开的。如何条件满足,我们就将其关闭。最后一件事情就是为popover设置一个$destroy监听,当popover被从Dom中移除时调用。
$scope.openPopover = function($event) { |
##加载服务
当照片进行加载时,用户会不知道应用在做什么,我们应该通知用户当前是在加载数据。我们可以使用$ionicLoading组件来完成这件事情。
![loading](/images/ionic/ionic_loading.png =300x)
一种聪明的方式就是将 $ionicLoading 组件调用用放在一个HTTP拦截器中。下面是我们压缩后的代码:
|
当HTTP请求发生时,request方法将会被调用。这里我们显示文字 “Loading”。为了避免依赖死循环,我们需要用Angular $injector组件来获取$ionicLoading服务。
当HTTP请求出现错误时,这里会调用requestError方法。我们会调用hideLoadingModalIfNecesary()方法,这个方法会检查是否有HTTP请求在被调用,如果没有ionicLoading服务会将自己隐藏。
当HTTP响应成功时,会调用response方法。之后做的事情与错误时的处理方法类似。
当HTTP响应错误时,会调用responseError方法。之后做的事情与上面两种情况处理方法类似。
为了使我们的HTTP拦截器生效,我们需要让AngularJS知道它的存在。一般我们会在www/app.js中做这件事情。这里是我们将拦截器通过$httpProvider添加到拦截器数据中的方法:
.config(function($httpProvider) { |
注意,这里我们没有显示真正的提示信息,只是在www/app.js中设置了一个默认信息:
.constant('$ionicLoadingConfig', { |
我们在第七章会详细讲解:设计应用。现在,我们只是想告诉你为什么不向ionicLoading show方法中传入信息。
##Map视图选项
将当前提交的信息在一个可以交互的地图上展示。一个标记会将用户的位置标在地图上。别的照片按它的位置展现在地图上。当点击这个这个标记时,相关联的照片会展现出来。如果你移动地图到一个位置 ,然后点击右上角的刷新图标;就会加载当前位置周边的照片。若想了解如何实现的,请看第五章:在ionic上集成地图。
##Card实图选项
卡片视图将以卡片叠加的方式来展现照片。若用户不喜欢当前的照片,可以将图片向左侧拖拽或划动。如果喜欢的话可以向右侧拖拽或划动。
这些可以摆动的卡券与非常受欢迎的手机应用Tinder相似。这种类似的实用的功能在Tinder中经常会被用到。
Drify(该公司负责我们的ionic技术)创始人之一Max Lynch已经创建了一个划动卡片的库,这个库经常会被ionic开发团队引用。在ionic中这样的效果展示通常会比较受欢迎的。
这个类库用CSS的动画来实现卡片左右拖拽和摆动的效果。他也使用了一个物理样式动化效果库Collide来实现将图片下拉时的动画效果。
在使用Tinder卡片库之前,一个非常相似的卡片划动效果被创建。我们只是在这里提一下它,在你看到这块时不要好奇。我们在这里没有涉及到这个库。
###工作文件
与该视图相关的文件位置在:www/templates/tab-card.html
,www/templates/card-intro.html
,www/js/controllers/card.js
和www/js/directives/no-touch-move.js
。
###popup介绍
当导航到卡片实图时,我们决定弹出展示说明信息,来向用户介绍和让他们熟悉如何使用卡片的操作。
我们使用$ionicPopup服务来展示Ionic样式的弹出窗信息。这个服务允许你定义四种类型的弹出窗:
show(选项) :通过加载的选项设置来完全自定义弹出窗
alert(选项) :带有一个按钮的弹出窗
confirm(选项):带有确认信息和“取消”和“确认”铵钮的弹出窗
prompt(选项):和confirm一样,但是多出一个与用户交互的输入框。
为我们Trendicity应用,自定义了一个弹出窗如:
// Show explanation message |
弹出窗的信息体部分通过template和templateUrl选项进行定义。我们这里单独定义这个模版文件:templates/card-intro.html
我们的模版文件中包含一个划动的盒子,可以流畅的在照片与照片之间进行划动。
<ion-slide-box does-continue="false" show-pager="false" delegate-handle="card-intro-slidebox" ng-init="disableSlideBox()"> |
这个盒子组件是由AngularJS服务$ionicSlideBoxDelegate生成。这个服务允许你控制这个组件的行为,如划动时的效果和控制是否自动播放。另外当再次渲染这个盒子时调用update()方法。
我们这里,禁止照片的自动切换和用户划动,而所有的操作只能通过铵钮来进行完成。
// Disable intro slidebox sliding |
弹出窗口提供了一个回调onTap,这里可以让用户去自己做一些事情。如果当前是第一个照片,点击“Next”铵钮,我们通过调用$ionicSlideBoxDelegate.$getByHandle(‘card-intro-slidebox’)实例的next()打开下一张照片,并且修改铵钮的文字为”OK”同时防止弹出窗口关闭。另外,当在最后一个照片时,我们会调用return并关闭弹出窗口。
最后,每次用户访问卡片视图时,我们会查看一下本地存储seenCardIntro的值来判断用户是否已经看这个卡片。
###Card视图
在www/templates/tab-card.html文件中,我们会展示卡片集合。我们通过ng-repeat来迭代显示照片。让后我们定义一些属性来响应用户对卡片的操作。
###Card视图对应的controller
我们需要CardViewCtrl使侧边滑出菜单失效,因为它会与Tinder卡片功能冲突 。这里我们通过$ionicSidemenuDelegate来设置当内容进入视图时拖拽不可用。当离开视图时,我们恢复可用,使别的视图下可以打开侧边菜单。
|
这里cardTransitionedLeft和cardTransitionRight 方法基本上一样的,除了,左滑进代表不喜欢,右滑是代表喜欢。两者都要判断是否已经登陆。如何用户喜欢当前的卡片但又没有登陆时,会要求用户登陆。一旦登陆成功,这个卡片就会被收藏。这是我们安全认证方案的结果。你可以通过第六章节的 验证 来了解更多的信息。
当一个卡片一旦过渡完并且被销毁时就会调用cardDestroyed方法。我们这里只是将卡片从照片数组中删除。
##List视图选项
Trendicity的列表选项是为了展示ionic核心的列表功能而开发的。比较受欢迎的组件为:拉取刷新,button bars action sheets,视图中的手势。这一部分我们将解析开发列表的过程,和上述的组件。
###涉及文件
列表视图功能涉及的文件:www/templates/tab-list.html,www/js/controllers/list.js和www/js/directives/on-dbl-top.js。路由在www/js/app.js中定义的:
.state('app.home.list',{ |
###模板布局
列表视图模板可以被切分成三部分:刷新,帖子列表,和包含用户可操作铵钮的铵钮条。
###刷新帖子列表
Ionic提供了非常有用的指令,名字为ion-refresher。在使用这个组件时,将标签放到你的视图上,并添加在视图控制器中添加一个处理用户交互的方法,一旦用户向下拉取内容或者触发刷新机制。
![列表视图](/images/ionic/list_view.png =300x)
为了保存简单易用,我们选择一个刷新铵钮(ion-arrow-down-c)来代替默认的图标,同时设置下拉时提示的文字。
<ion-refresher pulling-icon="ion-ios-arrow-up" spinner="none" pulling-text="Pull to refresh..." on-refresh="doRefresh()"></ion-refresher> |
注意:这个命令允许你通过下面属性来覆盖它默认的配置:
- on-refresh:一旦用户下拉放开后触发刷新机制。
- on-pulling:一旦用户开始下拉时调用。
- pulling-icon:当用户下拉时显示的图标。
- pulling-text:当用户下拉时显示的文本。
- refreshing-icon:当刷新机制一旦触发,这个图标就会显示。
- refreshing-text:一旦数据刷新后,显示的文本。
- disable-pulling-rotation:一旦on-refresh调用开始调用,停止旋转图标。
ionrefresher组件值得注意的提升就是,添加了一个定时器,当很快获取到数据时,刷新显示数据也要耗时最少要400ms。这种情况超时创造一个平滑过渡的假像,使用户感觉到数据是刷新了。
我们回到Trendicity,refresher设置当用户希望获取新的帖子时,触发控制器的doRefresh方法。下面就是刷新时调用的方法:
// Refresh feed posts |
就像前面章节解释的那样,我们使用HomeCtrl的getPosts方式