精华内容
下载资源
问答
  • 文章目录一、【固定式】容器固定值,不随浏览器窗口变化改变1.单列布局2.混合布局:单双单3.混合布局:单三单二、【自适应】宽高...混合布局:单三单(侧栏固定)四、【响应式】还未学到css3,尽情期待...... T...





    🔶图我就不发了,源代码可以复制粘贴直接看效果。CCS3布局,等我学完了会完善此篇博客的。

    另附传送门:
    1.静态布局、自适应布局、流式布局、响应式布局、弹性布局等的概念和区别
    2.2019年前端五大布局,你学会了多少?
    3.利用HTML和CSS实现常见的布局
    4.前端布局方式分类
    5.响应式布局与自适应式布局有什么不同
    6.响应式和自适应有什么区别
    7.CSS 自适应布局
    8.使用CSS3 Media Queries实现网页自适应
    9.当外面的容器的height使用百分比时,如何设置line-height可以使里面的文字垂直居中。
    10.position,float,display布局方式规整
    11.张鑫旭——基于display: inline-block的列表布局(这是效果页)
    12.HTML主流布局差异动态演示 流式 自适应 响应式 静态(这是效果页)
    13.超经典面试题:用多种方法实现圣杯布局和双飞翼布局
    14.圣杯布局和双飞翼布局的理解与思考
    15.经典的流式布局实现方法总结(三)(圣杯布局&双飞翼布局)


    一、【静态布局】宽高固定值,浏览器放大缩小,布局不变(缩小浏览器后,水平有滚动条)

    1-1.单列布局

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: "Microsoft YaHei";
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	border: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	min-width: 100%;
    	min-height: 100%;
    	overflow: hidden;
    	background: #fcc;
    	position: absolute;
    }
    
    .container {
    	width: 1070px;
    	margin: 0 auto;
    }
    
    /*index.css*/
    .header, .nav, .main-body, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header-wrap {
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .header {
    	height: 180px;
    	line-height: 180px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .footer-wrap {
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .footer {
    	height: 80px;
    	line-height: 80px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .nav-wrap {
    	background: rgba(10, 199, 33, 0.2);
    }
    
    .nav {
    	height: 46px;
    	line-height: 46px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .main-body-wrap {
    	background: rgba(0, 0, 0, 0.2);
    }
    
    .main-body {
    	height: 600px;
    	line-height: 600px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header-wrap">
    			<div class="header container">头部</div>
    		</div>
    		<div class="nav-wrap">
    			<div class="nav container">导航栏</div>
    		</div>
    		<div class="main-body-wrap">
    			<div class="main-body container">主要</div>
    		</div>
    		<div class="footer-wrap">
    			<div class="footer container">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    1-2.混合布局:单双单

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	min-width: 100%;
    	min-height: 100%;
    	background: #fcc;
    	position: absolute;
    }
    
    .container {
    	width: 1070px;
    	margin: 0 auto;
    }
    
    /*index.css*/
    .header, .nav, .main-left, .main-middle, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header-wrap {
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .header {
    	height: 180px;
    	line-height: 180px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .footer-wrap {
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .footer {
    	height: 80px;
    	line-height: 80px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .nav-wrap {
    	background: rgba(10, 199, 33, 0.2);
    }
    
    .nav {
    	height: 46px;
    	line-height: 46px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .main-left, .main-middle {
    	height: 600px;
    	line-height: 600px;
    }
    
    .main-left {
    	width: 270px;
    	float: left;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-middle {
    	width: 800px;
    	float: right;
    	background: rgba(0, 0, 0, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header-wrap">
    			<div class="header container">头部</div>
    		</div>
    		<div class="nav-wrap">
    			<div class="nav container">导航栏</div>
    		</div>
    		<div class="main-body-wrap">
    			<div class="main-body-box container clearfix">
    				<div class="main-left">左栏</div>
    				<div class="main-middle">主要</div>
    			</div>
    		</div>
    		<div class="footer-wrap">
    			<div class="footer container">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    1-3.混合布局:单三单

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: "Microsoft YaHei";
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	min-width: 100%;
    	min-height: 100%;
    	background: #fcc;
    	position: absolute;
    }
    
    .container {
    	width: 1070px;
    	margin: 0 auto;
    }
    
    /*index.css*/
    .header, .nav, .main-left, .main-middle, .main-right, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	color: rgba(255, 255, 255, 0.8);
    	text-align: center;
    }
    
    .header-wrap {
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .header {
    	height: 180px;
    	line-height: 180px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .footer-wrap {
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .footer {
    	height: 80px;
    	line-height: 80px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .nav-wrap {
    	background: rgba(10, 199, 33, 0.2);
    }
    
    .nav {
    	height: 46px;
    	line-height: 46px;
    	border-left: 1px solid rgba(255, 255, 255, 0.5);
    	border-right: 1px solid rgba(255, 255, 255, 0.5);
    }
    
    .main-body-box {
    	width: 1470px;
    	margin: 0 auto;
    }
    
    .main-left, .main-middle, .main-right {
    	/*width: auto;		可不设,自动分配*/
    	height: 600px;
    	line-height: 600px;
    	float: left;
    }
    
    .main-left, .main-right {
    	width: 200px;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-middle {
    	background: rgba(0, 0, 0, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header-wrap">
    			<div class="header container">头部</div>
    		</div>
    		<div class="nav-wrap">
    			<div class="nav container">导航栏</div>
    		</div>
    		<div class="main-body">
    			<div class="main-body-box clearfix">
    				<div class="main-left">左栏</div>
    				<div class="main-middle container">主要</div>
    				<div class="main-right">右栏</div>
    			</div>
    		</div>
    		<div class="footer-wrap">
    			<div class="footer container">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    二、【流式布局 / 百分比布局 / 等比缩放布局】宽度百分比,浏览器放大缩小,布局不变(水平无滚动条,高一般随内容撑开或固定值)

    2-1.单列布局

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarer {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-body, .footer {
    	display: table;
    	width: 100%;
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .text {
    	display: table-cell;
    	vertical-align:middle;
    }
    
    .header {
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .main-body {
    	height: 600px;
    	background: rgba(0, 0, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body">
    			<div class="text">主要</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    2-2.混合布局:单双单

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .footer {
    	display: table;
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header, .main-body, .footer {
    	width: 100%;
    }
    
    .text {
    	display: table-cell;
    	vertical-align: middle;
    }
    
    .header {
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-left, .main-middle {
    	height: 600px;
    	float: left;
    }
    
    .main-left {
    	width: 30%;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-middle {
    	width: 70%;
    	background: rgba(0, 0, 0, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body clearfix">
    			<div class="main-left">
    				<div class="text">左栏</div>
    			</div>
    			<div class="main-middle">
    				<div class="text">主要</div>
    			</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    2-3.混合布局:单三单

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .main-right, .footer {
    	display: table;
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header, .footer, .main-body {
    	width: 100%;
    }
    
    .text {
    	display: table-cell;
    	vertical-align: middle;
    }
    
    .header {
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-left, .main-middle, .main-right {
    	height: 600px;
    	float: left;
    }
    
    .main-left, .main-right {
    	width: 20%;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-middle {
    	width: 60%;
    	background: rgba(0, 0, 0, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body clearfix">
    			<div class="main-left">
    				<div class="text">左栏</div>
    			</div>
    			<div class="main-middle">
    				<div class="text">主要</div>
    			</div>
    			<div class="main-right">
    				<div class="text">右栏</div>
    			</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    三、【静态+流式】侧栏宽固定,主体宽度自适应

    3-1.混合布局:单双单

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	/*min-width: 600px;		可加上最小宽值,这样就会溢出显示滚动条*/
    	overflow: hidden;		/*声明溢出隐藏,防止浮动错位带来的部分内容溢出*/
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header, .footer, .main-left {
    	display: table;
    }
    
    .text {
    	display: table-cell;
    	vertical-align: middle;
    }
    
    .header {
    	width: 100%;
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	width: 100%;
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-body {
    	width: 100%;
    	height: 600px;
    	position: relative;
    }
    
    .main-left, .main-middle {
    	height: 100%;
    	position: absolute;
    }
    
    .main-left {
    	width: 300px;
    	background: rgba(100, 100, 100, 0.2);
    	left: 0px;
    }
    
    .main-middle {
    	width: auto;
    	line-height: 0;
    	overflow: hidden;		/*这里隐藏错位居中多出的位置,当然,可以去掉overflow,在子元素用float,就是下面被注释的那行*/
    	background: rgba(0, 0, 0, 0.2);
    	left: 300px;
    	right: 0;
    }
    
    .middle-text-wrap {
    	/*float: left;	这里是水平居中用,不过我上面用text-align居中了,所以注释掉,当然,可以不注释,把错位框浮动回来,就不需要overflow隐藏溢出了*/
    	position: relative;
    	left: 50%;
    	top: 50%;
    }
    
    .middle-text {
    	position: relative;
    	left: -50%;		/*或者 right: 50%; */
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body">
    			<div class="main-left">
    				<div class="text">左栏</div>
    			</div>
    			<div class="main-middle">
    				<div class="middle-text-wrap clearfix">
    					<div class="middle-text">主要</div>
    				</div>
    			</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    3-2.混合布局:单三单(单侧栏固定)

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	/*min-width: 600px;		想要窗口缩小后不隐藏右栏,可设最小值,这样就能出现滚动条了*/
    	overflow: hidden;		/*声明溢出隐藏,防止浮动错位带来的部分内容溢出*/
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .main-right, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .text {
    	display: table-cell;
    	vertical-align: middle;
    }
    
    .header, .footer {
    	display: table;
    	width: 100%;
    }
    
    .header {
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-body {
    	width: 100%;
    	min-width: 300px;		/*此处控制中间自适应最小显示宽度,300px——即缩小后显示左侧栏,其他部分压缩*/
    	position: relative;
    }
    
    .main-left, .main-body-box, .main-middle, .main-right {
    	height: 600px;
    	float: left;
    }
    
    .main-left {
    	display: table;
    	width: 300px;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-body-box {
    	float: none;	/*由于使用了position: absolute; 所以float: left; 可以不需要,当然不 none,也是可以的,不冲突*/
    	line-height: 0;	/*行高为0,解决垂直方向上行高带来的误差而无法居中的问题*/
    	position: absolute;
    	left: 300px;
    	right: 0;
    }
    
    .main-middle {
    	width: 70%;
    	background: rgba(0, 0, 0, 0.2);
    	position: relative;
    }
    
    .main-right {
    	width: 30%;
    	background: rgba(100, 100, 100, 0.2);
    	position: relative;
    }
    
    .box-text-wrap {
    	/*float: left;	绝对定位absolute和float的水平错位居中知识,此处可不用float,我上面text-align居中了*/
    	position: relative;
    	left: 50%;
    	top: 50%;
    }
    
    .box-text {
    	position: relative;
    	left: -50%;		/*或者 right: 50%; */
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body clearfix">
    			<div class="main-left">
    				<div class="text">左栏</div>
    			</div>
    			<div class="main-body-box">
    				<div class="main-middle">
    					<div class="box-text-wrap">
    						<div class="box-text">主要</div>
    					</div>
    				</div>
    				<div class="main-right">
    					<div class="box-text-wrap">
    						<div class="box-text">右栏</div>
    					</div>
    				</div>
    			</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    3-3.混合布局:单三单(双侧栏固定)

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <title></title>
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: 'Microsoft YaHei';
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	height: 100%;
    	/*min-width: 600px;		想要窗口缩小后不隐藏右栏,可设最小值,这样就能出现滚动条了*/
    	overflow: hidden;		/*声明溢出隐藏,防止浮动错位带来的部分内容溢出*/
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .main-right, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .text {
    	display: table-cell;
    	vertical-align: middle;
    }
    
    .header, .footer {
    	display: table;
    	width: 100%;
    }
    
    .header {
    	height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-body {
    	width: 100%;
    	min-width: 600px;
    	position: relative;
    }
    
    .main-left, .main-middle, .main-right {
    	height: 600px;
    	float: left;
    }
    
    .main-left, .main-right {
    	display: table;
    	width: 300px;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-right {
    	position: absolute;
    	right: 0;
    }
    
    .main-middle {
    	width: auto;
    	line-height: 0;
    	overflow: hidden;		/*中间部分,因为是自适应,浏览器缩小挤压后,用overflow隐藏掉文字溢出*/
    	background: rgba(0, 0, 0, 0.2);
    	position: absolute;
    	left: 300px;
    	right: 300px;
    }
    
    .middle-text-wrap {
    	/*float: left;	绝对定位absolute与float的水平错位居中知识,此处可不用float,我上面text-align居中了*/
    	position: relative;
    	left: 50%;
    	top: 50%;
    }
    
    .middle-text {
    	position: relative;
    	left: -50%;		/*或者 right: 50%; */
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">
    			<div class="text">头部</div>
    		</div>
    		<div class="main-body clearfix">
    			<div class="main-left">
    				<div class="text">左栏</div>
    			</div>
    			<div class="main-middle">
    				<div class="middle-text-wrap clearfix">
    					<div class="middle-text">主要</div>
    				</div>
    			</div>
    			<div class="main-right">
    				<div class="text">右栏</div>
    			</div>
    		</div>
    		<div class="footer">
    			<div class="text">页脚</div>
    		</div>
    	</div>
    </body>
    </html>
    

    四、【圣杯布局】&【双飞翼布局】(属于【流式布局】:左右侧栏固定,主栏自适应)

    4-1.【圣杯布局】

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: "Microsoft YaHei";
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	min-width: 550px;
    	min-height: 100%;
    	overflow: hidden;
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .main-right, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header, .main-middle, .footer {
    	width: 100%;
    }
    
    .header {
    	height: 180px;
    	line-height: 180px;	
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	line-height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    }
    
    .main-body {
    	padding-left: 200px; 
    	padding-right: 150px;
    }
    
    .main-left, .main-middle, .main-right {
    	height: 600px;
    	line-height: 600px;
    	float: left;
    }
    
    .main-left, .main-right {
    	width: 200px;
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-left {
    	margin-left: -100%;
    	position: relative;
    	right: 200px;
    }
    
    .main-right {
    	margin-right: -200px; 
    }
    
    .main-middle {
    	background: rgba(0, 0, 0, 0.2);
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">头部</div>
    		<div class="main-body clearfix">
    			<div class="main-middle">主要</div>
    			<div class="main-left">左栏</div>
    			<div class="main-right">右栏</div>
    		</div>
    		<div  class="footer">页脚</div>
    	</div>
    </body>
    </html>
    

    4-2.【双飞翼布局】

    <!DOCTYPE HTML>
    <html>
    <head>
    <meta charset="utf-8" />
    <!--
    <link rel="stylesheet" href="css/reset.css" />
    <link rel="stylesheet" href="css/common.css" />
    <link rel="stylesheet" href="css/index.css" />
    -->
    <style type="text/css">
    /*reset.css*/
    body, h1, h2, h3, h4, h5, h6, dl, dt, dd, ol, ul, li, p, a, input, form, textarea {
    	padding: 0;
    	margin: 0;
    	font-family: "Microsoft YaHei";
    }
    
    ol, ul, li {
    	list-style: none;
    }
    
    img {
    	display: block;
    	border: none;
    }
    
    a {
    	display: block;
    	text-decoration: none;
    }
    
    .clearfix {
    	zoom: 1;
    }
    
    .clearfix:after {
    	display: block;
    	content: '';
    	clear: both;
    	visibility: hidden;
    	height: 0;
    }
    
    /*common.css*/
    .bg {
    	width: 100%;
    	min-width: 550px;
    	min-height: 100%;
    	overflow: hidden;
    	background: #fcc;
    	position: absolute;
    }
    
    /*index.css*/
    .header, .main-left, .main-middle, .main-right, .footer {
    	font-size: 20px;
    	font-weight: bold;
    	text-align: center;
    	color: rgba(255, 255, 255, 0.8);
    }
    
    .header, .main-middle-wrap, .footer {
    	width: 100%;
    }
    
    .header {
    	height: 180px;
    	line-height: 180px;
    	background: rgba(150, 100, 0, 0.2);
    }
    
    .footer {
    	height: 80px;
    	line-height: 80px;
    	background: rgba(0, 66, 200, 0.2);
    	clear: both;
    }
    
    .main-left, .main-middle-wrap, .main-right {
    	float: left;
    }
    
    .main-left, .main-middle, .main-right {
    	line-height: 600px;
    }
    
    .main-left, .main-right {
    	background: rgba(100, 100, 100, 0.2);
    }
    
    .main-middle {
    	height: 600px;
    	margin-left: 200px;
    	margin-right: 200px;
    	background: rgba(0, 0, 0, 0.2);
    }
    
    .main-left {
    	width: 200px;
    	height: 600px;
    	margin-left: -100%;
    }
    
    .main-right {
    	width: 200px;
    	height: 600px;
    	margin-left: -200px;
    }
    </style>
    </head>
    
    <body>
    	<div class="bg">
    		<div class="header">头部</div>
    		<div class="main-middle-wrap"">
    			<div class="main-middle">主要</div>
    		</div>
    		<div class="main-left">左栏</div>
    		<div class="main-right">右栏</div>
    		<div  class="footer">页脚</div>
    	</div>
    </body>
    </html>
    

    五、【自适应布局】还未学到css3,尽情期待…

    六、【响应式布局】还未学到css3,尽情期待…

    七、【弹性布局】还未学到css3,尽情期待…

    八、【瀑布流布局】还未学到css3,尽情期待…

    九、【栅格布局】还未学到css3、框架,尽情期待…

    展开全文
  • 刚入门Flutter的时候,不知道你是否也被无穷无尽的build所困扰,或者莫名其妙就发现列表突然卡顿了, 我们的feed使用的是双列瀑布流,[一个比较有名的第三方库]...这篇文章总结了一些基本的UI渲染与列表绘制原理,希望从...

    刚入门Flutter的时候,不知道你是否也被无穷无尽的build所困扰,或者莫名其妙就发现列表突然卡顿了, 我们的feed使用的是双列瀑布流,[一个比较有名的第三方库]https://github.com/letsar/flutter_staggered_grid_view),但使用过程中发现滑动卡顿,
    这篇文章总结了一些基本的UI渲染与列表绘制原理,希望从原理到代码帮助大家能理清一些思路😸

    UI绘制逻辑

    举个最简单的例子:

     

    
    class MyApp extends StatelessWidget  {
    
     @override
     Widget build(BuildContext context)  {
     return Text("MyApp");
    
    }
    
    

    众所周知,flutter里的UI有三棵树,分别为widget树,element树和renderObject树。

    其中element树连接widget&renderObject。

    当我们的页面第一次渲染时,整个app会有一个rootElement(这个是啥可以不管,就理解成最顶层flutter会帮你创建一个element, 绑定一个rootWidget), 然后它会调用build方法:(flutter里的build走的都是rebuild, 不要问我为什么)

    这里build就会进入MyApp.build, 创建出Text("MyApp")

     

    @override
    void performRebuild() {
        built = build();
       _child = updateChild(_child, built, slot);
    }
    
    
    • 对于statelesselement, build即执行widget.build

    • 对于statefulelement, build执行state.build

    build完后会进入updateChild(非常关键的方法)

    updateChild

    我们先看下这个element.updateChild的逻辑:

    • 当newWidget(即这里的Text("MyApp")不为空时),因为child没有创建过,所以child为空,因此进入inflateWidget(newWidget, newSlot)流程 (可以先不看中间那一大段逻辑,直接看最后一句)

     

    Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
      if (newWidget == null) {
        if (child != null)
          deactivateChild(child);
        return null;
      }
      if (child != null) {
        if (child.widget == newWidget) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
          return child;
        }
        if (Widget.canUpdate(child.widget, newWidget)) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
          child.update(newWidget);
          return child;
        }
        deactivateChild(child);
      }
      return inflateWidget(newWidget, newSlot);
    }
    

    这里inflateWidget就可以简单理解成开始创建element并挂载

     

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
      ....
      final Element newChild = newWidget.createElement();
      
      newChild.mount(this, newSlot);
    
      return newChild;
    }
    

    在inflateWidget中, widget创建对应的element, 然后这个element会挂载到this=parent上去。即关联子element与父element, 同时调用child element的performRebuild,可以看到又回到了最前面。开始会去构建Text的child(当然例子里没加)

    所以,整棵树就是以这样的逻辑完成初次构建的!

    SetState

    _element.markNeedsBuild();

    会把自己加入到​_dirtyElements​中,当下一次刷新时会调用​buildScope​

     

    void buildScope(Element context, [ VoidCallback callback ]) {
        _dirtyElements[index].rebuild();
    }
    
    @override
    void performRebuild() {
        built = build();
       _child = updateChild(_child, built, slot);
    }
    

    可以看到就走到了刚才的​updateChild​,那么我们再来看刚才中间没看的那段逻辑:

     

    if (child != null) {
        if (child.widget == newWidget) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
          return child;
        }
        if (Widget.canUpdate(child.widget, newWidget)) {
          if (child.slot != newSlot)
            updateSlotForChild(child, newSlot);
          child.update(newWidget);
          return child;
        }
        deactivateChild(child);
      }
    
    
    • 如果新构建出来的widget和之前是一样的,那不用重新构建element, 可能只需要移动一下位置而已。(updateSlot)

    • 如果不一样,但是但是可更新,那需要调一下​child.update(newWidget)​;即要更新它的子节点

    • 如果都不能更新,那又要创新创建element和renderObject了。而我们知道,走到这一步会很耗时,是我们不希望看到的结果。

    那么什么是可更新呢?

     

    static bool canUpdate(Widget oldWidget, Widget newWidget) {
      return oldWidget.runtimeType == newWidget.runtimeType
          && oldWidget.key == newWidget.key;
    }
    

    即需要类是同一个类,key是同一个key。 类是同一个类好办,key是啥?

    Key

    前面创建element时(inflateWidget)少说了一些代码:

     

    Element inflateWidget(Widget newWidget, dynamic newSlot) {
      final Key key = newWidget.key;
      if (key is GlobalKey) {
        final Element newChild = _retakeInactiveElement(key, newWidget);
        if (newChild != null) {
          newChild._activateWithParent(this, newSlot);
          final Element updatedChild = updateChild(newChild, newWidget, newSlot);
          return updatedChild;
        }
      }
      final Element newChild = newWidget.createElement();
      newChild.mount(this, newSlot);
      return newChild;
    }
    

    这里看到,判断了如果key is GlobalKey, 会进行_retakeInactiveElement的操作,就会从以前element中找对应的key的element, element进行复用防止重建

    对于key存在两类:

    • GlobalKey

    GlobalKey会记录到一个全局的map中,从globalkey可以拿到element/state/widegt,因此可以用它调用某widget中的方法

    static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};

    • LocalKey

    考虑某种场景,某column下有两个widget, 点击某按钮后要交换这两个widget,如果没有key则交换会失败。原理可见https://juejin.im/post/5ca2152f6fb9a05e1a7a9a26#heading-4中的​StatefulContainer 比较过程。​

    优先用localKey, 要跨widget访问时再用globalkey。

    可优化的地方

    既然从updateChild中知道只要widget和过去element持有的widget一样,那就不会重新创建element,也就不会耗时。因此很多页面可以将第一次构建出来的widget缓存下来

    此时就回想到以前代码里为了解决build卡顿,做了很多如下操作(对于statefulWidget)

     

    Widget _child;
    @override
    Widget build(BuildContext context) {
        if(_child == null){
            _child = xxx
        }
        return _child;
    }
    

    ...那么,这种代码如果setState是不会起到作用的,updateChild中会走到:

     

    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }
    

    完全没更新直接返回了。。

    flutter中的列表

    ListView 组件的子元素都是按需加载的。换句话说,只有在可视区域的元素才会被初始化

    ViewPort

    ViewPort可以理解为可见视图,列表就是绘制在这片视图上的。

     

    child.layout(SliverConstraints(
      axisDirection: axisDirection,
      growthDirection: growthDirection,
      userScrollDirection: adjustedUserScrollDirection,
      scrollOffset: sliverScrollOffset,
      precedingScrollExtent: precedingScrollExtent,
      overlap: maxPaintOffset - layoutOffset,
      remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
      crossAxisExtent: crossAxisExtent,
      crossAxisDirection: crossAxisDirection,
      viewportMainAxisExtent: mainAxisExtent,
      remainingCacheExtent: math.max(0.0, remainingCacheExtent + cacheExtentCorrection),
      cacheOrigin: correctedCacheOrigin,
    ), parentUsesSize: true);
    
    • remainingPaintExtent表示在Viewport中剩余绘制区域大小

    • scrollOffset: 相比于ViewPort向上滑出的距离, 往上滑动为正

    • remainingCacheExtent: 可以绘制的cache剩余大小

    • cacheOrigin: math.max(cacheOrigin, -sliverScrollOffset) 这个比较优异,cacheOrigin默认为-250, 如果scrolloffset是100, 那么cacheOrigin = -100;如果scrolloffset是-300,那么cacheOrigin就是-250,后面会看到为什么要这么设计

    RenderSliverList

    1.png

    在flutter中,Sliver可以看成是一个可滑动组件中的child。即一个可滑动组件是由多个child组成的。

    PerformLayout

    2.png

    这张图实线框框是你的屏幕,虚线框框是目前列表滑到的距离。

    几个变量解释:

    • Fake scrollOffset: 目前这个sliver已经滚动的距离。

    • cacheOrigin 给这个列表提供的缓存

    • True scrollOffset = fake scrolloffset + cacheorigin 即整个列表会从这个true scrolloffset开始绘制

    • remainingCacheExtent 剩下可以绘制的部分(包含cache)

    • targetEndScrollOffset = scrollOffset + remainingExtent 即整个列表绘制到这个targetEndScrollView

    注意后面说的scrollOffset均为trueScrollOffset

    Ps: 这里的每个变量都是可以和前面ViewPort中child.layout里的变量对应起来的。所以看到这里如果scrolloffset是300, 那之前说过传入的cache为-250,因此从50开始绘制;如果offset是-200, 那么cache就是-200,就会从0开始绘制。合理~

    step1

    找到目前整个列表中存在的第一个child(前面有些child可能已经被回收了即不存在了),计算它是否大于scrollOffset, 如果大于说明中间有child应该被layout但是这个child却不存在, 因此需要创建并插入到父节点中。

     

    for (double earliestScrollOffset = childScrollOffset(earliestUsefulChild);
        earliestScrollOffset > scrollOffset;
        earliestScrollOffset = childScrollOffset(earliestUsefulChild)) {
        earliestUsefulChild = insertAndLayoutLeadingChild(childConstraints, parentUsesSize: true);
    }
    

    step2

    如果endScrollOffset(当前最后一个child)在scrollOffset之前,说明这部分child是需要被回收掉的

     

    while (endScrollOffset < scrollOffset) {
      leadingGarbage += 1;
      if (!advance()) {
      collectGarbage(leadingGarbage - 1, 0);
      final double extent = childScrollOffset(lastChild) + paintExtentOf(lastChild);
      geometry = SliverGeometry(
        scrollExtent: extent,
        paintExtent: 0.0,
        maxPaintExtent: extent,
      );
      return;
    }
    

    step3

    从scrollOffset到endScrollOffset 遍历child, 如果存在就layout, 不存在就create&insert

     

    while (endScrollOffset < targetEndScrollOffset) {
      if (!advance()) { //advance里就是看当前child存不存在,存在layout,不存在就create
        reachedEnd = true;
        break;
      }
    }
    

    step4

    在targetEndScrollOffset后面的child也需要回收掉

     

    // Finally count up all the remaining children and label them as garbage.
    if (child != null) {
      child = childAfter(child);
      while (child != null) {
        trailingGarbage += 1;
        print("trailingGarbage:${trailingGarbage}");
        child = childAfter(child);
      }
    }
    

    step5

    返回geometry,这个geometry就是list告诉viewport自己layout后的结果:

    • scrollExtent: 总的List可滚动距离,后面会说如果计算

    • paintExtent: 自己绘制了多少【可见】大小

    • cacheExtent: 自己占了多少cache大小。

     

    geometry = SliverGeometry(
      scrollExtent: estimatedMaxScrollOffset,
      paintExtent: paintExtent,
      cacheExtent: cacheExtent,
      maxPaintExtent: estimatedMaxScrollOffset,
      // Conservative to avoid flickering away the clip during scroll.
      hasVisualOverflow: endScrollOffset > targetEndScrollOffsetForPaint || constraints.scrollOffset > 0.0,
    );
    

    CollectGarbage

    即调用 _childManager.removeChild(child);

     

    if (childParentData.keepAlive) { //keepalive后面说
      remove(child);
      _keepAliveBucket[childParentData.index] = child;
      child.parentData = childParentData;
      super.adoptChild(child);
      childParentData._keptAlive = true;
    } else {
      _childManager.removeChild(child);
    }
    

    estimatedMaxScrollOffset

    list会预估最大可滚动距离,根据目前已经渲染的child的平均高度

     

    if (reachedEnd) {
      estimatedMaxScrollOffset = endScrollOffset;
    } else {
      estimatedMaxScrollOffset = childManager.estimateMaxScrollOffset(
        constraints,
        firstIndex: indexOf(firstChild),
        lastIndex: indexOf(lastChild),
        leadingScrollOffset: childScrollOffset(firstChild),
        trailingScrollOffset: endScrollOffset,
      );
      assert(estimatedMaxScrollOffset >= endScrollOffset - childScrollOffset(firstChild));
    }
    

    shrinkWrap

    什么是shrinkWrap

    https://stackoverflow.com/questions/54007073/what-does-the-shrink-wrap-property-do-in-flutter

     

    return Column(
      children: <Widget>[
        ListView(
          children: <Widget>[
            Container(color: Colors.red, child: Text("1")),
            Container(color: Colors.orange, child: Text("2")),
          ],
        ),
      ],
    );
    

    开发业务的时候发现这么写是会报错的,因为column的大小是需要靠listview撑起来的,而listview本身是会撑满整个父view的:

     

    size = constraints.biggest;
    // We ignore the return value of applyViewportDimension below because we are
    // going to go through performLayout next regardless.
    switch (axis) {
      case Axis.vertical:
        offset.applyViewportDimension(size.height);
        break;
      case Axis.horizontal:
        offset.applyViewportDimension(size.width);
        break;
    }
    

    因此两者就死锁了。。。flutter遇到这种情况会直接报错,并提供shrinkWrap这个组件,此时viewport的大小将不是由它的父亲而决定,而是由它自己决定。此时它的mainAxisExtent会变成infinate,即会完整的进行排布。

    Item keepAlive

    这是一个有点鸡肋的变量,如果你列表里的item mixin AutomaticKeepAliveClientMixin 同时keepalive返回true, 那么你列表里所有的Item都不会被回收。。。那么显而易见内存可能会飙升,仅适合与列表内容不多又想改善点性能的情况。

    我们回顾下创建和销毁child:

    销毁:

     

    if (childParentData.keepAlive) {
      assert(!childParentData._keptAlive);
      remove(child);
      _keepAliveBucket[childParentData.index] = child;
      child.parentData = childParentData;
      super.adoptChild(child);
      childParentData._keptAlive = true;
    } else {
      assert(child.parent == this);
      _childManager.removeChild(child);
      assert(child.parent == null);
    }
    

    可以看到如果keepalive为true, 会放入一个_keepAliveBucket的篮子中。

    创建:

     

    if (_keepAliveBucket.containsKey(index)) {
      final RenderBox child = _keepAliveBucket.remove(index);
      final SliverMultiBoxAdaptorParentData childParentData = child.parentData;
      assert(childParentData._keptAlive);
      dropChild(child);
      child.parentData = childParentData;
      insert(child, after: after);
      childParentData._keptAlive = false;
    } else {
      _childManager.createChild(index, after: after);
    }
    

    会从_keepAliveBucket这个篮子中去寻找当前index下的child, 以免重建。

    SliverChildDelegate

    当你创建一个Listview时,是可以传入一个叫delegate的值的

     

    const ListView.custom({
        @required this.childrenDelegate,
    })
    

    后面,列表每次build一个元素时,都会走widget.delegate.build

     

    Widget _build(int index) {
      return _childWidgets.putIfAbsent(index, () => widget.delegate.build(this, index));
    }
    

    以及layout结束时,会调用​widget.delegate.didFinishLayout(firstIndex, lastIndex);​传入此次绘制的第一个Index&最后一个index

     

    @override
    void didFinishLayout() {
      assert(debugAssertChildListLocked());
      final int firstIndex = _childElements.firstKey() ?? 0;
      final int lastIndex = _childElements.lastKey() ?? 0;
      widget.delegate.didFinishLayout(firstIndex, lastIndex);
    }
    

    这里其实就暴露接口给业务,让业务知道绘制情况。

    list的keepalive

    在build的child外面套了一层AutomaticKeepAlive,这里一开始可能会和前面的item keepalive混淆。

    具体可以查看https://www.jianshu.com/p/db7ed17a4273

    总结下,只有item mixin了AutomaticKeepAliveClientMixin, 上级中有人实现了AutomaticKeepAlive才可以

    滚动实现

    稍微总结下,在Sliver布局中,SliverConstraints(约束)和输出SliverGeometry 为输入和输出,而手势监听和滚动距离的计算则是靠Scrollable& ScrollController完成的,这两者会进行down&move&up手势的处理,具体不再具体分析了。代码如下:

     

    @override
    Widget build(BuildContext context) {
      final List<Widget> slivers = buildSlivers(context);
      final AxisDirection axisDirection = getDirection(context);
    
      final ScrollController scrollController = primary
        ? PrimaryScrollController.of(context)
        : controller;
      final Scrollable scrollable = Scrollable(
        dragStartBehavior: dragStartBehavior,
        axisDirection: axisDirection,
        controller: scrollController,
        physics: physics,
        semanticChildCount: semanticChildCount,
        viewportBuilder: (BuildContext context, ViewportOffset offset) {
           //我们的列表布局被通过buildViewport嵌套在了ViewPort或者ShrinkWrappingViewport中
          return buildViewport(context, offset, axisDirection, slivers);
        },
      );
    

    双列瀑布流

    为什么我们用的双列瀑布流会卡?只滑动0.1mm都感到滑动, 看过这个第三方库就会知道以下几方面导致:

    1. 不断创建新的child

     

    for (var index = viewportOffset.firstChildIndex;
        mainAxisOffsets.any((o) => o <= targetEndScrollOffset);
        index++) {
        addAndLayoutChild(index, geometry.getBoxConstraints(constraints));
    }
    
    _createOrObtainChild(index);
    
    _childManager.createChild(index);
    

    在布局时,它会计算当前可见+缓存总的区域,在这片区域里进行addAndLayoutChild操作 => _createOrObtainChild中不管child存不存在都会进行_childManager.createChild(index);导致重新build

    1. 不断回收老的child

    indices.toSet().difference(visibleIndices).forEach(_destroyOrCacheChild);

    在每次layout时和listview一致会进行child的回收,但是回收策略比较无语,会比较当前所有存在的child和【屏幕可见】的child,如果不是屏幕可见则会进行destroy回收。

    这两点使得每次稍微滚动就会不断回收不断build, 最终导致卡顿。

    解决方法也很简单

    1. 在create中判断当前child是否存在,如果存在则不创建

    2. 销毁时对于指定缓存区域中的不销毁。

    这两点能导致fps的提升,当然还不能达到非常顺畅的fps, 原因为loadmore时会重新setState导致重新build, 这一点需要研究新的方案(其实就是我还没研究出来)

    技巧

    • print&debug大法好

    相比于android&ios, flutter的framework源码会直接缓存在本地,因此可以自由的在源码里面加print与调试。(调试完记得代码改回去不然运行可能会出现未知异常)

    • 善用flutter performance(仅在as里尝试,vscode自行判断)

    debug连接时勾上Track widget rebuilds 右边就能实时看到哪个widget build了, 同时你的dart代码里对应的widget左边会转黄色的圈(无缘无故build是真的烦), 注意打断点时右边会显示不出来(不知道为啥)

    推荐资料

    https://juejin.im/post/5caec613f265da03a00fbcde#heading-2

    https://zhuanlan.zhihu.com/p/52723705

    https://juejin.im/post/5ca2152f6fb9a05e1a7a9a26#heading-10



    作者:LSteven
    链接:https://www.jianshu.com/p/99084a3d1abe
    来源:简书
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

    展开全文
  • 一个强大的粘性标签库

    千次阅读 2018-11-09 19:07:10
    RecyclerView粘性标签库 (请关注新写的一个更优秀的方案...大粘性标签支持垂直方向的线性、网格、瀑布流布局管理器 小粘性标签支持垂直方向的线性和网格一行只有一网格布局管理器 支持标签的单击、...

    RecyclerView粘性标签库

    请关注新写的一个更优秀的方案StickyItemDecoration

    一个强大的粘性标签库,实现思路来源于「pinned-section-item-decoration」,感觉有用的话star个呗(^∀^)

    功能

    • 大粘性标签支持垂直方向的线性、网格、瀑布流布局管理器
    • 小粘性标签支持垂直方向的线性和网格一行只有一列网格布局管理器
    • 支持标签的单击、双击和长按事件
    • 支持标签内部子控件的单击、双击和长按事件
    • 可以绘制线性、网格、瀑布流布局的分隔线,支持自定义分割线样式(PS:垂直瀑布流布局需要Item高度固定,不能随机变化导致Item位置切换

    扩展库

    BaseRecyclerViewAdapterHelper(强烈推荐使用此适配器,可大大减少工作量。当前demo使用的是v2.9.42。)

    它能做什么?

    首先在dependencies添加

    如果使用AndroidX库请依赖
    compile 'com.oushangfeng:PinnedSectionItemDecoration:1.3.2-androidx'
    否则
    compile 'com.oushangfeng:PinnedSectionItemDecoration:1.3.2'

    Adapter记得要实现对网格布局和瀑布流布局的标签占满一行的处理,调用FullSpanUtil工具类进行处理

        @Override
        public void onAttachedToRecyclerView(RecyclerView recyclerView) {
            super.onAttachedToRecyclerView(recyclerView);
            FullSpanUtil.onAttachedToRecyclerView(recyclerView, this, StockEntity.StockInfo.TYPE_HEADER);
        }
    
        @Override
        public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
            super.onViewAttachedToWindow(holder);
            FullSpanUtil.onViewAttachedToWindow(holder, this, StockEntity.StockInfo.TYPE_HEADER);
        }
    

    实现大粘性标签RecyclerView只需要添加一个PinnedHeaderItemDecoration,由于参数太多,现在只支持使用创建者模式创建,注意大标签所在的最外层布局不能设置marginTop,暂时没想到方法解决 往上滚动遮不住真正的标签「供参考的StockActivity

          OnHeaderClickAdapter clickAdapter = new OnHeaderClickAdapter() {
    
              @Override
              public void onHeaderClick(View view, int id, int position) {
                  switch (id) {
                      case R.id.fl:
                           // case OnItemTouchListener.HEADER_ID:
                           Toast.makeText(StockActivity.this, "click, tag: " + mAdapter.getData().get(position).pinnedHeaderName, Toast.LENGTH_SHORT).show();
                           break;
                       case R.id.iv_more:
                           Toast.makeText(StockActivity.this, "click " + mAdapter.getData().get(position).pinnedHeaderName + "'s more button", Toast.LENGTH_SHORT)
                                 .show();
                           break;
                       case R.id.checkbox:
                           final CheckBox checkBox = (CheckBox) view;
                           checkBox.setChecked(!checkBox.isChecked());
                           // 刷新ItemDecorations,导致重绘刷新头部
                           mRecyclerView.invalidateItemDecorations();
    
                           mAdapter.getData().get(position).check = checkBox.isChecked();
                           mAdapter.notifyItemChanged(position + mHeaderItemDecoration.getDataPositionOffset());
    
                           break;
                   }
               }
    
           };
    
         mRecyclerView.addItemDecoration(
                 // 设置粘性标签对应的类型
                 new PinnedHeaderItemDecoration.Builder(StockEntity.StockInfo.TYPE_HEADER)
                 // 设置分隔线资源ID
                 .setDividerId(R.drawable.divider)
                 // 开启绘制分隔线,默认关闭
                 .enableDivider(true)
                 // 通过传入包括标签和其内部的子控件的ID设置其对应的点击事件
                 .setClickIds(R.id.iv_more)
                 // 是否关闭标签点击事件,默认开启
                 .disableHeaderClick(false)
                 // 设置标签和其内部的子控件的监听,若设置点击监听不为null,但是disableHeaderClick(true)的话,还是不会响应点击事件
                 .setHeaderClickListener(clickAdapter)
                 .create());
        
    

    大标签布局

    实现小粘性标签稍微复杂点,比如这个是数据的布局A

    <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
                 xmlns:tools="http://schemas.android.com/tools"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:background="#70E593">
    
        <ImageView
            android:id="@+id/iv_animal"
            android:layout_gravity="center"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            tools:src="@mipmap/panda0"/>
    
        <TextView
            android:id="@+id/tv_pos"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:padding="8dp"
            android:textColor="#000000"
            android:textSize="18dp"
            tools:text="1"/>
    
    </FrameLayout>
    

    布局A

    这个是带有小标签的布局B

    <FrameLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
    
        <ImageView
            android:id="@+id/iv_animal"
            android:layout_width="match_parent"
            android:layout_height="120dp"
            android:background="#70E593"
            tools:src="@mipmap/panda0"/>
    
        <ImageView
            android:id="@+id/iv_small_pinned_header"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:background="#5A5A5A"
            android:padding="8dp"
            android:textColor="#ffffff"
            android:textSize="18dp"
            tools:src="@mipmap/panda0"
            tools:text="熊猫"/>
    
    </FrameLayout>
    

    布局B

    布局B就相当于在原来A的基础上放上个小标签,然后实现小粘性标签RecyclerView只需要添加一个SmallPinnedHeaderItemDecoration,只支持使用创建者模式创建,注意标签不能设置marginTop, 因为往上滚动遮不住真正的标签「供参考的SecondActivity

         OnHeaderClickAdapter headerClickAdapter = new OnHeaderClickAdapter() {
    
              @Override
              public void onHeaderClick(View view, int id, int position) {
                  if (id == R.id.iv_small_pinned_header) {
                      Toast.makeText(SecondActivity.this, "click tag: " + mAdapter.getData().get(position).getPinnedHeaderName(), Toast.LENGTH_SHORT).show();
                  }
              }
         };
         mRecyclerView.addItemDecoration(
                 // 构造方法需要传入小标签的ID和粘性标签对应的类型
                 new SmallPinnedHeaderItemDecoration.Builder(R.id.tv_small_pinned_header,BaseHeaderAdapter.TYPE_HEADER)
                 // 开启绘制分隔线,默认关闭
                 .enableDivider(true)
                 // 设置分隔线资源ID
                 .setDividerId(R.drawable.divider)
                 // 通过传入包括标签和其内部的子控件的ID设置其对应的点击事件
                 .setClickIds(R.id.iv_small_pinned_header)
                 // 是否关闭标签点击事件,默认开启
                 .disableHeaderClick(true)
                 // 设置标签和其内部的子控件的监听,若设置点击监听不为null,但是disableHeaderClick(true)的话,还是不会响应点击事件
                 .setHeaderClickListener(clickAdapter)
                 .create());
        
    

    开源地址:https://github.com/oubowu/PinnedSectionItemDecoration/blob/master/README-CN.md

     

    展开全文
  • 并且公司一直是使用的平板,单列时间轴已不足以胜任,故一直使用双列的时间轴TimeLine,这是一个借助2列的瀑布流布局的RecyclerView完成的时间轴,已满足我司日常的需求,不过,使用瀑布流带来的烦恼是: ...

    前言

    因为公司业务经常会涉及到流程,所以使用类似时间轴的控件是不可避免。并且公司一直是使用的平板,单列时间轴已不足以胜任,故一直使用双列的时间轴TimeLine,这是一个借助2列的瀑布流布局的RecyclerView完成的时间轴,已满足我司日常的需求,不过,使用瀑布流带来的烦恼是:

    • 位置不好控制:时间轴的每个点的距离不能够均匀分布(不过这也是瀑布流的优点,充分利用空间)
    • 选择单一:如果你的项目既有单列时间轴,又想集成双列时间轴,这种情况下就需要集成两个第三方库
    • 样式单一:多数时间轴库的样式选择过于单一,不能够自己定制

    为了解决以上的痛点,我提供了一种仍基于RecyclerView的新解决方案,该方案已被我集成在Orient-Ui中,以下是我基于此方案自定义实现的几个例子:
    在这里插入图片描述
    在这里插入图片描述

    一、介绍

    为什么说Orient-Ui中的TimeLine灵活呢?因为该方案具有如下特点:

    • 主体样式包括单列时间轴双列时间轴:单列时间轴使用的RecyclerView中的LinearLayoutManager(线性布局),双列时间轴则使用的Orient-Ui中的DoubleSideLayoutManager(两侧布局)
    • 内容样式可控性强:可以轻松控制时间轴的点绘制方式、时间轴中的标题位置和内容、以及时间轴中的时间线等
    • 样式高度的自定义:需要自己在给定的区域内绘制时间标题时间点,这也是该方案灵活的原因

    上述的特点的描述可能不是特别清晰,我们借助两张图加以说明:
    在这里插入图片描述
    左右两边分别代表着双列时间轴单列时间轴,其中dot area和title area是需要实现绘制的区域,其他的例如标题放在RecyclerView子视图的上侧还是还是左侧、标题的颜色、时间点采用的资源文件绘制还是自绘制、时间线的风格及占用大小等需要创建的时候进行设置。

    二、使用

    我会以上述图片为例,分别介绍单例时间轴双列时间轴。不过在此之前,你需要在build.gradle添加依赖:

    implementation 'com.orient:Orient-Ui:1.0.0'
    

    复制代码
    1. 单列时间轴
    第一步 继承SingleTimeLineDecoration

    我们需要实现抽象方法SingleTimeLineDecoration#onDrawTitleItem,以及针对当前时间点的风格选择复写合适的方法,如果时间点采用的是FLAG_DOT_DRAW(绘制风格),需要复写SingleTimeLineDecoration#onDrawDotItem方法,如果时间点采用的是FLAG_DOT_RES(绘制资源文件),则需要复写SingleTimeLineDecoration#onDrawDotResItem方法:

    public class StepSTL extends SingleTimeLineDecoration {

    private Paint mRectPaint;
    
    public StepSTL(SingleTimeLineDecoration.Config config) {
        super(config);
    
        mRectPaint = new Paint();
        mRectPaint.setMaskFilter(new BlurMaskFilter(10, BlurMaskFilter.Blur.SOLID));
        mDotPaint.setMaskFilter(new BlurMaskFilter(6, BlurMaskFilter.Blur.SOLID));
    }
    
    @Override
    protected void onDrawTitleItem(Canvas canvas, int left, int top, int right, int bottom, int pos) {
        ITimeItem item = timeItems.get(pos);
    
        int rectWidth = UIUtils.dip2px(120);
        int height = bottom - top;
        int paddingLeft = UIUtils.dip2px(10);
        mRectPaint.setColor(item.getColor());
        canvas.drawRoundRect(left+paddingLeft,top,left+rectWidth,bottom,UIUtils.dip2px(6),UIUtils.dip2px(6),mRectPaint);
    
    
        String title = item.getTitle();
        if(TextUtils.isEmpty(title))
            return;
        Rect mRect = new Rect();
    
        mTextPaint.getTextBounds(title,0,title.length(),mRect);
        int x = left + (rectWidth - mRect.width())/2;
        //int x = left + UIUtils.dip2px(20);
        int y = bottom - (height - mRect.height())/2;
        canvas.drawText(title,x,y,mTextPaint);
    }
    
    @Override
    protected void onDrawDotItem(Canvas canvas, int cx, int cy, int radius, int pos) {
        ITimeItem item = timeItems.get(pos);
        mDotPaint.setColor(item.getColor());
        canvas.drawCircle(cx,cy,UIUtils.dip2px(6),mDotPaint);
    }
    

    }

    第二步 创建数据

    创建数据类型的时候需要实现ITimeItem接口:

    public class TimeItem implements ITimeItem {
    	private String name;
    	private String title;
    	private String detail;
    	private int color;
    	private int res;
    	// 构造方法省略
    	// ... 省略get 和 set 方法
      	// 得到当前的标题
    	@Override
    	public String getTitle() {
    		return title;
    	}
      	// 时间点的颜色
    	@Override
    	public int getColor() {
    		return color;
    	}
      	// 时间点的资源文件
      	// 如果采用的是绘制资源文件
    	@Override
    	public int getResource() {
    		return res;
    	}
    	// ... 
    }
    

    第三步 初始化RecyclerView

    构建RecyclerView对象,并设置布局方式为LinearLayoutManager以及初始化适配器,这是使用RecyclerView的必备知识,相信你早就烂熟于心了。

    第四步 创建时间轴

    // 初始化数据 
    // 仅需要知道该行代码作用即可
    List<TimeItem> timeItems = TimeItem.initStepInfo();
    // 适配器更新数据 
    // 适配器已经被我封装过
    mAdapter.addAllData(timeItems);
    // 构建时间轴
    TimeLine decoration = new SingleTimeLineDecoration.Builder(getContext(), timeItems) // 添加数据
    				// 标题颜色 和 标题文本的大小为20sp
                    .setTitle(Color.parseColor("#ffffff"), 20)	
    				// 标题位置设置为子视图上边,所占高度为40dp
                    .setTitleStyle(SingleTimeLineDecoration.FLAG_TITLE_TYPE_TOP, 40) 
    				// 时间线风格 绘制时间线区域的宽度(非时间线宽度)为50dp
                    .setLine(SingleTimeLineDecoration.FLAG_LINE_DIVIDE, 50, Color.parseColor("#8d9ca9")) 
    				// 时间点的样式 此处为自绘制
                    .setDot(SingleTimeLineDecoration.FLAG_DOT_DRAW)
    				// 相同的标题隐藏
                    .setSameTitleHide()
    				// 设置实现的时间轴
                    .build(StepSTL.class);
    mRecyclerView.addItemDecoration(decoration);
    

    这步完成之后就可以正常显示了,效果就是上述的活动步骤的那张图:
    在这里插入图片描述
    第五步 更新、删除、添加

    如果需要对数据进行增删查改,那么在进行这些操作之后,你需要对TimeLine中的数据进行更新操作,它同样支持增删改。

    函数 解释
    addItems 增加数据
    replace 替换数据
    remove 清除数据
    2. 双列时间轴
    双列时间轴与单列时间轴的区别还是很大的,我会一一说明。

    第一步 继承DoubleTimeLineDecoration

    这里你需要注意的是当前绘制标题是在两列布局中的左列还是右列,需要处理的方法基本同SingleTimeLineDecoration,不过处理的方法来自DoubleTimeLineDecoration。

    第二步 创建数据

    同SingleTimeLineDecoration。

    第三步 初始化RecyclerView

    这里再用线性布局显然是行不通的,需要改为我写的DoubleSideLayoutManager(两侧布局):

    mRecyclerView.setLayoutManager(new DoubleSideLayoutManager(DoubleSideLayoutManager.START_LEFT));
    

    其他同SingleTimeLineDecoration。

    第四步 创建时间轴

    设置方法同SingleTimeLineDecoration。不过需要注意的是两列时间轴不支持:

    • 将标题放置在子视图上侧
    • 相同标题隐藏
    • FLAG_LINE_DIVIDE风格的时间线

    第五步 更新、删除、添加

    同SingleTimeLineDecoration。

    以上就是单列时间轴和双列时间轴的配置了。

    三、更多信息

    经过上面了解,我相信你已经认识到,TimeLine能做的不仅仅是一个时间轴,与流程、步骤相关的它一样可以胜任。不过需要指出的是,TimeLine做的只是为你提供时间轴的布局逻辑,具体时间点和标题的绘制仍需你自己实现,更多的绘制方案以及配置信息还需查看项目地址: github.com/mCyp/Orient…

    查看更多内容:关注安卓巴士

    展开全文
  • 页面布局的方式——前端

    千次阅读 2019-05-31 08:24:20
    文章目录页面布局的方式——前端页面布局的方式飞翼布局多栏布局弹性布局(Flexbox)瀑布流布局流式布局(Fluid)响应式布局注 页面布局的方式 页面布局的方式主要有:飞翼、多栏、弹性、流式、瀑布流、响应式...
  • 页面布局的方式有哪些?

    千次阅读 2019-05-23 19:30:54
    方式:飞翼、多栏、弹性、流式、瀑布流、响应式布局 (1)、飞翼布局 经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝UED的工程师...
  • 方式:飞翼、多栏、弹性、流式、瀑布流、响应式布局 (1)、飞翼布局 经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝UED的工程师...
  • 页面布局那些事

    2017-12-14 00:00:00
    方式:飞翼、多栏、弹性、流式、瀑布流、响应式布局 (1)、飞翼布局 经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝UED的工程师...
  • 页面布局的方式

    2018-12-26 19:40:42
    页面布局的方式:飞翼、多栏、弹性、流式、瀑布流、响应式布局 飞翼布局  经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝...
  • css的6大布局方式

    千次阅读 2017-04-18 20:17:16
    总共有6大布局方式飞翼、多栏、弹性、流式、瀑布流、响应式布局 (1)、飞翼布局  经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由...
  • 飞翼、多栏、弹性、流式、瀑布流、响应式布局 飞翼布局:经典三布局,也叫做圣杯布局 多栏布局: 栏栅格系统:就是利用浮动实现的多栏布局,在bootstrap中用的非常多。 多布局:栅格系统并没有真正实现分栏...
  • 飞翼、多栏、弹性、流式、瀑布流、响应式布局 (1)、飞翼布局 经典三布局,也叫做圣杯布局【Holy Grail of Layouts】是Kevin Cornell在2006年提出的一个布局模型概念,在国内最早是由淘宝UED的工程师传播开来...
  • 作为国内的美女网红流量圣地,小红书一直以来都是广大宅男心中的那一片乐土。虽然前段时间出了一些不这么...)对,就是这种并不整齐的双列排布(瀑布流),已经成为了众多App的主推排版方式。这一篇文章,我们就要用F...
  • 目录 一、负边距与浮动布局 1.1、负边距 ...1.1.1、向上移动1.1.2、去除列表右边框1.1.3、负边距+定位,实现水平垂直居中1.1.4、...二、弹性布局(Flexbox)三、流式布局(Fluid)三、瀑布流布局 3.1、常见瀑
  • 线性代数:理解矩阵的作用

    千次阅读 2018-03-13 18:02:20
    现在我们正式进入矩阵的学习了,矩阵Matrix这个词我相信小伙伴们第一次接触,基本就是在小时候看黑客帝国的时候,黑客帝国中有个经典的场景,就是数字世界在解放后的尼奥双眼中已经变成了一串串数字瀑布流,就是下面...
  • 网格样式与瀑布流样式任意切换 内置 SEO 优化 自带与主题 UI 完美兼容搭配的 erphpdown 前端用户中心页面(此功能若单独找我们定制也需要几百) 收费付费下载资源、付费查看内容、付费观看视频、付费下载音乐,上传...
  • 技巧16 系列产生在“行”或“”有何不同 技巧17 招添加新数据系列 技巧18 招删除现有数据系列 技巧19 随手修改图表的源数据 技巧20 轻松搞定数据系列的名称 技巧21 改变数据系列的次序 技巧22 以多个区域的数据...

空空如也

空空如也

1 2
收藏数 32
精华内容 12
关键字:

双列瀑布流