搞前端的同窗可能都習慣了CSS局部的思惟,過去也出現過一些跟佈局或者樣式相關的標籤,例如:big, center, font, s, strike, tt, u;可是目前也被CSS所代替,已經不推薦使用。可是在Flutter裏面,是沒有CSS這樣一個概念的,佈局和樣式均可能會是一個組件或者是組件裏面的屬性所定義和實現的,對於習慣寫樣式的前端同窗可能須要適應一下。前端
如今可能要想一下,Flutter爲啥沒有像瀏覽器同樣抽離出CSS?
咱們知道在瀏覽器裏面JS,CSS,HTML各司其職:行爲,表現和結構,已經深刻人心,也被不少人所推崇。可是Flutter好像反其道而行之,樣式糅合在結構裏面,這樣究竟有啥意思尼?
首先應該是一個性能的考慮,瀏覽器解析CSS其實也是一個性能消耗點,沒有CSS解析天然也能夠加快頁面的顯示。
其次再討論一下CSS,CSS確實很是適合描述樣式和佈局,可是也有很明顯的缺點:做用域全局性,代碼冗餘,代碼難以重用,難以模塊化等;咱們修修補補,又創造了less,sass等工具幫助咱們去解決問題,可是自身的缺陷依然會存在,甚至有點鑽牛角尖,由於存在了CSS,因此只能改進CSS。
而在Flutter,沒有了CSS,以上的問題天然蕩然無存,那麼描述樣式會不會變得很麻煩?大道行之,咱們的前輩們早就在代碼上總結出不少設計模式或者技術去解決代碼重用,代碼冗餘,模塊化的問題,爲何咱們不去用已經存在好久並且行之有效的技術去解決問題尼。天然把樣式糅合進結構會增長信息量,對咱們閱讀代碼可能會是一個小小的挑戰,可是應該也會很快適應下來的,我相信。
咱們不少時候都在創造新的工具的解決問題,其實也有可能創造出新的問題,有時候迴歸根本,不必定是一件壞事。算法
主要控制文字方向canvas
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Text('我是一段文本') ); }
加入控件後設計模式
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Directionality( textDirection: TextDirection.rtl, child: new Text('我是一段文本') ) ); }
跟文本相關的還有一個DefaultTextStyle控件,提供了更多的控制選項:textAlign,softWrap,style和maxLines等,都是控制總體:換行,文字居中和多行省略等,相對style提供都是文字自身樣式相關:字重,字體大小等瀏覽器
const TextStyle({ this.inherit: true, this.color, this.fontSize, this.fontWeight, this.fontStyle, this.letterSpacing, this.wordSpacing, this.textBaseline, this.height, this.decoration, this.decorationColor, this.decorationStyle, this.debugLabel, String fontFamily, String package, })
演示一下效果:sass
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Directionality( textDirection: TextDirection.ltr, child: new DefaultTextStyle( style: new TextStyle( fontSize: 14.0, color: Colors.blue, decoration: TextDecoration.underline ), maxLines: 2, softWrap: true, overflow: TextOverflow.ellipsis, child: new Text('我是一段超長的文本啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦' '啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦' '啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦' '啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦') ) ) ); }
其實Text控件就已經帶上這些屬性:app
const Text(this.data, { Key key, this.style, this.textAlign, this.textDirection, this.softWrap, this.overflow, this.textScaleFactor, this.maxLines, })
爲何又要獨立出這些控件專門管理呢,咱們知道CSS屬性裏面有些屬性時繼承父元素的,例如:字體大小,顏色等;這樣的話,咱們很容易統一一個模塊裏面的樣式,並不須要每一個元素都要去設置一遍,這裏的這些控件也是起到這樣的功能,其實除了些字體樣式還有不少地方會有這種繼承關係,例如:主題顏色,語言文字等等。因此後面Text控件很容易從控件樹上找到這些父控件,獲取它們設置的屬性,就這樣就能夠把父控件的樣式繼承下來。
怎麼作到的呢,不管Directionality仍是DefaultTextStyle都是InheritedWidget的子類,InheritedWidget實現了一個發佈/訂閱的模式,當子控件調用inheritFromWidgetOfExactType方法獲取父控件時,同時也把本身加入到InheritedWidget的訂閱者列表裏面,因此當InheritedWidget屬性改變的時候,就會調起子組件didChangeDependencies方法去通知子組件。框架
這個控件感受必須得介紹一下,由於在前端咱們有一個canvas元素,能夠提供給咱們直接去繪製元素,給了咱們很大的靈活性,那麼Flutter中對應的應該就是這個控件了。
如何使用:
先繼承CustomPainterless
class CustomPainterSample extends CustomPainter { double progress; CustomPainterSample({this.progress: 0.0}); @override void paint(Canvas canvas, Size size) { Paint p = new Paint(); p.color = Colors.green; p.isAntiAlias = true; p.style = PaintingStyle.fill; canvas.drawCircle(size.center(const Offset(0.0, 0.0)), size.width / 2 * progress, p); } @override bool shouldRepaint(CustomPainter oldDelegate) { return true; } }
這裏我畫了一個綠色的圓,而後把這個CustomPainterSample傳到CustomPaint控件。ide
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new CustomPaint( painter: new CustomPainterSample(progress: this.progress), ) ); }
固然了,既然能夠隨便畫點東西,作點動畫也是妥妥的,好再加個放大的動畫,完整代碼:
class SquareFragmentState extends State<SquareFragment> with TickerProviderStateMixin { double progress = 0.0; @override void initState() { AnimationController ac = new AnimationController( vsync: this, duration: const Duration(milliseconds: 10000) ); ac.addListener(() { this.setState(() { this.progress = ac.value; }); }); ac.forward(); } @override Widget build(BuildContext context) { return new Container( color: Colors.white, child: new CustomPaint( painter: new CustomPainterSample(progress: this.progress), ) ); } }
這裏mixin了TickerProviderStateMixin,裏面有一個createTicker方法,主要是監聽每一幀生成而後回調,主要是由SchedulerBinding.instance.scheduleFrameCallback方法所驅動的。
剪切元素的邊界,這裏相似CSS的border-radius屬性;
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Align( alignment: Alignment.center, child: new ClipRRect( borderRadius: const BorderRadius.all(const Radius.circular(30.0)), child: new Container( width: 180.0, height: 180.0, color: Colors.red, ), ), ), ); }
效果:
把radius值調到90,變成了圓形:
相似的能夠剪切元素的還有ClipOval,ClipPath,這裏就不深刻介紹了。
先看效果:
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Align( alignment: Alignment.center, child: new PhysicalModel( color: Colors.black, elevation: 6.0, child: new Container( width: 180.0, height: 180.0, color: Colors.red, ), ), ), ); }
能夠看到紅色方塊底下有一個陰影,讓紅色方塊有一種懸浮的感受,有material design的風格。
相似於CSS的transform屬性,能夠提供沿着X,Y或者Z軸旋轉,位移拉伸等效果。
Widget build(BuildContext context) { return new Container( color: Colors.white, child: new Align( alignment: Alignment.center, child: new Transform( transform: new Matrix4.rotationZ(PI / 2), child: new Container( color: Colors.black, child: new Text('垂直文字', style: const TextStyle(color: Colors.red),) ) ), ), ); }
得注意一下,Transform控件中的transformHitTests屬性,若是咱們沿着X軸位移一個按鈕,通常來講,咱們照樣能夠直接點擊位移以後的按鈕,由於transformHitTests爲true的時候,在hitTest會判斷點擊落點是否在transfrom所作的操做(旋轉,拉伸或者位移等)後的區域裏面,可是若是爲false,此時點擊按鈕原來的區域仍然會觸發點擊事件,可是直接點擊就不行了。
能夠提供位移,可是並無Tranform控件提供那麼多變換,僅僅是上下左右的位移,並且位移的基準是以child的大小進行的。
Widget build(BuildContext context) { return new Container( color: Colors.white, alignment: Alignment.center, child: new FractionalTranslation( translation: const Offset(1.0, 0.0), child: new Container( width: 100.0, height: 100.0, color: Colors.red, ), ) ); }
效果:
紅色方塊往右邊移動了一個身位,就跟CSS中transfrom: translate(100%, 0)效果同樣的。
旋轉盒子,可使用quarterTurns屬性控制旋轉,每次旋轉quarterTurns * 90度。
Widget build(BuildContext context) { return new Container( color: Colors.white, alignment: Alignment.center, child: new RotatedBox( quarterTurns: -1, child: new Container( width: 100.0, height: 100.0, color: Colors.red, child: new Text('我倒轉了'), ), ) ); }
在前端每一個元素都基本會有border, margin, padding,可是在Flutter裏面可能不得不吐槽連padding都要用個控件,未免太過於麻煩。對於此框架的開發者們也有本身一套見解,在Flutter裏面組合簡單的控件去實現複雜的控件,而不是經過繼承去實現能夠說是Flutter的主要設計思想,因此你會發現儘管Container控件提供了padding的參數,但其實它也背後也是經過建立Padding控件來實現效果的。
在CSS中有background-position和background-size兩個屬性控制背景圖如何平鋪,例如:若是背景圖比元素尺寸大或者小的時候,是否要進行拉伸,若是要拉伸,是拉伸圖片寬度仍是拉伸圖片高度來適應等等。
而FittedBox所作的事情也是差很少,它有兩個很重要的參數:aligment 和 fit。
fit可取值:
基本這個跟CSS的background-size取值都同樣的。
而aligment則是控制,當子元素大小沒有徹底佔滿父元素的時候,如何定位,是居中仍是靠左靠右。
雖然拿background-size來作對比,可是background-size只是控制背景圖片,而FittedBox幾乎能夠對任何元素起做用,由於它是經過Transform放大縮小子元素來達到剛纔所說的效果。
Widget build(BuildContext context) { return new Container( color: Colors.white, alignment: Alignment.center, child: new Container( width: 200.0, height: 100.0, color: Colors.black child: new FittedBox( fit: BoxFit.fitHeight, alignment: Alignment.bottomRight, child: new Container( color: Colors.red, width: 300.0, height: 240.0, alignment: Alignment.center, child: new Text('AAA'), ), ) ) ); }
效果:
這裏紅盒子大小是比黑盒子大的,可是fit爲BoxFit.fitHeight就會經過拉伸高度來適應黑盒子,若是把fit屬性改爲BoxFit.fitWidth,效果就是這樣的:
能夠看到字體是被直接縮小了。
爲何把兩個控件一塊兒講呢?由於它們都依賴了相同的RenderObject:RenderConstrainedBox,而RenderConstrainedBox只有一個參數:additionalConstraints。
而這個參數在performLayout中:
void performLayout() { if (child != null) { child.layout(_additionalConstraints.enforce(constraints), parentUsesSize: true); size = child.size; } else { size = _additionalConstraints.enforce(constraints).constrain(Size.zero); } }
而BoxConstraints.enforce方法:
BoxConstraints enforce(BoxConstraints constraints) { return new BoxConstraints( minWidth: minWidth.clamp(constraints.minWidth, constraints.maxWidth), maxWidth: maxWidth.clamp(constraints.minWidth, constraints.maxWidth), minHeight: minHeight.clamp(constraints.minHeight, constraints.maxHeight), maxHeight: maxHeight.clamp(constraints.minHeight, constraints.maxHeight) ); }
可見additionalConstraints是在原基礎constraints增長了本身的約束,可是並不會打破原來的約束條件。
主要有三個參數:aligment, widthFactor 和 heightFactor。
aligment參數控制child的定位;widthFactor 和 heightFactor 控制child的約束,若是widthFactor或者heightFactor不爲null,會產生一個新的BoxConstraints:它的minWidth 和 maxWidth爲原BoxConstraint.maxWidth widthFactor;minHeight 和 maxHeight爲原BoxConstraint.maxHeight heightFactor。
代碼:
Widget build(BuildContext context) { return new Align( alignment: Alignment.center, child: new Container( color: Colors.green, width: 300.0, height: 300.0, child: new FractionallySizedBox( heightFactor: .5, widthFactor: .5, alignment: Alignment.topLeft, child: new Container( color: Colors.red, ) ) ) ); }
效果:
能夠看到當widthFactor和heigthFractor時,紅色盒子寬高都爲綠色的一半。
看名稱也知道跟控制尺寸有關了,這個控件主要有兩個參數:maxWidth和maxHeight,當constraints是unbounded的時候,也就是maxWidth和maxHeight都是infinite的時候,會用maxWidth和maxHeight替換原來的maxWidth和maxHeight,因此若是contraints是bounded的時候並不會起做用。
關鍵代碼:
BoxConstraints _limitConstraints(BoxConstraints constraints) { return new BoxConstraints( minWidth: constraints.minWidth, maxWidth: constraints.hasBoundedWidth ? constraints.maxWidth : constraints.constrainWidth(maxWidth), minHeight: constraints.minHeight, maxHeight: constraints.hasBoundedHeight ? constraints.maxHeight : constraints.constrainHeight(maxHeight) ); } @override void performLayout() { if (child != null) { child.layout(_limitConstraints(constraints), parentUsesSize: true); size = constraints.constrain(child.size); } else { size = _limitConstraints(constraints).constrain(Size.zero); } }
對比ConstrainedBox,明顯使用範圍就沒有那麼廣了。
從前面的幾個控件:SizedBox,ConstrainedBox和LimitedBox分析知道,咱們彷佛沒有辦法打破由parent傳遞下來的約束條件,可是咱們總會有一些狀況是子組件的尺寸大於父組件的狀況,那麼怎麼解決的尼?來,就看這裏的OverflowBox控件,這個控件提供了幾個參數:minWidth,minHeight,maxWidth,maxHeight 和 aligment;先看代碼:
BoxConstraints _getInnerConstraints(BoxConstraints constraints) { return new BoxConstraints( minWidth: _minWidth ?? constraints.minWidth, maxWidth: _maxWidth ?? constraints.maxWidth, minHeight: _minHeight ?? constraints.minHeight, maxHeight: _maxHeight ?? constraints.maxHeight ); } void performLayout() { if (child != null) { child.layout(_getInnerConstraints(constraints), parentUsesSize: true); alignChild(); } }
這裏能夠看到直接使用咱們傳入的參數替換了本來的minxWidth,maxWidth等,因此底下的組件能夠根據新的約束條件來佈局。
作一下demo:
Widget build(BuildContext context) { return new Align( alignment: Alignment.center, child: new Container( color: Colors.green, alignment: Alignment.center, width: 300.0, height: 300.0, child: new OverflowBox( maxWidth: double.INFINITY, maxHeight: double.INFINITY, child: new Container( height: 600.0, width: 200.0, color: Colors.red, ), ) ) ); }
效果:
若是沒有OverflowBox控件,紅色的盒子是不可能超過綠色盒子的;而aligment能夠控制紅色盒子在綠色盒子裏面的定位,如今是居中顯示的。
剛纔OverflowBox是由於咱們修改了約束條件因此child佈局大小確實被改變了,因此會發生溢出,而SizedOverflowBox這個控件並不會改變約束條件,可是它仍是可能會發生溢出,爲何尼?由於SizedOverflowBox可讓控件看上去「變小一點」,這怎樣作到的尼?這個控件有一個參數:size,這個參數就是讓咱們決定這個控件看上去應該多大。
關鍵代碼在RenderSizedOverflowBox類中:
@override double computeMinIntrinsicWidth(double height) { return _requestedSize.width; } @override double computeMaxIntrinsicWidth(double height) { return _requestedSize.width; } @override double computeMinIntrinsicHeight(double width) { return _requestedSize.height; } @override double computeMaxIntrinsicHeight(double width) { return _requestedSize.height; } void performLayout() { size = constraints.constrain(_requestedSize); if (child != null) { child.layout(constraints); alignChild(); } }
示例代碼:
Widget build(BuildContext context) { return new Align( alignment: Alignment.center, child: new Container( color: Colors.green, alignment: Alignment.center, width: 300.0, height: 300.0, child: new SizedOverflowBox( size: new Size(200.0, 300.0), child: new Container( color: Colors.red ) ) ) ); }
截圖:
在CSS有一個屬性visibility,當設置爲hidden時,元素是存在可是不會繪製出來;在Flutter中Offstage也能夠作到這種效果。
在RenderOffstage類中:
class RenderOffstage extends RenderProxyBox { ... @override void performLayout() { if (offstage) { child?.layout(constraints); } else { super.performLayout(); } } @override bool hitTest(HitTestResult result, { Offset position }) { return !offstage && super.hitTest(result, position: position); } @override void paint(PaintingContext context, Offset offset) { if (offstage) return; super.paint(context, offset); } ... }
可見當offstage爲true時,佈局仍是會繼續進行的,可是paint方法裏面會直接返回,hitTest方法也會直接跳過,也就是不能響應任何手勢。
這個控件能夠用來讓子控件大小維持在一個固定寬高比,例如:16:9。
直接看佈局算法:
Size _applyAspectRatio(BoxConstraints constraints) { if (constraints.isTight) return constraints.smallest; double width = constraints.maxWidth; double height; // We default to picking the height based on the width, but if the width // would be infinite, that's not sensible so we try to infer the height // from the width. if (width.isFinite) { height = width / _aspectRatio; } else { height = constraints.maxHeight; width = height * _aspectRatio; } // Similar to RenderImage, we iteratively attempt to fit within the given // constraints while maintaining the given aspect ratio. The order of // applying the constraints is also biased towards inferring the height // from the width. if (width > constraints.maxWidth) { width = constraints.maxWidth; height = width / _aspectRatio; } if (height > constraints.maxHeight) { height = constraints.maxHeight; width = height * _aspectRatio; } if (width < constraints.minWidth) { width = constraints.minWidth; height = width / _aspectRatio; } if (height < constraints.minHeight) { height = constraints.minHeight; width = height * _aspectRatio; } return constraints.constrain(new Size(width, height)); }
簡單分析一下:
若是constraints是tight,那麼這個控件並不會起啥做用,因此這個控件通常須要Align控件包裹一下。
若是寬度不是Inifinte,它首先會選擇最大寬度,不然根據maxHeight來反推寬度。
萬一高度超出約束條件,它就會反過來,選擇最大的高度反推出寬度,那麼萬一寬度小於最小寬度,它又會根據最小寬度計算高度等等。
固然最後仍是會根據約束條件來規範最終的Size,因此可能出來效果是跟咱們預設的寬高比不一致,可是這種狀況應該不多。
示例代碼:
Widget build(BuildContext context) { return new Align( alignment: Alignment.center, child: new Container( color: Colors.green, alignment: Alignment.center, width: 300.0, height: 300.0, child: new AspectRatio( aspectRatio: 2.0, child: new Container( color: Colors.red, ), ) ) ); }
截圖:
Sizes its child's width to the child's maximum intrinsic width.
說實在這個控件看了半天沒想出用於哪些場景,搜了一下代碼,基本都用在一些浮窗上。佈局過程是調用getMaxIntrinsicWidth方法遞歸詢問子控件最大的intrinsicWidth,由於這個方法須要遞歸下去,若是每一個控件都調用比較耗性能,當獲取到intrinsicWidth,就會使用這個值做爲約束條件(固然也受到原始的約束條件約束),而後傳遞給child,因此正如上面的話所說,可是仍是想不到哪些場景會須要。
正如圖上,基線能夠影響着文字水平排布;若是兩段文字的基線不同,兩段文字的可能會出現一上一下,並非在同一水平線上排布,就像這樣:
這是兩個Text控件,文字大小分別是12dp和32dp,因此他們的基線位置是不同的,因此這樣的排布並非咱們想要的,因此咱們可使用Baseline控件讓他們都在同樣的基線上,修改後:
這纔是咱們常見的,代碼以下:
Widget build(BuildContext context) { return new Wrap( children: <Widget>[ new Baseline( baseline: 30.0, baselineType: TextBaseline.alphabetic, child: new Text( 'AAAAA', style: new TextStyle( fontSize: 12.0, textBaseline: TextBaseline.alphabetic, ), ) ), new Baseline( baseline: 30.0, baselineType: TextBaseline.alphabetic, child: new Text( 'BBB', style: new TextStyle( fontSize: 32.0, textBaseline: TextBaseline.alphabetic, ), ), ) ], );
把基線的位置都定義爲30,兩段文字都會在來30的水平線上排布,就能夠看到如今整齊的效果。