向客户机显示iPhone内容
使用 iUI 和 iPhone 列表结构
iPhone 和 iPod touch 使 Mobile Safari 成为风靡美国的手机浏览器。虽然使用 Mobile Safari 呈 现普通 Web 页面绰绰有余,但是许多 Web 开发人员都创建了针对 iPhone 的应用程序版本。本文是 “ 使用 Ruby on Rails 和 Eclipse 开发 iPhone 应用程序” 系列的第 2 部分,介绍了将下钻(drill- down)列表作为导航方法的常见用途。
本系列的 第 1 部分 采用了一个现有的 Ruby on Rails Web 应用程序,然后对它进行增强,供 iPhone 用户使用。第 1 部分集中讲解必要的客户端支持,这种支持允许向 iPhone 用户发送不同的内容 ,并允许接受方选择特定的内容,然后全屏显示该站点。第 2 和第 3 部分集中讲解了实际发送给用户的 内容,以及如何使这些内容符合 iPhone 或 iPod touch 用户的期望。第 2 部分集中讲解将下钻列表作 为导航方法的常见用途,第 3 部分集中讲解表单、分组及其他更高级的特性。
在本文中,您将使用层叠样式表(CSS)和 JavaScript 库 iUI 处理 iPhone 内容。iUI 库拥有与 Apple 的 iPhone 人机界面指南(human-interface guidelines)相匹配 CSS 类,以及处理模仿原始 iPhone OS 应用程序界面的向侧面滑动(sideswipe)效果的 JavaScript。然而,我们通常不希望在应用 程序中使用 iUI,所以我将讨论一些处理这些通用元素的实用 CSS 和 JavaScript。与出色的 Rails 实 践一致,我为 Ruby helper 方法的通用 iUI 模式构造了 HTML。这些方法绑定在一个 Rails 插件中,可 以下载并添加到任何 Rails 应用程序。
您将继续基于 第 1 部分 使用的 Soups OnLine 示例进行构建。Soups OnLine 是我的著作 Professional Ruby On Rails 中的一个示例,同时也是一个列出了 “汤的烹饪方法” 的站点。在多数 情况下,这个站点的具体细节与本文没有多大关系。整合 iPhone 界面所需要的步骤与 Soups OnLine 示 例的具体细节没有什么关联。最重要的细节是这个应用程序(由于保留了早期的 iPhone 形式)包含一个 控制器 RecipesController,它通过 index 方法处理食谱清单。在 第 1 部分 添加了 BrowsersController,用于管理 Mobile Safari 版本的站点中的各种选择。
要在本文使用这些示例,需要一个 Ruby 编辑器或 IDE,比如 Eclipse。模仿 iPhone 显示的浏览器 模拟器能提供不少帮助。有以下模拟器可供选择:用于 Eclipse 的 Aptana iPhone 插件、用于 Mac 的 iPhoney 以及官方 iPhone 软件开发工具箱(SDK)模拟器。本系列的第 1 部分讨论了各个模拟器的安装 、用法和优缺点。本文的示例使用 iUI 工具箱和 rails_iui plug-in。
iPhone 和用户体验
第一次接触 iPhone 时,一般人都会立即注意到它与传统的桌面浏览器区别不大。例如,要把普通的 笔记本电脑装在口袋里则是件非常困难的事情。这些差别深刻地影响到应该如何在 Iphone 上构建 Web 应用程序,给用户带来最佳的体验。最重要的差别包括:
iPhone 的屏幕尺寸(320x480)比用于桌面电脑 Web 应用程序的最小目标应用程序还要小得多。 iPhone 屏幕的高宽比也与典型的桌面或笔记本电脑的显示器有巨大的差别。
iPhone 的像素密度比桌面电脑显示器要高得多,便于阅读小文本或更改图像的相对尺寸。
用户可以 90° 旋转 Mobile Safari 视图,更改图像的大小,更重要的是,还可以更改屏幕的高宽比 。
Mobile Safari 的触摸屏界面不如鼠标界面精确,这意味着它的按钮、链接等目标及它们之间的距离 应该比桌面电脑应用程序的大。
iPhone 所使用的网络环境通常比较慢。但是用户却热切期望网络能够即时响应他们请求。
这些差别的结果表明:iPhone Web 开发不是一场在屏幕上填塞东西的比赛。尽管能够设法将所有的导 航栏、标志、插入广告以及内容填塞到 iPhone 屏幕上,但是移动用户会难以忍受网速的下降,或他们必 须很费劲才能选择屏幕上的目标。相反,iPhone Web 开发的目标是创建简洁的用户界面,为移动用户提 供最重要的功能。的确,对于 Web 应用程序的某些部分,移动用户需要更多的点击才能访问,但是必须 突出应用程序的核心部分。
例如,Amazon 和 Digg 是两个很受欢迎的 Web 站点,它们具有专用于 iPhone 的版本。Digg 使用了 iUI 框架(已在本文讨论)的变体,模仿 iPhone 的外观和体验。而 Amazon 使用更有个性的外观,这个 外观在 Mobile Safari 浏览器中也表现得很好。下面给出了 Mobile Digg 的图片。(由于某种原因, Amazon.com 模仿得不是很好)。
图 1. iPhone 版的 Digg
Digg 和 Amazon 都一样,只为移动用户保留了核心功能 —— Digg 的新闻列表,Amazon 的搜索功能 。只显示核心功能可以将站点适当地显示在 iPhone 屏幕上,同时也使用户能够直接访问最重要的站点功 能。在本文的剩余部分,我将展示如何改造站点,使它适合于 iPhone。
将 iUI 添加到 Rails 应用程序
要使应用程序具备 iPhone 的外观和体验有两种主要选择:
根据 Apple 的示例代码或其他外观不错的站点,将您自己的 CSS 和 JavaScript 添加到您的站点。
使用现有的工具箱。
最出色的现有工具箱是 iUI。它已经创建了按钮图形、字体选择和 JavaScript 效果,您只需要关注 站点的内容。这是这个工具箱的优点。它的缺点是规定了站点的组织方式:
需要在指定位置使用特定的文档对象模型(Document Object Model,DOM)ID。
与服务器的默认交互通过 Asynchronous JavaScript + XML(Ajax)来实现。
我认为 iUI 最适合用于能够轻松实现为列表的站点。Apple 的 iPhone 人机界面指南把列表格式当作 组织 iPhone 内容的 “最有效方法”,因此,可能的话,最好考虑列使用列表组织。
iUI 被封装为一个包含 JavaScript 文件、CSS 文件和一系列图像的目录。因为 Rails 会在特定的目 录下寻找这些文件,所以必须通过以下步骤集成 iUI 文件与 Rails 应用程序:
将 iui.js JavaScript 文件移动到 Rails 应用程序的 public/javascripts 目录下。
将 CSS 文件 iui.css 移动到 public/stylesheets。
将图像文件(.png and .gif)移动到 public/images。
移动会打乱 CSS 文件里面的相对 URL,因此需要将所有的引用形式 url(button.png) 更改为 url (/images/button.png)。这样,CSS 文件才能在 Rails 发行版中准确地定位图像。
如果觉得手工完成这些步骤比较复杂,可以使用 rails_iui 插件所包含的一组 Rake 任务,它可以下 载并安装 iUI,包括在 CSS 文件中更改 URL。执行命令是 rake iui:install。iUI 还包含压缩版的 CSS 和 JavaScript 文件(为了加快下载速度,删除了额外的空白)。文件名为 iuix.js 和 iuix.css。在自 动的 Rake 任务中,可以选择使用这些文件的压缩版。
在项目中安装 iUI 后,需要在布局中添加 JavaScript 和 CSS 文件。iPhone 布局文件(在本示例中 为 app/views/layouts/recipes.iphone.erb)的开头应该包含以下两行:
<%= stylesheet_link_tag 'iui' %> <%= javascript_include_tag 'iui' %>
如果正在使用 rails_iui 插件,则可以简单表达为 <%= include_iui_files %>。
至此,您已经为创建 iPhone 内容做好准备。
创建 iPhone 布局
在原始版本的 Soups OnLine 应用程序中,导航部分位于侧边栏,正文内容位于中央。这不适用于 iPhone,因此,我将把这个应用程序转换为列表结构。应用程序的主页将以列表的形式包含基本相同的导 航选择,并且用户可以向下钻取每个条目。例如,Recipes 导航条目将把用户带到另一个条目,这个条目 显示了最近添加的 “食谱”,并且还可以选择显示更多的条目。这里的每个条目将链接到特定 “食谱” 的显示页面 。
我将从 3 个层次讨论这些代码:
rails_iui 插件定义的 Rails helper
这个插件使用 iUI 定义的样式类生成的 HTML
在非 iUI 项目中要用到的关于 CSS 本身的一些详细信息
在默认情况下,iUI 会覆盖对单击正常链接的响应。iUI 执行 Ajax 调用并且重新绘制页面的可见区 域,而不是重新绘制整个页面。因此,iUI 可以为每个链接添加一种向侧面滑动的效果,这种效果类似于 在 iPhone 的 iPod 应用程序中下钻艺术家或专辑列表时产生的效果。通过在锚标记中更改 target 属性 ,可以用两种方式对此进行覆盖。如果链接目标是 _self,将使用刷新整个页面的正常超链接行为。如果 链接目标是 _replace,将使用服务器请求的结果代替锚标记。
从 Rails 的角度看,iUI 结构意味着正文布局仅呈现一次。在这之后,所有调用都是 Ajax 调用。即 使是常规的 link_to 调用,也必须作为 Ajax 调用和 :layout => false 一起发布。此外,这还意味 着对于 iPhone Web 应用程序中的简单 Ajax 活动,不需要使用 link_to_remote。
所以这个应用程序的初始用户页面仅是导航部分。这意味着必须为应用程序设置一个默认路径。这个 应用程序没有呈现自己的文本,仅显示正文导航的布局。如果缺乏明显的位置来放置这个路径,那么通过 向 config/routes.rb file 添加下面的行,将它添加到在第 1 部分中创建的 BrowsersController: map.root :controller => "browsers"。
控制器操作在 app/controllers/browsers_controller 中进行,并且很简单。
清单 1. 默认布局路径-控制器操作
layout "recipes" def index respond_to do |format| format.html {redirect_to recipes_url} format.iphone {render :text => "", :layout => true} end end
在 iPhone 中,仅呈现没有文本的布局。如果请求的是 HTML,它会重定向到 RecipesController 索 引页面,这个页面是应用程序的桌面视图的主页面。
iPhone 的呈现活动在呈现器中进行,目前正在调用一些由 rails_iui 插件定义的 helper 函数,根 据 iUI 的要求设置页面,如清单 2 所示。(如果 rails_iui 插件放置在 Rails 应用程序的 vendor/plugin/rails_iui 目录下,rails_iui helper 将自动应用到所有视图)。
清单 2. iPhone 主导航栏的布局正文
<%= iui_toolbar "Soups OnLine", new_search_url %> <%= iui_list iphone_menu.items, :top => content_tag(:h1, "Soups OnLine", :class => "header"), :bottom => link_to ("Switch To Desktop View", {:controller => "browsers", :action => :desktop}, :class => "mobile_link") %>
生成的屏幕类似于图 2。
图 2. iPhone Soups OnLine 主导航栏
这里有两个 helper 函数。第一个是 iui_toolbar,用于设置大多数 iPhone 应用程序顶部的灰蓝色 工具栏。Rails helper 类似于清单 3。
清单 3. iUI 工具栏的 Rails helper
def button_link_to(name, options, html_options = nil) html_options[:class] = "button" link_to(name, options, html_options) end def iui_toolbar(initial_caption, search_url = nil) back_button = button_link_to("", "#", :id => "backButton") header = content_tag(:h1, initial_caption, :id => "header_text") search_link = if search_url then button_link_to("Search", search_url, :id => "searchButton") else "" end content = [back_button, header, search_link].join("\n") content_tag(:div, content, :class => "toolbar") end
这些代码生成下面的 HTML。
清单 4. iUI 工具栏的 HTML
Soups OnLine
Search
HTML 中的几个条目由 iUI 定义。toolbar 类定义顶部工具栏的颜色、尺寸和位置。工具栏内部的 h1 标记也由白色文本的 iUI 专门定义。backButton DOM ID 由 iUI 保存,它在单击链接后由 iUI JavaScript 创建。下一个 rails_iui helper 将使用 header_text DOM ID。清单 5 提供了一些来自 iUI 的相关 CSS。
清单 5. iUI 的头部 CSS
body > .toolbar { box-sizing: border-box; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; border-bottom: 1px solid #2d3642; border-top: 1px solid #6d84a2; padding: 10px; height: 45px; background: url(/images/toolbar.png) #6d84a2 repeat-x; } .toolbar > h1 { position: absolute; overflow: hidden; left: 50%; margin: 1px 0 0 -75px; height: 45px; font-size: 20px; width: 150px; font-weight: bold; text-shadow: rgba(0, 0, 0, 0.4) 0px -1px 0; text-align: center; text-overflow: ellipsis; white-space: nowrap; color: #FFFFFF; }
第二个 rails_iui helper 函数,如清单 6 所示,从菜单条目列表中生成实际的列表。在这种情况下 ,创建菜单条目对象并不重要。(详细情况请查阅 Professional Ruby on Rails —— 参见 参考资料) 。出于本书的需要,菜单条目是一个具有标题和 URL 属性(描述单击时菜单条目的去向)的对象。
清单 6. rails-iui 列表 helper
def list_element(item) onclick_one = "$('header_text').innerHTML='#{item.caption}'; " onclick_two = "$('backButton').addEventListener('click', function() {$('header_text').innerHTML='Soups OnLine'; }, false);" link = link_to(item.caption, item.option_hash, :onclick => "#{onclick_one} " + " #{onclick_two}") content_tag(:li, link) end def append_options(list_content, options = {}) list_content = options[:top] + list_content if options[:top] list_content += options[:bottom] if options[:bottom] list_content end def iui_list(items, options = {}) list_content = items.map {|i| list_element(i)}.join("n") list_content = append_options(list_content, options) content_tag(:ul, list_content, :selected => "true") end
在 HTML 列表中,菜单列表中的每个条目都有各自的 li 元素。它包含连接到正确的 URL 的链接以及 一些管理工具栏标题的 JavaScript。JavaScript 处理程序负责两件事情。首先,它更改工具栏的文本以 反映新的链接。(因为新的链接仅调用 Ajax 更新页面的正文,所以这只能在客户端处理)。其次,它更 改处理程序的 Back 按钮,因此 Back 按钮将工具栏的标题改为原来的 Soups OnLine。这并不能真正解 决在深层下钻时保持标题同步的问题。但在撰写本文时,iUI 和 rails_iui 都不支持这个功能。
将所有的列表条目放置在一个 HTML UL 列表中,这个列表具有特定的属性对 selected=true。iUI 使 用这来决定将哪个列表放置在 iPhone 视见区的正文部分。如果在页面中 selected 设置为 true 的位置 存在一个 HTML 标记,CSS 会使用 CSS 声明 display: block 将它赋值到视见区的整个正文。与正文标 志的尺寸定义一起,这将给 selected 条目一个完整的视见区。这是很有用的。在一个 iUI 示例中,同 一个页面出现了几个代表多层下钻的列表。最初只会显示 selected 列表,其他列表则通过单一页面内的 锚和名称链接来访问。
然而,因为 selected 列表是完整的视见区,具有 Soups OnLine 标志的头部和具有 Switch to Desktop View 链接的底部必须置于 UL 标记的内部。helper 函数允许列表的顶部和底部包含任意的 HTML —— 它们在前面的布局正文片段(清单 2)中是 :top 和 :bottom。生成的 HTML 类似于清单 7。 我为菜单的第一个元素添加了完整的清单,但对于其他元素,则省略了重复的清单。
清单 7. iUI 列表 HTML
单击 Most Recent Recipes 条目,界面内容就会向侧面滑动,屏幕如图 3 所示。在这里,iUI JavaScript 更改了标题和 Back 按钮。
图 3. 只有一级的下钻
要创建这个屏幕,Recipe Controller 的 index 方法需要将 format.iphone {render :layout => false} 放置到它的 respond_to 块中,如下所示。
清单 8. 食谱的索引操作
def index @recipes = Recipe.find_for_index(params[:type]) respond_to do |format| format.html # index.html.erb format.xml { render :xml => @recipes } format.iphone {render :layout => false} end end
呈现的文件 app/views/recipes/index.iphone.erb 使用了相同的 rails_iui helper 函数。这假设 Recipe 对象能够恰当地响应 helper 函数(<%= iui_list @recipes %>)调用的 caption 和 option_hash 方法。
使用替换扩展列表
我在前面提到,在 iUI 锚标记中将目标设为 _replace 会导致这个标记调用的结果自动地替换原始列表。这能够使列表的最后一个元素显示某些内容,比如 “Next 25 items”,同时也使新的条目像原始条目一样出现在相同的列表上,便于用户上下滚动整个列表。
要想使用已经构建的 helper 函数实现替换功能,必须通过两种方式扩展 iui_list 方法。列表 helper 函数需要一个选项来为列表添加更多的条目 —— 目前,假设它是列表底部的一个额外选项。然后,对单击的响应需要返回标记为 li 的条目列表,但没有包围的 ul 标记,这个标记已经存在于需要更改的页面中。
这个实现的第一部分是一些特定的 link_to helper 函数,用于管理 iUI 特定的 _replace 和 _self 行为。然后,我将再添加一个方法,根据 target 参数实现不同链接类型的转换。这两种方法如下所示。
清单 9. iUI 链接 helper 函数
def link_to_replace(name, options, html_options = {}) html_options[:target] = "_replace" link_to(name, options, html_options) end def link_to_external(name, options, html_options = {}) html_options[:target] = "_self" link_to(name, options, html_options) end def link_to_target(target, name, options, html_options = {}) if target == :replace link_to_replace(name, options, html_options) elsif target == :self or target == :external link_to_external(name, options, html_options) else link_to(name, options, html_options) end end
准备好链接 helper 函数后,可以扩展 iui_list 函数和附加的 append_options 方法,添加新的功能。
清单 10. iUI 链接 helper 函数
def append_options(list_content, options = {}) list_content = options[:top] + list_content if options[:top] list_content += list_element(options[:more], :replace) if options[:more] list_content += options[:bottom] if options[:bottom] list_content end def iui_list(items, options = {}) list_content = items.map {|i| list_element(i)}.join("\n") list_content = append_options(list_content, options) if options[:as_replace] list_content else content_tag(:ul, list_content, :selected => "true") end end
额外的 list 元素实际上添加到 append_options 方法的第二行。应该在 :more 选项中传递这个元素,就像 items 列表元素应该有一个标题和一个 URL 一样。如果传递 :as_replace => true 的话,iui_list 中的最后一个 if 语句会造成 ul 列表标记被省略。
调用具有最后链接的 iui_list 方法类似于下面的清单,:more 选项在列表的底部提供了列表元素:
<%= iui_list @recipes,
:more => ListModel.new(nil, "Next 25 items", more_recipes_url) %>
响应 more_recipes_url 的控制器操作 —— 不管结果是什么 —— 应该使用 :as_replace => true 调用 iui_list。
iUI 还有一个处理列表的技巧。使用 CSS group 类将在列表内部生成一个标题,类似于本机 iPod 歌曲应用程序的清单。
图 4. 具有组标题的列表
构建组列表的 rails_iui helper 函数重用构建普通列表的大部分代码。这个方法使用一个块动态地决定标题。
清单 11. 构建具有组的列表的 rails_iui helper 函数
def iui_grouped_list(items, options = {}, &group_by_block) groups = items.group_by(&group_by_block).sort group_elements = groups.map do |group, members| group = content_tag(:li, group, :class => "group") member_elements = [group] + members.map { |m| list_element(m) } end content_tag(:ul, group_elements.flatten.join("\n"), :selected => "true") end
iui_grouped_list 方法使用 Rails ActiveSupport group_by 方法,将列表转换成 [group, [members]] 的 2-D 列表。分类可以保证组以字母顺序排列(在应用这个方法之前,您肯定希望将每个条目按顺序排列)。
这个方法的视图代码类似于(块返回食谱标题的首字母):
<%= iui_grouped_list(@recipes) {|r| r.title[0, 1]} %>
结束语
到目前为止,已经学习了如何向 Mobile Safari 用户提供定制内容。也懂得了如何使用列表外观显示站点导航,这种方式不仅符合 iPhone 用户的期望,也加快了加载速度(在网速比较慢的情况下也不例外)。
本系列的第 3 部分将讨论用户下钻并获取了一些内容之后要显示的内容。这包括显示面板和对话框,以及使用普通的 iPhone 圆角矩形样式。您将明白如何对旋转 iPhone 设备以及翻转 Mobile Safari 侧边作出响应。