“This is the 18th day of my participation in the August Gwen Challenge. For more information: August Gwen Challenge” juejin.cn/post/698796…
A few days ago, I tried the Flutter Stepper. It is simple and practical, but there are also limitations in style and other aspects. The use of the Flutter Stepper has been tried in the previous article to illustrate the basic Stepper, now I try to add some new features based on this.
- The line between steps supports straight lines and dotted lines, and the color and size can be customized.
- Step Header Icon supports user-defined text/Icon/local image/network image, and the size and color can be customized respectively.
- Horizontal Stepper supports sliding and does not limit overall width;
- Step buttons support single explicit and implicit processing;
- Stepper supports full display and separate display of each Step content;
- Other custom ThemeData;
First of all, we need to understand the composition of Stepper. Based on the idea that everything is a Widget, Xiao CAI draws a basic composition diagram:
New feature extensions
1. Dotted line
Only the straight line between steps is a little monotonous. For different actual scenes, small dishes try dotted lines.
- Define line type, nomal is straight line, circle is dotted line;
enum LineType { normal, circle }
Copy the code
- The radius of the dots is based on the width of the dots. The distance between the dots is based on the size of the dots. Draw _circleLength/radius / 4-1 dots in one length. The reason why the side dish is -1 is that the dots between the beginning and the end are too close to each other (it can be set freely).
class _LinePainter extends CustomPainter { final Color color; final double radius; final ACEStepperType type; _LinePainter({this.color, this.radius, this.type}); @override bool hitTest(Offset point) => true; @override bool shouldRepaint(_LinePainter oldPainter) => oldPainter.color ! = color; @override void paint(Canvas canvas, Size size) { double _circleLength = (type == ACEStepperType.horizontal) ? size.width.toDouble() : size.height.toDouble(); double _circleSize = _circleLength / radius / 4 > 2 ? _circleLength / radius / 4 - 1 : _circleLength / radius / 4; Path _path = Path(); for (int i = 0; i < _circleSize; i++) { _path.addArc(Rect.fromCircle(center: Offset( type == ACEStepperType.horizontal ? radius + 4 * radius * i : The radius, type = = ACEStepperType. Horizontal? The radius: the radius radius, + 4 * * I), the radius: the radius), 0.0, 2 * PI); } canvas.drawPath(_path, Paint().. color = color.. strokeCap = StrokeCap.round.. style = PaintingStyle.fill); }}Copy the code
- Draw a straight line or rounded dotted line for the scene;
class StepperLine extends StatelessWidget { final Color color; final LineType lineType; final ACEStepperType type; StepperLine({@required this.color, this.type = ACEStepperType.horizontal, this.lineType = LineType.normal}); @override Widget build(BuildContext context) { double _width = (type == ACEStepperType.horizontal) ? _kLineHeight : _kLineWidth; double _height = (type == ACEStepperType.horizontal) ? _kLineWidth : _kLineHeight; double _diameter = (type == ACEStepperType.horizontal) ? _height : _width; return lineType == LineType.normal ? Container(width: _width, height: _height, color: color) : Container(width: _width, height: _height, child: CustomPaint(Painter: _diameter * 0.5 (color: color, radius: _diameter * 0.5, type: type)); }}Copy the code
2. Customize Header Icon content
Step Header Icon has four properties, but the display contents are immutable except array subscript increment. The small menu adds custom text /Icon/ local image/network image display, not a single array subscript.
- Define the Header type; Text is the display text content, icon is IconData, ass_URL is the local image path, and net_URL is the network image. The default increasing array subscripts are not set.
enum IconType { text, icon, ass_url, net_url }
Copy the code
- Draw a circle;
class _CirclePainter extends CustomPainter { final Color color; final double size; _CirclePainter({this.color, this.size}); @override bool hitTest(Offset point) => true; @override bool shouldRepaint(_CirclePainter oldPainter) => oldPainter.color ! = color; @override void paint(Canvas Canvas, Size Size) {final double radius = this. Size * 0.5; Canvas. DrawArc (rect. fromCircle(center: Offset(radius, radius), radius: radius), 0.0, 2 * PI, false, Paint().. color = color.. strokeCap = StrokeCap.round.. StrokeWidth = 1.0.. style = PaintingStyle.stroke); }}Copy the code
- Draw Header content;
Widget _buildIcon(IconType type, CircleData circleData, int index) { Color contentActiveColor = widget.themeData == null ? _kContentActiveColor : widget.themeData.contentActiveColor ?? _kContentActiveColor; Color contentColor = widget.themeData == null ? _kContentColor : widget.themeData.contentColor ?? _kContentColor; Color _color = widget.steps[index].isActive ? contentActiveColor : contentColor; switch (type) { case IconType.text: return Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color)); break; case IconType.icon: return circleData.circleIcon ! = null ? Icon(circleData.circleIcon, size: _kCircleIconSize, color: _color) : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color)); break; case IconType.ass_url: return circleData.circleAssUrl ! = null ? Padding(padding: EdgeInsets.all(_kCirclePadding), child: Image.asset(circleData.circleAssUrl, color: _color)) : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color)); break; case IconType.net_url: return circleData.circleNetUrl ! = null ? Padding(padding: EdgeInsets.all(_kCirclePadding), child: Image.network(circleData.circleNetUrl)) : Text(circleData.circleText ?? (index + 1).toString(), style: TextStyle(color: _color)); break; default: return Text((index + 1).toString(), style: TextStyle(color: _color)); break; }}Copy the code
- Place the drawing Icon inside the ring;
Widget _buildCircle(IconType type, double size, CircleData circleData, int index) {
Color circleActiveColor = widget.themeData == null ? _kCircleActiveColor : widget.themeData.circleActiveColor ?? _kCircleActiveColor;
Color circleColor = widget.themeData == null ? _kCircleColor : widget.themeData.circleColor ?? _kCircleColor;
return Stack(children: <Widget>[
Container(child: CustomPaint(painter: _CirclePainter(color: widget.steps[index].isActive ? circleActiveColor : circleColor, size: size))),
Container(width: size, height: size, child: Center(child: _buildIcon(type, circleData, index)))
]);
}
Copy the code
3. Swipe sideways
Analysis of the source code, Stepper horizontal way is to place the Step in the Row, if the number of steps will cause width overflow; Adjust the storage mode, the custom ACEStepper will be placed in the horizontal ListView, will not limit the width, placed multiple ACEsteps can be horizontal sliding;
Widget _buildHorizontal() { return Column(children: <Widget>[ Container(height: Widget. headerHeight <= 0.0? _kHeaderHeight: widget.headerHeight, Child: ListView(primary: false, shrinkWrap: true, scrollDirection: Axis.horizontal, children: <Widget>[ for (int i = 0; i < widget.steps.length; i += 1) Column(key: _keys[i], children: <Widget>[ InkWell(child: _buildHorizontalHeader(i), onTap: () => (widget.onStepTapped != null) ? widget.onStepTapped(i) : null) ]) ])), Expanded(child: ListView(children: <Widget>[ Container(child: widget.steps[widget.currentStep].content ?? SizedBox.shrink()), _buildVerticalControls() ])) ]); }Copy the code
4. A single button is recessive
In the vertical Stepper, the Controls button is displayed by default. In order to adapt to more scenes, the small dish allows the button to be displayed separately.
Widget _buildVerticalControls() { return (widget.controlsBuilder ! = null) ? widget.controlsBuilder(context, onStepContinue: widget.onStepContinue, onStepCancel: widget.onStepCancel) : Container(child: Row(children: <Widget>[ widget.isContinue ? FlatButton( onPressed: widget.onStepContinue, child: Text(' continue ')) : sizedbox.shrink (), widget.iscancel? FlatButton(onPressed: Widget.onstepcancel, child: Text(' cancel ')) : SizedBox.shrink() ])); }Copy the code
5. Content display
When a single Step is selected in Stepper, the Content will be displayed. However, xiao CAI tries to make a timeline of logistics information, and all Content will be displayed. Therefore, a state is added to allow users to display all Content.
Widget _buildVerticalBody(int index) {
double circleDiameter = widget.themeData == null ? _kCircleDiameter : widget.themeData.circleDiameter ?? _kCircleDiameter;
return Stack(children: <Widget>[
PositionedDirectional(
start: _kTopTipsWidth + (circleDiameter - _kLineWidth) * 0.5, top: Size.zero.width, bottom: Size.zero.width - 2,
child: _isLast(index) ? SizedBox.shrink() : AspectRatio(aspectRatio: 1, child: SizedBox.expand(child: _buildLine(index, false)))),
widget.isAllContent ? Container(
margin: EdgeInsets.only(left: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
child: Column(crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(), _buildVerticalControls() ]))
: AnimatedCrossFade(firstChild: SizedBox.shrink(),
secondChild: Container(margin: EdgeInsetsDirectional.only(start: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
child: Column(children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(), _buildVerticalControls() ])),
crossFadeState: _isCurrent(index) ? CrossFadeState.showSecond : CrossFadeState.showFirst,
duration: Duration(milliseconds: 1))
]);
}
Copy the code
6. Customize ThemeData
In order to expand the flexibility of Stepper’s display effect, the side dish added the ThemeData theme to display the colors of various positions flexibly.
Class ACEStepThemeData {final Color circleColor, // circleActiveColor, // circle selected Color contentColor, // Circle content default color contentActiveColor, // circle content select the color lineColor; Final double circleDiameter; ACEStepThemeData({this.circleColor = _kCircleColor, this.lineColor = _kLineColor, this.lineColor = _kLineColor, this.circleActiveColor = _kCircleActiveColor, this.contentColor = _kContentColor, this.contentActiveColor = _kContentActiveColor, this.circleDiameter = _kCircleDiameter}); }Copy the code
The source code is introduced
Const ACEStepper({Key Key, @required this.steps, // ACEStep array this.physics, This. type = acesteppertype. vertical, // direction: CurrentStep = 0, // currently ACEStep this.onStepTapped, // ACEStep click callback this.onStepContinue, // ACEStep continue button callback this.onStepCancel, // cancel button callback this.isContinue = true, // continue button explicit this.isCancel = true, This. ControlsBuilder, // custom controls this. ThemeData, // Theme style this.isallContent = false}); Const ACEStep({@required this.title, // title widgete@required this.circleData, // title icon content this.content, // Widget this.subtitle, // Widget this.toptips, // Top prompt Widget this.lineType = linetype. normal, This.icontype = icontype. text, // title icon this.isactive = false}); // Whether to highlightCopy the code
Analysis of the source code, small food custom ACEStepper and Stepper usage similar, but increased the extension, specific use please go to GitHub;
Matters needing attention
1. Header connection mode
The connection of Step Header Icon is the splicing of two fixed length lines and the ring. The first and last line are hidden and displayed. This causes a problem. If the Title/subTitle Content is set too large, the Header and Content will not connect properly. Small vegetables have not found the right way to deal with, hope to have a solution to the friend more guidance!
2. Content Connection mode
In the vertical Stepper, the corresponding line of Content display is a separate line, connecting with the upper and lower two headers; However, the Content size is not fixed, and the dotted line drawn by xiaocai needs to obtain its height for drawing; Side dish analysis source code through State/AspectRatio for processing, AspectRatio will study in the follow-up blog;
Widget _buildVerticalBody(int index) {
return Stack(children: <Widget>[
PositionedDirectional(
start: _kTopTipsWidth + (circleDiameter - _kLineWidth) * 0.5, top: Size.zero.width, bottom: Size.zero.width - 2,
child: _isLast(index) ? SizedBox.shrink() : AspectRatio(aspectRatio: 1, child: SizedBox.expand(child: _buildLine(index, false)))),
Container(margin: EdgeInsets.only(left: _kTopTipsWidth + _kCircleMargin * 2 + circleDiameter),
child: Column(crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[widget.steps[index].content ?? SizedBox.shrink(), _buildVerticalControls()]))
]);
}
Copy the code
3. Horizontal Header height
ACEStepper Header with ListView to store ACEStepper, to solve the problem of horizontal overflow; However, putting Header and Content in Column will involve the problem of wrong ListView height, and the Expend method is also not very good to deal with. The basic height is set at present. Have a better plan of friends please more guidance!
Xiao CAI’s customization of ACEStepper is not mature enough, there are still a lot of places to optimize, there are suggestions, please give more guidance!
Source: Little Monk A Ce