current position:Home>[flutter topic] 70 graphic custom acestepper | August update challenge

[flutter topic] 70 graphic custom acestepper | August update challenge

2022-01-27 03:50:31 A Ze little monk

      “ This is my participation 8 The fourth of the yuegengwen challenge 18 God , Check out the activity details : 8 Yuegengwen challenge ” juejin.cn/post/698796…

       I tried the side dish a few days ago Flutter Stepper Simple and practical , But there are also limitations in style ,Stepper The use of side dishes was tried in the previous article The diagram is basically Stepper Stepper , Now I'll try to add some new features on this basis ;

  1. Step Connection support between Line and dot dotted line , And the color and size can be customized ;
  2. Step Header Icon Chinese support Custom text /icon/ Local pictures / Network picture , And the size and color can be customized separately ;
  3. The transverse Stepper Support sliding , Do not limit the overall width ;
  4. Step The middle button supports single explicit and implicit processing ;
  5. Stepper Each of them Step The content supports full display and separate display ;
  6. Other customizations ThemeData;

       The side dish is ready in Stepper On this basis , First of all, understand Stepper The composition of the , According to everything Widget Thought , The side dish draws a basic composition diagram :

New features extend

1. Dot dotted line

      Step The line between them is only a straight line, a little monotonous , For different actual scenarios , Side dishes try dots and dashes ;

  1. Define the connection type ,nomal Is a straight line ,circle Dotted lines for dots ;
enum LineType { normal, circle }
 Copy code 
  1. Draw dotted lines , The side dish preparation supports custom connection width ( A straight line / Dotted line ), Therefore, the radius of the dot is obtained according to the width , The distance between the dots is the size of a dot , Draw... Over a length _circleLength / radius / 4 - 1 Just a dot , The reason why the side dish -1 Because at the connection junction , The dots between the head and tail are too close ( Free to set );
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 : radius,
                  type == ACEStepperType.horizontal ? radius : radius + 4 * radius * i),
              radius: radius), 0.0, 2 * pi);
    }
    canvas.drawPath(_path, Paint()..color = color..strokeCap = StrokeCap.round..style = PaintingStyle.fill);
  }
}
 Copy code 
  1. Draw a straight line or rounded dotted line in 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: _LinePainter(color: color, radius: _diameter * 0.5, type: type)));
  }
}
 Copy code 

2. Header Icon Content customization

      Step Header Icon There are four properties , But the display content is not only the index of the array is incremented, but also Icon immutable , The side dish adds custom text /Icon/ Local pictures / Display of network pictures , Not a single array subscript ;

  1. Definition Header type ;text To show the text content ,icon by IconData,ass_url For local picture path ,net_url For network pictures , Do not set the array subscript that is incremented by default ;
enum IconType { text, icon, ass_url, net_url }
 Copy code 
  1. 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 code 
  1. 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 code 
  1. Will draw Icon Place in 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 code 

3. Slide sideways

       Analysis of the source code ,Stepper The horizontal way is to Step Put in Row in , If Step Too much will cause width overflow ; Adjust the storage mode of dishes , Will customize ACEStepper Place in the horizontal direction ListView in , No width restrictions , Place multiple ACEStep It can slide horizontally ;

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 code 

4. A single button is displayed or hidden

       The longitudinal Stepper in Controls The button is displayed by default , Side dishes in order to adapt to more scenes , Allow buttons 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 code 

5. Content Content display

      Stepper Select a single Step It will show Content Content , But small dishes try to make a logistics information timeline ,Content The content should be displayed , So add a state , Allow users to show 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 code 

6. Customize ThemeData

       In order to extend the Stepper Flexibility to show results , The side dish has been added ThemeData The theme is flexible, showing the color of each position, etc ;

class ACEStepThemeData {
  final Color circleColor,      //  Ring default color 
      circleActiveColor,        //  Circle selected color 
      contentColor,             //  The default color of the ring content 
      contentActiveColor,       //  Ring content selected color 
      lineColor;                //  The color of the connection 
  final double circleDiameter;  //  The diameter of the ring 

  ACEStepThemeData(
      {this.circleColor = _kCircleColor,
      this.lineColor = _kLineColor,
      this.circleActiveColor = _kCircleActiveColor,
      this.contentColor = _kContentColor,
      this.contentActiveColor = _kContentActiveColor,
      this.circleDiameter = _kCircleDiameter});
}
 Copy code 

The source code is introduced

const ACEStepper(
  {Key key,
  @required this.steps,                 // ACEStep  Array 
  this.physics,                         //  Slide animation 
  this.type = ACEStepperType.vertical,  //  Direction : The transverse / The longitudinal 
  this.currentStep = 0,                 //  At present  ACEStep
  this.onStepTapped,                    // ACEStep  Click callback 
  this.onStepContinue,                  // ACEStep  Continue button callback 
  this.onStepCancel,                    // ACEStep  Cancel button callback 
  this.isContinue = true,               //  The Continue button is hidden 
  this.isCancel = true,                 //  The Cancel button is displayed or hidden 
  this.headerHeight,                    //  The transverse  Header  Height 
  this.controlsBuilder,                 //  Custom control 
  this.themeData,                       //  The theme style 
  this.isAllContent = false});          //  Whether the content is fully displayed 

const ACEStep(
    {@required this.title,              //  title  Widget
    @required this.circleData,          //  Title icon content 
    this.content,                       //  Content  Widget
    this.subtitle,                      //  Subtitle  Widget
    this.toptips,                       //  Top tip  Widget
    this.lineType = LineType.normal,    //  Connection mode 
    this.iconType = IconType.text,      //  Title icon mode 
    this.isActive = false});            //  Is it highlighted 
 Copy code 

       Analysis of the source code , The dishes are custom made ACEStepper And Stepper Usage is similar. , Just added extensions , For specific use, please go to GitHub;

matters needing attention

1. Header How to connect

      Step Header Icon The connection is the splicing of two fixed length connecting lines and rings , Hide the display when the connection is in the first and last ; This creates a problem , When Title / subTitle When the content setting is too large , Can cause Header And Content The connection is not connected ; The side dish has not found a suitable treatment method yet , I hope friends with solutions can give me more guidance !

2. Content How to connect

       In the longitudinal direction Stepper in Content The corresponding connection of the display is a separate connection , And up and down Header Connect ; but Content The size is not fixed , The dot dotted line drawn by the side dish needs to obtain its height to draw ; Side dish analysis source code through State / AspectRatio To deal with ,AspectRatio I 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 code 

3. The transverse Header Height

       Side dishes are being processed ACEStepper Header Time use ListView Deposit ACEStepper, Solved the problem of horizontal overflow ; But will Header And Content Put it in Column It will involve ListView High error problem , The side dish adopts Expend The way is not handled well , At present, the basic height is set ; If you have a better plan, please give more guidance !


       It's a piece of cake ACEStepper The customization of is not mature enough , There are still many areas that need to be optimized , If you have any suggestions, please give more guidance !

source : Little monk aze

copyright notice
author[A Ze little monk],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/01/202201270350206476.html

Random recommended