随着大数据产业的发展,越来越多的公司开始实现数据资源的管理和应用,尤其是一些在日常生活中经常使用大屏幕的大中型企业。此时,用户界面设计者需要呈现相应的视觉效果。
接下来为大家介绍大屏可视化的UI设计。
--大屏UI设计--
--大数据可视化ui设计赏析--
(图片均来源于网络)
点击查看更多UI/UE设计案例⬇️⬇️⬇️
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
更多精彩文章:
包括单行注释和块级注释。
// alert(“HelloWorld!”)
/*
这是一个
多行的
块级注释
*/
“use strict”;。
ECMA-262描述了一组具有特定用途的关键字和一组不能用做标识符的保留字。
var message,当然了,也可以直接在定义的时候对变量做一个初始化,例如var message = ‘hi’ ;
var message = ‘hi’ ;
message = 100 ; //有效,但不推荐
//这个例子代表变量message一开始保存了一个字符串“hi”,然后该值又被一个数字值100取代了。
function test(){
var message = ‘hi’ ; //局部变量
} ;
test();
alert(message); //错误
//为什么是错误?
//这里,变量message是在函数里用var定义的,当函数被调用时,就会创建该变量并为其赋值。而在此之后,这个变量会立即被销毁。所以在执行alerat()那行代码的时候message已经被销毁了,因此报错。
那么,该怎么解决呢?
function test(){
message = ‘hi’ ; //局部变量
} ;
test();
alert(message); // hi
//在函数内部不用var会创建全局变量。
//但我们并不提倡这种做法,因为局部作用域中定义的全局变量很难去维护。
//所以我们应该选择在开始就定义好所有的变量。转载: 原创作者: 呱?!

即:给响应头设置一个Access-Control-Allow-Origin属性


将callback当成一个参数传递下去

前后端运行结果


vue中关于插槽的文档说明很短,语言又写的很凝练,再加上其和methods,data,computed等常用选项使用频率、使用先后上的差别,这就有可能造成初次接触插槽的开发者容易产生“算了吧,回头再学,反正已经可以写基础组件了”,于是就关闭了vue说明文档。
实际上,插槽的概念很简单,下面通过分三部分来讲。这个部分也是按照vue说明文档的顺序来写的。
进入三部分之前,先让还没接触过插槽的同学对什么是插槽有一个简单的概念:插槽,也就是slot,是组件的一块HTML模板,这块模板显示不显示、以及怎样显示由父组件来决定。 实际上,一个slot最核心的两个问题这里就点出来了,是显示不显示和怎样显示。
由于插槽是一块模板,所以,对于任何一个组件,从模板种类的角度来分,其实都可以分为非插槽模板和插槽模板两大类。
非插槽模板指的是html模板,指的是‘div、span、ul、table’这些,非插槽模板的显示与隐藏以及怎样显示由插件自身控制;插槽模板是slot,它是一个空壳子,因为它显示与隐藏以及最后用什么样的html模板显示由父组件控制。但是插槽显示的位置确由子组件自身决定,slot写在组件template的哪块,父组件传过来的模板将来就显示在哪块。
首先是单个插槽,单个插槽是vue的官方叫法,但是其实也可以叫它默认插槽,或者与具名插槽相对,我们可以叫它匿名插槽。因为它不用设置name属性。
单个插槽可以放置在组件的任意位置,但是就像它的名字一样,一个组件中只能有一个该类插槽。相对应的,具名插槽就可以有很多个,只要名字(name属性)不同就可以了。
下面通过一个例子来展示。
父组件:
-
<template>
-
<div class="father">
-
<h3>这里是父组件</h3>
-
<child>
-
<div class="tmpl">
-
<span>菜单1</span>
-
<span>菜单2</span>
-
<span>菜单3</span>
-
<span>菜单4</span>
-
<span>菜单5</span>
-
<span>菜单6</span>
-
</div>
-
</child>
-
</div>
-
</template>
子组件:
-
<template>
-
<div class="child">
-
<h3>这里是子组件</h3>
-
<slot></slot>
-
</div>
-
</template>
在这个例子里,因为父组件在<child></child>里面写了html模板,那么子组件的匿名插槽这块模板就是下面这样。也就是说,子组件的匿名插槽被使用了,是被下面这块模板使用了。
-
<div class="tmpl">
-
<span>菜单1</span>
-
<span>菜单2</span>
-
<span>菜单3</span>
-
<span>菜单4</span>
-
<span>菜单5</span>
-
<span>菜单6</span>
-
</div>
最终的渲染结果如图所示:

-
-
注:所有demo都加了样式,以方便观察。其中,父组件以灰色背景填充,子组件都以浅蓝色填充。
匿名插槽没有name属性,所以是匿名插槽,那么,插槽加了name属性,就变成了具名插槽。具名插槽可以在一个组件中出现N次。出现在不同的位置。下面的例子,就是一个有两个具名插槽和单个插槽的组件,这三个插槽被父组件用同一套css样式显示了出来,不同的是内容上略有区别。
父组件:
-
<template>
-
<div class="father">
-
<h3>这里是父组件</h3>
-
<child>
-
<div class="tmpl" slot="up">
-
<span>菜单1</span>
-
<span>菜单2</span>
-
<span>菜单3</span>
-
<span>菜单4</span>
-
<span>菜单5</span>
-
<span>菜单6</span>
-
</div>
-
<div class="tmpl" slot="down">
-
<span>菜单-1</span>
-
<span>菜单-2</span>
-
<span>菜单-3</span>
-
<span>菜单-4</span>
-
<span>菜单-5</span>
-
<span>菜单-6</span>
-
</div>
-
<div class="tmpl">
-
<span>菜单->1</span>
-
<span>菜单->2</span>
-
<span>菜单->3</span>
-
<span>菜单->4</span>
-
<span>菜单->5</span>
-
<span>菜单->6</span>
-
</div>
-
</child>
-
</div>
-
</template>
子组件:
-
<template>
-
<div class="child">
-
// 具名插槽
-
<slot name="up"></slot>
-
<h3>这里是子组件</h3>
-
// 具名插槽
-
<slot name="down"></slot>
-
// 匿名插槽
-
<slot></slot>
-
</div>
-
</template>
显示结果如图:

可以看到,父组件通过html模板上的slot属性关联具名插槽。没有slot属性的html模板默认关联匿名插槽。
最后,就是我们的作用域插槽。这个稍微难理解一点。官方叫它作用域插槽,实际上,对比前面两种插槽,我们可以叫它带数据的插槽。什么意思呢,就是前面两种,都是在组件的template里面写
-
匿名插槽
-
<slot></slot>
-
具名插槽
-
<slot name="up"></slot>
但是作用域插槽要求,在slot上面绑定数据。也就是你得写成大概下面这个样子。
-
<slot name="up" :data="data"></slot>
-
export default {
-
data: function(){
-
return {
-
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
-
}
-
},
-
}
我们前面说了,插槽最后显示不显示是看父组件有没有在child下面写模板,像下面那样。
-
<child>
-
html模板
-
</child>
写了,插槽就总得在浏览器上显示点东西,东西就是html该有的模样,没写,插槽就是空壳子,啥都没有。
OK,我们说有html模板的情况,就是父组件会往子组件插模板的情况,那到底插一套什么样的样式呢,这由父组件的html+css共同决定,但是这套样式里面的内容呢?
正因为作用域插槽绑定了一套数据,父组件可以拿来用。于是,情况就变成了这样:样式父组件说了算,但内容可以显示子组件插槽绑定的。
我们再来对比,作用域插槽和单个插槽和具名插槽的区别,因为单个插槽和具名插槽不绑定数据,所以父组件是提供的模板要既包括样式由包括内容的,上面的例子中,你看到的文字,“菜单1”,“菜单2”都是父组件自己提供的内容;而作用域插槽,父组件只需要提供一套样式(在确实用作用域插槽绑定的数据的前提下)。
下面的例子,你就能看到,父组件提供了三种样式(分别是flex、ul、直接显示),都没有提供数据,数据使用的都是子组件插槽自己绑定的那个人名数组。
父组件:
-
<template>
-
<div class="father">
-
<h3>这里是父组件</h3>
-
<!--第一次使用:用flex展示数据-->
-
<child>
-
<template slot-scope="user">
-
<div class="tmpl">
-
<span v-for="item in user.data">{{item}}</span>
-
</div>
-
</template>
-
-
</child>
-
-
<!--第二次使用:用列表展示数据-->
-
<child>
-
<template slot-scope="user">
-
<ul>
-
<li v-for="item in user.data">{{item}}</li>
-
</ul>
-
</template>
-
-
</child>
-
-
<!--第三次使用:直接显示数据-->
-
<child>
-
<template slot-scope="user">
-
{{user.data}}
-
</template>
-
-
</child>
-
-
<!--第四次使用:不使用其提供的数据, 作用域插槽退变成匿名插槽-->
-
<child>
-
我就是模板
-
</child>
-
</div>
-
</template>
子组件:
-
<template>
-
<div class="child">
-
-
<h3>这里是子组件</h3>
-
// 作用域插槽
-
<slot :data="data"></slot>
-
</div>
-
</template>
-
-
export default {
-
data: function(){
-
return {
-
data: ['zhangsan','lisi','wanwu','zhaoliu','tianqi','xiaoba']
-
}
-
}
-
}
结果如图所示:
转载地址:https://segmentfault.com/a/1190000012996217
转载作者/云荒杯倾
首先利用CSS设置好盒子的大小,然后摆放盒子的位置。最后把网页元素比如文字图片等等,放入盒子里面。
盒子模型由元素的内容边框border内边距padding和外边距margin组成
盒子里面的文字和图片等元素是内容区域,盒子的厚度我们称为盒子的边框
盒子内容与边框的距离是内边距,盒子与盒子之间的距离是外边距
语法
border:border-width粗细|border-style样式|border-color颜色
边框综合设置
border: 1px solid red; 没有顺序
表格的细线边框
cellspacing=“0” ,将单元格与单元格之间的距离设置为0
border-collapse:collapse; 表示相邻边框合并在一起
padding属性用于设置内边距,是指边框与内容之间的距离
属性
padding-left左内边距padding-right右内边距padding-top上内边距padding-bottom下内边距
简写
2个值 padding: 上下内边距 左右内边距 ;
4个值 padding: 上内边距 右内边距 下内边距 左内边距 ;
内盒尺寸计算(元素实际大小)
盒子的实际的大小 = 内容的宽度和高度 + 内边距 + 边框
margin属性用于设置外边距。margin控制盒子和盒子之间的距离,属性和简写与padding相同
盒子必须指定宽度(width)然后就给左右的外边距都设置为auto
盒子内的文字水平居中是text-align:center, 而且还可以让行内元素和行内块居中对齐
块级盒子水平居中 左右margin 改为 auto
代码
* {
padding:0; /* 清除内边距 */
margin:0; /* 清除外边距 */
}
注意
行内元素为了兼容性, 尽量只设置左右内外边距, 不设置上下内外边距
当上下相邻的两个块元素相遇时,如果上面的元素有下外边距margin-bottom,下面的元素有上外边距margin-top,则他们之间的垂直间距不是margin-bottom与margin-top之和,取两个值中的较大者这种现象被称为相邻块元素垂直外边距的合并
解决方案
尽量给只给一个盒子添加margin值
对于两个嵌套关系的块元素,如果父元素没有上内边距及边框,父元素的上外边距会与子元素的上外边距发生合并,合并后的外边距为两者中的较大者
解决方案
可以为父元素定义上边框
可以为父元素定义上内边距
可以为父元素添加overflow:hidden
按照优先使用宽度(width)内边距(padding)外边距(margin)
原因
margin有外边距合并还有ie6下面margin加倍的bug所以最后使用
padding会影响盒子大小,需要进行加减计算,其次使用
width没有问题,经常使用宽度剩余法高度剩余法来做
块级元素会独占一行,从上向下顺序排列
行内元素会按照从左到右顺序排列,碰到父元素边缘则自动换行
让盒子从普通流中浮起来,主要作用让多个块级盒子一行显示
将盒子定在浏览器的某一个位置
因为行内块元素可以实现多个元素一行显示但中间会有空白缝隙
因为行内块元素不能实现盒子左右对齐
元素的浮动是指设置了浮动属性的元素
会脱离标准普通流的控制并可以移动到指定位置
让多个盒子(div)水平排列成一行,使得浮动成为布局的重要手段
浮动最早是用来控制图片,实现文字环绕图片的效果
可以实现盒子的左右对齐等等
选择器 { float: 属性值; }
属性值
none(元素不浮动(默认))left(元素左浮动)right(右浮动)
浮
加了浮动的盒子是浮起来的,漂浮在其他标准流盒子的上面
漏
加了浮动的盒子不占位置,它原来的位置漏给了标准流的盒子
特
浮动元素改变display属性, 类似转换成行内块元素,但是元素之间没有空白缝隙
浮动和标准流的父盒子搭配
实际的导航栏中不直接用链接a而是用li包含链接(li+a)
li+a语义更清晰
直接用a搜索引擎容易辨别为有堆砌关键字嫌疑而影响网站排名
浮动元素与父盒子的关系
子盒子的浮动参照父盒子对齐
不会与父盒子的边框重叠,也不会超过父盒子的内边距
浮动元素与兄弟盒子的关系
在一个同一个父级盒子中,如果前一个兄弟盒子是浮动的,那么当前盒子会与前一个盒子的顶部对齐
在一个同一个父级盒子中,如果前一个兄弟盒子是普通流的,那么当前盒子会显示在前一个兄弟盒子的下方
浮动元素不占用原文档流的位置,会对后面的元素排版产生影响
父级元素因为子级浮动导致内部高度为0,清除浮动后,父级会根据浮动的子盒子检测高度,父级有高度就不会影响下面的标准流
选择器{clear:属性值;} clear 清除
属性值
left清除左浮动right清除右浮动both同时清除左右浮动
是W3C推荐的做法是通过在浮动元素末尾添加一个空的标签例如<div style=”clear:both”></div>,或则其他标签br等亦可
优缺点
通俗易懂,书写方便,但是添加许多无意义的标签,结构化较差
可以给父级添加:overflow为hidden|auto|scroll都可以实现
优缺点
代码简洁,但是内容增多时候容易造成不会自动换行导致内容被隐藏掉,无法显示需要溢出的元素
:after 方式为空元素额外标签法的升级版,.clearfix:after { content: ""; display: block; height: 0; clear: both;visibility: hidden; }
.clearfix {*zoom: 1;} /* IE6、7 专有 */
优缺点
符合闭合浮动思想结构语义化正确,但是由于IE6-7不支持:after,使用zoom:1触发hasLayout
方法
.clearfix:before,.clearfix:after {
content:"";
display:table;
}
.clearfix:after {
clear:both;
}
.clearfix {
*zoom:1;
}
优缺点
代码更简洁,但由于IE6-7不支持:after使用zoom:1触发hasLayout
本文将研究 ES6 的 for ... of 循环。
在过去,有两种方法可以遍历 javascript。
首先是经典的 for i 循环,它使你可以遍历数组或可索引的且有 length 属性的任何对象。
for(i=0;i<things.length;i++) { var thing = things[i] /* ... */ }
其次是 for ... in 循环,用于循环一个对象的键/值对。
for(key in things) { if(!thing.hasOwnProperty(key)) { continue; } var thing = things[key] /* ... */ }
for ... in 循环通常被视作旁白,因为它循环了对象的每一个可枚举属性。这包括原型链中父对象的属性,以及被分配为方法的所以属性。换句话说,它遍历了一些人们可能想不到的东西。使用 for ... in 通常意味着循环块中有很多保护子句,以避免出现不需要的属性。
早期的 javascript 通过库解决了这个问题。许多 JavaScript库(例如:Prototype.js,jQuery,lodash 等)都有类似 each 或 foreach 这样的工具方法或函数,可让你无需 for i 或 for ... in 循环去遍历对象和数组。
for ... of 循环是 ES6 试图不用第三方库去解决其中一些问题的方式。
for ... of 循环
for(const thing of things) { /* ... */ }
它将遍历一个可迭代(iterable)对象。
可迭代对象是定义了 @@ iterator 方法的对象,而且 @@iterator 方法返回一个实现了迭代器协议的对象,或者该方法是生成器函数。
在这句话中你需要理解很多东西:
@@是什么意思?)
下面逐个解决这些疑问。
首先,javascript 对象中的一些内置对象天然的可以迭代,比如最容易想到的就是数组对象。可以像下面的代码中一样在 for ... of 循环中使用数组:
const foo = [ 'apples','oranges','pears' ] for(const thing of foo) { console.log(thing)
}
输出结果是数组中的所有元素。
apples oranges
pears
还有数组的 entries 方法,它返回一个可迭代对象。这个可迭代对象在每次循环中返回键和值。例如下面的代码:
const foo = [ 'apples','oranges','pears' ] for(const thing of foo.entries()) { console.log(thing)
}
将输出以下内容
[ 0, 'apples' ]
[ 1, 'oranges' ]
[ 2, 'pears' ]
当用下面的语法时,entries 方法会更有用
const foo = [ 'apples','oranges','pears' ] for(const [key, value] of foo.entries()) { console.log(key,':',value)
}
在 for 循环中声明了两个变量:一个用于返回数组的第一项(值的键或索引),另一个用于第二项(该索引实际对应的值)。
一个普通的 javascript 对象是不可迭代的。如果你执行下面这段代码:
// 无法正常执行 const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of foo)
{ console.log(key,':',value)
}
会得到一个错误
$ node test.js
/path/to/test.js:6 for(const [key, value] of foo) {
TypeError: foo is not iterable
然而全局 Object 对象的静态 entries 方法接受一个普通对象作为参数,并返回一个可迭代对象。就像这样的程序:
const foo = { 'apples':'oranges', 'pears':'prunes' } for(const [key, value] of Object.entries(foo))
{ console.log(key,':',value)
}
能够得到你期望的输出:
$ node test.js
apples : oranges
pears : prunes
如果你想创建自己的可迭代对象,则需要花费更多的时间。你会记得前面说过:
可迭代对象是定义了 @@ iterator 方法的对象,而且 @@iterator 方法返回一个实现了迭代器协议的对象,或者该方法是生成器函数。
搞懂这些内容的最简单方法就是一步一步的去创建可迭代对象。首先,我们需要一个实现 @@iterator 方法的对象。 @@ 表示法有点误导性,我们真正要做的是用预定义的 Symbol.iterator 符号定义方法。
如果用迭代器方法定义对象并尝试遍历:
const foo = {
[Symbol.iterator]: function() {
}
} for(const [key, value] of foo) { console.log(key, value)
}
得到一个新错误:
for(const [key, value] of foo) {
^
TypeError: Result of the Symbol.iterator method is not an object
这是 javascript 告诉我们它在试图调用 Symbol.iterator 方法,但是调用的结果不是对象。
为了消除这个错误,需要用迭代器方法来返回实现了迭代器协议的对象。这意味着迭代器方法需要返回一个有 next 键的对象,而 next 键是一个函数。
const foo = {
[Symbol.iterator]: function() { return { next: function() {
}
}
}
} for(const [key, value] of foo) { console.log(key, value)
}
如果运行上面的代码,则会出现新错误。
for(const [key, value] of foo) {
^
TypeError: Iterator result undefined is not an object
这次 javascript 告诉我们它试图调用 Symbol.iterator 方法,而该对象的确是一个对象,并且实现了 next 方法,但是 next 的返回值不是 javascript 预期的对象。
next 函数需要返回有特定格式的对象——有 value 和 done 这两个键。
next: function() { //... return { done: false, value: 'next value' }
}
done 键是可选的。如果值为 true(表示迭代器已完成迭代),则说明迭代已结束。
如果 done 为 false 或不存在,则需要 value 键。 value 键是通过循环此应该返回的值。
所以在代码中放入另一个程序,它带有一个简单的迭代器,该迭代器返回前十个偶数。
class First20Evens { constructor() { this.currentValue = 0 }
[Symbol.iterator]() { return { next: (function() { this.currentValue+=2 if(this.currentValue > 20) { return {done:true}
} return { value:this.currentValue
}
}).bind(this)
}
}
} const foo = new First20Evens; for(const value of foo) { console.log(value)
}
手动去构建实现迭代器协议的对象不是唯一的选择。生成器对象(由生成器函数返回)也实现了迭代器协议。上面的例子用生成器构建的话看起来像这样:
class First20Evens { constructor() { this.currentValue = 0 }
[Symbol.iterator]() { return function*() { for(let i=1;i<=10;i++) { if(i % 2 === 0) { yield i
}
}
}()
}
} const foo = new First20Evens; for(const item of foo) { console.log(item)
}
本文不会过多地介绍生成器,如果你需要入门的话可以看这篇文章。今天的重要收获是,我们可以使自己的 Symbol.iterator 方法返回一个生成器对象,并且该生成器对象能够在 for ... of 循环中“正常工作”。 “正常工作”是指循环能够持续的在生成器上调用 next,直到生成器停止 yield 值为止。
$ node sample-program.js 2 4 6 8
10
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
Flutter 构建模式
目前,Flutter一共提供了三种运行模式,分别是Debug、Release和Profile模式。其中,Debug模式主要用在软件编写过程中,Release模式主要用于应用发布过程中,而Profile模式则主要用于应用性能分析时,每个模式都有自己特殊的使用场景。下面简介介绍下这几种模式:
Debug模式
Debug模式又名调试模式,Debug模式可以同时在物理设备、仿真器或者模拟器上运行应用。默认情况下,使用flutter run命令运行应用程序时就是使用的Debug模式。在Debug模式下,所有的断言、服务扩展是开启的,并且在模式对快速开发和运行周期进行了编译优化,当使用调试工具进行代码调试时可以直接连接到应用的进程里。
Release模式
Release模式又名发布模式,此模式只能在物理设备上运行,不能在模拟器上运行。使用flutter run --release命令运行应用程序时就是使用的Release模式。在Release模式下,断点、调试信息和服务扩展是不可用的,并且Release模式针对快速启动、快速执行和安装包大小进行了优化。
Profile模式
Profile模式只能在物理设备上运行,不能在模拟器上运行。此模式主要用于应用性能分析,一些应用调试能力是被保留的,目的是分析应用存在的性能问题。Profile模式和Release模式大体相同,不同点体现在,Profile模式的某些服务扩展是启用的,某些进程调试手段也是开启的。
调试模式
在 Debug 模式下,app 可以被安装在物理设备、仿真器或者模拟器上进行调试。在Debug模式下,可以进行如下操作:
断点 是开启的。
服务扩展是开启的。
针对快速开发和运行周期进行了编译优化(但不是针对执行速度、二进制文件大小或者部署)。
调试开启,类似 开发者工具 等调试工具可以连接到进程里。
如果是在 Web 平台下的调试模式,可以进行如下操作:
本次构建 没有 最小化资源并且整个构建 没有 优化性能。
为了简化调试,这个 Web 应用使用了 dartdevc 编译器。
默认情况下,运行 flutter run 会使用 Debug 模式,同时 IDE 也支持这些模式。例如,Android Studio 提供了 Run > Debug… 菜单选项,而且在项目面板中还有一个三角形的绿色运行按钮图标 。
Release 模式
当你想要最大的优化以及最小的占用空间时,就使用 Release 模式来部署 app。 release 模式是不支持模拟器或者仿真器的,使用 Release 模式意味着。
断点是不可用的。
调试信息是不可见的。
调试是禁用的。
编译针对快速启动、快速执行和小的 package 的大小进行了优化。
服务扩展是禁用的。
对于Web开发来说,使用 Release 模式意味着。
这次构建资源已经被压缩,并且性能得以优化。
这个 Web 应用通过 dart2js 编译器构建,以确保更优秀的性能。
Profile 模式
在 Profile 模式下,一些调试能力是被保留的,足够分析你的 app 性能。Profile 模式在仿真器和模拟器上是不可用的,因为他们的行为不能代表真实的性能。和 release 相比, profile 模式有以下不同:
一些服务扩展是启用的。例如,支持 performance overlay。
Tracing 是启用的,一些调试工具,比如 开发者工具 可以连接到进程里。
在 Web 平台使用Profile 模式意味着:
资源文件没有被压缩,但是整体性能已经优化。
这个 Web 应用通过 dart2js 编译器构建。
调试工具
在Flutter应用开发中,有很多工具可以帮助调试 Flutter 应用程序,常见的如下所示。
开发者工具,是一套运行在浏览器的性能及分析工具。
Android Studio/IntelliJ 和 VS Code(借助 Flutter 和 Dart 插件)支持内置的源代码调试器,可以设置断点,单步调试,检查数值。
Flutter inspector,是开发者工具提供的 widget 检查器,也可直接在 Android Studio 和 IntelliJ 中使用(借助 Flutter 插件)。检查器可以可视化展现 widget 树,查看单个 widget 及其属性值,开启性能图层,等等。
开发者工具
要调试及分析应用,开发者工具可能是你的首选。开发者工具运行在浏览器,支持以下特性:
源代码调试器
Widget 检查器,展示可视化的 widget 树; “widget select” 模式,在应用中选择一个 widget,会在 widget 树直接定位到它的位置。
内存分析
时间线视图,支持跟踪,导入及导出跟踪信息
日志视图
如果你在Debug 模式 或Profile 模式 运行,那么可以在浏览器打开开发者工具连接到你的应用。开发者工具不能用在 Release 模式 编译的应用,因为调试和分析信息都被删除了。如果你要用开发者工具分析应用,需确保使用 Profile 模式运行应用。
在这里插入图片描述
断点调试
和其他语言一样,Flutter的断点调试支持在 IDE 或编辑器(比如 Android Studio/IntelliJ 和 VS Code)、或者通过编码两种方式。
其中,开发者工具调试器如下图所示。
在这里插入图片描述
如果需要,在源代码中设置断点,然后点击工具栏中的 【Debug】 按钮,或选择 【Run】 > 【Debug】即可开启调试功能。
在这里插入图片描述
开启调试后,可以在控制台看到如下一些信息。
底部的 Debugger 窗口会显示出堆栈和变量信息。
底部的 Console 窗口会显示详细的日志输出。
调试基于默认的启动配置,如果需要自定义,点击选择目标下拉按钮,选择 Edit configuration 进行配置。
在进行断点调试时,使用得最多的就是单步调试,三个单步调试按钮在暂停后会变为可用状态。
使用 Step in 来进入被调用的方法,在遇到方法内的第一行可执行代码时结束。
使用 Step over 直接执行某个方法调用而不进入内部;该按钮在当前方法内按行执行。
使用 Step out 来跳出当前方法,这种方式会直接执行完所有当前方法内的语句。
除此之外,我们还可以使用代码的方式进行断点调试,我们可以在源代码中使用 debugger()函数来开启断点,当代码运行到此处时就会刮起,如下所示。
import 'dart:developer';
void someFunction(double offset) {
debugger(when: offset > 30.0);
// ...
}
Dart 分析器
如果你使用的是 Android Studio或者VSCode,那么工具会自带的 Dart 分析器默认会检查代码,并发现可能的错误。如果你使用命令行,则可以使用 flutter analyze命令来检查代码。Dart 分析器非常依赖你在代码中添加的类型注解,以帮助跟踪问题。
另外,我们可以使用flutter analyze --flutter-repo命令将分析结果打印到控制台上,每次运行这个命名之前,请先运行flutter update-packages 升级的包,这样就可以获取的依赖包。如果你不这样做,你可能会从dart:ui得到一些错误消息,比如偏移量等。因为执行flutter analysis 命令时并不会主动去拉取依赖。
对于一次性的Dart分析,直接使用flutter analyze --flutter-repo即可,对于连续分析,则可以使用flutter analyze --flutter-repo --watch命令。如果你想知道多少个成员变量丢失了dartdocs,可以添加一个dartdocs参数。
Flutter inspector 工具
Flutter inspector 是分析Flutter组件状态树的利器,Flutter使用小部件来控制页面组件到布局的精准控制,Flutter inspector 可以帮助我们进行如下一些分析。
进行布局分析,理解布局层次
诊断布局问题
在这里插入图片描述
在调试模式下,我们点击Android Studio右边Flutter inspector按钮即可开启Flutter inspector分析,Flutter inspector提供了如下的可视化调试工具。
在这里插入图片描述
Select widget mode:启用此按钮后,选择组件树的代码会自动跳转到对应的源代码里面。
Refresh tree : 重新加载的组件信息。
Slow Animations:放慢动画速度,以便进行视觉上的查验。
Debug Paint: 边框、方向的可视化。
Paint Baselines: 每个渲染框在它的每个文本基线上画一条线。
Repaint Rainbow:查看重绘的严重程度,严重的会被爆红。
除了上面的功能外,我们还可以点击【Open DevTools】打开Flutter的调试页面,可以借助它进行很多性能分析,后面会具体介绍。
在这里插入图片描述
测量应用启动时间
要收集有关 Flutter 应用程序启动所需时间的详细信息,可以在运行 flutter run 命令时使用 trace-startup 和 profile 选项,如下所示。
flutter run --trace-startup --profile
跟踪输出被保存到 Flutter 工程目录在 build 目录下,一个名为 start_up_info.json 的 JSON 文件中,输出列出了从应用程序启动到这些跟踪事件(以微秒捕获)所用的时间,如下所示。
{
"engineEnterTimestampMicros": 2346054348633,
"timeToFrameworkInitMicros": 812748,
"timeToFirstFrameRasterizedMicros": 1573154,
"timeToFirstFrameMicros": 1221472,
"timeAfterFrameworkInitMicros": 408724
}
对应的具体含义如下:
进入 Flutter 引擎时
展示应用第一帧时
初始化Flutter框架时
完成Flutter框架初始化时
使用Android Studio进行调试
Flutter官方推荐使用Android Studio或VSCode进行应用开发, 和其他语言的调试一样,Dart代码的调试流程也差不多。如果还没有Flutter项目,可以新建一个示例项目。通过单击首先,点击调试图标(Debug-run icon)同时打开调试面板并在控制台中运行应用,首次运行应用是最慢的,应用启动后,界面应该是下面这样的。
在这里插入图片描述
然后,我们在在 counter++ 这一行上添加断点。在应用里,点击 + 按钮(FloatingActionButton,或者简称 FAB)来增加数字,应用会暂停。
在这里插入图片描述
你可以 step in/out/over Dart 语句、热重载和恢复执行应用、以及像使用其他调试器一样来使用 Dart 调试器。
Flutter inspector
Flutter inspector 是一个用来可视化以及查看 Flutter widget 树的工具,提供如下功能:
了解现有布局
诊断布局问题
可以使用 Android Studio 窗口右侧的垂直按钮来打开Flutter inspector,如下图所示。
在这里插入图片描述
Flutter outline
Flutter Outline 是一个可视的显示页面构建方法的功能,注意在构建方法上可能与 widget 树不同,可以使用 Android Studio 窗口右侧的垂直按钮切换 outline 的显示。
在这里插入图片描述
Tip: 我们可以安装一个 Presentation Assistant 插件来辅助我们进行开发,Presentation Assistant 提供了很多的快捷功能。例如,当焦点在编辑面板中时,输入 command-Shift-A(Mac)或者 shift-control-A(Windows 和 Linux),该插件会同时显示「查找」面板并显示在所有三个平台上执行此操作的提示。
在这里插入图片描述
然后在输入框中输入attach关键字,显示如下图。
在这里插入图片描述
使用 Android Gradle 调试
为了调试原生代码,你需要一个包含 Android 原生代码的应用。在本节中,你将学会如何连接两个调试器到你的应用:
1)Dart 调试器。
2)Android Gradle 调试器。
创建一个基本的 Flutter 应用,然后替换 lib/main.dart 的代码为以下示例代码。
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'URL Launcher',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'URL Launcher'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Future<void> _launched;
Future<void> _launchInBrowser(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: false, forceWebView: false);
} else {
throw 'Could not launch $url';
}
}
Future<void> _launchInWebViewOrVC(String url) async {
if (await canLaunch(url)) {
await launch(url, forceSafariVC: true, forceWebView: true);
} else {
throw 'Could not launch $url';
}
}
Widget _launchStatus(BuildContext context, AsyncSnapshot<void> snapshot) {
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
return Text('');
}
}
@override
Widget build(BuildContext context) {
String toLaunch = 'https://flutter.dev';
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.all(16.0),
child: Text(toLaunch),
),
RaisedButton(
onPressed: () => setState(() {
_launched = _launchInBrowser(toLaunch);
}),
child: Text('Launch in browser'),
),
Padding(padding: EdgeInsets.all(16.0)),
RaisedButton(
onPressed: () => setState(() {
_launched = _launchInWebViewOrVC(toLaunch);
}),
child: Text('Launch in app'),
),
Padding(padding: EdgeInsets.all(16.0)),
FutureBuilder<void>(future: _launched, builder: _launchStatus),
],
),
),
);
}
}
然后,添加 url_launcher 依赖到 pubspec 文件,并执行 flutter pub get命令拉取依赖包。
name: flutter_app
description: A new Flutter application.
version: 1.0.0+1
dependencies:
flutter:
sdk: flutter
url_launcher: ^3.0.3
cupertino_icons: ^0.1.2
dev_dependencies:
flutter_test:
sdk: flutter
点击调试按钮(Debug-run icon)来同时打开调试面板并启动应用,如下图所示。
在这里插入图片描述
点击 【Attach debugger to Android process】 按钮,从进程对话框中,你应该可以看到每一个设备的入口。选择 show all processes 来显示每个设备可用的进程。
在这里插入图片描述
在调试面板中,你现在应该可以看到一个 Android Debugger 标签页,然后依次选择【app_name】 > 【android】 > 【app】 > 【src】 >【 main】 > 【java】 > 【io.flutter plugins】在项目面板,然后双击 GeneratedProjectRegistrant 在编辑面板中打开 Java 代码,此时Dart 和原生调试器都在与同一个进程交互。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
构建库的常见方法有两种:一种是自己手动构建webpack库打包,设置output为 library; 另一种是基于vue-cli3输出库资源包。我们采用第二种vue脚手架的方式构建库。
新增编译库命令
// package.json
"scripts": {
// ...
"lib": "vue-cli-service build --target lib --name Step --dest dist packages/index.js"
}
// packages/index.js 默认打包Step
import Step from '../steps/src/step';
Step.install = function(Vue) {
Vue.component(Step.name, Step);
};
export default Step;
--name: 库名称。
--target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。
--dest: 输出目录,默认 dist。
[entry]: 最后一个参数为入口文件,默认为 src/App.vue。
更多详细配置查看 ☛ vue脚手架官网
如果该库依赖于其他库,请在vue.config.js 配置 externals
// vue.config.js
module.exports = {
configureWebpack:{
externals: {
vue: 'Vue',
'vue-router':'VueRouter',
axios: 'axios'
}
}
}
执行 npm run lib 就可以发现我们的库被打包到了 根目录的dist文件夹下。
添加 .npmignore 文件(可选)
和 .gitignore 的语法一样,具体需要提交什么文件,看各自的实际情况
# 忽略目录
examples/
packages/
public/
# 忽略指定文件
vue.config.js
babel.config.js
*.map
配置npm库信息
配置package.json文件,以发布库文件。
{
"name": "gis",
"version": "1.2.5",
"description": "基于 Vue 的库文件",
"main": "dist/gis.umd.min.js",
"keyword": "vue gis",
"private": false,
"files": ["dist"],
"license": "MIT"
}
name: 包名,该名字是唯一的。可在 npm 官网搜索名字,如果存在则需换个名字。
version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。
description: 描述。
main: 入口文件,该字段需指向我们最终编译后的包文件。
keyword:关键字,以空格分离希望用户最终搜索的词。
author:作者
files: 要上传的文件
private:是否私有,需要修改为 false 才能发布到 npm
license: 开源协议
dependencies: 依赖库
注意每次发布新的库,需要更改版本号,规则如下:
"version": "1.2.5" 主版本号为 1,次版本号 2,修订号 5
主版本号(Major):当你做了不兼容的API修改
次版本号(Minor):当你做了向下兼容的功能性新增
修订号(Patch):当你做了向下兼容的问题修正
登录npm
首先设置登录的npm镜像地址
npm config set registry http://168.20.20.57.4873
然后在终端执行登录命令,输入用户名、密码、邮箱即可登录
npm login
接着发布库资源到npm
npm publish
最后发布成功可到官网查看对应的包并下载
npm install package_name
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务无缝轮播一直是面试的热门题目,而大部分答案都是复制第一张到最后。诚然,这种方法是非常标准,那么有没有另类一点的方法呢?
第一种方法是需要把所有图片一张张摆好,然后慢慢移动的,
但是我能不能直接不摆就硬移动呢?
如果你使用过vue的transition,我们是可以通过给每一张图片来添加入场动画和离场动画来模拟这个移动
这样看起来的效果就是图片从右边一直往左移动,但是这个不一样的地方是,我们每一个元素都有这个进场动画和离场动画,我们根本不用关心它是第几个元素,你只管轮播就是。
很简单,我们自己实现一个transtition的效果就好啦,主要做的是以下两点
xx-enter-active动画
xx-leave-active, 注意要让动画播完才消失
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' el.addEventListener('animationend',animationEvent)
} function animationEvent(e){
e.target.className = e.target.className.replace(' slide-leave-active','')
e.target.style.display = 'none' e.target.removeEventListener('animationend',animationEvent)
} function show(el){
el.style.display = 'flex' el.className += ' slide-enter-active' }
这里我们使用了animationend来监听动画结束,注意这里每次从新添加类的时候需要重新添加监听器,不然会无法监听。如果不使用这个方法你可以使用定时器的方式来移除leave-active类。
function hide(el){
el.className = el.className.replace(' slide-enter-active','')
el.className += ' slide-leave-active' setTimeout(()=>
{ //动画结束后清除class el.className = el.className.replace(' slide-leave-active','')
el.style.display = 'none' }, ANIMATION_TIME) //这个ANIMATION_TIME为你在css中动画执行的时间 }
.slide-enter-active{ position: absolute; animation: slideIn ease .5s forwards;
} .slide-leave-active{ position: absolute; animation: slideOut ease .5s forwards;
} @keyframes slideIn {
0%{ transform: translateX(100%);
}
100%{ transform: translateX(0);
}
} @keyframes slideOut {
0%{ transform: translateX(0);
}
100%{ transform: translateX(-100%);
}
}
需要注意的是这里的 forwards属性,这个属性表示你的元素状态将保持动画后的状态,如果不设置的话,动画跑完一遍,你的元素本来执行了离开动画,执行完以后会回来中央位置杵着。这个时候你会问了,上面的代码不是写了,动画执行完就隐藏元素吗?
如果你使用上面的setTimeout来命令元素执行完动画后消失,那么可能会有一瞬间的闪烁,因为实际业务中,你的代码可能比较复杂,setTimeout没法在那么精准的时间内执行。保险起见,就让元素保持动画离开的最后状态,即translateX(-100%)。此时元素已经在屏幕外了,不用关心它的表现了
很简单,我们进一个新元素的时候同时移除旧元素即可,两者同时执行进场和离场动画即可。
function autoPlay(){
setTimeout(()=>{
toggleShow(新元素, 旧元素) this.autoPlay()
},DURATION) //DURATION为动画间隔时间 } function toggleShow(newE,oldE){ //旧ele和新ele同时动画 hide(oldE)
show(newE)
}
垂直居中基本上是入门 CSS 必须要掌握的问题了,我们肯定在各种教程中都看到过“CSS 垂直居中的 N 种方法”,通常来说,这些方法已经可以满足各种使用场景了,然而当我们碰到了需要使用某些特殊字体进行混排、或者使文字对齐图标的情况时,也许会发现,无论使用哪种垂直居中的方法,总是感觉文字向上或向下偏移了几像素,不得不专门对它们进行位移,为什么会出现这种情况呢?
下图是一个使用各种常见的垂直居中的方法来居中文字的示例,其中涉及到不同字体的混排,可以看出,虽然这里面用了几种常用的垂直居中的方法,但是在实际的观感上这些文字都没有恰好垂直居中,有些文字看起来比较居中,而有些文字则偏移得很厉害。
在线查看:CodePen(字体文件直接引用了谷歌字体,如果没有效果需要注意网络情况)
通过设置vertical-align:middle对文字进行垂直居中时,父元素需要设置font-size: 0,因为vertical-align:middle是将子元素的中点与父元素的baseline + x-height / 2的位置进行对齐的,设置字号为 0 可以保证让这些线的位置都重合在中点。
我们用鼠标选中这些文字,就能发现选中的区域确实是在父层容器里垂直居中的,那么为什么文字却各有高低呢?这里就涉及到了字体本身的构造和相关的度量值。
这里先提出一个问题,我们在 CSS 中给文字设置了 font-size,这个值实际设置的是字体的什么属性呢?
下面的图给出了一个示例,文字所在的标签均为 span,对每种字体的文字都设置了红色的 outline 以便观察,且设有 line-height: normal。从图中可以看出,虽然这些文字的字号都是 40px,但是他们的宽高都各不相同,所以字号并非设置了文字实际显示的大小。
为了解答这个问题,我们需要对字体进行深入了解,以下这些内容是西文字体的相关概念。首先一个字体会有一个 EM Square(也被称为 UPM、em、em size)[4],这个值最初在排版中表示一个字体中大写 M 的宽度,以这个值构成一个正方形,那么所有字母都可以被容纳进去,此时这个值实际反映的就成了字体容器的高度。在金属活字中,这个容器就是每个字符的金属块,在一种字体里,它们的高度都是统一的,这样每个字模都可以放入印刷工具中并进行排印。在数码排印中,em 是一个被设置了大小的方格,计量单位是一种相对单位,会根据实际字体大小缩放,例如 1000 单位的字体设置了 16pt 的字号,那么这里 1000 单位的大小就是 16pt。Em 在 OpenType 字体中通常为 1000 ,在 TrueType 字体中通常为 1024 或 2048(2 的 n 次幂)。
金属活字,图片来自 http://designwithfontforge.com/en-US/The_EM_Square.html
字体本身还有很多概念和度量值(metrics),这里介绍几个常见的概念,以维基百科的这张图为例(下面的度量值的计量单位均为基于 em 的相对单位):
接下来我们在 FontForge 软件里看看这些值的取值,这里以 Arial 字体给出一个例子:
从图中可以看出,在 General 菜单中,Arial 的 em size 是 2048,字体的 ascent 是1638,descent 是410,在 OS/2 菜单的 Metrics 信息中,可以得到 capital height 是 1467,x height 为 1062,line gap 为 67。
然而这里需要注意,尽管我们在 General 菜单中得到了 ascent 和 descent 的取值,但是这个值应该仅用于字体的设计,它们的和永远为 em size;而计算机在实际进行渲染的时候是按照 OS/2 菜单中对应的值来计算,一般操作系统会使用 hhea(Horizontal Header Table)表的 HHead Ascent 和 HHead Descent,而 Windows 是个特例,会使用 Win Ascent 和 Win Descent。通常来说,实际用于渲染的 ascent 和 descent 取值要比用于字体设计的大,这是因为多出来的区域通常会留给注音符号或用来控制行间距,如下图所示,字母顶部的水平线即为第一张图中 ascent 高度 1638,而注音符号均超过了这个区域。根据资料的说法[5],在一些软件中,如果文字内容超过用于渲染的 ascent 和 descent,就会被截断,不过我在浏览器里实验后发现浏览器并没有做这个截断(Edge 86.0.608.0 Canary (64 bit), MacOS 10.15.6)。
在本文中,我们将后面提到的 ascent 和 descent 均认为是 OS/2 选项中读取到的用于渲染的 ascent 和 descent 值,同时我们将 ascent + descent 的值叫做 content-area。
理论上一个字体在 Windows 和 MacOS 上的渲染应该保持一致,即各自系统上的 ascent 和 descent 应该相同,然而有些字体在设计时不知道出于什么原因,导致其确实在两个系统中有不同的表现。以下是 Roboto 的例子:
Differences between Win and HHead metrics cause the font to be rendered differently on Windows vs. iOS (or Mac I assume) · Issue #267 · googlefonts/roboto
那么回到本节一开始的问题,CSS 中的font-size设置的值表示什么,想必我们已经有了答案,那就是一个字体 em size 对应的大小;而文字在设置了line-height: normal时,行高的取值则为 content-area + line-gap,即文本实际撑起来的高度。
知道了这些,我们就不难算出一个字体的显示效果,上面 Arial 字体在line-height: normal和font-size: 100px时撑起的高度为(1854 + 434 + 67) / 2048 * 100px = 115px。
在实验中发现,对于一个行内元素,鼠标拉取的 selection 高度为当前行line-height最高的元素值。如果是块状元素,当line-height的值为大于 content-area 时,selection 高度为line-height,当其小于等于 content-area 时,其高度为 content-area 的高度。
在中间插一个问题,我们应该都使用过 line-height 来给文字进行垂直居中,那么 line-height 实际是以字体的哪个部分的中点进行计算呢?为了验证这个问题,我新建了一个很有“设计感”的字体,em size 设为 1000,ascent 为 800,descent 为 200,并对其分别设置了正常的和比较夸张的 metrics:
上面图中左边是 FontForge 里设置的 metrics,右边是实际显示效果,文字字号设为 100px,四个字母均在父层的 flex 布局下垂直居中,四个字母的 line-height 分别为 0、1em、normal、3em,红色边框是元素的 outline,黄色背景是鼠标选取的背景。由上面两张图可以看出,字体的 metrics 对文字渲染位置的影响还是很大的。同时可以看出,在设置 line-height 时,虽然 line gap 参与了撑起取值为 normal 的空间,但是不参与文字垂直居中的计算,即垂直居中的中点始终是 content-area 的中点。
我们又对字体进行了微调,使其 ascent 有一定偏移,这时可以看出 1em 行高的文字 outline 恰好在正中间,因此可以得出结论:在浏览器进行渲染时,em square 总是相对于 content-area 垂直居中。
说完了字体构造,又回到上一节的问题,为什么不同字体文字混排的时候进行垂直居中,文字各有高低呢?
在这个问题上,本文给出这样一个结论,那就是因为不同字体的各项度量值均不相同,在进行垂直居中布局时,content-area 的中点与视觉的中点不统一,因此导致实际看起来存在位置偏移,下面这张图是 Arial 字体的几个中线位置:
从图上可以看出来,大写字母和小写字母的视觉中线与整个字符的中线还是存在一定的偏移的。这里我没有找到排版相关学科的定论,究竟以哪条线进行居中更符合人眼观感的居中,以我个人的观感来看,大写字母的中线可能看起来更加舒服一点(尤其是与没有小写字母的内容进行混排的时候)。
需要注意一点,这里选择的 Arial 这个字体本身的偏移比较少,所以使用时整体感觉还是比较居中的,这并不代表其他字体也都是这样。
对于中文字体,本身的设计上没有基线、升部、降部等说法,每个字都在一个方形盒子中。但是在计算机上显示时,也在一定程度上沿用了西文字体的概念,通常来说,中文字体的方形盒子中文字体底端在 baseline 和 descender 之间,顶端超出一点 ascender,而标点符号正好在 baseline 上。
我们已经了解了字体的相关概念,那么如何解决在使用字体时出现的偏移问题呢?
通过上面的内容可以知道,文字显示的偏移主要是视觉上的中点和渲染时的中点不一致导致的,那么我们只要把这个不一致修正过来,就可以实现视觉上的居中了。
为了实现这个目标,我们可以借助 vertical-align 这个属性来完成。当 vertical-align 取值为数值的时候,该值就表示将子元素的基线与父元素基线的距离,其中正数朝上,负数朝下。
这里介绍的方案,是把某个字体下的文字通过计算设置 vertical-align 的数值偏移,使其大写字母的视觉中点与用于计算垂直居中的点重合,这样字体本身的属性就不再影响居中的计算。
具体我们将通过以下的计算方法来获取:首先我们需要已知当前字体的 em-size,ascent,descent,capital height 这几个值(如果不知道 em-size,也可以提供其他值与 em-size 的比值),以下依然以 Arial 为例:
const emSize = 2048; const ascent = 1854; const descent = 434; const capitalHeight = 1467;
// 计算前需要已知给定的字体大小 const fontSize = FONT_SIZE; // 根据文字大小,求得文字的偏移 const verticalAlign = ((ascent - descent - capitalHeight) / emSize) * fontSize; return ( <span style={{ fontFamily: FONT_FAMILY, fontSize }}> <span style={{ verticalAlign }}>TEXT</span> </span> )
由此设置以后,外层 span 将表现得像一个普通的可替换元素参与行内的布局,在一定程度上无视字体 metrics 的差异,可以使用各种方法对其进行垂直居中。
由于这种方案具有固定的计算步骤,因此可以根据具体的开发需求,将其封装为组件、使用 CSS 自定义属性或使用 CSS 预处理器对文本进行处理,通过传入字体信息,就能修正文字垂直偏移。
虽然上述的方案可以在一定程度上解决文字垂直居中的问题,但是在实际使用中还存在着不方便的地方,我们需要在使用字体之前就知道字体的各项 metrics,在自定义字体较少的情况下,开发者可以手动使用 FontForge 等工具查看,然而当字体较多时,挨个查看还是比较麻烦的。
目前的一种思路是我们可以使用 Canvas 获取字体的相关信息,如现在已经有开源的获取字体 metrics 的库 FontMetrics.js。它的核心思想是使用 Canvas 渲染对应字体的文字,然后使用 getImageData 对渲染出来的内容进行分析。如果在实际项目中,这种方案可能导致潜在的性能问题;而且这种方式获取到的是渲染后的结果,部分字体作者在构建字体时并没有严格将设计的 metrics 和字符对应,这也会导致获取到的 metrics 不够准确。
另一种思路是直接解析字体文件,拿到字体的 metrics 信息,如 opentype.js 这个项目。不过这种做法也不够轻量,不适合在实际运行中使用,不过可以考虑在打包过程中自动执行这个过程。
此外,目前的解决方案更多是偏向理论的方法,当文字本身字号较小的情况下,浏览器可能并不能按照预期的效果渲染,文字会根据所处的 DOM 环境不同而具有 1px 的偏移[9]。
CSS Houdini 提出了一个 Font Metrics 草案[6],可以针对文字渲染调整字体相关的 metrics。从目前的设计来看,可以调整 baseline 位置、字体的 em size,以及字体的边界大小(即 content-area)等配置,通过这些可以解决因字体的属性导致的排版问题。
[Exposed=Window] interface FontMetrics {
readonly attribute double width;
readonly attribute FrozenArray<double> advances;
readonly attribute double boundingBoxLeft;
readonly attribute double boundingBoxRight;
readonly attribute double height;
readonly attribute double emHeightAscent;
readonly attribute double emHeightDescent;
readonly attribute double boundingBoxAscent;
readonly attribute double boundingBoxDescent;
readonly attribute double fontBoundingBoxAscent;
readonly attribute double fontBoundingBoxDescent;
readonly attribute Baseline dominantBaseline;
readonly attribute FrozenArray<Baseline> baselines;
readonly attribute FrozenArray<Font> fonts;
};
从 https://ishoudinireadyyet.com/ 这个网站上可以看到,目前 Font Metrics 依然在提议阶段,还不能确定其 API 具体内容,或者以后是否会存在这一个特性,因此只能说是一个在未来也许可行的文字排版处理方案。
文本垂直居中的问题一直是 CSS 中最常见的问题,但是却很难引起注意,我个人觉得是因为我们常用的微软雅黑、苹方等字体本身在设计上比较规范,在通常情况下都显得比较居中。但是当一个字体不是那么“规范”时,传统的各种方法似乎就有点无能为力了。
本文分析了导致了文字偏移的因素,并给出寻找文字垂直居中位置的方案。
由于涉及到 IFC 的问题本身就很复杂[7],关于内联元素使用 line-height 与 vertical-align 进行居中的各种小技巧因为与本文不是强相关,所以在文章内也没有提及,如果对这些内容比较感兴趣,也可以通过下面的参考资料寻找一些相关介绍。
蓝蓝设计( www.lanlanwork.com )是一家专注而深入的界面设计公司,为期望卓越的国内外企业提供卓越的UI界面设计、BS界面设计 、 cs界面设计 、 ipad界面设计 、 包装设计 、 图标定制 、 用户体验 、交互设计、 网站建设 、平面设计服务
蓝蓝设计的小编 http://www.lanlanwork.com