From db2de6d68cbd897bc75cc982ec44b559e6061ce5 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 22 Mar 2026 16:53:45 +0800 Subject: [PATCH 1/2] Remove remaining deprecated cookbook recipe --- .../typing_indicator/lib/excerpt1.dart | 31 - .../typing_indicator/lib/excerpt2.dart | 73 -- .../typing_indicator/lib/excerpt3.dart | 213 ---- .../typing_indicator/lib/excerpt4.dart | 203 ---- .../effects/typing_indicator/lib/main.dart | 389 ------- .../effects/typing_indicator/pubspec.yaml | 21 - firebase.json | 3 +- .../cookbook/effects/typing-indicator.md | 1027 ----------------- .../learning-resources-index/cookbook.yml | 7 - 9 files changed, 2 insertions(+), 1965 deletions(-) delete mode 100644 examples/cookbook/effects/typing_indicator/lib/excerpt1.dart delete mode 100644 examples/cookbook/effects/typing_indicator/lib/excerpt2.dart delete mode 100644 examples/cookbook/effects/typing_indicator/lib/excerpt3.dart delete mode 100644 examples/cookbook/effects/typing_indicator/lib/excerpt4.dart delete mode 100644 examples/cookbook/effects/typing_indicator/lib/main.dart delete mode 100644 examples/cookbook/effects/typing_indicator/pubspec.yaml delete mode 100644 src/content/cookbook/effects/typing-indicator.md diff --git a/examples/cookbook/effects/typing_indicator/lib/excerpt1.dart b/examples/cookbook/effects/typing_indicator/lib/excerpt1.dart deleted file mode 100644 index e5bb6f0ae7c..00000000000 --- a/examples/cookbook/effects/typing_indicator/lib/excerpt1.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -// #docregion typing-indicator -class TypingIndicator extends StatefulWidget { - const TypingIndicator({ - super.key, - this.showIndicator = false, - this.bubbleColor = const Color(0xFF646b7f), - this.flashingCircleDarkColor = const Color(0xFF333333), - this.flashingCircleBrightColor = const Color(0xFFaec1dd), - }); - - final bool showIndicator; - final Color bubbleColor; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - State createState() => _TypingIndicatorState(); -} - -class _TypingIndicatorState extends State { - @override - Widget build(BuildContext context) { - // TODO: - return const SizedBox(); - } -} - -// #enddocregion typing-indicator diff --git a/examples/cookbook/effects/typing_indicator/lib/excerpt2.dart b/examples/cookbook/effects/typing_indicator/lib/excerpt2.dart deleted file mode 100644 index 5034c726204..00000000000 --- a/examples/cookbook/effects/typing_indicator/lib/excerpt2.dart +++ /dev/null @@ -1,73 +0,0 @@ -// ignore_for_file: unused_element - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import './excerpt1.dart'; - -// #docregion typing-indicator-state -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - late Animation _indicatorSpaceAnimation; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value); - }, - ); - } -} - -// #enddocregion typing-indicator-state diff --git a/examples/cookbook/effects/typing_indicator/lib/excerpt3.dart b/examples/cookbook/effects/typing_indicator/lib/excerpt3.dart deleted file mode 100644 index 0ce68634864..00000000000 --- a/examples/cookbook/effects/typing_indicator/lib/excerpt3.dart +++ /dev/null @@ -1,213 +0,0 @@ -// ignore_for_file: unused_element, unused_field - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import './excerpt1.dart'; - -// #docregion bubbles -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this) - ..addListener(() { - setState(() {}); - }); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - _smallBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), - reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut), - ); - _mediumBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.2, 0.7, curve: Curves.elasticOut), - reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut), - ); - _largeBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.3, 1.0, curve: Curves.elasticOut), - reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class CircleBubble extends StatelessWidget { - const CircleBubble({ - super.key, - required this.size, - required this.bubbleColor, - }); - - final double size; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: size, - height: size, - decoration: BoxDecoration(shape: BoxShape.circle, color: bubbleColor), - ); - } -} - -class AnimatedBubble extends StatelessWidget { - const AnimatedBubble({ - super.key, - required this.animation, - required this.left, - required this.bottom, - required this.bubble, - }); - - final Animation animation; - final double left; - final double bottom; - final Widget bubble; - - @override - Widget build(BuildContext context) { - return Positioned( - left: left, - bottom: bottom, - child: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.scale( - scale: animation.value, - alignment: Alignment.bottomLeft, - child: child, - ); - }, - child: bubble, - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - ); - } -} - -// #enddocregion bubbles diff --git a/examples/cookbook/effects/typing_indicator/lib/excerpt4.dart b/examples/cookbook/effects/typing_indicator/lib/excerpt4.dart deleted file mode 100644 index 06a0c59bf69..00000000000 --- a/examples/cookbook/effects/typing_indicator/lib/excerpt4.dart +++ /dev/null @@ -1,203 +0,0 @@ -// ignore_for_file: unused_element -import 'dart:math'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -import './excerpt1.dart'; -import './excerpt3.dart'; - -// #docregion animation-controller -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - // other initializations... - - _repeatingController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void dispose() { - _appearanceController.dispose(); - _repeatingController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - _repeatingController.repeat(); // <-- Add this - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - _repeatingController.stop(); // <-- Add this - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - repeatingController: _repeatingController, // <-- Add this - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FlashingCircle( - index: 0, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 1, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 2, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - ], - ), - ); - } -} - -class FlashingCircle extends StatelessWidget { - const FlashingCircle({ - super.key, - required this.index, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - }); - - final int index; - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: repeatingController, - builder: (context, child) { - final circleFlashPercent = dotIntervals[index].transform( - repeatingController.value, - ); - final circleColorPercent = sin(pi * circleFlashPercent); - - return Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color.lerp( - flashingCircleDarkColor, - flashingCircleBrightColor, - circleColorPercent, - ), - ), - ); - }, - ); - } -} - -// #enddocregion animation-controller diff --git a/examples/cookbook/effects/typing_indicator/lib/main.dart b/examples/cookbook/effects/typing_indicator/lib/main.dart deleted file mode 100644 index d01a8380bb6..00000000000 --- a/examples/cookbook/effects/typing_indicator/lib/main.dart +++ /dev/null @@ -1,389 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -void main() { - runApp( - const MaterialApp( - home: ExampleIsTyping(), - debugShowCheckedModeBanner: false, - ), - ); -} - -const _backgroundColor = Color(0xFF333333); - -class ExampleIsTyping extends StatefulWidget { - const ExampleIsTyping({super.key}); - - @override - State createState() => _ExampleIsTypingState(); -} - -class _ExampleIsTypingState extends State { - bool _isSomeoneTyping = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _backgroundColor, - appBar: AppBar(title: const Text('Typing Indicator')), - body: Column( - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: 25, - reverse: true, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(left: 100), - child: FakeMessage(isBig: index.isOdd), - ); - }, - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: TypingIndicator(showIndicator: _isSomeoneTyping), - ), - Container( - color: Colors.grey, - padding: const EdgeInsets.all(16), - child: Center( - child: CupertinoSwitch( - onChanged: (newValue) { - setState(() { - _isSomeoneTyping = newValue; - }); - }, - value: _isSomeoneTyping, - ), - ), - ), - ], - ), - ); - } -} - -class TypingIndicator extends StatefulWidget { - const TypingIndicator({ - super.key, - this.showIndicator = false, - this.bubbleColor = const Color(0xFF646b7f), - this.flashingCircleDarkColor = const Color(0xFF333333), - this.flashingCircleBrightColor = const Color(0xFFaec1dd), - }); - - final bool showIndicator; - final Color bubbleColor; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - State createState() => _TypingIndicatorState(); -} - -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this) - ..addListener(() { - setState(() {}); - }); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - _smallBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), - reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut), - ); - _mediumBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.2, 0.7, curve: Curves.elasticOut), - reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut), - ); - _largeBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.3, 1.0, curve: Curves.elasticOut), - reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut), - ); - - _repeatingController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - _repeatingController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - _repeatingController.repeat(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - _repeatingController.stop(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - repeatingController: _repeatingController, - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class CircleBubble extends StatelessWidget { - const CircleBubble({ - super.key, - required this.size, - required this.bubbleColor, - }); - - final double size; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: size, - height: size, - decoration: BoxDecoration(shape: BoxShape.circle, color: bubbleColor), - ); - } -} - -class AnimatedBubble extends StatelessWidget { - const AnimatedBubble({ - super.key, - required this.animation, - required this.left, - required this.bottom, - required this.bubble, - }); - - final Animation animation; - final double left; - final double bottom; - final Widget bubble; - - @override - Widget build(BuildContext context) { - return Positioned( - left: left, - bottom: bottom, - child: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.scale( - scale: animation.value, - alignment: Alignment.bottomLeft, - child: child, - ); - }, - child: bubble, - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FlashingCircle( - index: 0, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 1, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 2, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - ], - ), - ); - } -} - -class FlashingCircle extends StatelessWidget { - const FlashingCircle({ - super.key, - required this.index, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - }); - - final int index; - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: repeatingController, - builder: (context, child) { - final circleFlashPercent = dotIntervals[index].transform( - repeatingController.value, - ); - final circleColorPercent = sin(pi * circleFlashPercent); - - return Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color.lerp( - flashingCircleDarkColor, - flashingCircleBrightColor, - circleColorPercent, - ), - ), - ); - }, - ); - } -} - -class FakeMessage extends StatelessWidget { - const FakeMessage({super.key, required this.isBig}); - - final bool isBig; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), - height: isBig ? 128 : 36, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - color: Colors.grey.shade300, - ), - ); - } -} diff --git a/examples/cookbook/effects/typing_indicator/pubspec.yaml b/examples/cookbook/effects/typing_indicator/pubspec.yaml deleted file mode 100644 index 62b73a888ff..00000000000 --- a/examples/cookbook/effects/typing_indicator/pubspec.yaml +++ /dev/null @@ -1,21 +0,0 @@ -name: typing_indicator -description: A new Flutter project. - -publish_to: none -version: 1.0.0+1 - -resolution: workspace -environment: - sdk: ^3.11.0 - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.8 - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/firebase.json b/firebase.json index bf5d7fe2c79..72769628583 100644 --- a/firebase.json +++ b/firebase.json @@ -125,8 +125,9 @@ { "source": "/cookbook", "destination": "/learn/learning-resources", "type": 301 }, { "source": "/cookbook/games/google-mobile-ads", "destination": "/cookbook/plugins/google-mobile-ads", "type": 301 }, { "source": "/cookbook/images/cached-images", "destination": "/cookbook/images/network-image", "type": 301 }, - { "source": "/cookbook/effects/photo-filter-carousel", "destination": "/learn/learning-resources", "type": 301 }, { "source": "/cookbook/effects/gradient-bubbles", "destination": "/learn/learning-resources", "type": 301 }, + { "source": "/cookbook/effects/photo-filter-carousel", "destination": "/learn/learning-resources", "type": 301 }, + { "source": "/cookbook/effects/typing-indicator", "destination": "/learn/learning-resources", "type": 301 }, { "source": "/cookbook/networking/named-routes", "destination": "/cookbook/navigation/named-routes", "type": 301 }, { "source": "/cookbook/testing/integration-test-introduction", "destination": "/cookbook/testing/integration", "type": 301 }, { "source": "/cookbook/testing/integration-test-profiling", "destination": "/cookbook/testing/integration/profiling", "type": 301 }, diff --git a/src/content/cookbook/effects/typing-indicator.md b/src/content/cookbook/effects/typing-indicator.md deleted file mode 100644 index 99c5172c23a..00000000000 --- a/src/content/cookbook/effects/typing-indicator.md +++ /dev/null @@ -1,1027 +0,0 @@ ---- -title: Create a typing indicator -description: How to implement a typing indicator. ---- - - - -{% render "docs/deprecated.md" %} - -Modern chat apps display indicators when other users -are actively typing responses. These indicators help -prevent rapid and conflicting responses between you -and the other person. In this recipe, you build a -speech bubble typing indicator that animates in and out of view. - -The following animation shows the app's behavior: - -![The typing indicator is turned on and off](/assets/images/docs/cookbook/effects/TypingIndicator.webp){:.site-mobile-screenshot} - -## Define the typing indicator widget - -The typing indicator exists within its own widget so that -it can be used anywhere in your app. As with any widget -that controls animations, the typing indicator needs to -be a stateful widget. The widget accepts a boolean value -that determines whether the indicator is visible. -This speech-bubble-typing indicator accepts a color -for the bubbles and two colors for the light and dark -phases of the flashing circles within the large speech bubble. - -Define a new stateful widget called `TypingIndicator`. - - -```dart -class TypingIndicator extends StatefulWidget { - const TypingIndicator({ - super.key, - this.showIndicator = false, - this.bubbleColor = const Color(0xFF646b7f), - this.flashingCircleDarkColor = const Color(0xFF333333), - this.flashingCircleBrightColor = const Color(0xFFaec1dd), - }); - - final bool showIndicator; - final Color bubbleColor; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - State createState() => _TypingIndicatorState(); -} - -class _TypingIndicatorState extends State { - @override - Widget build(BuildContext context) { - // TODO: - return const SizedBox(); - } -} -``` - -## Make room for the typing indicator - -The typing indicator doesn't occupy any space when it -isn't displayed. Therefore, the indicator needs to grow -in height when it appears, and shrink in height -when it disappears. - -The height of the typing indicator could be the natural -height of the speech bubbles within the typing indicator. -However, the speech bubbles expand with an elastic curve. -This elasticity would be too visually jarring if it quickly -pushed all the conversation messages up or down. Instead, -the height of the typing indicator animates on its own, -smoothly expanding before the bubbles appear. -When the bubbles disappear, the height smoothly contracts to zero. -This behavior requires an [explicit animation][] for the -height of the typing indicator. - -Define an animation for the height of the typing indicator, -and then apply that animated value to the `SizedBox` -widget within the typing indicator. - - -```dart -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - late Animation _indicatorSpaceAnimation; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value); - }, - ); - } -} -``` - -The `TypingIndicator` runs an animation forward or backward -depending on whether the incoming `showIndicator` variable -is `true` or `false`, respectively. - -The animation that controls the height uses different -animation curves depending on its direction. -When the animation moves forward, it needs to quickly make -space for the speech bubbles. For this reason, -the forward curve runs the entire height animation within -the first 40% of the overall appearance animation. -When the animation reverses, it needs to give the speech bubbles -enough time to disappear before contracting the height. -An ease-out curve that uses all the available time is a -good way to accomplish this behavior. - -:::note -The `AnimatedBuilder` widget rebuilds the `SizedBox` -widget as the `_indicatorSpaceAnimation` changes. -The alternative to using `AnimatedBuilder` is to -invoke `setState()` every time the animation changes, -and then rebuild the entire widget tree within `TypingIndicator`. -Invoking `setState()` in this manner is acceptable, -but as other widgets are added to this widget tree, -rebuilding the entire tree just to change the height -of the `SizedBox` widget wastes CPU cycles. -::: - -## Animate the speech bubbles - -The typing indicator displays three speech bubbles. -The first two bubbles are small and round. The third -bubble is oblong and contains a few flashing circles. -These bubbles are staggered in position from the lower -left of the available space. - -Each bubble appears by animating its scale from 0% to 100%, -and each bubble does this at slightly different times so -that it looks like each bubble appears after the one before it. -This is called a [staggered animation][]. - -Paint the three bubbles in the desired positions from the -lower left. Then, animate the scale of the bubbles -so that the bubbles are staggered whenever the `showIndicator` -property changes. - - -```dart -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this) - ..addListener(() { - setState(() {}); - }); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - _smallBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), - reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut), - ); - _mediumBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.2, 0.7, curve: Curves.elasticOut), - reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut), - ); - _largeBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.3, 1.0, curve: Curves.elasticOut), - reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class CircleBubble extends StatelessWidget { - const CircleBubble({ - super.key, - required this.size, - required this.bubbleColor, - }); - - final double size; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: size, - height: size, - decoration: BoxDecoration(shape: BoxShape.circle, color: bubbleColor), - ); - } -} - -class AnimatedBubble extends StatelessWidget { - const AnimatedBubble({ - super.key, - required this.animation, - required this.left, - required this.bottom, - required this.bubble, - }); - - final Animation animation; - final double left; - final double bottom; - final Widget bubble; - - @override - Widget build(BuildContext context) { - return Positioned( - left: left, - bottom: bottom, - child: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.scale( - scale: animation.value, - alignment: Alignment.bottomLeft, - child: child, - ); - }, - child: bubble, - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - ); - } -} -``` - -## Animate the flashing circles - -Within the large speech bubble, the typing indicator -displays three small circles that flash repeatedly. -Each circle flashes at a slightly different time, -giving the impression that a single light source is -moving behind each circle. This flashing animation -repeats indefinitely. - -Introduce a repeating `AnimationController` to -implement the circle flashing and pass it to the -`StatusBubble`. - - -```dart -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - // other initializations... - - _repeatingController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void dispose() { - _appearanceController.dispose(); - _repeatingController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - _repeatingController.repeat(); // <-- Add this - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - _repeatingController.stop(); // <-- Add this - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - repeatingController: _repeatingController, // <-- Add this - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FlashingCircle( - index: 0, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 1, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 2, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - ], - ), - ); - } -} - -class FlashingCircle extends StatelessWidget { - const FlashingCircle({ - super.key, - required this.index, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - }); - - final int index; - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: repeatingController, - builder: (context, child) { - final circleFlashPercent = dotIntervals[index].transform( - repeatingController.value, - ); - final circleColorPercent = sin(pi * circleFlashPercent); - - return Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color.lerp( - flashingCircleDarkColor, - flashingCircleBrightColor, - circleColorPercent, - ), - ), - ); - }, - ); - } -} -``` - -Each circle calculates its color using a sine (`sin`) -function so that the color changes gradually at the -minimum and maximum points. Additionally, -each circle animates its color within a specified interval -that takes up a portion of the overall animation time. -The position of these intervals generates the visual -effect of a single light source moving behind the three dots. - -Congratulations! You now have a typing indicator that lets users -know when someone else is typing. The indicator animates in and out, -and displays a repeating animation while the other user is typing. - -## Interactive example - -Run the app: - -* Click the round on/off switch at the bottom - of the screen to turn the typing indicator bubble - on and off. - - - - -```dartpad title="Flutter typing indicator hands-on example in DartPad" run="true" -import 'dart:math'; - -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; - -void main() { - runApp( - const MaterialApp( - home: ExampleIsTyping(), - debugShowCheckedModeBanner: false, - ), - ); -} - -const _backgroundColor = Color(0xFF333333); - -class ExampleIsTyping extends StatefulWidget { - const ExampleIsTyping({super.key}); - - @override - State createState() => _ExampleIsTypingState(); -} - -class _ExampleIsTypingState extends State { - bool _isSomeoneTyping = false; - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: _backgroundColor, - appBar: AppBar(title: const Text('Typing Indicator')), - body: Column( - children: [ - Expanded( - child: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 8), - itemCount: 25, - reverse: true, - itemBuilder: (context, index) { - return Padding( - padding: const EdgeInsets.only(left: 100), - child: FakeMessage(isBig: index.isOdd), - ); - }, - ), - ), - Align( - alignment: Alignment.bottomLeft, - child: TypingIndicator(showIndicator: _isSomeoneTyping), - ), - Container( - color: Colors.grey, - padding: const EdgeInsets.all(16), - child: Center( - child: CupertinoSwitch( - onChanged: (newValue) { - setState(() { - _isSomeoneTyping = newValue; - }); - }, - value: _isSomeoneTyping, - ), - ), - ), - ], - ), - ); - } -} - -class TypingIndicator extends StatefulWidget { - const TypingIndicator({ - super.key, - this.showIndicator = false, - this.bubbleColor = const Color(0xFF646b7f), - this.flashingCircleDarkColor = const Color(0xFF333333), - this.flashingCircleBrightColor = const Color(0xFFaec1dd), - }); - - final bool showIndicator; - final Color bubbleColor; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - State createState() => _TypingIndicatorState(); -} - -class _TypingIndicatorState extends State - with TickerProviderStateMixin { - late AnimationController _appearanceController; - - late Animation _indicatorSpaceAnimation; - - late Animation _smallBubbleAnimation; - late Animation _mediumBubbleAnimation; - late Animation _largeBubbleAnimation; - - late AnimationController _repeatingController; - final List _dotIntervals = const [ - Interval(0.25, 0.8), - Interval(0.35, 0.9), - Interval(0.45, 1.0), - ]; - - @override - void initState() { - super.initState(); - - _appearanceController = AnimationController(vsync: this) - ..addListener(() { - setState(() {}); - }); - - _indicatorSpaceAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.4, curve: Curves.easeOut), - reverseCurve: const Interval(0.0, 1.0, curve: Curves.easeOut), - ).drive(Tween(begin: 0.0, end: 60.0)); - - _smallBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.0, 0.5, curve: Curves.elasticOut), - reverseCurve: const Interval(0.0, 0.3, curve: Curves.easeOut), - ); - _mediumBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.2, 0.7, curve: Curves.elasticOut), - reverseCurve: const Interval(0.2, 0.6, curve: Curves.easeOut), - ); - _largeBubbleAnimation = CurvedAnimation( - parent: _appearanceController, - curve: const Interval(0.3, 1.0, curve: Curves.elasticOut), - reverseCurve: const Interval(0.5, 1.0, curve: Curves.easeOut), - ); - - _repeatingController = AnimationController( - vsync: this, - duration: const Duration(milliseconds: 1500), - ); - - if (widget.showIndicator) { - _showIndicator(); - } - } - - @override - void didUpdateWidget(TypingIndicator oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.showIndicator != oldWidget.showIndicator) { - if (widget.showIndicator) { - _showIndicator(); - } else { - _hideIndicator(); - } - } - } - - @override - void dispose() { - _appearanceController.dispose(); - _repeatingController.dispose(); - super.dispose(); - } - - void _showIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 750) - ..forward(); - _repeatingController.repeat(); - } - - void _hideIndicator() { - _appearanceController - ..duration = const Duration(milliseconds: 150) - ..reverse(); - _repeatingController.stop(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _indicatorSpaceAnimation, - builder: (context, child) { - return SizedBox(height: _indicatorSpaceAnimation.value, child: child); - }, - child: Stack( - children: [ - AnimatedBubble( - animation: _smallBubbleAnimation, - left: 8, - bottom: 8, - bubble: CircleBubble(size: 8, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _mediumBubbleAnimation, - left: 10, - bottom: 10, - bubble: CircleBubble(size: 16, bubbleColor: widget.bubbleColor), - ), - AnimatedBubble( - animation: _largeBubbleAnimation, - left: 12, - bottom: 12, - bubble: StatusBubble( - repeatingController: _repeatingController, - dotIntervals: _dotIntervals, - flashingCircleDarkColor: widget.flashingCircleDarkColor, - flashingCircleBrightColor: widget.flashingCircleBrightColor, - bubbleColor: widget.bubbleColor, - ), - ), - ], - ), - ); - } -} - -class CircleBubble extends StatelessWidget { - const CircleBubble({ - super.key, - required this.size, - required this.bubbleColor, - }); - - final double size; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: size, - height: size, - decoration: BoxDecoration(shape: BoxShape.circle, color: bubbleColor), - ); - } -} - -class AnimatedBubble extends StatelessWidget { - const AnimatedBubble({ - super.key, - required this.animation, - required this.left, - required this.bottom, - required this.bubble, - }); - - final Animation animation; - final double left; - final double bottom; - final Widget bubble; - - @override - Widget build(BuildContext context) { - return Positioned( - left: left, - bottom: bottom, - child: AnimatedBuilder( - animation: animation, - builder: (context, child) { - return Transform.scale( - scale: animation.value, - alignment: Alignment.bottomLeft, - child: child, - ); - }, - child: bubble, - ), - ); - } -} - -class StatusBubble extends StatelessWidget { - const StatusBubble({ - super.key, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - required this.bubbleColor, - }); - - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - final Color bubbleColor; - - @override - Widget build(BuildContext context) { - return Container( - width: 85, - height: 44, - padding: const EdgeInsets.symmetric(horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(27), - color: bubbleColor, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - FlashingCircle( - index: 0, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 1, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - FlashingCircle( - index: 2, - repeatingController: repeatingController, - dotIntervals: dotIntervals, - flashingCircleDarkColor: flashingCircleDarkColor, - flashingCircleBrightColor: flashingCircleBrightColor, - ), - ], - ), - ); - } -} - -class FlashingCircle extends StatelessWidget { - const FlashingCircle({ - super.key, - required this.index, - required this.repeatingController, - required this.dotIntervals, - required this.flashingCircleBrightColor, - required this.flashingCircleDarkColor, - }); - - final int index; - final AnimationController repeatingController; - final List dotIntervals; - final Color flashingCircleDarkColor; - final Color flashingCircleBrightColor; - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: repeatingController, - builder: (context, child) { - final circleFlashPercent = dotIntervals[index].transform( - repeatingController.value, - ); - final circleColorPercent = sin(pi * circleFlashPercent); - - return Container( - width: 12, - height: 12, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Color.lerp( - flashingCircleDarkColor, - flashingCircleBrightColor, - circleColorPercent, - ), - ), - ); - }, - ); - } -} - -class FakeMessage extends StatelessWidget { - const FakeMessage({super.key, required this.isBig}); - - final bool isBig; - - @override - Widget build(BuildContext context) { - return Container( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 24), - height: isBig ? 128 : 36, - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), - color: Colors.grey.shade300, - ), - ); - } -} -``` - -[explicit animation]: /ui/animations#tween-animation -[staggered animation]: /ui/animations/staggered-animations diff --git a/src/data/learning-resources-index/cookbook.yml b/src/data/learning-resources-index/cookbook.yml index b77022e2e57..d1e93148e15 100644 --- a/src/data/learning-resources-index/cookbook.yml +++ b/src/data/learning-resources-index/cookbook.yml @@ -144,13 +144,6 @@ label: Flutter docs url: /cookbook/effects/staggered-menu-animation -- name: Create a typing indicator - description: Build a speech bubble typing indicator that animates in and out of view. - type: recipe - link: - label: Flutter docs - url: /cookbook/effects/typing-indicator - - name: Create an expandable FAB description: Create a floating action button that spawns other action buttons. tags: From 4f072642b2e3659fd2387eb50a6deb00c13feab1 Mon Sep 17 00:00:00 2001 From: Parker Lougheed Date: Sun, 22 Mar 2026 16:54:56 +0800 Subject: [PATCH 2/2] Also remove code from other removed recipes --- .github/dependabot.yml | 3 - .../lib/bubble_background.dart | 108 --------- .../gradient_bubbles/lib/bubble_painter.dart | 74 ------ .../lib/bubble_painter_empty.dart | 39 --- .../effects/gradient_bubbles/lib/main.dart | 229 ------------------ .../effects/gradient_bubbles/pubspec.yaml | 20 -- examples/pubspec.yaml | 2 - 7 files changed, 475 deletions(-) delete mode 100644 examples/cookbook/effects/gradient_bubbles/lib/bubble_background.dart delete mode 100644 examples/cookbook/effects/gradient_bubbles/lib/bubble_painter.dart delete mode 100644 examples/cookbook/effects/gradient_bubbles/lib/bubble_painter_empty.dart delete mode 100644 examples/cookbook/effects/gradient_bubbles/lib/main.dart delete mode 100644 examples/cookbook/effects/gradient_bubbles/pubspec.yaml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 90973c4a3cd..e1006e4bb66 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -56,13 +56,10 @@ updates: - "/examples/cookbook/effects/download_button" - "/examples/cookbook/effects/drag_a_widget" - "/examples/cookbook/effects/expandable_fab" - - "/examples/cookbook/effects/gradient_bubbles" - "/examples/cookbook/effects/nested_nav" - "/examples/cookbook/effects/parallax_scrolling" - - "/examples/cookbook/effects/photo_filter_carousel" - "/examples/cookbook/effects/shimmer_loading" - "/examples/cookbook/effects/staggered_menu_animation" - - "/examples/cookbook/effects/typing_indicator" - "/examples/cookbook/forms/focus" - "/examples/cookbook/forms/retrieve_input" - "/examples/cookbook/forms/text_field_changes" diff --git a/examples/cookbook/effects/gradient_bubbles/lib/bubble_background.dart b/examples/cookbook/effects/gradient_bubbles/lib/bubble_background.dart deleted file mode 100644 index 842ca6f4bb6..00000000000 --- a/examples/cookbook/effects/gradient_bubbles/lib/bubble_background.dart +++ /dev/null @@ -1,108 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -@immutable -class BubbleBackground extends StatelessWidget { - const BubbleBackground({super.key, required this.colors, this.child}); - - final List colors; - final Widget? child; - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: BubblePainter( - colors: colors, - bubbleContext: context, - scrollable: ScrollableState(), - ), - child: child, - ); - } -} - -// #docregion BubblePainter -class BubblePainter extends CustomPainter { - BubblePainter({ - required ScrollableState scrollable, - required BuildContext bubbleContext, - required List colors, - }) : _scrollable = scrollable, - _bubbleContext = bubbleContext, - _colors = colors; - - final ScrollableState _scrollable; - final BuildContext _bubbleContext; - final List _colors; - - @override - bool shouldRepaint(BubblePainter oldDelegate) { - return oldDelegate._scrollable != _scrollable || - oldDelegate._bubbleContext != _bubbleContext || - oldDelegate._colors != _colors; - } - - @override - void paint(Canvas canvas, Size size) { - final scrollableBox = _scrollable.context.findRenderObject() as RenderBox; - final scrollableRect = Offset.zero & scrollableBox.size; - final bubbleBox = _bubbleContext.findRenderObject() as RenderBox; - - final origin = bubbleBox.localToGlobal( - Offset.zero, - ancestor: scrollableBox, - ); - final paint = Paint() - ..shader = ui.Gradient.linear( - scrollableRect.topCenter, - scrollableRect.bottomCenter, - _colors, - [0.0, 1.0], - TileMode.clamp, - Matrix4.translationValues(-origin.dx, -origin.dy, 0.0).storage, - ); - canvas.drawRect(Offset.zero & size, paint); - } -} -// #enddocregion BubblePainter - -class Message { - Message(); - - bool isMine = true; - String text = 'The quick brown fox jumps over the lazy dog'; - String from = 'Flutter Dev'; -} - -class MyChat extends StatefulWidget { - const MyChat({super.key}); - - @override - State createState() => _MyChatState(); -} - -class _MyChatState extends State { - Message message = Message(); - - @override - Widget build(BuildContext context) { - // #docregion BubbleBackground - return BubbleBackground( - // The colors of the gradient, which are different - // depending on which user sent this message. - colors: message.isMine - ? const [Color(0xFF6C7689), Color(0xFF3A364B)] - : const [Color(0xFF19B7FF), Color(0xFF491CCB)], - // The content within the bubble. - child: DefaultTextStyle.merge( - style: const TextStyle(fontSize: 18.0, color: Colors.white), - child: Padding( - padding: const EdgeInsets.all(12), - child: Text(message.text), - ), - ), - ); - // #enddocregion BubbleBackground - } -} diff --git a/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter.dart b/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter.dart deleted file mode 100644 index 78c86246b59..00000000000 --- a/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -@immutable -class BubbleBackground extends StatelessWidget { - const BubbleBackground({super.key, required this.colors, this.child}); - - final List colors; - final Widget? child; - - @override - Widget build(BuildContext context) { - return CustomPaint( - // #docregion ScrollableContext - painter: BubblePainter( - colors: colors, - bubbleContext: context, - scrollable: ScrollableState(), - ), - // #enddocregion ScrollableContext - child: child, - ); - } -} - -// #docregion bp-without-paint -class BubblePainter extends CustomPainter { - BubblePainter({ - required ScrollableState scrollable, - required BuildContext bubbleContext, - required List colors, - }) : _scrollable = scrollable, - _bubbleContext = bubbleContext, - _colors = colors; - - final ScrollableState _scrollable; - final BuildContext _bubbleContext; - final List _colors; - - @override - bool shouldRepaint(BubblePainter oldDelegate) { - return oldDelegate._scrollable != _scrollable || - oldDelegate._bubbleContext != _bubbleContext || - oldDelegate._colors != _colors; - } - // #enddocregion bp-without-paint - - @override - void paint(Canvas canvas, Size size) { - final scrollableBox = _scrollable.context.findRenderObject() as RenderBox; - final scrollableRect = Offset.zero & scrollableBox.size; - final bubbleBox = _bubbleContext.findRenderObject() as RenderBox; - - final origin = bubbleBox.localToGlobal( - Offset.zero, - ancestor: scrollableBox, - ); - final paint = Paint() - ..shader = ui.Gradient.linear( - scrollableRect.topCenter, - scrollableRect.bottomCenter, - _colors, - [0.0, 1.0], - TileMode.clamp, - Matrix4.translationValues(-origin.dx, -origin.dy, 0.0).storage, - ); - canvas.drawRect(Offset.zero & size, paint); - } - - // #docregion bp-without-paint -} - -// #enddocregion bp-without-paint diff --git a/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter_empty.dart b/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter_empty.dart deleted file mode 100644 index 2b747ab9685..00000000000 --- a/examples/cookbook/effects/gradient_bubbles/lib/bubble_painter_empty.dart +++ /dev/null @@ -1,39 +0,0 @@ -// ignore_for_file: unused_field - -import 'package:flutter/material.dart'; - -// #docregion BubblePainterEmpty -@immutable -class BubbleBackground extends StatelessWidget { - const BubbleBackground({super.key, required this.colors, this.child}); - - final List colors; - final Widget? child; - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: BubblePainter(colors: colors), - child: child, - ); - } -} - -class BubblePainter extends CustomPainter { - BubblePainter({required List colors}) : _colors = colors; - - final List _colors; - - @override - void paint(Canvas canvas, Size size) { - // TODO: - } - - @override - bool shouldRepaint(BubblePainter oldDelegate) { - // TODO: - return false; - } -} - -// #enddocregion BubblePainterEmpty diff --git a/examples/cookbook/effects/gradient_bubbles/lib/main.dart b/examples/cookbook/effects/gradient_bubbles/lib/main.dart deleted file mode 100644 index 80b5d50aa3e..00000000000 --- a/examples/cookbook/effects/gradient_bubbles/lib/main.dart +++ /dev/null @@ -1,229 +0,0 @@ -import 'dart:math'; -import 'dart:ui' as ui; - -import 'package:flutter/material.dart'; - -void main() { - runApp(const App(home: ExampleGradientBubbles())); -} - -@immutable -class App extends StatelessWidget { - const App({super.key, this.home}); - - final Widget? home; - - @override - Widget build(BuildContext context) { - return MaterialApp( - debugShowCheckedModeBanner: false, - title: 'Flutter Chat', - theme: ThemeData.dark(), - home: home, - ); - } -} - -@immutable -class ExampleGradientBubbles extends StatefulWidget { - const ExampleGradientBubbles({super.key}); - - @override - State createState() => _ExampleGradientBubblesState(); -} - -class _ExampleGradientBubblesState extends State { - late final List data; - - @override - void initState() { - super.initState(); - data = MessageGenerator.generate(60, 1337); - } - - @override - Widget build(BuildContext context) { - return Theme( - data: ThemeData( - brightness: Brightness.dark, - primaryColor: const Color(0xFF4F4F4F), - ), - child: Scaffold( - appBar: AppBar(title: const Text('Flutter Chat')), - body: ListView.builder( - padding: const EdgeInsets.symmetric(vertical: 16.0), - reverse: true, - itemCount: data.length, - itemBuilder: (context, index) { - final message = data[index]; - return MessageBubble(message: message, child: Text(message.text)); - }, - ), - ), - ); - } -} - -@immutable -class MessageBubble extends StatelessWidget { - const MessageBubble({super.key, required this.message, required this.child}); - - final Message message; - final Widget child; - - @override - Widget build(BuildContext context) { - final messageAlignment = message.isMine - ? Alignment.topLeft - : Alignment.topRight; - - return FractionallySizedBox( - alignment: messageAlignment, - widthFactor: 0.8, - child: Align( - alignment: messageAlignment, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 6.0, horizontal: 20.0), - child: ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - child: BubbleBackground( - colors: [ - if (message.isMine) ...const [ - Color(0xFF6C7689), - Color(0xFF3A364B), - ] else ...const [Color(0xFF19B7FF), Color(0xFF491CCB)], - ], - child: DefaultTextStyle.merge( - style: const TextStyle(fontSize: 18.0, color: Colors.white), - child: Padding( - padding: const EdgeInsets.all(12.0), - child: child, - ), - ), - ), - ), - ), - ), - ); - } -} - -@immutable -class BubbleBackground extends StatelessWidget { - const BubbleBackground({super.key, required this.colors, this.child}); - - final List colors; - final Widget? child; - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: BubblePainter( - scrollable: Scrollable.of(context), - bubbleContext: context, - colors: colors, - ), - child: child, - ); - } -} - -class BubblePainter extends CustomPainter { - BubblePainter({ - required ScrollableState scrollable, - required BuildContext bubbleContext, - required List colors, - }) : _scrollable = scrollable, - _bubbleContext = bubbleContext, - _colors = colors, - super(repaint: scrollable.position); - - final ScrollableState _scrollable; - final BuildContext _bubbleContext; - final List _colors; - - @override - void paint(Canvas canvas, Size size) { - final scrollableBox = _scrollable.context.findRenderObject() as RenderBox; - final scrollableRect = Offset.zero & scrollableBox.size; - final bubbleBox = _bubbleContext.findRenderObject() as RenderBox; - - final origin = bubbleBox.localToGlobal( - Offset.zero, - ancestor: scrollableBox, - ); - final paint = Paint() - ..shader = ui.Gradient.linear( - scrollableRect.topCenter, - scrollableRect.bottomCenter, - _colors, - [0.0, 1.0], - TileMode.clamp, - Matrix4.translationValues(-origin.dx, -origin.dy, 0.0).storage, - ); - canvas.drawRect(Offset.zero & size, paint); - } - - @override - bool shouldRepaint(BubblePainter oldDelegate) { - return oldDelegate._scrollable != _scrollable || - oldDelegate._bubbleContext != _bubbleContext || - oldDelegate._colors != _colors; - } -} - -enum MessageOwner { myself, other } - -@immutable -class Message { - const Message({required this.owner, required this.text}); - - final MessageOwner owner; - final String text; - - bool get isMine => owner == MessageOwner.myself; -} - -class MessageGenerator { - static List generate(int count, [int? seed]) { - final random = Random(seed); - return List.unmodifiable( - List.generate(count, (index) { - return Message( - owner: random.nextBool() ? MessageOwner.myself : MessageOwner.other, - text: _exampleData[random.nextInt(_exampleData.length)], - ); - }), - ); - } - - static final _exampleData = [ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'In tempus mauris at velit egestas, sed blandit felis ultrices.', - 'Ut molestie mauris et ligula finibus iaculis.', - 'Sed a tempor ligula.', - 'Test', - 'Phasellus ullamcorper, mi ut imperdiet consequat, nibh augue condimentum nunc, vitae molestie massa augue nec erat.', - 'Donec scelerisque, erat vel placerat facilisis, eros turpis egestas nulla, a sodales elit nibh et enim.', - 'Mauris quis dignissim neque. In a odio leo. Aliquam egestas egestas tempor. Etiam at tortor metus.', - 'Quisque lacinia imperdiet faucibus.', - 'Proin egestas arcu non nisl laoreet, vitae iaculis enim volutpat. In vehicula convallis magna.', - 'Phasellus at diam a sapien laoreet gravida.', - 'Fusce maximus fermentum sem a scelerisque.', - 'Nam convallis sapien augue, malesuada aliquam dui bibendum nec.', - 'Quisque dictum tincidunt ex non lobortis.', - 'In hac habitasse platea dictumst.', - 'Ut pharetra ligula libero, sit amet imperdiet lorem luctus sit amet.', - 'Sed ex lorem, lacinia et varius vitae, sagittis eget libero.', - 'Vestibulum scelerisque velit sed augue ultricies, ut vestibulum lorem luctus.', - 'Pellentesque et risus pretium, egestas ipsum at, facilisis lectus.', - 'Praesent id eleifend lacus.', - 'Fusce convallis eu tortor sit amet mattis.', - 'Vivamus lacinia magna ut urna feugiat tincidunt.', - 'Sed in diam ut dolor imperdiet vehicula non ac turpis.', - 'Praesent at est hendrerit, laoreet tortor sed, varius mi.', - 'Nunc in odio leo.', - 'Praesent placerat semper libero, ut aliquet dolor.', - 'Vestibulum elementum leo metus, vitae auctor lorem tincidunt ut.', - ]; -} diff --git a/examples/cookbook/effects/gradient_bubbles/pubspec.yaml b/examples/cookbook/effects/gradient_bubbles/pubspec.yaml deleted file mode 100644 index efca27919ed..00000000000 --- a/examples/cookbook/effects/gradient_bubbles/pubspec.yaml +++ /dev/null @@ -1,20 +0,0 @@ -name: gradient_bubbles -description: A new Flutter project. -publish_to: none -version: 1.0.0+1 - -resolution: workspace -environment: - sdk: ^3.11.0 - -dependencies: - flutter: - sdk: flutter - cupertino_icons: ^1.0.8 - -dev_dependencies: - flutter_test: - sdk: flutter - -flutter: - uses-material-design: true diff --git a/examples/pubspec.yaml b/examples/pubspec.yaml index 23ab4600674..84208b495a0 100644 --- a/examples/pubspec.yaml +++ b/examples/pubspec.yaml @@ -38,12 +38,10 @@ workspace: - cookbook/effects/download_button - cookbook/effects/drag_a_widget - cookbook/effects/expandable_fab - - cookbook/effects/gradient_bubbles - cookbook/effects/nested_nav - cookbook/effects/parallax_scrolling - cookbook/effects/shimmer_loading - cookbook/effects/staggered_menu_animation - - cookbook/effects/typing_indicator - cookbook/forms/focus - cookbook/forms/retrieve_input - cookbook/forms/text_field_changes