【发布时间】:2022-08-20 16:32:08
【问题描述】:
我正在尝试在列表视图(可滚动)和另一个永远不可滚动的列表视图/行之间实现动画列表项切换。我试过使用local hero 和flutter sidekick 但没有成功。我需要初始列表小部件是可滚动的,而本地英雄不支持它。 Flutter Sidekick 很棒,但它与 gridview 一起使用,这是不可能自定义 UI 的。
标签: flutter dart flutter-layout flutter-animation
我正在尝试在列表视图(可滚动)和另一个永远不可滚动的列表视图/行之间实现动画列表项切换。我试过使用local hero 和flutter sidekick 但没有成功。我需要初始列表小部件是可滚动的,而本地英雄不支持它。 Flutter Sidekick 很棒,但它与 gridview 一起使用,这是不可能自定义 UI 的。
标签: flutter dart flutter-layout flutter-animation
只是为了给你一个开始,我已经编写了一个演示代码。我使用了一个 Stack 小部件,将小部件设置为动画到它的位置。使用全局键查找小部件在点击时的位置和最终位置。
import 'package:flutter/material.dart';
void main() async {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(home: Center(child: DemoClass()));
}
}
class DemoClass extends StatefulWidget {
DemoClass({Key? key}) : super(key: key);
@override
State<DemoClass> createState() => _DemoClassState();
}
class _DemoClassState extends State<DemoClass> {
List<Widget> profiles = [];
int selectedIndex = -1;
double firstItemLeftPos = -200;
double firstItemTopPos = 0;
GlobalKey finalWidgetKey = GlobalKey();
List<GlobalKey> profileKey = [];
int animDuration = 0;
@override
void initState() {
// TODO: implement initState
super.initState();
profiles = List.generate(20, (index) {
GlobalKey itemKey = GlobalKey();
profileKey.add(itemKey);
return InkWell(
key: profileKey[index],
onTap: () {
RenderBox box =
profileKey[index].currentContext?.findRenderObject() as RenderBox;
Offset position = box.localToGlobal(Offset.zero);
selectedIndex = index;
firstItemTopPos = position.dy;
firstItemLeftPos = position.dx;
animDuration = 0;
setState(() {});
Future.delayed(Duration(milliseconds: 10), () {
RenderBox FinalRenderbox =
finalWidgetKey.currentContext?.findRenderObject() as RenderBox;
Offset finalPos = FinalRenderbox.localToGlobal(Offset.zero);
firstItemLeftPos = finalPos.dx;
firstItemTopPos = finalPos.dy;
animDuration = 500;
setState(() {});
});
},
child: CircleAvatar(
child: Text("$index"),
),
);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
Expanded(
child: Stack(
children: [
Positioned(
left: 20,
top: 30,
child: Container(
key: finalWidgetKey,
height: 40,
width: 40,
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
),
),
AnimatedPositioned(
left: firstItemLeftPos,
top: firstItemTopPos,
child: CircleAvatar(
child: Text("$selectedIndex"),
),
duration: Duration(milliseconds: animDuration))
],
),
),
SizedBox(
height: 100,
width: MediaQuery.of(context).size.width,
child: ListView(
scrollDirection: Axis.horizontal,
children: profiles,
),
)
],
),
);
}
}
【讨论】:
我的概念是这个 UI 是一切都可以放在 Stack 上。每个圆圈都包含 userData 及其位置参数。虽然我们需要放置在两个不同的点上,但我们可以使用两个 bool 检查该位置是否为空。会有两个动画,AnimatedPositioned和AnimatedContainer(用于调整大小),你可以选择不同的动画。而对于滚动事件,我们可以使用Gesturedetector delta。在这里,您需要控制选择和滚动的列表排列。
//https://gist.github.com/yeasin50/4e0bad0a1fd66dd2b1e5fd1b37d1fdc4
class AnimatedUI extends StatefulWidget {
const AnimatedUI({Key? key}) : super(key: key);
@override
State<AnimatedUI> createState() => _AnimatedUIState();
}
enum PosOn { a, b, bottom }
class UserX {
final Color color;
final String name;
PosOn posOn;
UserX({
required this.color,
required this.name,
this.posOn = PosOn.bottom,
});
UserX copyWith({
Color? color,
String? name,
PosOn? posOn,
}) {
return UserX(
posOn: posOn ?? this.posOn,
color: color ?? this.color,
name: name ?? this.name,
);
}
}
class _AnimatedUIState extends State<AnimatedUI> {
final smallSize = 64.0;
late final usersList = List.generate(
24,
(index) => UserX(
color: index.isEven ? Colors.red : Colors.orange,
name: "name $index"));
bool aIsEmpty = true;
bool bIsEmpty = true;
double deltaX = 0.0;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.deepPurple,
body: LayoutBuilder(
builder: (context, constraints) {
final width = constraints.maxWidth;
final height = constraints.maxHeight;
final maxAvatarSize = constraints.maxWidth * .25;
final destinationA = Offset(width * .3, height * .2);
final destinationB =
Offset(destinationA.dx + maxAvatarSize, height * .3);
const Duration duration = Duration(milliseconds: 200);
return Stack(
children: [
Positioned(
left: destinationA.dx,
top: destinationA.dy,
child: borderRight(maxAvatarSize),
),
Positioned(
left: destinationB.dx,
top: destinationB.dy,
child: borderRight(maxAvatarSize),
),
for (int i = 0; i < usersList.length; i++)
AnimatedPositioned(
duration: duration,
top: () {
if (usersList[i].posOn == PosOn.a) {
return destinationA.dy;
} else if (usersList[i].posOn == PosOn.b) {
return destinationB.dy;
}
return height - smallSize - 16;
}(),
left: () {
if (usersList[i].posOn == PosOn.a) {
return destinationA.dx;
} else if (usersList[i].posOn == PosOn.b) {
return destinationB.dx;
}
return deltaX + (smallSize + 8) * i;
}(),
child: AnimatedContainer(
duration: duration,
width: usersList[i].posOn == PosOn.bottom
? smallSize
: maxAvatarSize,
height: usersList[i].posOn == PosOn.bottom
? smallSize
: maxAvatarSize,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Material(
color: usersList[i].color,
shape: const CircleBorder(),
child: InkWell(
customBorder: const CircleBorder(),
onTap: () {
final currentPos = usersList[i].posOn;
if (currentPos == PosOn.a) {
usersList[i] =
usersList[i].copyWith(posOn: PosOn.bottom);
aIsEmpty = true;
} else if (currentPos == PosOn.b) {
usersList[i] =
usersList[i].copyWith(posOn: PosOn.bottom);
bIsEmpty = true;
} else {
if (aIsEmpty) {
usersList[i] =
usersList[i].copyWith(posOn: PosOn.a);
aIsEmpty = false;
} else if (bIsEmpty) {
usersList[i] =
usersList[i].copyWith(posOn: PosOn.b);
bIsEmpty = false;
}
}
setState(() {});
},
child: SizedBox(
width: smallSize,
height: smallSize,
),
),
),
),
),
),
Align(
alignment: Alignment.bottomCenter,
child: SizedBox(
height: smallSize,
child: GestureDetector(
onHorizontalDragUpdate: (details) {
deltaX += details.delta.dx;
setState(() {});
},
),
),
)
],
);
},
),
);
}
Container borderRight(double maxAvatarSize) {
return Container(
width: maxAvatarSize,
height: maxAvatarSize,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.cyanAccent,
width: 4,
),
),
alignment: Alignment.center,
child: Text(
"Add",
style: TextStyle(color: Colors.white),
),
);
}
}
【讨论】: