Мое фактическое приложение намного сложнее, но я смог упростить его до этого примера, который демонстрирует проблему.
У меня есть 2 кнопки, которые должны отражать одни и те же данные. В этом примере у меня есть 2 кнопки «Мне нравится» (сердце). Они могут быть либо в любимом, либо в непохожем состоянии. Однако оба статуса должны совпадать. Я делаю это через ChangeNotifier
, ChangeNotifierProvider
и Consumer
.
В реальном приложении в основном есть несколько экранов с кнопками, визуальное состояние которых должно соответствовать одним и тем же данным.
Все это работает нормально до сих пор.
Вот в чем проблема. Мне нужна кнопка, чтобы сделать анимацию при нажатии. Эта анимация должна происходить только в нажатой кнопке, а не в других. В этом простом примере кнопка выполняет всплывающую анимацию. Тем не менее, notifyListener ChangeNotifier
предотвращает анимацию, поскольку я думаю, что он в основном перестраивает виджет и, таким образом, предотвращает анимацию. Если я закомментирую notifyListener
, то анимация будет работать, но, очевидно, данные больше не будут обновляться, и, следовательно, другие кнопки не будут отражать правильный статус «нравится/не нравится».
Я пробовал назначать клавиши на кнопки, но это не помогло. Я теряюсь в том, как я могу решить это. По сути, мне нужно предотвратить влияние на нажатую кнопку Consumer
и notifyListener
.
Код:
var b1 = GlobalKey();
var b2 = GlobalKey();
final buttonStateProvider = ButtonStateProvider();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ChangeNotifierProvider.value(
value: buttonStateProvider,
child: Consumer<ButtonStateProvider>(builder: (context, provider, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
PoppingButton(
key: b1,
liked: provider.isLiked,
onTap: () {
provider.toggleLiked();
},
),
PoppingButton(
key: b2,
liked: provider.isLiked,
onTap: () {
provider.toggleLiked();
},
),
],
);
}),
),
),
);
}
//-------------------
class ButtonStateProvider extends ChangeNotifier {
bool isLiked = false;
void toggleLiked() {
isLiked = !isLiked;
notifyListeners();
}
}
class PoppingButton extends StatelessWidget {
const PoppingButton({super.key, required this.liked, required this.onTap});
final bool liked;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
var keyForHeart = GlobalKey<PopWidgetState>();
return GestureDetector(
onTap: (() {
keyForHeart.currentState?.pop();
onTap.call();
}),
child: PopWidget(
key: keyForHeart,
child: Icon(
liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
size: 100,
color: liked ? Colors.pink : Colors.grey,
),
),
);
}
}
class PopWidget extends StatefulWidget {
const PopWidget({super.key, required this.child});
final Widget child;
@override
State<PopWidget> createState() => PopWidgetState();
}
class PopWidgetState extends State<PopWidget> with SingleTickerProviderStateMixin {
late AnimationController animationController = AnimationController(duration: const Duration(milliseconds: 50), vsync: this);
late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));
void pop() {
animationController.forward();
}
@override
void initState() {
super.initState();
animationController.addStatusListener(animationListenerHandler);
}
void animationListenerHandler(status) {
// print("Status: ${animationController.status}");
switch (animationController.status) {
case AnimationStatus.completed:
animationController.reverse();
break;
case AnimationStatus.dismissed:
break;
default:
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// print("scaleAnimation.value: ${scaleAnimation.value}");
return AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return Transform.scale(
scale: scaleAnimation.value,
child: widget.child,
);
}),
);
}
}
Состояние пользовательского интерфейса:
@MohammedAlfateh да, точно!
Добавьте слушателя в свой ButtonStateProvider
и проигрывайте анимацию при каждом изменении состояния. PopWidgetState
init
:
@override
void initState() {
super.initState();
Provider.of<ButtonStateProvider>(context, listen: false).addListener(() {
pop();
});
animationController.addStatusListener(animationListenerHandler);
}
Извините, я не понимаю, куда идет этот код. Что, если PopWidgetState
является частной/третьей стороной?
PopWidgetState
class initState
, это из вашего кода, который вы разместили в своем вопросе. И удалите ключи, которые вы создали для своих виджетов.
Я немного изменил ваш код, вам не нужно GlobalKey
Я также добавил некоторые комментарии к изменениям, которые я сделал
final buttonStateProvider = ButtonStateProvider();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ChangeNotifierProvider.value(
value: buttonStateProvider,
child: Consumer<ButtonStateProvider>(
builder: (context, provider, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
for (int i = 1; i < 3; i++)
PoppingButton(
id: i,
liked: provider.isLiked,
tappedId: provider.tappedId,
onTap: () => provider.toggleLiked(i),
),
],
);
}),
),
),
);
}
//-------------------
class ButtonStateProvider extends ChangeNotifier {
bool isLiked = false;
// used to indentfiy the widget to play the scale animation
int tappedId = 0;
void toggleLiked(int id) {
isLiked = !isLiked;
tappedId = id;
notifyListeners();
}
}
class PoppingButton extends StatelessWidget {
const PoppingButton(
{super.key,
required this.liked,
required this.tappedId,
required this.onTap,
required this.id});
final bool liked;
final VoidCallback onTap;
final int id;
final int tappedId;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: (() {
onTap.call();
}),
child: PopWidget(
key: ValueKey(id),
id: id,
tappedId: tappedId,
isLiked: liked,
child: Icon(
liked ? CupertinoIcons.suit_heart_fill : CupertinoIcons.suit_heart,
size: 100,
color: liked ? Colors.pink : Colors.grey,
),
),
);
}
}
class PopWidget extends StatefulWidget {
const PopWidget(
{super.key,
required this.child,
required this.tappedId,
required this.isLiked,
required this.id});
final Widget child;
final bool isLiked;
final int id;
final int tappedId;
@override
State<PopWidget> createState() => PopWidgetState();
}
class PopWidgetState extends State<PopWidget>
with SingleTickerProviderStateMixin {
late AnimationController animationController = AnimationController(
duration: const Duration(milliseconds: 50), vsync: this);
late var scaleAnimation = Tween<double>(begin: 1.0, end: 1.4).animate(
CurvedAnimation(parent: animationController, curve: Curves.easeOutSine));
@override
void initState() {
super.initState();
animationController.addStatusListener(animationListenerHandler);
}
void animationListenerHandler(status) {
// print("Status: ${animationController.status}");
switch (animationController.status) {
case AnimationStatus.completed:
animationController.reverse();
break;
case AnimationStatus.dismissed:
break;
default:
}
}
@override
void dispose() {
animationController.dispose();
super.dispose();
}
// update the animation state when [isLiked] is true
// and check the id of the tapped widget and the widget id
@override
void didUpdateWidget(covariant PopWidget oldWidget) {
if (widget.isLiked && widget.id == widget.tappedId) {
animationController.forward();
}
super.didUpdateWidget(oldWidget);
}
@override
Widget build(BuildContext context) {
// print("scaleAnimation.value: ${scaleAnimation.value}");
return AnimatedBuilder(
animation: animationController,
builder: ((context, child) {
return Transform.scale(
scale: scaleAnimation.value,
child: widget.child,
);
}),
);
}
}
Позвольте мне попробовать это. didChangeDependencies
похож на didUpdateWidget(covariant PopWidget oldWidget)
? Ваш ответ натолкнул меня на мысль использовать didUpdateWidget
для анимации только в том случае, если статус изменился по сравнению с предыдущим состоянием.
Вы можете использовать didUpdateWidget
здесь, однако это даст тот же результат, но я думаю, что лучше использовать его здесь, чтобы вы могли использовать ValueKey
вместо GlobalKey
, что легче, я обновлю ответ.
Итак, что вы пытаетесь заархивировать, так это то, что обе кнопки должны быть обновлены до одного и того же состояния, но только нажатая кнопка должна быть анимирована с масштабом?