关于响应式的另一种思考

1,151 阅读9分钟
原文链接: zhuanlan.zhihu.com
原文
原作者

一说到响应式设计,肯定离不开媒体查询 (Media Query) ,它可以改变页面的布局以精确适应不同的设备。媒体查询通常是我们在实现响应式设计时最后考虑的方案,在使用媒体查询之前,我们总是会多思考一下:还有没有其他的实现方法呢?下文就由此思考,总结了一些技巧来实现简单的响应式布局,或许展示的效果并不是很完美,但希望这些解决思路会为大家带来一些新的启发。

四大金刚

「四大金刚」是笔者从 'The Fab Four Technique ' 转译过来的,最初在 Rémi Parmentier 的博客中被提到, 初衷是帮助回复电子邮件,通过延伸发现它可以很容易地用于普通网页,为创建自适应网页带来了新的方法,如 Thierry 所示。 例如,利用「四大金刚」中的四个关键属性 width , min-width , max-width 和 calc 创建一个基于宽度的响应式效果:

{
  min-width: 50%;
  width: calc((25em - 100%) * 1000);
  max-width: 100%;
}

外部容器用百分比控制其宽度,然后 calc 函数将该值与所设定的断点(这里设置断点为25em)进行比较,然后得到一个正数(如果宽度小于断点),或者一个负数(如果宽度大于断点),或者正好是零。当为正数时,容器的宽度由 max-width 限制,当为负数或零宽度时容器宽度由 min-width 限制。

所以,在上面的例子中,我们设置一个断点为 25em 。如果 font-size 为 16px ,则容器的断点被解析为 400px 。如果容器是 400px 或更大(换句话说大于或等于断点),宽度将解析为零的或一个大的负数:

计算过程:(400-400 = 0)* 1000 = 0 (400-401 = -1)* 1000 = -1000

因此,当容器宽度大于 400px 时,min-width 就会发挥作用,所以上例中的元素的总共宽度将为50%。当容器是 399px 或更低(换句话说小于断点),宽度解析为一个大的正数:

计算过程:(400-399 = 1)* 1000 = 1000

在这种情况下,max-width 的最终宽度为 100% 。下图应该有助于可视化这个过程:

<img src="https://pic2.zhimg.com/v2-0bd368b02edfaf0fc65312fd09536ced_b.png" data-rawwidth="1672" data-rawheight="1430" class="origin_image zh-lightbox-thumb" width="1672" data-original="https://pic2.zhimg.com/v2-0bd368b02edfaf0fc65312fd09536ced_r.png">

接下来的四个演示都使用这种技术切换元素的宽度以实现响应式。

浮动图像的全宽和半宽

我们经常会需要实现图片在大屏幕上占比 50% ,小屏幕图片占比 100% 的效果,如下图所示,那么如果不用媒体查询需要怎么做呢?

  • 大屏幕情况


  • <img src="https://pic1.zhimg.com/v2-bb5ab32f812681795d05bb2c2b072bb4_b.png" data-rawwidth="2808" data-rawheight="1188" class="origin_image zh-lightbox-thumb" width="2808" data-original="https://pic1.zhimg.com/v2-bb5ab32f812681795d05bb2c2b072bb4_r.png">
  • 小屏幕情况
    <img src="https://pic2.zhimg.com/v2-e893d7d33e958dce23214137b2e0279d_b.png" data-rawwidth="698" data-rawheight="944" class="origin_image zh-lightbox-thumb" width="698" data-original="https://pic2.zhimg.com/v2-e893d7d33e958dce23214137b2e0279d_r.png">


关键代码:

<article class="float-module">
...
  <div class="float-module__image  float-module__image--left">
    <img src="..." />
  </div>
...
  <p>...</p>
</article>
.float-module__image {
  min-width: 50%;
  width: calc((25em - 100%) * 1000);
  max-width: 100%; 
  margin-bottom: 1rem;
}
.float-module__image img {
  width: 100%;
}
.float-module__image--left {
  float: left;
  margin-right: 1rem;
}
.float-module::after {
  content: "";
  display: table;
  clear: both;
}
这里是完整例子

浮动图像的显示和隐藏

以往我们在实现响应式时,为了在小屏幕突出视觉重点,会隐藏一些在大屏幕上才会显示的元素,这会让我们不得不多写一些CSS 样式去实现该效果。我们可以利用 calc 来实现在小屏幕时隐藏图片。显示效果如下图:

  • 大屏幕情况

  • &amp;amp;amp;amp;lt;img src="https://pic1.zhimg.com/v2-97d1f6ecf0199150ebeabdb21b8981d0_b.png" data-rawwidth="2808" data-rawheight="1132" class="origin_image zh-lightbox-thumb" width="2808" data-original="https://pic1.zhimg.com/v2-97d1f6ecf0199150ebeabdb21b8981d0_r.png"&amp;amp;amp;amp;gt;
  • 小屏幕情况
    &amp;amp;amp;amp;lt;img src="https://pic4.zhimg.com/v2-90247e540fe6821eee6913b1ade76a33_b.png" data-rawwidth="706" data-rawheight="454" class="origin_image zh-lightbox-thumb" width="706" data-original="https://pic4.zhimg.com/v2-90247e540fe6821eee6913b1ade76a33_r.png"&amp;amp;amp;amp;gt;


关键代码:

<article class="float-hidden-module">
  <div class="float-hidden-module__image  float-hidden-module__image--left">
    <img src="..." />
  </div>
  <p>...</p>
 ...
</article>
.float-hidden-module:after {
  content: "";
  display: table;
  clear: both;
}
.float-hidden-module__image {
  width: calc((25em - 100%) * -1000);
  max-width: 50%; 
  margin-bottom: 1rem;
  overflow: hidden;
}
.float-hidden-module__image img {

  /* 图片与文本之间创建空隙 */
  width: calc(100% - 1em);
}
.float-hidden-module__image--left {
  float: left;
}

当屏幕宽度大于 400px 时,.float-hidden-module__image 的 width > 0,因此 max-width 生效,该容器的宽度为 50%,当屏幕宽度小于或等于 400px 时,.float-hidden-module__image 的 width < = 0,该容器宽度为零,此时图片被隐藏。


为了清楚起见,更简短的展示:

{
  /* 移除 min-width ,因为我们希望宽度 < = 0 时都被视为 0 */
  width: calc((25em - 100%) * -1000);
  max-width: 100%;
}
这里是完整例子

文字和图片的覆盖和堆叠

很多时候,我们会需要在全屏显示的图片上添加文字描述,当屏幕宽度缩小,图片上不足以放下文字的时候,文字置于图片下方显示,显示效果如下图:

  • 大屏幕情况

  • &amp;amp;amp;amp;lt;img src="https://pic4.zhimg.com/v2-d29ee36137172307b90ed77348b40a97_b.png" data-rawwidth="2812" data-rawheight="1154" class="origin_image zh-lightbox-thumb" width="2812" data-original="https://pic4.zhimg.com/v2-d29ee36137172307b90ed77348b40a97_r.png"&amp;amp;amp;amp;gt;
  • 小屏幕情况
    &amp;amp;amp;amp;lt;img src="https://pic3.zhimg.com/v2-9db0d3080024011f28e7984ad0d2b80a_b.png" data-rawwidth="728" data-rawheight="880" class="origin_image zh-lightbox-thumb" width="728" data-original="https://pic3.zhimg.com/v2-9db0d3080024011f28e7984ad0d2b80a_r.png"&amp;amp;amp;amp;gt;


关键代码:

<div class="overlay-stacked-module">
  <div class="overlay-stacked-module__image">
    <img src="..." />
  </div>
  <div class="overlay-stacked-module__pull"></div>
  <div class="overlay-stacked-module__body">
  ...
    <p>
     ...
    </p>
  </div>
</div>
$module-breakpoint: 30em;
$primary-color: rgb(230,206,137);
$secondary-color: rgb(171, 101, 20);

.overlay-stacked-module {
  /* 如果 'pull' 容器的高度 > 'module_body' 容器内容的高度,那么它就会被挤到这个容器的下方,因此需要隐藏溢出*/
  overflow: hidden;
}

.overlay-stacked-module__image {
  position: relative;
}

.overlay-stacked-module__image::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  top: 0;

  /* 额外的 .5% 是为了弥补四舍五入带来的损失 */
  height: 100.5%;

  /* 设置渐变,使文字更加清晰 */
  background-image: linear-gradient(to bottom, rgba($primary-color,0) 0%,rgba($primary-color,0) 50%,rgba($primary-color,1) 100%);
  当屏幕宽度大于断点时,max-width 生效
  width: calc((#{$module-breakpoint} - 100%) * -1000);
  max-width: 100%;
}

.overlay-stacked-module__image img {
  width: 100%;
}

.overlay-stacked-module__pull {
  /* 文字浮于图片上方 */
  margin-bottom: -10em;

  max-height: 10em;

  overflow: hidden;
}
.overlay-stacked-module__pull::before {
  content: "";
  display: block;
  padding-bottom: calc((#{$module-breakpoint} - 100%) * 1000);
}
/* padding 是基于宽度的,所以我们可以有一个基于宽度的断点,将其与容器宽度(100%)进行比较,并将设置比较大的正负 padding 。当宽度为 0 时,负的 padding 无效,因此,实际上我们基于容器的 padding > = 0。*/
.overlay-stacked-module__body {
  padding: 1em;
  /* 保证 body 容器出现在图片之上 */
  position: relative;
}

和之前的做法有些相似,用另一个 div 将文本覆盖到图片上,如果图像太小,图片就会被文字遮挡,或者文字在图片下面。这部分技术实现有些复杂,慢慢分析一下:

.pull {
  /* 将文字浮于图片之上 */
  margin-bottom: -10em;
}

这里,负边距将下面的文字向上拉起,使得它覆盖图像。 但是,当容器宽度超过断点时,我们需要将文字恢复正常,避免文字将图像完全遮挡掉,但是由于没有 min / max-margin 属性,因此不能使用「四大金刚」来实现这一点。可以换另外一种想法,如果给 padding 一个百分比,在百分比的容器中, padding-top 和 padding-bottom 会影响元素的高度。 有了这个想法,就可以使用 calc 创建一个 padding-bottom 值,这样容器的宽度可以在 「零」 或 「非常大」 之间进行切换:

padding-bottom: calc((30em - 100%) * 1000);

我们不能直接应用 在 .pull 这个 div 上 ,因为没有 min / max-padding 属性来限制这些值,解决方案是将 padding 放在伪元素上以强制更改高度,并在 .pull 元素上使用 max-height 将高度限制为与负边距 (margin) 相同的值,以抵消该边距。

.pull {
  /* 文字浮于图片之上 */
  margin-bottom: -10em;
  max-height: 10em;

  /* 为了美观,隐藏溢出部分 */
  overflow: hidden;
}

.pull::before {
  content: "";
  display: block;
  padding-bottom: calc((30em - 100%) * 1000);
}

渐变叠加效果是通过如前面所述向已应用的背景渐变伪元素添加一个 on/off 开关来实现的:

.image::after {
  content: "";
  display: block;
  position: absolute;
  left: 0;
  top: 0;
  /* 额外的 .5% 是为了弥补四舍五入带来的损失 */
  height: 100.5%;
  max-width: 100%;
}
这里是完整例子

导航栏项目的显示和隐藏

导航栏的所有项目在大屏幕时依次排开显示,当屏幕宽度逐渐变小,最右侧的项目逐渐被隐藏,并在最右方出现一个 「更多」按钮,点击该按钮能够展开被折叠隐藏的项目。不用媒体查询,依然使用「四大金刚」,只不过这里是基于容器的高度,而不是宽度。显示效果:

  • 大屏幕下显示:

  • &amp;amp;amp;amp;lt;img src="https://pic1.zhimg.com/v2-c942392e62add929ee91e4b7adb46ca4_b.png" data-rawwidth="2754" data-rawheight="182" class="origin_image zh-lightbox-thumb" width="2754" data-original="https://pic1.zhimg.com/v2-c942392e62add929ee91e4b7adb46ca4_r.png"&amp;amp;amp;amp;gt;
  • 小屏幕下显示:
    &amp;amp;amp;amp;lt;img src="https://pic2.zhimg.com/v2-ba062e41e362bd510e8186917942c691_b.png" data-rawwidth="732" data-rawheight="178" class="origin_image zh-lightbox-thumb" width="732" data-original="https://pic2.zhimg.com/v2-ba062e41e362bd510e8186917942c691_r.png"&amp;amp;amp;amp;gt;



  • 小屏幕下点击「more」:
    &amp;amp;amp;amp;lt;img src="https://pic1.zhimg.com/v2-a39a676bdd32c75ebf3ea0bfe81b17f8_b.png" data-rawwidth="734" data-rawheight="256" class="origin_image zh-lightbox-thumb" width="734" data-original="https://pic1.zhimg.com/v2-a39a676bdd32c75ebf3ea0bfe81b17f8_r.png"&amp;amp;amp;amp;gt;

关键代码:

<div class="outer">
  <div class="inner">
    <div class="item">...</div>
    ...
    <div class="control">...</div>
  </div>
</div>
...
.outer {
  height: 2.25em;
  overflow: hidden;
}
/* 点击外部容器时,高度自动,以展示被隐藏的项目*/
.outer:target {
  height: auto;
}

外部容器具有固定的高度,并且隐藏任何溢出,除非元素是 :target 状态。

.inner {
  display: flex;
  flex-wrap: wrap;
}

内部容器是一个弹性容器,用 flex-wrap 实现换行,所以容器的高度随着元素内部的高度增加而增加,但第一行下面的元素将被 .outer 容器的 overflow:hidden 隐藏,从而出现截断效果。

.control {
  height: calc((2.25em - 100%) * -1000);
  max-height: 2.25em;
}

.outer:target .control--open {
  display: none;
}

.outer:target .control--close {
  display: block;
}

more / less 控件只有在容器的高度超过断点时可见,并且 :target 状态可以控制哪个控件是可见的,这样子我们就简单实现了一个随着屏幕宽度切换而展示不同效果的导航栏。


这里是完整例子

小结

上面几种方法实现了简单的响应式部分需求,但是很多情况下,如果没有媒体查询就不能实现。 例如颜色值,字体大小和线高度,边框和框阴影等等。但是如果不用媒体查询就能实现自己需要的响应式,岂不是更加美好(⁎⁍̴̛ᴗ⁍̴̛⁎)。