개요
Flutter에서는 상당히 많은 기본 위젯들을 제공해준다.
Container, Column, Row, Stack, ListView 등등... 많은 기본 위젯들을 제공해준다.
기본 위젯들을 잘 조합하여 새로운 위젯을 만들 수 있지만, 때로는 기본 위젯의 조합으로도 만들 수 없는 위젯들을 만들어야할 때가 있다. ▼
이럴 때는 우리가 따로 정의하여 사용을 해야하는데, 이를 도와주는 것이 `CustomPaint`다.
CustomPaint를 통해 직접 위젯의 모양을 정의하여 새로운 형태의 위젯을 만들 수 있다.
들어가기 앞서 : 기본적인 좌표계에 대해서
들어가기 앞서 좌표계에 대해 알아야 한다.
이는 `CustomPaint`에서만 사용되는 것은 아니고 앱 전반적으로 다 사용되는 내용이다.
우리가 배웠던 좌표계는 아마 아래와 같은 형태일 것이다. ▼
가로는 x, 세로는 y로 두고 오른쪽과 위로 갈 수록 양으로 향하는 좌표계가 그동안 학교에서 배웠던 좌표계일 것이다.
하지만 모바일 앱에서의, 모바일 앱 뿐만 아니라 프로그래밍에서의, 좌표는 이와 다르다.
가로와 세로가 각각 x, y인 것은 같으나, 좌측 상단에서 좌표계가 시작한다.
x좌표의 경우 오른쪽으로 가면 증가한다는 것은 동일하지만, y좌표의 경우 위가 아닌 아래로 가면 증가한다. ▼
이는 모바일 앱에 일반적인, 우리가 아는 전통적인 좌표계를 설정하게 되면 생기는 여러 문제점들을 해결하기 위해 도입된 좌표계이다.
전통적인 좌표계를 사용하게 되면 기기 정 가운데부터 (0, 0)으로 잡아야 하는데, 이렇게 되면 모든 기기들마다 정 가운데의 좌표가 달라지며 설정하기도 어려워진다.
가로가 500px인 기기의 경우, 픽셀로 따졌을 때 정 중앙이 250px이 되지만, 가로가 300px인 기기의 경우 정 중앙이 150px이 되면서 계산이 복잡해진다.
하지만 좌측 상단부터 시작하게 되면 어느 기기든 할 것 없이 오른쪽과 아래로 기기의 크기만큼만 움직일 수 있게 정의되기에 계산이 더 편해진다.
그래서 CustomPaint로 좌표를 설정하여 위젯을 그리게 될 때 전통적인 좌표계가 아니라 위와 같은 좌측 상단부터 시작하는 좌표계를 생각하고 설정해야한다.
CustomPainter의 주요 메소드
CustomPainter로 새로운 형태의 위젯을 만들기 위해서는 아래와 같이 CustomPainter클래스를 상속해야한다.
상속하고 나면 2개의 메소드를 필수로 구현해야한다.
하나는 `paint`, 다른 하나는 `shouldRepaint` 메소드이다. ▼
class CutomPaintTest extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
throw UnimplementedError();
}
}
shouldRepaint 메소드
`shouldRepaint` 메소드는 위젯이 다시 호출될 때 이를 다시 렌더링 할 지 안할지를 정하는 메소드이다.
`true`가 반환되면 다시 렌더링을, `false`가 반환되면 렌더링을 다시 하지 않는다.
아래와 같이 확인을 하여 다시 그리기도 하고, 별 다른 조건없이 계속 다시 그리게 할 예정이라면 `true`를 반환하면 된다. ▼
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return old.myParameter != this.myParameter;
}
paint 메소드
`paint` 메소드는 이름 그대로 어떻게 위젯이 그려질지를 정하는 메소드이다.
매개변수로 들어오는 `canvas`를 이용하여 화면에 어떻게 그려질 지를 입력한다. ▼
@override
void paint(Canvas canvas, Size size) {
Paint leftPaint = Paint()
..color = Colors.black
..strokeWidth = 1
..style = PaintingStyle.fill;
canvas.drawPath(getLeftPath(size.width, size.height), leftPaint);
}
기본적으로 제공되는 프로퍼티들을 채워 어떻게 그려질 지를 정할 수 있다.
기본적인 프로퍼티들 몇개를 알아보자.
color
이름 그대로 색상을 정의한다.
`Color` 클래스의 인스턴스를 받는다.
style
`PaintingStyle enum`을 받는다.
`PaintingStyle enum`에는 `fill`과 `stroke` 두 개의 `enum`만 있다.
`fill`은 공간을 채워주고, `stroke`는 공간을 채우지 않고 테두리만 표현한다.
strokeWidth
`strokeWidth`는 테두리 굵기를 정하며 `double`형을 받는다.
`style`이 `PaintingStyle.fill`로 설정되어있으면 동작하지 않는다.
그 외에도 많은 프로퍼티들이 있지만 하나하나 다 설명하기에는 그 양이 너무나도 많기에 나머지는 공식문서를 참고하는 것을 권장한다.
만든 CustomPaint 사용하기
그렇다면 열심히 만든 CustomPaint는 어떻게 사용할까?
'그냥 위젯으로 넣으면 되는거 아닌가?'
될 거 같지만 안된다.
우리가 만든 CustomPaint 클래스는 Widget을 상속한 게 아니기 때문이다.
그래서 우리는 이를 `Widget`으로 바꿔야하는데, 이를 도와주는 것이 `CustomPaint`라는 위젯이다.
아래와 같이 넣어주면 Widget의 형태로 사용할 수 있다. ▼
CustomPaint(
child: CustomPaintTest()
)
canvas의 메소드
기본적으로 제공되는 메소드들
canvas에는 기본적으로 제공되는 메소드들이 많다.
선, 사각형, 원 등등 많은 요소들을 제공해준다. ▼
canvas.drawLine(); // 선을 그린다.
canvas.drawRect(); //사각형을 그린다.
canvas.drawCircle(); // 원을 그린다.
canvas.drawArc(); // 원호를 그린다.
canvas.drawImage(); // 이미지를 그린다.
canvas.drawImageNine(); //나인패치 이미지를 그린다.
canvas.drawParagraph(); //텍스트 문단을 그린다.
canvas.drawPath(); // 주어진 경로를 따라 그린다.
나인패치(nine-patch) 이미지란?
나인패치 이미지는 안드로이드에서 해상도 문제로 이미지가 깨지는 문제를 방지하기 위해 만든 이미지다.
확대/축소 되는 부분과 그대로 사용되는 부분을 나누어 만들어진 이미지 파일로, 아래의 문서를 참고하여 제작할 수 있다. ▼
아래와 같이 간단하게 사용할 수 있다. ▼
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: CustomPaint(
painter: CutomPaintTest(),
),
),
),
);
}
}
class CutomPaintTest extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()..color = Colors.black;
canvas.drawCircle(Offset.zero, 20, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
위의 코드를 실행하면 아래와 같이 나온다.
200 * 200의 파란색 컨테이너라는 캔버스 위에, `Offset(0, 0)`의 위치에 `radius`가 20인 검정색 원을 그렸기에 아래와 같이 나온다. ▼
함수마다 요구하는 것이 조금씩 다르기에 잘 확인하고 사용해야 한다.
drawPath()
앞의 메소드들도 중요하지만, 이번에 중점적으로 볼 것은 `drawPath` 메소드다.
기본적으로 제공되는 메소드들은 사각형을 그린다거나, 원을 그린다와 같은 `Container`로 해결할 수 있는 것들인데, `drawPath`는 그보다 더 한 것들을 만들 수 있게 도와준다.
예를 들면 이런 모양을 만들 때 사용할 수 있다. ▼
`Container`와 `Stack`을 통해 잘 겹치면 저 모양을 표현할 수는 있지만 터치 반경을 조절하거나 할 때 문제가 생긴다.
겹쳐서 만든 것은 그 영역이 실제 영역이 아니라 눈에만 그렇게 보이기 때문이다.
하지만 `CustomPaint`로 만들게 되면 실제로 렌더링 되는 부분이 보이는 영역과 같아지므로 위의 문제에서 자유로워진다.
그러면 `drawPath`에는 어떤 것들을 매개변수로 받는지 알아보자.
1. Path
`drawPath`가 받는 첫번째 매개변수는 `Path`다.
`drawPath`가 그릴 경로를 담고 있는 `Path` 클래스 인스턴스를 넘겨줘야 한다.
그렇다면 Path에 뭐가 있는지를 알아볼 차례다.
Path에 있는 메소드들 중에서 기본적인 부분만 알아보자.
조금 더 심화적인 내용을 원한다면 아래의 공식 문서를 참고하는 것을 추천한다.
이번 포스트에는 `moveTo`, `lineTo`, `arcToPoint`, `arcTo` 메소드들을 보겠다.
moveTo(double x, double y)
첫번째로 `moveTo()` 메소드다.
이름 그대로 그리는 점을 이동시킨다.
`moveTo()`를 실행하지 않으면 기본적으로 `(0, 0)`에서 시작하게 된다.
lineTo(double x, double y)
다음은 `lineTo()` 메소드다.
선을 그어주는 메소드로, 현재 위치부터 매개변수로 넣어준`x`와 `y` 좌표까지 직선을 긋는다.
만약에 `moveTo()`나 `lineTo()` 메소드를 전혀 실행시키지 않고 `lineTo(20, 20)` 메소드를 실행하면, `(0,0)` 부터 `(20, 20)`까지 선을 긋게 된다. ▼
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1;
Path path = Path()..lineTo(20, 20);
canvas.drawPath(path, paint);
}
Q. 화면에 그려지지 않아요. OR 화면에 그리려면 어떻게?
A. 두번째 매개변수인 `Paint`를 넣어줘야 렌더링이 제대로 된다.
허나 Paint에 설정을 조금 해줘야한다.
Paint에 대해서는 조금 뒤에 배울 것이니 임시방편으로 아래의 코드를 넣으면 된다. ▼
Paint paint = Paint() ..color = Colors.black ..style = PaintingStyle.stroke ..strokeWidth = 1;
arcToPoint(Offset arcEnd, {Radius radius = Radius.zero, double rotation = 0.0, bool largeArc = false, bool clockwise = true})
`arcToPoint` 메소드는 시작 지점(현재 지점)에서 명시된 끝 지점까지 호를 그린다.
`clockwise`가 `true`면 시계 방향, false면 반시계 방향으로 호를 그린다.
`largeArc`는 기본적으로 `false`로 설정되어있는데, 이를 `true`로 바꾸면 360도에서 우리가 지정한 각도를 뺀 나머지를 그리게 된다.
아래와 같이 코드를 작성하면 그림과 같이 호를 그려준다. ▼
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1;
Path path = Path()
..moveTo(0, 50)
..arcToPoint(
const Offset(50, 0),
radius: const Radius.circular(50),
clockwise: false,
);
canvas.drawPath(path, paint);
}
`largeArc`를 `true`로 해놓으면 아래와 같이 나온다. ▼
arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo)
`arcTo`메소드는 시작하는 지점과 끝나는 지점을 명시한 뒤, 시작 각도와 끝 각도를 명시하여 호를 그리는 메소드다.
`arcToPoint` 메소드와는 다르게 원을 그리는 메소드이므로, 타원의 형태나 곡선을 그리길 원했다면 알맞지 않을 수 있다.
아래와 같이 코드를 작성하면 그림과 같이 호를 그려준다. ▼
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..style = PaintingStyle.stroke
..strokeWidth = 1;
Path path = Path()
..moveTo(0, 50)
..arcTo(
Rect.fromCenter(
center: const Offset(50, 50), width: 100, height: 100),
1 * pi,
0.5 * pi,
false);
canvas.drawPath(path, paint);
}
2. Paint
위에서 `Paint` 클래스에 대해서 설명을 해놓았다.
`Paint` 클래스의 인스턴스를 넣어주면 정의한 `Paint`대로, `path`를 따라 렌더링해준다.
티켓 예제
사실 설명만 들으면 이게 뭔지... 싶을 수 있다.
그래서 전에 만든 티켓 예제를 첨부했다. ▼
class Coupon extends StatelessWidget {
Coupon({
super.key,
this.width = 280,
this.height = 90,
this.radius,
});
double width;
double height;
double? radius = 8;
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: CustomPaint(
painter: CouponPaint(
leftSideColor: Colors.red,
radius: 8,
rightSideColor: Colors.black,
),
),
);
}
}
class CouponPaint extends CustomPainter {
const CouponPaint(
{required this.radius,
required this.leftSideColor,
required this.rightSideColor});
final double radius;
final Color leftSideColor;
final Color rightSideColor;
@override
void paint(Canvas canvas, Size size) {
Paint leftPaint = Paint()
..color = leftSideColor
..strokeWidth = 1
..style = PaintingStyle.fill;
Paint rightPaint = Paint()
..color = rightSideColor
..strokeWidth = 1
..style = PaintingStyle.fill;
canvas.drawPath(getLeftPath(size.width, size.height), leftPaint);
canvas.drawPath(getRightPath(size.width, size.height), rightPaint);
}
Path getLeftPath(double x, double y) {
Path path = Path()
..moveTo(0, radius)
..lineTo(0, y / 3)
..arcToPoint(Offset(0, y * 2 / 3), radius: Radius.circular(y / 6))
..lineTo(0, y - radius)
..arcToPoint(Offset(radius, y),
radius: Radius.circular(radius), clockwise: false)
..lineTo(y, y)
..lineTo(y, 0)
..lineTo(radius, 0)
..arcToPoint(Offset(0, radius),
radius: Radius.circular(radius), clockwise: false)
..close();
return path;
}
Path getRightPath(double x, double y) {
Path path = Path()
..moveTo(y, 0)
..lineTo(x - radius, 0)
..arcToPoint(Offset(x, radius), radius: Radius.circular(radius))
..lineTo(x, y / 3)
..arcToPoint(Offset(x, y * 2 / 3),
radius: Radius.circular(y / 6), clockwise: false)
..lineTo(x, y - radius)
..arcToPoint(Offset(x - radius, y), radius: Radius.circular(radius))
..lineTo(y, y)
..close();
return path;
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
`Path`를 하나씩 따라가면서 어떻게 그려질 지 확인하면 좀 더 쉽게 이해할 수 있다. ▼
마치며
이번에는 간단하게 CustomPaint를 통해 위젯을 새롭게 만드는 법을 알아보았다.
처음에는 굉장히 복잡한데, moveTo와 lineTo 메소드를 사용하면서 간단하게 여러 위젯들을 만들어보면 점점 별 거 아니었다는 생각이 들 것이다.
적절한 계산으로 너비와 높이에 따라 비율 조정하는 부분도 필요하지만 이 부분은 위의 두 메소드를 사용하다보면 익힐 것이라고 충분히 생각이 든다.
'Develop > Flutter' 카테고리의 다른 글
[Flutter] Dart는 싱글 스레드 언어 (0) | 2024.08.04 |
---|---|
[Flutter] Dart의 컴파일 과정 (0) | 2024.08.04 |
[Flutter] 패키지 사용법 (0) | 2024.04.01 |
[Flutter][Widget] Row, Column 위젯 (0) | 2024.03.24 |
[Flutter][Widget] Container 위젯 그리고 Container의 크기 설정 (0) | 2024.03.20 |