---
# System prepended metadata

title: 移動端開發

---

# 移動端開發

## 移動端測試
> - 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)
