Reputation: 61
I want to display text below the item when the item is selected, however, it seems unusual, the text will be displayed centered relative to the image above and displayed full meaning it overflows the space of the 2 items next to it. So is there any way I can fulfill this request?
Upvotes: 0
Views: 75
Reputation: 61
I want to update my current solution, I'm using CustomPainter to draw a list of items and draw text below the item selected.
With the result I have attached, the problem I have when drawing the image is when the image data is successfully retrieved but the image is not displayed on UI until I click on the item to execute setState, the content is displayed after. Code to get image data and Draw Images on canvas:
// Load the network image using NetworkImage class
NetworkImage(imageUrl).resolve(ImageConfiguration.empty).addListener(
ImageStreamListener(
(ImageInfo imageInfo, bool synchronousCall) {
// Once the image is loaded, draw it onto the canvas
if (synchronousCall) {
paintImage(
canvas: canvas,
rect: rect,
image: imageInfo.image,
fit: BoxFit.cover, // Adjust the fit as needed
);
}
},
onError: (exception, stackTrace) {},
onChunk: (event) {},
),
);
Full code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: BrandList(),
);
}
}
class BrandList extends StatelessWidget {
final List<String> imageUrls = [
'https://images.squarespace-cdn.com/content/v1/5ede2122e582b96630a4a73e/1609429780069-GXY5R5AZWUEO8LFIISZR/Chanel+Logo+2021.png',
'https://99designs-blog.imgix.net/blog/wp-content/uploads/2022/05/image10-3-e1659037365697.png?auto=format&q=60&fit=max&w=930',
'https://images.squarespace-cdn.com/content/v1/5ede2122e582b96630a4a73e/1609429780069-GXY5R5AZWUEO8LFIISZR/Chanel+Logo+2021.png',
'https://99designs-blog.imgix.net/blog/wp-content/uploads/2022/05/image10-3-e1659037365697.png?auto=format&q=60&fit=max&w=930',
'https://images.squarespace-cdn.com/content/v1/5ede2122e582b96630a4a73e/1609429780069-GXY5R5AZWUEO8LFIISZR/Chanel+Logo+2021.png',
'https://99designs-blog.imgix.net/blog/wp-content/uploads/2022/05/image10-3-e1659037365697.png?auto=format&q=60&fit=max&w=930',
'https://images.squarespace-cdn.com/content/v1/5ede2122e582b96630a4a73e/1609429780069-GXY5R5AZWUEO8LFIISZR/Chanel+Logo+2021.png',
'https://99designs-blog.imgix.net/blog/wp-content/uploads/2022/05/image10-3-e1659037365697.png?auto=format&q=60&fit=max&w=930',
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Custom Horizontal List'),
),
body: BrandSelectableList(
imageUrls: imageUrls,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
),
);
}
}
class BrandSelectableList extends StatefulWidget {
final List<String> imageUrls;
final EdgeInsets padding;
final int itemSpacing;
final int itemSize;
final int borderWidth;
final Color? borderColor;
final Color? borderColorSelected;
final double height;
final TextStyle? textStyle;
const BrandSelectableList({
super.key,
required this.imageUrls,
this.borderColor,
this.borderWidth = 1,
this.padding = EdgeInsets.zero,
this.itemSize = 64,
this.itemSpacing = 24,
this.height = 92,
this.borderColorSelected,
this.textStyle,
});
@override
_BrandSelectableListState createState() => _BrandSelectableListState();
}
class _BrandSelectableListState extends State<BrandSelectableList> {
final ScrollController _scrollController = ScrollController();
int selectedIndex = -1;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: EdgeInsets.only(top: 2.0, left: widget.padding.left, right: widget.padding.right),
scrollDirection: Axis.horizontal,
controller: _scrollController,
child: SizedBox(
height: widget.height,
child: GestureDetector(
onTapDown: (details) {
int index = (details.localPosition.dx / (widget.itemSize + widget.itemSpacing)).floor();
setState(() {
selectedIndex = index;
});
},
child: CustomPaint(
size: Size(
widget.itemSize * widget.imageUrls.length +
widget.itemSpacing * (widget.imageUrls.length - 1),
widget.height,
),
painter: SingleSelectedListPainter(
imageUrls: widget.imageUrls,
selectedIndex: selectedIndex,
itemWidth: widget.itemSize,
itemSpacing: widget.itemSpacing,
padding: widget.padding,
borderWidth: widget.borderWidth,
borderColor: widget.borderColor,
borderColorSelected: widget.borderColorSelected,
height: widget.height,
),
),
),
),
);
}
}
class SingleSelectedListPainter extends CustomPainter {
final int selectedIndex;
final List<String> imageUrls;
final EdgeInsets padding;
final int itemSpacing;
final int itemWidth;
final int borderWidth;
final Color? borderColor;
final Color? borderColorSelected;
final double height;
final TextStyle? textStyle;
SingleSelectedListPainter({
required this.imageUrls,
required this.selectedIndex,
required this.padding,
required this.itemSpacing,
required this.itemWidth,
required this.borderWidth,
this.borderColor,
this.borderColorSelected,
required this.height,
this.textStyle,
});
@override
void paint(Canvas canvas, Size size) {
Path clipPath = Path();
for (int i = 0; i < imageUrls.length; i++) {
double startX = i * (itemWidth + itemSpacing).toDouble();
Rect itemRect =
Rect.fromPoints(Offset(startX, 0), Offset(startX + itemWidth, itemWidth.toDouble()));
drawNetworkImage(canvas, clipPath, itemRect, imageUrls[i], i);
if (selectedIndex == i) {
drawTextBelowImage(canvas, itemRect, "Selected item at index $i", i);
}
}
}
void drawTextBelowImage(Canvas canvas, Rect rect, String text, int index) {
final size = (TextPainter(
maxLines: 1,
text: TextSpan(
text: text,
style: const TextStyle(color: Colors.black, fontSize: 11.0, fontWeight: FontWeight.w600),
),
textAlign: TextAlign.center,
textDirection: TextDirection.ltr,
)..layout())
.size;
TextPainter textPainter = TextPainter(
text: TextSpan(
text: text,
style: const TextStyle(color: Colors.black, fontSize: 11.0, fontWeight: FontWeight.w600),
),
textAlign: index == 0
? TextAlign.start
: index == imageUrls.length - 1
? TextAlign.end
: TextAlign.center,
textDirection: index == imageUrls.length - 1 ? TextDirection.rtl : TextDirection.ltr,
);
textPainter.layout(maxWidth: size.width < 72 ? rect.width : size.width);
// Position the text below the circle image
double textY = height - textPainter.height;
double textX = index == 0
? rect.left
: index == imageUrls.length - 1
? rect.left - textPainter.width + itemWidth
: rect.center.dx - textPainter.width / 2;
textPainter.paint(canvas, Offset(textX, textY));
}
void drawNetworkImage(Canvas canvas, Path clipPath, Rect rect, String imageUrl, int index) {
final size = Size(rect.width, rect.height);
canvas.save();
Paint borderPaint = Paint()
..color =
index == selectedIndex ? borderColorSelected ?? Colors.black : borderColor ?? Colors.grey
..style = PaintingStyle.stroke
..strokeWidth = borderWidth.toDouble();
canvas.drawCircle(
Offset((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2),
size.width / 2,
borderPaint,
);
/// Clip the canvas to draw the image within the circle
clipPath.addOval(
Rect.fromCircle(
center: Offset((rect.left + rect.right) / 2, (rect.top + rect.bottom) / 2),
radius: size.width / 2,
),
);
canvas.clipPath(
clipPath,
doAntiAlias: false,
);
// Load the network image using NetworkImage class
NetworkImage(imageUrl).resolve(ImageConfiguration.empty).addListener(
ImageStreamListener(
(ImageInfo imageInfo, bool synchronousCall) {
// Once the image is loaded, draw it onto the canvas
if (synchronousCall) {
paintImage(
canvas: canvas,
rect: rect,
image: imageInfo.image,
fit: BoxFit.cover, // Adjust the fit as needed
);
}
},
onError: (exception, stackTrace) {},
onChunk: (event) {},
),
);
canvas.restore();
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}
Upvotes: 0
Reputation: 14885
Try below code it can hope it helps to you. Refer ValueNotifier and ValueListenableBuilder for changing the state. Change Text and color of container as per your need.
Note: You can use setState for changing the state as well.
class ExampleApp extends StatelessWidget {
ExampleApp({Key? key}) : super(key: key);
ValueNotifier<int> selectedContainer = ValueNotifier<int>(-1);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SizedBox(
height: 100,
child: ListView.builder(
itemCount: 10,
shrinkWrap: true,
scrollDirection: Axis.horizontal,
itemBuilder: (context, index) {
return ValueListenableBuilder(
valueListenable: selectedContainer,
builder: (context, selectedIndex, child) {
return GestureDetector(
onTap: () {
selectedContainer.value = index;
},
child: Column(
children: [
Container(
height: 60,
width: 60,
margin: EdgeInsets.symmetric(horizontal: 5),
decoration: BoxDecoration(
color: Colors.grey,
shape: BoxShape.circle,
border: Border.all(
color: selectedContainer.value == index
? Colors.red
: Colors.transparent)),
),
SizedBox(height: 15),
selectedContainer.value == index
? Text(
'Index - $index',
textAlign: TextAlign.center,
)
: SizedBox(),
],
),
);
},
);
},
),
),
),
);
}
}
Result Screens: Before item selected
After Item selected
Upvotes: 0