摘要:第步實現(xiàn)內(nèi)容列表跳轉(zhuǎn)至索引字符到這里其實索引導(dǎo)航欄組件的開發(fā)已經(jīng)結(jié)束,不過畢竟看不到效果嘛,所以就實現(xiàn)了簡單的內(nèi)容列表組件,從而可以對導(dǎo)航欄組件進(jìn)行測試。
先來看下實現(xiàn)后的效果:
鏈接:在線DEMO,源代碼
這個索引導(dǎo)航欄的效果在很多 APP 中都有應(yīng)用,我也是參考了一些 APP 的效果進(jìn)行實現(xiàn)。
不過之前接觸移動端頁面開發(fā)較少,所以是邊學(xué)邊做,也就把這個過程中的一些東西整理記錄下來。
設(shè)計這個功能的基本需求可以總結(jié)為一句話:手指在導(dǎo)航欄(也就是 DEMO 上頁面右側(cè)的包含字母的豎條)拖動時,根據(jù)當(dāng)前手指位置,頁面主體內(nèi)容列表跳轉(zhuǎn)到對應(yīng)字母的內(nèi)容項。
當(dāng)然,延伸開來,可以是對于已經(jīng)排序的列表,導(dǎo)航欄顯示對應(yīng)的索引字符列表,支持快速跳轉(zhuǎn)到對應(yīng)的索引位置。
這里主要介紹導(dǎo)航欄的實現(xiàn),只看導(dǎo)航欄的話,其實要實現(xiàn)的東西比較簡單,只需要在手指移動時獲取對應(yīng)的字母即可。頁面主體內(nèi)容列表的跳轉(zhuǎn)應(yīng)該交由另一個列表組件實現(xiàn)。
在程序代碼中,組合導(dǎo)航欄和內(nèi)容列表兩個組件,導(dǎo)航欄索引字母更新時,內(nèi)容列表跳轉(zhuǎn)到對應(yīng)的位置。
結(jié)合 DEMO,整體的實現(xiàn)邏輯為:
// 創(chuàng)建一個內(nèi)容列表組件 var itemList = new ItemList(data) // 創(chuàng)建一個索引導(dǎo)航組件 var indexSidebar = new IndexSidebar() // 組合兩個組件實現(xiàn)功能 // 監(jiān)聽索引導(dǎo)航組件,一旦索引字符更新,內(nèi)容列表跳轉(zhuǎn)至對應(yīng)的索引字符 indexSidebar.on("charChange", function (ch) { itemList.gotoChar(ch) })
接下來,我們一步步實現(xiàn)。
第 1 步:創(chuàng)建 IndexSidebar “類”我選擇采用實例化“類”的方式來創(chuàng)建新的組件對象,定義“類”,其實就是創(chuàng)建一個構(gòu)造函數(shù)(當(dāng)然,采用 ES6 語法會更清晰,不過考慮兼容性這里不使用):
function IndexSidebar(options) { // TODO 處理 options this.initialize(options) } IndexSidebar.prototype.initialize = function (options) { // TODO 初始化 }
這里借鑒 Backbone 的模式,將組件初始化的邏輯多帶帶寫在一個 initialize() 方法中,當(dāng)然邏輯也可以都寫在構(gòu)造函數(shù)中。
在實現(xiàn)具體的功能前,我們可以先讓前面設(shè)計的代碼跑起來,首先補(bǔ)全導(dǎo)航組件的接口方法,支持監(jiān)聽事件:
// 特定事件觸發(fā)時,調(diào)用傳入的回調(diào)函數(shù)并傳入事件數(shù)據(jù) IndexSidebar.prototype.on = function (event, callback) { // TODO 實現(xiàn)事件監(jiān)聽 }
這里選擇采用事件模式(或者說觀察者模式吧),這樣可以有多個“觀察者”,為了完整,同樣借鑒已有的模式實現(xiàn),我們補(bǔ)全其他會用到的事件接口方法:
// 觸發(fā)特定事件,并給出事件數(shù)據(jù)供監(jiān)聽的回調(diào)函數(shù)使用 IndexSidebar.prototype.trigger = function (event, data) { // TODO } // 解除事件監(jiān)聽 IndexSidebar.prototype.off = function (event, callback) { // TODO }
接著來搭個列表組件的架子,同樣是類的模式,不過簡單點(diǎn),畢竟主要是為了實現(xiàn)索引導(dǎo)航欄組件,列表組件只是輔助:
// 內(nèi)容列表組件 function ItemList(data) { return { gotoChar: function (ch) { // TODO 實現(xiàn)按索引字符跳轉(zhuǎn)功能 } } }
這里偷懶了,雖然兼容 new ItemList(data) 的用法,但其實并沒有按照“類”的模式實現(xiàn)。
好了,有了上面的這些代碼,前面的設(shè)計應(yīng)該可以運(yùn)行了....雖然現(xiàn)在沒什么用。
第 2 步:實現(xiàn)手指拖動更新索引字母我們首先解決導(dǎo)航組件最重要的交互功能,也就是手指拖動的動作處理。由于之前沒做過觸摸的功能,我只好先查下相關(guān)的事件用法(當(dāng)然,盡管沒用過,還是知道有相關(guān)的事件):
Touch events - MDN
touchstart - MDN
touchmove - MDN
touchend - MDN
看了上面這些文檔,我發(fā)現(xiàn) touch 相關(guān)的事件還有個特殊的事件數(shù)據(jù),對應(yīng)的是手指觸摸屏幕的位置:Touch - MDN,顯然這個數(shù)據(jù)是會用到的。
之前做 PC 頁面的時候,也做過類似的鼠標(biāo)拖動的處理,使用到的瀏覽器事件主要是:mousedown, mousemove, mouseup。大致的處理邏輯是:
鼠標(biāo)按下(mousedown)時,記錄拖動開始
鼠標(biāo)移動(mousemove)時,如果拖動開始,則根據(jù)鼠標(biāo)位置更新并計算相關(guān)數(shù)據(jù)
鼠標(biāo)松開(mouseup)時,記錄拖動結(jié)束
這個邏輯也可以用在手指觸摸的拖動上。注意一個小細(xì)節(jié),手指在屏幕上觸摸時,可能同時有多個位置,所以觸摸事件的位置相關(guān)數(shù)據(jù)是一個列表:TouchList - MDN。不過我這里不關(guān)心,只取列表中的第一個位置數(shù)據(jù)使用。
這一部分的代碼邏輯實現(xiàn)為:
IndexSidebar.prototype.initEvents = function (options) { var el = this.el // el 對應(yīng)導(dǎo)航欄容器元素,初始化過程略 var touching = false el.addEventListener("touchstart", function (e) { if (!touching) { // 取消缺省行為,否則在 iOS 環(huán)境中會出現(xiàn)頁面上下抖動 e.preventDefault() var t = e.touches[0] start(t.clientX, t.clientY) } }, false) // 拖動過程中手指可能會移出導(dǎo)航欄,所以是在 document 上監(jiān)聽 // 不過貌似在 el 上監(jiān)聽也可以,這個暫不討論了 // 后面的 touchend 也是類似的緣故 document.addEventListener("touchmove", function handler(e) { if (touching) { e.preventDefault() var t = e.touches[0] move(t.clientX, t.clientY) } }, false) document.addEventListener("touchend", function (e) { if (touching) { e.preventDefault() end() } }, false) // TODO 實現(xiàn)索引字符的更新 function start(clientX, clientY) {} function move(clientX, clientY) {} function end() {} }
之所以抽出 start(), move(), end() 三個函數(shù),是為了在 PC 瀏覽器器上支持鼠標(biāo)的拖動,這樣監(jiān)聽鼠標(biāo)拖動相關(guān)事件時,也能使用這里的邏輯。
怎么計算手指觸摸位置的字符呢?這個我想大家應(yīng)該都能想到,我這里采用的是比較笨的方法,就是根據(jù)觸摸位置計算索引導(dǎo)航欄中的距離最近的字符,大致過程為:
已知手指相對屏幕(其實是視口,這里不區(qū)分了)位置(clientX, clientY)和索引字符數(shù)組(chars)
獲取索引導(dǎo)航組件距屏幕頂部的距離(boxClientTop)和自身的高度(boxHeight)
計算得到手指位置在組件內(nèi)部的相對高度(offsetY):offsetY = clientY - boxClientTop
根據(jù)手指位置的相對高度與組件高度的比例,從索引字符數(shù)組中取出對應(yīng)位置的字符(略,這個不難算)
這里就不貼代碼了,都是一些瑣碎的計算,還要額外考慮手指位置在豎直方向上超出導(dǎo)航欄范圍的情況。
經(jīng)過以上計算,可以得到一個索引字符 ch,接下來要做的就是通知“觀察者”們,字符更新了(如果和上一個索引字符不同的話):
this.trigger("charChange", ch)第 3 步:實現(xiàn)組件事件接口
這個其實可以不必多寫,類似的實現(xiàn)有很多。不過為了不依賴其他庫,我選擇自己實現(xiàn)。我就直接貼自己實現(xiàn)的版本了:
/* Event Emitter API */ IndexSidebar.prototype.trigger = function (event, data) { var listeners = this._listeners && this._listeners[event] if (listeners) { listeners.forEach(function (listener) { listener(data) }) } } IndexSidebar.prototype.on = function (event, callback) { this._listeners = this._listeners || {} var listeners = this._listeners[event] || (this._listeners[event] = []) listeners.push(callback) } IndexSidebar.prototype.off = function (event, callback) { var listeners = this._listeners && this._listeners[event] if (listeners) { var i = listeners.indexOf(callback) if (i > -1) { listeners.splice(i, 1) if (listeners.length === 0) { this._listeners[event] = null } } } }
使用對象屬性 _listeners 來記錄事件監(jiān)聽函數(shù),當(dāng)然這里可以只實現(xiàn)成單個數(shù)組,不必搞得這么復(fù)雜。不過為了可能的組件擴(kuò)展的需要,還是這么實現(xiàn)了,這樣如果還需要支持其他類型的事件,例如對外暴露觸摸開始事件“touchStarted”,事件接口這里就不需要修改了。
第 4 步:實現(xiàn)內(nèi)容列表跳轉(zhuǎn)至索引字符到這里其實索引導(dǎo)航欄組件的開發(fā)已經(jīng)結(jié)束,不過畢竟看不到效果嘛,所以就實現(xiàn)了簡單的內(nèi)容列表組件,從而可以對導(dǎo)航欄組件進(jìn)行測試。
內(nèi)容列表組件在創(chuàng)建時,傳入了數(shù)據(jù),根據(jù)這些數(shù)據(jù)渲染出列表,并且在渲染的過程中記錄索引,從而在輸出的 HTML 結(jié)構(gòu)上做出標(biāo)記,以便查找并跳轉(zhuǎn):
// 內(nèi)容列表組件 function ItemList(data) { var list = [] var map = {} var html html = data.map(function (item) { // 數(shù)組中每項為 "Angola 安哥拉" 的形式,且已排序 var i = item.lastIndexOf(" ") var en = item.slice(0, i) var cn = item.slice(i + 1) var ch = en[0] if (map[ch]) { return "
由于已在 HTML 結(jié)構(gòu)上標(biāo)記了索引字符,所以 gotoChar 的邏輯其實就是找?guī)в袠?biāo)記的元素,然后讓其移動滾動到組件頂部顯示:
return { gotoChar: function (ch) { if (ch === "*") { // 滾動至頂部 elItemList.scrollTop = 0 } else if (ch === "#") { // 滾動至底部 elItemList.scrollTop = elItemList.scrollHeight } else { // 滾動至特定索引字符處 var target = elItemList.querySelector("[data-ch="" + ch + ""]") if (target) { target.scrollIntoView() } } } }
OK,以上就是所有的邏輯了。
第 5 步:完善索引導(dǎo)航組件其實基本功能已經(jīng)實現(xiàn),不過既然是想作為開源組件發(fā)布,還是再“包裝”下,主要做了以下幾方面的完善:
支持根據(jù)屏幕高度調(diào)整導(dǎo)航欄的高度
計算屏幕高度,和組件距離屏幕頂部和底部的距離,將索引字符平均分布。
支持組件配置選項,并提供缺省選項
由于不想依賴其他庫,且考慮兼容性(不能使用 Object.assign),所以自己實現(xiàn)了:
var defaultOptions = { chars: "*ABCDEFGHIJKLMNOPQRSTUVWXYZ#", isAdjust: true, // 是否需要自動調(diào)整導(dǎo)航欄高度 offsetTop: 70, offsetBottom: 10, lineScale: 0.7, charOffsetX: 80, charOffsetY: 20 } function IndexSidebar(options) { options = options || {} // 遍歷缺省選項逐一處理 for (var k in defaultOptions) { if (defaultOptions.hasOwnProperty(k)) { // 未給出選項值時使用缺省選項值 options[k] = options[k] || defaultOptions[k] } } this.options = options this.initialize(options) }
支持不同的方式引用組件
這個和一般的模塊差不多,不過額外支持了一下 SeaJS(define.cmd):
(function (factory) { if (typeof module === "object" && module.export) { module.export = factory() } else if (typeof define === "function" && (define.amd || define.cmd)) { define([], factory) } else if (typeof window !== "undefined") { window.IndexSidebar = factory() } })(function () { // ... return IndexSidebar })總結(jié)
從看到這個需求,到查文檔、設(shè)計、實現(xiàn),以及作為開源工具發(fā)布,用了大概不到 1 天的時間。希望可以有同學(xué)能夠從我的這個過程中收獲一些東西吧。
當(dāng)然,也歡迎提出意見、建議,更歡迎參與完善這個組件:
https://github.com/luobotang/...
最后,特別歡迎使用:
npm i index-sidebar
感謝閱讀!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/91240.html
摘要:緊接著就是導(dǎo)航欄的特效編寫,殊不知,就是這個效果,讓原本計劃上午完成的事情,愣是被我研究了大半天才解決。剛開始我的布局是,導(dǎo)航欄是一個,下面有八個,分別是八個欄目。 showImg(https://segmentfault.com/img/bVYUar?w=720&h=537); 前言 今天這篇文章的標(biāo)題,顯然是要搞事情。一個JS交互效果,居然花費(fèi)了一天的寶貴時間才研究出來,我是不是不...
摘要:在文件的標(biāo)簽中加上以下代碼新手上路注冊登陸上面只是引用了一些簡單的的,也沒什么難的,不用傷心。 Laravel身為最優(yōu)雅的PHP框架,很多學(xué)習(xí)PHP的小伙伴造就對Laravel垂涎欲滴。今天就來實現(xiàn)你的愿望,讓我們一起從零開始,利用Laravel實現(xiàn)Web應(yīng)用最常見的注冊和登錄功能!所有的課程源碼已放在Github上:laravel-start. Race Start ! 首先我們來...
摘要:開始使用郵箱配置好了之后,我們就可以開始使用了,首先我們來修改一下我們的導(dǎo)航欄,因為我們想實現(xiàn)的就是我們常常看到的在導(dǎo)航欄的右側(cè)的注冊和登錄按鈕。 原文來自: https://jellybool.com/post/programming-with-yii2-integrating-user-regi... 本來打算昨晚寫的這篇教程,但是忙著約會去了,所以現(xiàn)在補(bǔ)上吧。 上一篇...
閱讀 3342·2021-11-19 11:36
閱讀 2947·2021-09-27 13:34
閱讀 2007·2021-09-22 15:17
閱讀 2415·2019-08-30 13:49
閱讀 767·2019-08-26 13:58
閱讀 1366·2019-08-26 10:47
閱讀 2548·2019-08-23 18:05
閱讀 609·2019-08-23 14:25