前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >控制页面的滚动:自定义下拉到刷新和溢出效果

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

作者头像
itclanCoder
发布2020-10-28 11:38:06
3.3K0
发布2020-10-28 11:38:06
举报
文章被收录于专栏:itclanCoder

前言

通过阅读本文,你可以通过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模式可能很难处理。作为一个例子,带一个应用程序抽屉带有大量用户可能需要滚动的项目。当它们到达底部时,溢出容器将停止滚动,因为没有更多内容可供使用。换句话说,用户到达“滚动边界”。但是请注意,如果用户继续滚动会发生什么情况。抽屉后面的内容开始滚动!滚动由父容器占领;例子中的主页面本身

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

(在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行为来实现这一点:包含持有聊天消息的元素

代码语言:javascript
复制
#chat .msgs {
  overflow: auto;
  overscroll-behavior: contain;
  height: 300px;
}

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

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

页面重叠场景

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

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

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

禁用拉到刷新

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

代码语言:javascript
复制
body {
  /* 禁用“拉到刷新”功能,但允许发生滚动发光效果 Disables pull-to-refresh but allows overscroll glow effects. */
  overscroll-behavior-y: contain;
}

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

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

以下是完整代码的一部分

代码语言:javascript
复制
<style>
  body.refreshing #inbox {
    filter: blur(1px);
    touch-action: none; /* 防止滚动 prevent scrolling */
  }
  body.refreshing .refresher {
    transform: translate3d(0,150%,0) scale(1);
    z-index: 1;
  }
  .refresher {
    --refresh-width: 55px;
    pointer-events: none;
    width: var(--refresh-width);
    height: var(--refresh-width);
    border-radius: 50%; 
    position: absolute;
    transition: all 300ms cubic-bezier(0,0,0.2,1);
    will-change: transform, opacity;
    ...
  }
</style>

<div class="refresher">
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
  <div class="loading-bar"></div>
</div>

<section id="inbox"><!-- msgs --></section>

<script>
  let _startY;
  const inbox = document.querySelector('#inbox');

  inbox.addEventListener('touchstart', e => {
    _startY = e.touches[0].pageY;
  }, {passive: true});

  inbox.addEventListener('touchmove', e => {
    const y = e.touches[0].pageY;
    // 在容器顶部时激活自定义的拉到刷新效果 Activate custom pull-to-refresh effects when at the top of the container
    // 用户正在滚动 and user is scrolling up.
    if (document.scrollingElement.scrollTop === 0 && y > _startY &&
        !document.body.classList.contains('refreshing')) {
      // refresh inbox.
    }
  }, {passive: true});
</script>

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

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

代码语言:javascript
复制
body {
  /* 禁用拉到刷新和过卷滚发光效果。 仍然保持滑动导航Disables pull-to-refresh and overscroll glow effect.
     Still keeps swipe navigations. */
     overscroll-behavior-y: none;
}

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

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

完整Demo

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

这是示例中HTML结构代码:

代码语言:javascript
复制
<main>
        <!-- header start -->
        <header>
            <div>
                <h2>关注微信itclanCoder公众号...</h2>
            </div>
        </header>
        <!-- header end -->
        <!-- 顶部下拉刷新的小图标start -->
        <div class="refresher">
            <div class="loading-bar"></div>
            <div class="loading-bar"></div>
            <div class="loading-bar"></div>
            <div class="loading-bar"></div>
        </div>
        <!-- 顶部下拉花心的小图标end -->
        <section id="inbox">
            <!-- filled dynamically -->
        </section>
        <!-- 聊天框开始 -->
        <chat-window title="与itclancoder聊天" open>
            <div class="msg">我们聊聊吧</div>
            <div class="msg">我是川川</div>
            <div class="msg">你好啊</div>
            <div class="msg">嗯</div>
            <div class="msg">聊天止于嘻嘻,哈哈,哦哦,嗯</div>
            <div class="msg">全宇宙第一帅</div>
            <div class="msg">没有之一</div>
            <div class="msg">在此窗口中滚动不会滚动页面后面的页面</div>
            <div class="msg">这信息包含有<code>overflow:auto</code> and uses <code>overscroll-behavior-y:contain</code> 去阻止这个默认行为.</div>
        </chat-window>
    </main>
    <template id="chat-window-template">
        <!-- 注意这里的css只能放在里面,放到外面去的话,样式不起作用 -->
        <style 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;
        }
        </style>
        <!-- 聊天框顶部开始 -->
        <div class="toolbar">
            <span id="title"></span>
            <button id="close">?</button>
        </div>
        <!-- 聊天框顶部结束 -->

        <div class="msgs-container">
            <div class="msgs">
                <slot></slot>
            </div>
            <!-- 底下输入 -->
            <div id="input-container">
                <input type="text" placeholder="Enter text">
            </div>
        </div>
        <!-- 聊天框结束 -->
    </template>

这是css代码

代码语言:javascript
复制
@charset "UTF-8";

/**
 * 
 * @authors 随笔川迹 (itclanCode@163.com)
 * @date    2018-04-05 01:53:00
 * @version $Id$
 * @link (https://juejin.im/post/5a005392518825295f5d53c8)
 * @weChatPublicId (itclanCoder)
 * @QQGroup (643468880)
 * @PersonWeChatId (suibichuanji)
 * @PersonQQ (1046678249)
 * @describe 功能描述 禁用固定位置元素上的滚动链接Demo css样式
 *
 *
 */

* {
    box-sizing: border-box;
}

html {
    /*
      --是css Houndini,是一套正在到来的css APi,css对变量的支持,允许在css中
      声明如--height,--width的自定义属性,而后通过var()函数对变量求值,可以理解为简化版的less/sass,但是它不能通过DOM API存取
     */
    --header-height: 60px;
}

html,
body {
    font-family: "Open Sans", sans-serif;
    font-weight: 300;
    font-size: 16px;
    background: #fafafa;
    margin: 0;
    overscroll-behavior-y: contain;
    /* 禁用下拉刷新,保持发光效果 disable pull to refresh, keeps glow effects */
}

h1,
h2,
h3 {
    margin: 0;
    font-weight: inherit;
}

header {
    padding: 0 8px;
    background: #f44336;
    color: #fff;
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    z-index: 2;
    height: var(--header-height);
    display: flex;
    align-items: center;
    justify-content: space-between;
}

#inbox {
    padding-top: var(--header-height);
    /* height of header */
}

chat-window {
    position: fixed;
    bottom: 0;
    right: 16px;
}

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

section div {
    margin: 0;
    padding: 16px 8px;
    border-top: 1px solid #ccc;
    display: flex;
    justify-content: space-between;
}

section .label::after {
    counter-increment: email;
    content: counter(email);
    margin-left: 8px;
}

body.refreshing #inbox,
body.refreshing header {
    filter: blur(1px);
    /*滤镜,1像素模糊*/
    touch-action: none;
    /* 防止滚动 prevent scrolling */
}

body.refreshing .refresher {  /*下拉刷新小图标*/
    transform: translate3d(0, 150%, 0) scale(1);
    z-index: 1;
    visibility: visible;
}

.refresher {
    /*
      pointer-events即可让这个HTML元素(包括它的所有子孙元素)失去所有的事件响应。鼠标焦点会直接无视它,click、mouseover等所有事件会穿透它到达它的下一级元素
      1. 阻止用户的点击动作产生任何效果
      2. 阻止缺省鼠标指针的显示
      3. 阻止CSS里的hover和active状态的变化触发事件
      4. 阻止JavaScript点击动作触发的事件
      在许多网站上过节的时候页面最上层会用canvas绘制的雨、雪花,避免这些悬浮物遮挡住页面从而影响鼠标点击,可以使用pointer-events=none属性,让这些上方的canvas不会遮挡鼠标事件,让鼠标事件可以穿透上方的canvas来点击页面
    */
    pointer-events: none;
    --refresh-width: 55px;
    background: #fff;
    width: var(--refresh-width);
    height: var(--refresh-width);
    border-radius: 50%;
    position: absolute;
    left: calc(50% - var(--refresh-width) / 2);
    padding: 8px;
    box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 1px 5px 0 rgba(0, 0, 0, 0.12),
    0 3px 1px -2px rgba(0, 0, 0, 0.2);
    transition: all 300ms cubic-bezier(0, 0, 0.2, 1);
    will-change: transform, opacity;
    /*使用了CSS3 will-change加速创建新的渲染层*/
    display: inline-flex;
    justify-content: space-evenly;
    align-items: center;
    visibility: hidden;
}

body.refreshing .refresher.shrink {
    transform: translate3d(0, 150%, 0) scale(0);
    opacity: 0;
}

.refresher.done {
    transition: none;
}

.loading-bar {
    width: 4px;
    height: 18px;
    border-radius: 4px;
    animation: loading 1s ease-in-out infinite;
}

.loading-bar:nth-child(1) {
    background-color: #3498db;
    animation-delay: 0;
}

.loading-bar:nth-child(2) {
    background-color: #c0392b;
    animation-delay: 0.09s;
}

.loading-bar:nth-child(3) {
    background-color: #f1c40f;
    animation-delay: .18s;
}

.loading-bar:nth-child(4) {
    background-color: #27ae60;
    animation-delay: .27s;
}

@keyframes loading {
    0% {
        transform: scale(1);
    }
    20% {
        transform: scale(1, 2.2);
    }
    40% {
        transform: scale(1);
    }
}

这是示例的js代码

代码语言:javascript
复制
/**
 * 
 * @authors 随笔川迹 (itclanCode@163.com)
 * @date    2018-04-05 01:56:33
 * @version $Id$
 * @weChatPublicId ((itclanCoder))
 * @QQGroup ((643468880))
 * @PersonWeChatId ((suibichuanji))
 * @PersonQQ ((1046678249))
 * @link ((https://juejin.im/post/5a005392518825295f5d53c8))
 * @describe 禁用固定位置元素上的滚动链js
 */
(() => {
    if (!CSS.supports('overscroll-behavior-y', 'contain')) {
        alert("Your browser doesn't support overscroll-behavior :(");
    }
    // 定义<chat-window>自定义元素 Define <chat-window> custom element.
    customElements.define('chat-window', class extends HTMLElement {
        constructor() {
            super();

            const shadowRoot = this.attachShadow({ mode: 'open' });
            const tmpl = document.querySelector('#chat-window-template');
            shadowRoot.appendChild(tmpl.content.cloneNode(true));

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

            const closeButton = shadowRoot.querySelector('#close');
            closeButton.addEventListener('click', e => {
                this.remove();
            });

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

            this.input = shadowRoot.querySelector('input');
            this.input.addEventListener('keypress', e => {
                if (e.code === 'Enter' && this.input.value) {
                    const msg = document.createElement('div');
                    msg.classList.add('msg');
                    msg.textContent = this.input.value;
                    this.appendChild(msg);
                    e.target.value = null;
                    this._scrollToBottom();
                }
            });

            const toolbar = shadowRoot.querySelector('.toolbar');
            toolbar.addEventListener('click', e => {
                this.open = !this.open;
                if (this.open) {
                    this.input.focus();
                } else {
                    this.input.blur();
                    this.blur();
                }
            });

            this.tabIndex = 0;
            this.open = this.hasAttribute('open');
        }

        get open() {
            return this._open;
        }

        set open(val) {
            this._open = val;
            if (this._open) {
                this.setAttribute('open', '');
                this._scrollToBottom();
            } else {
                this.removeAttribute('open');
            }
        }

        _scrollToBottom() {
            this.msgs.scrollTop = this.msgs.scrollHeight;
        }
    });
    async function simulateRefreshAction() {
        const sleep = (timeout) => new Promise(resolve => setTimeout(resolve, timeout));
        const transitionEnd = function(propertyName, node) {
            return new Promise(resolve => {
                function callback(e) {
                    e.stopPropagation();
                    if (e.propertyName === propertyName) {
                        node.removeEventListener('transitionend', callback);
                        resolve(e);
                    }
                }
                node.addEventListener('transitionend', callback);
            });
        }
        const refresher = document.querySelector('.refresher');
        document.body.classList.add('refreshing');
        await sleep(2000);
        refresher.classList.add('shrink');
        await transitionEnd('transform', refresher);
        refresher.classList.add('done');
        refresher.classList.remove('shrink');
        document.body.classList.remove('refreshing');
        await sleep(0); // let new styles settle.
        refresher.classList.remove('done');
    }

    function getRandomIntInclusive(min, max) {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min + 1)) + min;
    }

    function formatDateBasedOnToday(date) {
        const today = new Date();
        const opts = {};
        if (date.getDay() === today.getDay()) {
            opts.minute = 'numeric';
            opts.hour = 'numeric';
        } else {
            opts.month = 'short';
            opts.day = 'numeric';
        }
        return new Intl.DateTimeFormat('en-US', opts).format(date);
    }

    function populatePage(inbox) {
        const frag = new DocumentFragment();
        let date = new Date();
        for (let i = 0; i < NUM_EMAILs; ++i) {
            date.setMinutes(date.getMinutes() - getRandomIntInclusive(1, 10));
            const div = document.createElement('div');
            div.innerHTML = `<span class="label">Email</span></span>${formatDateBasedOnToday(date)}</span>`;
            frag.appendChild(div);
        }
        inbox.appendChild(frag);
    }
    const NUM_EMAILs = 100;
    let _startY = 0;
    const inbox = document.querySelector('#inbox');
    inbox.addEventListener('touchstart', e => {
        _startY = e.touches[0].pageY;
    }, { passive: true });
    inbox.addEventListener('touchmove', e => {
        const y = e.touches[0].pageY;
        // Activate custom pull-to-refresh effects when at the top fo the container
        // and user is scrolling up.
        if (document.scrollingElement.scrollTop === 0 && y > _startY &&
            !document.body.classList.contains('refreshing')) {
            simulateRefreshAction();
        }
    }, { passive: true });
    populatePage(inbox);
})();
(function(i, s, o, g, r, a, m) {
    i['GoogleAnalyticsObject'] = r;
    i[r] = i[r] || function() {
        (i[r].q = i[r].q || []).push(arguments)
    }, i[r].l = 1 * new Date();
    a = s.createElement(o),
        m = s.getElementsByTagName(o)[0];
    a.async = 1;
    a.src = g;
    m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-43475701-1', 'ebidel.github.io');
ga('send', 'pageview');

最终示例效果如下所示

(示例效果)

总结

本文主要是针对页面上的滚动,自定义下拉刷新与溢出效果,通过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后帅小伙,具有情怀的代码男,路上正追逐斜杠青年的践行者,愿做你耳朵旁边的枕男,眼睛笔尖下的窗户

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2018-09-24,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 itclanCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 总结
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档