flutter inner shadow

flunge·2021년 12월 7일
1

시계는 와치

목록 보기
1/5

프로젝트 디자인을 하다 테마가 Material design과 Neumorphic design의 중간 정도의 어떤것이 나오게 됐다.
생각하는 거의 모든 것이 구현 되어 있는 flutter라서 당연히 inner shadow도 어떻게 방법이 있을 줄 알았는데 없었다.


문제의 디자인

구현

CustomPaint의 painter속성에 그림자를 구현한 CustomPainter 객체를 주기로했다.

Container(
  height: height, //MediaQuery의 height*0.1값
  margin: const EdgeInsets.fromLTRB(20, 10, 20, 10),
  decoration: BoxDecoration(
    borderRadius: BorderRadius.only(
      topLeft: Radius.circular(height*0.25),
      bottomLeft: Radius.circular(height*0.25),
      topRight: Radius.circular(height*0.5),
      bottomRight: Radius.circular(height*0.5)
    ),
    color: Colors.green, 
  ),
),

Container(
  
  ...
  
  child: CustomPaint(
    painter: MyPainter(height: height),
    child: Container(
      margin: const EdgeInsets.all(8),
    ),
  ),
),

최상위 컨테이너 위젯의 child로 CustiomPaint위젯을 주고 painter속성에 MyPainter를 준다.

Path

그림자가 필요한 영역을 딴다.
우선 영역을 채울 Paint객체부터

class MyPainter extends CustomPainter{
  double height;

  MyPainter({required this.height});
  
  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    var paint = Paint()
      ..strokeWidth = 2
      ..style = ui.PaintingStyle.stroke;
      
    // path객체를 구현하는 부분
    
    canvas.drawPath(path, paint4);		
  }

  @override
  bool shouldRepaint(covariant CustomPainter 
  ...

}

테스트를 위해 선 형태의 페인트 객체를 생성


위젯의 왼쪽 위 부분

var path = Path()
  ..moveTo(0, height*0.25)
  ..addArc(
  Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
    degToRad(180), 
    degToRad(90)
  );

BorderRadius 값은 위의 이미지처럼 작은 원의 반지름 값을 가진다.
시작 점을 (0, radius)로 이동하고 1/4크기의 부채꼴을 그린다.
addArc의 2,3번째 인자는 시작 각도와 끝 각도인데 degree값이 아닌 radian값을 가지기 때문에 바꿔줘야한다.

왼쪽 구석에 부채꼴 모양으로 그려진 모습


처음 작은 부채꼴에서 그 다음 부채꼴까지 직선

var path = Path()
  ..moveTo(0, height*0.25)
  ..addArc(
      Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
        degToRad(180), 
        degToRad(90)
  )
  ..lineTo(size.width-height*0.5, 0);


선이 연장됐다.


위와 같은 방식으로 진행

var path = Path()
  ..moveTo(0, height*0.25)
  ..addArc(
      Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
        degToRad(180), 
        degToRad(90)
  )
  ..lineTo(size.width-height*0.5, 0)
  ..addArc(
      Rect.fromCircle(center: Offset(size.width-height/2, height/2), radius: height/2), 
        degToRad(270), 
        degToRad(90)
  );


절반 해냈다.
이제는 베지어 곡선을 이용한다.

현재 recent점까지 이동한 상황이고 quadraticBezierTo메소드에 (x1, y1), (x2, y2)좌표를 준다.

var path = Path()
  ..moveTo(0, height*0.25)
  ..addArc(
      Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
        degToRad(180), 
        degToRad(90)
  )
  ..lineTo(size.width-height*0.5, 0)
  ..addArc(
      Rect.fromCircle(center: Offset(size.width-height/2, height/2), radius: height/2), 
        degToRad(270), 
        degToRad(90)
  )
  ..quadraticBezierTo(size.width-7, 7, size.width-height/2, 7);


위의 선과 임의의 수 7만큼 거리를 뒀다. 이 다음 직선을 그리고

같은 방식으로 베지어 곡선을 그린다

이제 마지막 부분을 이어준다.
여기서 꼬리를 길게 이어주는 작업을 한다.

var path = Path()
  ..moveTo(0, height*0.25)
  ..addArc(
      Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
        degToRad(180), 
        degToRad(90)
  )
  ..lineTo(size.width-height*0.5, 0)
  ..addArc(
      Rect.fromCircle(center: Offset(size.width-height/2, height/2), radius: height/2), 
        degToRad(270), 
        degToRad(90)
  )
  ..quadraticBezierTo(size.width-7, 7, size.width-height/2, 7)
  ..lineTo(0, height*0.75)
  ..lineTo(0, height*0.25);


그림자의 테두리를 다 만들었다.

페인트 객체의 style을 stroke가 아닌 fill로 채운 모습
이제 이 영역을 그림자처럼 투명도를 낮추고 블러 효과를 주면된다.

var paint = Paint()
  ..color = Colors.black.withOpacity(0.25)
  ..maskFilter = MaskFilter.blur(BlurStyle.normal, convertRadiusToSigma(4))
  ..style = ui.PaintingStyle.fill;

페인트 객체의 maskFilter속성을 주면 되는데 MaskFilter.blur메소드의 두 번째 인자를 주목하면 저 값은 시그마(sigma)값을 필요로 한다. 이 시그마가 뭐냐면 가우시안 커널의 표준편차(sigma)를 의미한다.

double convertRadiusToSigma(double radius) {
  return radius * 0.57735 + 0.5;
}

Radius를 이용해서 값을 만든다. 이런 식으로 페인트 객체를 만들면

그럴싸한 그림자가 완성 됐다!

전체 코드

class MyPainter extends CustomPainter{
  double height;

  MyPainter({required this.height});

  @override
  void paint(ui.Canvas canvas, ui.Size size) {
    var paint = Paint()
      ..color = Colors.black.withOpacity(0.25)
      ..maskFilter = MaskFilter.blur(BlurStyle.normal, convertRadiusToSigma(4))
      ..style = ui.PaintingStyle.fill;

    var path = Path()
      ..moveTo(0, height*0.25)
      ..addArc(
        Rect.fromCircle(center: Offset(height*0.25, height*0.25), radius: height*0.25), 
        degToRad(180).toDouble(), 
        degToRad(90).toDouble()
      )
      ..lineTo(size.width-height*0.5, 0)
      ..addArc(
        Rect.fromCircle(center: Offset(size.width-height/2, height/2), radius: height/2), 
        degToRad(270).toDouble(), 
        degToRad(90).toDouble()
      )
      ..quadraticBezierTo(size.width-7, 7, size.width-height/2, 7)
      ..lineTo(height*0.25, 7)
      ..quadraticBezierTo(5, 7, 4, height*0.3)
      ..lineTo(0, height*0.75)
      ..lineTo(0, height*0.25);
  }

  double convertRadiusToSigma(double radius) {
    return radius * 0.57735 + 0.5;
  }

  double degToRad(double degree){
    return degree * (math.pi / 180);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    // TODO: implement shouldRepaint
    return false;
  }

}

마치며

CustiomPaint를 쓸 일이 있을까 싶었는데 쓰게 됐고 이번에 써보면서 이제 어떤 모양의 위젯이라도 만들 수 있을 것만 같은 자신감이 생겼다.

2개의 댓글

comment-user-thumbnail
2024년 2월 14일

이거 왠지 제가 필요한거같은 삘! 잘 보겠습니다 선댓글!

1개의 답글