Design a background from 2 images in flutter

2,466

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;
  }
}
Share:
2,466
jaaq
Author by

jaaq

Updated on December 13, 2022

Comments

  • jaaq
    jaaq over 1 year

    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

    Blow 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
    jaaq almost 5 years
    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.
  • Mikhail Ponkin
    Mikhail Ponkin almost 5 years
    @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
  • jaaq
    jaaq almost 5 years
    Awesome thanks. I'll award you the much earned bounty, once stackoverflow lets me :)
  • jaaq
    jaaq almost 5 years
    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?
  • Mikhail Ponkin
    Mikhail Ponkin almost 5 years
    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.
  • jaaq
    jaaq almost 5 years
    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!
  • TryHard
    TryHard about 4 years
    @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: stackoverflow.com/questions/61551166/…