创建水平滚动的正确方式【CSS 网格布局】

2022-11-22 16:35:40 浏览数 (1)


theme: fancy

原文链接 Creating horizontal scrolling containers the right way [CSS Grid] -- 作者 Dannie Vinther

自从奈飞 Netflix 成为了家喻户晓的名字以来,在移动端中我们一直使用横向布局。水平滚动容器(列表)已经成为了一种常见的布局做法,而不是将东西都堆叠在页面上,这将减少占用小屏幕设备垂直的空间。

本文,我们探讨 CSS 网格的弹性布局,它是如何帮助我们实现水平滚动的,同时处理它带来的缺陷。

UX(用户体验)的考虑

UX/UE -> User Experience 译者加

本文不会深入讨论水平滚动的用户体验方面。但是,当采用水平滚动布局时,至少需要满足两点 UX 原则:

  • 你的设计必须在视觉上提醒他人,这是一组可以水平滚动的内容。最好的方法,就是让可滚动的内容露出一部分。
  • 用户知道什么时候滚到末尾,这很重要。我们注意到用户重复进行滚动操作,是因为他们认为自己并未充分滚动。一种方法指明列表已经滚到最后:在列表末尾使用额外的空间

布局大纲

开始前,我们概览下需要实现的布局特性:

  • 滚动的容器必须准守页面的整体布局。比如,外边距和内边距整体要一致。
  • 滚动的部分内容,必须在容器边缘露出来。
  • 滚动时,容器的内容必须从屏幕的边缘滑出来。
  • 容器内两个内容之间的距离要小于边缘的距离,这样容器两端都会有更大的空间(这提示用户他们已经滑到最后)。

如下:

需要注意的是,容器两端的距离和周围内容的距离是匹配的(也就是整体布局要和谐)。

整体布局

现在,我们已经基本明白水平滚动容器的特性了。接下来,我们考虑使用 CSS Grid 网格布局来编码。使用 CSS Grid 网格布局方便我们控制元素之间的距离,无需进一步计算。

对于整体布局,我们将使用简单但强大的 CSS Grid 技术:

代码语言:javascript复制
.app {
  display: grid;
  grid-template-columns: 20px 1fr 20px;
}
.app > * {
  grid-column: 2 / -2;
}
.app > .full {
  grid-column: 1 / -1;
}

.app 类元素下的子元素都会被“容器化”,它们都有 20px 的边距,使得内容远离边缘。带 .full 类名的子元素,将会占据全部视窗的宽度且没有内边距。

滚动容器

我们使用六个卡片来创建水平滚动容器,一次显示两张。因为我们考虑整体布局,水平滚动的两边填充内边距,我们删除了 .full 类,然后添加如下:

代码语言:javascript复制
.hs {  
  display: grid;  
  grid-gap: 10px;  
  grid-template-columns: repeat(6, calc(50% - 40px));  
  grid-template-rows: minmax(150px, 1fr);  
}

使用 grid-template-columns,我们可以设置每个卡片需要的空间。在这个例子中,卡片占有视图空间的 50% 减去间隔 40px。这时候,我们会看到第三张卡片露出来。

然而,需要注意的是,卡片两端被砍断部分。还记得不,当水平滚动的时候,我们希望可滚动的内容是从屏幕的边缘滑出。

所以,我们在容器中添加 .full 类,并填补缺失的内边距。

代码语言:javascript复制
.hs {  
  display: grid;  
  grid-gap: 10px;  
  grid-template-columns: repeat(6, calc(50% - 40px));  
  grid-template-rows: minmax(150px, 1fr);  
  padding: 0 20px; // 添加
}

乍一看,我们好像实现了需求,但是当你滚动到尾部的时候,你会注意到并没有其他空间了 -- 所以这并不符合整体布局。

你可能想在最后一个元素添加 margin-right 的属性值以处理这个问题:

代码语言:javascript复制
.hs > li:last-child {
  margin-right: 20px;
}

很不幸,这并不起作用。那么,我们要怎么处理呢?

建议的解决方案

考虑我们目前都有了些什么内容,我们删除容器中的内边距:

代码语言:javascript复制
.hs {  
  display: grid;  
  grid-gap: 10px;  
  grid-template-columns: repeat(6, calc(50% - 40px));  
  grid-template-rows: minmax(150px, 1fr);
}

如果我们在 grid-template-columns 两边添加内边距,会实现我们要的布局。

我们在网格列两端添加了 2 x 10px 的空间。结合 10px 的网格距离,我们总共有 20px,所以满足我们整体布局的内边距要求。

代码语言:javascript复制
.hs {  
  display: grid;  
  grid-gap: 10px;  
  grid-template-columns: // 更改
    10px  
    repeat(6, calc(50% - 40px))  
    10px;  
    grid-template-rows: minmax(150px, 1fr);
}

为了不让第一张卡片占用第一列的 10px 的空间,我们在每一端引入空的伪元素:

代码语言:javascript复制
.hs::before,  
.hs::after {  
  content: ‘’;  
}

伪元素 ::before::after 非常适合 grid-columns 布局,因为会自动添加到水平滚动容器的开头和结尾。伪元素能够参与网格化布局让人心存感激。

现在,我们实现了一开始在大纲中提到的特性。

注意事项

这项技术的一个注意事项是在 grid-template-columns 中对既定卡片数量的计算。

代码语言:javascript复制
grid-template-columns:  
  10px
  repeat(6, calc(50% - 40px))  
  10px;

如果容器中只是包含 4 个卡片,你需要为该特定容器设定新的网格规则。这不是很灵活。

一种使其更灵活的处理方式是,你可以使用 Javascript 来计算卡片的数量,然后将其分配给 CSS 变量。

代码语言:javascript复制
var root = document.documentElement;
const lists = document.querySelectorAll('.hs');lists.forEach(el => {
  const listItems = el.querySelectorAll('li');
  const n = el.children.length;
  el.style.setProperty('--total', n); 
});

然后,你就可以在 grid-template-columns 中使用变量:

代码语言:javascript复制
grid-template-columns:  
  10px  
  repeat(var(--total) , calc(50% - 40px)) // 重点 
  10px;

更新: 如 Alex Baciu 提及,我们可以通过使用隐式网格完全省略 Javascript(或者 CSS 变量解决方案)。这样,我们不需要计算超出列的数量,因为这是浏览器为我们计算的。

为此,我们调整下代码:

代码语言:javascript复制
.hs {
  ...
  grid-template-columns: 10px;
  grid-auto-flow: column; 
  grid-auto-columns: calc(50% - var(--gutter) * 2);
  ...
....hs:before,
.hs:after {
  content: '';
  width: 10px; 
}

我们仍然需要最初的 10px 内边距来弥补不足,然而,剩下的卡片通过自动放置算法布局。为此,我们需要设置 grid-auto-flowcolumn(默认值是 row)。

最后,我们需要确保的是 .hs:after ,它继承了其他卡片的大小,其占用的空间不超过 10px。所以我们需要通过固定的宽度来限定。

代码片段

你可能会争辩,代码变得不那么清晰了,因为赋值更加分散,使得正在发生的东西变得混乱。但是,我觉得还行 :)

译者加:本文滚动的技术交流为主,熟悉其原理。真正业务上操作,建议使用成熟的 Swiper 操作。

本文正在参加「金石计划 . 瓜分6万现金大奖」

0 人点赞