# 移動端開發 ## 移動端測試 > - Chrome > - 模擬器(夜神) > - 真的用手機測試 > - 內網服務器(手機跟電腦必須連在同一個網路) > - 電腦開一個服務器端口 > - 手機打電腦的 IP:端口 > - 查IP指令 `$ ifconfig` > - `en0.inet 192.168.xx.xx` > - 手機打開瀏覽器輸入 `192.168.xx.xx:port` > - http://linux.vbird.org/linux_server/0140networkcommand.php > - 外網服務器 > - 服務器配置環境-Linux(centos 比較常見) > - `yum` `mysql` `httpd` `iptable` ... > - Code 同步 > - `ftp` `ssh` > - 維護 > - dev-server + webpack > - [browser-sync](https://www.browsersync.io/)(內網服務器) > - 安裝 > - 首先, 因為是裝到全局, > 如果還沒解決安裝權限問題(`permission denied`)要先搞定 > - 接著還要搞定 XCODE 套件問題 > - 細節寫在前端其他的筆記上 > - 都弄好就可以安裝了 `$ npm install browser-sync -g` > - 使用: 在想要開啟的文件同個目錄下執行 > - 初始化: > - `browser-sync init` , 會產生一個 `vi bs-config.js` > - 進去設定檔把 `file` 跟 `server` 改成 true > - 開啟服務器 `browser-sync start -server` > - 端口預設: > - `3001`: 控制各種UI設定 > - `3000`: 當前目錄 > - 效果: > - 所有打開 `3000` 的頁面是同步的, 也就是每個客戶端都在操控同一個頁面 > - 如果還有問題, 那可以考慮: > - 防火牆: 主機跟路由器都有可能 > - IP 地址問題 ```shell $ # 總結: 如果以後沒用過那些鳥毛問題 $ # 1. 搞定存取權限 $ sudo chown -R $(whoami) $(npm config get prefix)/{lib/node_modules,bin,share} $ # 2. 搞定 XCODE 套件 $ sudo rm -rf /Library/Developer/CommandLineTools $ xcode-select --install $ # 3. 安裝 $ npm install browser-sync -g $ # 4. 拿到初始化設定檔 $ browser-sync init Config file created bs-config.js ... $ # 5. 改設定檔 $ vi bs-config.js module.exports = { "ui": { "port": 3001 # UI 控制端口 }, "files": true, # 改, 才能開檔案 ..., "server": true, # 改, 才能開服務器 "proxy": false, "port": 3000, # 服務器當前目錄端口 ..., $ # 6. 開啟服務器 $ browser-sync start -server [Browsersync] Access URLs: -------------------------------------- Local: http://localhost:3000 External: http://192.xxx.xx.xx:3000 # 瀏覽器們(手機也是)開這端口就能玩了 -------------------------------------- UI: http://localhost:3001 UI External: http://localhost:3001 -------------------------------------- ``` ## 移動佈局 ### viewport > - `width`: 用多少寬度的裝置來解析這個頁面 > - `device-width` : 根據裝置的寬度來解析 > - `initial-scale` : 縮放大小, 預設不縮放 > - 有些網站會動態設置scale, 來取代 rem > - 假設基準螢幕寬為 300px, 偵測螢幕寬為 400px, > 用JS取偵測寬度後除以300=1.3333, 然後丟到 scale 的倍數上, 以維持畫面 > 不過用 rem 的人應該比較多, 因為 scale 有些不支援, 有些顯示的又很奇怪 > - `user-scalable` : 可否縮放 > - 為了避免有些裝置不支持這個屬性, > 通常會寫最大/小縮放倍數寫成 1.0 來表示不縮放 > - `maximum-scale` : 最大縮放倍數 > - `minimum-scale` :最小縮放倍數 > - 有些網站會為了避免瀏覽器不支援限制縮放的功能, > 只要偵測到縮放, 頁面就跟著修改 `initial-scale` 的倍數 > - 例如偵測放大2.0倍, `initial-scale` 就調成 0.5, 達成完全限制縮放的效果 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no,maximum-scale=1.0,minimum-scale=1.0'> </head> <body> <input type='button' value='按鈕'/> </body> ``` ### `border-box` 盒模型 `box-sizing: border-box` > - 普通盒子大小: width + padding + border > - border-box: width 設定多大就多大, width的值包含了 padding 與 border > - 需求: 五個 20% 大的盒子排一排 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; list-style: none; } ul li { float: left; width: 20%; height: 200px; border: 1px solid black; /* 這麼一加就直接掉下來了 (20%+2) * 5 > 100% */ box-sizing: border-box; /* width 多大就多大, border 跟 padding 會往內擠 */ } </style> </head> <body> <ul> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> </body> ``` ### flex > - 自帶 border-box 特性 > - 對 border padding margin 都吃 > - 常與 max-width min-width 配合 > - flex 其實就是平分父級內容的寬度 ```htmlmixed= <head> <meta charset='utf-8'> <style> * { margin: 0; padding: 0; list-style: none; } ul { display: flex; } ul li { flex:1; border: 3px solid black; padding: 5px; margin: 5px; } /* 中盒固定寬, 左右自適應 */ ul li.main { max-width: 500px; min-width: 500px; } </style> </head> <body> <ul> <li>1</li> <li class='main'>2</li> <li>3</li> </ul> </body> ``` ### rem > - 這是一種單位: px, em, ... > - `px` : 絕對的大小 > - `vh` `vw` : 根據螢幕的寬高來定 > - `vh` 好處: 有時候高度 % 不支援而支援 `vh`, 此時就可以考慮使用來寫 `vh` > - `em` : 相對的大小, 相對於自身字體大小 > - `rem` : 相對的大小, 相對於 root 元素 (HTML) 的字體大小 > - 每種解析度的大小只需要調整 HTML 的字體大小即可, 使用效率高很多 > 否則為了解決每種解析度, 導致每個大小都要算一遍, 有夠麻煩 > - 結論: 如果可以, 一切尺寸都用 rem 或 % , px 就少用了 > - Q. rem 與響應式的不同 > - 所謂響應式, 主要是讓不同解析度下長得不一樣 > - rem 重點在於不同解析度下, 顯示相同 ```htmlmixed= <head> <meta charset='utf-8'> <style> html { font-size: 10px; } div { height: 3rem; width: 4rem; border: 1rem solid black; } </style> </head> <body> <div>123</div> </body> ``` > - 使用方法: > - 必須先決定基準寬度 > - 必須決定基準字體大小 > - 最好選好除的值, 否則你會除到發瘋 > - 但也別太小, 通常習慣 20 50 之類的好除又大 > - rem 計算方法: > - `1rem` = 螢幕寬度 與 HTML-Fontsize 的比例關系 > - 假設 `螢幕 320px` `font-size = 16px` => `1rem` = `20px` > 假設我要做螢幕 `1024px` 的同比寬度的話, fontSize? > - 1024/20 = 51.2px > - 基準螢幕/基準字體大小 = `document.documentElement.clientWidth` / 字體大小 > 字體大小 = `document.documentElement.clientWidth` * 基準螢幕 / 基準字體 ### touch 事件 > - `touchstart` `touchmove` `touchend` > - 與滑鼠的 `mousedown` `mousemove` `mouseup` 對應 > - 區別在於滑鼠只有一個, touch 有多點觸控 > - 微軟有嘗試推出一個 `pointer` 事件, 想把這兩個東西結合 > #### `ev.targetTouches` > - 目標物體上的手指的偽陣列, 裡面裝各種觸控資訊 > 同時觸碰`該元素`幾隻手指就有幾筆資料 > - vs `ev.touches` : > - 螢幕上`所有`手指的資料, 不管碰到誰都存進來 > - 很少用, 因為很亂 > - 有兼容性問題, 有些手機不支援 > - 下面這串可以丟到手機測試看看, > 幾根手指碰到藍盒子(targetTouches), 藍盒子就顯示幾, > 而紅盒子(touches)會顯示所有碰到盒子的手指數目加總 > ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width'> <style> div,nav { height: 200px; width: 200px; background: skyblue; position: absolute; font-size: 40px; color: red; } </style> <script> window.onload = function () { let oBox = document.querySelectorAll(`div`); let oNav = document.querySelector(`nav`); Array.from(oBox).forEach(box=>{ box.addEventListener(`touchstart`, function (ev) { console.log(ev.targetTouches) // 註 this.innerHTML = ev.targetTouches.length; oNav.innerHTML = ev.touches.length; }, false) }); } </script> </head> <body> <div style='left:0; top:0;'></div> <div style='left: 0; top: 250px;'></div> <nav style='left: 0; top: 500px; background: pink'></nav> </body> ``` ```txt # 註 TouchList {0: Touch, length: 1} length: 1 0: Touch identifier: 0 target: div screenX: 417.75 screenY: 273.546875 clientX: 99.75 clientY: 108.546875 pageX: 99.75 pageY: 108.546875 radiusX: 11.5 radiusY: 11.5 rotationAngle: 0 force: 1 __proto__: Touch ``` #### 簡單的移動盒子 > - 移動盒子邏輯: > - 因為拖拉時滑鼠在盒子的位置是固定的, 所以點擊後計算滑鼠在盒子的位置 > 點擊的XY - 盒子距離左上的值 > - 移動時, 偵測滑鼠的位置再偵測滑鼠在盒子的位置, 就能算出盒子的偏移量 > - 鬆手時, 清除事件(即使手機端的盒子不會黏在滑鼠上也一樣要清, 效率與好習慣的考量) ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width'> <style> div { height: 100px; width: 100px; background: gray; position: absolute; left: 0; top: 0; } </style> <script> window.onload = function () { let div = document.querySelector(`div`); div.addEventListener(`touchstart`, function (ev) { // 1.點擊後 let divX = ev.targetTouches[0].clientX - div.offsetLeft; let divY = ev.targetTouches[0].clientY - div.offsetTop; console.log(ev); // document.addEventListener(`mousemove`, ()=>{}) // 以前move會加在document上, 避免滑鼠移太快, 來不及偵測, 導致移出盒子而出問題 // 移動端瀏覽器不會有這問題, 因為移動端瀏覽器有個規定是不管移動多快, 都會觸發事件 // 所以直接加在Box 上就好了 function defMove(ev) { // 2.拖拉 div.style.left = ev.targetTouches[0].clientX - divX + 'px'; div.style.top = ev.targetTouches[0].clientY - divY + 'px'; } // 即使在移動端, 盒子不會黏在滑鼠上, 但事件還是會存在, 沒用就要養成刪掉的習慣 function defEnd() { // 3.鬆手 console.log(ev); // 注意: // - touchend 對象的如果要拿位置資料, 要去 changedTouches 拿 // 而不是 targetTouches div.removeEventListener(`touchmove`, defMove, false); div.removeEventListener(`touchend`, defEnd, false); } div.addEventListener(`touchmove`, defMove, false) div.addEventListener(`touchend`, defEnd, false) }, false) } </script> </head> <body> <div></div> </body> ``` > - 問題: > 1. 如果使用 transform 而不是 postion, transform沒辦法直接獲取(除非在行內寫) > 2. 使用 rem > #### 獲取 transfrom > - `getComputedStyle(elem, false)` ? > - 這東西是返回一個`計算`後的樣式, 亦即被解析後的值 > - 瀏覽器在加載完頁面(HTML, CSS, JS...)後,會對這些東西進行解析 > 解析完後按照解析結果來轉換, 一切出於性能考量 > - 二參 false 可以兼容一些舊版的 FF, 不過舊版的FF也很難找到了, 所以加不加無所謂 > - `matrix()` ? > - 矩陣 > - 電腦任何圖形操作,事實上都是通過矩陣操作來完成 > - 而 transform 也是如此,解析後事實上就是使用 `matrix()` 來執行 > - 矩陣操作有些可逆有些不可逆, 例如 transform 就很不好搞 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width'> <style> div { transform: translateX(100px) rotate(45deg); } </style> <script> window.onload = function () { let oBox = document.querySelector(`div`); console.log(oBox.style.transform); // =>沒有任何東西 console.log(getComputedStyle(oBox, false).transform); // matrix(0.707107, 0.707107, -0.707107, 0.707107, 100, 0) // 瀏覽器把 translateX(100px) rotate(45deg) 變成 matrix(xx,xx,xx) // 之後執行時, 他就直接執行 martix() 了 } </script> </head> <body> <div></div> </body> ``` > - 既然無法直接獲取, 那就只能自己先把初始值存起來 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width'> <style> html { font-size: 10px; /* rem基準 */ } div { height: 100px; width: 100px; background: gray; transform: translate(0, 0); /* transform */ } </style> <script> window.onload = function () { let div = document.querySelector(`div`); let x = 0, /* 存 translate */ y = 0; div.addEventListener(`touchstart`, function (ev) { /* 計算當前點擊位置 */ let divX = ev.targetTouches[0].clientX - x; let divY = ev.targetTouches[0].clientY - y; function defMove(ev) { /* 計算偏移量 */ x = ev.targetTouches[0].clientX - divX; y = ev.targetTouches[0].clientY - divY; console.log(x,y); /* 1.賦予偏移量 2. rem=px/10 */ div.style.transform = `translate(${x/10}rem, ${y/10}rem)` } function defEnd(ev) { div.removeEventListener(`touchmove`, defMove, false); div.removeEventListener(`touchend`, defEnd, false); } div.addEventListener(`touchmove`, defMove, false) div.addEventListener(`touchend`, defEnd, false) }, false) } </script> </head> <body> <div></div> </body> ``` > - Q. 如何鎖定方向? > - 在手機操作上, 經常看到只能移動左右或上下的操作, > 例如拖拉輪播圖, 網頁只能上下滑動 > - 作法: > - 比較拖拉的距離, 如果拖拉超過一定距離(ex. 5px), 那就鎖定該方向 > - Q. 為啥需要超過一定距離, 而不是只要一移動就鎖定? > - 手指操作會有誤差, 用戶體驗考量 ```htmlmixed= <style> html { font-size: 10px; } div { height: 100px; width: 100px; background: gray; transform: translate(0, 0); } </style> <script> window.onload = function () { let div = document.querySelector(`div`); let x = 0, y = 0; div.addEventListener(`touchstart`, function (ev) { let divX = ev.targetTouches[0].clientX - x; let divY = ev.targetTouches[0].clientY - y; // 存放移動方向 let dir = ''; let startX = ev.targetTouches[0].clientX; let startY = ev.targetTouches[0].clientY; function defMove(ev) { // 移動方向是空的我就不動(鎖定) if (dir=='') { // 如果移動x方向超過 5px, 那就將方向設為 x if (Math.abs(ev.targetTouches[0].clientX - startX) >= 5) { dir = 'x'; // 如果移動的 y 方向超過 5px, 那就將方向設為 y } else if (Math.abs(ev.targetTouches[0].clientY - startY) >= 5) { dir = 'y'; } // 如果方向為x, 那我就只移動 x (水平鎖定) } else if (dir == 'x') { x = ev.targetTouches[0].clientX - divX; // 如果方向為 y, 那我就只移動 y (垂直鎖定) } else if (dir == 'y') { y = ev.targetTouches[0].clientY - divY; } div.style.transform = `translate(${x/10}rem, ${y/10}rem)` } function defEnd(ev) { div.removeEventListener(`touchmove`, defMove, false); div.removeEventListener(`touchend`, defEnd, false); } div.addEventListener(`touchmove`, defMove, false) div.addEventListener(`touchend`, defEnd, false) }, false) } </script> </head> <body> <div></div> </body> ``` ### 實作: 輪播(未完成) > - 基準565px, 字體10px => 56.5px/1rem > - 1. 一開始必須動態的去改 HTML font-size > - 2. 完成上面那些事情(鎖定方向, 拖拉) > - 3. 完成輪播圖的那些有的沒的功能 > - 缺少自動輪播, 刷新頁面, 切換前後圖, y 軸觸控應該要給頁面而不是 banner > - `onresize`: 重設頁面寬度時觸發 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width'> <style> * { margin: 0; padding: 0; list-style: none; } html { font-size: 10px; /* 基準 font-size 10 */ } header { width: 56.5rem; /* 基準寬度 565px */ height: 5.5rem; background: #fd8538; overflow: hidden; /* BFC, 清h1上margin */ position: relative; z-index: 999; } h1 { height: 4rem; width: 5rem; background: url(https://api.fnkr.net/testimg/50x40/FFF/000) no-repeat; text-indent: -9999rem; /* 讓文字縮到看不見, 只想顯示背景圖又必須在標籤打文字(h1+網頁名)時常用 */ background-size: 100% 100%; margin: .7rem; } .banner { height: 18rem; width: 56.5rem; position: relative; overflow: hidden; /* banner ul 一定會超出, 所以要截掉 */ } .banner ul { width: 999rem; /* 一般都設很大來裝圖, 方便不用算, 反正父級截掉就好了 */ height: 18rem; overflow: hidden; /* BFC, 清浮動 */ transform: translateX(-56.5rem); /* 閃避輪播圖插進去的第一張圖 */ } .banner ul li { height: 18rem; width: 56.5rem; float: left; } .banner ul li img { height: 100%; width: 100%; } .banner ol { position: absolute; bottom: 2rem; left: 50%; transform: translateX(-50%); } .banner ol li { height: 1.5rem; width: 1.5rem; border-radius: 50%; background: gray; margin: 0 .4rem; float: left; } .banner .active { background: #fd511f; } /**/ .load { line-height: 5rem; text-align: center; font-size: 2rem; } .pageContent { position: relative; top: -5rem; /* 蓋掉load */ background: white; /* 蓋掉load */ } </style> <script> /* 別用 onload, 如果用 onload, 要等頁面加載完才改fontSize, 會有一些延遲*/ /* 這裡使用 onresize 以免手機橫向改變寬度 */ window.onresize = function () { document.documentElement.style.fontSize = document.documentElement.clientWidth / 56.5 + 'px'; } window.onload = function () { let oBanner = document.querySelector(`.banner`); let oBannerUl = document.querySelector(`.banner ul`); let oBannerUlLi = oBannerUl.children; let oPageContent = document.querySelector(`.pageContent`); let oLoad = document.querySelector(`.load`); let oOlLi = document.querySelectorAll(`ol li`); // 輪播圖的前後兩張 oBannerUl.appendChild(oBannerUlLi[0].cloneNode(true)); oBannerUl.insertBefore(oBannerUlLi[oBannerUlLi.length-2].cloneNode(true), oBannerUlLi[0]); // 因為插了前後兩張, x 初始值必須先移一張圖的寬度 let x=-oBannerUlLi[0].offsetWidth, y=0; // 4. 自動輪播, 有空再弄 // 1. 點擊後 oBanner.addEventListener(`touchstart`, function (ev) { oPageContent.style.transition = `none`; /* 註1. */ oBannerUl.style.transition = `none`; // 1-1 開始計算位置 let dir=''; let startX = ev.targetTouches[0].clientX; /* 點下去的座標X */ let startY = ev.targetTouches[0].clientY; /* 點下去的座標Y */ let boxX = startX - x; /* 滑鼠在盒子的X */ let boxY = startY - y; /* 滑鼠在盒子的Y */ // 2. 移動時 function fnMove(ev) { // 2-1. 判斷方向 if (dir == '') { if (Math.abs(ev.targetTouches[0].clientX - startX) >= 5) { /* 如果移動超過5 */ dir = 'x'; } else if (Math.abs(ev.targetTouches[0].clientY - startY) >=5) { dir = 'y'; } } else { // 2-2 設定位移量 if (dir == 'x') { x = ev.targetTouches[0].clientX - boxX; /* 盒子偏移量 */ } else { y = ev.targetTouches[0].clientY - boxY; } } // 2-3 移動盒子 oBannerUl.style.transform = `translateX(${x}px)` /* 註2-1 */ // 手機常見的效果: 往下拉時, 畫面跟著移動的距離小於拖拉的距離, 避免經常觸發刷新 if (y>0) { /* 如果往下拉時, 移動距離是滑動的一半 */ oPageContent.style.transform = `translateY(${y/3}px)`; } else { oPageContent.style.transform = `translateY(${y}px)`; } if (y>200) { oLoad.innerHTML = '鬆手刷新'; } else { oLoad.innerHTML = '下拉刷新'; } } // 3. 鬆手時 function fnEnd() { oBanner.removeEventListener(`touchmove`, fnMove, false); oBanner.removeEventListener(`touchend`, fnEnd, false); // 3-1 如果下拉不到刷新頁面的程度, 鬆手時回到原位 if (y>0) { oPageContent.style.transition = `1s all ease`; /* 註1. */ oPageContent.style.transform = `translateY(0)`; y = 0; } // 3-2 如果超過就發請求刷新頁面 if (y>200) { // let xhr = let XMLHttpRequest(); } // 3-3 更換圖片, 超過一半就換, 不到就回到上一張 let n = Math.round(-x/oBannerUl.children[0].offsetWidth); /* 註2-2 */ x = -n * oBannerUl.children[0].offsetWidth; oBannerUl.style.transition = `1s all ease`; oBannerUl.style.transform = `translateX(${x}px)`; // 因為上面運算都是 px 在算, 直接給px 省得麻煩 // 否則還要取 fontSize 出來除 // 3-4 換點點 Array.from(oOlLi).forEach((li, index)=>{ li.className = index==n?`active`:``; }); // 3-5 判斷 n // 如果是最後一張, 瞬移到第一張 // 如果是第一張, 瞬移到倒數第二張 // 我感覺應該在這裡先清動畫 // 有空再弄 } oBanner.addEventListener(`touchmove`, fnMove, false); oBanner.addEventListener(`touchend`, fnEnd, false); }, false); } /* 1. 先主動換一次 font-size */ window.onresize(); </script> </head> <body> <header> <h1>網頁名</h1> </header> <div class='load'>下拉刷新</div> <div class='pageContent'> <section class='banner'> <ul> <li> <img src='https://picsum.photos/1200/400?random=1'/> </li> <li> <img src='https://picsum.photos/1200/400?random=2'/> </li> <li> <img src='https://picsum.photos/1200/400?random=3'/> </li> <li> <img src='https://picsum.photos/1200/400?random=4'/> </li> </ul> <ol> <li class='active'></li> <li></li> <li></li> <li></li> </ol> </section> <nav></nav> </div> </body> ``` ```txt # 註1. 第一次往下拉時, 添加了 transition, 放掉位置歸0 第二次往下拉時, 每做一次移動就做一次動畫, 添加幾百次, 導致畫面卡到爆 所以必須想辦法清掉, 最簡單的方法就是點擊的時候馬上清掉 transition, 放掉再加回去 # 註2. 假設我的寬度是 200px 我x移動了 -50px => 50/200 => 0.25 => 不到一半, 回到第一張 [0] 我x移動了 -175px => 175/200 => 0.875 => 超過一半, 跳第二張 [1] 我x移動了 -350px => 350/200 => 1.75 => 超過一張半, 跳到第三張 [2] 而數學上剛好有一個方法很適合: 四捨五入 round, 四捨五入基本概念就是取最接近的那個整數(有無一半) round(0.25) = 0; round(0.875) = 1; round(1.75) = 2; # 註2-1 動態效果單位直接用px比較方便, 否則還要再把當前頁面寬度下的 fontSize 拿出來除 尤其取值做運算(如2-2的情況), 取出來的值已經都是當下頁面的適應寬度了, 沒有必要再多轉 rem, 除非真的看 px 很不爽 =.= ``` ### 多點觸控 > - 1. 避免多點觸控: 如果不需要做多點觸控, 但又有多隻手指觸碰物件時, 如何處理? > - 計算平均位置 > <img src='https://i.imgur.com/3XcHxX1.jpg' style='width: 200px'/> > - Σ 所有x / n > - Σ 所有y / n > - 多點觸控主要考慮在手勢識別(gesture) > - 旋轉量: 後角度-前角度 > - 縮放比: 前距離/後距離 > - 運用大量三角函式 ```javascript= Math {abs: ƒ, acos: ƒ, acosh: ƒ, asin: ƒ, asinh: ƒ, …} abs: ƒ abs() acos: ƒ acos() acosh: ƒ acosh() asin: ƒ asin() asinh: ƒ asinh() atan: ƒ atan() # 反正切 atanh: ƒ atanh() atan2: ƒ atan2() # 反正切 ceil: ƒ ceil() cbrt: ƒ cbrt() expm1: ƒ expm1() clz32: ƒ clz32() cos: ƒ cos() # 餘弦 Cosinus(cos) cosh: ƒ cosh() exp: ƒ exp() floor: ƒ floor() fround: ƒ fround() hypot: ƒ hypot() imul: ƒ imul() log: ƒ log() log1p: ƒ log1p() log2: ƒ log2() log10: ƒ log10() max: ƒ max() min: ƒ min() pow: ƒ pow() # 次方 pow(base, exponent) random: ƒ random() round: ƒ round() sign: ƒ sign() sin: ƒ sin() # 正弦 sine(sin) sinh: ƒ sinh() sqrt: ƒ sqrt() # 開根號(x) tan: ƒ tan() # 正切 tangent(tan) tanh: ƒ tanh() trunc: ƒ trunc() E: 2.718281828459045 LN10: 2.302585092994046 LN2: 0.6931471805599453 LOG10E: 0.4342944819032518 LOG2E: 1.4426950408889634 PI: 3.141592653589793 # PI SQRT1_2: 0.7071067811865476 SQRT2: 1.4142135623730951 Symbol(Symbol.toStringTag): "Math" __proto__: Object ``` #### 旋轉 > - JS.MathModule > - [三角函數](https://zh.wikipedia.org/wiki/%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0) 與 [反三角函數](https://zh.wikipedia.org/wiki/%E5%8F%8D%E4%B8%89%E8%A7%92%E5%87%BD%E6%95%B0) > ![](https://i.imgur.com/2JSanJQ.jpg) > - `atan(x/y)` vs `atan2(y, x)` > - atan 是給邊長比 > - atan2 是給兩個邊長 > - 兩個返回的都是`弧度` > - 至於為啥公式是這樣, 我也不知道 > - 數學上有三種衡量角度的方法: 角度, 弧度, 梯度 > - 電腦長使用`角度`與`弧度` > - JS 是以弧度為主, 而 CSS3 則是以角度為主, 所以取值後要轉一下 ```txt // 角度一圈: 360度 // 弧度一圈: 2 PI //=> 360 角度 = 2PI 弧度 //=> 1 角度 = PI/180 弧度 //=> n 角度 = n*PI/180 弧度 //=> 2PI 弧度 = 360 角度 //=> 1 弧度 = 180/PI 角度 //=> n 弧度 = n*180/PI 角度 ``` ```javascript= Math.atan2(100, 100) // 0.7853981633974483 Math.atan(1) // 0.7853981633974483 Math.atan2(100,100)*180/Math.PI // 45 ``` ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no,maximum-scale=1.0,minimum-scale=1.0'> <style> .box { line-height: 200px; width: 200px; background: skyblue; text-align: center; position: absolute; top: 50px; left: 50px; transform: rotate(0deg); } </style> <script> function fnRotate(touch1, touch2) { let x = touch1.clientX-touch2.clientX; let y = touch1.clientY-touch2.clientY; return Math.atan2(y,x)*180/Math.PI; } window.onload = function () { let oBox = document.querySelector(`.box`); // 旋轉時, 手不會只放在盒子上, 通常都會觸碰到盒子的周圍以外 // 所以這裡事件直接綁在 DOM 上 let ang1, ang2 /* 兩個三角形的角度 */ let newAng=0, oleAng; /* 紀錄新舊角度 */ document.addEventListener(`touchstart`, ev=>{ if (ev.targetTouches.length >= 2) { /* 拿出去封裝起來復用 let x = ev.targetTouches[0].clientX-ev.targetTouches[1].clientX; let y = ev.targetTouches[0].clientY-ev.targetTouches[1].clientY ; let ang1 = Math.atan2(y,x)*180/Math.PI; */ ang1 = fnRotate(ev.targetTouches[0], ev.targetTouches[1]); oldAng = newAng; /* 把初始角度記錄下來 */ } // 放這裡會有問題, 因為這裡面會執行兩次以上的 touchstart, 導致觸發兩個 touchmove // 雖然觸發兩次的值都是相同的, 但是會影響性能, 所以不要放這 // oBox.addEventListener(`touchmove`, ev=>{}, false); }, false); // 放這裡不會有問題, 因為想要移動, 本來就必須先觸碰到螢幕, // 所以不管怎樣都會先執行 start 再執行 move document.addEventListener(`touchmove`, ev=>{ if (ev.targetTouches.length >= 2) { ang2 = fnRotate(ev.targetTouches[0], ev.targetTouches[1]); newAng = oldAng+ang2-ang1; /* 新角度=舊角度+偏移量 */ oBox.style.transform = `rotate(${newAng}deg)`; } }, false); } </script> </head> <body> <div class='box'>sdasdd</div> </body> ``` #### 縮放 > - <img src='https://i.imgur.com/5XjFJ4T.jpg' style='width: 300px'/> > - `Math.sqrt(x)`: 開根號 > - `Math.pow(basic, 次方)` : 次方 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no'> <style> .box { height: 200px; width: 200px; background: blue; margin: 20px 0 0 20px; transform: scale(1); } </style> <script> function calcDistance(touch1, touch2) { return Math.sqrt(Math.pow(touch1.clientX-touch2.clientX,2)+Math.pow(touch1.clientY-touch2.clientY,2)); } window.onload = function () { let oBox = document.querySelector(`.box`); let dis1, dis2, newScale=1, oldScale; document.addEventListener(`touchstart`, ev=>{ if (ev.targetTouches.length >= 2) { dis1 = calcDistance(ev.targetTouches[0], ev.targetTouches[1]); oldScale = newScale; } }, false) document.addEventListener(`touchmove`, ev=>{ if (ev.targetTouches.length >= 2) { dis2 = calcDistance(ev.targetTouches[0], ev.targetTouches[1]) newScale = oldScale * dis2 / dis1; /* 這裡是乘 */ oBox.style.transform = `scale(${newScale})`; } }, false); } </script> </head> <body> <div class='box'></div> </body> ``` > - 這種寫法只有取前兩根手指的數值,簡單又穩定 ## IScroll > - 一個專門做拖曳滾動效果的庫 > - 基本上都是手機瀏覽器在用的, PC 端習慣上都用滾輪, 很少用拖曳滾動的 ```shell $ npm init -y $ npm i iscroll -D $ tree node_modules/iscroll/build/ node_modules/iscroll/build/ ├── iscroll-infinite.js # 無限緩存滾動 ├── iscroll-lite.js # 精簡版 ├── iscroll-probe.js # 複雜版 ├── iscroll-zoom.js # 增加縮放功能 └── iscroll.js # 一般版 $ # 把要用的版本複製出來, 導入比較方便 $ % cp node_modules/iscroll/build/iscroll.js iscroll.js ``` ### 基礎使用方法 `new IScroll(元素 || querySelector字串 [ ,options])` > - 一傳進去, 馬上就可以拖動了 > - 傳進去的元素的`第一個`子元素會有拖曳效果, 其他都沒有 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='view-port' content='width=device-width'> <script src='iscroll.js'></script> <style> .fa { height: 300px; width: 300px; background: blue; margin: 100px auto 0; overflow: hidden; } </style> <script> window.onload = function () { let scroll = new IScroll(`.fa`); console.log(scroll); // 註 } </script> </head> <body> <div class='fa'> <div class='son'> 大量的字 </div> </div> </body> ``` ```txt # IScroll 有各種參數 IScroll {…} ... options: bounce: true # 使否允許拖超過 bounceTime: 600 # 拖拉超過時, 跳回來的時間 bounceEasing: {...} # 回來的效果曲線 scrollX: undefined # 是否允許橫向拖 scrollY: true # 是否允許縱向拖 freeScroll: undefined # 是否允許自由拖動(預設會方向鎖定) directionLockThreshold: 5 # 方向鎖定的門檻(閾) startX: 0 # 默認位置 startY: 0 # 默認位置 disablePointer: false # 是否禁用 Pointer(Touch + Mouse) disableTouch: true # 是否禁用 Touch disableMouse: true # 是否禁用 Mouse # 我測試的結果: 最好把 Pointer 關起來, 開其他兩個, 否則卡到爆 mouseWheelSpeed: 20 # 滾輪速度 invertWheelDirection: 1 # 是否將滾輪方向相反 momentum: true # 開起會更加炫砲(官方說法), 性能降低 # 我感覺沒多大差別, 性能降很多, 所以關掉就好 bindToWrapper: false # 把事件綁定到容器上, 如果false就是綁定到 DOM eventPassthrough: undefined # 冒泡 useTransition: true # 如果關這個, 他就會用 position, 性能就會降低 useTransform: true preventDefault: true # 阻止滾動默認, 關掉應該整個頁面都會滾動... preventDefaultException: {...} resizeScrollbars: true snapThreshold: 0.334 HWCompositing: true resizePolling: 60 ... __proto__: Object ``` ### IScroll 事件 ```txt - beforeScrollStart : 點擊元素時觸發 - scrollStart : 開始滾動時觸發 - scroll : 滾動時觸發 (必須用 probe 版本) - scrollEnd : 滾動結束時觸發(如果有回彈, 包括回彈結束, 不是鬆手就觸發) - scrollCancel : 點擊後卻沒滾動時觸發 - zoomStart : 縮放開始時觸發 - zoomEnd : 縮放結束時觸發 ``` ### `probeType` > - 探測優先級 > - `0` : 不觸發 > - `1` : 低, 觸發次數少 > - `2` : 中, 拖曳滾動就觸發 > - `3` : > - 高, 拖曳滾動就觸發 + 檢測物件運動 > - 例如拖曳超過後鬆手的回彈, 2 檢測不到, 3 才有 > - 自動禁用 `transition`, 因為這樣會拿不到值, 他會改用定時器來回彈 > - 當然越高性能越低 > - 必須使用 `iscroll-probe.js` 版本才有 ```htmlmixed= <head> <meta charset='utf-8'> <meta name='view-port' content='width=device-width'> <!-- probe 版才能偵測scroll --> <script src='iscroll-probe.js'></script> <style> .fa { height: 300px; width: 300px; background: blue; margin: 100px auto 0; overflow: hidden; position: relative; } .fa .son { width: 500px; overflow: hidden; /* BFC, 解決<P>的預設margin-top影響*/ } </style> <script> window.onload = function () { let oFa = document.querySelector(`.fa`); let oSon = document.querySelector(`.son`); let scroll = new IScroll(`.fa`, { scrollX: true, momentum: false, probeType: 3 // 偵測優先級 }); console.log(scroll); scroll.on(`beforeScrollStart`, function () { console.log(1); }) scroll.on(`scrollStart`, function () { console.log(2); }) scroll.on(`scroll`, function () { console.log(3); }) scroll.on(`scrollEnd`, function () { console.log(4); }) scroll.on(`scrollCancel`, function () { console.log(5); }) } </script> </head> <body> <div class='fa'> <div class='son'> 很多的字 </div> </div> </body> ``` ### 實作: 拖動刷新頁面 > - `scroll.disable()` : 停止動作 > - `scroll.enable()` : 開始動作 > - `scroll.scrollTo(x, y, time, easing)` : 移動到某處 > - `transitionend` : 動畫結束時觸發 > - ```htmlmixed= <head> <meta charset='utf-8'> <meta name='view-port' content='width=device-width'> <script src='iscroll-probe.js'></script> <style> .fa { height: 300px; width: 300px; background: blue; margin: 100px auto 0; overflow: hidden; position: relative; } .fa .update, .fa .loading { width: 100%; line-height: 40px; text-align: center; position: absolute; left: 0; } .fa .update { top: 0; } .fa .loading { bottom: 0; } .fa .son { width: 500px; position: relative; z-index: 999; background: blue; overflow: hidden; /* BFC, 解決<P>的預設margin-top影響*/ } </style> <script> window.onload = function () { let oUpdate = document.querySelector(`.update`); let oLoading = document.querySelector(`.loading`); let oFa = document.querySelector(`.fa`); let oSon = document.querySelector(`.son`); let scroll = new IScroll(`.fa`, { disableTouch: false, disableMouse: false, disablePointer: true, momentum: false, mouseWheelSpeed: 50, probeType: 3 }); scroll.on(`scroll`, function () { if (scroll.y >= 40) { oUpdate.innerHTML = '鬆手刷新'; } else { oUpdate.innerHTML = '下拉刷新'; } // console.log(scroll.y, oSon.offsetHeight-oFa.offsetHeight); // 拖曳的值超過最大可拖曳的量 40 的話 if ((-scroll.y-(oSon.offsetHeight-oFa.offsetHeight))>=40) { oLoading.innerHTML = `加載中`; } else { oLoading.innerHTML = `上拉加載`; } }) // 這東西要等到動畫結束才觸發, 沒啥用 scroll.on(`scrollEnd`, function () {}) // 只能自己寫 oFa.addEventListener(`touchend`, ev=>{ // 1. 判斷是否需要更新頁面 if (scroll.y >= 40) { // 2. 超過頁面回彈到更新頁面 div 下, 並顯示加載中 scroll.disable(); // 鬆手停住 // scroll.scrollTo(x, y, time, easing): 移動到哪個位置 // 問題: 如果我加了動畫時間, 他竟然會回到 0px 的位置..., 停不下來=.=, 所以只好自己幹 // scroll.scrollTo(0, oUpdate.offsetHeight, scroll.options.bounceTime); oSon.style.transition = `.5s all ease`; oSon.style.transform = `translateY(${oUpdate.offsetHeight}px)`; oUpdate.innerHTML = `<img src='${loadingBase64}'/>加載中`; // 3. 拿資料 let xhr = new XMLHttpRequest(); xhr.open(`GET`, `http://localhost:5566/1.txt`, true); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { // 4. 改資料 // 為了測試效果才設定時器的 setTimeout(function() { oSon.innerHTML = `<div class='son'><p>${xhr.responseText}</p></div>`; oSon.style.transform = `translateY(0)`; function fnNoname() { oSon.style.transition = `none`; oSon.removeEventListener(`transitionend`, fnNoname, false); scroll.y = 0; scroll.enable(); } oSon.addEventListener(`transitionend`, fnNoname, false); }, 3000); } else { console.log(2); } } } } }, false); } let loadingBase64 = 'data:image/gif;base64...'; </script> </head> <body> <div class='fa'> <div class='son'> <p>很多的字</p> </div> <div class='update'>下拉刷新</div> <div class='loading'>上拉加載</div> </div> </body> ``` ### 實作: 動畫序列 > - 跟 IScroll 沒啥關係, 只是拿來拖動方便用而已 > - 動畫序列就只是改變背景的 position 而已 > - ![](https://i.imgur.com/vyuYVfW.jpg) ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta name='view-port' content='width=device-width'> <script src='iscroll-probe.js'></script> <style> .fa { height: 300px; width: 300px; background: blue; margin: 100px auto 0; overflow: hidden; position: relative; } .fa .son { width: 100%; height: 500px; position: relative; background: skyblue; overflow: hidden; /* BFC, 解決<P>的預設margin-top影響*/ text-align: right; } .fa .img { height: calc(416px / 3); width: calc(416px / 3); position: absolute; top: 0; left: 0; background: url(1.jpg) no-repeat; background-position: 0 0; } </style> <script> window.onload = function () { let oImg = document.querySelector(`.img`); let scroll = new IScroll(`.fa`, { disableMouse: false, disableTouch: false, disablePointer: true, scrollY: true, probeType: 3 }); scroll.on(`scroll`, ev=>{ // 1. 設定每 10px 走一幀 let frame = Math.floor(scroll.y/10); // 2. 載下來的圖最多 9 幀, (只能換 8 次) if (frame >= 9) { frame = 8; } // 3-1. 計算 X 跳第幾幀 * 每幀寬度 // console.log(-frame%3*(416/3)); // 3-2. 計算 Y 跳第幾行, 三幀跳一行 * 高度 // console.log(-Math.floor(frame/3)*(416/3)); // console.log(`-${(frame%3)*(416/3)}px -${(frame/3)*(416/3)}px`); oImg.style.backgroundPosition = `-${frame%3*(416/3)}px -${Math.floor(frame/3)*(416/3)}px`; }); } </script> </head> <body> <div class='fa'> <div class='son'> 123 </div> <div class='img'></div> </div> </body> </html> ``` ## hammerJS `new Hammer(元素)`; > - 主要做手勢相關的事件 > - 給指定元素加上各種事件 ```shell % npm i hammerjs -D % cp node_modules/hammerjs/hammer.js hammer.js ``` ### hammerJS 事件 > - `tap` > - 短點擊(250ms左右內鬆手) > - vs. click : 手機端有延遲(300ms左右) > - Zepto 也有 tap 方法,也可以解決這個延遲 > - `press` > - 長點擊(點擊超過 300ms 左右) > - `swipe` > - 快速滑動(速度超過 300ms/s)才會觸發 > - `swipeup` `swipedown` `swipeleft` `swiperight` > - 只會觸發一次 > - `pan` > - 滑動 > - `panup` `pandown` `panleft` `panright` > - `panstart` `panmove` `panend` `pancancel` (較常使用這組) > - 只要滑動都會觸發 > - `swipe` `pan` 預設都只觸發左右, 必須自己去把方向打開 > - `hammer.get('pan').set({direction: Hammer.DIRECTION_ALL})` > - `hammer.get('swipe').set({direction: Hammer.DIRECTION_ALL})` > - `rotate` > `get('rotate').set({enable: true})` > - rotate 預設是關閉的 > - 必須先拿到rotate的配置, 並開啟 rotate 的功能 > - `ev.rotation` : 紀錄旋轉`角度`(不用轉, 讚) > - `scroll` > `get('scroll').set({enable: true})` > - scroll 預設也是關閉的, 一樣拿配置傳參開啟 > - `ev.scroll` : 縮放倍數 > - `ev.center.x` `ev.center.y`: 紀錄兩指中間的 x, y ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta name='view-port' content='width=device-width,initial-scale=1.0,user-scalable=no,maximum-scale=1.0,minimum-scale=1.0'> <script src='hammer.js'></script> <style> .box { height: 600px; width: 400px; background: skyblue; margin: 250px auto 0; transform: rotate(0deg); } </style> <script> window.onload = function () { let oBox = document.querySelector(`.box`); let hammer = new Hammer(oBox); console.log(hammer); // 短點擊 hammer.on(`tap`, ev=>{ console.log(`tap`, ev); // console.log(ev.center.x, ev.center.y) // 兩隻手指的X, Y中心點 }) // 長點擊 hammer.on(`press`, ev=>{ console.log(`press`, ev) }) // 快速滑動 hammer.on(`swipe`, ev=>{ console.log(`swipe`, ev); }) hammer.on(`swipeup`, ev=>{ console.log(`swipeup`); }) hammer.on(`swipedown`, ev=>{ console.log(`swipedown`); }) hammer.on(`swipeleft`, ev=>{ console.log(`swipeleft`); }) hammer.on(`swiperight`, ev=>{ console.log(`swiperight`); }) // 滑動 hammer.on(`pan`, ev=>{ console.log(`pan`, ev); }) hammer.on(`panup`, ev=>{ console.log(`panup`); }) hammer.on(`pandown`, ev=>{ console.log(`pandown`); }) hammer.on(`panleft`, ev=>{ console.log(`panleft`); }) hammer.on(`panright`, ev=>{ console.log(`panright`); }) hammer.on(`panstart`, ev=>{ // 有移動門檻(閾) console.log(`panstart`); }) hammer.on(`panmove`, ev=>{ console.log(`panmove`); }) hammer.on(`panend`, ev=>{ console.log(`panend`); }) hammer.on(`pancancel`, ev=>{ console.log(`pancancel`); }) let deg = 0, o_deg; let scale=1.0, o_scale; hammer.get(`rotate`).set({enable: true}); hammer.get(`pinch`).set({enable: true}); // 旋轉 hammer.on(`rotatestart`, ev=>{ o_deg = deg; }) hammer.on(`rotatemove`, ev=>{ deg = o_deg + ev.rotation; oBox.style.transform = `rotate(${deg}deg) scale(${scale})`; }) //縮放 hammer.on(`pinchstart`, ev=>{ o_scale = scale; }); hammer.on(`pinchmove`, ev=>{ scale = o_scale * ev.scale; oBox.style.transform = `rotate(${deg}deg) scale(${scale})`; }) } </script> </head> <body> <div class='box'></div> </body> </html> ``` ### 實作: 列表拖拉顯示刪除(未完成) > <img src='https://i.imgur.com/SAh6m44.png' style='width: 300px'/> > - 筆記1. 讓畫面跟隨螢幕高度 => `html, body {height: 100%}` > - 筆記2. <img src='https://i.imgur.com/UeFsgHC.png' style='width: 100px'/> => <img src='https://i.imgur.com/2YLTgdF.png' style='width: 100px'/> > - 筆記3. swipe & pan 預設只偵測水平方向, 垂直必須傳參打開 > - `hammer.get('pan').set({direction: Hammer.DIRECTION_ALL});` > - `hammer.get('swipe').set({direction: Hammer.DIRECTION_ALL});` > - 未完成: > - 點擊刪除非常有問題 > - 頁面拖動還沒寫 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no'> <script src='hammer.js'></script> <style> * { margin: 0; padding: 0; list-style: none; } body,html { /* 讓畫面跟隨螢幕高度 */ height: 100%; } .box { /* 盒子定死在螢幕高度 */ height: 100%; overflow: hidden; } .usr_txt { /* Ul 做上下拖動 */ transform: translateY(0px); } .usr_txt li { line-height: 30px; border-bottom: 1px solid black; position: relative; overflow: hidden; transition: .5s all ease; height: 30px; } .usr_txt a { position: absolute; right: 0; top: 0; background: red; text-decoration: none; color: white; width: 0; text-align: center; /*transition: .5s all ease; */ /* move 不同疊加動畫, 導致畫面卡到爆 */ overflow: hidden; /* 把 span 加出來的空間隱藏, 否則空間會多 50px 出來*/ } .usr_txt a span { /* 必須給文字空間, 否則在一開始空間不夠時, 字會掉下去 */ min-width: 50px; display: block; } </style> <script> window.onload = function () { let list = [ `asds1adqw`, `asds23adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw`, `asds3123adqw` ] let oUl = document.getElementsByClassName(`usr_txt`)[0]; let oDel = document.getElementsByClassName(`del`); list.forEach(txt=>{ let oLi = document.createElement(`li`); oLi.innerHTML = `<span>${txt}</span><a class='del' href='javascript:;'><span>刪除</span></a>`; oUl.appendChild(oLi); let oA = oLi.querySelector(`a`); let hammer = new Hammer(oLi); // swipe & pan 預設只偵測水平方向, 垂直必須傳參打開 hammer.get(`pan`).set({direction: Hammer.DIRECTION_ALL}); // 這東西有個缺點: 鬆手時才觸發, 而我想拉動時就觸發, 所以改用 panstart 系列 // hammer.on(`swipeleft`, ev=>{ // Array.from(oDel).forEach(del=>{ // del.style.width = 0; // }); // oA.style.width = `50px`; // }) // hammer.on(`swiperight`, ev=>{ // oA.style.width = `0`; // }) let start_x, start_y, dir = ''; // 計算方向用的變量 let end_del_w = 0, start_del_w = 0; // 存放開始與結束值得變量 // 1. 點擊時, 初始化所有的值 hammer.on(`panstart`, ev=>{ start_x = ev.center.x; start_y = ev.center.y; dir = ''; start_del_w = end_del_w; }); // 2. 移動時開始計算值 hammer.on(`panmove`, ev=>{ // 2-1 鎖定方向 if (dir == '') { if (Math.abs(ev.center.x - start_x) >= 5) { dir = 'x'; } else if (Math.abs(ev.center.y - start_y) >= 5) { dir = 'y'; } } else { // 2-2 計值賦值 if (dir == 'x') { let w = start_del_w + start_x - ev.center.x; // 存放目前變動的值 if (w < 80) { oA.style.width = w + `px`; } else { oA.style.width = 80 + (w-80)/4 + `px`; } end_del_w = w; } else { // 頁面拖動 (待續) } } }); // 3. 鬆手時做結束處理 hammer.on(`panend`, ev=>{ // 3-1 寬度歸位 oA.style.transition = `.5s all ease`; if (end_del_w>60) { oA.style.width = `80px`; end_del_w = 80; } else { oA.style.width = `0px`; end_del_w = 0; } // 3-2 清除事件 function fn_rm_transition() { this.style.transition = 'none'; this.removeEventListener(`transitionend`, fn_rm_transition, false); } oA.addEventListener(`transitionend`, fn_rm_transition, false); }); // 這有些問題, 有空再改 oA.onclick = function (ev) { oA.style.width = 0; oA.addEventListener(`transitionend`, ev=>{ oLi.style.height = 0; oLi.style.borderBottom = `0px solid #ccc`; ev.cancelBubble = true; oLi.addEventListener(`transitionend`, ev=>{ oUl.removeChild(oLi); },false); }, false); } }) } </script> </head> <body> <div class='box'> <ul class='usr_txt'></ul> </div> </body> </html> ``` ### 實作: 拖曳手機版面(未完成) > - 1. 動態刷新頁面 > - 2. 點擊導航欄, 跳動頁面效果 > - 3. 拖曳頁面(hammer)動畫效果 > - 4. 導航欄顏色漸變效果 > - 5. 導航欄底部圖標滑動效果 > - 用起來還有一些問題 > - 導航欄跟內容的index沒有聯起來 > - Y拖動沒有寫 ```htmlmixed= <!DOCTYPE html> <html> <head> <meta charset='utf-8'> <meta name='viewport' content='width=device-width,initial-scale=1.0,user-scalable=no'> <script src='hammer.js'></script> <style> * { padding: 0; margin: 0; list-style: none; } .page .nav { width: 100%; overflow: hidden; padding: 6px 0; height: 14px; } .page .nav ul { position: relative; overflow: hidden; } .page .nav ul li { float: left; width: 80px; text-align: center; } .page .nav ul li.active { color: #F00; } .page .nav ul .line { position: absolute; left: 0; bottom: 0; width: 80px; height: 3px; background: skyblue; transition: .5s all ease; } .page .wrap { width: 100%; height: 600px; overflow: hidden; } .page .content { overflow: hidden; height: 600px; } .page .content-item { float: left; width: 375px; height: 600px; overflow: hidden; box-sizing: border-box; border: 1px solid #ccc; } </style> <script> window.onload = function () { let oPage = document.querySelector(`.page`); let oNav = document.querySelector(`.page .nav`); let oNavUl = document.querySelector(`.page .nav ul`); let aNavLi = oNavUl.getElementsByTagName(`li`); let oContent = document.querySelector(`.content`); let aContentItem = document.querySelectorAll(`.content-item`); oNavUl.style.width = aNavLi[0].offsetWidth * aNavLi.length + `px`; oContent.style.width = aContentItem[0].offsetWidth * aContentItem.length + `px`; // 1. 刷新頁面 function loadData(id) { let xhr = new XMLHttpRequest(); xhr.open(`GET`, `http://localhost:5566/${id+1}.txt`, true); xhr.send(); xhr.onreadystatechange = function () { if (xhr.readyState == 4) { if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) { aContentItem[id].innerHTML = `${xhr.responseText}`; } else { console.log(`NOT OK`); } } } } loadData(0); function apple(n) { // 2-2. 處理標題層 // 2-2-1. 點擊換色 Array.from(aNavLi).forEach(li=>{li.className = '';}); aNavLi[n].className = 'active'; // 5. 底下小圖標, 跟著li走的, 第幾個被選中, 圖標就跳到第幾個 document.querySelector(`.line`).style.left = aNavLi[n].offsetLeft + 'px'; // 2-2-2. 計算偏移量(註1.) let left = aNavLi[n].offsetLeft - (document.documentElement.clientWidth-aNavLi[n].offsetWidth)/2; // 2-2-3. 偏移不可為負, 否則左邊沒東西了會空掉 if (left < 0) { left = 0; // 2-2-4. 計算最大可偏移量: 總寬-螢幕寬, 右邊沒東西了, 再移就空掉了(註2.) } else if (left > oNavUl.offsetWidth - oNav.offsetWidth) { left = oNavUl.offsetWidth - oNav.offsetWidth; } // 2-2-5. 偏移 oNavUl.style.transition = `.5s all ease`; oNavUl.style.transform = `translateX(${-left}px)`; // 2-3. 處理內容層 oContent.style.transition = `.5s all ease`; // 2-3-1. 位移n個時, 就直接 n * 每個寬度即可, 向左偏移, 所以要負數 oContent.style.transform = `translateX(${-aContentItem[0].offsetWidth*n}px)`; // 2-3-2. 清除動畫效果, 由於oContent跟oNavUl, 時間是一樣的, 寫一個直接在裡面兩個都砍就好了 function fnEnd() { oContent.style.transition = 'none'; oNavUl.style.transition = 'none'; oNavUl.removeEventListener(`transitionend`, fnEnd, false); } oNavUl.addEventListener(`transitionend`, fnEnd, false); // 2-3-3. 換頁時加載頁面 loadData(n); } // 2-1. 把所有 aNavLi 加點擊事件 Array.from(aNavLi).forEach((li, index)=>{ let hammer = new Hammer(li); hammer.on(`tap`, ev=>{ apple(index); }); }); // 3. 拖曳效果 // 塊級作用域, 避免名字重複的麻煩 { let hammer = new Hammer(oContent); let start_x, start_y; // 存放點擊時的 X Y let translateX = 0, old_translateX; // 存放新舊偏移量 // 3-1. 點擊時初始化所有值 hammer.on(`panstart`, ev=>{ start_x = ev.center.x; start_y = ev.center.y; old_translateX = translateX; }); // 3-2. 位移時 hammer.on(`panmove`, ev=>{ // 假設拖曳方向為X // 3-2-1. 新位置 = 位移量 + 上次鬆手舊位子 translateX = ev.center.x - start_x + old_translateX; oContent.style.transform = `translateX(${translateX}px)`; // 4. 顏色漸變 (註3.) let w = aContentItem[0].offsetWidth; // 一個頁面寬 // 4-1 計算現在位移了幾個`完整`頁面 let n = Math.floor(-translateX/w); // 例如一頁100, 我移了440, 那我現在此時此刻應該至少移了 4 個頁面 // 440/100 向下取整 => 4 // console.log(translateX,n, w); // 4-2. 計算現在正在兩個頁面間的位移比例 let tmp = (-translateX-(n)*w)/w; // 上面假設, 440 - 400 = 40 // 我在第4跟5之間的頁面位移, 目前位移的比例是 40 % console.log(tmp); // // // console.log(tmp); if (n<0) { n = 0; } // 4-3. 賦予兩個頁面顏色, 顏色的最大值為 0XFF aNavLi[n+1].style.color = `rgb(${Math.round(tmp*0xFF)},0,0)`; aNavLi[n].style.color = `rgb(${Math.round((1-tmp)*0xFF)},0,0)`; }) // 3-3 鬆手歸位 hammer.on(`panend`, ev=>{ // 3-3-1. 如果新位置為負的, 就回到第一頁(不位移) if (translateX>0) { translateX = 0; } // 3-3-2. 判斷位移多少頁: let n = Math.round(-translateX/aContentItem[0].offsetWidth); // 3-3-3. 如果偏移了超過九頁(到第十頁), 那就不能再往下移了, 卡死 if (n > aContentItem.length-1) { n = aContentItem.length-1; } // 3-3-4. 執行位移 translateX = -n * aContentItem[0].offsetWidth; // 3-3-5. 同時標題層跟內容層都需要處理(位移, 內容加載等) apple(n); }) } } </script> </head> <body> <div class='page'> <div class='nav'> <ul> <li class='active'>測試1</li> <li>測試2</li> <li>測試3</li> <li>測試4</li> <li>測試5</li> <li>測試6</li> <li>測試7</li> <li>測試8</li> <li>測試9</li> <li>測試0</li> <div class='line'></div> </ul> </div> <div class='wrap'> <div class='content'> <div class='content-item'>1</div> <div class='content-item'>2</div> <div class='content-item'>3</div> <div class='content-item'>4</div> <div class='content-item'>5</div> <div class='content-item'>6</div> <div class='content-item'>7</div> <div class='content-item'>8</div> <div class='content-item'>9</div> <div class='content-item'>0</div> </div> </div> </div> </body> </html> ``` > - 註1. > ![](https://i.imgur.com/0fjY9nM.jpg) > - 註2. > ![](https://i.imgur.com/YkcFLuk.jpg) > - 註3. > ![](https://i.imgur.com/knYqbOD.jpg)