CSS is large and complex, flexible and cumbersome, and mapping the rules of CSS to the controls of flutter is quite a challenge. But what can be achieved to what extent and what part of the need to practice.

CSS is a set of rules that apply to tags. In order to achieve transformation, tags must first be parsed — that is, to convert structured data from text into in-memory object data — as is the case with HTML and small programs.

To prepare

Simple parsing of tags is not a problem, there are ready-made HTML/XML parsing libraries; The key is how to parse CSS. It’s not realistic to implement the various matching of CSS rules, descendant selectors, cascading and overwriting effects. Fortunately, there are a lot of powerful tools available on the Web to let us pick and choose. Our ultimate goal is to turn a single node with CSS attributes into the corresponding Flutter control, so we need to get the CSS attributes of a single node first. If we translate the various attributes in the CSS file directly into inline styles, we will know all the CSS attributes corresponding to the current node when we parse the node. So we need to find a library or tool that can turn CSS into an inline style. Target clear how to do is actually very simple, just look up there are a lot of tools, such as @team-Griffin/CSS-longhand, CSS-longhand, CSS-irritable -expand, CSS -shorthand-expanders, fela-plugin-expa-type, grunt- CSS -longhand, inline-style-expa-type, and juice are most popular

So what we’re parsing is an HTML/XML file that juice has converted to represent the CSS of a single node with the following classes:

class CSSStyle {
  final Map<String.String> _attrs;

  const CSSStyle(this._attrs);

  String? operator[] (String key) => _attrs[key];

  double? _getDouble(Stringkey) => _attrs[key]? .let((it) =>double.tryParse(it));

}
Copy the code

The role of CSS

CSS not only helps us determine the control type of the current node, but also provides additional controls such as padding/margin, which are attached to the control of the current node as the parent, as follows:

Widget w = builder.build(element, children); CSSStyle css = ... ; final padding = css.padding; if (padding ! = null) { w = Padding( padding: padding, child: w, ); } final margin = css.margin; if (margin ! = null) { w = Padding( padding: margin, child: w, ); }Copy the code

Because the controls of Flutter are so rich and powerful, the core idea is to take advantage of a combination of existing controls; Of course you can draw your own controls like Kraken, but you need a very deep level of flutter rendering skills, so I won’t do that yet.

Position attribute analysis

The beginning of the problem is how to achieve the position attribute, detailed introduction can see MDN, its value is relative, absolute, fixed. Static value is equal to no set value.

relative

The meaning of this property is easy to understand, but it is actually offset:

final positionCSS = css['position'];
if (positionCSS == 'relative') {
  final left = css._getDouble('left');
  final top = css._getDouble('top');
  final right = css._getDouble('right');
  final bottom = css._getDouble('bottom');
  finaldx = left ?? right? .let((it) => -right);finaldy = top ?? bottom? .let((it) => -bottom);if(dx ! =null|| dy ! =null) {
    w = Transform.translate(
      offset: Offset(
        dx ?? 0,
        dy ?? 0, ), child: w, ); }}Copy the code

Left,top is dx, dy, right, bottom is -dx,-dy

absolute

This attribute is relatively easy to understand. The absolute element does not participate in the sibling layout, it is a deviation from the parent. The effect of this layout is FrameLayout on Android, but stacks up in the Flutter.

It’s not hard to understand, but it’s a little tricky. An element or node that is declared absolute, no matter what kind of control it is, needs its parent to be a Stack. What does that mean? This means that we have to take into account the properties of the children of a node when parsing its control! It is no longer a corresponding problem but an analytical problem. To represent a node in HTML/XML, use the following class:

class _AssembleElement { final String name; final CSSStyle style; final String? klass; final Map<String, String>? extra; final List<_AssembleElement> children; _AssembleElement(this.name, this.style, this.klass, this.extra, this.children); @override String toString() { return '<$name style="$style" ${extra? .let((it) => 'extra="$extra"') ?? "'} > '; }}Copy the code

Style =”” style=”” style=”” style=”” style=”” style=”” style=”” style=”” style=”” style=”” Add the following logic where we determine which control the current node should correspond to:

Widget build(_AssembleElement e, List<Widget> children) { final alignChildren = e.children.where((e) => e.style['position'] == 'absolute'); if (alignChildren.length > 0) { return Stack( children: children, ); }... }Copy the code

The combination of the two produces the correct control object.

fixed

The most troublesome is the fixed value, according to the document description

Elements are moved out of the normal document flow, and instead of reserving space for the element, the element position is specified by specifying its position relative to the screen viewport.

Usually used for views that are displayed all the time on the page without scrolling, similar to the FloatingActionButton in flutter, which is actually a Stack effect but needs to be handled in a different way. Since it will be moved out of the normal document flow, the nodes need to be placed separately during parsing. After parsing, another root node should be installed outside the root node, which can be divided into three steps:

  1. When parsing encounters a property declared asposition: fixedTo place the node separately (_fixedPosition) :
children = elementNodes
    .map((child) => _fromXml(child, ancestorStyle))
    .toList();
children.removeWhere((e) {
  final isFixed = e.style['position'] == 'fixed';
  if (isFixed) {
    _fixedPosition.add(e);
  }
  return isFixed;
});
Copy the code
  1. Add the root node after parsing:
var rootElement = _fromXml(root, null);
if (_fixedPosition.isNotEmpty) {
  final children = [
    rootElement,
    ..._fixedPosition,
  ];
  rootElement = _AssembleElement('_floatStack', const CSSStyle({}), null, null, children);
  _fixedPosition.clear();
}
Copy the code
  1. Apply this special root node when creating the entire view:

The _assembleWidget represents the way to do the view creation for a single node, and the 0 represents the depth.

Widget build(BuildContext context, _AssembleElement root) {
  if (root.name == '_floatStack') {
    final children = root.children.map((e) => _assembleWidget(e, 0)).toList(growable: false);
    return Stack(
      children: children,
    );
  }
  return _assembleWidget(root, 0);
}
Copy the code

So we can relatively complete CSS position this property effect! In the end, the key to translating into the Flutter control is to define the semantics, achieve the key presentation, and then take the root and abandon some of the secondary and marginal effects.