原文链接 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 技术:
.app {
  display: grid;
  grid-template-columns: 20px 1fr 20px;
}
.app > * {
  grid-column: 2 / -2;
}
.app > .full {
  grid-column: 1 / -1;
}.app 类元素下的子元素都会被“容器化”,它们都有 20px 的边距,使得内容远离边缘。带 .full 类名的子元素,将会占据全部视窗的宽度且没有内边距。
滚动容器
我们使用六个卡片来创建水平滚动容器,一次显示两张。因为我们考虑整体布局,水平滚动的两边填充内边距,我们删除了 .full 类,然后添加如下:
.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 类,并填补缺失的内边距。
.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 的属性值以处理这个问题:
.hs > li:last-child {
  margin-right: 20px;
}很不幸,这并不起作用。那么,我们要怎么处理呢?
建议的解决方案
考虑我们目前都有了些什么内容,我们删除容器中的内边距:
.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,所以满足我们整体布局的内边距要求。
.hs {
  display: grid;
  grid-gap: 10px;
  grid-template-columns: // 更改
    10px
    repeat(6, calc(50% - 40px))
    10px;
    grid-template-rows: minmax(150px, 1fr);
}为了不让第一张卡片占用第一列的 10px 的空间,我们在每一端引入空的伪元素:
.hs::before,
.hs::after {
content: ‘’;
}
伪元素 ::before 和 ::after 非常适合 grid-columns 布局,因为会自动添加到水平滚动容器的开头和结尾。伪元素能够参与网格化布局让人心存感激。
现在,我们实现了一开始在大纲中提到的特性。
注意事项
这项技术的一个注意事项是在 grid-template-columns 中对既定卡片数量的计算。
grid-template-columns:
10px
repeat(6, calc(50% - 40px))
10px;
如果容器中只是包含 4 个卡片,你需要为该特定容器设定新的网格规则。这不是很灵活。
一种使其更灵活的处理方式是,你可以使用 Javascript 来计算卡片的数量,然后将其分配给 CSS 变量。
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 中使用变量:
grid-template-columns:
10px
repeat(var(--total) , calc(50% - 40px)) // 重点
10px;
更新: 如 Alex Baciu 提及,我们可以通过使用隐式网格完全省略 Javascript(或者 CSS 变量解决方案)。这样,我们不需要计算超出列的数量,因为这是浏览器为我们计算的。
为此,我们调整下代码:
.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-flow 为 column(默认值是 row)。
最后,我们需要确保的是 .hs:after ,它继承了其他卡片的大小,其占用的空间不超过 10px。所以我们需要通过固定的宽度来限定。

你可能会争辩,代码变得不那么清晰了,因为赋值更加分散,使得正在发生的东西变得混乱。但是,我觉得还行 :)
译者加:本文滚动的技术交流为主,熟悉其原理。真正业务上操作,建议使用成熟的 Swiper 操作。










