浅谈移动端单屏解决方案

1,471 阅读12分钟

前文《移动端常见适配方案》中,我介绍了移动端的一些常见适配方案

移动端适配就是在进行屏幕宽度的等比例缩放

文中我强调了移动端适配是对屏幕宽度进行缩放:对于普通的流式布局(长屏幕页面),页面内容是可以上下滚动的。屏幕小,一屏幕看到的东西虽然变少,但是用户可以通过手势滚动页面,继续浏览下一屏的内容。因此在常规情况下,对于屏幕宽度进行等比例缩放已经能解决大部分应用场景了

但是对于一种特殊的场景:单屏页面(又称翻屏页面),由于需要把一整屏的内容完整展示给用户,同时又要求页面不能出现滚动条,那么,仅仅只是针对屏幕宽度进行等比例缩放的适配,其实效果并不理想

设备独立像素 (CSS像素)

设备独立像素是一种可以被程序所控制的虚拟像素,在Web开发中对应CSS像素

iphone7举例:

iphone7设备独立像素375 * 667

也就是手机全屏下的大小,同时也是chrome模拟器展示的尺寸

可以通过js的screen.widthscreen.height获取

设备像素 (物理像素)

设备像素也可以叫物理像素,由设备的屏幕决定,其实就是屏幕中控制显示的最小单位

iphone7举例:

iphone7设备像素750 * 1334

750 * 1334这个尺寸也可以称为设计像素,我们设计和开发页面时,就是以这个设计像素为准

设备像素比(DPR)

设备像素比(dpr) = 设备像素 / 设备独立像素

iphone7举例:

iphone7dpr = 2 = 750 / 375

也就是说,在iphone7下,1 css像素 = 2 物理像素

css中一个1x1大小的正方形里面,其实有4个物理像素

pic

dpr大于2的屏幕也称为视网膜屏幕(Retina)

实际物理像素

iphone7的实际物理像素是750 * 1334,刚好等于设备像素。但不是所有的设备都是实际物理像素等于设备像素

iphone7 plus的实际物理像素是1080 * 1920。它的dpr为3,设备独立像素414 * 736,根据公式可以得出,它的设备像素等于1242 x 2208,远大于实际物理像素。手机会自动把1242 * 2208个像素点塞进1080 * 1920个物理像素点来渲染,我们不用关心这个过程

单屏幕

前面介绍了这么多概念,其实在真正开发中,我们主要关心的是设备独立像素设备像素

设备像素 决定了设计稿的尺寸。移动端设计稿一般是750 * 1334 尺寸大小( iPhone6 的设备像素为标准的设计图),因此相对比较固定

设备独立像素 决定了设备的屏幕大小。iOS平台下,屏幕尺寸还算相对固定,但是到了Android平台下,屏幕尺寸那就千奇百怪,百花争鸣了。

特别需要注意的一点:即使设备独立像素确定了大小,我们的网页被用户看到的时候,实际高度还是比设备独立像素的高度小很多

主要原因是:我们的网页往往是在手机的浏览器上访问的,而这些浏览器自带了顶部地址栏和底部工具栏,这两部分的高度又进一步压缩了我们网页展示的高度(如果我们的网页是在第三方客户端内打开的,比如微博,微信,Twitter, Facebook,那么一般只有顶部地址栏)

举个例子,iphone11的设备独立像素是414 * 896

左图是在safari浏览器下:可以看到上下红框部分是浏览器自带的区域,只有蓝框是实际网页展示的高度,这个蓝框的大小是 414 * 715(documentElement.clientWidth/documentElement.clientHeight),已经比设备独立像素的高度少了181像素(896 - 715)

右图是在微信自带浏览器下:可以看到顶部红框部分是浏览器自带的区域,只有蓝框是实际网页展示的高度,这个蓝框的大小是 414 * 804(documentElement.clientWidth/documentElement.clientHeight),也比设备独立像素的高度少了92像素(896 - 804)

收集到的一些常见设备尺寸大小:

品牌操作系统设备设备独立像素 (screen.width/screen.height)自带浏览器下(clientWidth/clientHeight)
苹果iOSiPhone 7375 * 667375 * 548
iPhone 12390 * 844390 * 664
Ipnone 11/XR414 * 896414 * 715
iPhone X375 * 812375 * 635
华为安卓P40360 * 780360 * 625
nova 8 SE360 * 800360 * 659
Mate 30424 * 918424 * 774
荣耀8360 * 640360 * 501
P10360 * 640360 * 526
畅玩7x360 * 720360 * 584
Oppo安卓R15x360 * 780360 * 650
R17360 * 780360 * 628
K1360 * 780360 * 622
Xiaomi安卓MIX 2393 * 786393 * 666
小米10393 * 851393 * 720
小米6360 * 640360 * 521
k40393 * 873393 * 713

单屏难点

设计稿的的宽高比是固定的,但是真实设备的宽高比永远不是统一的,并且网页的可视区域还会随着访问方式(浏览器,APP客户端)有所改变。

同一份设计稿,却要在不同尺寸的设备上,都展示出良好的布局:页面的内容要尽可能完整展示在一屏之中(甚至不能有滚动条)

安全区 + 长背景图

750 * 1334 的设计稿,需要考虑到长屏幕时,页面的展示情况

比如默认750 * 1334大小的内容需要完整展示出来(安全区),长屏幕750 * 1750时,把安全区的内容垂直居中展示即可

此时,我们就需要使用一张长背景图(750 * 1750)上下居中作为整个网页的背景

<style>
  * {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
  }

  html {
    /* 750px 的设计图,1rem = 100px */
    font-size: calc(100 * 100vw / 750);
  }

  html,
  body {
    width: 100%;
    height: 100%;
    position: relative;
  }

  #app {
    width: 100%;
    height: 100%;
    position: relative;
    /* 长背景图上下居中 */
    background: url('./img/bg.jpg') no-repeat center / cover #fff;
    overflow: hidden;
  }

  .safe-content {
    width: 100%;
    height: 100%;
    position: absolute;
    left: 0;
     /* 限定安全区的高度 */
    max-height: 13.34rem;
    top: 50%;
    /* 安全区上下居中 */
    transform: translateY(-50%);
  }
</style>
<body>
  <div id='app'>
    <div class='safe-content'>
      <div class='block1'></div>
      <div class='block2'></div>
    </div>
  </div>
</body>

之后,我们把页面所有的内容,相对safe-content进行布局

完整页面,点击此处预览 (手机模式查看)

完整代码,直接右键源代码或者点击下方按钮展开

点击展开源代码
<!DOCTYPE html>
<html lang='en'>

<head>
  <meta charset='UTF-8'>
  <meta http-equiv='X-UA-Compatible' content='IE=edge'>
  <meta name='viewport' content='width=device-width, initial-scale=1.0'>
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    html {
      /* 750px 的设计图,1rem = 100px */
      font-size: calc(100 * 100vw / 750);
    }

    html,
    body {
      width: 100%;
      height: 100%;
      position: relative;
    }

    #app {
      width: 100%;
      height: 100%;
      position: relative;
      background: url('./img/bg.jpg') no-repeat center / cover #fff;
      overflow: hidden;
    }

    .safe-content {
      width: 100%;
      height: 100%;
      position: absolute;
      left: 0;
      max-height: 13.34rem;
      top: 50%;
      transform: translateY(-50%);
      /* border: 1px solid red; */
    }

    .go-back {
      width: 0.91rem;
      height: 0.92rem;
      background: url('./img/back.png') no-repeat center / contain;
      position: absolute;
      left: 0.14rem;
      top: 0.15rem;
    }

    .rule-btn {
      width: 0.83rem;
      height: 0.83rem;
      background: url('./img/rule-btn.png') no-repeat center / contain;
      position: absolute;
      left: 1.27rem;
      top: 0.20rem;
    }

    .username {
      min-width: 1.67rem;
      height: 0.41rem;
      line-height: 0.41rem;
      background: url('./img/user-bg.png') no-repeat center / cover;
      position: absolute;
      right: 0;
      top: 0.25rem;
      color: #fcfcfc;
      font-size: 0.22rem;
      text-align: right;
      padding-right: 0.13rem;
      padding-left: 0.48rem;
    }

    .gift-container {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .gift-logo {
      width: 6.79rem;
      height: 7.03rem;
      background: url('./img/game-title.png') no-repeat center / contain;
    }

    .gift-title {
      width: 3.45rem;
      height: 0.63rem;
      background: url('./img/congratulate.png') no-repeat center / contain;
    }

    .gift-content {
      margin-top: -1.8rem;
      display: flex;
      flex-direction: column;
      align-items: center;
    }

    .gift-name {
      font-size: 0.3216rem;
      line-height: 0.24rem;
      letter-spacing: 0.032rem;
      color: #bd5874;
      text-align: center;
      margin-top: 0.2rem;
      text-shadow: 0 0 5px #fff, 0 0 5px #fff;
    }

    .gift-icon {
      margin: 0.4rem 0 0.5rem;
      width: 1.94rem;
      height: 1.65rem;
      background: url('./img/gift-1.png') no-repeat center / contain;
    }

    .gift-desc {
      font-size: 0.2rem;
      line-height: 0.2426rem;
      letter-spacing: 0.019rem;
      color: #90949e;
      text-align: center;
      margin-bottom: 0.2rem;
      text-shadow: 0 0 5px #fff, 0 0 5px #fff;
    }

    .gift-get-info {
      font-size: 0.23rem;
      line-height: 0.30rem;
      color: #d16b88;
      text-align: center;
      text-shadow: 0 0 5px #fff, 0 0 5px #fff;
    }
  </style>
</head>

<body>
  <div id='app'>
    <div class="safe-content">
      <div class="go-back"></div>
      <div class="rule-btn"></div>
      <div class="username">深红</div>
      <div class="gift-container">
        <div class="gift-logo"></div>
        <div class="gift-content">
          <div class="gift-title"></div>
          <p class="gift-name">水漾烛光礼盒</p>
          <div class="gift-icon"></div>
          <div class="gift-desc">
            <p>蒂普提克香氛蜡烛(70g)*1</p>
            <p>krramel沐浴套装*1</p>
          </div>
          <div class="gift-get-info">
            <p>请到游戏内【精彩活动-实物周边奖励兑换】</p>
            <p>填写领取信息</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</body>

</html>

宽高比

上面的方案,对于移动端(屏幕高度大于屏幕宽度)的大部分场景,的确够用了。

但是在折叠屏手机(屏幕宽度和高度差别不大),ipad,pc端(屏幕高度小于屏幕宽度)的设备下,我们的页面就很有可能超出了完整的一屏。

如果此时,父级元素还设置了overflow: hidden;,那么用户甚至不能滑动查看超出屏幕的内容,如果底部是一个可交互的按钮,那么用户就永远不能触发之后的流程了!

问题原因

我们的rem适配方案,是相对于屏幕宽度进行缩放的,但是不同机型的手机,可视区域的宽高比并不固定,因此对于部分手机,页面内容就很有可能出现超出屏幕底部或者底部留有空白

对于底部留有空白,一般发生在可视高度比可视宽度大很多的情况,前面介绍的安全区 + 长背景图方案,就是针对此种情况的解决方案。

而对于超出屏幕底部,一般发生在可视高度和可视宽度相差不大(折叠屏手机),甚至可视高度比可视宽度小(横屏或者pc端)的情况,解决方案一般如下:

  • 使用css进行宽高比判断
  • 使用js进行宽高比判断
  • 使用js动态修改rem大小
  • 使用js动态缩放整体页面
  • 使用vwvh进行布局

aspect-ratio

注意和device-aspect-ratio进行区分,device-aspect-ratio是和设备尺寸进行绑定的,但是我们之前介绍过:网页的可视区域会随着访问方式(浏览器,APP客户端)有所改变,因此aspect-ratio才是我们真正需要的属性。

aspect-ratio 定义输出设备中的页面可见区域宽度与高度的比率

同时它有两个max-aspect-ratiomin-aspect-ratio兄弟属性,可以和max-widthmin-width进行类比:

@media screen and (min-aspect-ratio: 9/16) {
    // 只要宽高比大于等于9/16,就会执行
}

@media screen and (min-aspect-ratio: 3/4) {
    // 只要宽高比大于等于3/4,就会执行
}

@media screen and (min-aspect-ratio: 1/1) {
    // 只要宽高比大于等于1/1,就会执行
}

对于上面的页面,我们正常的设备独立像素是375 * 667,我们可以这样进行高度划分:

  • 高度大于667:无需调整,我们只怕高度小,不怕高度大,高度大时已经有前面的方案: 安全区 + 长背景图
  • 530-667:还是正常,我们也不需要调整
  • 490-530
  • 375-490
  • 小于375:pc端或者横屏,高度已经比宽度小了
@media screen and (min-aspect-ratio: 375 / 530) {
  .safe-content {
    transform: translateY(-50%) scale(0.8);
  }
}

@media screen and (min-aspect-ratio: 375 / 490) {
  .gift-logo {
    transform: scale(0.6);
  }
}

@media screen and (min-aspect-ratio: 375 / 375) {
  .safe-content {
    transform: translateY(-50%) scale(0.32);
  }
  .gift-logo {
    transform: scale(0.4);
  }
}

比较简单暴力的,就是直接对主要页面区域施加 transform: scale(0.8) 这类样式,直接缩小。

至于我刚刚划分的高度区间,是通过chrome模拟器自己一次次实验调整出来的,这个要具体页面具体分析,并没有一个统一的宽高比规定。

完整页面,点击此处aspect-ratio-预览 (手机模式查看)

完整代码,直接右键源代码

JS添加全局类

function detectAspectRatio(aspectRatio) {
  document.documentElement.classList.remove('pc', 'mobile1', 'mobile2');
  if (aspectRatio >= 375 / 375) {
    return document.documentElement.classList.add('pc');
  }

  if (aspectRatio >= 375 / 490) {
    return document.documentElement.classList.add('mobile1');
  }
  if (aspectRatio >= 375 / 530) {
    return document.documentElement.classList.add('mobile2');
  }
}
function init() {
  const clientWidth = document.documentElement.clientWidth;
  const clientHeight = document.documentElement.clientHeight;
  const aspectRatio = clientWidth / clientHeight;
  detectAspectRatio(aspectRatio);
}
init();
window.addEventListener('resize', init);

本质上就是把css的媒体查询aspect-ratio用js实现了一遍,所以这种方案区别不大。

动态修改rem

默认情况下,我们的rem是根据可视区域宽度进行计算的,但是在高度较小的情况下,我们可以动态的修改rem的参考对象,让它根据可视高度进行计算

我们甚至可以实现:无论窗口怎么变,我们的内容都保持原来的比例,并尽量占满窗口

封装成一个通用flexible.js方法

const defaultConfig = {
  pageWidth: 750,
  pageHeight: 1334,
  pageFontSize: 32,
};

const flexible = (config = defaultConfig) => {
  const {
    pageWidth = defaultConfig.pageWidth,
    pageHeight = defaultConfig.pageHeight,
    pageFontSize = defaultConfig.pageFontSize,
  } = config;
  const pageAspectRatio = defaultConfig.pageAspectRatio || (pageWidth / pageHeight);
  // 根据屏幕大小及dpi调整缩放和大小
  function onResize() {
    let clientWidth = document.documentElement.clientWidth;
    let clientHeight = document.documentElement.clientHeight;
    let aspectRatio = clientWidth / clientHeight;

    // 根元素字体
    let e = 16;
    if (clientWidth > pageWidth) {
      // 认为是ipad/pc
      console.log('认为是ipad/pc');
      e = pageFontSize * (clientHeight / pageHeight);
    } else if (aspectRatio > pageAspectRatio) {
      // 宽屏移动端
      console.log('宽屏移动端');
      e = pageFontSize * (clientHeight / pageHeight);
    } else {
      // 正常移动端
      console.log('正常移动端');
      e = pageFontSize * (clientWidth / pageWidth);
    }

    document.documentElement.style.fontSize = `${e}px`;
    let realitySize = parseFloat(window.getComputedStyle(document.documentElement).fontSize);
    if (e !== realitySize) {
      e = e * e / realitySize;
      document.documentElement.style.fontSize = `${e}px`;
    }
  }

  const handleResize = () => {
    onResize();
  };

  window.addEventListener('resize', handleResize);
  onResize();
  return (defaultSize) => {
    window.removeEventListener('resize', handleResize);
    if (defaultSize) {
      if (typeof defaultSize === 'string') {
        document.documentElement.style.fontSize = defaultSize;
      } else if (typeof defaultSize === 'number') {
        document.documentElement.style.fontSize = `${defaultSize}px`;
      }
    }
  };
};

使用时:

flexible({ pageFontSize: 100 });
.safe-content {
  width: 7.5rem;
  height: 13.34rem;
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  margin: auto;
}

这时,只需要把safe-contentwitdhheight写死,就能保证宽高比永远保持为750 * 1334

完整页面,点击此处动态修改rem-预览 (手机模式查看)

改变屏幕大小,可以看到safe-content一直都保持正常的宽高比,并且总是宽度占满(宽度比高度小)或者高度占满(高度比宽度小)

完整代码,直接右键源代码

另外一个简化版本

(function init(screenRatioByDesign = 750 / 1334) {
  let docEle = document.documentElement
  function setHtmlFontSize() {
    var screenRatio = docEle.clientWidth / docEle.clientHeight;
    // 7.5 = 750 / 100  (100是设计稿上的1rem大小)
    var fontSize = (
      screenRatio > screenRatioByDesign
        ? (screenRatioByDesign / screenRatio)
        : 1
    ) * docEle.clientWidth / 7.5;
    docEle.style.fontSize = fontSize.toFixed(3) + "px";
  }

  setHtmlFontSize()

  window.addEventListener('resize', setHtmlFontSize)
})()

完整页面,点击此处动态修改rem2-预览 (手机模式查看)

动态缩放整体页面

前面的适配方案,我们都借助了rem进行页面的大小缩放。其实我们也可以直接使用px进行页面的布局,最后统一使用js进行整体缩放

 function init() {
    const winWidth = document.documentElement.clientWidth;
    const winHeight = document.documentElement.clientHeight;
    const winScale = winWidth / winHeight;

    const page = document.querySelector('.safe-content');
    const pageWidth = 750;
    const pageHeight = 1334;
    const pageScale = pageWidth / pageHeight;

    const origin = '50% 50% 0';

    let initialScale = 1;

    if (winScale > pageScale) {
      // 宽度长了,但是高度不够
      // 高度占满,宽度水平居中
      console.log('高度占满,宽度水平居中');
      initialScale = winHeight / pageHeight;
    } else {
      console.log('宽度占满,高度垂直居中');
      // 高度长了,但是宽度不够
      // 宽度占满,高度垂直居中
      initialScale = winWidth / pageWidth;
    }

    page.style.width = pageWidth + 'px';
    page.style.height = pageHeight + 'px';
    page.style.transform = 'scale(' + initialScale + ')';
    page.style.transformOrigin = origin;
    page.style.left = (pageWidth - winWidth) / -2 + 'px';
    page.style.top = (pageHeight - winHeight) / -2 + 'px';
  }
  init();
  window.addEventListener('resize', init);
.safe-content {
  /* 布局直接写死成设计稿上的大小 */
  width: 750px;
  height: 1334px;
  position: absolute;
  left: 0;
  top: 0;
  border: 1px solid red;
}

.gift-logo {
  /* 布局直接写死成设计稿上的大小 */
  width: 679px;
  height: 703px;
  background: url('./img/game-title.png') no-repeat center / contain;
}

这时,只需要把safe-contentwitdhheight写死,就能保证宽高比永远保持为750 * 1334

完整页面,点击此处动态缩放整体页面-预览 (手机模式查看)

改变屏幕大小,可以看到safe-content一直都保持正常的宽高比,并且总是宽度占满(宽度比高度小)或者高度占满(高度比宽度小)

完整代码,直接右键源代码

vw和vh

单屏页面布局时,垂直定位尽可能相对高度进行定位,这时可以选择使用百分比或者vh

@use 'sass:math';

@function px2vh($px, $height: 1334) {
  @return math.div($px, $height) * 100 * 1vh;
}

可以封装一个scss方法,将测量得到的px转换成vh

.demo {
  position: absolute;
  left: 0;
  top: 25%; // 垂直定位单位为%
  top: px2vh(100); // 垂直定位单位为vh
  width: 100%;
  height: 1rem;
}

如果希望页面的元素在不同的高度下,均能完整展示,可以全部使用vh进行布局

.gift-title {
  width: 25.86vh;
  height: 4.72vh;
  background: url('./img/congratulate.png') no-repeat center / contain;
}

.gift-name {
  font-size: 2.39vh;
}

完整页面,点击此处vh-预览 (手机模式查看)

完整代码,直接右键源代码

总结

前面介绍的5种适配方案,可以总结如下:

  1. 使用vwvh这两个原生的css3单位,天然支持宽度和高度的适配:对于需要高度适配的元素,使用vh,对于需要宽度适配的元素,使用vw
  2. rem相对宽度计算:划分几个高度区域,对于特定的宽高比,单独进行适配。整体页面还是相对宽度进行缩放,只针对部分宽高比,对页面进行特定的样式改动
  3. rem动态改变:即可相对宽度,也可相对高度进行计算,此种方案,可以做到保持设计稿的宽高比例,并尽量占满窗口的极致效果
  4. 不使用rem,写死px,直接js整体缩放页面:此种方案,也可以做到保持设计稿的宽高比例,并尽量占满窗口的极致效果

对于保持设计稿的宽高比例,并尽量占满窗口的效果,可以点击下面的demo进行预览理解:

调整屏幕大小,可以看见页面会上下居中或者左右居中,并且保持宽高比

保持设计稿的宽高比例,并尽量占满窗口

源码直接右键查看即可

参考