天天看點

前端頁面雙向滾動方案

原創 安箋 淘系技術 6月29日

前端頁面雙向滾動方案
脫離canvas後,頁面如何實作上下左右雙滑動?又如何在安卓系統和iOS系統上實作?

背景

在許多業務場景中會遇到讓一個頁面在web端可以實作雙向滾動,且在滾動的時候可以上部吸頂,左側吸在最左邊這中類似excel表現的述求。

但是excel移動端本身使用canvas來畫的,是以能保持較好的體驗和性能,那麼脫離canvas,我們是否有别的方案可以去靠近實作呢?如何在移動端實作,并同時相容安卓和ios系統?

實作目标

如下圖是以,可以左右移動,也支援上下移動,在移動的對應方向的頂部需要吸頂。在pc端實作較為簡單,通過滾動就可以實作,在移動端要想實作這一套方案,并同時相容不同系統,這裡我踩了一些雷,下文将訴說實作這個的全部技術方案的改變和踩雷的一些點。

前端頁面雙向滾動方案

技術方案

▐  初期思想

前端頁面雙向滾動方案

如上圖所示,不就是橫向滾動和豎向預設滾動嗎?

移動端螢幕寬高固定,本就可以左右滾動,那麼隻需要元素本身的大小大于螢幕的寬高不就可以了嗎?

設定一個div,寬大于screen.width, 高大于screen.height即可,最終demo大概效果如下圖所示:

前端頁面雙向滾動方案

左右的确都相當的絲滑,能橫着滾,也能豎着滾了,就是全方位也都能滾動,着實詫異了一會兒。

▐  更新思維

如果那麼當我朝着某個方向滾動的時候,禁掉一個方向的滾動,就可以解決了。

但是容器嵌套就出問題了,橫向滾動的時候,我們領容器高度變成視口高度,overflow-y設定為hidden,橫向依舊可以滾動,縱向設定為禁止滾動,在chrome裡模拟了一下,的确不錯,在真機上測試的時候,發現安卓端正常,ios端一旦手動設定overflow-y,發現縱向原先滾動到的位置會丢失,縱向内容直接回到最頂部,即縱向scrollTop變成了0。

設定縱向滾動,橫向overflow-x為hidden時,橫向原先滾動的距離也會被清除,回到最左側。

那麼通過手動記錄之前的滾動位置,在設定某一方向禁止滾動時,把高度給他還原回去,就能解決了。

思路是對的,但是會出現強烈的閃屏現象,尤其在這種資料密集型的頁面上(ios頁面也閃,低端安卓機沒做嘗試,meta30存在一定延遲的閃屏)。

▐  改變方案

雙向滾動到此基本上算是失敗了。

單向滾動肯定沒問題,另一個方向我們可以使用模拟滾動的方式來自己寫一套"滾動"效果。

首先我們判斷橫向滾動和縱向滾動那一方向使用原生滾動呢?

我選擇了縱向,縱向資料體量大,容易有滾動的訴求,橫向存在資料量不足可能不需要滾動。

效果如下圖所示:

前端頁面雙向滾動方案
前端頁面雙向滾動方案

左側第一列和頂部第一行吸頂這裡使用了css的屬性 position:sticky。 

也有人可以考慮使用一些特殊布局,例如左側做絕對定位,右側内容自行滾動等。

模拟滾動的方式,我們采用touch事件來仿造,通過touchStart、touchMove、touchEnd 這3個事件來模拟scroll事件。

首先在touchStart中擷取手指頭接觸到螢幕的點的 (x,y) 坐标,然後在touchMove事件中擷取螢幕出點的第二個觸點的(x,y)坐标,來擷取觸摸方向。

//獲得角度
function getAngle(angx, angy) {
  return Math.atan2(angy, angx) * 180 / Math.PI;
};

function getDirection(startx, starty, endx, endy) {
  const angx = endx - startx;
  const angy = endy - starty;
  let result = 0;

  //如果滑動距離太短
  if (Math.abs(angx) < 2 && Math.abs(angy) < 2) {
    return result;
  }
  const angle = getAngle(angx, angy);
  if (angle >= -150 && angle <= -30) {
    return 'top';
  } else if (angle > 30 && angle < 150) {
    return 'down';
  } else if ((angle >= 150 && angle <= 180) || (angle >= -180 && angle < -150)) {
    return 'left';
  } else if (angle >= -30 && angle <= 30) {
    return 'right'
  }
}      

由于大家在使用手機時,橫向滑動的行為軌迹并非是一個水準180度的橫線,是以在這裡設定了一定值,例如多少角度到多少角度認為是橫向滾動。

方向定好,多少角度這個可以根據自己業務上的判斷做修改。

前端頁面雙向滾動方案
前端頁面雙向滾動方案

連續觸摸滑動的坐标軸我們已經能拿到了,通過css3的transform來使元素進行移動,通過不斷修改移動的值達到元素滾動的效果。

這裡有一點需要注意,并非我們觸摸劃過多少像素,元素就移動多少像素,這裡還涉及一個速度和距離的概念。

還有我們手指頭松開以後,元素應該還會滾動一部分,然後速度慢慢降下來,直到0為止。

先分析一直處于觸點滑動這個階段,我們并非劃過多少像素,元素就跟着滾動多少像素,這裡看情況來放大或者縮小這裡的滾動值,展現給使用者的感受就是滑的快還是劃得慢。

我們由于橫向資料量并不大,是以這裡我們所有滾動距離都做了一定縮小處理。

// 代碼隻留個架構
function rowScrollAction(scrollDirection, endx, isEnding = false) {
    scrollGap = (endx - startx) * 0.85; // 每個滾動距離都乘上0.85
    let scrollLen = preX + scrollGap; // preX為之觸摸之前已經滾動到的位置
    if (isEnding) {
      scrollLen = endx
    }
    if (scrollDirection === 'right') {
      ....
    }
    if (scrollDirection === 'left') {
     ....
    }
  }      

這裡還需要注意的點是,如果已經滾動到橫向邊界了,那麼就不允許在滾動了,這裡需要特殊處理一下。

那麼觸摸結束以後,元素還應該按照慣性繼續向對應方向進行滾動,速度慢慢下降,直到結束為止。

速度不能直線下降,不能是一次函數,因為:y=ax+b

a代表加速度,加速的值一直不變,那麼就會勻速降下去,體驗上并不好,這裡選擇了二次函數的概念。

前端頁面雙向滾動方案
前端頁面雙向滾動方案

如圖所示,慢慢靠近目标值,那麼就要口子朝下的抛物線,且是對稱軸左側的這一部分函數。根據函數定義:

前端頁面雙向滾動方案
前端頁面雙向滾動方案

通過a、b參數來産出一個對應計算函數:

// 将"目前時間"映射[0, 1]間(currentTime /= duration)
// 根據緩入公式計算出"目前時間"應該移動的百分比(currentTime * currentTime)
// 根據"總移動長度"和"移動百分比"算出應移動的具體值(changeValue * ...)
// 加上初始位置(+ startValue)
function easeOut(currentTime, startValue, changeValue, duration) {
  currentTime /= duration;
  return -changeValue * currentTime * (currentTime - 2) + startValue;
}      

本業務場景中的實作代碼如下:

/**
*  target: 滾動最終像素值
*/
const scrollAnimation = (target) => {
    function easeOut(t, b, c, d) {
      return -c * (t /= d) * (t - 2) + b; // 用時間t做x變量
    }
    var toTarget = target;
    var endTimer = 500;
    var startTimer = 0;

    var step = function () {
      let value = easeOut(startTimer, preX, toTarget, endTimer); // 傳回一個距離值
      // rowScrollAction(tempDirection, value, true); 執行對應元素滾動多少像素的方法 
      startTimer += 25;
      if (startTimer <= endTimer) {
        // 繼續運動
        requestAnimationFrame(step);
      } else {
        // 動畫結束
        touchTimes = 0;
        // 動畫結束後的回調
      }
    };      

當滾動到邊界時,記得清空循環計算哦

▐  最後優化

當橫向滾動到邊界時,模拟ios的彈簧效果(安卓和ios端保持一緻)

前端頁面雙向滾動方案

實作方式就是對3.3中的滾動到邊界時做對應的處理。我們達到邊界以後,在滾動距離上多加50~100個像素值,然後利用抛物線的原理:

前端頁面雙向滾動方案

利用兩邊的值,先慢慢靠近最高點,然後再回到對應y值上。

結論

接手一個項目,要嚴謹一點,如果是之前沒接觸過這種技術方案開發的,應該拿到項目時先寫個小demo,然後再去評估開發時間。

不然腦海中出現的技術方案可以讓你當場拍闆,但是在實踐中會發現出現預估錯誤,可是開發排期影響的就是整個小team的時間,你的delay很可能導緻大家的排期都被打亂,不得不從拼命加班搶回開發時間。

還要學會給自己留充足的buffer能夠去試錯,能夠應對突發情況。

繼續閱讀