如何在 SingleChildScrollView 中动态定位小部件并在 Flutter 中的位置之间进行动画处理

问题描述 投票:0回答:1

我目前正在开发一个 Flutter 项目,其中在 SingleChildScrollView 中有一堆小部件。这些小部件的位置是从现有小部件中检索的,并且由于它们位于 SingleChildScrollView 内,因此位置可以随着用户滚动而动态更改。

我创建了小部件层次结构的简化版本,其中有一个红色容器、一个蓝色容器和一个绿色容器,它们应根据某些条件与红色或蓝色容器重叠。我使用 AnimatedPositioned 来实现动画效果。

这是我的代码片段:

import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  bool move = false;

  final GlobalKey redContainerKey = GlobalKey();
  final GlobalKey blueContainerKey = GlobalKey();

  double? _topPosition;

  // Position of Strings
  Offset _getWidgetPosition(GlobalKey key) {
    RenderBox renderBox = key.currentContext!.findRenderObject() as RenderBox;
    return renderBox.localToGlobal(const Offset(0, 0));
  }

  void _getPosition() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      double topPosition = move
          ? _getWidgetPosition(redContainerKey).dy
          : _getWidgetPosition(blueContainerKey).dy;

      setState(() {
        _topPosition = topPosition;
      });

      print(topPosition);
    });
  }

  final _scrollController = ScrollController();

  // Scroll to Top
  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      duration: const Duration(milliseconds: 500),
      curve: Curves.linear,
    );
  }

  // Scroll to Bottom
  void _scrollToBottom() {
    Future.delayed(const Duration(milliseconds: 500), () {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 500),
        curve: Curves.linear,
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: SingleChildScrollView(
        controller: _scrollController,
        child: Stack(
          children: [
            Column(
              children: [
                Container(
                  key: redContainerKey,
                  color: Colors.red,
                  height: 50,
                  width: MediaQuery.of(context).size.width,
                ),
                Container(height: 700),
                Container(
                  key: blueContainerKey,
                  color: Colors.blue,
                  height: 50,
                  width: MediaQuery.of(context).size.width,
                ),
              ],
            ),
            AnimatedPositioned(
              top: _topPosition,
              duration: const Duration(seconds: 1),
              child: AnimatedContainer(
                duration: const Duration(seconds: 1),
                color: Colors.green,
                height: 50,
                width: MediaQuery.of(context).size.width,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            move = !move;
            if (move) {
              _scrollToTop();
            } else {
              _scrollToBottom();
            }
            _getPosition();
          });
        },
      ),
    );
  }
}

虽然这有效,但问题是绿色容器并不总是按预期与红色或蓝色容器重叠,特别是当屏幕因滚动而移动时。我正在寻找有关如何在 SingleChildScrollView 中无缝实现这种动态定位和动画效果的指导。任何帮助或建议将不胜感激!

flutter flutter-animation
1个回答
0
投票
  1. 引入键:我通过为堆栈中的小部件分配唯一的键来解决这个问题。此步骤对于动态参考他们的位置至关重要。
  2. 计算精确位置:为了实现无缝定位,我通过从目标控件的 y 位置减去堆栈当前的 y 位置来计算精确位置。必须考虑 AppBar 的高度等因素。
import 'package:flutter/material.dart';

class MyWidget extends StatefulWidget {
  const MyWidget({Key? key}) : super(key: key);

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  // Global Key
  final GlobalKey stackKey = GlobalKey();
  final GlobalKey redContainerKey = GlobalKey();
  final GlobalKey blueContainerKey = GlobalKey();

  // Flag to Indicate Type of Movement
  bool move = false;

  // Position (y) for Animated Positioned
  double? _topPosition;

  // Position of Widget
  Offset _getWidgetPosition(GlobalKey key) {
    RenderBox renderBox = key.currentContext!.findRenderObject() as RenderBox;
    return renderBox
        .localToGlobal(const Offset(0, -80)); // Account for AppBar height
  }

  // Retrieving Position of Widget
  void _getPosition() {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      double topPosition = move
          ? _getWidgetPosition(redContainerKey).dy
          : (_getWidgetPosition(blueContainerKey).dy -
              _getWidgetPosition(stackKey).dy);

      setState(() {
        _topPosition = topPosition;
      });
    });
  }

  // Scroll Controller
  final _scrollController = ScrollController();

  // Scroll to Top
  void _scrollToTop() {
    _scrollController.animateTo(
      0.0,
      duration: const Duration(milliseconds: 1000),
      curve: Curves.linear,
    );
  }

  // Scroll to Bottom
  void _scrollToBottom() {
    Future.delayed(const Duration(milliseconds: 500), () {
      _scrollController.animateTo(
        _scrollController.position.maxScrollExtent,
        duration: const Duration(milliseconds: 500),
        curve: Curves.linear,
      );
    });
  }

  // Scroll Notification
  bool handleScrollNotification(ScrollNotification notification) {
    if (notification is ScrollEndNotification) {
      _getPosition();
    }
    return true;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(),
      body: NotificationListener<ScrollNotification>(
        onNotification: handleScrollNotification,
        child: SingleChildScrollView(
          controller: _scrollController,
          child: Stack(
            key: stackKey,
            children: [
              Column(
                children: [
                  Container(
                    key: redContainerKey,
                    color: Colors.red,
                    height: 50,
                    width: MediaQuery.of(context).size.width,
                  ),
                  Container(height: 1000),
                  Container(
                    key: blueContainerKey,
                    color: Colors.blue,
                    height: 50,
                    width: MediaQuery.of(context).size.width,
                  ),
                  Container(height: 700), // Change this to observe desired behaviour
                ],
              ),
              AnimatedPositioned(
                top: _topPosition,
                duration: const Duration(seconds: 1),
                child: AnimatedContainer(
                  duration: const Duration(seconds: 1),
                  color: Colors.green,
                  height: 50,
                  width: MediaQuery.of(context).size.width,
                ),
              ),
            ],
          ),
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            move = !move;
          });
          if (move) {
            _scrollToTop();
          } else {
            _scrollToBottom();
          }
          _getPosition();
        },
      ),
    );
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.