I am building an app where you can browse various products. Each product also has a product detail page. On that product detail page, I want to display the product image, the price, a product description and also a comment section.
Because the product description can be quite long, I don't want to show the entire description right away to the user, but rather enable the user to be able to expand the description with a click on read more.
Below my product description is a comments section. The comments can expand to the top as well as to the bottom. The reason behind this is it is basically like a paginated site where you can load older comments and newer comments. I have solved this as described here.
However, because I have center key set, the product description does expand to the top when I click read more. That's the behavior I want to change.
At all time, the user should stay where he is on the screen. I do not want to use functions like jumpTo
to jump to the top when the product description expands up. I want to solve this problem properly and have the product description expanding down. How can I archive that?
Secondly, I also want to archive that when a user opens the page it starts at the top, with the product image, like every other page. What do I need to do in order to archive that goal too?
Here you can find an example of my code. I have added two floating buttons which simulate comments being added to the top and to the bottom: https://dartpad.dev/?id=486578f48833dd3d53b1f76080ac6f23
I am grateful for any kind of help!
I have completed the necessary work on your case and have devised a final solution.
The current code utilizes the center
key for the CustomScrollView
, causing the upper half to move upwards and the lower half to move downwards. This conflicts with the requirement for the product description to expand downwards.
Therefore, I propose removing the center
key and implementing a custom ScrollPhysics
class to maintain the position of the CustomScrollView
.
You must manually determine when to retain the scroll and when not to. The system cannot automatically recognize what is appropriate for your specific case. Please refer to the _shouldRetainScroll
usage for more information.
Also, with the removal of the center
key, the page will now start at the top when a user opens it.
Finally, the solution may not be perfect, but give it a go. I look forward to your response.
Below is the implemented code:
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
home: TestPage(),
));
}
class TestPage extends StatefulWidget {
const TestPage({super.key});
@override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
List<Widget> newList = List.generate(
20,
(index) => Text('Upper ${index.toString()}'),
);
List<Widget> myList = List.generate(
20,
(index) => Text('Lower ${index.toString()}'),
);
final Key centerKey = const ValueKey('second-sliver-list');
final scrollController = ScrollController();
final keyTop = GlobalKey();
final ValueNotifier<bool> _shouldRetainScroll = ValueNotifier(false);
@override
void dispose() {
scrollController.dispose();
_shouldRetainScroll.dispose();
super.dispose();
}
@override
void initState() {
super.initState();
}
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
String productDescription =
"Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal. Peanut butter, a creamy concoction crafted from roasted peanuts, is a timeless culinary delight cherished by millions worldwide. Its rich, nutty flavor and smooth texture make it a versatile ingredient that transcends traditional boundaries, finding its way into sandwiches, desserts, sauces, and even savory dishes. This delectable spread has captured the hearts and palates of food enthusiasts for generations, evolving from a humble pantry staple to a beloved icon of gastronomy. In this exploration, we delve deep into the captivating world of peanut butter, uncovering its history, culinary uses, nutritional benefits, and enduring appeal.";
return Scaffold(
floatingActionButton: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
FloatingActionButton.extended(
onPressed: () {
_shouldRetainScroll.value = true;
setState(() {
newList.add(Text('Upper ${newList.length}'));
});
},
label: const Text('Add to Upper'),
),
const SizedBox(height: 10),
FloatingActionButton.extended(
onPressed: () {
_shouldRetainScroll.value = false;
scrollController.jumpTo(scrollController.position.pixels + 0.000000001); //Quite tricky here. Purpose is force the PositionRetainedScrollPhysics recalculate the position;
setState(() {
myList.add(Text('Lower ${myList.length}'));
});
},
label: const Text('Add to Lower'),
),
],
),
appBar: AppBar(),
body: CustomScrollView(
//center: centerKey,
physics: PositionRetainedScrollPhysics(shouldRetainScroll: _shouldRetainScroll),
controller: scrollController,
slivers: [
SliverToBoxAdapter(
key: keyTop,
child: Column(
children: [
Image.network(
"https://m.media-amazon.com/images/I/719NQBiqh9L.jpg"),
SizedBox(height: 16.0),
Text(
'Product Name',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
SizedBox(height: 8.0),
Text(
'\$100', // Replace with actual price
style: TextStyle(fontSize: 16),
),
SizedBox(height: 16.0),
AnimatedContainer(
duration: Duration(milliseconds: 500),
curve: Curves.easeInOut,
height: _isExpanded
? null
: 85.0, // Adjust height to show around 3-4 lines
child: Text(
productDescription,
style: TextStyle(fontSize: 16),
overflow: TextOverflow.fade,
),
),
SizedBox(height: 16.0),
GestureDetector(
onTap: () {
_shouldRetainScroll.value = false;
setState(() {
_isExpanded = !_isExpanded;
});
},
child: Text(
_isExpanded ? 'Read less' : 'Read more',
style: TextStyle(color: Colors.blue),
),
),
],
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(
title: newList.reversed.toList()[index],
);
},
childCount: newList.length,
),
),
SliverList(
//key: centerKey,
delegate: SliverChildBuilderDelegate(
(context, index) {
return ListTile(title: myList[index]);
},
childCount: myList.length,
),
),
],
),
);
}
}
class PositionRetainedScrollPhysics extends ScrollPhysics {
final ValueNotifier<bool> shouldRetainScroll;
const PositionRetainedScrollPhysics({super.parent, required this.shouldRetainScroll});
@override
PositionRetainedScrollPhysics applyTo(ScrollPhysics? ancestor) {
return PositionRetainedScrollPhysics(
parent: buildParent(ancestor),
shouldRetainScroll: shouldRetainScroll
);
}
@override
double adjustPositionForNewDimensions({
required ScrollMetrics oldPosition,
required ScrollMetrics newPosition,
required bool isScrolling,
required double velocity,
}) {
final position = super.adjustPositionForNewDimensions(
oldPosition: oldPosition,
newPosition: newPosition,
isScrolling: isScrolling,
velocity: velocity,
);
final diff = newPosition.maxScrollExtent - oldPosition.maxScrollExtent;
if (oldPosition.pixels > oldPosition.minScrollExtent && diff > 0 && shouldRetainScroll.value == true) {
return position + diff;
} else {
return position;
}
}
}