移动端适配:为什么SVG方案可以修正retina屏下1px问题?

1,069 阅读6分钟

为什么高清屏下1px更宽

高清屏(retina屏)是指高dpr的设备,其物理像素的密度更大。又分为有两倍屏,三倍屏。

dpr:物理像素/css像素

普通屏1个css像素对应一个物理像素2倍屏中一个css像素对应4个物理像素;三倍屏中则是9个。

按照这样的置换规则后一张相同的图片在不同的设备上才会显示相同的大小。

1px的线在高清屏下本应不需要做特殊处理。两倍屏下会自动用2个物理像素去展示‘1px’的细线,普通屏用一个物理像素去展示‘1px’的细线,他们应该看起来是相同的。

但是,就像数学中的概念:线是没有宽度的,点是没有大小的。像素同样是没有大小的。

两倍屏的物理像素密度是普通屏的两倍,并不是每一个物理像素是普通屏的1/4大小,而是物理像素的间距是普通屏间距的1/2。

用两倍屏下用两排像素去展示,自然会比普通屏中用一排像素去展示看起来更粗

如何修正高清屏下的1px问题

要解决1px问题,本质就是让高清屏用一个物理像素去展示一个css像素

可以按照不同屏幕的dpr作出转换,比如在2倍屏下将1px的细线写成border:0.5px。但这种方法只在ios上支持,安卓上会显示成被当成0px处理。

更通用的方案中,有svg伪类元素两种。

SVG方案

这种方案本质上border并没有变细,但是boder被一分为二,靠内侧的是透明的

关键的样式代码是css中的svg生成函数。

SVG即矢量图,用xml标签写在html文件中。

通过postcss-write-svg这个postcss插件将css中svg函数生成的图像处理成base64。这样就可以在css文件直接调用svg函数。

/* src/index.css */
@svg custom-name {
  width4px;
  height4px;
  @rect {
    fill: transparent;
    width100%;
    height100%;
    stroke-width1;
    stroke: var(--color, black);
  }
}
.svg-retina-border {
  border1px solid;
  border-imagesvg(custom-name param(--color green)) 1 repeat;
}
.normal-border {
  border: px solid green;
}
// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-write-svg')({
      utf8: false // base64格式
    })
  ]
}

处理过后的样子

// scr/index.js
import './index.css'
const root = document.getElementById('root')

const div2 = document.createElement('div')
div2.innerHTML = 'SVG-retina-border'
div2.className = 'svg-retina-border'
root.append(div2)

root.append(document.createElement('br'))

const div3 = document.createElement('div')
div3.innerHTML = 'normal-border'
div3.className = 'normal-border'
root.append(div3)

<!DOCTYPE html>
<!-- src/index.html -->
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
// webpack.config.js
const path = require('path')
const HtmlPlugin = require('html-webpack-plugin')
module.exports = {
  mode: 'development',
  entry: {
    entry1: './src/index.js'
  },
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].js'
  },
  module: {
    rules: [
      {
        test: /\.css$/,
        use: ['style-loader''css-loader''postcss-loader']
      }
    ]
  },
  plugins: [
    new HtmlPlugin({
      template: './src/index.html'
    })
  ],
  devServer: {
    contentBase: path.resolve(__dirname, 'dist'),
    host: '0.0.0.0',
    port: 3005,
    compress: true,
    disableHostCheck: true
  }
}

SVG

分别直接用xml的svg标签和css实现了两个100px,边框宽为1的矩形。

高清屏下效果如下。

  <-- 视口大小-->
  <svg xmlns="custom-namespace" width="100" height="100">
      <rect
        <--矩形大小-->
        width="100"
        height="100"
        fill="transparent"
        <--svg中所有的单位都是-->
        stroke-width="1"
        stroke="black"
      />
    </svg>
    <div
      style="
        width: 100px;
        height: 100px;
        border: 1px solid black;
        box-sizing: border-box;
      "
    ></div>

stroke-widthborder一样,都将矩形的边设为了1px,但是用svg实现的矩形边框看起来却更细。

关键的地方是使用svg标记的视口大小和使用rect标记的矩形大小相同的

svg中没有盒模型的概念,它的stroke画线并不是对应css中的border更像是不占空间的outline。因为不占空间,它会以rect(矩形)的边界为中心画线,一条线一半宽度在矩形内,一半在矩形外

而因为视口宽度正好等于矩形的大小,看到的线宽就只有一半了

(用svg画一个100px大小+1px边宽的方形)

(用css画一个100px大小+1px边宽的方形)(border-box)

如果把矩形缩小一点,不占满视口,这时候看到的border是完整的,所以和没处理过的1px一样粗。

border-image

border-image是三个属性的缩写

border-image-source: url('https://misc.aotu.io/leeenx/border-image/box.png');
border-image-slice: 33% 20% 3 fill;
border-image-repeat: stretch;

border-image-source:图片链接或base64;
border-image-slice:图片切割的四个位置。把图片切成9块,除中间一块,其他八块分别被当成边框使用。接受1-4个参数(使用类似于padding/margin的尺寸设置)。可以是百分比(相对于图片自身),也可以是数字(单位是px)。最后的fill决定中间那块图片会不会被当成background使用。
border-image-repeat:stretch/round(平铺)/repeat(重复)上下左右四个正位的图片怎样被当成border使用。

round(平铺)会压缩,repeat(重复)会剪裁。

border-image必须配合border使用。最终border宽度是border-widthborder-style也必须指定,border-color可以不用。

网上border-image的介绍很详细,所以只重点写了svg部分。
关于border-image推荐:border-image