Flutter二维滚动点击无法停止及相关功能问题求助
Flutter 3.13二维滚动问题解决方案
针对你遇到的点击无法停止滚动、无滚动加速效果、无法添加ScrollController这三个问题,以下是具体解决思路和代码修改方案:
1. 核心问题根源
当前实现使用默认的ScrollableDetails,未绑定iOS风格的滚动物理效果,导致无法触发滚动减速动画和点击停止行为;同时未通过ScrollableDetails传入自定义控制器,因此无法使用ScrollController控制滚动。
2. 修改后的完整代码
// Copyright (c) 2019, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), useMaterial3: true, ), scrollBehavior: const MaterialScrollBehavior().copyWith( dragDevices: PointerDeviceKind.values.toSet(), ), debugShowCheckedModeBanner: false, home: const MyHomePage(title: 'Flutter Demo Home Page'), ); } } class MyHomePage extends StatefulWidget { final String title; const MyHomePage({ Key? key, required this.title, }) : super(key: key); @override State<MyHomePage> createState() => _MyHomePageState(); } class _MyHomePageState extends State<MyHomePage> { // 定义垂直、水平滚动控制器 final ScrollController _verticalController = ScrollController(); final ScrollController _horizontalController = ScrollController(); @override void dispose() { // 销毁控制器避免内存泄漏 _verticalController.dispose(); _horizontalController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(widget.title)), body: TwoDimensionalGridView( diagonalDragBehavior: DiagonalDragBehavior.free, // 配置垂直滚动详情:绑定控制器+iOS物理效果 verticalDetails: ScrollableDetails.vertical( controller: _verticalController, physics: const BouncingScrollPhysics(), ), // 配置水平滚动详情:绑定控制器+iOS物理效果 horizontalDetails: ScrollableDetails.horizontal( controller: _horizontalController, physics: const BouncingScrollPhysics(), ), delegate: TwoDimensionalChildBuilderDelegate( maxXIndex: 999, maxYIndex: 999, builder: (BuildContext context, ChildVicinity vicinity) { return Container( color: vicinity.xIndex.isEven && vicinity.yIndex.isEven ? Colors.amber[50] : (vicinity.xIndex.isOdd && vicinity.yIndex.isOdd ? Colors.purple[50] : null), height: 200, width: 200, child: Center( child: Text( 'Row ${vicinity.xIndex}: Column ${vicinity.yIndex}')), ); }), ), ); } } class TwoDimensionalGridView extends TwoDimensionalScrollView { const TwoDimensionalGridView({ super.key, super.primary, super.mainAxis = Axis.vertical, super.verticalDetails = const ScrollableDetails.vertical(), super.horizontalDetails = const ScrollableDetails.horizontal(), required TwoDimensionalChildBuilderDelegate delegate, super.cacheExtent, super.diagonalDragBehavior = DiagonalDragBehavior.none, super.dragStartBehavior = DragStartBehavior.start, super.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, super.clipBehavior = Clip.hardEdge, }) : super(delegate: delegate); @override Widget buildViewport(BuildContext context, ViewportOffset verticalOffset, ViewportOffset horizontalOffset) { return SimpleBuilderTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalDetails.direction, verticalOffset: verticalOffset, verticalAxisDirection: verticalDetails.direction, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildBuilderDelegate, cacheExtent: cacheExtent, clipBehavior: clipBehavior, ); } } class SimpleBuilderTableViewport extends TwoDimensionalViewport { const SimpleBuilderTableViewport({ super.key, required super.verticalOffset, required super.verticalAxisDirection, required super.horizontalOffset, required super.horizontalAxisDirection, required TwoDimensionalChildBuilderDelegate super.delegate, required super.mainAxis, super.cacheExtent, super.clipBehavior = Clip.hardEdge, }); @override RenderTwoDimensionalViewport createRenderObject(BuildContext context) { return RenderSimpleBuilderTableViewport( horizontalOffset: horizontalOffset, horizontalAxisDirection: horizontalAxisDirection, verticalOffset: verticalOffset, verticalAxisDirection: verticalAxisDirection, mainAxis: mainAxis, delegate: delegate as TwoDimensionalChildBuilderDelegate, childManager: context as TwoDimensionalChildManager, cacheExtent: cacheExtent, clipBehavior: clipBehavior, ); } @override void updateRenderObject( BuildContext context, RenderSimpleBuilderTableViewport renderObject) { renderObject ..horizontalOffset = horizontalOffset ..horizontalAxisDirection = horizontalAxisDirection ..verticalOffset = verticalOffset ..verticalAxisDirection = verticalAxisDirection ..mainAxis = mainAxis ..delegate = delegate ..cacheExtent = cacheExtent ..clipBehavior = clipBehavior; } } class RenderSimpleBuilderTableViewport extends RenderTwoDimensionalViewport { RenderSimpleBuilderTableViewport({ required super.horizontalOffset, required super.horizontalAxisDirection, required super.verticalOffset, required super.verticalAxisDirection, required TwoDimensionalChildBuilderDelegate delegate, required super.mainAxis, required super.childManager, super.cacheExtent, super.clipBehavior = Clip.hardEdge, }) : super(delegate: delegate); @override void layoutChildSequence() { final double horizontalPixels = horizontalOffset.pixels; final double verticalPixels = verticalOffset.pixels; final double viewportWidth = viewportDimension.width + cacheExtent; final double viewportHeight = viewportDimension.height + cacheExtent; final TwoDimensionalChildBuilderDelegate builderDelegate = delegate as TwoDimensionalChildBuilderDelegate; final int maxRowIndex; final int maxColumnIndex; maxRowIndex = builderDelegate.maxYIndex!; maxColumnIndex = builderDelegate.maxXIndex!; final int leadingColumn = math.max((horizontalPixels / 200).floor(), 0); final int leadingRow = math.max((verticalPixels / 200).floor(), 0); final int trailingColumn = math.min( ((horizontalPixels + viewportWidth) / 200).ceil(), maxColumnIndex, ); final int trailingRow = math.min( ((verticalPixels + viewportHeight) / 200).ceil(), maxRowIndex, ); double xLayoutOffset = (leadingColumn * 200) - horizontalOffset.pixels; for (int column = leadingColumn; column <= trailingColumn; column++) { double yLayoutOffset = (leadingRow * 200) - verticalOffset.pixels; for (int row = leadingRow; row <= trailingRow; row++) { final ChildVicinity vicinity = ChildVicinity(xIndex: column, yIndex: row); final RenderBox child = buildOrObtainChildFor(vicinity)!; child.layout(constraints.tighten(width: 200.0, height: 200.0)); parentDataOf(child).layoutOffset = Offset(xLayoutOffset, yLayoutOffset); yLayoutOffset += 200; } xLayoutOffset += 200; } final double verticalExtent = 200 * (maxRowIndex + 1); verticalOffset.applyContentDimensions( 0.0, clampDouble( verticalExtent - viewportDimension.height, 0.0, double.infinity), ); final double horizontalExtent = 200 * (maxColumnIndex + 1); horizontalOffset.applyContentDimensions( 0.0, clampDouble( horizontalExtent - viewportDimension.width, 0.0, double.infinity), ); } }
关键修改说明
- 将
MyHomePage改为StatefulWidget,创建并管理ScrollController实例,避免内存泄漏。 - 在
TwoDimensionalGridView的verticalDetails和horizontalDetails中传入自定义控制器和BouncingScrollPhysics,该物理效果会自动处理iOS上的滚动加速和点击停止滚动行为。 - 无需修改自定义的Viewport和RenderObject代码,
TwoDimensionalScrollView会自动将控制器与滚动偏移绑定。
内容的提问来源于stack exchange,提问作者user433575
相关产品推荐
相关产品推荐

