imageflutterdartwidgetcustom-painting

Flutter custom image collage


I would like to be able to build collages and for that I tried using CustomPaint to draw the shape and then filling that shape with an image. This is what I tried:

  Container(
    height: 300,
    width: 300,
    decoration: BoxDecoration(
      borderRadius: BorderRadius.circular(10.0),
      border: Border.all(color: Colors.white, width: 5),
    ),
    child: CustomPaint(
      painter: DrawTriangleShape(color: Colors.blue),
      child: Image.asset('assets/images/exampleImage.jpg'),
    ),
  ),

And my DrawTriangleShape:

class DrawTriangleShape extends CustomPainter {
  Paint painter;

  DrawTriangleShape({color: Colors}) {
    painter = Paint()
      ..color = color
      ..style = PaintingStyle.fill;
  }

  @override
  void paint(Canvas canvas, Size size) {
    var path = Path();

    path.moveTo(size.width / 1, 0);
    path.lineTo(0, size.height);
    path.lineTo(size.height, size.width);
    path.close();

    canvas.drawPath(path, painter);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return false;
  }
}

The outcome:

enter image description here

As you can see it is not really the desired outcome as the image is not filling the shape. I didn't find anything on this. Is it even possible? And if not, what would be the best approach to build collages with custom shapes?

Let me know if you need more info!


Solution

  • Instead of a CustomPaint, use a ClipPath widget.

    Using the same path as your CustomPaint:

    enter image description here

    Full source code

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          home: HomePage(),
        ),
      );
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        return Scaffold(
          body: Center(
            child: ClipPath(
              clipper: CustomClipperImage(),
              child: Image.asset('images/abstract.jpg'),
            ),
          ),
        );
      }
    }
    
    class CustomClipperImage extends CustomClipper<Path> {
      @override
      getClip(Size size) {
        return Path()
          ..moveTo(size.width / 1, 0)
          ..lineTo(0, size.height)
          ..lineTo(size.height, size.width)
          ..close();
      }
    
      @override
      bool shouldReclip(CustomClipper oldClipper) {
        return false;
      }
    }
    

    Additional example with a white border and scaling the image.

    enter image description here

    Full source code

    import 'package:flutter/material.dart';
    
    void main() {
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          home: HomePage(),
        ),
      );
    }
    
    class HomePage extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        final width = MediaQuery.of(context).size.width * .8;
        final height = width * .7;
        return Scaffold(
          body: Container(
            color: Colors.black54,
            child: Center(
              child: Stack(
                children: [
                  CustomPaint(
                    painter: MyPainter(),
                    child: Container(width: width, height: height),
                  ),
                  ClipPath(
                    clipper: CustomClipperImage(),
                    child: Transform.scale(
                      scale: 3,
                      child: Image.asset('images/abstract.jpg',
                          width: width, height: height),
                    ),
                  ),
                ],
              ),
            ),
          ),
        );
      }
    }
    
    class CustomClipperImage extends CustomClipper<Path> {
      @override
      getClip(Size size) {
        return Path()
          ..moveTo(size.width, 0)
          ..lineTo(0, size.height)
          ..lineTo(size.height, size.height)
          ..close();
      }
    
      @override
      bool shouldReclip(CustomClipper oldClipper) {
        return false;
      }
    }
    
    class MyPainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final path = Path()
          ..moveTo(size.width, 0)
          ..lineTo(0, size.height)
          ..lineTo(size.height, size.height)
          ..close();
        final paint = Paint()
          ..color = Colors.white
          ..strokeWidth = 8.0
          ..style = PaintingStyle.stroke;
        canvas.drawPath(path, paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }
    

    Solution with zoom and pan on the image

    Using a GestureDetector to detect the change of scale and localFocalPoint. And then using a Matrix4 to Transform our Image.asset:

    enter image description here

    Full source code:

    import 'package:flutter/material.dart';
    import 'package:flutter_hooks/flutter_hooks.dart';
    
    void main() {
      runApp(
        MaterialApp(
          debugShowCheckedModeBanner: false,
          title: 'Flutter Demo',
          home: HomePage(),
        ),
      );
    }
    
    class HomePage extends HookWidget {
      @override
      Widget build(BuildContext context) {
        final _width = MediaQuery.of(context).size.width * .8;
        final _height = _width * .6;
        final _zoom = useState<double>(1.0);
        final _previousZoom = useState<double>(1.0);
        final _offset = useState<Offset>(Offset.zero);
        final _previousOffset = useState<Offset>(Offset.zero);
        final _startingFocalPoint = useState<Offset>(Offset.zero);
        return Scaffold(
          body: Container(
            color: Colors.black54,
            child: Center(
              child: GestureDetector(
                onScaleStart: (details) {
                  _startingFocalPoint.value = details.localFocalPoint;
                  _previousOffset.value = _offset.value;
                  _previousZoom.value = _zoom.value;
                },
                onScaleUpdate: (details) {
                  _zoom.value = _previousZoom.value * details.scale;
                  final Offset normalizedOffset =
                      (_startingFocalPoint.value - _previousOffset.value) /
                          _previousZoom.value;
                  _offset.value =
                      details.localFocalPoint - normalizedOffset * _zoom.value;
                },
                child: Stack(
                  children: [
                    CustomPaint(
                      painter: MyPainter(),
                      child: Container(width: _width, height: _height),
                    ),
                    ClipPath(
                      clipper: CustomClipperImage(),
                      child: Transform(
                        transform: Matrix4.identity()
                          ..translate(_offset.value.dx, _offset.value.dy)
                          ..scale(_zoom.value),
                        // ..rotateZ(_rotation.value),
                        child: Image.asset('images/milkyway.jpg',
                            width: _width, height: _height),
                      ),
                    ),
                  ],
                ),
              ),
            ),
          ),
        );
      }
    }
    
    class CustomClipperImage extends CustomClipper<Path> {
      @override
      getClip(Size size) {
        return Path()
          ..moveTo(size.width, 0)
          ..lineTo(0, size.height)
          ..lineTo(size.height, size.height)
          ..close();
      }
    
      @override
      bool shouldReclip(CustomClipper oldClipper) {
        return false;
      }
    }
    
    class MyPainter extends CustomPainter {
      @override
      void paint(Canvas canvas, Size size) {
        final path = Path()
          ..moveTo(size.width, 0)
          ..lineTo(0, size.height)
          ..lineTo(size.height, size.height)
          ..close();
        final paint = Paint()
          ..color = Colors.white
          ..strokeWidth = 8.0
          ..style = PaintingStyle.stroke;
        canvas.drawPath(path, paint);
      }
    
      @override
      bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
    }