移动端自适应和发丝线的实现

本文介绍移动端的自适应、flexible.js的原理和发丝线的实现
首先我们需要了解相关概念

基本概念

像素

不同场景下像素的含义不同。

设备像素和图像像素

显示设备(显示器)是通过排列的显示器件来显示图像的,一个这样的显示器件称为一个“设备像素”。例如iPhone6横向375个像素,纵向667个像素。每个像素可以独立设置颜色。

图像像素指一个图像(位图)的最小展示单元,每个像素只能有一种颜色 。

物理像素和逻辑像素

物理像素指的就是设备像素,一个设备的物理像素是它的固有属性,显示设备还会提供逻辑像素给应用程序使用,例如我们平时设置显示器的分辨率为1280 × 800或者1440 × 900,就是设置显示器横向和竖向展示逻的辑像素的个数。

我们在CSS中使用的px单位就是逻辑像素(页面缩放的话,CSS的px尺寸和逻辑像素会成一定比例)。

为什么需要逻辑像素呢?因为物理像素因设备而异,不同像素排列方式和密度不同,如果程序设置一个展示元素的尺寸使用物理像素,那么它在不同的设备的尺寸是有很大区别的。因此显示器提供逻辑像素,让程序可以定义“希望实际看到的元素尺寸”。例如,设备的分辨率是375 × 667,逻辑像素1个像素对应物理1个像素;如果设备分辨率是750 × 1334,那么这个屏设置1个逻辑像素对应4个物理像素,这样显示的元素在两个屏上看起来基本一样大了。

一个逻辑像素里可能包含1个或者多个物理像素点,包含的越多则图像看起来越清晰。

设备像素比

设备独立像素指独立于设备的像素,即设备无关的像素,即逻辑像素,设备像素比指

物理像素 / 设备独立像素

设备像素比越大,说明一个逻辑像素对应的物理像素越多,图像就会更清晰。

例如,有些移动设备的设备像素比是2,即2个物理像素的宽度等于一个逻辑像素宽度,这种设备叫“2倍屏”,也有“3倍屏”。

设备的设备像素比可以通过window.devicePixelRatio来获取。

视网膜屏(retina屏)

所谓“Retina”是一种显示技术,可以将更多的像素点压缩至一块屏幕里,从而达到更高的分辨率并提高屏幕显示的细腻程度。这种分辨率在正常观看距离下足以使人肉眼无法分辨其中的单独像素,也被称为视网膜显示屏。

Retina 既不是指分辨率,也不是单独指PPI,而是指视觉效果。其计算公式为(可以不用了解):

a = acttan( h / 2d )

a 代表人眼视角,h 代表像素间距,d 代表肉眼与屏幕的距离。符合以上条件的屏幕可以使肉眼看不见单个物理像素点。这样的显示屏就可被苹果称作“Retina显示屏”。

简单地说,视网膜屏就是设备的分辨率很高的屏。

meta viewport

name为"viewport"的meta标签,可以设置viewport相关的属性。下面说明一下viewport的概念。

关于viewport,可以参考viewport深入理解,在这只简单介绍一下。

viewport(视口),指移动端设备的可视区域,这包含了两个方面,一个是我们可以在手机上看到的网页的大小(layout viewport),这个可以通过document.documentElement.clientWidth获取到。另一个是可视区的大小(visual viewport),可以通过window.outerWidth获取到。

手机visual viewport的大小各不相同(注意,这里的大小指的都是逻辑像素),通常手机默认的layout viewport的大小是980px或者1024px(也可能是其它值,这个是由设备自己决定的)。

手机默认的layout一般都和PC近似,而大于手机的可视区。这是为了让移动端显示PC的网页,PC的网页通常较大,超出移动端可视的范围就会产生滚动条。

如果我们设计专门的移动端的网页,不希望很大的layout viewport的尺寸,而是和移动端适配,不产生滚动条。那么可以设置name为"viewport"的meta标签。

width

name为"viewport"的meta标签支持layout viewport的尺寸设置。

<meta name="viewport" content="width=device-width">

这样设置的结果是,layout viewport的宽度等于设备的视口宽度。通常移动端的网页都要这样设置。

scale

viewport meta标签也支持设置缩放比例,效果和用户手动放缩一样,例如缩放比例是2,那么CSS的1px对应逻辑像素就变为2px。

<meta name="viewport" content="width=device-width, initial-scale=2.0">

viewport meta标签的scale控制页面的缩放,缩放之后,CSS的1px代表的逻辑像素有所变化,例如scale设置为0.5的话,CSS的1px会对应0.5px的逻辑像素。

viewport的scale相关有几个关键属性

  • initial-scale 设置页面的初始缩放值,为一个数字,可以带小数
  • minimum-scale 允许用户的最小缩放值,为一个数字,可以带小数
  • maximum-scale 允许用户的最大缩放值,为一个数字,可以带小数
  • height 设置layout viewport 的高度,这个属性对我们并不重要,很少使用
  • user-scalable 是否允许用户进行缩放,值为"no"或"yes", no 代表不允许,yes代表允许

注意,如果设置scale小于1,那么屏幕会缩小,所以视口能够展示的逻辑像素增多,因此视口的尺寸会变大。例如对于一个视口宽度为327的设备,设置scale为0.5后

console.log(document.documentElement.clientWidth); // 654

移动端自适应

前端面试刷题网站:灵题库,收集大厂面试真题,相关知识点详细解析。

简述

移动端自适应的基本思路是,让我们对元素的尺寸定义是和设备尺寸成一定比例的,比如一个盒子在10px的屏幕上展示2px宽度,在20px的屏幕上展示4px,这样在不同的设备上就可以自适应地展示了。

如何实现上述效果呢?首先会想到使用百分比,但是百分比是相对于父元素的比例,如果使用百分比会让元素的展示不符合预期,例如父元素增大时候,子元素也一定跟随增大。因此百分比最好是相对于屏幕的宽度(通常不适用高度作为基准)。

如果有CSS的尺寸单位能够表达相对于屏幕的宽度的百分比,就可以很方便地实现自适应了。

总而言之,移动端自适应的实现原理是,使用一些CSS的长度单位,让我们写的CSS尺寸能够和屏幕宽度保持固定比例。 这个比例是多少?很明显,就是UI图上的元素尺寸 / UI图上的屏幕宽度。

自适应开发流程

通常我们开发页面时候,UI图会提供每个元素的尺寸。如何根据UI图进行自适应开发呢?

我们现在已知的信息包括:UI图的屏幕宽度(UI_screen_width),UI图的元素的尺寸(UI_element_width)。那么我们就可以计算出UI图的元素相对于屏幕宽度的百分比,有了这个百分比,再结合CSS的相对于屏幕宽度的比例的长度单位,就可以计算出这个元素的尺寸值了。对于不同的单位,计算方法也是不同的。

所以自适应开发流程就是先根据UI图计算元素的百分比,再根据百分比计算出元素在某个自适应单位下的实际值。

一般在实际项目中,会有一个算法计算从UI_element_width到使用自适应单位的尺寸值,开发时候就按照UI图的数值来写,在打包构建时候转成自适应单位。

因此在开发自适应页面时候,我们需要:

  • 计算方法
  • 转换工具

下面介绍两个主流的自适应方案,和对应的计算方法&转换工具。

vm自适应

CSS中有提供viewport单位,vm和vh,100vm = 屏幕宽度,100vh = 屏幕高度。使用vm来实现自适应是很方便的。

下面我们看如何根据元素相对于屏幕尺寸的比例,计算元素尺寸在vm单位下的取值。

比如UI图使用750px的屏幕宽度,一个元素的宽度是75px,那么元素宽度就是10%的屏幕宽度,使用vm单位就是10vm。

计算公式是什么呢?因为不管是UI图,还是实际的开发,元素宽度和屏幕宽度比例一样,设元素的实际值是x(vm)则可以得到:

x(vm) / 100vm = UI_element_width / UI_screen_width

所以x = UI_element_width / UI_screen_width × 100

这就是使用vm自适应的计算方法。

通常使用CSS预处理器的话,就可以实现在打包时候对长度进行转换了。例如使用Sass:

//iPhone 6尺寸作为设计稿基准
$vm_base: 750; 
@function vw($px) {
    @return ($px / $vm_base) * 100vw;
}

.demo-dev {
        width: vm(100);
}

rem自适应

**

在一段时间内,由于vm的兼容性不能满足项目需求,因此有一些替代方案,其中最流行的就是rem自适应方案。

rem也是CSS中的一个长度单位,1rem = 16px。html元素的style.fontSize默认是16px,如果html元素的style.fontSize改为其他的值,则1rem也相应变化,例如修改document.documentElement.style.fontSize=75px,则1rem = 75px

根据rem的这个特性,我们看下如何使用rem来实现自适应。

我们先设定,document.documentElement.style.fontSize简写为html.fontSize;屏幕宽度document.documentElement.getBoundingClientRect().width简写为visual_layout_width

我们上面已经提到,自适应方案的本质就是让我们写的CSS的尺寸和屏幕宽度保持固定比例,根据rem的特性(1rem = html.fontSize),只需要让html.fontSize和屏幕宽度保持固定比例,我们使用rem来写CSS单位,就可以达到CSS尺寸和屏幕宽度保持固定比例的目的了。例如UI图屏幕宽度是750px,UI图元素尺寸是75px。我们只要设置html.fontSize = 1 / 10 × visual_layout_width,然后元素的CSS尺寸是1rem,这样元素就会根据屏幕宽度自适应了。

其实html.fontSize设置的和屏幕宽度成一定比例即可,具体比例是多少都无所谓,只要计算元素的rem尺寸时候使用相同比例计算就可以。

在flexible.js里面,这个比例是1 / 10,即

html.fontSize = 1 / 10 × visual_layout_width。

下面我们看使用rem实现自适应的计算方法,设html的font size和屏幕宽度比例为r,即:

r = html.fontSize / visual_layout_width

所以屏幕宽度的值是

visual_layout_width = html.fontSize / r

设元素实际值是x(rem),那么因为x和visual_layout_width的比值,等于UI_element_width / UI_screen_width。所以x(rem) = html.fontSize(px) / r × UI_element_width / UI_screen_width = 1(rem) / r × UI_element_width / UI_screen_width,所以x = UI_element_width / UI_screen_width / r

综上,rem自适应方案实现流程为:

  1. 首先选定一个比例r
  2. 然后确定UI图的屏幕宽度UI_screen_width
  3. 然后js动态修改document.documentElement.style.fontSize = document.documentElement.getBoundingClientRect().width × r
  4. 通过公式x = UI_element_width / UI_screen_width / r根据UI图元素的尺寸计算实际CSS代码的rem值。

类似vm,rem方案也可以通过预编译器来实现转换工作

//iPhone 6尺寸作为设计稿基准
$rem_rate: 0.1;
$ui_screen_width: 750;
@function rem($px) {
    @return ($px / $ui_screen_width / $rem_rate);
}

.demo-dev {
        width: rem(75);
}

也可以使用px2rem-loader工具进行转换。

0.5px实现

参考文章

简介

在上面“物理像素和逻辑像素”部分介绍了,通常我们使用CSS设置的是逻辑像素,不能直接设置物理像素。有时候前端需要实现比较细的线,例如0.5px的逻辑像素(或者说1px的物理像素),这种效果成为“发丝线”。按照正常的思路,我们直接设置0.5px就可以,但是不同浏览器的处理不同,因此0.5px并不是在每个浏览器都能达到预期效果。

有几个方案都能实现0.5px的效果,下面简单介绍一下这些方案。

更详细的说明,请阅读 怎么画一条0.5px的边

**

方案

**

直接设置0.5px

**

.half-px {
    height: 0.5px;
}

**

transform

**

.half-px {
    height: 1px;
    transform: scaleY(0.5);
    transform-origin: 50% 100%;
}

**

线性渐变

**

.half-px {
    height: 1px;
    background: linear-gradient(0deg, #fff, #000);
}

**

box-shadow

.half-px {
    height: 1px;
    background: none;
    box-shadow: 0 0.5px 0 #000;
}

**

svg

.half-px {
    background: none;
    height: 1px;
    background: url("data:image/svg+xml;utf-8,<svg xmlns='http://www.w3.org/2000/svg' width='100%' height='1px'><line x1='0' y1='0' x2='100%' y2='0' stroke='#000'></line></svg>");
}

**

viewport scale缩放

先设置viewport的缩放比例

<meta name="viewport" content="width=device-width,initial-scale=0.5">

设置完viewport的scale之后,通过上面“meta viewport”部分的说明,CSS的1px会对应0.5px的逻辑像素。因此直接设置元素的尺寸为1px即可实现0.5px的效果。

.half-px {
        height: 1px;
}

为了实现一个物理像素的细线,通常scale设置成设备像素比的倒数:1 / window.devicePixelRatio

flexible.js

**

简述

flexible.js主要实现了两个功能:

  1. 基于rem的自适应
  2. 基于viewport scale的发丝线

我们在项目中引入flexible.js(通过script标签引入或者内联到html中都可以)之后,就可以按照上面介绍的rem自适应方案实现自适应效果了。

我们上面已经提到了基于rem的自适应方案,flexible.js固定document.documentElement.style.fontSizedocument.documentElement.getBoundingClientRect().width的比例为1 / 10。flexible.js实现了根据屏幕宽度计算html的fontSize并动态设置。

我们使用flexible.js时候,只需要实现px到rem的转换即可:

$rem_rate: 0.1; // flexible.js固定的比例,这样设定是为了方便计算
$ui_screen_width: 750; // UI图的屏幕宽度尺寸
@function rem($px) {
    @return ($px / $ui_screen_width / $rem_rate);
}

.demo-dev {
        width: rem(75); // 1rem
}

另外,引入flexible.js之后,由于它动态改了viewport的scale,所以可以直接写1px来实现发丝线的效果。

对于rem自适应的功能,需要注意的是,在flexible.js仓库中作者有说明:

由于viewport单位得到众多浏览器的兼容,lib-flexible这个过渡方案已经可以放弃使用,不管是现在的版本还是以前的版本,都存有一定的问题。建议大家开始使用viewport来替代此方案。

viewport方案对代码侵入性小,不需要引入额外脚本,而且在主流浏览器中的兼容性越来越好,所以是更优的方案。

原理

**

源码

;(function(win, lib) {
    var doc = win.document;
    var docEl = doc.documentElement;
    var metaEl = doc.querySelector('meta[name="viewport"]');
    var flexibleEl = doc.querySelector('meta[name="flexible"]');
    var dpr = 0;
    var scale = 0;
    var tid;
    var flexible = lib.flexible || (lib.flexible = {});
    
    if (metaEl) {
        console.warn('将根据已有的meta标签来设置缩放比例');
        var match = metaEl.getAttribute('content').match(/initial-scale=([\d.]+)/);
        if (match) {
            scale = parseFloat(match[1]);
            dpr = parseInt(1 / scale);
        }
    } else if (flexibleEl) {
        var content = flexibleEl.getAttribute('content');
        if (content) {
            var initialDpr = content.match(/initial-dpr=([\d.]+)/);
            var maximumDpr = content.match(/maximum-dpr=([\d.]+)/);
            if (initialDpr) {
                dpr = parseFloat(initialDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));    
            }
            if (maximumDpr) {
                dpr = parseFloat(maximumDpr[1]);
                scale = parseFloat((1 / dpr).toFixed(2));    
            }
        }
    }

    if (!dpr && !scale) {
        var isAndroid = win.navigator.appVersion.match(/android/gi);
        var isIPhone = win.navigator.appVersion.match(/iphone/gi);
        var devicePixelRatio = win.devicePixelRatio;
        if (isIPhone) {
            // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案
            if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) {                
                dpr = 3;
            } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){
                dpr = 2;
            } else {
                dpr = 1;
            }
        } else {
            // 其他设备下,仍旧使用1倍的方案
            dpr = 1;
        }
        scale = 1 / dpr;
    }

    docEl.setAttribute('data-dpr', dpr);
    if (!metaEl) {
        metaEl = doc.createElement('meta');
        metaEl.setAttribute('name', 'viewport');
        metaEl.setAttribute('content', 'initial-scale=' + scale + ', maximum-scale=' + scale + ', minimum-scale=' + scale + ', user-scalable=no');
        if (docEl.firstElementChild) {
            docEl.firstElementChild.appendChild(metaEl);
        } else {
            var wrap = doc.createElement('div');
            wrap.appendChild(metaEl);
            doc.write(wrap.innerHTML);
        }
    }

    function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

    win.addEventListener('resize', function() {
        clearTimeout(tid);
        tid = setTimeout(refreshRem, 300);
    }, false);
    win.addEventListener('pageshow', function(e) {
        if (e.persisted) {
            clearTimeout(tid);
            tid = setTimeout(refreshRem, 300);
        }
    }, false);

    if (doc.readyState === 'complete') {
        doc.body.style.fontSize = 12 * dpr + 'px';
    } else {
        doc.addEventListener('DOMContentLoaded', function(e) {
            doc.body.style.fontSize = 12 * dpr + 'px';
        }, false);
    }
    

    refreshRem();

    flexible.dpr = win.dpr = dpr;
    flexible.refreshRem = refreshRem;
    flexible.rem2px = function(d) {
        var val = parseFloat(d) * this.rem;
        if (typeof d === 'string' && d.match(/rem$/)) {
            val += 'px';
        }
        return val;
    }
    flexible.px2rem = function(d) {
        var val = parseFloat(d) / this.rem;
        if (typeof d === 'string' && d.match(/px$/)) {
            val += 'rem';
        }
        return val;
    }

})(window, window['lib'] || (window['lib'] = {}));

**

自适应

**

flexible.js实现rem自适应方案的一个主要操作是根据设备宽度设置了html的fontSize,比例固定为 1/ 10。

function refreshRem(){
        var width = docEl.getBoundingClientRect().width;
        if (width / dpr > 540) {
            width = 540 * dpr;
        }
        var rem = width / 10;
        docEl.style.fontSize = rem + 'px';
        flexible.rem = win.rem = rem;
    }

设置好html的fontSize后,我们就可以根据UI图计算元素尺寸了。

发丝线

**

flexible.js设置了viewport的scale为设备像素比的倒数

 scale = 1 / dpr;

这样加载了flexible.js之后,CSS的1px对应的是1个物理像素,因此就可以实现发丝线了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,482评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,377评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,762评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,273评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,289评论 5 373
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,046评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,351评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,988评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,476评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,948评论 2 324
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,064评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,712评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,261评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,264评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,486评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,511评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,802评论 2 345

推荐阅读更多精彩内容