如何获得一个像素完美、线性缩放的用户界面

355 阅读7分钟

根据视口宽度动态调整CSS值并不是一个新话题。你可以在CSS-Tricks的文章中找到大量深入的介绍,比如篇文章或这篇文章

不过,这些例子大多使用相对的CSS单位无单位值来实现动态缩放。这就失去了像素的完美性,而且一旦屏幕低于或高于某个阈值,通常就会出现文字包装和布局移动。

但是,如果我们真的想要像素的完美性呢?比方说,如果我们正在开发一个复杂的实时分析仪表盘,在会议室的大电视上观看,或者作为一些PWA,专门在手机和平板设备上打开,而不是在文字密集的博客和新闻网站上打开,那该怎么办?这些都是我们需要更精确的情况。

换句话说,如果我们想统一缩放设计,怎么办?当然,我们可以像本文中所涉及的那样,根据可用的宽度,用CSS变换来实现内容的scale ,这样就可以保留正确的比例了。

然而,我们也可以使用CSS中的像素值来实现流体比例缩放的UI。它们根据设备的屏幕空间进行适当的缩放,同时保留其像素级的完美比例。此外,如果用像素值工作更舒服或更熟悉的话,我们仍然可以使用像素值并自动将其转换为相对的CSS单位。

缩放我们的用户界面

让我们试着实现这个由Craftwork提供的很棒的仪表板。我们需要以这样一种方式使它完美地缩放,并保留所有的文本行数、边距、图像尺寸等。

让我们在CSS像素值中工作,并使用SCSS以获得速度和便利。因此,如果我们要针对这些卡片小部件中的一个标题,我们的SCSS可能看起来像这样。

.cardWidget {
  .cardHeading {
    font-size: 16px;
  }
}

没有什么花哨的东西。没有什么是我们以前没有见过的。作为一个像素值,这将不会有任何规模。

这个设计是用一个宽度为1600px 的容器创建的。让我们假设在1600px ,卡片的标题的理想字体大小应该是16px ,因为它就是这样设计的。

现在我们有了这个宽度的 "理想 "容器宽度的字体大小,让我们用当前*视口宽度来相应地调整我们的CSS像素值。

/*
  1600px is the ideal viewport width that the UI designers who
  created the dashboard used when designing their Figma artboards

  Please not we are not using pixel units here, treating it purely
  as a numeric value.
*/
--ideal-viewport-width: 1600;
/*
  The actual width of the user device
*/
--current-viewport-width: 100vw;

.cardWidget {
  .cardHeading {
    /*
      16px is the ideal font size that the UI designers want for
      1600px viewport width.

      Please note that we are not using pixel units here,
      treating it purely as a numeric value.
    */
    --ideal-font-size: 16;
    /*
      Calculate the actual font size:

      We take our idealFontSize and multiply it by the difference
      between the current viewport width and the ideal viewport width.
    */
    font-size: calc(
      var(--ideal-font-size) * (var(--current-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

正如你所看到的,我们把从设计中获得的理想字体大小作为一个基数,然后乘以当前视口宽度和理想视口宽度之间的差值。这在数学上看起来如何呢?假设我们在一个与模拟图完全相同宽度的屏幕上观看这个网络应用。

--current-device-width: 100vw; // represents 1600px or full width of the screen
--ideal-viewport-width: 1600; // notice that the ideal and current width match
--ideal-font-size: 16;
// this evaluates to:
font-size: calc(16 * 1600px / 1600);
// same as:
font-size: calc(16 * 1px);
// final result:
font-size: 16px;

所以,由于我们的视口宽度完全吻合,我们的font-size ,最后正好是16px ,理想的视口宽度是1600px

再举个例子,假设我们在一个较小的笔记本屏幕上观看这个网络应用,其宽度为1366px 。这里是最新的数学计算。

font-size: calc(16 * 1366px / 1600);
// same as:
font-size: calc(16 * 0.85375px);
// final result:
font-size: 13.66px;

或者让我们说,我们是在一个完整的高清晰度显示器上观看,宽度为1920px

font-size: calc(16 * 1920px / 1600);
// same as:
font-size: calc(16 * 1.2px);
// final result:
font-size: 19.2px;

你可以看到,尽管我们使用像素值作为参考,但实际上我们能够根据理想和当前视口尺寸之间的宽度差异来按比例调整我们的CSS值。

下面是我制作的一个小演示,来说明这个技术。

CodePen嵌入回退

为了方便起见,这里有一个视频。

夹住最小和最大视口宽度

使用目前这种方法,无论视口有多大或多小,设计都会根据视口的大小进行调整。我们可以用CSSclamp() 来防止这种情况,它允许我们设置最小宽度为350px ,最大宽度为3840px 。这意味着,如果我们在一个宽度为5000px 的设备上打开网页应用程序,我们的布局将保持在3840px

--ideal-viewport-width: 1600;
--current-viewport-width: 100vw;
/*
  Set our minimum and maximum allowed layout widths:
*/
--min-viewport-width: 350px;
--max-viewport-width: 3840px;

.cardWidget {
  .cardHeading {
    --ideal-font-size: 16;
    font-size: calc(
      /*
        The clamp() function takes three comma separated expressions
        as its parameter, in the order of minimum value, preferred value
        and maximum value:
      */
      --clamped-viewport-width: clamp(var(--min-viewport-width), var(--current-viewport-width), var(--max-viewport-width);
      /*
        Use the clamped viewport width in our calculation
      */
      var(--ideal-font-size) * var(--clamped-viewport-width) / var(--ideal-viewport-width)
    );
  }
}

让我们做一个单位转换的助手

我们的代码是相当冗长的。让我们写一个简单的SCSS函数,将我们的值从像素转换为相对单位。这样,我们就可以在任何地方导入和重用这个东西,而不需要那么多的重复。

/*
  Declare a SCSS function that takes a value to be scaled and
  ideal viewport width:
*/
@function scaleValue(
  $value,
  $idealViewportWidth: 1600px,
  $min: 350px,
  $max: 3840px
) {
  @return calc(
    #{$value} * (clamp(#{$min}, 100vw, #{$max}) / #{$idealViewportWidth})
  );
}

/*
  We can then apply it on any numeric CSS value.

  Please note we are passing not pixel based, but numeric values:
*/
.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
}

将其移植到Javascript中

有时候,CSS并不能解决这个问题,我们必须使用JavaScript来确定一个组件的大小。比方说,我们正在动态地构建一个SVG,我们需要根据理想的设计宽度来确定其widthheight 属性的大小。下面是实现这一目的的JavaScript。

/*
  Our helper method to scale a value based on the device width
*/
const scaleValue = (value, idealViewportWidth = 1600) => {
  return value * (window.innerWidth / idealViewportWidth)
}

/*
  Create a SVG element and set its width, height and viewbox properties
*/
const IDEAL_SVG_WIDTH = 512
const IDEAL_SVG_HEIGHT = 512

const svgEl = document.createElement('svg')
/* Scale the width and height */
svgEl.setAttribute('width', scaleValue(IDEAL_SVG_WIDTH))
svgEl.setAttribute('height', scaleValue(IDEAL_SVG_WIDTH))

/*
  We don't really need to scale the viewBox property because it will
  perfectly match the ratio of the scaled width and height
*/
svg.setAttribute('viewBox', `0 0 ${IDEAL_SVG_WIDTH} ${IDEAL_SVG_HEIGHT}`)

这种技术的缺点

这个解决方案并不完美。例如,一个主要的缺点是,用户界面不再是可缩放的。无论用户如何缩放,设计都将保持锁定,就像在100%的缩放下观看一样。

也就是说,我们可以轻松地使用传统的媒体查询,在不同的视口宽度上设置不同的理想数值。

.myElement {
  width: #{scaleValue(500)};
  height: #{scaleValue(500)};
  box-shadow: #{scaleValue(2)} #{scaleValue(2)} rgba(black, 0.5);
  font-size: #{scaleValue(24)};
  @media (min-width: 64em) {
    width: #{scaleValue(800)};
    font-size: #{scaleValue(42)};
  }
}

现在我们可以同时从媒体查询和我们的像素完美的线性缩放中获益。

总结

所有这些都是实现流体UI的另一种方法。我们将像素完美值视为纯数字值,并将其乘以当前视口宽度与设计中的 "理想 "视口宽度之间的差值。

我在自己的工作中广泛使用这种技术,希望你也能找到一些用处。