「下拉刷新」由著名設計師 Loren Brichter 設計,並應用於 Twitter 第三方應用 Tweetie 中。2010年4月,Twitter 收購 Tweetie 開發商 Atebits 後,該專利歸 Twitter 全部。這一章咱們就來看看如何實現一個簡單的下拉刷新組件。css
<img src="http://xmlplus.cn/img/pullrefresh.png" class="img-responsive"/>css3
和前面在設計組件時的作法同樣,咱們先想一想看最終的成品組件是如何使用的,這須要點想像力。下拉刷新組件當作一個容器組件是合理的,用戶能夠對容器的內容進行下拉操做。若是用戶完成了完整的下拉觸發操做,該組件應該會有下拉完成的事件反饋,假定這個事件名爲 ready
。根據以上的分析,咱們頗有可能獲得下面的一個該組件的應用示例。web
Index: { xml: `<PullRefresh id='example'> <h1>Twitter</h1> <h2>Loren Brichter</h2> </PullRefresh>`, fun: function (sys, items, opts) { sys.example.on("ready", () => console.log("ready")); } }
示例中的使用方式是很是簡潔的,但咱們還漏了一點。當刷新完畢,數據返回後,還要告知組件對象給出刷新成功的提示而且返回初始狀態。好了,下面給出的是加入新接口的應用示例。app
// 06-01 Index: { xml: `<PullRefresh id='example'> <h1>Twitter</h1> <h2>Loren Brichter</h2> <button id='refresh'>click</button> </PullRefresh>`, fun: function (sys, items, opts) { sys.example.on("ready", () => { setTimeout(() => sys.example.trigger("complete"), 3000); }); } }
該示例經過定時器模擬了下拉刷新完成後給出刷新成功的提示而且返回初始狀態。框架
如今讓咱們把目光轉移到下拉刷新組件的內部,看看該如何去實現。觀察文章開始部分的大圖,很天然地咱們能夠將整個組件劃分爲三個子組件,以下面的 XML 文檔所示。svg
<div id="refresh"> <Status id="status"/> <div id="content"></div> </div>
但爲了方便控制,下面的佈局可能會好一些。其中組件 page 表明視口,它與其父級 refresh 有相同的寬高尺寸。另外,內容組件 content 與視口組件 page 也具備相同的寬高尺寸。未定義的狀態條組件 Status 的高度爲 40px
,這樣在初始狀態下,狀態條組件與內容組件須要向上便宜 40 個像素。函數
// 06-01 PullRefresh: { css: `#refresh { position: relative; height: 100%; cursor: pointer; overflow-y: hidden; } #page { height: 100%; transform: translateY(0); } #status, #content { transform: translateY(-40px); } #content { height: 100%; }`, xml: `<div id='refresh' xmlns:i='pullrefresh'> <div id='page'> <i:Status id='status'/> <div id='content'></div> </div> </div>`, map: { "appendTo": "content" } }
暫且放下 PullRefresh 組件,咱們先看看如何實現狀態指示條。狀態指示條用於顯示「下拉刷新」、「鬆開刷新」、「加載中...」以及「刷新成功」四個狀態提示,而且每一時刻僅顯示一個狀態。對於狀態的切換,這裏會先用到咱們下一章將講到的路由組件 ViewStack,這裏僅須要瞭解如何使用便可。組件 ViewStack 對外只顯示子級的一個子組件,同時偵聽一個 switch
事件,該事件的派發者攜帶了一個切換到的目標對象的名稱,也就是 ID。該組件根據這個 ID 來切換到目標視圖。下面是狀態條組件的完整實現。佈局
// 06-01 Status: { css: "#statusbar { height: 2.5em; line-height: 2.5em; text-align: center; }", xml: <ViewStack id="statusbar"> <span id="pull">Pull to refresh...</span> <span id="release">Release to refresh...</span> <span id="loading">Loading...</span> <span id="success">Loading success</span> </ViewStack>, fun: function (sys, items, opts) { var stat = "pull"; function getValue() { return stat; } function setValue(value) { sys.statusbar.trigger("switch", stat = value); } return Object.defineProperty({}, "value", { get: getValue, set: setValue }); } }
該組件提供一個 value 接口用戶設置與獲取組件的顯示狀態。父級組件可根據不一樣的時機調用該接口。動畫
如今讓咱們來考慮下拉刷新組件操做實現的具體細節。咱們須要考慮的事件主要有三個:stouchstart
、touchmove
以及 touchend
。下面是一個實現框架:spa
// 06-01 PullRefresh: { fun: function (sys, items, opts) { var startY, translateY; sys.page.on("touchstart", function(e) { // 1 記錄下當前觸點的座標以及 page 的偏移 // 2 偵聽 touchmove 和 touchend事件 }); function touchmove(e) { // 1 計算出垂直方向上的偏移 // 2 處理狀態條與內容內面跟隨觸點移動 // 3 根據觸點移動的距離顯示至關的狀態條內容 } function touchend(e) { // 1 移除 touchmove 和 touchend 事件 // 2 根據觸點移動的距離決定返回原始狀態或者進入刷新狀態並派發事件 } } }
如今咱們一個個地來實現上面的三個偵聽器。首先是 touchstart
偵聽器:
// 06-01 sys.page.on("touchstart", function (e) { startY = e.targetTouches[0].pageY; translateY = parseInt(sys.page.css("transform").match(/\d+/)[0]); sys.page.on("touchmove", touchmove).on("touchend", touchend).css("transition", ""); });
下拉刷新過程當中會涉及到動畫,對於動畫目前通常有兩種選擇,可使用 JQuery 動畫函數,也能夠是 css3,這須要看各人喜愛了。這裏咱們選擇使用 css3 來實現。如上所示在下拉開始時須要把動畫給禁用掉,不然會對後續形成干擾。
其次是 touchmove
偵聽器。該偵聽器必需判斷出偏移的正負值,當偏移爲正時才容許移動頁面。
// 06-01 function touchmove(e) { var offset = e.targetTouches[0].pageY - startY; if ( offset > 0 ) { sys.page.css("transform", "translateY(" + (offset + translateY) + "px)"); if (items.status.value != "loading") items.status.value = offset > 40 ? "release" : "pull"; } }
最後是 touchend
偵聽器。該處理器須要處理三種狀況。狀況一,若是狀態條處理等待數據返回狀態,則回彈頁面使狀態條還處於該狀態。狀況二,若是用戶下拉幅度未超過 40px,則回彈頁面使狀態條處於隱藏狀態。狀況三,若是用戶下拉幅度超過 40px,則派發一個 ready 事件,並切換狀態條至等待數據返回狀態。
// 06-01 function touchend(e) { var offset = e.changedTouches[0].pageY - startY; sys.page.off("touchmove").off("touchend").css("transition", "all 0.3s ease-in 0s"); if ( items.status.value == "release" ) { sys.page.css("transform", "translateY(40px)"); } else if ( offset < 40 ) { sys.page.css("transform", "translateY(0)"); } else { release(); } }
因爲狀況三的處理較複雜,因此獨立封裝成一個函數處理。請看下面的 release 函數。
// 06-01 function release() { items.status.value = "release"; sys.refresh.once("complete", () => { items.status.value = "message"; setTimeout(e => { sys.page.css("transform", "translateY(0)").once("webkitTransitionEnd", e => items.status.value = "pull"); }, 300); }); sys.page.css("transform", "translateY(40px)").trigger("ready"); }
此函數主要完成兩件事,其一是派發 ready 事件,提醒上級組件發送數據請求,其二是偵聽 complete 事件,一旦接收到來自上級派發的 complete 事件則顯示完成數據請求的提示並返回初始狀態。
上面咱們實現的狀態條是純文字的,這一節讓咱們把 加載中...
替換成一個動畫,從而給用戶帶來更好的體驗。下面實現的動畫組件 Release 包含一個旋轉的相似菊花同樣的東西,同時還包含文本。
// 06-02 Release: { css: `#loader { display: inline-block; position: relative; height: 2.5em; line-height: 2.5em; } #spinner { width: 1.2em; height: 1.2em; position: absolute; top: .7em; } #label { display: inline-block; font-size: 0.75em; margin: 0 0 0 2em; }`, xml: `<div id='loader'> <Spinner id='spinner'/><span id='label'/> </div>`, map: { appendTo: "label" } }, Spinner: { css: `#loader { width: 1.5em; height: 1.5em; animation: spin 1s linear infinite;... } @keyframes $spin { 0% {transform: rotate(0deg);} 100% {transform: rotate(360deg); } } @-webkit-keyframes $spin {0% {-webkit-transform: rotate(0deg);}... }`, xml: `<svg id='loader' width='48' height='48' viewBox='0 0 1024 1024'> <path d='M512.151961 3.978614l-0.308015 0c-21.655206 0-39.162952...'/> ... </svg>` }
你只須要在狀態條組件 Status 中把名爲 release 的組件替換成上面新實現的 Release,其他地方不用改,示例就能很好的工做了。
// 06-02 Status: { css: "#statusbar { height: 2.5em; line-height: 2.5em; text-align: center; }", xml: `<ViewStack id='statusbar'> <span id='pull'>Pull to refresh...</span> <span id='release'>Release to refresh...</span> <Release id='loading'>Loading...</Release> <span id='success'>Loading success</span> </ViewStack>`, fun: function (sys, items, opts) { var status = "pull"; function getValue() { return status; } function setValue(value) { sys.statusbar.trigger("switch", status = value); } return Object.defineProperty({}, "value", {get: getValue, set: setValue}); } }