1、一些基本概念javascript
一、HBuilder
css
按照個人理解,HBuilder是一個基於eclipse二次開發的IDE,主要對HTML5的開發作了許多優化,剛發佈沒多久,目前基本穩定,可是仍然有一些BUG。html
用HBuilder開發移動APP的好處是能夠鏈接手機實時調試APP(對於Android其實也就是用adb跟DDMS),鏈接手機後IDE的界面上會出現你所鏈接的手機,對項目中的文件修改回實時同步到手機上,下圖是ADT和HB的對比:java
二、HTML5 Plusnode
其實就是HBuildeBuild發團隊推出的一種框架,儘管官方說這是一個開放性的HTML5的擴展標準,可是從目前其提供的資源以及行爲來看,我跟人不太認同官方的說法。目前也只有HBuilder能打包基於HTML5+的項目,一樣地,HBuilder開發移動應用用的就是這個框架。jquery
目前市場上可見的、基於此框架作的應用有CSDN的新聞客戶端,基於此框架能夠調用許多手機的原生服務。如下分別是CSDN的客戶端及HBuilder提供的示例程序,HBuilder中有這兩個項目的源代碼:android
2、實驗目標(需求分析)web
一、目標算法
首先,是肯定一下用HB和H5+能不能作出原生手機應用的效果,其次是看看開發的複雜度。chrome
在此,我定下一個目標:仿照網易新聞的APP,用HB作一個新聞客戶端的雛形,重點是作出幾個手機客戶端常見的手勢操做以及一些動畫效果。
二、分析
下圖是對網易新聞客戶端使用過程的幾個截圖,也是此次要實現的內容:
①、首頁(主頁面),包含一個LOGO、一個菜單按鈕、一個用戶按鈕、一個導航欄、一個新聞標題列,標題列的頂部是一個圖片輪播欄;
②、在首頁中拖動頁面,頁面可漸漸縮放以呈現出菜單,點擊菜單按鈕也是一樣的效果;
③、在首頁左右拖動可在頭條、推薦、娛樂等導航間進行切換;
④、點擊新聞列表進入新聞內容頁,在內容頁中拖動頁面可返回。
第一步就是實現上面的內容,其中大部分手勢操做在許多原生應用中都是司空見慣的,可是對於H5應用來講仍是比較罕見,也沒有幾個案例能夠參考,所以就蠻力開擼了。
3、正文
一、大概思路
既然是基於HTML5的,那麼就必須發揮HTML5的優點。其一就是兼容性,一次開發便可在多個平臺上使用,這就意味着一樣須要在代碼中進行設備檢測,也要進行屏幕自適應,這些都是移動端網站用得比較多的手段了,也再也不詳述。官方也說了建議別用各類JS庫,那麼全部動畫效果、頁面佈局都須要本身來寫。
固然,必要的偷懶仍是得有的,用個jQuery,在不影響性能的地方使用也無妨。
按照上面的分析,目前要作的是兩個頁面:第一個頁面就是新聞列表頁,包含那個縮放菜單;第二個頁面是新聞內容的查看頁面。
看完HTML5+的文檔(雖然內容很少,可是該有的內容都有,仍是不錯的),大概明白了新聞列表頁中的全部效果都得基於HTML5來寫,也就是說,寫這個頁面能夠用PC瀏覽器來進行調試。
進入新聞內容頁的時候,能夠用H5+提供的窗體接口,此頁面中拖動返回之類的操做能夠利用修改窗體的位置來實現。
頁面中的圖標均可以在FontAwesome這個圖標字體裏邊找到,能夠省下一大筆時間。
那麼,接下來就是開擼了。
二、代碼結構
代碼列表以下:
index是新聞列表頁,view是新聞內容查看頁,pic是圖片瀏覽頁(點擊圖片會進到這個頁面,先放置);js目錄中,frame.js中放置一些公用代碼,main.js是處理index邏輯的代碼,main.XXX的幾個js文件則是頭條、推薦等導航的邏輯分管。
三、DOM操做及模板機制
從圖片輪播到新聞列表,均可以先作好一個模板而後給予模板動態生成,因此,首先,我寫了幾個模板:
<section data-node="tpl" style="display:none;"> <div class="content-imghead" data-node="imghead"> <div class="content-imghead-container" data-node="container"> </div> </div> <div class="content-imgtitle" data-node="imgtitle"> <div class="content-imgtitle-desc" data-node="desc"></div> <div class="content-imgtitle-words" data-node="words"></div> <div class="conntent-imgtitle-dot i1" data-node="i1"></div> <div class="conntent-imgtitle-dot i2" data-node="i2"></div> <div class="conntent-imgtitle-dot i3" data-node="i3"></div> <div class="conntent-imgtitle-dot i4" data-node="i4"></div> </div> <div class="content-horitem" data-node="horitem"> <img class="content-horitem-img" src="img/news_default_320_160.png" data-node="img"></img> <div class="content-horitem-title" data-node="title">新聞標題新聞標題新聞標題新聞標題新聞標題</div> <div class="content-horitem-desc" data-node="desc">副標題描述副標題描述副標題描述副標題描述</div> <div class="content-horitem-icon" data-node="icon"><i class="icon-facetime-video"></i></div> </div> </section>
上面的代碼中,從上而下分別是圖片輪播的輪播頁、圖片輪播的標題頁、新聞標題列表項的模板。HTML代碼中有data-node這個屬性,這是後面用來獲取DOM用的,data-node的相關JS代碼以下:
var F = window['F'] = { getNodes : function(obj){ var i; if(!obj.childNodes || obj.childNodes.length == 0){ return obj; } for(i = 0; i < obj.childNodes.length; i ++){ if(obj.childNodes[i].dataset && obj.childNodes[i].dataset.node){ obj[obj.childNodes[i].dataset.node] = F.getNodes(obj.childNodes[i]); } } return obj; } };
也就是一個簡易的DOM獲取的方式,經過這句代碼:
_doc = F.getNodes($('body')[0]);
能夠分層遍歷獲取body下全部帶了data-node的節點,譬如上面的模板,能夠經過_doc.tpl.imghead來取得圖片輪播的模板。
四、頁面
調界面的過程是痛苦的,持續三四個小時的眼鏡乾澀的調整過程不提,總而言之最終寫好的頁面長這樣:
仿真度也足夠了吧?在此感謝FontAwesome,否則畫圖標可能還要花上一兩個小時……
(其實作完界面以後我就下班了,那天是4.29,就快五一了,心情激動啊)
最終首頁的佈局以下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>...</title> <link rel="stylesheet" type="text/css" href="css/main.css"/> <link rel="stylesheet" type="text/css" href="css/font-awesome.css"/> <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> <script src="js/frame.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.top.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.recommend.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.entertainment.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.sport.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.finance.js" type="text/javascript" charset="utf-8"></script> <script src="js/main.js" type="text/javascript" charset="utf-8"></script> <style type="text/css"></style> </head> <body> <div class="side" data-node="side_left"> <div class="side-list" data-node="list"> <div class="side-list-item selected"><i class="icon-list-alt"></i>新聞</div> <div class="side-list-item"><i class="icon-rss"></i>訂閱</div> <div class="side-list-item"><i class="icon-picture"></i>圖片</div> <div class="side-list-item"><i class="icon-film"></i>視頻</div> <div class="side-list-item"><i class="icon-comments-alt"></i>跟帖</div> <div class="side-list-item"><i class="icon-headphones"></i>電臺</div> </div> </div> <div class="page" data-node="page_main"> <div class="touchcover" data-node="cover"></div> <div class="header" data-node="header"> <div class="header-btn-left" data-node="btn_left_menu"><i class="icon-reorder"></i></div> <div class="header-btn-right"><i class="icon-user"></i></div> </div> <div class="navbar" data-node="navbar"> <div class="navi selected" data-node="navi0">頭條</div> <div class="navi" data-node="navi1">推薦</div> <div class="navi" data-node="navi2">娛樂</div> <div class="navi" data-node="navi3">體育</div> <div class="navi" data-node="navi4">財經</div> <div class="naviselector" data-node="selector"></div> </div> <div class="naviswitch"><i class="icon-angle-down"></i></div> <div class="container" data-node="container"> <div class="content" data-node="content1"> </div> <div class="content" data-node="content2"> </div> <div class="content" data-node="content3"> </div> <div class="content" data-node="content4"> </div> <div class="content" data-node="content5"> </div> </div> </div> <section data-node="tpl" style="display:none;"> <div class="content-imghead" data-node="imghead"> <div class="content-imghead-container" data-node="container"> </div> </div> <div class="content-imgtitle" data-node="imgtitle"> <div class="content-imgtitle-desc" data-node="desc"></div> <div class="content-imgtitle-words" data-node="words"></div> <div class="conntent-imgtitle-dot i1" data-node="i1"></div> <div class="conntent-imgtitle-dot i2" data-node="i2"></div> <div class="conntent-imgtitle-dot i3" data-node="i3"></div> <div class="conntent-imgtitle-dot i4" data-node="i4"></div> </div> <div class="content-horitem" data-node="horitem"> <img class="content-horitem-img" src="img/news_default_320_160.png" data-node="img"></img> <div class="content-horitem-title" data-node="title">新聞標題新聞標題新聞標題新聞標題新聞標題</div> <div class="content-horitem-desc" data-node="desc">副標題描述副標題描述副標題描述副標題描述</div> <div class="content-horitem-icon" data-node="icon"><i class="icon-facetime-video"></i></div> </div> </section> </body> </html>
新聞內容頁的佈局以下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title></title> <link rel="stylesheet" type="text/css" href="css/view.css"/> <link rel="stylesheet" type="text/css" href="css/font-awesome.css"/> <script src="js/frame.js" type="text/javascript" charset="utf-8"></script> <script src="js/jquery.js" type="text/javascript" charset="utf-8"></script> <script src="js/view.js" type="text/javascript" charset="utf-8"></script> <style type="text/css"></style> </head> <body> <div class="page" data-node="page_view"> <div class="header" data-node="header"> <div class="header-btn-left" data-node="btn_back"> <i class="icon-angle-left"></i> </div> <div class="header-tip" data-node="tip">1245</div> </div> <div class="content" data-node="content"> <h3 class="title">新聞標題</h3> <p class="note">副標題 02-02 00:00</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> <p>新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容新聞內容</p> </div> <div class="footer" data-node="footer"> <input type="text" class="footer-reply" placeholder="寫跟帖" data-node="input" /> <div class="footer-btn-favor" data-node="btn_favor"><i class="icon-star-empty"></i></div> <div class="footer-btn-share" data-node="btn_share"><i class="icon-share"></i></div> </div> </div> </body> </html>
五、屏幕自適應
作這個的時候,手頭上的測試機有:iPhone 五、Note 三、MI 三、MI 2S
其實移動端的頁面自適應,最關鍵的仍是devicePixelRatio這個值,看iPhone4的分辨率比3GS高了四倍,其實對於網頁來講仍是同樣的分辨率,看Note 3的1080P多高,其實在頁面上比起腎果來也多不了多少……
所以,我可恥地用了最省事的自適應方法:在頁面加載完後一次insertRule來修改樣式!
這樣作的好處是省事省資源(生下來的資源能夠作不少事啊……),壞處是面對屏幕翻轉、頁面尺寸更改等事件會表現得一塌糊塗。
首先,我用了這麼一坨代碼來獲取頁面的尺寸屬性以及設定一些屬性
G = {}; // 屏幕寬度 G.WIDTH = $(window).width(), // 屏幕高度 G.HEIGHT = $(window).height(), // 首頁滑到這個位置則斷定用戶要打開左側菜單 G.LIMIT_SLIDE_PAGESIDE = G.WIDTH / 4; // 菜單打開時,首頁滑到這個位置則認爲用戶要關閉菜單 G.LIMIT_SLIDE_PAGEBACK = G.WIDTH / 4 * 3; // 菜單打開時首頁頁面滑到這個位置 G.SITE_X_PAGESIDE = G.WIDTH / 3 * 2; // 頁面縮放最小值 G.SCALE_MAX_PAGESIDE = 0.2; // 菜單打開時頁面縮放的比率 G.SCALE_PAGESIDE = 1 - G.SITE_X_PAGESIDE / G.WIDTH * G.SCALE_MAX_PAGESIDE; // 菜單滑動的動畫時間 G.TIME_SLIDE_ANIMATE = 200; // 頁面導航切換的動畫時間 G.TIME_SPAGE_ANIMATE = 200; // 菜單打開時,菜單的頂部對齊縮小後的頁面頂部 G.SITE_SCALE_BOTTOM = (G.HEIGHT - G.SCALE_PAGESIDE * G.HEIGHT) / 2;
而後,我又用了這麼個方式來讓一些頁面元素自適應:
style = document.styleSheets[document.styleSheets.length - 1]; style.insertRule('.container{height:' + (G.HEIGHT - 80) + 'px;}', 0); style.insertRule('.content{height:' + (G.HEIGHT - 80) + 'px; width:' + G.WIDTH + 'px;}', 0); style.insertRule('.content-imghead{height:' + (G.WIDTH / 2 - 1) + 'px; width:' + G.WIDTH + 'px;}', 0); style.insertRule('.content-imghead-img{height:' + (G.WIDTH / 2 - 1) + 'px; width:' + G.WIDTH + 'px;}', 0); style.insertRule('.content-imghead-container{height:' + (G.WIDTH / 2 - 1) + 'px;}', 0); style.insertRule('.content-horitem{width:' + (G.WIDTH - 24) + 'px;}', 0);
CSS的代碼很枯燥,在此就不提了。
六、手勢動畫
這實際上是APP最重要的部分,手勢的合理與否很大程度上決定了應用的生死(這是一個伏筆)。
左右滑動能夠經過設置style.left的值、能夠用translateX來實現,縮放效果則只能用transform的scale函數來實現(這也是一個伏筆)。
我定義了一堆變量用來存放動畫過程的一些參數:
// 運行時參數 R = {}; R.isIOS = navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) || navigator.userAgent.match(/iPad/i); R.PAGE_S = 'main', R.container = _doc.page_main.container; R.navIndex = 0; R.navCount = 5; R.animating = false; R.scaleRate = 1; // 頁面從哪裏開始滑 R.fromX = 0; // 頁面滑動的步進 R.step_t = 64; // 頁面縮放的步進 R.step_s = (1 - G.SCALE_PAGESIDE) / G.SITE_X_PAGESIDE * 64;
其中,isIOS這個是直接抄CSDN的APP的,在寫完這個以後我還作了對chrome、ASOP自帶瀏覽器等的判斷,最終仍是刪掉了。
在滑動頁面開關菜單的過程當中,最重要的是保證動做的合理與流暢,譬如用戶滑動到必定程度後鬆手時,頁面是回彈(操做取消)仍是自動滑到另外一邊(操做生效)、自動滑動的過程當中滑動速率跟用戶手動滑動的部分速率一致、動畫效果不卡幀等。
所以,我在新聞列表頁中定義了五套touch事件處理函數來分別處理不一樣的操做,分別是:
①、沒有任何操做時(路由)
②、動做多是滑到左側列表菜單
③、左側菜單打開時的響應
④、多是左側菜單滑回頁面
⑤、多是 主頁橫向左側翻頁
⑥、 主頁橫向右側翻頁
斷定邏輯以下:
T.on(_doc, 'start', function(e){ var sx, sy, cx, cy, rt, fx, ft, step_t, step_s, an = false; switch(e.touches.length){ // 觸摸事件 // 單點手勢 case 1: sx = e.touches[0].clientX; sy = e.touches[0].clientY; switch(R.PAGE_S){ // 在主屏幕中,判斷是否切換到菜單 case 'main': T.on(_doc.page_main, 'move' , m0); T.on(_doc.page_main, 'end' , e0); T.on(_doc.page_main, 'calcel', c0); break; // 在左側菜單中,判斷是否切換回主屏幕 case 'side-l': T.on(_doc.page_main.cover, 'move' , m2); T.on(_doc.page_main.cover, 'end' , e2); T.on(_doc.page_main.cover, 'calcel', c2); break; } break; default: break; } /* ... */ }
(由於是第一次用H5作APP,對其不熟悉,也許其中有很多多餘的部分)
T.on是另外封裝的一個兼容函數,相關代碼以下:
var T = window['T'] = { on : function(dom, type, func){ if(("ontouch" + type) in dom){ dom["ontouch" + type] = func; }else{ dom.addEventListener("touch" + type, func); } }, off : function(dom, type, func){ if(("ontouch" + type) in dom){ dom["ontouch" + type] = null; }else{ dom.removeEventListener("touch" + type, func); } }, offNormal : function(dom, func1, func2, func3){ if('ontouchstart' in dom){ dom.ontouchmove = null; dom.ontouchcancel = null; dom.ontouchend = null; }else{ dom.removeEventListener('touchmove', func1); dom.removeEventListener('touchend', func2); dom.removeEventListener('touchcancel', func3); } } };
圖片輪播則用了另外的一套touch事件,綁定在imghead上。
提及拖動的原理,在個人理解中,關鍵就是兩個座標:開始拖動時的橫座標SX、當前拖動位置的橫座標CX,拖動距離就是CX - SX,那麼只須要把頁面的位移設置爲CX - SX就完事了。
當觸摸事件結束以後,該如何自動完成剩下的動畫?最初,我是直接用了CSS3的transition屬性,當觸摸結束後,給元素加一個transition:linear;之類的樣式,動畫效果結束後再去掉這個樣式,後來,我發現這個樣式在android平臺下存在一個略微嚴重的問題:卡。
也不知道ASOP自帶的瀏覽器是怎麼回事,也不知道爲何HBuilder在打包的時候要封裝Android的自帶瀏覽器,這個瀏覽器的性能真的很糟糕,不管多好的手機,只要用這個瀏覽器,其表現跟若干年前的單核手機沒什麼區別。
其實說到這,這篇文章已經能夠結束了,事實上,作到這,我已經沒有什麼激情再把這個應用原型寫下去了,由於我預感目前在Android上H5+是玩不下去了。
最後我用了這麼一個方式來兼顧動畫效果:
function menuSlideIn(){ if(R.animating){ return; } R.PAGE_S = 'side-l'; if(R.isIOS){ _menuSlideIn(); }else{ $(_doc.page_main.cover).show(); _doc.page_main.style.left = G.SITE_X_PAGESIDE + 'px'; } } function _menuSlideIn(){ R.animating = true; R.scaleRate -= R.step_s; R.fromX += R.step_t; _doc.page_main.style.left = R.fromX + 'px'; _doc.page_main.style.webkitTransform = 'scale(' + R.scaleRate + ', ' + R.scaleRate + ')'; if(R.fromX >= G.SITE_X_PAGESIDE){ $(_doc.page_main.cover).show(); R.animating = false; _doc.page_main.style.left = G.SITE_X_PAGESIDE + 'px'; _doc.page_main.style.webkitTransform = 'scale(' + G.SCALE_PAGESIDE + ', ' + G.SCALE_PAGESIDE + ')'; }else{ requestAnimationFrame(_menuSlideIn); } }
對於左側的菜單,在非IOS上禁用縮放效果。甚至在用戶拖動完畢後不顯示動畫效果而直接讓菜單閃到那個位置,卡幀的動畫還不如不播,所以我乾脆用了requestAnimationFrame這個Android上沒有的函數。
最後是新聞內容頁,用戶點擊新聞時,調用這麼個方法打開新頁面:
plus.ui.createWindow("view.html").show('slide-in-right', 200);
萬幸,H5+自帶了一系列的窗口打開、關閉動畫效果。
由於這個頁面是在新窗口的,所以要實現拖動返回的話,就再也不是設置DOM的style.left屬性了,而是調用這麼個方法:
W.setOption({left : ox});
來設置窗體的位置。
相應地,設置窗體位置就要換一種算法了,由於窗體移動後,每次檢測到的X位置所相對的位置都改變了。
假設頁面一開始的偏移是OX = 0,用戶觸摸屏幕的位置是SX,用戶第一次移動時的位置是CX,那麼,把OX設置爲CX - SX,而後把頁面的位置設置爲OX;
第二次以及以後的移動事件中,OX = OX + CX - SX,而後把頁面的偏移設置爲OX;
結束觸摸以後,假如當前頁面的偏移達到了返回的斷定點,那麼調用:
plus.ui.closeWindow(W, 'slide-out-right', 200);
關掉這個窗口,不然讓頁面動畫返回左邊緣。
在IOS上,以上的動畫效果很流暢完美地實現了,可是,不知道是否是個人使用方式不對,在Android下這段代碼的表現是悲劇的。
在拖動過程當中,頁面不斷地閃動,拖着拖着,頁面就消失了!
因此,這段代碼,我是這麼寫的:
function m1(e){ cx = e.touches[0].clientX; cy = e.touches[0].clientY; _doc.page_view.header.tip.innerHTML = cx; if(R.isIOS){ if(lx !== null){ ox = ox + cx - sx; W.setOption({ left : ox }); }else{ ox = cx - sx; W.setOption({ left : ox }); } } }
是的,對於非IOS的系統,我直接取消掉了這個效果。
至此,文章真的要結束了,由於在Android上,這個應用雛形的用戶體驗已經低到了極致。
4、總結
實驗結果是:H5+能夠作APP,不太能作APK。
我衷心但願這篇文章裏出現的Android下的糟糕表現是個人技術及實現思路太糟糕而形成的,由於我真心渴望能夠直接用HTML5寫出媲美原生的應用的那一天的到來。
也許代碼中還有不少的能夠優化的地方,也許進行極致的優化以後應用能夠運行得比較流暢,但這倒是不是我所想看到的,須要作得這麼完美才能實用的技術,是沒法推廣的。
對這個半成品都算不上的應用感興趣的話,能夠移步http://www.lxrmido.com/webcv2/看看實際效果
對代碼感興趣的同窗能夠把svn://www.lxrmido.com/webcv2這個目錄checkout下來看看,但願對你有所幫助
最後的最後,但願你們能夠告訴我是否是真的哪裏想歪了,總感受HBuilder不該該那麼脆弱……