控制页面的滚动:自定义下拉到刷新和溢出效果

虽互不曾谋面,但希望能和你成为笔尖下的朋友

以读书,技术,生活为主,偶尔撒点鸡汤

不作,不敷衍,意在真诚吐露,用心分享

点击上方,可关注本刊

控制页面的滚动:自定义下拉到刷新和溢出效果_第1张图片

前言

通过阅读本文,你可以通过css 中overcroll-behavior属性值的设置,处理浏览器溢出滚动,以及禁用移动设备上刷新,下拉滚动时的发光和橡皮圈回弹效果,当然也可以看到css中 Houndini(胡迪宁),也就是css中也可以写变量等知识,如果文有误导的地方,欢迎路过的老师拍砖指正

目录

  • 背景,滚动边界与滚动链接

  • 引入overscroll行为(对应的三个属性值,auto,contain,none)

  • 防止滚动逃离固定位置元素通过overscroll-behavior:contain解决

  • 禁用拉到刷新(overscroll-behavior-y: contain)

  • 禁用超滚色条纹和橡皮筋效果要在滚动边界时禁用反弹效果(橡皮筋效果),使用overscroll-behavior-y: none:

  • 完整Demo

  • 总结

CSS overscroll-behavior属性允许开发人员在达到内容的顶部/底部时覆盖浏览器的默认溢出滚动行为。使用该案例包括禁用移动设备上的“拉动到刷新”功能,消除过度滚动发光和橡皮筋效果,并防止页面内容在模态/叠加层下滚动

背景

滚动边界和滚动链接

滚动是与页面交互的最基本的方式之一,但是由于浏览器的诡异默认行为,某些UX模式可能很难处理。作为一个例子,带一个应用程序抽屉带有大量用户可能需要滚动的项目。当它们到达底部时,溢出容器将停止滚动,因为没有更多内容可供使用。换句话说,用户到达“滚动边界”。但是请注意,如果用户继续滚动会发生什么情况。抽屉后面的内容开始滚动!滚动由父容器占领;例子中的主页面本身

被证实这种行为称为滚动链接;滚动内容时浏览器的默认行为。通常情况下,默认设置非常好,但有时候这并不理想,甚至不可预料。当用户点击滚动边界时,某些应用可能希望提供不同的用户体验

控制页面的滚动:自定义下拉到刷新和溢出效果_第2张图片

(在Chrome Android上滚动链接)

拉到刷新效果

拉到刷新是一种直观的手势,通过Facebook和Twitter等移动应用推广。拉下页面并释放,为更新近的帖子被加载。事实上,这种特殊用户体验非常流行,以至于Android这样的移动浏览器都采用了相同的效果。向下滑动页面顶部会刷新整个页面

(左边为原生拉到刷新操作,自定义拉到刷新,右边为原生拉到刷新操作刷新整个页面)

对于像Twitter PWA这样的情况,禁用本地“拉动到刷新”操作可能是有意义的。为什么?在这个应用程序中,你可能不希望用户不小心刷新页面。还有可能看到双刷新动画!另外,定制浏览器的动作可能会更好,并将其与网站的品牌更紧密地对齐。不幸的是,这种类型的定制很难实现。开发人员最终编写不必要的JavaScript,添加非被动触摸监听器(阻止滚动),或者将整个页面粘贴到100vw / vh中(以防止页面溢出)。这些变通办法在滚动性能方面具有良好记录的负面影响

引入overscroll行为

overscroll-behavior属性是一个新的CSS功能,用于控制当你过度滚动容器(包括页面本身)时发生的情况。你可以使用它来取消滚动链接,禁用/自定义拉动到刷新操作,禁用iOS上的橡皮圈效果(当Safari实现超滚动行为时)等等。最好的部分是,使用overscroll行为不会对页面性能产生负面影响

该属性有三个可能的值

  1. auto - 默认,源于元素的滚动可能会传播给祖先(父级)元素

  2. contain - 防止滚动链接。滚动不会传播给祖先,但会显示节点内的本地效果。例如,Android上的滚动滚动效果或iOS上的橡皮筋效果,它会在用户点击滚动边界时通知用户。注意:使用overscroll-behavior:包含html元素可防止超滚动导航操作

  3. none - 与包含相同,但它也可以防止节点本身内的超滚动效果(例如,Android超量滚动发光或iOS橡皮圈) 注意:overscroll-behavior还支持overscroll-behavior-x和overscroll-behavior-y的简写,如果您只想定义特定轴的行为

让我们深入一些例子来看看如何使用overscroll-behavior

防止滚动逃离固定位置元素

chatbox聊天场景

考虑位于页面底部的固定定位聊天室。其意图是聊天室是独立的组件,它与它后面的内容分开滚动。但是,由于滚动链接,只要用户点击聊天历史记录中的最后一条消息,文档就开始滚动

对于这个应用程序,让chatbox内的滚动内容始终处于聊天状态更为合适。我们可以通过添加超滚动 `overscroll-behavior:contain行为来实现这一点:包含持有聊天消息的元素


     
     
     
     
  1. #chat .msgs {

  2.  overflow: auto;

  3.  overscroll-behavior: contain;

  4.  height: 300px;

  5. }

本质上,我们创建了聊天室的滚动上下文和主页面之间的逻辑分隔。最终的结果是当用户到达聊天记录的顶部/底部时,主页面保持放置状态。在聊天框中开始的滚动不会传播出去

控制页面的滚动:自定义下拉到刷新和溢出效果_第3张图片

(聊天窗口下的内容也会滚动)

页面重叠场景

下面”方案的另一个变动就是是当你看到内容在固定位置叠加后滚动时。一个死的样品overscroll行为是为了!浏览器试图帮助,但它最终使网站看起来越来越多。

示例 - 带和不带过度滚动行为的模态:包含

(左边之前:页面内容在叠加层下滚动,右边之后:页面内容不会在叠加层下滚动)

禁用拉到刷新

关闭pull-to-refresh操作是一行CSS。只要阻止整个视口定义元素的滚动链接。在大多数情况下,这是


     
     
     
     
  1.  body {

  2.  /* 禁用“拉到刷新”功能,但允许发生滚动发光效果 Disables pull-to-refresh but allows overscroll glow effects. */

  3.  overscroll-behavior-y: contain;

  4. }

通过这个简单的添加,我们修复了聊天框演示中的双拉到更新动画,并且可以实现使用整洁加载动画的自定义效果。收件箱刷新时整个收件箱也会变模糊 

控制页面的滚动:自定义下拉到刷新和溢出效果_第4张图片控制页面的滚动:自定义下拉到刷新和溢出效果_第5张图片

左边为之前,右边为之后)

以下是完整代码的一部分


     
     
     
     
  1. class="refresher">

  2.   class="loading-bar">

  •   class="loading-bar">

  •   class="loading-bar">

  •   class="loading-bar">

  • id="inbox">

  • 禁用超滚色条纹和橡皮条纹效果

    要在滚动边界时禁用反弹效果(橡皮筋效果),请使用 overscroll-behavior-y:none:

    
         
         
         
         
    1. body {

    2.  /* 禁用拉到刷新和过卷滚发光效果。 仍然保持滑动导航Disables pull-to-refresh and overscroll glow effect.

    3.     Still keeps swipe navigations. */

    4.     overscroll-behavior-y: none;

    5. }

    控制页面的滚动:自定义下拉到刷新和溢出效果_第6张图片控制页面的滚动:自定义下拉到刷新和溢出效果_第7张图片

    左边之前:下拉滚动边界显示辉光,右边之后:下拉时辉光禁用

    注意:这仍然会保留左/右滑动导航。为了防止导航,你可以使用overscroll-behavior-x:none

    完整Demo

    把它放在一起,完整的聊天框演示,使用overscroll-behavior行为来创建一个自定义的拉动到刷新动画,并禁用滚动从转义聊天室小部件。这提供了一个最佳的用户体验,如果没有CSS过度滚动行为

    这是示例中HTML结构代码:

    
         
         
         
         
    1.        

    2.        

    3.            

    4.                

      关注微信itclanCoder公众号...

    5.            

  •        

  •        

  •        

  •         class="refresher">

  •             class="loading-bar">

  •             class="loading-bar">

  •             class="loading-bar">

  •             class="loading-bar">

  •        

  •        

  •         id="inbox">

  •            

  •        

  •        

  •         title="与itclancoder聊天" open>

  •             class="msg">我们聊聊吧

  •             class="msg">我是川川

  •             class="msg">你好啊

  •             class="msg">

  •             class="msg">聊天止于嘻嘻,哈哈,哦哦,嗯

  •             class="msg">全宇宙第一帅

  •             class="msg">没有之一

  •             class="msg">在此窗口中滚动不会滚动页面后面的页面

  •             class="msg">这信息包含有overflow:auto and uses overscroll-behavior-y:contain 去阻止这个默认行为.

  •        

  •    

  •     id="chat-window-template">

  •        

  •         type="text/css">

  •         :host {

  •            display: block;

  •            max-width: 250px;

  •            background: #fff;

  •            contain: content;

  •        }

  •         :host([open]) .msgs {

  •            display: flex;

  •        }

  •        .toolbar {

  •            padding: 8px;

  •            background: #404040;

  •            color: #fff;

  •            display: flex;

  •            justify-content: space-between;

  •            align-items: center;

  •            border-top-left-radius: 3px;

  •            border-top-right-radius: 3px;

  •            cursor: pointer;

  •            user-select: none;

  •        }

  •        .toolbar #title {

  •            text-overflow: ellipsis;

  •            white-space: nowrap;

  •            overflow: hidden;

  •        }

  •        .toolbar #close {

  •            font-size: inherit;

  •            background: none;

  •            border: none;

  •            color: inherit;

  •        }

  •        .msgs {

  •            border-left: 1px solid #ccc;

  •            border-right: 1px solid #ccc;

  •            display: flex;

  •            flex-direction: column;

  •            align-items: start;

  •            height: 300px;

  •            overflow: auto;

  •            overscroll-behavior-y: contain;

  •        }

  •         ::slotted(.msg) {

  •            padding: 8px 16px;

  •            margin: 8px;

  •            border-radius: 5px;

  •            background-color: #eee;

  •        }

  •         ::slotted(.msg:nth-child(even)) {

  •            align-self: flex-end;

  •        }

  •        #input-container {

  •            border: 1px solid #ccc;

  •            border-top: 1px solid #aaa;

  •        }

  •        #input-container input {

  •            padding: 8px;

  •            font-size: inherit;

  •            width: 100%;

  •            height: 100%;

  •            border: none;

  •            box-sizing: border-box;

  •        }

  •        .msgs-container {

  •            display: none;

  •        }

  •         :host([open]) .msgs-container {

  •            display: block;

  •        }

  •        

  •        

  •         class="toolbar">

  •             id="title">

  •             id="close">????

  •        

  •        

  •         class="msgs-container">

  •             class="msgs">

  •                

  •            

  •            

  •             id="input-container">

  •                 type="text" placeholder="Enter text">

  •            

  •        

  •        

  •    

  • 这是css代码

    
         
         
         
         
    1. @charset "UTF-8";

    2. /**

    3. *

    4. * @authors 随笔川迹 ([email protected])

    5. * @date    2018-04-05 01:53:00

    6. * @version $Id$

    7. * @link (https://juejin.im/post/5a005392518825295f5d53c8)

    8. * @weChatPublicId (itclanCoder)

    9. * @QQGroup (643468880)

    10. * @PersonWeChatId (suibichuanji)

    11. * @PersonQQ (1046678249)

    12. * @describe 功能描述 禁用固定位置元素上的滚动链接Demo css样式

    13. *

    14. *

    15. */

    16. * {

    17.    box-sizing: border-box;

    18. }

    19. html {

    20.    /*

    21.      --是css Houndini,是一套正在到来的css APi,css对变量的支持,允许在css中

    22.      声明如--height,--width的自定义属性,而后通过var()函数对变量求值,可以理解为简化版的less/sass,但是它不能通过DOM API存取

    23.     */

    24.    --header-height: 60px;

    25. }

    26. html,

    27. body {

    28.    font-family: "Open Sans", sans-serif;

    29.    font-weight: 300;

    30.    font-size: 16px;

    31.    background: #fafafa;

    32.    margin: 0;

    33.    overscroll-behavior-y: contain;

    34.    /* 禁用下拉刷新,保持发光效果 disable pull to refresh, keeps glow effects */

    35. }

    36. h1,

    37. h2,

    38. h3 {

    39.    margin: 0;

    40.    font-weight: inherit;

    41. }

    42. header {

    43.    padding: 0 8px;

    44.    background: #f44336;

    45.    color: #fff;

    46.    position: fixed;

    47.    top: 0;

    48.    left: 0;

    49.    right: 0;

    50.    z-index: 2;

    51.    height: var(--header-height);

    52.    display: flex;

    53.    align-items: center;

    54.    justify-content: space-between;

    55. }

    56. #inbox {

    57.    padding-top: var(--header-height);

    58.    /* height of header */

    59. }

    60. chat-window {

    61.    position: fixed;

    62.    bottom: 0;

    63.    right: 16px;

    64. }

    65. p {

    66.    /*属性设置某个选择器出现次数的计数器的值。默认为 0,利用这个属性,计数器可以设置或重置为任何值,可以是正值或负值。如果没有提供 number,则默认为 0。*/

    67.    counter-reset: email;

    68. }

    69. p div {

    70.    margin: 0;

    71.    padding: 16px 8px;

    72.    border-top: 1px solid #ccc;

    73.    display: flex;

    74.    justify-content: space-between;

    75. }

    76. p .label::after {

    77.    counter-increment: email;

    78.    content: counter(email);

    79.    margin-left: 8px;

    80. }

    81. body.refreshing #inbox,

    82. body.refreshing header {

    83.    filter: blur(1px);

    84.    /*滤镜,1像素模糊*/

    85.    touch-action: none;

    86.    /* 防止滚动 prevent scrolling */

    87. }

    88. body.refreshing .refresher {  /*下拉刷新小图标*/

    89.    transform: translate3d(0, 150%, 0) scale(1);

    90.    z-index: 1;

    91.    visibility: visible;

    92. }

    93. .refresher {

    94.    /*

    95.      pointer-events即可让这个HTML元素(包括它的所有子孙元素)失去所有的事件响应。鼠标焦点会直接无视它,click、mouseover等所有事件会穿透它到达它的下一级元素

    96.      1. 阻止用户的点击动作产生任何效果

    97.      2. 阻止缺省鼠标指针的显示

    98.      3. 阻止CSS里的hover和active状态的变化触发事件

    99.      4. 阻止JavaScript点击动作触发的事件

    100.      在许多网站上过节的时候页面最上层会用canvas绘制的雨、雪花,避免这些悬浮物遮挡住页面从而影响鼠标点击,可以使用pointer-events=none属性,让这些上方的canvas不会遮挡鼠标事件,让鼠标事件可以穿透上方的canvas来点击页面

    101.    */

    102.    pointer-events: none;

    103.    --refresh-width: 55px;

    104.    background: #fff;

    105.    width: var(--refresh-width);

    106.    height: var(--refresh-width);

    107.    border-radius: 50%;

    108.    position: absolute;

    109.    left: calc(50% - var(--refresh-width) / 2);

    110.    padding: 8px;

    111.    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),

    112.    0 1px 5px 0 rgba(0, 0, 0, 0.12),

    113.    0 3px 1px -2px rgba(0, 0, 0, 0.2);

    114.    transition: all 300ms cubic-bezier(0, 0, 0.2, 1);

    115.    will-change: transform, opacity;

    116.    /*使用了CSS3 will-change加速创建新的渲染层*/

    117.    display: inline-flex;

    118.    justify-content: space-evenly;

    119.    align-items: center;

    120.    visibility: hidden;

    121. }

    122. body.refreshing .refresher.shrink {

    123.    transform: translate3d(0, 150%, 0) scale(0);

    124.    opacity: 0;

    125. }

    126. .refresher.done {

    127.    transition: none;

    128. }

    129. .loading-bar {

    130.    width: 4px;

    131.    height: 18px;

    132.    border-radius: 4px;

    133.    animation: loading 1s ease-in-out infinite;

    134. }

    135. .loading-bar:nth-child(1) {

    136.    background-color: #3498db;

    137.    animation-delay: 0;

    138. }

    139. .loading-bar:nth-child(2) {

    140.    background-color: #c0392b;

    141.    animation-delay: 0.09s;

    142. }

    143. .loading-bar:nth-child(3) {

    144.    background-color: #f1c40f;

    145.    animation-delay: .18s;

    146. }

    147. .loading-bar:nth-child(4) {

    148.    background-color: #27ae60;

    149.    animation-delay: .27s;

    150. }

    151. @keyframes loading {

    152.    0% {

    153.        transform: scale(1);

    154.    }

    155.    20% {

    156.        transform: scale(1, 2.2);

    157.    }

    158.    40% {

    159.        transform: scale(1);

    160.    }

    161. }

    这是示例的js代码

    
         
         
         
         
    1. /**

    2. *

    3. * @authors 随笔川迹 ([email protected])

    4. * @date    2018-04-05 01:56:33

    5. * @version $Id$

    6. * @weChatPublicId ((itclanCoder))

    7. * @QQGroup ((643468880))

    8. * @PersonWeChatId ((suibichuanji))

    9. * @PersonQQ ((1046678249))

    10. * @link ((https://juejin.im/post/5a005392518825295f5d53c8))

    11. * @describe 禁用固定位置元素上的滚动链js

    12. */

    13. (() => {

    14.    if (!CSS.supports('overscroll-behavior-y', 'contain')) {

    15.        alert("Your browser doesn't support overscroll-behavior :(");

    16.    }

    17.    // 定义自定义元素 Define custom element.

    18.    customElements.define('chat-window', class extends HTMLElement {

    19.        constructor() {

    20.            super();

    21.            const shadowRoot = this.attachShadow({ mode: 'open' });

    22.            const tmpl = document.querySelector('#chat-window-template');

    23.            shadowRoot.appendChild(tmpl.content.cloneNode(true));

    24.            shadowRoot.querySelector('#title').textContent = this.title;

    25.            const closeButton = shadowRoot.querySelector('#close');

    26.            closeButton.addEventListener('click', e => {

    27.                this.remove();

    28.            });

    29.            this.msgs = shadowRoot.querySelector('.msgs');

    30.            this.input = shadowRoot.querySelector('input');

    31.            this.input.addEventListener('keypress', e => {

    32.                if (e.code === 'Enter' && this.input.value) {

    33.                    const msg = document.createElement('div');

    34.                    msg.classList.add('msg');

    35.                    msg.textContent = this.input.value;

    36.                    this.appendChild(msg);

    37.                    e.target.value = null;

    38.                    this._scrollToBottom();

    39.                }

    40.            });

    41.            const toolbar = shadowRoot.querySelector('.toolbar');

    42.            toolbar.addEventListener('click', e => {

    43.                this.open = !this.open;

    44.                if (this.open) {

    45.                    this.input.focus();

    46.                } else {

    47.                    this.input.blur();

    48.                    this.blur();

    49.                }

    50.            });

    51.            this.tabIndex = 0;

    52.            this.open = this.hasAttribute('open');

    53.        }

    54.        get open() {

    55.            return this._open;

    56.        }

    57.        set open(val) {

    58.            this._open = val;

    59.            if (this._open) {

    60.                this.setAttribute('open', '');

    61.                this._scrollToBottom();

    62.            } else {

    63.                this.removeAttribute('open');

    64.            }

    65.        }

    66.        _scrollToBottom() {

    67.            this.msgs.scrollTop = this.msgs.scrollHeight;

    68.        }

    69.    });

    70.    async function simulateRefreshAction() {

    71.        const sleep = (timeout) => new Promise(resolve => setTimeout(resolve, timeout));

    72.        const transitionEnd = function(propertyName, node) {

    73.            return new Promise(resolve => {

    74.                function callback(e) {

    75.                    e.stopPropagation();

    76.                    if (e.propertyName === propertyName) {

    77.                        node.removeEventListener('transitionend', callback);

    78.                        resolve(e);

    79.                    }

    80.                }

    81.                node.addEventListener('transitionend', callback);

    82.            });

    83.        }

    84.        const refresher = document.querySelector('.refresher');

    85.        document.body.classList.add('refreshing');

    86.        await sleep(2000);

    87.        refresher.classList.add('shrink');

    88.        await transitionEnd('transform', refresher);

    89.        refresher.classList.add('done');

    90.        refresher.classList.remove('shrink');

    91.        document.body.classList.remove('refreshing');

    92.        await sleep(0); // let new styles settle.

    93.        refresher.classList.remove('done');

    94.    }

    95.    function getRandomIntInclusive(min, max) {

    96.        min = Math.ceil(min);

    97.        max = Math.floor(max);

    98.        return Math.floor(Math.random() * (max - min + 1)) + min;

    99.    }

    100.    function formatDateBasedOnToday(date) {

    101.        const today = new Date();

    102.        const opts = {};

    103.        if (date.getDay() === today.getDay()) {

    104.            opts.minute = 'numeric';

    105.            opts.hour = 'numeric';

    106.        } else {

    107.            opts.month = 'short';

    108.            opts.day = 'numeric';

    109.        }

    110.        return new Intl.DateTimeFormat('en-US', opts).format(date);

    111.    }

    112.    function populatePage(inbox) {

    113.        const frag = new DocumentFragment();

    114.        let date = new Date();

    115.        for (let i = 0; i < NUM_EMAILs; ++i) {

    116.            date.setMinutes(date.getMinutes() - getRandomIntInclusive(1, 10));

    117.            const div = document.createElement('div');

    118.            div.innerHTML = `Email${formatDateBasedOnToday(date)}`;

    119.            frag.appendChild(div);

    120.        }

    121.        inbox.appendChild(frag);

    122.    }

    123.    const NUM_EMAILs = 100;

    124.    let _startY = 0;

    125.    const inbox = document.querySelector('#inbox');

    126.    inbox.addEventListener('touchstart', e => {

    127.        _startY = e.touches[0].pageY;

    128.    }, { passive: true });

    129.    inbox.addEventListener('touchmove', e => {

    130.        const y = e.touches[0].pageY;

    131.        // Activate custom pull-to-refresh effects when at the top fo the container

    132.        // and user is scrolling up.

    133.        if (document.scrollingElement.scrollTop === 0 && y > _startY &&

    134.            !document.body.classList.contains('refreshing')) {

    135.            simulateRefreshAction();

    136.        }

    137.    }, { passive: true });

    138.    populatePage(inbox);

    139. })();

    140. (function(i, s, o, g, r, a, m) {

    141.    i['GoogleAnalyticsObject'] = r;

    142.    i[r] = i[r] || function() {

    143.        (i[r].q = i[r].q || []).push(arguments)

    144.    }, i[r].l = 1 * new Date();

    145.    a = s.createElement(o),

    146.        m = s.getElementsByTagName(o)[0];

    147.    a.async = 1;

    148.    a.src = g;

    149.    m.parentNode.insertBefore(a, m)

    150. })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

    151. ga('create', 'UA-43475701-1', 'ebidel.github.io');

    152. ga('send', 'pageview');

    最终示例效果如下所示

    控制页面的滚动:自定义下拉到刷新和溢出效果_第8张图片

    (示例效果)

    总结

    本文主要是针对页面上的滚动,自定义下拉刷新与溢出效果,通过css中的overscroll-behavior:container阻止滚动链接,也就是在触发子元素的事件操作时,不会传递给父级元素,相当于是阻止事件的冒泡,当然阻止滚动链接在页面上有水平方向的,也有垂直方向的,垂直方向上设置overscroll-behavior-y:none:时可在下拉滚动时禁用反弹效果(橡皮筋效果)

    当然文中的刷新动画效果是css3的@keyframes的,当然还有解决这种溢出,系统默认滚动条,橡皮筋回弹,以及禁止长按选中文字,选中图片,系统默认菜单,事件点透问题时可以使用document.addEventListener('touchstart',function(ev){ev.preventDefault();})解决

    Demo源码地止:https://ebidel.github.io/demos/chatbox.html

    原文出处:httpsdevelopers.google.comwebupdates201711overscroll-behaviorutm_source=mobiledevweekly&utm_medium=email)

    [](https://mobiledevweekly.com/issues/185

    推荐阅读:

    作者川川,一个靠前排的90后帅小伙,具有情怀的代码男,路上正追逐斜杠青年的践行者,愿做你耳朵旁边的枕男,眼睛笔尖下的窗户

    随手点赞

    手留余香


    (个人微信:suibichuanji) 

    你可能感兴趣的:(控制页面的滚动:自定义下拉到刷新和溢出效果)