前端开发者学堂 - fedev.cn

改变用户体验的滚动新特性

发布于 大漠

@evilmartians的《滚动的特性》一文介绍了目前有关于滚动相关的特性。今天我想花点时间重新整理一下,时至今日,CSS中为浏览器滚动提供的相关新特性究竟能给用户带来哪些新的体验。

墨守成规的滚动条

一直以来,如果仅使用CSS来控制滚动条,我们只能借用overflow属性,比如:

overflow: auto | scroll;

// 或者
overflow-x: auto | scroll;

// 或者
overflow-y: auto | scroll;

当元素内容溢出容器之后,就会出现滚动条,其滚动效果如下:

这样的效果大家或许已经习惯,觉得页面或者元素滚动的效果就应该如此。甚至在Modal弹框中的滚动效果,大家觉得也应该是如此:

除了滚动体验之外,视觉体验相对而言更为糟糕,不同系统的浏览器渲染的滚动条风格也不是一致:

对于大部分同学而言,这些东西就应该是如此,不应该也不会有太多的变化。但对于有追求的设计师或者工程师来说,还是不想墨守成规,想给用户带来不一样的体验,不管是视觉外观上,还是滚动流畅性。正因为如此,早期有很多优秀的JavaScript库来改变这一切。

随着技术的革新,就在现在或者未来的不久,我们可以采用纯CSS的一些特性来改变这一切,让用户有一个更好的体验。

改变滚动条外观效果

前面提到过了,滚动条外观的效果在不同系统存在不一样的效果已是事实。虽然在Github有上百个库可以让开发者实现个性化的滚动外观,但对于CSSer而言,能用纯CSS解决的问题就决不使用JavaScript来解决。

在Webkit内核提供了-webkit-scrollbar(由七个伪元素)属性,可以轻易的帮助我们实现自定义(个性化)滚动条UI风格。在介绍这七个伪元素属性之前,先来看一下滚动条的结构:

仅从上图来看,是不是有一种似曾相识。是的,它的结构和我们平时看见的进度条或input[type="range"]类似:

也就是说,我们可以像制作进度条一样来处理滚动条UI效果。不同的是采用的CSS属性不一样。刚才也提到了,-webkit-scrollbar提供了七个伪元素,通过这些伪元素,我们可以来定制滚动条外观效果。这七个伪元素分别是:

  • ::-webkit-scrollbar:整个滚动条
  • ::-webkit-scrollbar-button:滚动条上的按钮(下下箭头)
  • ::-webkit-scrollbar-thumb:滚动条上的滚动滑块
  • ::-webkit-scrollbar-track:滚动条轨道
  • ::-webkit-scrollbar-track-piece:滚动条没有滑块的轨道部分
  • ::-webkit-scrollbar-corner:当同时有垂直和水平滚动条时交汇的部分
  • ::-webkit-resizer:某些元素的交汇部分的部分样式(类似textarea的可拖动按钮)

具体使用的时候非常的简单。HTML结构和我们平时是一样的:

<div class="scrollbar">
    <div class="force-overflow"></div>
</div>

有关于滚动条的UI样式风格都在.scrollbar上设置,比如:

.scrollbar {}
.scrollbar::-webkit-scrollbar{}
.scrollbar::-webkit-scrollbar-thum{}
.scrollbar::-webkit-scrollbar-track{}

在对应的-webkit-scrollbar属性写上你想要的UI样式风格,你就可以得到对应的滚动条UI样式风格,比如下图这样的:

具体的代码可以查看@akinjideCodepen上写的Demo

有关于采用-webkit-scrollbar属性实现自定义滚动条UI效果的详细教程,可以阅读@Akinjide Bankole教程。其他教程也可以查看下面的文章:

如果你不想烧脑,也可以使用在线生成器,比如@Darryl Huffman写的Scrollbar Generator:

虽然能用纯CSS搞定这一切,肯定也有不少同学会好奇,现在能否用于实际项目当中。还是来看看浏览器对其支持性吧:

顠红的较少了,或许大家更为安心了。如果你想做得更好,那么可以借助在《五个最新的CSS特性以及如何使用它们》文中介绍的@supports特性来做降级处理。如果浏览器支持-webkit-scrollbar,那么采用自定义属性,如果不支持,则采用默认的滚动条风格。是不是很完美。

丝滑般的滚动

视差滚动效果,大家应该不会感到陌生吧:

视差效果曾经很多品牌网站都可见。有关于什么是视差滚动效果,这里不做详细的阐述,如果你从未接触过或者想了解如何制作视差滚动效果,建议你花点时间阅读下面两篇文章:

同样的,早期也涌现很多优秀的JavaScript库来实现视差滚动效果。只不过这里我们不是着重介绍怎么制作视差滚动效果。而是来聊聊,怎么通过使用CSS提供的滚动特性,实现丝滑般的滚动效果。而视差滚动效果是一个很好的示例。

先来看两个效果,一个是普通的滚动效果,另一个是采用了新特性的滚动效果:

普通滚动效果

普通滚动效果,大家常见的滚动效果

优化后的滚动效果

怎么实现后者这样丝滑般的滚动效果,才是接下来的目的。很多同学可能首先会想到jQuery.scrollTo方法或者类似的解决方法。但jQuery已经慢慢的淡出公众眼线。

不过也有可能立马想到原生的JavaScript,比如window.scrollTo(x, y)方法。更优秀的程序员可能会借助window.setTimeout()window.setInterval()Web Animation APIwindow.requestAnimationFrame()让滚动效果更为平滑,也就是我们想要给用户丝滑般的滚动体验。比@pawelgrzybek提供的这段代码:

function scrollIt(destination, duration = 200, easing = 'linear', callback) {

    const easings = {
        linear(t) {
            return t;
        },
        easeInQuad(t) {
            return t * t;
        },
        easeOutQuad(t) {
            return t * (2 - t);
        },
        easeInOutQuad(t) {
            return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
        },
        easeInCubic(t) {
            return t * t * t;
        },
        easeOutCubic(t) {
            return (--t) * t * t + 1;
        },
        easeInOutCubic(t) {
            return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
        },
        easeInQuart(t) {
            return t * t * t * t;
        },
        easeOutQuart(t) {
            return 1 - (--t) * t * t * t;
        },
        easeInOutQuart(t) {
            return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
        },
        easeInQuint(t) {
            return t * t * t * t * t;
        },
        easeOutQuint(t) {
            return 1 + (--t) * t * t * t * t;
        },
        easeInOutQuint(t) {
            return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
        }
    };

    const start = window.pageYOffset;
    const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();

    const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
    const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
    const destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop;
    const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);

    if ('requestAnimationFrame' in window === false) {
        window.scroll(0, destinationOffsetToScroll);
            if (callback) {
            callback();
        }
        return;
    }

    function scroll() {
        const now = 'now' in window.performance ? performance.now() : new Date().getTime();
        const time = Math.min(1, ((now - startTime) / duration));
        const timeFunction = easings[easing](time);
        window.scroll(0, Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start));

        if (window.pageYOffset === destinationOffsetToScroll) {
            if (callback) {
                callback();
            }
            return;
        }

        requestAnimationFrame(scroll);
    }

    scroll();
}

调用方式很简单,像下面这样即可:

document.querySelector('.js-btn1').addEventListener('click', () => {
    scrollIt(
        document.querySelector('.js-section1'),
        300,
        'easeOutQuad',
        () => console.log(`Just finished scrolling to ${window.pageYOffset}px`)
    );
});

再简单点还可以这样使用:

document.querySelector('.js-btn50000').addEventListener('click', () => scrollIt(50000));

你将看到的效果如下:

除此之外,还可以使用JavaScript原生的API,比如:

window.scrollBy({
    "behavior": "smooth",
    "left": left,
    "top": top
});
window.scrollTo({
    "behavior": "smooth",
    "left": left,
    "top": top
});

你也可以使用Element.scrollIntoView()方法:

document.querySelector('.hello').scrollIntoView({ 
    behavior: 'smooth' 
});

上面看到的都是JavaScript方式来解决。还是回到原话题上,CSS能不能解决。回答是可以的。CSSOM View Module提供了一个新属性scroll-behavior。直接使用这个属性就可以轻易的帮助我们实现丝滑般的滚动效果。

CSS属性 scroll-behavior 为一个滚动框指定滚动行为,其他任何的滚动,例如那些由于用户行为而产生的滚动,不受这个属性的影响。在根元素中指定这个属性时,它反而适用于视窗。

scroll-behavior有两个关键词值:

  • auto:滚动框立即滚动
  • smooth:滚动框通过一个用户代理定义的时间段使用定义的时间函数来实现平稳的滚动,用户代理平台应遵循约定,如果有的话

除此之外,其还有三个全局的值:inheritinitialunset(有关于这三个词的详细介绍可以点击这里)。

使用起来很简单,只需要这个元素上使用scroll-behavior:smooth。因此,很多时候为了让页面滚动更平滑,建议在html中直接这样设置一个样式:

html {
    scroll-behavior:smooth;
}

口说无凭,来看个效果对比,你会有更好的感觉:

那么回到最初的示例,使用这个属性就可以搞定了,最终效果如下:

这样的体验是不是好多了。不过浏览器对其支持度相对而言还是一般般:

虽然支持度不是很完美,但如果你想在项目中使用也是无仿的,对于不支持的浏览器,可以采用该属性的polyfill

有关于scroll-behavior属性更多的介绍可以再花点时间阅读下面这些文章:

优化滚动边界

上图这样的效果,对于我们而言再正常不过了。当用户滚动到(Modal弹框或下拉列表)末尾(后再继续滚动时),整个页面都会开始滚动。而我们期望的效果是:

滚动到底部时,滚动停止,因为我们到达了“滚动边界(Scroll Boundary)”。但事实上,在Web页面中滚动并不会停止,而继续滚动抽屉(弹框或下拉列表)后面的内容(页面的内容)。

对于这种效果,被定义为滚动链接(Scroll Chaining)。当滚动内容时是浏览器的一种默认行为。通常情况下,这种默认情况很好,但有时也不可取,甚至出乎意料。某些应用程序可能希望用户在遇到滚动边界时提供不同的用户体验。

当滚动元素到达底部时,可以通过(改变)页面的overflow属性或在滚动元素的滚动事件处理函数中取消默认行为来解决这问题。如果你选择使用JavaScript来处理,要记住,要处理的不是scroll事件,而是每当用户使用鼠标滚轮或触摸板时触发的wheel事件

function handleOverscroll(event) { 
    const delta = -event.deltaY; 
    if (delta < 0 && elem.offsetHeight - delta > elem.scrollHeight - elem.scrollTop) { 
        elem.scrollTop = elem.scrollHeight; 
        event.preventDefault(); 
        return false; 
    } 
    
    if (delta > elem.scrollTop) { 
        elem.scrollTop = 0; 
        event.preventDefault(); 
        return false; 
    } 
    
    return true; 
}

不幸的是,这个解决方案不太可靠。同时可能对页面性能产生负面影响。值得庆幸的是,CSS通过overscroll-behavior这个新属性可以帮助我们解决这个问题,达到优化滚动边界的现象,达到我们想要给用户的新体验。

overscroll-behavior属性可以控制一个容器或页面body容器滚动时发生的默认行为。可以使用这个属性取消滚动链接、禁用、自定义下拉刷新,禁用在iOS上的回弹效果等。而且使用overscroll-behavior不会像前面提到其他方案一样对页面有性能影响。

overscroll-behavior属性有三个值:

  • auto:其默认值。元素(容器)的滚动会传播给其祖先元素。有点类似JavaScript中的冒泡行为一样
  • contain:阻止滚动链接。滚动行为不会传播给其祖先元素,但会影响节点内的局部显示。例如,Android上的光辉效果或iOS上的回弹效果。当用户触摸滚动边界时会通知用户。注意,overscroll-behavior:containhtml元素上使用,可以阻止导航滚动操作
  • none:和contain一样,但它也可以防止节点本身的滚动效果

overscroll-behavior属性是overscroll-behavior-xoverscroll-behavior-y的简写,如果你只想控制其中一个方向的滚动行为,可以使用其中的某一个属性。

overscroll-behavior可以帮助我们:

overscroll-behavior也是一个新的CSS特性,到目前为止,支持的浏览器还有限:

同样的,@DylanPiercey也给overscroll-behavior提供了一个Polyfill

如果你想更深入的了解overscroll-behavior属性,建议你花一些时间阅读下面相关教程:

提供一个流式精确的滚动体验

幻灯片效果,比如Swiper的效果。比如:

对于这样的效果,我们都想为触控以及输入设备的用户提供一个流式、精确的滚动体验,比如像下面这样的效果,让Card精确定位在正中间:

对于这样的效果,被称为Scroll Snap效果,实现类似的效果,有许多JavaScript库可供你选择。但我们今天不是来介绍怎么通过JavaScript库实现精确的滚动体验,而是想告诉大家,我们可以使用CSS Scroll Snap Points来实现。

CSS Scroll Snap Points工作原理很简单:

通过在x以及y轴上定义“snap points”来控制滚动容器的滚动行为。当用户在水平或者垂直方向滚动时,利用捕捉点,滚动容器会捕捉到内容区域的某一点。

Scroll Snap Points主要提供了以下几个属性:

  • scroll-snap-type:定义在滚动容器中的一个snap点如何被严格的执行
  • scroll-snap-type-x:定义在滚动容器中水平轴上snap点如何被严格的执行
  • scroll-snap-type-y:定义在滚动容器中垂直轴上snap点如何被严格的执行
  • scroll-snap-coordinate:结合元素的最近的祖先元素滚动容器的scroll-snap-destination 定义的轴,定义了元素中xy坐标偏移的位置。如果元素已经变型,snap坐标也以相同的方式进行变型,为了使元素的snap点向元素一样被显示。
  • scroll-snap-destination:定义滚动容器的可视化viewport 中元素snap点的xy坐标位置
  • scroll-snap-points-x:定义滚动容器中内容的snap点的水平位置
  • scroll-snap-points-y:定义滚动容器中内容的snap点的垂直位置
  • scroll-snap-align:元素相对于其父元素滚动容器的对齐方式。它具有两个值,x 和 y。如果你只使用其中的一个,另外一个值默认相同
  • scroll-snap-padding:与视觉窗口的滚动容器有关。工作原理也相近与正常的内边距,值设置一致。此属性具有动画效果,所以如果你想要对齐snap点进行滚动,这将是一个很好的而选择。

有关于这方在更详细的介绍建议阅读下面这些文章:

其他特性

最后再简单说两个CSS特性,其中之一就是-webkit-overflow-scrolling: touch;,另一个即pointer-events:none。前者主要用来解决惯性滚动的问题,后者主要提高页面滚动时候的绘制性能。有关于这两个特性这里不做过多展开,感兴趣的同学可以自己花点时间深纠。

总结

非常感谢你花很长时间把这篇文章阅读完。整理完之后,感觉有很多内容和@evilmartians的《滚动的特性》一文有点类似。但这篇文章还是围绕CSS提供的一些新特性:scroll-behavioroverscroll-behaviorCSS Scroll Snap Points::-webkit-scrollbaroverflow-scrolling: touchpointer-events:none等,为用户提供一个更好的滚动体验。如果你感兴趣,欢迎花点时间深入了解这些内容。如果你有更好的建议或经验,欢迎在下面的评论中与我一起共享。jordans for sale paypal