Ifqy Gifha azhar
Ifqy Gifha azhar

Reputation: 33

How to make the widget singlechildscrollview position on center beetween left triangle and right triangle using Globalkey Flutter

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

screenshot widget

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

Answers (1)

Amirali Eric Janani
Amirali Eric Janani

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

Related Questions