向客戶機顯示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 主導航欄的布局正文
<body> <%= 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") %> </body>
生成的屏幕類似於圖 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
<div class="toolbar"> <a href="#" class="button" id="backButton"></a> <h1 dddd="header_text">Soups OnLine</h1> <a href="http://localhost:3000/search/new" class="button" id="searchButton">Search </a> </div>
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
<ul selected="true"> <h1 class="header">Soups OnLine</h1> <li> <a href="/recipes" onclick="$('header_text').innerHTML='Most Recent Recipes'; $('backButton').addEventListener('click', function() {$('header_text').innerHTML='Soups OnLine'; }, false);"> Most Recent Recipes</a> </li> ### OTHER LIST ITEMS REMOVED <a href="/browsers/desktop" class="mobile_link">Switch To Desktop View</a></ul>
單擊 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 側邊作出響應。