Reputation: 33
how to make the widget SingleChildScrollView
postition on center between left triangle and righ triangle using global key?, it is possible ?, because i want scroll is not cliping
controller code (btw i using getx)
final GlobalKey leftTriangleKey = GlobalKey();
final GlobalKey rightTriangleKey = GlobalKey();
final GlobalKey scrollKey = GlobalKey();
void makeScrollInCenterTriangle() {
final leftTriangleRender =
leftTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final rightTriangleRender =
rightTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final scrollRender =
scrollKey.currentContext?.findRenderObject() as RenderBox?;
if (leftTriangleRender != null &&
rightTriangleRender != null &&
scrollRender != null) {
// Get the positions of the left and right triangles
final leftTrianglePosition =
leftTriangleRender.localToGlobal(Offset.zero);
final rightTrianglePosition =
rightTriangleRender.localToGlobal(Offset.zero);
// Calculate the width of the space between the triangles
final leftTriangleWidth = leftTriangleRender.size.width;
final rightTriangleWidth = rightTriangleRender.size.width;
final totalWidth = rightTrianglePosition.dx -
leftTrianglePosition.dx -
leftTriangleWidth -
rightTriangleWidth;
// Find the center position between the triangles
final scrollWidth = scrollRender.size.width;
final centerPosition = (leftTrianglePosition.dx + leftTriangleWidth) +
(totalWidth - scrollWidth) / 2;
// Position the scroll view in the center between the triangles
scrollController.position.jumpTo(centerPosition);
}
}
widget
return Obx(
() => Stack(
alignment: Alignment.center,
children: [
// Horizontal scroll
SingleChildScrollView(
key: controller.scrollKey,
scrollDirection: Axis.horizontal,
controller: controller.scrollController,
child: Row(
children: List.generate(topics.length, (index) {
return Obx(() {
final isActive = controller.activeTopicIndex.value ==
(index + controller.startIndex.value);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0.w),
child: ButtonIconAnim(
key: controller.getButtonKey(index),
height: 50.h,
width: isActive
? widthOpen
: controller.buttonWidth.value,
cardColor: isActive
? CardColors.tealPrimary
: CardColors.tealSecondary,
radius: BorderRadius.circular(10.r),
icon: Center(
child: TextNoAnim(
text: topics[index],
fontWeight: FontWeight.bold,
fontSize: fontSize,
cardColor: isActive
? CardColors.tealPrimary
: CardColors.tealSecondary,
maxLines: 1,
),
),
onTap: () {
controller.changeTopic(
index + controller.startIndex.value,
topics[index]);
},
),
);
});
}),
),
),
// Left Triangle (Previous Topics)
if (controller.itemsLeft.value > 0)
Positioned(
left: 20,
child: GestureDetector(
onTap: () => controller.scrollToPreviousTopics(),
child: LeftTriangle(
key: controller.leftTriangleKey,
color: CardColors.tealSecondary,
child: Text(
controller.itemsLeft.value.toString(),
style: TextStyle(
color: CardColors.tealSecondary.text,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Right Triangle (Next Topics)
if (controller.itemsRight.value > 0)
Positioned(
right: 20,
child: GestureDetector(
onTap: () => controller.scrollToNextTopics(),
child: RightTriangle(
key: controller.rightTriangleKey,
color: CardColors.tealSecondary,
child: Text(
controller.itemsRight.value.toString(),
style: TextStyle(
color: CardColors.tealSecondary.text,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
full code controller
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../pages/dummy/dummy.dart';
class NavigationMinigameController extends GetxController {
// Active topic index
RxInt activeTopicIndex = 0.obs;
// Current PageView index
RxInt currentPageIndex = 0.obs;
// Set the selected topic
RxString selectedTopic = "Operations\nAnd Algebra".obs;
var itemsLeft = 0.obs;
var itemsRight = 0.obs;
double itemWidth = 50;
final int visibleItem = 4;
final threshold = 50;
var buttonWidth = 50.w.obs;
var buttonOpenWidth = 100.w.obs;
var lastItemWidth = 50.w.obs;
var firstItemWidth = 50.w.obs;
final GlobalKey leftTriangleKey = GlobalKey();
final GlobalKey rightTriangleKey = GlobalKey();
final GlobalKey scrollKey = GlobalKey();
final GlobalKey sizedboxKey = GlobalKey();
List<GlobalKey> buttonKeys =
List.generate(topics.length, (index) => GlobalKey());
// List to track visible topics (only 3 visible at a time)
RxList<String> visibleTopics = <String>[].obs;
// Track the start index for visible topics
RxInt startIndex = 0.obs;
ScrollController scrollController = ScrollController();
// Method to change active topic
void changeTopic(int index, String topic) {
activeTopicIndex.value = index;
selectedTopic.value = topic;
}
// Update PageView index
void updatePageIndex(int index) {
currentPageIndex.value = index;
}
// Scroll to next set of topics
void scrollToNextTopics() {
// Always allow scrolling if there are more topics beyond current view
if (startIndex.value + visibleItem < topics.length) {
startIndex.value += 1;
visibleTopics.value = topics.sublist(startIndex.value,
(startIndex.value + visibleItem).clamp(0, topics.length));
// Recalculate items left and right
_updateHiddenItemCounts();
}
}
// Scroll to previous set of topics
void scrollToPreviousTopics() {
// Always allow scrolling if start index is greater than 0
if (startIndex.value > 0) {
startIndex.value -= 1;
visibleTopics.value = topics.sublist(startIndex.value,
(startIndex.value + visibleItem).clamp(0, topics.length));
// Recalculate items left and right
_updateHiddenItemCounts();
}
}
// Update hidden item counts
void _updateHiddenItemCounts() {
// Items on the left side
itemsLeft.value = startIndex.value;
// Items on the right side
itemsRight.value = topics.length - (startIndex.value + visibleItem);
}
GlobalKey getButtonKey(int index) {
return buttonKeys[index];
}
void makeScrollInCenterTriangle() {
final leftTriangleRender =
leftTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final rightTriangleRender =
rightTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final scrollRender =
scrollKey.currentContext?.findRenderObject() as RenderBox?;
if (leftTriangleRender != null &&
rightTriangleRender != null &&
scrollRender != null) {
// Get the positions of the left and right triangles
final leftTrianglePosition =
leftTriangleRender.localToGlobal(Offset.zero);
final rightTrianglePosition =
rightTriangleRender.localToGlobal(Offset.zero);
// Calculate the width of the space between the triangles
final leftTriangleWidth = leftTriangleRender.size.width;
final rightTriangleWidth = rightTriangleRender.size.width;
final totalWidth = rightTrianglePosition.dx -
leftTrianglePosition.dx -
leftTriangleWidth -
rightTriangleWidth;
// Find the center position between the triangles
final scrollWidth = scrollRender.size.width;
final centerPosition = (leftTrianglePosition.dx + leftTriangleWidth) +
(totalWidth - scrollWidth) / 2;
// Position the scroll view in the center between the triangles
scrollController.position.jumpTo(centerPosition);
}
}
// void _onScroll() {
// double currentOffset = scrollController.offset;
// //log("current offset $currentOffset");
//
// // Check if scrolling to the left or right
// if (currentOffset > 0) {
// // Scrolling right, adjust the last item width
// double distanceFromRightEdge =
// scrollController.position.maxScrollExtent - currentOffset;
// double newWidth = 50 - (distanceFromRightEdge / threshold) * 50;
// if (newWidth < 0) newWidth = 0; // Minimum width
// lastItemWidth.value = newWidth;
// log("last item update ${lastItemWidth.value}");
// } else if (currentOffset < 0) {
// // Scrolling ke kiri, perbarui lebar item pertama
// double distanceFromLeftEdge =
// scrollController.position.minScrollExtent - currentOffset;
// double newWidth = 50 - (distanceFromLeftEdge / threshold) * 50;
// if (newWidth < 0) newWidth = 0; // Lebar minimum
// firstItemWidth.value = newWidth;
// log("first item update ${firstItemWidth.value}");
// }
// }
void _scrollListener() {
scrollController.addListener(() {
// _onScroll();
// _detectTriangle();
if (scrollController.position.pixels <= 0) {
scrollToPreviousTopics(); // Trigger previous topic when scrolled to start
}
// Check if we've reached the end of the scroll
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent) {
scrollToNextTopics(); // Trigger next topic when scrolled to the end
}
});
}
@override
void onInit() {
// Add scroll listener to trigger next/previous topic logic
WidgetsBinding.instance.addPostFrameCallback((_) {
makeScrollInCenterTriangle();
// Initialize with first visibleItem items
visibleTopics.value = topics.sublist(0, visibleItem);
// Initial calculation of hidden items
_updateHiddenItemCounts();
_scrollListener();
});
super.onInit();
}
@override
void onClose() {
scrollController.removeListener(_scrollListener);
scrollController.dispose();
super.onClose();
}
}
full code widget
import 'package:custom_widget_component/core/widgets/button_shadow/const/card_colors.dart';
import 'package:custom_widget_component/core/widgets/button_shadow/example/left_triangle.dart';
import 'package:custom_widget_component/core/widgets/button_shadow/example/right_triangle.dart';
import 'package:custom_widget_component/features/mingame_navigation/presentation/controllers/navigation_minigame_controller.dart';
import 'package:custom_widget_component/features/mingame_navigation/presentation/pages/dummy/dummy.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import '../../../../../../core/widgets/button_shadow/card_component/text_no_anim.dart';
import '../../../../../../core/widgets/button_shadow/example/button_icon_anim.dart';
class TopicWidget extends StatelessWidget {
const TopicWidget({
super.key,
});
@override
Widget build(BuildContext context) {
final controller = Get.put(NavigationMinigameController());
return LayoutBuilder(
builder: (context, constraints) {
//double parentWidth = constraints.maxWidth;
double parentHeight = constraints.maxHeight;
// Ukuran proporsional
double height = parentHeight * 0.06; // 6% dari tinggi parent
double fontSize = parentHeight * 0.02; // 2% dari tinggi parent
double widthOpen = 100.w;
height = height.clamp(50.h, 50.h);
fontSize = fontSize.clamp(12.sp, 20.sp);
return Obx(
() => Stack(
alignment: Alignment.center,
children: [
// Horizontal scroll
SingleChildScrollView(
key: controller.scrollKey,
scrollDirection: Axis.horizontal,
controller: controller.scrollController,
child: Row(
children: List.generate(topics.length, (index) {
return Obx(() {
final isActive = controller.activeTopicIndex.value ==
(index + controller.startIndex.value);
return Padding(
padding: EdgeInsets.symmetric(horizontal: 2.0.w),
child: ButtonIconAnim(
key: controller.getButtonKey(index),
height: 50.h,
width: isActive
? widthOpen
: controller.buttonWidth.value,
cardColor: isActive
? CardColors.tealPrimary
: CardColors.tealSecondary,
radius: BorderRadius.circular(10.r),
icon: Center(
child: TextNoAnim(
text: topics[index],
fontWeight: FontWeight.bold,
fontSize: fontSize,
cardColor: isActive
? CardColors.tealPrimary
: CardColors.tealSecondary,
maxLines: 1,
),
),
onTap: () {
controller.changeTopic(
index + controller.startIndex.value,
topics[index]);
},
),
);
});
}),
),
),
// Left Triangle (Previous Topics)
if (controller.itemsLeft.value > 0)
Positioned(
left: 20,
child: GestureDetector(
onTap: () => controller.scrollToPreviousTopics(),
child: LeftTriangle(
key: controller.leftTriangleKey,
color: CardColors.tealSecondary,
child: Text(
controller.itemsLeft.value.toString(),
style: TextStyle(
color: CardColors.tealSecondary.text,
fontWeight: FontWeight.bold,
),
),
),
),
),
// Right Triangle (Next Topics)
if (controller.itemsRight.value > 0)
Positioned(
right: 20,
child: GestureDetector(
onTap: () => controller.scrollToNextTopics(),
child: RightTriangle(
key: controller.rightTriangleKey,
color: CardColors.tealSecondary,
child: Text(
controller.itemsRight.value.toString(),
style: TextStyle(
color: CardColors.tealSecondary.text,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
},
);
}
}
Upvotes: 1
Views: 57
Reputation: 1787
Yes, you can but there some issues in your code that may prevent it from working as you are expected. you have to use correct position offsets it means that the centerPosition
should be in the scrollable container's local coordinate space. and the jumpTo
methode should account for the content width of the SingleChildScrollView
.
void makeScrollInCenterTriangle() {
WidgetsBinding.instance.addPostFrameCallback((_) {
final leftTriangleRender =
leftTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final rightTriangleRender =
rightTriangleKey.currentContext?.findRenderObject() as RenderBox?;
final scrollRender =
scrollKey.currentContext?.findRenderObject() as RenderBox?;
if (leftTriangleRender != null &&
rightTriangleRender != null &&
scrollRender != null) {
// Get the global positions of the left and right triangles
final leftTrianglePosition =
leftTriangleRender.localToGlobal(Offset.zero).dx;
final rightTrianglePosition =
rightTriangleRender.localToGlobal(Offset.zero).dx;
// Get the scrollable container's width and global position
final scrollWidth = scrollRender.size.width;
final scrollPosition = scrollRender.localToGlobal(Offset.zero).dx;
// Calculate the center position between the triangles
final centerPosition =
((rightTrianglePosition + leftTrianglePosition) / 2) - scrollPosition;
// Scroll to center
final targetOffset = scrollController.offset +
centerPosition -
(scrollWidth / 2); // Adjust to the scroll container's center
scrollController.animateTo(
targetOffset.clamp(
scrollController.position.minScrollExtent,
scrollController.position.maxScrollExtent,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
}
});
}
make sure to call makeScrollInCenterTriangle()
during initialzation or after any layout changes that may affect the position.
if you had any question feel free to ask.
Edit
Based on what you said in the comment part for making a centered scroll position in SingleChildScrollView
where the items align like the second image. you can use ListView
with PageView
or ScrollController
. in my opinion use ScrollController
and calculate the position for centering the item. and make sure scrolling stops exact in the center using scrollPhysics
.
A sample code of what I said:
import 'package:flutter/material.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: CenteredScrollView(),
),
);
}
}
class CenteredScrollView extends StatefulWidget {
@override
_CenteredScrollViewState createState() => _CenteredScrollViewState();
}
class _CenteredScrollViewState extends State<CenteredScrollView> {
late ScrollController _scrollController;
double itemWidth = 100.0; // Width of each item
double triangleWidth = 50.0; // Width of left/right triangle padding
@override
void initState() {
super.initState();
_scrollController = ScrollController(
initialScrollOffset: triangleWidth, // Start with offset matching left triangle
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// Top scrollable items
Expanded(
child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal,
physics: PageScrollPhysics(), // Snap items to center
itemCount: 10, // Example item count
itemBuilder: (context, index) {
return Center(
child: Container(
width: itemWidth,
margin: const EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
color: Colors.teal[100 * (index % 9)],
child: Text(
'$index',
style: TextStyle(fontSize: 20, color: Colors.white),
),
),
);
},
),
),
// Bottom triangles
Padding(
padding: const EdgeInsets.only(top: 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
IconButton(
icon: Icon(Icons.arrow_left, size: 50),
onPressed: () {
// Scroll left
_scrollController.animateTo(
_scrollController.offset - itemWidth,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
IconButton(
icon: Icon(Icons.arrow_right, size: 50),
onPressed: () {
// Scroll right
_scrollController.animateTo(
_scrollController.offset + itemWidth,
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
);
},
),
],
),
),
],
);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
}
Happy coding...
Upvotes: 0