ListView内可拖拽元素的拖放垂直偏移错误问题
解决ListView中Draggable元素放下时的垂直偏移问题
嘿,我完全懂你遇到的这个拖拽偏移糟心事——手动减100px确实是个临时凑活的办法,根本不是长久之计。让我帮你揪出问题根源,给你一个靠谱的解决方案!
问题到底出在哪?
你遇到的垂直偏移,本质是坐标系统不匹配导致的:
Draggable的onDraggableCanceled回调返回的offset是全局屏幕坐标(以整个设备屏幕左上角为原点);- 而
Positioned组件的top/left属性是相对于父容器Stack的局部坐标(以Stack的左上角为原点)。
当ListView滚动时,Stack的位置会相对于屏幕产生偏移,这时候直接把全局坐标赋值给Positioned的top,就会把ListView的滚动偏移也包含进去,自然就导致元素放下时位置偏下了。
正确解决方案:全局坐标转局部坐标
我们需要把回调里的全局坐标,转换为相对于Stack容器的局部坐标。可以通过RenderBox的globalToLocal方法轻松实现,具体修改如下:
修改DragBoxState中的onDraggableCanceled回调逻辑:
onDraggableCanceled: (velocity, offset) { // 获取当前DragBox所在父级Stack的渲染对象 final RenderBox stackRenderBox = context.findAncestorRenderObjectOfType<RenderBox>()!; // 将全局屏幕坐标转换为Stack的局部坐标 final Offset localOffset = stackRenderBox.globalToLocal(offset); setState(() { position = localOffset; }); }
完整修正后的代码
import 'package:flutter/material.dart'; void main() => runApp(new MyApp()); class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( appBar: AppBar( title: Text("Drag and drop bug"), ), body: App(), ), ); } } class App extends StatefulWidget { @override AppState createState() => AppState(); } class AppState extends State<App> { Color caughtColor = Colors.grey; Container spacer({double height: 400, Color color: Colors.blue}) { return Container( color: color, height: height, ); } @override Widget build(BuildContext context) { return ListView( children: <Widget>[ spacer(color: Colors.amber), Container( height: 400, child: Stack( children: <Widget>[ DragBox(Offset(0.0, 0.0), 'Box One', Colors.blueAccent), DragBox(Offset(200.0, 0.0), 'Box Two', Colors.orange), DragBox(Offset(300.0, 0.0), 'Box Three', Colors.lightGreen), ], ), ), spacer(color: Colors.cyan) ], ); } } class DragBox extends StatefulWidget { final Offset initPos; final String label; final Color itemColor; DragBox(this.initPos, this.label, this.itemColor); @override DragBoxState createState() => DragBoxState(); } class DragBoxState extends State<DragBox> { Offset position = Offset(0.0, 0.0); @override void initState() { super.initState(); position = widget.initPos; } @override Widget build(BuildContext context) { return Positioned( left: position.dx, top: position.dy, child: Draggable( data: widget.itemColor, child: Container( width: 100.0, height: 100.0, color: widget.itemColor, child: Center( child: Text( widget.label, style: TextStyle( color: Colors.white, decoration: TextDecoration.none, fontSize: 20.0, ), ), ), ), onDraggableCanceled: (velocity, offset) { // 获取Stack的渲染对象并转换坐标 final RenderBox stackRenderBox = context.findAncestorRenderObjectOfType<RenderBox>()!; final Offset localOffset = stackRenderBox.globalToLocal(offset); setState(() { position = localOffset; }); }, feedback: Container( width: 120.0, height: 120.0, color: widget.itemColor.withOpacity(0.5), child: Center( child: Text( widget.label, style: TextStyle( color: Colors.white, decoration: TextDecoration.none, fontSize: 18.0, ), ), ), ), ), ); } }
为什么这个方法管用?
context.findAncestorRenderObjectOfType<RenderBox>()会精准定位到当前DragBox所在的父级Stack的渲染对象,然后调用globalToLocal方法,自动把全局屏幕坐标转换成相对于Stack左上角的局部坐标。这样不管ListView怎么滚动,计算出来的位置都是准确的,完全不需要手动调整像素值这种hack手段。
内容的提问来源于stack exchange,提问作者Simone Romano
相关产品推荐
相关产品推荐

