前文《移动端常见适配方案》中,我介绍了移动端的一些常见适配方案
移动端适配就是在进行屏幕宽度的等比例缩放
文中我强调了移动端适配是对屏幕宽度进行缩放:对于普通的流式布局(长屏幕页面),页面内容是可以上下滚动的。屏幕小,一屏幕看到的东西虽然变少,但是用户可以通过手势滚动页面,继续浏览下一屏的内容。因此在常规情况下,对于屏幕宽度进行等比例缩放已经能解决大部分应用场景了
但是对于一种特殊的场景:单屏页面(又称翻屏页面),由于需要把一整屏的内容完整展示给用户,同时又要求页面不能出现滚动条,那么,仅仅只是针对屏幕宽度进行等比例缩放的适配,其实效果并不理想
设备独立像素 (CSS像素)
设备独立像素是一种可以被程序所控制的虚拟像素,在Web开发中对应CSS像素
以iphone7
举例:
iphone7
的设备独立像素
为 375 * 667
也就是手机全屏下的大小,同时也是chrome
模拟器展示的尺寸
可以通过js的screen.width
和screen.height
获取
设备像素 (物理像素)
设备像素也可以叫物理像素,由设备的屏幕决定,其实就是屏幕中控制显示的最小单位
以iphone7
举例:
iphone7
的设备像素
为 750 * 1334
750 * 1334
这个尺寸也可以称为设计像素
,我们设计和开发页面时,就是以这个设计像素为准
设备像素比(DPR)
设备像素比(dpr) = 设备像素 / 设备独立像素
以iphone7
举例:
iphone7
的dpr = 2 = 750 / 375
也就是说,在iphone7
下,1 css像素 = 2 物理像素
在css
中一个1x1
大小的正方形里面,其实有4个物理像素
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) |
---|---|---|---|---|
苹果 | iOS | iPhone 7 | 375 * 667 | 375 * 548 |
iPhone 12 | 390 * 844 | 390 * 664 | ||
Ipnone 11/XR | 414 * 896 | 414 * 715 | ||
iPhone X | 375 * 812 | 375 * 635 | ||
华为 | 安卓 | P40 | 360 * 780 | 360 * 625 |
nova 8 SE | 360 * 800 | 360 * 659 | ||
Mate 30 | 424 * 918 | 424 * 774 | ||
荣耀8 | 360 * 640 | 360 * 501 | ||
P10 | 360 * 640 | 360 * 526 | ||
畅玩7x | 360 * 720 | 360 * 584 | ||
Oppo | 安卓 | R15x | 360 * 780 | 360 * 650 |
R17 | 360 * 780 | 360 * 628 | ||
K1 | 360 * 780 | 360 * 622 | ||
Xiaomi | 安卓 | MIX 2 | 393 * 786 | 393 * 666 |
小米10 | 393 * 851 | 393 * 720 | ||
小米6 | 360 * 640 | 360 * 521 | ||
k40 | 393 * 873 | 393 * 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
动态缩放整体页面 - 使用
vw
和vh
进行布局
aspect-ratio
注意和device-aspect-ratio
进行区分,device-aspect-ratio
是和设备尺寸进行绑定的,但是我们之前介绍过:网页的可视区域会随着访问方式(浏览器,APP客户端)有所改变,因此aspect-ratio
才是我们真正需要的属性。
aspect-ratio 定义输出设备中的页面可见区域宽度与高度的比率
同时它有两个max-aspect-ratio
和min-aspect-ratio
兄弟属性,可以和max-width
和min-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-content
的witdh
和height
写死,就能保证宽高比永远保持为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-content
的witdh
和height
写死,就能保证宽高比永远保持为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种适配方案,可以总结如下:
- 使用
vw
和vh
这两个原生的css3
单位,天然支持宽度和高度的适配:对于需要高度适配的元素,使用vh
,对于需要宽度适配的元素,使用vw
rem
相对宽度计算:划分几个高度区域,对于特定的宽高比,单独进行适配。整体页面还是相对宽度进行缩放,只针对部分宽高比,对页面进行特定的样式改动rem
动态改变:即可相对宽度,也可相对高度进行计算,此种方案,可以做到保持设计稿的宽高比例,并尽量占满窗口的极致效果- 不使用
rem
,写死px
,直接js
整体缩放页面:此种方案,也可以做到保持设计稿的宽高比例,并尽量占满窗口的极致效果
对于保持设计稿的宽高比例,并尽量占满窗口的效果,可以点击下面的demo进行预览理解:
调整屏幕大小,可以看见页面会上下居中或者左右居中,并且保持宽高比
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。