3

I'd like to create a a new stateless widget class that is defined by 2 images(top, bottom) and a line(defined by a function, e.g. (x){x+500}, a width(can be 0, if it shouldn't be drawn), and a color) separating the two images.

For each pixel:

  • If the y position of a pixel is greater than the result of f(x) + width/2 a pixel of bottom shall be drawn
  • If it's smaller than f(x) - width / 2 a pixel of top shall be drawn
  • Else a pixel of the given line color shall be drawn

Below see an example of what mywidget({'top': A, 'bottom': B, 'f': (x){return sin(x)+500;}, 'width': 1, 'color': Color(0xFFFFFFFF)}); could look like:

(0,0)
+------+
|      |
|  A   |
| __   |
|/  \__|
|      |
|  B   |
+------+(e.g. 1920,1080)

Is there a line widget where the shape is defined by a (mathematic) function?

Is this the only way to do it? Or is there a container widget that already allows this? I have looked at the Stack widget but that's not quite solving the problem, as I'd need a structure to decide which pixel is rendered as described above. The function to decide which should happen should be supplyable by the user.

jaaq
  • 1,188
  • 11
  • 29

1 Answers1

8

ClipPath with CustomClipper<Path> can help you with it.
What you can get:
Result screenshot
Example source code:

import 'dart:math';

import 'package:flutter/material.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: ClippedPartsWidget(
          top: Container(
            color: Colors.red,
          ),
          bottom: Container(
            color: Colors.blue,
          ),
          splitFunction: (Size size, double x) {
            // normalizing x to make it exactly one wave
            final normalizedX = x / size.width * 2 * pi;
            final waveHeight = size.height / 15;
            final y = size.height / 2 - sin(normalizedX) * waveHeight;

            return y;
          },
        ),
      ),
    ),
  );
}

class ClippedPartsWidget extends StatelessWidget {
  final Widget top;
  final Widget bottom;
  final double Function(Size, double) splitFunction;

  ClippedPartsWidget({
    @required this.top,
    @required this.bottom,
    @required this.splitFunction,
  });

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: <Widget>[
        // I'm putting unmodified top widget to back. I wont cut it.
        // Instead I'll overlay it with clipped bottom widget.
        top,
        Align(
          alignment: Alignment.bottomCenter,
          child: ClipPath(
            clipper: FunctionClipper(splitFunction: splitFunction),
            child: bottom,
          ),
        ),
      ],
    );
  }
}

class FunctionClipper extends CustomClipper<Path> {
  final double Function(Size, double) splitFunction;

  FunctionClipper({@required this.splitFunction}) : super();

  Path getClip(Size size) {
    final path = Path();

    // move to split line starting point
    path.moveTo(0, splitFunction(size, 0));

    // draw split line
    for (double x = 1; x <= size.width; x++) {
      path.lineTo(x, splitFunction(size, x));
    }

    // close bottom part of screen
    path.lineTo(size.width, size.height);
    path.lineTo(0, size.height);

    return path;
  }

  @override
  bool shouldReclip(CustomClipper<Path> oldClipper) {
    // I'm returning fixed 'true' value here for simplicity, it's not the part of actual question
    // basically that means that clipping will be redrawn on any changes
    return true;
  }
}
Mikhail Ponkin
  • 2,563
  • 2
  • 19
  • 19
  • Wow, really nice! Thank you very much. Just one more thing about optionally drawing the path in a fixed color(previously ommited from example constructor call, sorry), Could I just draw the path using `Canvas.drawPath`? If so would you draw the path within the getClip function or instead save the path as a member of FunctionClipper so you can draw it within the build function of ClippedPartsWidget? Whats the way to go here? I'd like to only draw the splitFunction if width is given and `width>0`. – jaaq Aug 13 '19 at 09:02
  • 1
    @jaaq yes, you can use resulting path with `Canvas.drawPath`, but it will require using another set of widgets (`CustomPaint` with `CustomPainter`). About storing path: I would store it in FunctionClipper (or CustomPainter) for encapsulation if other widgets does not work with it – Mikhail Ponkin Aug 13 '19 at 10:05
  • Awesome thanks. I'll award you the much earned bounty, once stackoverflow lets me :) – jaaq Aug 13 '19 at 10:13
  • could you maybe help me again please? I tried wrapping the `ClippedPartsWidget` with a `CustomPaint` widget, where I draw the same path again using the same function(which is inefficient, but good enough for now). However, if I draw the path with say `width=3` it either doesn't show if the paint is passed to as a painter property or it overwrites the entire bottom(the clipped widget) with the color I give it. Could you maybe explain in more detail or show me how to finalize this widget so it can optionally render the line defined by splitFunction? – jaaq Aug 14 '19 at 13:00
  • Oh, you want to draw a line? Then I misunderstood you. Then another path is required, containing only the splitting line. There is also an option to set `Paint.style = PaintingStyle.stroke` but in this case it will draw outline of whole path. – Mikhail Ponkin Aug 14 '19 at 15:50
  • Yes that was exactly my issue, it didn't occur to me that fill-in could be the default. Again, very nice implementation, it works flawlessly now! – jaaq Aug 21 '19 at 14:56
  • @MikhailPonkin Do you have an idea how I can tell my application that it should not recalculate everything when I open my keyboard? I asked the question here: https://stackoverflow.com/questions/61551166/how-to-tell-flutter-widget-that-keyboard-doesnt-change-screen-size?noredirect=1#comment108878601_61551166 – TryHard May 03 '20 at 13:05