Foggy day
[Flutter] Image Widget - 사용법 본문
이번 포스팅에서는 Image Widget에 대해 알아보겠습니다.
각각의 항목에 설명과 이미지를 첨부했고, 페이지 하단에는 전체 코드를 올렸습니다. 내용이 다소 길고 모든 것을 표현하기 어려운 부분이 있었습니다. 궁금한 점 있으면 댓글로 문의해 주시기 바랍니다.
1. BoxFit
2. Alignment
3. width, height
4. opacity
5. cacheWidth, cacheHeight
6. color, colorBlendMode
7. filterQuality
8. loadingBuilder, frameBuilder
1. BoxFit
BoxFit enum 클래스에는 7가지 타입이 있습니다.
- contain : 이미지의 비율을 유지하면서 가능한한 크게 그려집니다. (기본값)
- fill : 이미지가 가진 비율을 무시하고 Image 위젯의 사이즈를 꽉 채웁니다.
- cover : 이미지의 비율을 유지하면 이미지 위젯의 영역을 꽉 채웁니다. 하지만 비율을 유지하면서 꽉 채워야 하기 때문에 이미지가 확대되는 현상이 나타납니다.(이미지 위젯의 사이즈에 따라서 상하좌우가 잘려 보일 수 있습니다)
- fitWidth : 이미지의 비율을 유지하면서 이미지 위젯의 가로 너비에 맞춰 그립니다. 이미지가 확대되면 상하좌우가 잘려 보일 수 있습니다.
- fitHeight : 이미지의 비율을 유지하면서 이미지 위젯의 세로 너비에 맞춰 그립니다. 이미지가 확대되면 상하좌우가 잘려보일 수 있습니다.
- none : 이미지의 크기는 조정하지 않고, 이미지를 중앙정렬합니다. 이미지의 해상도에 따라서 상당히 많이 잘려보일 수 있기 때문에 거의 사용되지 않습니다.
- scaleDown : 이미지를 중앙정렬하고, 가능한한 이미지를 축소합니다.
Widget _body() {
return Container(
color: Colors.grey.shade600,
width: 300,
height: 300,
child: Center(
child: Image.asset(
_assetPath,
fit: BoxFit.cover,
height: 200,
width: 200,
),
),
);
}
2. alignment
Alginment는 이미지 위젯 내에서 이미지를 어디로 배치시킬지를 결정합니다. Alignment에는 9개의 타입이 있으며 각 타입은 이름 이름 그대로 해당 포지션을 기준으로 이미지가 배치됩니다. 기본값은 center입니다(topLeft, topCenter, topRight, centerLeft, center, centerRight, bottomLeft, bottomCenter, bottomRight).
아래 이미지를 보면 BoxFit.cover로 지정하고, 3가지 타입을 비교했습니다. cover로 이미지가 확대되고, Alignment를 통해 어느 포지션부터 배치할지 결정하고 있습니다.
3. width, height
이미지 위젯에도 가로, 세로 값을 지정해줄 수 있습니다. 그런데 SizedBox/Container에 가로 세로 값을 지정해 주고, child로 Image를 사용한다면 Image의 width와 height값이 적용되지 않습니다. 부모인 Layout위젯의 사이즈만큼 채워야(fill) 하기 때문입니다. 그래서 Column이나 Row위젯 혹은 Center 같은 위젯들로 한번 감싸주는 것이 필요합니다. Layout위젯의 사이즈를 입력하지 않는다면 문제없습니다. 입력할 경우 Image의 가로세로 값이 적용되지 않는 것입니다.
적용 안되는 경우
Widget _body() {
return Container(
color: Colors.grey.shade600,
width: 300,
height: 300,
child: Image.asset(
_assetPath,
height: 200,
width: 200,
),
);
}
적용되는 경우
Widget _body() {
return Container(
color: Colors.grey.shade600,
width: 300,
height: 300,
child: Center(
child: Image.asset(
_assetPath,
height: 200,
width: 200,
),
),
);
}
그런데 지금 이미지 사이즈를 가로 200, 세로 200을 주었는데 가로가 더 긴 것을 볼 수 있습니다. 이미지 위젯의 사이즈는 200x200이지만 이미지 자체가 가로가 더 길기 때문에 나타난 현상입니다. 이러한 문제를 해결하기 위해서는 fit 특성을 사용해야 합니다.
4. opacity
이미지의 투명도를 설정할 수 있습니다. Image위젯의 opacity특성을 사용하는 것이 다른 위젯을 추가적으로 사용하는 것보다 더 효율적입니다.
https://api.flutter.dev/flutter/widgets/Image/opacity.html
5. cacheWidth, cacheHeight
cacheWidth, cacheHeight 특성은 Image Widget의 layout size는 지키면서 이미지 자체는 입력한 크기로 디코딩시킵니다.
6. color, colorBlendMode
color와 colorBlendMode는 함께 사용하는 특성입니다. 이미지와 입력한 컬러를 특정 방법으로 혼합할 수 있습니다.
다양한 BlendMode가 있으니 필요에 따라서 골라 사용하시면 됩니다.
7. filterQuality
FilterQuality는 이미지의 퀄리티를 개선시키는데 도움을 줄 수 있습니다. 공식 문서에 따르면 이미지가 이미 높은 해상도를 가지고 있고, 퀄리티가 좋다면 FilterQuality.none이 더 좋을 수도 있습니다. 하지만 이미지의 품질이 낮은 경우 FilterQuality 사용해서 품질 개선효과를 낼 수도 있습니다. FilterQuality에는 none, low, medium, high가 있습니다.
8. loadingBuilder, frameBuilder
frameBuilder 함수는 해당 이미지 위젯의 생성을 담당하는 빌더입니다. 해당 특성을 사용해서 이미지에 애니메이션 효과를 추가할 수 있습니다. context, child, frame, wasSynchronouslyLoaded 4개의 특성을 받을 수 있는데, 비동기적으로 로드 처리가 끝나면 wasSynchronouslyLoaded값이 true로 전달됩니다. frame은 이미지가 생성돼서 로드되기 전에는 null로 전달되고, 작업이 끝나면 0으로 변경됩니다. 이를 활용하여 Placeholder나 Animation 효과를 줄 수 있습니다.
loadingBuilder함수는 이미지의 바이트 로드 과정을 알려줍니다. 프린트를 출력해 보면 total 바이트와 현재 로드된 바이트 값을 볼 수 있습니다. 이를 활용해서 이미지 로드의 퍼센트를 구할 수 있습니다.
ImageChunkEvent#152d6(cumulativeBytesLoaded: 24575, expectedTotalBytes: 4871124)
ImageChunkEvent#c3036(cumulativeBytesLoaded: 57343, expectedTotalBytes: 4871124)
ImageChunkEvent#90956(cumulativeBytesLoaded: 114688, expectedTotalBytes: 4871124)
ImageChunkEvent#6d981(cumulativeBytesLoaded: 360446, expectedTotalBytes: 4871124)
ImageChunkEvent#3f7de(cumulativeBytesLoaded: 458750, expectedTotalBytes: 4871124)
ImageChunkEvent#f6412(cumulativeBytesLoaded: 516095, expectedTotalBytes: 4871124)
ImageChunkEvent#fa56a(cumulativeBytesLoaded: 609279, expectedTotalBytes: 4871124)
예제에서 사용한 이미지는 인터넷의 이미지를 사용한 것이기 때문에 유효하지 않을 수도 있습니다.
final String _networkImage =
"https://media.cntraveler.com/photos/639c6b27fe765cefd6b219b7/4:3/w_7696,h_5772,c_limit/Switzerland_GettyImages-1293043653.jpg";
int _imageProgress = 0;
bool _imageLoaded = false;
Widget _network() {
return Image.network(
_networkImage,
loadingBuilder: (context, child, loadingProgress) {
var lp = loadingProgress;
if (_imageProgress != 100 &&
lp != null &&
lp.expectedTotalBytes != null) {
_imageProgress =
((lp.cumulativeBytesLoaded / lp.expectedTotalBytes!) * 100)
.toInt();
}
if (_imageLoaded) {
_imageProgress = 100;
}
return Column(
children: [
Text(
_imageProgress.toString(),
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
child,
],
);
},
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
_imageLoaded = wasSynchronouslyLoaded;
if (wasSynchronouslyLoaded) return child;
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(seconds: 5),
curve: Curves.fastLinearToSlowEaseIn,
child: child,
);
},
);
}
Full Code
import 'package:flutter/material.dart';
class ImageScreen extends StatefulWidget {
const ImageScreen({Key? key}) : super(key: key);
@override
State<ImageScreen> createState() => _ImageScreenState();
}
class _ImageScreenState extends State<ImageScreen>
with SingleTickerProviderStateMixin {
final String _assetPath = "assets/image/leaf.jpg";
final String _networkImage =
"https://media.cntraveler.com/photos/639c6b27fe765cefd6b219b7/4:3/w_7696,h_5772,c_limit/Switzerland_GettyImages-1293043653.jpg";
bool showImage = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("ImageScreen"),
),
body: SizedBox(
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
"Colors.blueAccent\nBlendMode.lighten",
textAlign: TextAlign.center,
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() {
showImage = !showImage;
});
},
child: Text("이미지 사용")),
if (showImage) _network(),
],
),
),
);
}
int _imageProgress = 0;
bool _imageLoaded = false;
Widget _network() {
return Image.network(
_networkImage,
loadingBuilder: (context, child, loadingProgress) {
var lp = loadingProgress;
if (_imageProgress != 100 &&
lp != null &&
lp.expectedTotalBytes != null) {
_imageProgress =
((lp.cumulativeBytesLoaded / lp.expectedTotalBytes!) * 100)
.toInt();
}
if (_imageLoaded) {
_imageProgress = 100;
}
return Column(
children: [
Text(
_imageProgress.toString(),
style: const TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
child,
],
);
},
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
_imageLoaded = wasSynchronouslyLoaded;
if (wasSynchronouslyLoaded) return child;
return AnimatedOpacity(
opacity: frame == null ? 0 : 1,
duration: const Duration(seconds: 5),
curve: Curves.fastLinearToSlowEaseIn,
child: child,
);
},
);
}
Widget _image() {
return Container(
color: Colors.grey.shade600,
width: 300,
height: 300,
child: Center(
child: Image.asset(
_assetPath,
fit: BoxFit.cover,
height: 200,
width: 200,
alignment: Alignment.center,
cacheWidth: 1000,
cacheHeight: 300,
opacity: const AlwaysStoppedAnimation(1.0),
color: Colors.blueAccent,
colorBlendMode: BlendMode.lighten,
filterQuality: FilterQuality.high,
),
),
);
}
}
'Flutter > Flutter widget' 카테고리의 다른 글
[Flutter] ListView - 사용법 (2) | 2023.03.23 |
---|---|
[Flutter] SingleChildScrollView - 사용법 (0) | 2023.03.22 |
[Flutter] Text Widget - 사용법 (0) | 2023.03.21 |
[Flutter] PopupMenuButton - 사용법 (0) | 2023.03.21 |
[Flutter] Row - 사용법 (0) | 2023.03.20 |