개요

OverflowBox는 부모로부터 받은 제약 조건과 다른 제약 조건을 자식에게 적용하는 위젯으로, 자식이 부모 영역을 벗어나도록 허용할 수 있습니다.

이 위젯은 자식이 부모의 크기 제한을 무시하고 더 크거나 작은 크기를 가질 수 있도록 할 때 유용합니다. 예를 들어, 작은 컨테이너 안에 큰 아이콘이나 이미지를 표시할 때 사용할 수 있습니다.

참조: https://api.flutter.dev/flutter/widgets/OverflowBox-class.html

언제 사용하나요?

  • 자식이 부모 크기를 초과해야 할 때
  • 팝업이나 툴팁처럼 부모 영역 밖으로 나가는 UI를 만들 때
  • 애니메이션 중 일시적으로 크기가 확장되는 효과를 만들 때
  • 부모 제약을 무시하고 고정 크기를 가져야 할 때
  • 오버레이 효과나 뱃지처럼 부모 경계를 넘는 요소를 만들 때

기본 사용법

// 부모보다 큰 자식 허용
Container({
  width: 100,
  height: 100,
  color: 'blue',
  child: OverflowBox({
    maxWidth: 200,
    maxHeight: 200,
    child: Container({
      width: 150,
      height: 150,
      color: 'red'
    })
  })
})

// 부모보다 작은 자식 강제
Container({
  width: 200,
  height: 200,
  color: 'green',
  child: OverflowBox({
    maxWidth: 100,
    maxHeight: 100,
    child: Container({
      color: 'yellow'
    })
  })
})

// 특정 크기로 고정
OverflowBox({
  minWidth: 150,
  maxWidth: 150,
  minHeight: 150,
  maxHeight: 150,
  child: Container({
    color: 'purple'
  })
})

Props

minWidth

값: number | undefined

자식에게 적용할 최소 너비 제약입니다.

undefined인 경우 부모의 최소 너비 제약을 사용합니다.

OverflowBox({
  minWidth: 100,  // 자식은 최소 100 너비
  child: child
})

maxWidth

값: number | undefined

자식에게 적용할 최대 너비 제약입니다.

undefined인 경우 부모의 최대 너비 제약을 사용합니다.

OverflowBox({
  maxWidth: 300,  // 자식은 최대 300 너비
  child: child
})

minHeight

값: number | undefined

자식에게 적용할 최소 높이 제약입니다.

undefined인 경우 부모의 최소 높이 제약을 사용합니다.

OverflowBox({
  minHeight: 50,  // 자식은 최소 50 높이
  child: child
})

maxHeight

값: number | undefined

자식에게 적용할 최대 높이 제약입니다.

undefined인 경우 부모의 최대 높이 제약을 사용합니다.

OverflowBox({
  maxHeight: 200,  // 자식은 최대 200 높이
  child: child
})

alignment

값: Alignment (기본값: Alignment.center)

자식이 부모와 다른 크기일 때 정렬 방법입니다.

OverflowBox({
  maxWidth: 200,
  maxHeight: 200,
  alignment: Alignment.topRight,  // 오른쪽 위로 정렬
  child: child
})

child

값: Widget | undefined

새로운 제약 조건이 적용될 자식 위젯입니다.

실제 사용 예제

예제 1: 플로팅 액션 버튼 뱃지

const FloatingActionButtonWithBadge = ({ count, onPressed }) => {
  return Container({
    width: 56,
    height: 56,
    child: Stack({
      children: [
        // FAB
        FloatingActionButton({
          onPressed,
          child: Icon(Icons.shopping_cart)
        }),
        // 뱃지 (부모 영역을 벗어남)
        Positioned({
          top: 0,
          right: 0,
          child: OverflowBox({
            maxWidth: 30,
            maxHeight: 30,
            child: Container({
              width: 24,
              height: 24,
              decoration: BoxDecoration({
                color: 'red',
                shape: BoxShape.circle,
                border: Border.all({ 
                  color: 'white', 
                  width: 2 
                })
              }),
              child: Center({
                child: Text(count.toString(), {
                  style: TextStyle({
                    color: 'white',
                    fontSize: 12,
                    fontWeight: 'bold'
                  })
                })
              })
            })
          })
        })
      ]
    })
  });
};

예제 2: 확대 미리보기

const ZoomPreview = ({ image, zoomLevel = 2.0 }) => {
  const [isHovering, setIsHovering] = useState(false);
  const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
  
  return GestureDetector({
    onHover: (event) => {
      setIsHovering(true);
      setMousePosition({ x: event.localPosition.x, y: event.localPosition.y });
    },
    onHoverExit: () => setIsHovering(false),
    child: Container({
      width: 300,
      height: 300,
      child: Stack({
        children: [
          // 원본 이미지
          Image({
            src: image,
            objectFit: 'cover'
          }),
          // 확대된 영역
          if (isHovering) Positioned({
            left: mousePosition.x - 50,
            top: mousePosition.y - 50,
            child: OverflowBox({
              maxWidth: 200,
              maxHeight: 200,
              child: Container({
                width: 100 * zoomLevel,
                height: 100 * zoomLevel,
                decoration: BoxDecoration({
                  border: Border.all({ 
                    color: 'white', 
                    width: 2 
                  }),
                  borderRadius: BorderRadius.circular(50),
                  boxShadow: [
                    BoxShadow({
                      color: 'rgba(0,0,0,0.3)',
                      blurRadius: 10,
                      spreadRadius: 2
                    })
                  ]
                }),
                child: ClipOval({
                  child: Transform.scale({
                    scale: zoomLevel,
                    child: Transform.translate({
                      offset: {
                        x: -mousePosition.x,
                        y: -mousePosition.y
                      },
                      child: Image({
                        src: image,
                        objectFit: 'cover'
                      })
                    })
                  })
                })
              })
            })
          })
        ]
      })
    })
  });
};

예제 3: 커스텀 툴팁

const CustomTooltip = ({ text, child }) => {
  const [isVisible, setIsVisible] = useState(false);
  
  return MouseRegion({
    onEnter: () => setIsVisible(true),
    onExit: () => setIsVisible(false),
    child: Stack({
      clipBehavior: Clip.none,
      children: [
        child,
        if (isVisible) Positioned({
          top: -40,
          left: 0,
          right: 0,
          child: OverflowBox({
            maxWidth: 300,  // 부모보다 넓은 툴팁 허용
            child: Container({
              padding: EdgeInsets.symmetric({ 
                horizontal: 12, 
                vertical: 8 
              }),
              decoration: BoxDecoration({
                color: 'rgba(0,0,0,0.8)',
                borderRadius: BorderRadius.circular(6),
                boxShadow: [
                  BoxShadow({
                    color: 'rgba(0,0,0,0.2)',
                    blurRadius: 4,
                    offset: { x: 0, y: 2 }
                  })
                ]
              }),
              child: Row({
                mainAxisSize: MainAxisSize.min,
                children: [
                  Icon({
                    icon: Icons.info,
                    size: 14,
                    color: 'white'
                  }),
                  SizedBox({ width: 6 }),
                  Text(text, {
                    style: TextStyle({
                      color: 'white',
                      fontSize: 12
                    })
                  })
                ]
              })
            })
          })
        })
      ]
    })
  });
};

예제 4: 펄스 애니메이션 효과

const PulseAnimation = ({ child, color = 'blue' }) => {
  const [scale, setScale] = useState(1.0);
  const [opacity, setOpacity] = useState(0.5);
  
  useEffect(() => {
    const interval = setInterval(() => {
      setScale(s => s === 1.0 ? 1.5 : 1.0);
      setOpacity(o => o === 0.5 ? 0.0 : 0.5);
    }, 1000);
    
    return () => clearInterval(interval);
  }, []);
  
  return Container({
    width: 100,
    height: 100,
    child: Stack({
      children: [
        // 펄스 효과 (부모보다 큼)
        Center({
          child: OverflowBox({
            maxWidth: 150,
            maxHeight: 150,
            child: AnimatedContainer({
              duration: Duration.milliseconds(1000),
              width: 100 * scale,
              height: 100 * scale,
              decoration: BoxDecoration({
                color: color,
                shape: BoxShape.circle,
                opacity: opacity
              })
            })
          })
        }),
        // 실제 콘텐츠
        Center({ child })
      ]
    })
  });
};

예제 5: 드롭다운 메뉴

const CustomDropdown = ({ options, value, onChange }) => {
  const [isOpen, setIsOpen] = useState(false);
  
  return Container({
    width: 200,
    height: 48,
    child: Stack({
      clipBehavior: Clip.none,
      children: [
        // 드롭다운 버튼
        GestureDetector({
          onTap: () => setIsOpen(!isOpen),
          child: Container({
            padding: EdgeInsets.symmetric({ horizontal: 16 }),
            decoration: BoxDecoration({
              color: 'white',
              border: Border.all({ color: '#E0E0E0' }),
              borderRadius: BorderRadius.circular(8)
            }),
            child: Row({
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                Text(value || 'Select...'),
                Icon({
                  icon: isOpen ? Icons.arrow_drop_up : Icons.arrow_drop_down
                })
              ]
            })
          })
        }),
        // 드롭다운 메뉴 (부모 너비를 초과할 수 있음)
        if (isOpen) Positioned({
          top: 52,
          left: 0,
          child: OverflowBox({
            maxWidth: 300,  // 옵션이 길어도 표시 가능
            child: Container({
              constraints: BoxConstraints({
                minWidth: 200  // 최소 부모 너비만큼
              }),
              decoration: BoxDecoration({
                color: 'white',
                borderRadius: BorderRadius.circular(8),
                boxShadow: [
                  BoxShadow({
                    color: 'rgba(0,0,0,0.15)',
                    blurRadius: 8,
                    offset: { x: 0, y: 4 }
                  })
                ]
              }),
              child: Column({
                crossAxisAlignment: CrossAxisAlignment.start,
                mainAxisSize: MainAxisSize.min,
                children: options.map((option, index) => 
                  GestureDetector({
                    onTap: () => {
                      onChange(option);
                      setIsOpen(false);
                    },
                    child: Container({
                      width: double.infinity,
                      padding: EdgeInsets.all(16),
                      decoration: BoxDecoration({
                        border: index > 0 
                          ? Border(top: BorderSide({ color: '#F0F0F0' }))
                          : null
                      }),
                      child: Text(option, {
                        style: TextStyle({
                          color: value === option ? '#1976D2' : '#333'
                        })
                      })
                    })
                  })
                )
              })
            })
          })
        })
      ]
    })
  });
};

크기 동작 이해

OverflowBox의 크기 결정

// OverflowBox 자체는 부모 제약을 따름
Container({
  width: 100,
  height: 100,
  color: 'blue',
  child: OverflowBox({
    maxWidth: 200,
    maxHeight: 200,
    child: Container({
      width: 150,
      height: 150,
      color: 'red'  // 파란 영역을 벗어나 보임
    })
  })
})

// OverflowBox 크기: 100x100 (부모 제약)
// 자식 크기: 150x150 (OverflowBox 제약)

제약 조건 상속

// 일부 제약만 변경
OverflowBox({
  maxWidth: 300,  // 너비만 변경
  // minWidth: 부모에서 상속
  // minHeight: 부모에서 상속
  // maxHeight: 부모에서 상속
  child: child
})

// 모든 제약 변경
OverflowBox({
  minWidth: 100,
  maxWidth: 200,
  minHeight: 50,
  maxHeight: 150,
  child: child
})

주의사항

  • OverflowBox는 자식이 부모 영역을 벗어나도록 허용하므로 레이아웃 문제를 일으킬 수 있습니다
  • 오버플로우된 영역은 클리핑되지 않으므로 다른 위젯과 겹칠 수 있습니다
  • Stack과 함께 사용할 때는 clipBehavior: Clip.none을 설정해야 합니다
  • 터치 이벤트는 부모 영역 내에서만 감지됩니다
  • 성능상 과도한 오버플로우는 피하는 것이 좋습니다

관련 위젯

  • UnconstrainedBox: 모든 제약을 제거하는 위젯
  • ConstrainedBox: 추가 제약을 적용하는 위젯
  • SizedBox: 고정 크기를 지정하는 위젯
  • LimitedBox: 무한 제약에서만 크기를 제한하는 위젯
  • FittedBox: 자식을 부모에 맞게 스케일링하는 위젯