모바일 애플리케이션에서 이미지를 부분적으로 보여주고, 사용자가 클릭하면 확장하여 전체 이미지를 보여주는 기능은 자주 사용됩니다. 이를 효과적으로 구현하려면 이미지의 크기를 계산하고, 레이아웃을 조정하는 복잡한 작업이 필요할 수 있습니다.
이 글에서는 이런 복잡함을 간소화하고, 쉽게 재사용할 수 있는 ImageExpandableWidget을 만드는 방법을 소개하겠습니다. 이 위젯은 이미지를 기본적으로 부분적으로 보여주고, 사용자가 버튼을 클릭하면 전체 이미지를 보여줍니다.
이미지 부분 표시 및 전체 확장: 위젯은 이미지의 일부분만 보여주다가, 확장 시 전체 이미지를 표시합니다.
커스터마이징 가능한 확장 버튼: 확장 버튼의 텍스트, 아이콘, 스타일 등을 커스터마이징할 수 있습니다.
그라데이션 효과: 이미지 하단에 그라데이션을 추가하여 이미지가 부분적으로 가려져 있다는 시각적 힌트를 제공합니다.
확장 버튼 대신 커스터마이징된 위젯 제공: 필요에 따라 기본 제공 확장 버튼 대신, 커스터마이징된 위젯을 사용할 수 있습니다.
import 'package:flutter/material.dart';
class ImageExpandableWidget extends StatefulWidget {
final String imageUrl; // 더 일반적인 이미지 URL 필드
final double initialMaxHeight;
final double gradientHeight;
final Color gradientColor;
final String expandText;
final Widget? expandWidget; // 확장 버튼 대신 사용할 수 있는 커스터마이징된 위젯
final TextStyle? expandTextStyle;
final Icon? expandIcon;
final double imageBorderRadius;
const ImageExpandableWidget({
super.key,
required this.imageUrl, // 이미지 URL만 필요하도록 간소화
this.initialMaxHeight = 1075,
this.gradientHeight = 210,
this.gradientColor = Colors.white,
this.expandText = "더보기",
this.expandTextStyle,
this.expandIcon,
this.expandWidget,
this.imageBorderRadius = 6.0,
});
@override
State<ImageExpandableWidget> createState() => _ImageExpandableWidgetState();
}
class _ImageExpandableWidgetState extends State<ImageExpandableWidget> {
final GlobalKey _imageKey = GlobalKey();
late double maxHeight;
late ImageProvider imageProvider;
bool isExpanded = false; // 확장 여부를 내부 상태로 관리
@override
void initState() {
super.initState();
maxHeight = widget.initialMaxHeight;
imageProvider = NetworkImage(widget.imageUrl);
}
void _getImageSize() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final RenderBox? renderBox =
_imageKey.currentContext?.findRenderObject() as RenderBox?;
if (renderBox != null && renderBox.size.height <= maxHeight) {
setState(() {
maxHeight = renderBox.size.height;
isExpanded = true;
});
}
});
}
void _toggleExpand() {
setState(() {
isExpanded = !isExpanded;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Stack(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(widget.imageBorderRadius),
child: Container(
height: isExpanded ? null : maxHeight,
child: isExpanded
? Image(
image: imageProvider,
fit: BoxFit.fitWidth,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
return child;
} else {
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
(loadingProgress.expectedTotalBytes ?? 1)
: null,
),
);
}
},
)
: OverflowBox(
alignment: Alignment.topCenter,
maxHeight: double.infinity,
child: Image(
image: imageProvider,
fit: BoxFit.fitWidth,
key: _imageKey,
loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) {
_getImageSize();
return child;
} else {
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
(loadingProgress.expectedTotalBytes ?? 1)
: null,
),
);
}
},
),
),
),
),
if (!isExpanded)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
height: widget.gradientHeight,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
widget.gradientColor.withOpacity(0.0),
widget.gradientColor,
],
),
),
),
),
],
),
if (!isExpanded)
widget.expandWidget ??
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: InkWell(
onTap: _toggleExpand, // 내부에서 상태 전환
child: Container(
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(vertical: 14),
width: double.maxFinite,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(widget.imageBorderRadius),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
widget.expandText,
style: widget.expandTextStyle ??
Theme.of(context).textTheme.bodyLarge,
),
widget.expandIcon ??
const Icon(Icons.keyboard_arrow_down_sharp),
],
),
),
),
),
],
);
}
}
ImageExpandableWidget은 기본적으로 이미지의 일부분만 보여주고, 사용자가 더보기 버튼을 누르면 전체 이미지를 보여줍니다. 이미지는 OverflowBox를 사용하여 잘리는 부분을 처리하고, 확장되면 원래 크기의 이미지를 표시합니다.
이미지가 로드 중일 때 로딩 인디케이터를 표시하고, 이미지가 로드된 후에만 표시하는 방식으로 loadingBuilder를 사용합니다.
이미지의 하단에 그라데이션을 추가하여 사용자가 이미지가 확장될 수 있다는 시각적 힌트를 줍니다. 이 그라데이션은 사용자가 원하는 대로 높이와 색상을 조절할 수 있습니다.
기본 확장 버튼 대신 expandWidget을 통해 사용자가 완전히 커스터마이징된 확장 위젯을 제공할 수 있습니다. 이를 통해 확장 버튼에 대한 완벽한 제어가 가능합니다.
ImageExpandableWidget을 사용하여 간단하게 이미지 확장 기능을 추가할 수 있습니다. 기본 확장 버튼과 스타일을 사용할 수도 있고, 필요에 따라 커스터마이징할 수도 있습니다.
import 'package:flutter/material.dart';
import 'package:your_package/image_expandable_widget.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ImageExpandableWidget(
imageUrl: 'https://example.com/sample-image.jpg',
initialMaxHeight: 500,
gradientHeight: 150,
gradientColor: Colors.black.withOpacity(0.5),
expandText: "더보기",
expandTextStyle: TextStyle(color: Colors.blue),
expandIcon: Icon(Icons.expand_more),
expandWidget: ElevatedButton(
onPressed: () {},
child: Text('더 많은 이미지 보기'),
), // 커스터마이징된 확장 위젯 사용 가능
imageBorderRadius: 8.0,
),
);
}
}
ImageExpandableWidget을 사용하면 Flutter 앱에서 간편하게 이미지 확장 기능을 구현할 수 있습니다. 사용자는 확장 버튼, 그라데이션, 이미지 모서리 둥글기 등 다양한 옵션을 커스터마이징할 수 있으며, 필요에 따라 기본 제공 확장 버튼 대신 자신만의 위젯을 사용할 수도 있습니다.
이 위젯은 이미지가 잘리는 효과를 제공하며, 확장 후에는 전체 이미지를 보여줍니다. 앱에서 이미지 갤러리나 상세 설명을 보여줄 때 유용하게 사용할 수 있습니다.