# 移動端開發
## 移動端測試
> - 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)
> 
> - `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 而已
> - 
```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.
> 
> - 註2.
> 
> - 註3.
> 