1

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!

Chris
  • 1,828
  • 6
  • 40
  • 108

1 Answers1

1

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;
}
Thierry
  • 7,775
  • 2
  • 15
  • 33
  • Awesome! But another thing: is there a way to move the image around / zoom the image in and out ? Inside the frame – Chris Feb 28 '21 at 11:05
  • And there should be a white border on the frame, is that possible with clip path? – Chris Feb 28 '21 at 11:06
  • border is as expected, awesome! But the scaling is not working for me? – Chris Feb 28 '21 at 22:06
  • >> *But the scaling is not working for me.* I'm not sure I understand what you want, in fact. – Thierry Feb 28 '21 at 22:09
  • fair enough :D I mean zooming into the picture, with gestures. Also moving it around inside that canvas. Does that make sense? – Chris Feb 28 '21 at 22:11
  • 1
    OIC. Check my update. It will probably require some tweaking. Good night! – Thierry Feb 28 '21 at 23:19
  • damn. Youre good :D thanks! will work on this next week and probably have some questions about it, cy – Chris Feb 28 '21 at 23:31
  • Hi Thierry, so I implemented your solution but there is another thing, when zooming, the image should remain its position and should only be scaled. any idea how this can be achieved? – Chris Mar 04 '21 at 11:21
  • 1
    Ooops, my bad, I was using `focalPoint` instead of `localFocalPoint`. That's why the image was slowly translating when zooming in and out. Check my update. – Thierry Mar 04 '21 at 12:09
  • good to know that! working as expected now, thanks again :) – Chris Mar 04 '21 at 12:14
  • any way to make this also draggable ?:D https://stackoverflow.com/questions/66474773/flutter-make-image-with-gesturedetectors-also-draggable – Chris Mar 04 '21 at 12:15