Zero, preface,

1. Collection of FlutterUnit drawing

This article is affiliated to FlutterUnit, project address: FlutterUnit

The FlutterUnit Drawing Collection has begun. This collection will include some interesting drawing works, or some typical drawing samples to expose you to the wider possibilities of Flutter. (The black box below is also drawn)

The Chaos Random Portrait Triangular Mesh Hypnotic Squares

2. About the paintings in this paperSee relevant source code here

When you see GitHub’s avatar, you feel something. The default avatar is a 5 by 5 grid, randomly filled with color blocks to form a graph

[1]. The number of cells per row (column) can be specified and is odd [2]. The graph forms left and right symmetry [3]. Half of the image points randomly appear randomlyCopy the code

Results show

5 * 5 5 * 5 9 * 9
9 * 9 11 * 11 11 * 11

3. What does it do?
[1] practice drawing ability [2] practice data manipulation ability [3]. Save the widget as a picture and you get the default avatar [4]. Best of all, it's funCopy the code

One, the canvas grid and coordinates

1. Basic idea

Here it is: Think of our whiteboard as a grid (although you can sketch it on paper, you don’t have to draw it) so it is easy to see the relationships. Now the whiteboard becomes a plane coordinate system, and we can describe a position with a two-dimensional coordinate point. Let me draw this rectangle.

Now create the Position class to describe the coordinate positions.

class Position {
  final int x;
  final int y;

  Position(this.x, this.y);

  @override
  String toString() {
    return 'Position{x: $x, y: $y} '; }}Copy the code

2. Start with a point

Corresponds a Position object to a rectangular region in the grid

Rect.fromLTWH can draw a rectangle based on the upper-left coordinates and the width and height of the rectangle

Position(1, 1) | Position(4, 3)| Position(3, 2) | —|—|—|— | ||

class PortraitPainter extends CustomPainter {
  Paint _paint;/ / brush
  final int blockCount = 5; / / blocks
  final position = Position(1.1); / / pointPortraitPainter(): _paint = Paint().. color = Colors.blue;@override
  void paint(Canvas canvas, Size size) {
    // Crop the current region
    canvas.clipRect(
        Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
        
    var perW = size.width / blockCount;
    var perH = size.height / blockCount;
    _drawBlock(perW, perH, canvas, position);
  }

  / / to draw
  void _drawBlock(double perW, double perH, Canvas canvas, Position position) {
    canvas.drawRect(
        Rect.fromLTWH(position.x * perW, position.y * perH, perW, perH), _paint);
  }

  @override
  bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
Copy the code

3. Draw multiple points

When you can draw a point, the problem has changed from an image problem to a coordinate problem using the coordinate set List , by walking through the coordinate set, drawing the rectangle block

multipoint Go to the line
final List<Position> positions = [
  Position(1.0),
  Position(2.1),
  Position(0.1),
  Position(0.2),
  Position(1.3),
  Position(2.4),
  Position(3.0),
  Position(2.1),
  Position(4.1),
  Position(4.2),
  Position(3.3)];@override
void paint(Canvas canvas, Size size) {
  // The hero sees...
  // Walk through the coordinate set, draw the block
  positions.forEach((element) {
    _drawBlock(perW, perH, canvas, element);
  });
}
Copy the code

Second, random number and data operation

Above has completed the corresponding relationship between data and graphics, to reach the number is form, form is number of the state of unity.

Generally, data is received in the sketchpad class, which is only for drawing related operations, and variables that need DIY can be extracted.


1. Panel type :PortraitPainter
class PortraitPainter extends CustomPainter {
  Paint _paint;

  final int blockCount;
  final Color color;
  final List<Position> positions;

  PortraitPainter(this.positions, {this.blockCount = 9.this.color=Colors.blue}) : _paint = Paint().. color = color;@override
  void paint(Canvas canvas, Size size) {
    canvas.clipRect(
        Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));

    var perW = size.width / blockCount;
    var perH = size.height / blockCount;
    
    positions.forEach((element) {
      _drawBlock(perW, perH, canvas, element);
    });
  }
  
  void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
    canvas.drawRect(
        Rect.fromLTWH(position.x * dW, position.y * dH, dW, dH), _paint);
  }

  @override
  bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
Copy the code

2. Component class :RandomPortrait

Use the artboard with CustomPaint. Here, for demonstration purposes, the rebuilt graphics refresh when clicked

Now you just need to complete the generation of coordinate points as required.

class RandomPortrait extends StatefulWidget {
  @override
  _RandomPortraitState createState() => _RandomPortraitState();
}

class _RandomPortraitState extends State<RandomPortrait> {
  List<Position> positions = [];
  Random random = Random();
  final int blockCount = 9;

  @override
  Widget build(BuildContext context) {
    _initPosition();
    return GestureDetector(
        onTap: () {
          setState(() {});
        },
        child: CustomPaint(
            painter: PortraitPainter(positions, blockCount: blockCount)));
  }

  void _initPosition() {
    // TODO generates a set of coordinate points}}Copy the code

3. Generation point set

The idea is to make the left half of the points, and then traverse the points, and add the symmetry points when the left is not in the middle. About symmetric processing:

If a and B are symmetric about x equals c. Then (a.x + b.x)/2 = c, i.e. B.x = 2* c-a.xCopy the code
1 2 3
  void _initPosition() {
    positions.clear(); // Empty the set of points
    
    // The number of left halves (random)
    int randomCount = 2 + random.nextInt(blockCount * blockCount ~/ 2 - 2);
    / / axis of symmetry
    var axis = blockCount ~/ 2 ;
    // Add the left random point
    for (int i = 0; i < randomCount; i++) {
      int randomX = random.nextInt(axis+ 1);
      int randomY = random.nextInt(blockCount);
      var position = Position(randomX, randomY);
      positions.add(position);
    }
    // Add symmetry points
    for (int i = 0; i < positions.length; i++) {
      if (positions[i].x < blockCount ~/ 2) {
        positions
            .add(Position(2* axis - positions[i].x, positions[i].y)); }}}Copy the code

And that’s basically it, so you can do some optimizations later


4. Small optimization

[1]. Some margins can be left in the drawing process to make it more beautiful [2]. When the number of cells is 9*9, it may lead to small gaps in the connected blocks due to endless division (figure 2), which can be solved by rounded edge length

Leave a margin Small gap Small gap optimization
class PortraitPainter extends CustomPainter {
  Paint _paint;

  final int blockCount;
  final Color color;
  final List<Position> positions;

  final pd = 20.0;

  PortraitPainter(this.positions,
      {this.blockCount = 9.this.color = Colors.blue}) : _paint = Paint().. color = color;@override
  void paint(Canvas canvas, Size size) {
    canvas.clipRect(
        Rect.fromPoints(Offset.zero, Offset(size.width, size.height)));
    
    var perW = (size.width - pd * 2) / (blockCount);
    var perH = (size.height - pd * 2) / (blockCount);

    canvas.translate(pd, pd);
    positions.forEach((element) {
      _drawBlock(perW, perH, canvas, element);
    });
  }

  void _drawBlock(double dW, double dH, Canvas canvas, Position position) {
    canvas.drawRect(
        Rect.fromLTWH(
            position.x * dW.floor()*1.0, 
            position.y * dH.floor()*1.0, 
            dW.floor()*1.0, 
            dH.floor()*1.0), _paint);
  }

  @override
  bool shouldRepaint(PortraitPainter oldDelegate) => true;
}
Copy the code

3. Canvas drawing is saved as a picture

There are many ways to read the image data of a Widget. Here I have used a RepaintBoundary and encapsulated it briefly. After obtaining image data, you can save it to the local PC as a picture or send it to the server as a user profile picture. Anyway, byte stream in hand, everything is safe.


1. Widget2Image components

Simple encapsulation to simplify Widget2Image operation flow.

class Widget2Image extends StatefulWidget {
  final Widget child;
  final ui.ImageByteFormat format;

  Widget2Image(
      {@required this.child,
        this.format = ui.ImageByteFormat.rawRgba});

  @override
  Widget2ImageState createState() => Widget2ImageState();


  static Widget2ImageState of(BuildContext context) {
    final Widget2ImageState result = context.findAncestorStateOfType<Widget2ImageState>();
    if(result ! =null)
      return result;
    throw FlutterError.fromParts(<DiagnosticsNode>[
      ErrorSummary(
          'Widget2Image.of() called with a context that does not contain a Widget2Image.'),]); }}class Widget2ImageState extends State<Widget2Image> {
  final GlobalKey _globalKey = GlobalKey();

  @override
  Widget build(BuildContext context) {
    return RepaintBoundary(
      key: _globalKey,
      child: widget.child,
    );
  }

  Future<Uint8List> loadImage() {
    return _widget2Image(_globalKey);
  }

  Future<Uint8List> _widget2Image(GlobalKey key) async {
    RenderRepaintBoundary boundary = key.currentContext.findRenderObject();
    / / get the UI. The image
    ui.Image img = await boundary.toImage();
    // Get image bytes
    var byteData = await img.toByteData(format: widget.format);
    Uint8List bits = byteData.buffer.asUint8List();
    returnbits; }}Copy the code

2. UseWidget2Image
  @override
  Widget build(BuildContext context) {
    _initPosition();
    return Widget2Image( / / use
        format: ImageByteFormat.png,
        child: Builder( // Use Builder to drop the context one level
          builder: (ctx) => GestureDetector(
            onTap: () {
              setState(() {});
            },
            onLongPress: () async { // Execute the fetch method on time
              var bytes = await Widget2Image.of(ctx).loadImage();
           
              // After obtaining the image byte data ----, you can operate at will
              final dir = await getTemporaryDirectory();
              final dest = path.join(dir.path, "widget.png");
              await File(dest).writeAsBytes(bytes);
              Scaffold.of(context)
                  .showSnackBar(SnackBar(content: Text("Image saved to:$dest")));
            },
            child: CustomPaint(
                painter: PortraitPainter(positions, blockCount: blockCount)),
          ),
        ));
  }
Copy the code

This is the end of this article, which should be interesting. You can actually do a lot of interesting things with coordinates. For example, it doesn’t have to be a rectangle to draw circles, triangles, or even pictures.

If you make the grid smaller, it looks like a pixel world. Based on that, maybe you can make tetris or snake or something. The most important thing to say is that what drives the view is the data behind it, and imagination makes the data infinite.

Finally, please support FlutterUnit

@Zhangfengjietele 2020.10.11 not allowed to transfer ~ END ~