engineless flutter framework
diff --git a/bin/generate.dart b/bin/generate.dart
new file mode 100644
index 0000000..adc958a
--- /dev/null
+++ b/bin/generate.dart
@@ -0,0 +1,17 @@
+// Copyright 2016 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+void main() {
+
+}
diff --git a/lib/animation.dart b/lib/animation.dart
new file mode 100644
index 0000000..88bec71
--- /dev/null
+++ b/lib/animation.dart
@@ -0,0 +1,169 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter animation system.
+///
+/// To use, import `package:flutter/animation.dart`.
+///
+/// This library provides basic building blocks for implementing animations in
+/// Flutter. Other layers of the framework use these building blocks to provide
+/// advanced animation support for applications. For example, the widget library
+/// includes [ImplicitlyAnimatedWidget]s and [AnimatedWidget]s that make it easy
+/// to animate certain properties of a [Widget]. If those animated widgets are
+/// not sufficient for a given use case, the basic building blocks provided by
+/// this library can be used to implement custom animated effects.
+///
+/// This library depends only on core Dart libraries and the `physics.dart`
+/// library.
+///
+///
+/// ### Foundations: the Animation class
+///
+/// Flutter represents an animation as a value that changes over a given
+/// duration, and that value may be of any type. For example, it could be a
+/// [double] indicating the current opacity of a [Widget] as it fades out. Or,
+/// it could be the current background [Color] of a widget that transitions
+/// smoothly from one color to another. The current value of an animation is
+/// represented by an [Animation] object, which is the central class of the
+/// animation library. In addition to the current animation value, the
+/// [Animation] object also stores the current [AnimationStatus]. The status
+/// indicates whether the animation is currently conceptually running from the
+/// beginning to the end or the other way around. It may also indicate that the
+/// animation is currently stopped at the beginning or the end.
+///
+/// Other objects can register listeners on an [Animation] to be informed
+/// whenever the animation value and/or the animation status changes. A [Widget]
+/// may register such a *value* listener via [Animation.addListener] to rebuild
+/// itself with the current animation value whenever that value changes. For
+/// example, a widget might listen to an animation to update its opacity to the
+/// animation's value every time that value changes. Likewise, registering a
+/// *status* listener via [Animation.addStatusListener] may be useful to trigger
+/// another action when the current animation has ended.
+///
+/// As an example, the following video shows the changes over time in the
+/// current animation status and animation value for the opacity animation of a
+/// widget. This [Animation] is driven by an [AnimationController] (see next
+/// section). Before the animation triggers, the animation status is "dismissed"
+/// and the value is 0.0. As the value runs from 0.0 to 1.0 to fade in the
+/// widget, the status changes to "forward". When the widget is fully faded in
+/// at an animation value of 1.0 the status is "completed". When the animation
+/// triggers again to fade the widget back out, the animation status changes to
+/// "reverse" and the animation value runs back to 0.0. At that point the widget
+/// is fully faded out and the animation status switches back to "dismissed"
+/// until the animation is triggered again.
+///
+/// {@animation 420 100 https://flutter.github.io/assets-for-api-docs/assets/animation/animation_status_value.mp4}
+///
+/// Although you can't instantiate [Animation] directly (it is an abstract
+/// class), you can create one using an [AnimationController].
+///
+///
+/// ### Powering animations: AnimationController
+///
+/// An [AnimationController] is a special kind of [Animation] that advances its
+/// animation value whenever the device running the application is ready to
+/// display a new frame (typically, this rate is around 60 values per second).
+/// An [AnimationController] can be used wherever an [Animation] is expected. As
+/// the name implies, an [AnimationController] also provides control over its
+/// [Animation]: It implements methods to stop the animation at any time and to
+/// run it forward as well as in the reverse direction.
+///
+/// By default, an [AnimationController] increases its animation value linearly
+/// over the given duration from 0.0 to 1.0 when run in the forward direction.
+/// For many use cases you might want the value to be of a different type,
+/// change the range of the animation values, or change how the animation moves
+/// between values. This is achieved by wrapping the animation: Wrapping it in
+/// an [Animatable] (see below) changes the range of animation values to a
+/// different range or type (for example to animate [Color]s or [Rect]s).
+/// Furthermore, a [Curve] can be applied to the animation by wrapping it in a
+/// [CurvedAnimation]. Instead of linearly increasing the animation value, a
+/// curved animation changes its value according to the provided curve. The
+/// framework ships with many built-in curves (see [Curves]). As an example,
+/// [Curves.easeOutCubic] increases the animation value quickly at the beginning
+/// of the animation and then slows down until the target value is reached:
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4}
+///
+///
+/// ### Animating different types: Animatable
+///
+/// An `Animatable<T>` is an object that takes an `Animation<double>` as input
+/// and produces a value of type `T`. Objects of these types can be used to
+/// translate the animation value range of an [AnimationController] (or any
+/// other [Animation] of type [double]) to a different range. That new range
+/// doesn't even have to be of type double anymore. With the help of an
+/// [Animatable] like a [Tween] or a [TweenSequence] (see sections below) an
+/// [AnimationController] can be used to smoothly transition [Color]s, [Rect]s,
+/// [Size]s and many more types from one value to another over a given duration.
+///
+///
+/// ### Interpolating values: Tweens
+///
+/// A [Tween] is applied to an [Animation] of type [double] to change the
+/// range and type of the animation value. For example, to transition the
+/// background of a [Widget] smoothly between two [Color]s, a [ColorTween] can
+/// be used. Each [Tween] specifies a start and an end value. As the animation
+/// value of the [Animation] powering the [Tween] progresses from 0.0 to 1.0 it
+/// produces interpolated values between its start and end value. The values
+/// produced by the [Tween] usually move closer and closer to its end value as
+/// the animation value of the powering [Animation] approaches 1.0.
+///
+/// The following video shows example values produced by an [IntTween], a
+/// `Tween<double>`, and a [ColorTween] as the animation value runs from 0.0 to
+/// 1.0 and back to 0.0:
+///
+/// {@animation 530 150 https://flutter.github.io/assets-for-api-docs/assets/animation/tweens.mp4}
+///
+/// An [Animation] or [AnimationController] can power multiple [Tween]s. For
+/// example, to animate the size and the color of a widget in parallel, create
+/// one [AnimationController] that powers a [SizeTween] and a [ColorTween].
+///
+/// The framework ships with many [Tween] subclasses ([IntTween], [SizeTween],
+/// [RectTween], etc.) to animate common properties.
+///
+///
+/// ### Staggered animations: TweenSequences
+///
+/// A [TweenSequence] can help animate a given property smoothly in stages. Each
+/// [Tween] in the sequence is responsible for a different stage and has an
+/// associated weight. When the animation runs, the stages execute one after
+/// another. For example, let's say you want to animate the background of a
+/// widget from yellow to green and then, after a short pause, to red. For this
+/// you can specify three tweens within a tween sequence: One [ColorTween]
+/// animating from yellow to green, one [ConstantTween] that just holds the color
+/// green, and another [ColorTween] animating from green to red. For each
+/// tween you need to pick a weight indicating the ratio of time spent on that
+/// tween compared to all other tweens. If we assign a weight of 2 to both of
+/// the [ColorTween]s and a weight of 1 to the [ConstantTween] the transition
+/// described by the [ColorTween]s would take twice as long as the
+/// [ConstantTween]. A [TweenSequence] is driven by an [Animation] just like a
+/// regular [Tween]: As the powering [Animation] runs from 0.0 to 1.0 the
+/// [TweenSequence] runs through all of its stages.
+///
+/// The following video shows the animation described in the previous paragraph:
+///
+/// {@animation 646 250 https://flutter.github.io/assets-for-api-docs/assets/animation/tween_sequence.mp4}
+///
+///
+/// See also:
+///
+///  * [Introduction to animations](https://flutter.dev/docs/development/ui/animations)
+///    on flutter.dev.
+///  * [Animations tutorial](https://flutter.dev/docs/development/ui/animations/tutorial)
+///    on flutter.dev.
+///  * [Sample app](https://github.com/flutter/samples/tree/master/animations),
+///    which showcases Flutter's animation features.
+///  * [ImplicitlyAnimatedWidget] and its subclasses, which are [Widget]s that
+///    implicitly animate changes to their properties.
+///  * [AnimatedWidget] and its subclasses, which are [Widget]s that take an
+///    explicit [Animation] to animate their properties.
+library animation;
+
+export 'src/animation/animation.dart';
+export 'src/animation/animation_controller.dart';
+export 'src/animation/animations.dart';
+export 'src/animation/curves.dart';
+export 'src/animation/listener_helpers.dart';
+export 'src/animation/tween.dart';
+export 'src/animation/tween_sequence.dart';
diff --git a/lib/cupertino.dart b/lib/cupertino.dart
new file mode 100644
index 0000000..9c0ef65
--- /dev/null
+++ b/lib/cupertino.dart
@@ -0,0 +1,53 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Flutter widgets implementing the current iOS design language.
+///
+/// To use, import `package:flutter/cupertino.dart`.
+///
+/// This library is designed for apps that run on iOS. For apps that may also
+/// run on other operating systems, we encourage use of other widgets, for
+/// example the [Material
+/// Design](https://flutter.dev/docs/development/ui/widgets/material) set.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=3PdUaidHc-E}
+library cupertino;
+
+export 'src/cupertino/action_sheet.dart';
+export 'src/cupertino/activity_indicator.dart';
+export 'src/cupertino/app.dart';
+export 'src/cupertino/bottom_tab_bar.dart';
+export 'src/cupertino/button.dart';
+export 'src/cupertino/colors.dart';
+export 'src/cupertino/constants.dart';
+export 'src/cupertino/context_menu.dart';
+export 'src/cupertino/context_menu_action.dart';
+export 'src/cupertino/date_picker.dart';
+export 'src/cupertino/dialog.dart';
+export 'src/cupertino/form_row.dart';
+export 'src/cupertino/form_section.dart';
+export 'src/cupertino/icon_theme_data.dart';
+export 'src/cupertino/icons.dart';
+export 'src/cupertino/interface_level.dart';
+export 'src/cupertino/localizations.dart';
+export 'src/cupertino/nav_bar.dart';
+export 'src/cupertino/page_scaffold.dart';
+export 'src/cupertino/picker.dart';
+export 'src/cupertino/refresh.dart';
+export 'src/cupertino/route.dart';
+export 'src/cupertino/scrollbar.dart';
+export 'src/cupertino/search_field.dart';
+export 'src/cupertino/segmented_control.dart';
+export 'src/cupertino/slider.dart';
+export 'src/cupertino/sliding_segmented_control.dart';
+export 'src/cupertino/switch.dart';
+export 'src/cupertino/tab_scaffold.dart';
+export 'src/cupertino/tab_view.dart';
+export 'src/cupertino/text_field.dart';
+export 'src/cupertino/text_form_field_row.dart';
+export 'src/cupertino/text_selection.dart';
+export 'src/cupertino/text_theme.dart';
+export 'src/cupertino/theme.dart';
+export 'src/cupertino/thumb_painter.dart';
+export 'widgets.dart';
diff --git a/lib/fix_data.yaml b/lib/fix_data.yaml
new file mode 100644
index 0000000..cf74e86
--- /dev/null
+++ b/lib/fix_data.yaml
@@ -0,0 +1,36 @@
+# Copyright 2014 The Flutter Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+# TODO(Piinks): Add link to public user guide when available.
+
+# Please add new fixes to the top of the file, separated by one blank line
+# from other fixes. In a comment, include a link to the PR where the change
+# requiring the fix was made.
+
+version: 1
+transforms:
+
+  # Changes made in https://github.com/flutter/flutter/pull/41859
+  - title: 'Remove brightness'
+    date: 2020-12-10
+    element:
+      uris: [ 'cupertino.dart' ]
+      constructor: ''
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+
+  # Changes made in https://github.com/flutter/flutter/pull/41859
+  - title: 'Remove brightness'
+    date: 2020-12-10
+    element:
+      uris: [ 'cupertino.dart' ]
+      method: 'copyWith'
+      inClass: 'CupertinoTextThemeData'
+    changes:
+      - kind: 'removeParameter'
+        name: 'brightness'
+
+# Before adding a new fix: read instructions at the top of this file.
diff --git a/lib/foundation.dart b/lib/foundation.dart
new file mode 100644
index 0000000..82a1c16
--- /dev/null
+++ b/lib/foundation.dart
@@ -0,0 +1,59 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Core Flutter framework primitives.
+///
+/// The features defined in this library are the lowest-level utility
+/// classes and functions used by all the other layers of the Flutter
+/// framework.
+library foundation;
+
+export 'package:meta/meta.dart' show
+  factory,
+  immutable,
+  mustCallSuper,
+  nonVirtual,
+  optionalTypeArgs,
+  protected,
+  required,
+  visibleForTesting;
+
+// Examples can assume:
+// // @dart = 2.9
+// String _name;
+// bool _first;
+// bool _lights;
+// bool _visible;
+// bool inherit;
+// int columns;
+// int rows;
+// class Cat { }
+// double _volume;
+// dynamic _calculation;
+// dynamic _last;
+// dynamic _selection;
+
+export 'src/foundation/annotations.dart';
+export 'src/foundation/assertions.dart';
+export 'src/foundation/basic_types.dart';
+export 'src/foundation/binding.dart';
+export 'src/foundation/bitfield.dart';
+export 'src/foundation/change_notifier.dart';
+export 'src/foundation/collections.dart';
+export 'src/foundation/consolidate_response.dart';
+export 'src/foundation/constants.dart';
+export 'src/foundation/debug.dart';
+export 'src/foundation/diagnostics.dart';
+export 'src/foundation/isolates.dart';
+export 'src/foundation/key.dart';
+export 'src/foundation/licenses.dart';
+export 'src/foundation/node.dart';
+export 'src/foundation/object.dart';
+export 'src/foundation/observer_list.dart';
+export 'src/foundation/platform.dart';
+export 'src/foundation/print.dart';
+export 'src/foundation/serialization.dart';
+export 'src/foundation/stack_frame.dart';
+export 'src/foundation/synchronous_future.dart';
+export 'src/foundation/unicode.dart';
diff --git a/lib/gestures.dart b/lib/gestures.dart
new file mode 100644
index 0000000..7bc4302
--- /dev/null
+++ b/lib/gestures.dart
@@ -0,0 +1,33 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter gesture recognizers.
+///
+/// To use, import `package:flutter/gestures.dart`.
+library gestures;
+
+export 'src/gestures/arena.dart';
+export 'src/gestures/binding.dart';
+export 'src/gestures/constants.dart';
+export 'src/gestures/converter.dart';
+export 'src/gestures/debug.dart';
+export 'src/gestures/drag.dart';
+export 'src/gestures/drag_details.dart';
+export 'src/gestures/eager.dart';
+export 'src/gestures/events.dart';
+export 'src/gestures/force_press.dart';
+export 'src/gestures/hit_test.dart';
+export 'src/gestures/long_press.dart';
+export 'src/gestures/lsq_solver.dart';
+export 'src/gestures/monodrag.dart';
+export 'src/gestures/multidrag.dart';
+export 'src/gestures/multitap.dart';
+export 'src/gestures/pointer_router.dart';
+export 'src/gestures/pointer_signal_resolver.dart';
+export 'src/gestures/recognizer.dart';
+export 'src/gestures/resampler.dart';
+export 'src/gestures/scale.dart';
+export 'src/gestures/tap.dart';
+export 'src/gestures/team.dart';
+export 'src/gestures/velocity_tracker.dart';
diff --git a/lib/material.dart b/lib/material.dart
new file mode 100644
index 0000000..37b1aa5
--- /dev/null
+++ b/lib/material.dart
@@ -0,0 +1,154 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Flutter widgets implementing Material Design.
+///
+/// To use, import `package:flutter/material.dart`.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=DL0Ix1lnC4w}
+///
+/// See also:
+///
+///  * [flutter.dev/widgets](https://flutter.dev/widgets/)
+///    for a catalog of commonly-used Flutter widgets.
+///  * [material.io/design](https://material.io/design/)
+///    for an introduction to Material Design.
+library material;
+
+export 'src/material/about.dart';
+export 'src/material/animated_icons.dart';
+export 'src/material/app.dart';
+export 'src/material/app_bar.dart';
+export 'src/material/app_bar_theme.dart';
+export 'src/material/arc.dart';
+export 'src/material/back_button.dart';
+export 'src/material/banner.dart';
+export 'src/material/banner_theme.dart';
+export 'src/material/bottom_app_bar.dart';
+export 'src/material/bottom_app_bar_theme.dart';
+export 'src/material/bottom_navigation_bar.dart';
+export 'src/material/bottom_navigation_bar_theme.dart';
+export 'src/material/bottom_sheet.dart';
+export 'src/material/bottom_sheet_theme.dart';
+export 'src/material/button.dart';
+export 'src/material/button_bar.dart';
+export 'src/material/button_bar_theme.dart';
+export 'src/material/button_style.dart';
+export 'src/material/button_style_button.dart';
+export 'src/material/button_theme.dart';
+export 'src/material/calendar_date_picker.dart';
+export 'src/material/card.dart';
+export 'src/material/card_theme.dart';
+export 'src/material/checkbox.dart';
+export 'src/material/checkbox_list_tile.dart';
+export 'src/material/checkbox_theme.dart';
+export 'src/material/chip.dart';
+export 'src/material/chip_theme.dart';
+export 'src/material/circle_avatar.dart';
+export 'src/material/color_scheme.dart';
+export 'src/material/colors.dart';
+export 'src/material/constants.dart';
+export 'src/material/curves.dart';
+export 'src/material/data_table.dart';
+export 'src/material/data_table_source.dart';
+export 'src/material/data_table_theme.dart';
+export 'src/material/date.dart';
+export 'src/material/date_picker.dart';
+export 'src/material/date_picker_deprecated.dart';
+export 'src/material/debug.dart';
+export 'src/material/dialog.dart';
+export 'src/material/dialog_theme.dart';
+export 'src/material/divider.dart';
+export 'src/material/divider_theme.dart';
+export 'src/material/drawer.dart';
+export 'src/material/drawer_header.dart';
+export 'src/material/dropdown.dart';
+export 'src/material/elevated_button.dart';
+export 'src/material/elevated_button_theme.dart';
+export 'src/material/elevation_overlay.dart';
+export 'src/material/expand_icon.dart';
+export 'src/material/expansion_panel.dart';
+export 'src/material/expansion_tile.dart';
+export 'src/material/feedback.dart';
+export 'src/material/flat_button.dart';
+export 'src/material/flexible_space_bar.dart';
+export 'src/material/floating_action_button.dart';
+export 'src/material/floating_action_button_location.dart';
+export 'src/material/floating_action_button_theme.dart';
+export 'src/material/flutter_logo.dart';
+export 'src/material/grid_tile.dart';
+export 'src/material/grid_tile_bar.dart';
+export 'src/material/icon_button.dart';
+export 'src/material/icons.dart';
+export 'src/material/ink_decoration.dart';
+export 'src/material/ink_highlight.dart';
+export 'src/material/ink_ripple.dart';
+export 'src/material/ink_splash.dart';
+export 'src/material/ink_well.dart';
+export 'src/material/input_border.dart';
+export 'src/material/input_date_picker_form_field.dart';
+export 'src/material/input_decorator.dart';
+export 'src/material/list_tile.dart';
+export 'src/material/material.dart';
+export 'src/material/material_button.dart';
+export 'src/material/material_localizations.dart';
+export 'src/material/material_state.dart';
+export 'src/material/mergeable_material.dart';
+export 'src/material/navigation_rail.dart';
+export 'src/material/navigation_rail_theme.dart';
+export 'src/material/outline_button.dart';
+export 'src/material/outlined_button.dart';
+export 'src/material/outlined_button_theme.dart';
+export 'src/material/page.dart';
+export 'src/material/page_transitions_theme.dart';
+export 'src/material/paginated_data_table.dart';
+export 'src/material/popup_menu.dart';
+export 'src/material/popup_menu_theme.dart';
+export 'src/material/progress_indicator.dart';
+export 'src/material/radio.dart';
+export 'src/material/radio_list_tile.dart';
+export 'src/material/radio_theme.dart';
+export 'src/material/raised_button.dart';
+export 'src/material/range_slider.dart';
+export 'src/material/refresh_indicator.dart';
+export 'src/material/reorderable_list.dart';
+export 'src/material/scaffold.dart';
+export 'src/material/scrollbar.dart';
+export 'src/material/search.dart';
+export 'src/material/selectable_text.dart';
+export 'src/material/shadows.dart';
+export 'src/material/slider.dart';
+export 'src/material/slider_theme.dart';
+export 'src/material/snack_bar.dart';
+export 'src/material/snack_bar_theme.dart';
+export 'src/material/stepper.dart';
+export 'src/material/switch.dart';
+export 'src/material/switch_list_tile.dart';
+export 'src/material/switch_theme.dart';
+export 'src/material/tab_bar_theme.dart';
+export 'src/material/tab_controller.dart';
+export 'src/material/tab_indicator.dart';
+export 'src/material/tabs.dart';
+export 'src/material/text_button.dart';
+export 'src/material/text_button_theme.dart';
+export 'src/material/text_field.dart';
+export 'src/material/text_form_field.dart';
+export 'src/material/text_selection.dart';
+export 'src/material/text_selection_theme.dart';
+export 'src/material/text_selection_toolbar.dart';
+export 'src/material/text_selection_toolbar_text_button.dart';
+export 'src/material/text_theme.dart';
+export 'src/material/theme.dart';
+export 'src/material/theme_data.dart';
+export 'src/material/time.dart';
+export 'src/material/time_picker.dart';
+export 'src/material/time_picker_theme.dart';
+export 'src/material/toggle_buttons.dart';
+export 'src/material/toggle_buttons_theme.dart';
+export 'src/material/toggleable.dart';
+export 'src/material/tooltip.dart';
+export 'src/material/tooltip_theme.dart';
+export 'src/material/typography.dart';
+export 'src/material/user_accounts_drawer_header.dart';
+export 'widgets.dart';
diff --git a/lib/painting.dart b/lib/painting.dart
new file mode 100644
index 0000000..b73acac
--- /dev/null
+++ b/lib/painting.dart
@@ -0,0 +1,61 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter painting library.
+///
+/// To use, import `package:flutter/painting.dart`.
+///
+/// This library includes a variety of classes that wrap the Flutter
+/// engine's painting API for more specialized purposes, such as painting scaled
+/// images, interpolating between shadows, painting borders around boxes, etc.
+///
+/// In particular:
+///
+///  * Use the [TextPainter] class for painting text.
+///  * Use [Decoration] (and more concretely [BoxDecoration]) for
+///    painting boxes.
+library painting;
+
+export 'package:flute/ui.dart' show Shadow, PlaceholderAlignment, TextHeightBehavior;
+
+export 'src/painting/alignment.dart';
+export 'src/painting/basic_types.dart';
+export 'src/painting/beveled_rectangle_border.dart';
+export 'src/painting/binding.dart';
+export 'src/painting/border_radius.dart';
+export 'src/painting/borders.dart';
+export 'src/painting/box_border.dart';
+export 'src/painting/box_decoration.dart';
+export 'src/painting/box_fit.dart';
+export 'src/painting/box_shadow.dart';
+export 'src/painting/circle_border.dart';
+export 'src/painting/clip.dart';
+export 'src/painting/colors.dart';
+export 'src/painting/continuous_rectangle_border.dart';
+export 'src/painting/debug.dart';
+export 'src/painting/decoration.dart';
+export 'src/painting/decoration_image.dart';
+export 'src/painting/edge_insets.dart';
+export 'src/painting/flutter_logo.dart';
+export 'src/painting/fractional_offset.dart';
+export 'src/painting/geometry.dart';
+export 'src/painting/gradient.dart';
+export 'src/painting/image_cache.dart';
+export 'src/painting/image_decoder.dart';
+export 'src/painting/image_provider.dart';
+export 'src/painting/image_resolution.dart';
+export 'src/painting/image_stream.dart';
+export 'src/painting/inline_span.dart';
+export 'src/painting/matrix_utils.dart';
+export 'src/painting/notched_shapes.dart';
+export 'src/painting/paint_utilities.dart';
+export 'src/painting/placeholder_span.dart';
+export 'src/painting/rounded_rectangle_border.dart';
+export 'src/painting/shader_warm_up.dart';
+export 'src/painting/shape_decoration.dart';
+export 'src/painting/stadium_border.dart';
+export 'src/painting/strut_style.dart';
+export 'src/painting/text_painter.dart';
+export 'src/painting/text_span.dart';
+export 'src/painting/text_style.dart';
diff --git a/lib/physics.dart b/lib/physics.dart
new file mode 100644
index 0000000..2475523
--- /dev/null
+++ b/lib/physics.dart
@@ -0,0 +1,17 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Simple one-dimensional physics simulations, such as springs, friction, and
+/// gravity, for use in user interface animations.
+///
+/// To use, import `package:flutter/physics.dart`.
+library physics;
+
+export 'src/physics/clamped_simulation.dart';
+export 'src/physics/friction_simulation.dart';
+export 'src/physics/gravity_simulation.dart';
+export 'src/physics/simulation.dart';
+export 'src/physics/spring_simulation.dart';
+export 'src/physics/tolerance.dart';
+export 'src/physics/utils.dart';
diff --git a/lib/rendering.dart b/lib/rendering.dart
new file mode 100644
index 0000000..3f08fbf
--- /dev/null
+++ b/lib/rendering.dart
@@ -0,0 +1,75 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter rendering tree.
+///
+/// To use, import `package:flutter/rendering.dart`.
+///
+/// The [RenderObject] hierarchy is used by the Flutter Widgets library to
+/// implement its layout and painting back-end. Generally, while you may use
+/// custom [RenderBox] classes for specific effects in your applications, most
+/// of the time your only interaction with the [RenderObject] hierarchy will be
+/// in debugging layout issues.
+///
+/// If you are developing your own library or application directly on top of the
+/// rendering library, then you will want to have a binding (see [BindingBase]).
+/// You can use [RenderingFlutterBinding], or you can create your own binding.
+/// If you create your own binding, it needs to import at least
+/// [ServicesBinding], [GestureBinding], [SchedulerBinding], [PaintingBinding],
+/// and [RendererBinding]. The rendering library does not automatically create a
+/// binding, but relies on one being initialized with those features.
+library rendering;
+
+export 'package:flute/foundation.dart' show
+  VoidCallback,
+  ValueChanged,
+  ValueGetter,
+  ValueSetter,
+  DiagnosticLevel;
+export 'package:flute/semantics.dart';
+export 'package:vector_math/vector_math_64.dart' show Matrix4;
+
+export 'src/rendering/animated_size.dart';
+export 'src/rendering/binding.dart';
+export 'src/rendering/box.dart';
+export 'src/rendering/custom_layout.dart';
+export 'src/rendering/custom_paint.dart';
+export 'src/rendering/debug.dart';
+export 'src/rendering/debug_overflow_indicator.dart';
+export 'src/rendering/editable.dart';
+export 'src/rendering/error.dart';
+export 'src/rendering/flex.dart';
+export 'src/rendering/flow.dart';
+export 'src/rendering/image.dart';
+export 'src/rendering/layer.dart';
+export 'src/rendering/layout_helper.dart';
+export 'src/rendering/list_body.dart';
+export 'src/rendering/list_wheel_viewport.dart';
+export 'src/rendering/mouse_cursor.dart';
+export 'src/rendering/mouse_tracking.dart';
+export 'src/rendering/object.dart';
+export 'src/rendering/paragraph.dart';
+export 'src/rendering/performance_overlay.dart';
+export 'src/rendering/platform_view.dart';
+export 'src/rendering/proxy_box.dart';
+export 'src/rendering/proxy_sliver.dart';
+export 'src/rendering/rotated_box.dart';
+export 'src/rendering/shifted_box.dart';
+export 'src/rendering/sliver.dart';
+export 'src/rendering/sliver_fill.dart';
+export 'src/rendering/sliver_fixed_extent_list.dart';
+export 'src/rendering/sliver_grid.dart';
+export 'src/rendering/sliver_list.dart';
+export 'src/rendering/sliver_multi_box_adaptor.dart';
+export 'src/rendering/sliver_padding.dart';
+export 'src/rendering/sliver_persistent_header.dart';
+export 'src/rendering/stack.dart';
+export 'src/rendering/table.dart';
+export 'src/rendering/table_border.dart';
+export 'src/rendering/texture.dart';
+export 'src/rendering/tweens.dart';
+export 'src/rendering/view.dart';
+export 'src/rendering/viewport.dart';
+export 'src/rendering/viewport_offset.dart';
+export 'src/rendering/wrap.dart';
diff --git a/lib/scheduler.dart b/lib/scheduler.dart
new file mode 100644
index 0000000..f1cbdc2
--- /dev/null
+++ b/lib/scheduler.dart
@@ -0,0 +1,19 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter Scheduler library.
+///
+/// To use, import `package:flutter/scheduler.dart`.
+///
+/// This library is responsible for scheduler frame callbacks, and tasks at
+/// given priorities.
+///
+/// The library makes sure that tasks are only run when appropriate.
+/// For example, an idle-task is only executed when no animation is running.
+library scheduler;
+
+export 'src/scheduler/binding.dart';
+export 'src/scheduler/debug.dart';
+export 'src/scheduler/priority.dart';
+export 'src/scheduler/ticker.dart';
diff --git a/lib/semantics.dart b/lib/semantics.dart
new file mode 100644
index 0000000..f838d2a
--- /dev/null
+++ b/lib/semantics.dart
@@ -0,0 +1,19 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The Flutter semantics package.
+///
+/// To use, import `package:flutter/semantics.dart`.
+///
+/// The [SemanticsEvent] classes define the protocol for sending semantic events
+/// to the platform.
+///
+/// The [SemanticsNode] hierarchy represents the semantic structure of the UI
+/// and is used by the platform-specific accessibility services.
+library semantics;
+
+export 'src/semantics/binding.dart';
+export 'src/semantics/debug.dart';
+export 'src/semantics/semantics.dart';
+export 'src/semantics/semantics_service.dart';
diff --git a/lib/services.dart b/lib/services.dart
new file mode 100644
index 0000000..2d9715f
--- /dev/null
+++ b/lib/services.dart
@@ -0,0 +1,42 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// Platform services exposed to Flutter apps.
+///
+/// To use, import `package:flutter/services.dart`.
+///
+/// This library depends only on core Dart libraries and the `foundation`
+/// library.
+library services;
+
+export 'src/services/asset_bundle.dart';
+export 'src/services/autofill.dart';
+export 'src/services/binary_messenger.dart';
+export 'src/services/binding.dart';
+export 'src/services/clipboard.dart';
+export 'src/services/font_loader.dart';
+export 'src/services/haptic_feedback.dart';
+export 'src/services/keyboard_key.dart';
+export 'src/services/keyboard_maps.dart';
+export 'src/services/message_codec.dart';
+export 'src/services/message_codecs.dart';
+export 'src/services/platform_channel.dart';
+export 'src/services/platform_messages.dart';
+export 'src/services/platform_views.dart';
+export 'src/services/raw_keyboard.dart';
+export 'src/services/raw_keyboard_android.dart';
+export 'src/services/raw_keyboard_fuchsia.dart';
+export 'src/services/raw_keyboard_ios.dart';
+export 'src/services/raw_keyboard_linux.dart';
+export 'src/services/raw_keyboard_macos.dart';
+export 'src/services/raw_keyboard_web.dart';
+export 'src/services/raw_keyboard_windows.dart';
+export 'src/services/restoration.dart';
+export 'src/services/system_channels.dart';
+export 'src/services/system_chrome.dart';
+export 'src/services/system_navigator.dart';
+export 'src/services/system_sound.dart';
+export 'src/services/text_editing.dart';
+export 'src/services/text_formatter.dart';
+export 'src/services/text_input.dart';
diff --git a/lib/src/animation/animation.dart b/lib/src/animation/animation.dart
new file mode 100644
index 0000000..07732af
--- /dev/null
+++ b/lib/src/animation/animation.dart
@@ -0,0 +1,205 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'package:flute/foundation.dart';
+
+import 'tween.dart';
+
+// Examples can assume:
+// late AnimationController _controller;
+
+/// The status of an animation.
+enum AnimationStatus {
+  /// The animation is stopped at the beginning.
+  dismissed,
+
+  /// The animation is running from beginning to end.
+  forward,
+
+  /// The animation is running backwards, from end to beginning.
+  reverse,
+
+  /// The animation is stopped at the end.
+  completed,
+}
+
+/// Signature for listeners attached using [Animation.addStatusListener].
+typedef AnimationStatusListener = void Function(AnimationStatus status);
+
+/// An animation with a value of type `T`.
+///
+/// An animation consists of a value (of type `T`) together with a status. The
+/// status indicates whether the animation is conceptually running from
+/// beginning to end or from the end back to the beginning, although the actual
+/// value of the animation might not change monotonically (e.g., if the
+/// animation uses a curve that bounces).
+///
+/// Animations also let other objects listen for changes to either their value
+/// or their status. These callbacks are called during the "animation" phase of
+/// the pipeline, just prior to rebuilding widgets.
+///
+/// To create a new animation that you can run forward and backward, consider
+/// using [AnimationController].
+///
+/// See also:
+///
+///  * [Tween], which can be used to create [Animation] subclasses that
+///    convert `Animation<double>`s into other kinds of `Animation`s.
+abstract class Animation<T> extends Listenable implements ValueListenable<T> {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Animation();
+
+  // keep these next five dartdocs in sync with the dartdocs in AnimationWithParentMixin<T>
+
+  /// Calls the listener every time the value of the animation changes.
+  ///
+  /// Listeners can be removed with [removeListener].
+  @override
+  void addListener(VoidCallback listener);
+
+  /// Stop calling the listener every time the value of the animation changes.
+  ///
+  /// If `listener` is not currently registered as a listener, this method does
+  /// nothing.
+  ///
+  /// Listeners can be added with [addListener].
+  @override
+  void removeListener(VoidCallback listener);
+
+  /// Calls listener every time the status of the animation changes.
+  ///
+  /// Listeners can be removed with [removeStatusListener].
+  void addStatusListener(AnimationStatusListener listener);
+
+  /// Stops calling the listener every time the status of the animation changes.
+  ///
+  /// If `listener` is not currently registered as a status listener, this
+  /// method does nothing.
+  ///
+  /// Listeners can be added with [addStatusListener].
+  void removeStatusListener(AnimationStatusListener listener);
+
+  /// The current status of this animation.
+  AnimationStatus get status;
+
+  /// The current value of the animation.
+  @override
+  T get value;
+
+  /// Whether this animation is stopped at the beginning.
+  bool get isDismissed => status == AnimationStatus.dismissed;
+
+  /// Whether this animation is stopped at the end.
+  bool get isCompleted => status == AnimationStatus.completed;
+
+  /// Chains a [Tween] (or [CurveTween]) to this [Animation].
+  ///
+  /// This method is only valid for `Animation<double>` instances (i.e. when `T`
+  /// is `double`). This means, for instance, that it can be called on
+  /// [AnimationController] objects, as well as [CurvedAnimation]s,
+  /// [ProxyAnimation]s, [ReverseAnimation]s, [TrainHoppingAnimation]s, etc.
+  ///
+  /// It returns an [Animation] specialized to the same type, `U`, as the
+  /// argument to the method (`child`), whose value is derived by applying the
+  /// given [Tween] to the value of this [Animation].
+  ///
+  /// {@tool snippet}
+  ///
+  /// Given an [AnimationController] `_controller`, the following code creates
+  /// an `Animation<Alignment>` that swings from top left to top right as the
+  /// controller goes from 0.0 to 1.0:
+  ///
+  /// ```dart
+  /// Animation<Alignment> _alignment1 = _controller.drive(
+  ///   AlignmentTween(
+  ///     begin: Alignment.topLeft,
+  ///     end: Alignment.topRight,
+  ///   ),
+  /// );
+  /// ```
+  /// {@end-tool}
+  /// {@tool snippet}
+  ///
+  /// The `_alignment.value` could then be used in a widget's build method, for
+  /// instance, to position a child using an [Align] widget such that the
+  /// position of the child shifts over time from the top left to the top right.
+  ///
+  /// It is common to ease this kind of curve, e.g. making the transition slower
+  /// at the start and faster at the end. The following snippet shows one way to
+  /// chain the alignment tween in the previous example to an easing curve (in
+  /// this case, [Curves.easeIn]). In this example, the tween is created
+  /// elsewhere as a variable that can be reused, since none of its arguments
+  /// vary.
+  ///
+  /// ```dart
+  /// final Animatable<Alignment> _tween = AlignmentTween(begin: Alignment.topLeft, end: Alignment.topRight)
+  ///   .chain(CurveTween(curve: Curves.easeIn));
+  /// // ...
+  /// Animation<Alignment> _alignment2 = _controller.drive(_tween);
+  /// ```
+  /// {@end-tool}
+  /// {@tool snippet}
+  ///
+  /// The following code is exactly equivalent, and is typically clearer when
+  /// the tweens are created inline, as might be preferred when the tweens have
+  /// values that depend on other variables:
+  ///
+  /// ```dart
+  /// Animation<Alignment> _alignment3 = _controller
+  ///   .drive(CurveTween(curve: Curves.easeIn))
+  ///   .drive(AlignmentTween(
+  ///     begin: Alignment.topLeft,
+  ///     end: Alignment.topRight,
+  ///   ));
+  /// ```
+  /// {@end-tool}
+  ///
+  /// See also:
+  ///
+  ///  * [Animatable.animate], which does the same thing.
+  ///  * [AnimationController], which is usually used to drive animations.
+  ///  * [CurvedAnimation], an alternative to [CurveTween] for applying easing
+  ///    curves, which supports distinct curves in the forward direction and the
+  ///    reverse direction.
+  @optionalTypeArgs
+  Animation<U> drive<U>(Animatable<U> child) {
+    assert(this is Animation<double>);
+    return child.animate(this as Animation<double>);
+  }
+
+  @override
+  String toString() {
+    return '${describeIdentity(this)}(${toStringDetails()})';
+  }
+
+  /// Provides a string describing the status of this object, but not including
+  /// information about the object itself.
+  ///
+  /// This function is used by [Animation.toString] so that [Animation]
+  /// subclasses can provide additional details while ensuring all [Animation]
+  /// subclasses have a consistent [toString] style.
+  ///
+  /// The result of this function includes an icon describing the status of this
+  /// [Animation] object:
+  ///
+  /// * "&#x25B6;": [AnimationStatus.forward] ([value] increasing)
+  /// * "&#x25C0;": [AnimationStatus.reverse] ([value] decreasing)
+  /// * "&#x23ED;": [AnimationStatus.completed] ([value] == 1.0)
+  /// * "&#x23EE;": [AnimationStatus.dismissed] ([value] == 0.0)
+  String toStringDetails() {
+    assert(status != null);
+    switch (status) {
+      case AnimationStatus.forward:
+        return '\u25B6'; // >
+      case AnimationStatus.reverse:
+        return '\u25C0'; // <
+      case AnimationStatus.completed:
+        return '\u23ED'; // >>|
+      case AnimationStatus.dismissed:
+        return '\u23EE'; // |<<
+    }
+  }
+}
diff --git a/lib/src/animation/animation_controller.dart b/lib/src/animation/animation_controller.dart
new file mode 100644
index 0000000..2e38629
--- /dev/null
+++ b/lib/src/animation/animation_controller.dart
@@ -0,0 +1,910 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'package:flute/ui.dart' as ui show lerpDouble;
+
+import 'package:flute/foundation.dart';
+import 'package:flute/physics.dart';
+import 'package:flute/scheduler.dart';
+import 'package:flute/semantics.dart';
+
+import 'animation.dart';
+import 'curves.dart';
+import 'listener_helpers.dart';
+
+export 'package:flute/scheduler.dart' show TickerFuture, TickerCanceled;
+
+// Examples can assume:
+// late AnimationController _controller, fadeAnimationController, sizeAnimationController;
+// late bool dismissed;
+// void setState(VoidCallback fn) { }
+
+/// The direction in which an animation is running.
+enum _AnimationDirection {
+  /// The animation is running from beginning to end.
+  forward,
+
+  /// The animation is running backwards, from end to beginning.
+  reverse,
+}
+
+final SpringDescription _kFlingSpringDescription = SpringDescription.withDampingRatio(
+  mass: 1.0,
+  stiffness: 500.0,
+  ratio: 1.0,
+);
+
+const Tolerance _kFlingTolerance = Tolerance(
+  velocity: double.infinity,
+  distance: 0.01,
+);
+
+/// Configures how an [AnimationController] behaves when animations are
+/// disabled.
+///
+/// When [AccessibilityFeatures.disableAnimations] is true, the device is asking
+/// Flutter to reduce or disable animations as much as possible. To honor this,
+/// we reduce the duration and the corresponding number of frames for
+/// animations. This enum is used to allow certain [AnimationController]s to opt
+/// out of this behavior.
+///
+/// For example, the [AnimationController] which controls the physics simulation
+/// for a scrollable list will have [AnimationBehavior.preserve], so that when
+/// a user attempts to scroll it does not jump to the end/beginning too quickly.
+enum AnimationBehavior {
+  /// The [AnimationController] will reduce its duration when
+  /// [AccessibilityFeatures.disableAnimations] is true.
+  normal,
+
+  /// The [AnimationController] will preserve its behavior.
+  ///
+  /// This is the default for repeating animations in order to prevent them from
+  /// flashing rapidly on the screen if the widget does not take the
+  /// [AccessibilityFeatures.disableAnimations] flag into account.
+  preserve,
+}
+
+/// A controller for an animation.
+///
+/// This class lets you perform tasks such as:
+///
+/// * Play an animation [forward] or in [reverse], or [stop] an animation.
+/// * Set the animation to a specific [value].
+/// * Define the [upperBound] and [lowerBound] values of an animation.
+/// * Create a [fling] animation effect using a physics simulation.
+///
+/// By default, an [AnimationController] linearly produces values that range
+/// from 0.0 to 1.0, during a given duration. The animation controller generates
+/// a new value whenever the device running your app is ready to display a new
+/// frame (typically, this rate is around 60 values per second).
+///
+/// ## Ticker providers
+///
+/// An [AnimationController] needs a [TickerProvider], which is configured using
+/// the `vsync` argument on the constructor.
+///
+/// The [TickerProvider] interface describes a factory for [Ticker] objects. A
+/// [Ticker] is an object that knows how to register itself with the
+/// [SchedulerBinding] and fires a callback every frame. The
+/// [AnimationController] class uses a [Ticker] to step through the animation
+/// that it controls.
+///
+/// If an [AnimationController] is being created from a [State], then the State
+/// can use the [TickerProviderStateMixin] and [SingleTickerProviderStateMixin]
+/// classes to implement the [TickerProvider] interface. The
+/// [TickerProviderStateMixin] class always works for this purpose; the
+/// [SingleTickerProviderStateMixin] is slightly more efficient in the case of
+/// the class only ever needing one [Ticker] (e.g. if the class creates only a
+/// single [AnimationController] during its entire lifetime).
+///
+/// The widget test framework [WidgetTester] object can be used as a ticker
+/// provider in the context of tests. In other contexts, you will have to either
+/// pass a [TickerProvider] from a higher level (e.g. indirectly from a [State]
+/// that mixes in [TickerProviderStateMixin]), or create a custom
+/// [TickerProvider] subclass.
+///
+/// ## Life cycle
+///
+/// An [AnimationController] should be [dispose]d when it is no longer needed.
+/// This reduces the likelihood of leaks. When used with a [StatefulWidget], it
+/// is common for an [AnimationController] to be created in the
+/// [State.initState] method and then disposed in the [State.dispose] method.
+///
+/// ## Using [Future]s with [AnimationController]
+///
+/// The methods that start animations return a [TickerFuture] object which
+/// completes when the animation completes successfully, and never throws an
+/// error; if the animation is canceled, the future never completes. This object
+/// also has a [TickerFuture.orCancel] property which returns a future that
+/// completes when the animation completes successfully, and completes with an
+/// error when the animation is aborted.
+///
+/// This can be used to write code such as the `fadeOutAndUpdateState` method
+/// below.
+///
+/// {@tool snippet}
+///
+/// Here is a stateful `Foo` widget. Its [State] uses the
+/// [SingleTickerProviderStateMixin] to implement the necessary
+/// [TickerProvider], creating its controller in the [State.initState] method
+/// and disposing of it in the [State.dispose] method. The duration of the
+/// controller is configured from a property in the `Foo` widget; as that
+/// changes, the [State.didUpdateWidget] method is used to update the
+/// controller.
+///
+/// ```dart
+/// class Foo extends StatefulWidget {
+///   Foo({ Key? key, required this.duration }) : super(key: key);
+///
+///   final Duration duration;
+///
+///   @override
+///   _FooState createState() => _FooState();
+/// }
+///
+/// class _FooState extends State<Foo> with SingleTickerProviderStateMixin {
+///   late AnimationController _controller;
+///
+///   @override
+///   void initState() {
+///     super.initState();
+///     _controller = AnimationController(
+///       vsync: this, // the SingleTickerProviderStateMixin
+///       duration: widget.duration,
+///     );
+///   }
+///
+///   @override
+///   void didUpdateWidget(Foo oldWidget) {
+///     super.didUpdateWidget(oldWidget);
+///     _controller.duration = widget.duration;
+///   }
+///
+///   @override
+///   void dispose() {
+///     _controller.dispose();
+///     super.dispose();
+///   }
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     return Container(); // ...
+///   }
+/// }
+/// ```
+/// {@end-tool}
+/// {@tool snippet}
+///
+/// The following method (for a [State] subclass) drives two animation
+/// controllers using Dart's asynchronous syntax for awaiting [Future] objects:
+///
+/// ```dart
+/// Future<void> fadeOutAndUpdateState() async {
+///   try {
+///     await fadeAnimationController.forward().orCancel;
+///     await sizeAnimationController.forward().orCancel;
+///     setState(() {
+///       dismissed = true;
+///     });
+///   } on TickerCanceled {
+///     // the animation got canceled, probably because we were disposed
+///   }
+/// }
+/// ```
+/// {@end-tool}
+///
+/// The assumption in the code above is that the animation controllers are being
+/// disposed in the [State] subclass' override of the [State.dispose] method.
+/// Since disposing the controller cancels the animation (raising a
+/// [TickerCanceled] exception), the code here can skip verifying whether
+/// [State.mounted] is still true at each step. (Again, this assumes that the
+/// controllers are created in [State.initState] and disposed in
+/// [State.dispose], as described in the previous section.)
+///
+/// See also:
+///
+///  * [Tween], the base class for converting an [AnimationController] to a
+///    range of values of other types.
+class AnimationController extends Animation<double>
+  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
+  /// Creates an animation controller.
+  ///
+  /// * `value` is the initial value of the animation. If defaults to the lower
+  ///   bound.
+  ///
+  /// * [duration] is the length of time this animation should last.
+  ///
+  /// * [debugLabel] is a string to help identify this animation during
+  ///   debugging (used by [toString]).
+  ///
+  /// * [lowerBound] is the smallest value this animation can obtain and the
+  ///   value at which this animation is deemed to be dismissed. It cannot be
+  ///   null.
+  ///
+  /// * [upperBound] is the largest value this animation can obtain and the
+  ///   value at which this animation is deemed to be completed. It cannot be
+  ///   null.
+  ///
+  /// * `vsync` is the [TickerProvider] for the current context. It can be
+  ///   changed by calling [resync]. It is required and must not be null. See
+  ///   [TickerProvider] for advice on obtaining a ticker provider.
+  AnimationController({
+    double? value,
+    this.duration,
+    this.reverseDuration,
+    this.debugLabel,
+    this.lowerBound = 0.0,
+    this.upperBound = 1.0,
+    this.animationBehavior = AnimationBehavior.normal,
+    required TickerProvider vsync,
+  }) : assert(lowerBound != null),
+       assert(upperBound != null),
+       assert(upperBound >= lowerBound),
+       assert(vsync != null),
+       _direction = _AnimationDirection.forward {
+    _ticker = vsync.createTicker(_tick);
+    _internalSetValue(value ?? lowerBound);
+  }
+
+  /// Creates an animation controller with no upper or lower bound for its
+  /// value.
+  ///
+  /// * [value] is the initial value of the animation.
+  ///
+  /// * [duration] is the length of time this animation should last.
+  ///
+  /// * [debugLabel] is a string to help identify this animation during
+  ///   debugging (used by [toString]).
+  ///
+  /// * `vsync` is the [TickerProvider] for the current context. It can be
+  ///   changed by calling [resync]. It is required and must not be null. See
+  ///   [TickerProvider] for advice on obtaining a ticker provider.
+  ///
+  /// This constructor is most useful for animations that will be driven using a
+  /// physics simulation, especially when the physics simulation has no
+  /// pre-determined bounds.
+  AnimationController.unbounded({
+    double value = 0.0,
+    this.duration,
+    this.reverseDuration,
+    this.debugLabel,
+    required TickerProvider vsync,
+    this.animationBehavior = AnimationBehavior.preserve,
+  }) : assert(value != null),
+       assert(vsync != null),
+       lowerBound = double.negativeInfinity,
+       upperBound = double.infinity,
+       _direction = _AnimationDirection.forward {
+    _ticker = vsync.createTicker(_tick);
+    _internalSetValue(value);
+  }
+
+  /// The value at which this animation is deemed to be dismissed.
+  final double lowerBound;
+
+  /// The value at which this animation is deemed to be completed.
+  final double upperBound;
+
+  /// A label that is used in the [toString] output. Intended to aid with
+  /// identifying animation controller instances in debug output.
+  final String? debugLabel;
+
+  /// The behavior of the controller when [AccessibilityFeatures.disableAnimations]
+  /// is true.
+  ///
+  /// Defaults to [AnimationBehavior.normal] for the [new AnimationController]
+  /// constructor, and [AnimationBehavior.preserve] for the
+  /// [new AnimationController.unbounded] constructor.
+  final AnimationBehavior animationBehavior;
+
+  /// Returns an [Animation<double>] for this animation controller, so that a
+  /// pointer to this object can be passed around without allowing users of that
+  /// pointer to mutate the [AnimationController] state.
+  Animation<double> get view => this;
+
+  /// The length of time this animation should last.
+  ///
+  /// If [reverseDuration] is specified, then [duration] is only used when going
+  /// [forward]. Otherwise, it specifies the duration going in both directions.
+  Duration? duration;
+
+  /// The length of time this animation should last when going in [reverse].
+  ///
+  /// The value of [duration] us used if [reverseDuration] is not specified or
+  /// set to null.
+  Duration? reverseDuration;
+
+  Ticker? _ticker;
+
+  /// Recreates the [Ticker] with the new [TickerProvider].
+  void resync(TickerProvider vsync) {
+    final Ticker oldTicker = _ticker!;
+    _ticker = vsync.createTicker(_tick);
+    _ticker!.absorbTicker(oldTicker);
+  }
+
+  Simulation? _simulation;
+
+  /// The current value of the animation.
+  ///
+  /// Setting this value notifies all the listeners that the value
+  /// changed.
+  ///
+  /// Setting this value also stops the controller if it is currently
+  /// running; if this happens, it also notifies all the status
+  /// listeners.
+  @override
+  double get value => _value;
+  late double _value;
+  /// Stops the animation controller and sets the current value of the
+  /// animation.
+  ///
+  /// The new value is clamped to the range set by [lowerBound] and
+  /// [upperBound].
+  ///
+  /// Value listeners are notified even if this does not change the value.
+  /// Status listeners are notified if the animation was previously playing.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// See also:
+  ///
+  ///  * [reset], which is equivalent to setting [value] to [lowerBound].
+  ///  * [stop], which aborts the animation without changing its value or status
+  ///    and without dispatching any notifications other than completing or
+  ///    canceling the [TickerFuture].
+  ///  * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
+  ///    which start the animation controller.
+  set value(double newValue) {
+    assert(newValue != null);
+    stop();
+    _internalSetValue(newValue);
+    notifyListeners();
+    _checkStatusChanged();
+  }
+
+  /// Sets the controller's value to [lowerBound], stopping the animation (if
+  /// in progress), and resetting to its beginning point, or dismissed state.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// See also:
+  ///
+  ///  * [value], which can be explicitly set to a specific value as desired.
+  ///  * [forward], which starts the animation in the forward direction.
+  ///  * [stop], which aborts the animation without changing its value or status
+  ///    and without dispatching any notifications other than completing or
+  ///    canceling the [TickerFuture].
+  void reset() {
+    value = lowerBound;
+  }
+
+  /// The rate of change of [value] per second.
+  ///
+  /// If [isAnimating] is false, then [value] is not changing and the rate of
+  /// change is zero.
+  double get velocity {
+    if (!isAnimating)
+      return 0.0;
+    return _simulation!.dx(lastElapsedDuration!.inMicroseconds.toDouble() / Duration.microsecondsPerSecond);
+  }
+
+  void _internalSetValue(double newValue) {
+    _value = newValue.clamp(lowerBound, upperBound);
+    if (_value == lowerBound) {
+      _status = AnimationStatus.dismissed;
+    } else if (_value == upperBound) {
+      _status = AnimationStatus.completed;
+    } else {
+      _status = (_direction == _AnimationDirection.forward) ?
+        AnimationStatus.forward :
+        AnimationStatus.reverse;
+    }
+  }
+
+  /// The amount of time that has passed between the time the animation started
+  /// and the most recent tick of the animation.
+  ///
+  /// If the controller is not animating, the last elapsed duration is null.
+  Duration? get lastElapsedDuration => _lastElapsedDuration;
+  Duration? _lastElapsedDuration;
+
+  /// Whether this animation is currently animating in either the forward or reverse direction.
+  ///
+  /// This is separate from whether it is actively ticking. An animation
+  /// controller's ticker might get muted, in which case the animation
+  /// controller's callbacks will no longer fire even though time is continuing
+  /// to pass. See [Ticker.muted] and [TickerMode].
+  bool get isAnimating => _ticker != null && _ticker!.isActive;
+
+  _AnimationDirection _direction;
+
+  @override
+  AnimationStatus get status => _status;
+  late AnimationStatus _status;
+
+  /// Starts running this animation forwards (towards the end).
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is complete.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// During the animation, [status] is reported as [AnimationStatus.forward],
+  /// which switches to [AnimationStatus.completed] when [upperBound] is
+  /// reached at the end of the animation.
+  TickerFuture forward({ double? from }) {
+    assert(() {
+      if (duration == null) {
+        throw FlutterError(
+          'AnimationController.forward() called with no default duration.\n'
+          'The "duration" property should be set, either in the constructor or later, before '
+          'calling the forward() function.'
+        );
+      }
+      return true;
+    }());
+    assert(
+      _ticker != null,
+      'AnimationController.forward() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    _direction = _AnimationDirection.forward;
+    if (from != null)
+      value = from;
+    return _animateToInternal(upperBound);
+  }
+
+  /// Starts running this animation in reverse (towards the beginning).
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is dismissed.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// During the animation, [status] is reported as [AnimationStatus.reverse],
+  /// which switches to [AnimationStatus.dismissed] when [lowerBound] is
+  /// reached at the end of the animation.
+  TickerFuture reverse({ double? from }) {
+    assert(() {
+      if (duration == null && reverseDuration == null) {
+        throw FlutterError(
+          'AnimationController.reverse() called with no default duration or reverseDuration.\n'
+          'The "duration" or "reverseDuration" property should be set, either in the constructor or later, before '
+          'calling the reverse() function.'
+        );
+      }
+      return true;
+    }());
+    assert(
+      _ticker != null,
+      'AnimationController.reverse() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    _direction = _AnimationDirection.reverse;
+    if (from != null)
+      value = from;
+    return _animateToInternal(lowerBound);
+  }
+
+  /// Drives the animation from its current value to target.
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is complete.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// During the animation, [status] is reported as [AnimationStatus.forward]
+  /// regardless of whether `target` > [value] or not. At the end of the
+  /// animation, when `target` is reached, [status] is reported as
+  /// [AnimationStatus.completed].
+  TickerFuture animateTo(double target, { Duration? duration, Curve curve = Curves.linear }) {
+    assert(() {
+      if (this.duration == null && duration == null) {
+        throw FlutterError(
+          'AnimationController.animateTo() called with no explicit duration and no default duration.\n'
+          'Either the "duration" argument to the animateTo() method should be provided, or the '
+          '"duration" property should be set, either in the constructor or later, before '
+          'calling the animateTo() function.'
+        );
+      }
+      return true;
+    }());
+    assert(
+      _ticker != null,
+      'AnimationController.animateTo() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    _direction = _AnimationDirection.forward;
+    return _animateToInternal(target, duration: duration, curve: curve);
+  }
+
+  /// Drives the animation from its current value to target.
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is complete.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// During the animation, [status] is reported as [AnimationStatus.reverse]
+  /// regardless of whether `target` < [value] or not. At the end of the
+  /// animation, when `target` is reached, [status] is reported as
+  /// [AnimationStatus.dismissed].
+  TickerFuture animateBack(double target, { Duration? duration, Curve curve = Curves.linear }) {
+    assert(() {
+      if (this.duration == null && reverseDuration == null && duration == null) {
+        throw FlutterError(
+          'AnimationController.animateBack() called with no explicit duration and no default duration or reverseDuration.\n'
+          'Either the "duration" argument to the animateBack() method should be provided, or the '
+          '"duration" or "reverseDuration" property should be set, either in the constructor or later, before '
+          'calling the animateBack() function.'
+        );
+      }
+      return true;
+    }());
+    assert(
+      _ticker != null,
+      'AnimationController.animateBack() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    _direction = _AnimationDirection.reverse;
+    return _animateToInternal(target, duration: duration, curve: curve);
+  }
+
+  TickerFuture _animateToInternal(double target, { Duration? duration, Curve curve = Curves.linear }) {
+    double scale = 1.0;
+    if (SemanticsBinding.instance!.disableAnimations) {
+      switch (animationBehavior) {
+        case AnimationBehavior.normal:
+          // Since the framework cannot handle zero duration animations, we run it at 5% of the normal
+          // duration to limit most animations to a single frame.
+          // TODO(jonahwilliams): determine a better process for setting duration.
+          scale = 0.05;
+          break;
+        case AnimationBehavior.preserve:
+          break;
+      }
+    }
+    Duration? simulationDuration = duration;
+    if (simulationDuration == null) {
+      assert(!(this.duration == null && _direction == _AnimationDirection.forward));
+      assert(!(this.duration == null && _direction == _AnimationDirection.reverse && reverseDuration == null));
+      final double range = upperBound - lowerBound;
+      final double remainingFraction = range.isFinite ? (target - _value).abs() / range : 1.0;
+      final Duration directionDuration =
+        (_direction == _AnimationDirection.reverse && reverseDuration != null)
+        ? reverseDuration!
+        : this.duration!;
+      simulationDuration = directionDuration * remainingFraction;
+    } else if (target == value) {
+      // Already at target, don't animate.
+      simulationDuration = Duration.zero;
+    }
+    stop();
+    if (simulationDuration == Duration.zero) {
+      if (value != target) {
+        _value = target.clamp(lowerBound, upperBound);
+        notifyListeners();
+      }
+      _status = (_direction == _AnimationDirection.forward) ?
+        AnimationStatus.completed :
+        AnimationStatus.dismissed;
+      _checkStatusChanged();
+      return TickerFuture.complete();
+    }
+    assert(simulationDuration > Duration.zero);
+    assert(!isAnimating);
+    return _startSimulation(_InterpolationSimulation(_value, target, simulationDuration, curve, scale));
+  }
+
+  /// Starts running this animation in the forward direction, and
+  /// restarts the animation when it completes.
+  ///
+  /// Defaults to repeating between the [lowerBound] and [upperBound] of the
+  /// [AnimationController] when no explicit value is set for [min] and [max].
+  ///
+  /// With [reverse] set to true, instead of always starting over at [min]
+  /// the starting value will alternate between [min] and [max] values on each
+  /// repeat. The [status] will be reported as [AnimationStatus.reverse] when
+  /// the animation runs from [max] to [min].
+  ///
+  /// Each run of the animation will have a duration of `period`. If `period` is not
+  /// provided, [duration] will be used instead, which has to be set before [repeat] is
+  /// called either in the constructor or later by using the [duration] setter.
+  ///
+  /// Returns a [TickerFuture] that never completes. The [TickerFuture.orCancel] future
+  /// completes with an error when the animation is stopped (e.g. with [stop]).
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  TickerFuture repeat({ double? min, double? max, bool reverse = false, Duration? period }) {
+    min ??= lowerBound;
+    max ??= upperBound;
+    period ??= duration;
+    assert(() {
+      if (period == null) {
+        throw FlutterError(
+          'AnimationController.repeat() called without an explicit period and with no default Duration.\n'
+          'Either the "period" argument to the repeat() method should be provided, or the '
+          '"duration" property should be set, either in the constructor or later, before '
+          'calling the repeat() function.'
+        );
+      }
+      return true;
+    }());
+    assert(max >= min);
+    assert(max <= upperBound && min >= lowerBound);
+    assert(reverse != null);
+    stop();
+    return _startSimulation(_RepeatingSimulation(_value, min, max, reverse, period!, _directionSetter));
+  }
+
+  void _directionSetter(_AnimationDirection direction) {
+    _direction = direction;
+    _status = (_direction == _AnimationDirection.forward) ?
+      AnimationStatus.forward :
+      AnimationStatus.reverse;
+    _checkStatusChanged();
+  }
+
+  /// Drives the animation with a spring (within [lowerBound] and [upperBound])
+  /// and initial velocity.
+  ///
+  /// If velocity is positive, the animation will complete, otherwise it will
+  /// dismiss.
+  ///
+  /// The [springDescription] parameter can be used to specify a custom [SpringType.criticallyDamped]
+  /// or [SpringType.overDamped] spring to drive the animation with. Defaults to null, which uses a
+  /// [SpringType.criticallyDamped] spring. See [SpringDescription.withDampingRatio] for how
+  /// to create a suitable [SpringDescription].
+  ///
+  /// The resulting spring simulation cannot be of type [SpringType.underDamped],
+  /// as this can lead to unexpected look of the produced animation.
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is complete.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  TickerFuture fling({ double velocity = 1.0, SpringDescription? springDescription, AnimationBehavior? animationBehavior }) {
+    springDescription ??= _kFlingSpringDescription;
+    _direction = velocity < 0.0 ? _AnimationDirection.reverse : _AnimationDirection.forward;
+    final double target = velocity < 0.0 ? lowerBound - _kFlingTolerance.distance
+                                         : upperBound + _kFlingTolerance.distance;
+    double scale = 1.0;
+    final AnimationBehavior behavior = animationBehavior ?? this.animationBehavior;
+    if (SemanticsBinding.instance!.disableAnimations) {
+      switch (behavior) {
+        case AnimationBehavior.normal:
+          // TODO(jonahwilliams): determine a better process for setting velocity.
+          // the value below was arbitrarily chosen because it worked for the drawer widget.
+          scale = 200.0;
+          break;
+        case AnimationBehavior.preserve:
+          break;
+      }
+    }
+    final SpringSimulation simulation = SpringSimulation(springDescription, value, target, velocity * scale)
+      ..tolerance = _kFlingTolerance;
+    assert(
+      simulation.type != SpringType.underDamped,
+      'The resulting spring simulation is of type SpringType.underDamped.\n'
+      'This can lead to unexpected look of the animation, please adjust the springDescription parameter'
+    );
+    stop();
+    return _startSimulation(simulation);
+  }
+
+  /// Drives the animation according to the given simulation.
+  ///
+  /// The values from the simulation are clamped to the [lowerBound] and
+  /// [upperBound]. To avoid this, consider creating the [AnimationController]
+  /// using the [new AnimationController.unbounded] constructor.
+  ///
+  /// Returns a [TickerFuture] that completes when the animation is complete.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  ///
+  /// The [status] is always [AnimationStatus.forward] for the entire duration
+  /// of the simulation.
+  TickerFuture animateWith(Simulation simulation) {
+    assert(
+      _ticker != null,
+      'AnimationController.animateWith() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    stop();
+    _direction = _AnimationDirection.forward;
+    return _startSimulation(simulation);
+  }
+
+  TickerFuture _startSimulation(Simulation simulation) {
+    assert(simulation != null);
+    assert(!isAnimating);
+    _simulation = simulation;
+    _lastElapsedDuration = Duration.zero;
+    _value = simulation.x(0.0).clamp(lowerBound, upperBound);
+    final TickerFuture result = _ticker!.start();
+    _status = (_direction == _AnimationDirection.forward) ?
+      AnimationStatus.forward :
+      AnimationStatus.reverse;
+    _checkStatusChanged();
+    return result;
+  }
+
+  /// Stops running this animation.
+  ///
+  /// This does not trigger any notifications. The animation stops in its
+  /// current state.
+  ///
+  /// By default, the most recently returned [TickerFuture] is marked as having
+  /// been canceled, meaning the future never completes and its
+  /// [TickerFuture.orCancel] derivative future completes with a [TickerCanceled]
+  /// error. By passing the `canceled` argument with the value false, this is
+  /// reversed, and the futures complete successfully.
+  ///
+  /// See also:
+  ///
+  ///  * [reset], which stops the animation and resets it to the [lowerBound],
+  ///    and which does send notifications.
+  ///  * [forward], [reverse], [animateTo], [animateWith], [fling], and [repeat],
+  ///    which restart the animation controller.
+  void stop({ bool canceled = true }) {
+    assert(
+      _ticker != null,
+      'AnimationController.stop() called after AnimationController.dispose()\n'
+      'AnimationController methods should not be used after calling dispose.'
+    );
+    _simulation = null;
+    _lastElapsedDuration = null;
+    _ticker!.stop(canceled: canceled);
+  }
+
+  /// Release the resources used by this object. The object is no longer usable
+  /// after this method is called.
+  ///
+  /// The most recently returned [TickerFuture], if any, is marked as having been
+  /// canceled, meaning the future never completes and its [TickerFuture.orCancel]
+  /// derivative future completes with a [TickerCanceled] error.
+  @override
+  void dispose() {
+    assert(() {
+      if (_ticker == null) {
+        throw FlutterError.fromParts(<DiagnosticsNode>[
+          ErrorSummary('AnimationController.dispose() called more than once.'),
+          ErrorDescription('A given $runtimeType cannot be disposed more than once.\n'),
+          DiagnosticsProperty<AnimationController>(
+            'The following $runtimeType object was disposed multiple times',
+            this,
+            style: DiagnosticsTreeStyle.errorProperty,
+          ),
+        ]);
+      }
+      return true;
+    }());
+    _ticker!.dispose();
+    _ticker = null;
+    super.dispose();
+  }
+
+  AnimationStatus _lastReportedStatus = AnimationStatus.dismissed;
+  void _checkStatusChanged() {
+    final AnimationStatus newStatus = status;
+    if (_lastReportedStatus != newStatus) {
+      _lastReportedStatus = newStatus;
+      notifyStatusListeners(newStatus);
+    }
+  }
+
+  void _tick(Duration elapsed) {
+    _lastElapsedDuration = elapsed;
+    final double elapsedInSeconds = elapsed.inMicroseconds.toDouble() / Duration.microsecondsPerSecond;
+    assert(elapsedInSeconds >= 0.0);
+    _value = _simulation!.x(elapsedInSeconds).clamp(lowerBound, upperBound);
+    if (_simulation!.isDone(elapsedInSeconds)) {
+      _status = (_direction == _AnimationDirection.forward) ?
+        AnimationStatus.completed :
+        AnimationStatus.dismissed;
+      stop(canceled: false);
+    }
+    notifyListeners();
+    _checkStatusChanged();
+  }
+
+  @override
+  String toStringDetails() {
+    final String paused = isAnimating ? '' : '; paused';
+    final String ticker = _ticker == null ? '; DISPOSED' : (_ticker!.muted ? '; silenced' : '');
+    final String label = debugLabel == null ? '' : '; for $debugLabel';
+    final String more = '${super.toStringDetails()} ${value.toStringAsFixed(3)}';
+    return '$more$paused$ticker$label';
+  }
+}
+
+class _InterpolationSimulation extends Simulation {
+  _InterpolationSimulation(this._begin, this._end, Duration duration, this._curve, double scale)
+    : assert(_begin != null),
+      assert(_end != null),
+      assert(duration != null && duration.inMicroseconds > 0),
+      _durationInSeconds = (duration.inMicroseconds * scale) / Duration.microsecondsPerSecond;
+
+  final double _durationInSeconds;
+  final double _begin;
+  final double _end;
+  final Curve _curve;
+
+  @override
+  double x(double timeInSeconds) {
+    final double t = (timeInSeconds / _durationInSeconds).clamp(0.0, 1.0);
+    if (t == 0.0)
+      return _begin;
+    else if (t == 1.0)
+      return _end;
+    else
+      return _begin + (_end - _begin) * _curve.transform(t);
+  }
+
+  @override
+  double dx(double timeInSeconds) {
+    final double epsilon = tolerance.time;
+    return (x(timeInSeconds + epsilon) - x(timeInSeconds - epsilon)) / (2 * epsilon);
+  }
+
+  @override
+  bool isDone(double timeInSeconds) => timeInSeconds > _durationInSeconds;
+}
+
+typedef _DirectionSetter = void Function(_AnimationDirection direction);
+
+class _RepeatingSimulation extends Simulation {
+  _RepeatingSimulation(double initialValue, this.min, this.max, this.reverse, Duration period, this.directionSetter)
+      : _periodInSeconds = period.inMicroseconds / Duration.microsecondsPerSecond,
+        _initialT = (max == min) ? 0.0 : (initialValue / (max - min)) * (period.inMicroseconds / Duration.microsecondsPerSecond) {
+    assert(_periodInSeconds > 0.0);
+    assert(_initialT >= 0.0);
+  }
+
+  final double min;
+  final double max;
+  final bool reverse;
+  final _DirectionSetter directionSetter;
+
+  final double _periodInSeconds;
+  final double _initialT;
+
+  @override
+  double x(double timeInSeconds) {
+    assert(timeInSeconds >= 0.0);
+
+    final double totalTimeInSeconds = timeInSeconds + _initialT;
+    final double t = (totalTimeInSeconds / _periodInSeconds) % 1.0;
+    final bool _isPlayingReverse = (totalTimeInSeconds ~/ _periodInSeconds).isOdd;
+
+    if (reverse && _isPlayingReverse) {
+      directionSetter(_AnimationDirection.reverse);
+      return ui.lerpDouble(max, min, t)!;
+    } else {
+      directionSetter(_AnimationDirection.forward);
+      return ui.lerpDouble(min, max, t)!;
+    }
+  }
+
+  @override
+  double dx(double timeInSeconds) => (max - min) / _periodInSeconds;
+
+  @override
+  bool isDone(double timeInSeconds) => false;
+}
diff --git a/lib/src/animation/animations.dart b/lib/src/animation/animations.dart
new file mode 100644
index 0000000..2c5539f
--- /dev/null
+++ b/lib/src/animation/animations.dart
@@ -0,0 +1,725 @@
+// Copyright 2014 The Flutter Authors. 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:flute/foundation.dart';
+
+import 'animation.dart';
+import 'curves.dart';
+import 'listener_helpers.dart';
+
+// Examples can assume:
+// late AnimationController controller;
+
+class _AlwaysCompleteAnimation extends Animation<double> {
+  const _AlwaysCompleteAnimation();
+
+  @override
+  void addListener(VoidCallback listener) { }
+
+  @override
+  void removeListener(VoidCallback listener) { }
+
+  @override
+  void addStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  void removeStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  AnimationStatus get status => AnimationStatus.completed;
+
+  @override
+  double get value => 1.0;
+
+  @override
+  String toString() => 'kAlwaysCompleteAnimation';
+}
+
+/// An animation that is always complete.
+///
+/// Using this constant involves less overhead than building an
+/// [AnimationController] with an initial value of 1.0. This is useful when an
+/// API expects an animation but you don't actually want to animate anything.
+const Animation<double> kAlwaysCompleteAnimation = _AlwaysCompleteAnimation();
+
+class _AlwaysDismissedAnimation extends Animation<double> {
+  const _AlwaysDismissedAnimation();
+
+  @override
+  void addListener(VoidCallback listener) { }
+
+  @override
+  void removeListener(VoidCallback listener) { }
+
+  @override
+  void addStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  void removeStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  AnimationStatus get status => AnimationStatus.dismissed;
+
+  @override
+  double get value => 0.0;
+
+  @override
+  String toString() => 'kAlwaysDismissedAnimation';
+}
+
+/// An animation that is always dismissed.
+///
+/// Using this constant involves less overhead than building an
+/// [AnimationController] with an initial value of 0.0. This is useful when an
+/// API expects an animation but you don't actually want to animate anything.
+const Animation<double> kAlwaysDismissedAnimation = _AlwaysDismissedAnimation();
+
+/// An animation that is always stopped at a given value.
+///
+/// The [status] is always [AnimationStatus.forward].
+class AlwaysStoppedAnimation<T> extends Animation<T> {
+  /// Creates an [AlwaysStoppedAnimation] with the given value.
+  ///
+  /// Since the [value] and [status] of an [AlwaysStoppedAnimation] can never
+  /// change, the listeners can never be called. It is therefore safe to reuse
+  /// an [AlwaysStoppedAnimation] instance in multiple places. If the [value] to
+  /// be used is known at compile time, the constructor should be called as a
+  /// `const` constructor.
+  const AlwaysStoppedAnimation(this.value);
+
+  @override
+  final T value;
+
+  @override
+  void addListener(VoidCallback listener) { }
+
+  @override
+  void removeListener(VoidCallback listener) { }
+
+  @override
+  void addStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  void removeStatusListener(AnimationStatusListener listener) { }
+
+  @override
+  AnimationStatus get status => AnimationStatus.forward;
+
+  @override
+  String toStringDetails() {
+    return '${super.toStringDetails()} $value; paused';
+  }
+}
+
+/// Implements most of the [Animation] interface by deferring its behavior to a
+/// given [parent] Animation.
+///
+/// To implement an [Animation] that is driven by a parent, it is only necessary
+/// to mix in this class, implement [parent], and implement `T get value`.
+///
+/// To define a mapping from values in the range 0..1, consider subclassing
+/// [Tween] instead.
+mixin AnimationWithParentMixin<T> {
+  /// The animation whose value this animation will proxy.
+  ///
+  /// This animation must remain the same for the lifetime of this object. If
+  /// you wish to proxy a different animation at different times, consider using
+  /// [ProxyAnimation].
+  Animation<T> get parent;
+
+  // keep these next five dartdocs in sync with the dartdocs in Animation<T>
+
+  /// Calls the listener every time the value of the animation changes.
+  ///
+  /// Listeners can be removed with [removeListener].
+  void addListener(VoidCallback listener) => parent.addListener(listener);
+
+  /// Stop calling the listener every time the value of the animation changes.
+  ///
+  /// Listeners can be added with [addListener].
+  void removeListener(VoidCallback listener) => parent.removeListener(listener);
+
+  /// Calls listener every time the status of the animation changes.
+  ///
+  /// Listeners can be removed with [removeStatusListener].
+  void addStatusListener(AnimationStatusListener listener) => parent.addStatusListener(listener);
+
+  /// Stops calling the listener every time the status of the animation changes.
+  ///
+  /// Listeners can be added with [addStatusListener].
+  void removeStatusListener(AnimationStatusListener listener) => parent.removeStatusListener(listener);
+
+  /// The current status of this animation.
+  AnimationStatus get status => parent.status;
+}
+
+/// An animation that is a proxy for another animation.
+///
+/// A proxy animation is useful because the parent animation can be mutated. For
+/// example, one object can create a proxy animation, hand the proxy to another
+/// object, and then later change the animation from which the proxy receives
+/// its value.
+class ProxyAnimation extends Animation<double>
+  with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
+
+  /// Creates a proxy animation.
+  ///
+  /// If the animation argument is omitted, the proxy animation will have the
+  /// status [AnimationStatus.dismissed] and a value of 0.0.
+  ProxyAnimation([Animation<double>? animation]) {
+    _parent = animation;
+    if (_parent == null) {
+      _status = AnimationStatus.dismissed;
+      _value = 0.0;
+    }
+  }
+
+  AnimationStatus? _status;
+  double? _value;
+
+  /// The animation whose value this animation will proxy.
+  ///
+  /// This value is mutable. When mutated, the listeners on the proxy animation
+  /// will be transparently updated to be listening to the new parent animation.
+  Animation<double>? get parent => _parent;
+  Animation<double>? _parent;
+  set parent(Animation<double>? value) {
+    if (value == _parent)
+      return;
+    if (_parent != null) {
+      _status = _parent!.status;
+      _value = _parent!.value;
+      if (isListening)
+        didStopListening();
+    }
+    _parent = value;
+    if (_parent != null) {
+      if (isListening)
+        didStartListening();
+      if (_value != _parent!.value)
+        notifyListeners();
+      if (_status != _parent!.status)
+        notifyStatusListeners(_parent!.status);
+      _status = null;
+      _value = null;
+    }
+  }
+
+  @override
+  void didStartListening() {
+    if (_parent != null) {
+      _parent!.addListener(notifyListeners);
+      _parent!.addStatusListener(notifyStatusListeners);
+    }
+  }
+
+  @override
+  void didStopListening() {
+    if (_parent != null) {
+      _parent!.removeListener(notifyListeners);
+      _parent!.removeStatusListener(notifyStatusListeners);
+    }
+  }
+
+  @override
+  AnimationStatus get status => _parent != null ? _parent!.status : _status!;
+
+  @override
+  double get value => _parent != null ? _parent!.value : _value!;
+
+  @override
+  String toString() {
+    if (parent == null)
+      return '${objectRuntimeType(this, 'ProxyAnimation')}(null; ${super.toStringDetails()} ${value.toStringAsFixed(3)})';
+    return '$parent\u27A9${objectRuntimeType(this, 'ProxyAnimation')}';
+  }
+}
+
+/// An animation that is the reverse of another animation.
+///
+/// If the parent animation is running forward from 0.0 to 1.0, this animation
+/// is running in reverse from 1.0 to 0.0.
+///
+/// Using a [ReverseAnimation] is different from simply using a [Tween] with a
+/// begin of 1.0 and an end of 0.0 because the tween does not change the status
+/// or direction of the animation.
+///
+/// See also:
+///
+///  * [Curve.flipped] and [FlippedCurve], which provide a similar effect but on
+///    [Curve]s.
+///  * [CurvedAnimation], which can take separate curves for when the animation
+///    is going forward than for when it is going in reverse.
+class ReverseAnimation extends Animation<double>
+  with AnimationLazyListenerMixin, AnimationLocalStatusListenersMixin {
+
+  /// Creates a reverse animation.
+  ///
+  /// The parent argument must not be null.
+  ReverseAnimation(this.parent)
+    : assert(parent != null);
+
+  /// The animation whose value and direction this animation is reversing.
+  final Animation<double> parent;
+
+  @override
+  void addListener(VoidCallback listener) {
+    didRegisterListener();
+    parent.addListener(listener);
+  }
+
+  @override
+  void removeListener(VoidCallback listener) {
+    parent.removeListener(listener);
+    didUnregisterListener();
+  }
+
+  @override
+  void didStartListening() {
+    parent.addStatusListener(_statusChangeHandler);
+  }
+
+  @override
+  void didStopListening() {
+    parent.removeStatusListener(_statusChangeHandler);
+  }
+
+  void _statusChangeHandler(AnimationStatus status) {
+    notifyStatusListeners(_reverseStatus(status));
+  }
+
+  @override
+  AnimationStatus get status => _reverseStatus(parent.status);
+
+  @override
+  double get value => 1.0 - parent.value;
+
+  AnimationStatus _reverseStatus(AnimationStatus status) {
+    assert(status != null);
+    switch (status) {
+      case AnimationStatus.forward: return AnimationStatus.reverse;
+      case AnimationStatus.reverse: return AnimationStatus.forward;
+      case AnimationStatus.completed: return AnimationStatus.dismissed;
+      case AnimationStatus.dismissed: return AnimationStatus.completed;
+    }
+  }
+
+  @override
+  String toString() {
+    return '$parent\u27AA${objectRuntimeType(this, 'ReverseAnimation')}';
+  }
+}
+
+/// An animation that applies a curve to another animation.
+///
+/// [CurvedAnimation] is useful when you want to apply a non-linear [Curve] to
+/// an animation object, especially if you want different curves when the
+/// animation is going forward vs when it is going backward.
+///
+/// Depending on the given curve, the output of the [CurvedAnimation] could have
+/// a wider range than its input. For example, elastic curves such as
+/// [Curves.elasticIn] will significantly overshoot or undershoot the default
+/// range of 0.0 to 1.0.
+///
+/// If you want to apply a [Curve] to a [Tween], consider using [CurveTween].
+///
+/// {@tool snippet}
+///
+/// The following code snippet shows how you can apply a curve to a linear
+/// animation produced by an [AnimationController] `controller`.
+///
+/// ```dart
+/// final Animation<double> animation = CurvedAnimation(
+///   parent: controller,
+///   curve: Curves.ease,
+/// );
+/// ```
+/// {@end-tool}
+/// {@tool snippet}
+///
+/// This second code snippet shows how to apply a different curve in the forward
+/// direction than in the reverse direction. This can't be done using a
+/// [CurveTween] (since [Tween]s are not aware of the animation direction when
+/// they are applied).
+///
+/// ```dart
+/// final Animation<double> animation = CurvedAnimation(
+///   parent: controller,
+///   curve: Curves.easeIn,
+///   reverseCurve: Curves.easeOut,
+/// );
+/// ```
+/// {@end-tool}
+///
+/// By default, the [reverseCurve] matches the forward [curve].
+///
+/// See also:
+///
+///  * [CurveTween], for an alternative way of expressing the first sample
+///    above.
+///  * [AnimationController], for examples of creating and disposing of an
+///    [AnimationController].
+///  * [Curve.flipped] and [FlippedCurve], which provide the reverse of a
+///    [Curve].
+class CurvedAnimation extends Animation<double> with AnimationWithParentMixin<double> {
+  /// Creates a curved animation.
+  ///
+  /// The parent and curve arguments must not be null.
+  CurvedAnimation({
+    required this.parent,
+    required this.curve,
+    this.reverseCurve,
+  }) : assert(parent != null),
+       assert(curve != null) {
+    _updateCurveDirection(parent.status);
+    parent.addStatusListener(_updateCurveDirection);
+  }
+
+  /// The animation to which this animation applies a curve.
+  @override
+  final Animation<double> parent;
+
+  /// The curve to use in the forward direction.
+  Curve curve;
+
+  /// The curve to use in the reverse direction.
+  ///
+  /// If the parent animation changes direction without first reaching the
+  /// [AnimationStatus.completed] or [AnimationStatus.dismissed] status, the
+  /// [CurvedAnimation] stays on the same curve (albeit in the opposite
+  /// direction) to avoid visual discontinuities.
+  ///
+  /// If you use a non-null [reverseCurve], you might want to hold this object
+  /// in a [State] object rather than recreating it each time your widget builds
+  /// in order to take advantage of the state in this object that avoids visual
+  /// discontinuities.
+  ///
+  /// If this field is null, uses [curve] in both directions.
+  Curve? reverseCurve;
+
+  /// The direction used to select the current curve.
+  ///
+  /// The curve direction is only reset when we hit the beginning or the end of
+  /// the timeline to avoid discontinuities in the value of any variables this
+  /// animation is used to animate.
+  AnimationStatus? _curveDirection;
+
+  void _updateCurveDirection(AnimationStatus status) {
+    switch (status) {
+      case AnimationStatus.dismissed:
+      case AnimationStatus.completed:
+        _curveDirection = null;
+        break;
+      case AnimationStatus.forward:
+        _curveDirection ??= AnimationStatus.forward;
+        break;
+      case AnimationStatus.reverse:
+        _curveDirection ??= AnimationStatus.reverse;
+        break;
+    }
+  }
+
+  bool get _useForwardCurve {
+    return reverseCurve == null || (_curveDirection ?? parent.status) != AnimationStatus.reverse;
+  }
+
+  @override
+  double get value {
+    final Curve? activeCurve = _useForwardCurve ? curve : reverseCurve;
+
+    final double t = parent.value;
+    if (activeCurve == null)
+      return t;
+    if (t == 0.0 || t == 1.0) {
+      assert(() {
+        final double transformedValue = activeCurve.transform(t);
+        final double roundedTransformedValue = transformedValue.round().toDouble();
+        if (roundedTransformedValue != t) {
+          throw FlutterError(
+            'Invalid curve endpoint at $t.\n'
+            'Curves must map 0.0 to near zero and 1.0 to near one but '
+            '${activeCurve.runtimeType} mapped $t to $transformedValue, which '
+            'is near $roundedTransformedValue.'
+          );
+        }
+        return true;
+      }());
+      return t;
+    }
+    return activeCurve.transform(t);
+  }
+
+  @override
+  String toString() {
+    if (reverseCurve == null)
+      return '$parent\u27A9$curve';
+    if (_useForwardCurve)
+      return '$parent\u27A9$curve\u2092\u2099/$reverseCurve';
+    return '$parent\u27A9$curve/$reverseCurve\u2092\u2099';
+  }
+}
+
+enum _TrainHoppingMode { minimize, maximize }
+
+/// This animation starts by proxying one animation, but when the value of that
+/// animation crosses the value of the second (either because the second is
+/// going in the opposite direction, or because the one overtakes the other),
+/// the animation hops over to proxying the second animation.
+///
+/// When the [TrainHoppingAnimation] starts proxying the second animation
+/// instead of the first, the [onSwitchedTrain] callback is called.
+///
+/// If the two animations start at the same value, then the
+/// [TrainHoppingAnimation] immediately hops to the second animation, and the
+/// [onSwitchedTrain] callback is not called. If only one animation is provided
+/// (i.e. if the second is null), then the [TrainHoppingAnimation] just proxies
+/// the first animation.
+///
+/// Since this object must track the two animations even when it has no
+/// listeners of its own, instead of shutting down when all its listeners are
+/// removed, it exposes a [dispose()] method. Call this method to shut this
+/// object down.
+class TrainHoppingAnimation extends Animation<double>
+  with AnimationEagerListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
+
+  /// Creates a train-hopping animation.
+  ///
+  /// The current train argument must not be null but the next train argument
+  /// can be null. If the next train is null, then this object will just proxy
+  /// the first animation and never hop.
+  TrainHoppingAnimation(
+    Animation<double> this._currentTrain,
+    this._nextTrain, {
+    this.onSwitchedTrain,
+  }) : assert(_currentTrain != null) {
+    if (_nextTrain != null) {
+      if (_currentTrain!.value == _nextTrain!.value) {
+        _currentTrain = _nextTrain;
+        _nextTrain = null;
+      } else if (_currentTrain!.value > _nextTrain!.value) {
+        _mode = _TrainHoppingMode.maximize;
+      } else {
+        assert(_currentTrain!.value < _nextTrain!.value);
+        _mode = _TrainHoppingMode.minimize;
+      }
+    }
+    _currentTrain!.addStatusListener(_statusChangeHandler);
+    _currentTrain!.addListener(_valueChangeHandler);
+    _nextTrain?.addListener(_valueChangeHandler);
+    assert(_mode != null || _nextTrain == null);
+  }
+
+  /// The animation that is currently driving this animation.
+  ///
+  /// The identity of this object will change from the first animation to the
+  /// second animation when [onSwitchedTrain] is called.
+  Animation<double>? get currentTrain => _currentTrain;
+  Animation<double>? _currentTrain;
+  Animation<double>? _nextTrain;
+  _TrainHoppingMode? _mode;
+
+  /// Called when this animation switches to be driven by the second animation.
+  ///
+  /// This is not called if the two animations provided to the constructor have
+  /// the same value at the time of the call to the constructor. In that case,
+  /// the second animation is used from the start, and the first is ignored.
+  VoidCallback? onSwitchedTrain;
+
+  AnimationStatus? _lastStatus;
+  void _statusChangeHandler(AnimationStatus status) {
+    assert(_currentTrain != null);
+    if (status != _lastStatus) {
+      notifyListeners();
+      _lastStatus = status;
+    }
+    assert(_lastStatus != null);
+  }
+
+  @override
+  AnimationStatus get status => _currentTrain!.status;
+
+  double? _lastValue;
+  void _valueChangeHandler() {
+    assert(_currentTrain != null);
+    bool hop = false;
+    if (_nextTrain != null) {
+      assert(_mode != null);
+      switch (_mode!) {
+        case _TrainHoppingMode.minimize:
+          hop = _nextTrain!.value <= _currentTrain!.value;
+          break;
+        case _TrainHoppingMode.maximize:
+          hop = _nextTrain!.value >= _currentTrain!.value;
+          break;
+      }
+      if (hop) {
+        _currentTrain!
+          ..removeStatusListener(_statusChangeHandler)
+          ..removeListener(_valueChangeHandler);
+        _currentTrain = _nextTrain;
+        _nextTrain = null;
+        _currentTrain!.addStatusListener(_statusChangeHandler);
+        _statusChangeHandler(_currentTrain!.status);
+      }
+    }
+    final double newValue = value;
+    if (newValue != _lastValue) {
+      notifyListeners();
+      _lastValue = newValue;
+    }
+    assert(_lastValue != null);
+    if (hop && onSwitchedTrain != null)
+      onSwitchedTrain!();
+  }
+
+  @override
+  double get value => _currentTrain!.value;
+
+  /// Frees all the resources used by this performance.
+  /// After this is called, this object is no longer usable.
+  @override
+  void dispose() {
+    assert(_currentTrain != null);
+    _currentTrain!.removeStatusListener(_statusChangeHandler);
+    _currentTrain!.removeListener(_valueChangeHandler);
+    _currentTrain = null;
+    _nextTrain?.removeListener(_valueChangeHandler);
+    _nextTrain = null;
+    super.dispose();
+  }
+
+  @override
+  String toString() {
+    if (_nextTrain != null)
+      return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(next: $_nextTrain)';
+    return '$currentTrain\u27A9${objectRuntimeType(this, 'TrainHoppingAnimation')}(no next)';
+  }
+}
+
+/// An interface for combining multiple Animations. Subclasses need only
+/// implement the `value` getter to control how the child animations are
+/// combined. Can be chained to combine more than 2 animations.
+///
+/// For example, to create an animation that is the sum of two others, subclass
+/// this class and define `T get value = first.value + second.value;`
+///
+/// By default, the [status] of a [CompoundAnimation] is the status of the
+/// [next] animation if [next] is moving, and the status of the [first]
+/// animation otherwise.
+abstract class CompoundAnimation<T> extends Animation<T>
+  with AnimationLazyListenerMixin, AnimationLocalListenersMixin, AnimationLocalStatusListenersMixin {
+  /// Creates a CompoundAnimation. Both arguments must be non-null. Either can
+  /// be a CompoundAnimation itself to combine multiple animations.
+  CompoundAnimation({
+    required this.first,
+    required this.next,
+  }) : assert(first != null),
+       assert(next != null);
+
+  /// The first sub-animation. Its status takes precedence if neither are
+  /// animating.
+  final Animation<T> first;
+
+  /// The second sub-animation.
+  final Animation<T> next;
+
+  @override
+  void didStartListening() {
+    first.addListener(_maybeNotifyListeners);
+    first.addStatusListener(_maybeNotifyStatusListeners);
+    next.addListener(_maybeNotifyListeners);
+    next.addStatusListener(_maybeNotifyStatusListeners);
+  }
+
+  @override
+  void didStopListening() {
+    first.removeListener(_maybeNotifyListeners);
+    first.removeStatusListener(_maybeNotifyStatusListeners);
+    next.removeListener(_maybeNotifyListeners);
+    next.removeStatusListener(_maybeNotifyStatusListeners);
+  }
+
+  /// Gets the status of this animation based on the [first] and [next] status.
+  ///
+  /// The default is that if the [next] animation is moving, use its status.
+  /// Otherwise, default to [first].
+  @override
+  AnimationStatus get status {
+    if (next.status == AnimationStatus.forward || next.status == AnimationStatus.reverse)
+      return next.status;
+    return first.status;
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'CompoundAnimation')}($first, $next)';
+  }
+
+  AnimationStatus? _lastStatus;
+  void _maybeNotifyStatusListeners(AnimationStatus _) {
+    if (status != _lastStatus) {
+      _lastStatus = status;
+      notifyStatusListeners(status);
+    }
+  }
+
+  T? _lastValue;
+  void _maybeNotifyListeners() {
+    if (value != _lastValue) {
+      _lastValue = value;
+      notifyListeners();
+    }
+  }
+}
+
+/// An animation of [double]s that tracks the mean of two other animations.
+///
+/// The [status] of this animation is the status of the `right` animation if it is
+/// moving, and the `left` animation otherwise.
+///
+/// The [value] of this animation is the [double] that represents the mean value
+/// of the values of the `left` and `right` animations.
+class AnimationMean extends CompoundAnimation<double> {
+  /// Creates an animation that tracks the mean of two other animations.
+  AnimationMean({
+    required Animation<double> left,
+    required Animation<double> right,
+  }) : super(first: left, next: right);
+
+  @override
+  double get value => (first.value + next.value) / 2.0;
+}
+
+/// An animation that tracks the maximum of two other animations.
+///
+/// The [value] of this animation is the maximum of the values of
+/// [first] and [next].
+class AnimationMax<T extends num> extends CompoundAnimation<T> {
+  /// Creates an [AnimationMax].
+  ///
+  /// Both arguments must be non-null. Either can be an [AnimationMax] itself
+  /// to combine multiple animations.
+  AnimationMax(Animation<T> first, Animation<T> next) : super(first: first, next: next);
+
+  @override
+  T get value => math.max(first.value, next.value);
+}
+
+/// An animation that tracks the minimum of two other animations.
+///
+/// The [value] of this animation is the maximum of the values of
+/// [first] and [next].
+class AnimationMin<T extends num> extends CompoundAnimation<T> {
+  /// Creates an [AnimationMin].
+  ///
+  /// Both arguments must be non-null. Either can be an [AnimationMin] itself
+  /// to combine multiple animations.
+  AnimationMin(Animation<T> first, Animation<T> next) : super(first: first, next: next);
+
+  @override
+  T get value => math.min(first.value, next.value);
+}
diff --git a/lib/src/animation/curves.dart b/lib/src/animation/curves.dart
new file mode 100644
index 0000000..8e3faac
--- /dev/null
+++ b/lib/src/animation/curves.dart
@@ -0,0 +1,1702 @@
+// Copyright 2014 The Flutter Authors. 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:flute/ui.dart';
+
+import 'package:flute/foundation.dart';
+
+/// An abstract class providing an interface for evaluating a parametric curve.
+///
+/// A parametric curve transforms a parameter (hence the name) `t` along a curve
+/// to the value of the curve at that value of `t`. The curve can be of
+/// arbitrary dimension, but is typically a 1D, 2D, or 3D curve.
+///
+/// See also:
+///
+///  * [Curve], a 1D animation easing curve that starts at 0.0 and ends at 1.0.
+///  * [Curve2D], a parametric curve that transforms the parameter to a 2D point.
+abstract class ParametricCurve<T> {
+  /// Abstract const constructor to enable subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const ParametricCurve();
+
+  /// Returns the value of the curve at point `t`.
+  ///
+  /// This method asserts that t is between 0 and 1 before delegating to
+  /// [transformInternal].
+  ///
+  /// It is recommended that subclasses override [transformInternal] instead of
+  /// this function, as the above case is already handled in the default
+  /// implementation of [transform], which delegates the remaining logic to
+  /// [transformInternal].
+  T transform(double t) {
+    assert(t != null);
+    assert(t >= 0.0 && t <= 1.0, 'parametric value $t is outside of [0, 1] range.');
+    return transformInternal(t);
+  }
+
+  /// Returns the value of the curve at point `t`.
+  ///
+  /// The given parametric value `t` will be between 0.0 and 1.0, inclusive.
+  @protected
+  T transformInternal(double t) {
+    throw UnimplementedError();
+  }
+
+  @override
+  String toString() => objectRuntimeType(this, 'ParametricCurve');
+}
+
+/// An parametric animation easing curve, i.e. a mapping of the unit interval to
+/// the unit interval.
+///
+/// Easing curves are used to adjust the rate of change of an animation over
+/// time, allowing them to speed up and slow down, rather than moving at a
+/// constant rate.
+///
+/// A [Curve] must map t=0.0 to 0.0 and t=1.0 to 1.0.
+///
+/// See also:
+///
+///  * [Curves], a collection of common animation easing curves.
+///  * [CurveTween], which can be used to apply a [Curve] to an [Animation].
+///  * [Canvas.drawArc], which draws an arc, and has nothing to do with easing
+///    curves.
+///  * [Animatable], for a more flexible interface that maps fractions to
+///    arbitrary values.
+@immutable
+abstract class Curve extends ParametricCurve<double> {
+  /// Abstract const constructor to enable subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Curve();
+
+  /// Returns the value of the curve at point `t`.
+  ///
+  /// This function must ensure the following:
+  /// - The value of `t` must be between 0.0 and 1.0
+  /// - Values of `t`=0.0 and `t`=1.0 must be mapped to 0.0 and 1.0,
+  /// respectively.
+  ///
+  /// It is recommended that subclasses override [transformInternal] instead of
+  /// this function, as the above cases are already handled in the default
+  /// implementation of [transform], which delegates the remaining logic to
+  /// [transformInternal].
+  @override
+  double transform(double t) {
+    if (t == 0.0 || t == 1.0) {
+      return t;
+    }
+    return super.transform(t);
+  }
+
+  /// Returns a new curve that is the reversed inversion of this one.
+  ///
+  /// This is often useful with [CurvedAnimation.reverseCurve].
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_flipped.mp4}
+  ///
+  /// See also:
+  ///
+  ///  * [FlippedCurve], the class that is used to implement this getter.
+  ///  * [ReverseAnimation], which reverses an [Animation] rather than a [Curve].
+  ///  * [CurvedAnimation], which can take a separate curve and reverse curve.
+  Curve get flipped => FlippedCurve(this);
+}
+
+/// The identity map over the unit interval.
+///
+/// See [Curves.linear] for an instance of this class.
+class _Linear extends Curve {
+  const _Linear._();
+
+  @override
+  double transformInternal(double t) => t;
+}
+
+/// A sawtooth curve that repeats a given number of times over the unit interval.
+///
+/// The curve rises linearly from 0.0 to 1.0 and then falls discontinuously back
+/// to 0.0 each iteration.
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_sawtooth.mp4}
+class SawTooth extends Curve {
+  /// Creates a sawtooth curve.
+  ///
+  /// The [count] argument must not be null.
+  const SawTooth(this.count) : assert(count != null);
+
+  /// The number of repetitions of the sawtooth pattern in the unit interval.
+  final int count;
+
+  @override
+  double transformInternal(double t) {
+    t *= count;
+    return t - t.truncateToDouble();
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'SawTooth')}($count)';
+  }
+}
+
+/// A curve that is 0.0 until [begin], then curved (according to [curve]) from
+/// 0.0 at [begin] to 1.0 at [end], then remains 1.0 past [end].
+///
+/// An [Interval] can be used to delay an animation. For example, a six second
+/// animation that uses an [Interval] with its [begin] set to 0.5 and its [end]
+/// set to 1.0 will essentially become a three-second animation that starts
+/// three seconds later.
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_interval.mp4}
+class Interval extends Curve {
+  /// Creates an interval curve.
+  ///
+  /// The arguments must not be null.
+  const Interval(this.begin, this.end, { this.curve = Curves.linear })
+    : assert(begin != null),
+      assert(end != null),
+      assert(curve != null);
+
+  /// The largest value for which this interval is 0.0.
+  ///
+  /// From t=0.0 to t=`begin`, the interval's value is 0.0.
+  final double begin;
+
+  /// The smallest value for which this interval is 1.0.
+  ///
+  /// From t=`end` to t=1.0, the interval's value is 1.0.
+  final double end;
+
+  /// The curve to apply between [begin] and [end].
+  final Curve curve;
+
+  @override
+  double transformInternal(double t) {
+    assert(begin >= 0.0);
+    assert(begin <= 1.0);
+    assert(end >= 0.0);
+    assert(end <= 1.0);
+    assert(end >= begin);
+    t = ((t - begin) / (end - begin)).clamp(0.0, 1.0);
+    if (t == 0.0 || t == 1.0)
+      return t;
+    return curve.transform(t);
+  }
+
+  @override
+  String toString() {
+    if (curve is! _Linear)
+      return '${objectRuntimeType(this, 'Interval')}($begin\u22EF$end)\u27A9$curve';
+    return '${objectRuntimeType(this, 'Interval')}($begin\u22EF$end)';
+  }
+}
+
+/// A curve that is 0.0 until it hits the threshold, then it jumps to 1.0.
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_threshold.mp4}
+class Threshold extends Curve {
+  /// Creates a threshold curve.
+  ///
+  /// The [threshold] argument must not be null.
+  const Threshold(this.threshold) : assert(threshold != null);
+
+  /// The value before which the curve is 0.0 and after which the curve is 1.0.
+  ///
+  /// When t is exactly [threshold], the curve has the value 1.0.
+  final double threshold;
+
+  @override
+  double transformInternal(double t) {
+    assert(threshold >= 0.0);
+    assert(threshold <= 1.0);
+    return t < threshold ? 0.0 : 1.0;
+  }
+}
+
+/// A cubic polynomial mapping of the unit interval.
+///
+/// The [Curves] class contains some commonly used cubic curves:
+///
+///  * [Curves.ease]
+///  * [Curves.easeIn]
+///  * [Curves.easeOut]
+///  * [Curves.easeInOut]
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4}
+///
+/// The [Cubic] class implements third-order Bézier curves.
+///
+/// See also:
+///
+///  * [Curves], where many more predefined curves are available.
+///  * [CatmullRomCurve], a curve which passes through specific values.
+class Cubic extends Curve {
+  /// Creates a cubic curve.
+  ///
+  /// Rather than creating a new instance, consider using one of the common
+  /// cubic curves in [Curves].
+  ///
+  /// The [a], [b], [c], and [d] arguments must not be null.
+  const Cubic(this.a, this.b, this.c, this.d)
+    : assert(a != null),
+      assert(b != null),
+      assert(c != null),
+      assert(d != null);
+
+  /// The x coordinate of the first control point.
+  ///
+  /// The line through the point (0, 0) and the first control point is tangent
+  /// to the curve at the point (0, 0).
+  final double a;
+
+  /// The y coordinate of the first control point.
+  ///
+  /// The line through the point (0, 0) and the first control point is tangent
+  /// to the curve at the point (0, 0).
+  final double b;
+
+  /// The x coordinate of the second control point.
+  ///
+  /// The line through the point (1, 1) and the second control point is tangent
+  /// to the curve at the point (1, 1).
+  final double c;
+
+  /// The y coordinate of the second control point.
+  ///
+  /// The line through the point (1, 1) and the second control point is tangent
+  /// to the curve at the point (1, 1).
+  final double d;
+
+  static const double _cubicErrorBound = 0.001;
+
+  double _evaluateCubic(double a, double b, double m) {
+    return 3 * a * (1 - m) * (1 - m) * m +
+           3 * b * (1 - m) *           m * m +
+                                       m * m * m;
+  }
+
+  @override
+  double transformInternal(double t) {
+    double start = 0.0;
+    double end = 1.0;
+    while (true) {
+      final double midpoint = (start + end) / 2;
+      final double estimate = _evaluateCubic(a, c, midpoint);
+      if ((t - estimate).abs() < _cubicErrorBound)
+        return _evaluateCubic(b, d, midpoint);
+      if (estimate < t)
+        start = midpoint;
+      else
+        end = midpoint;
+    }
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'Cubic')}(${a.toStringAsFixed(2)}, ${b.toStringAsFixed(2)}, ${c.toStringAsFixed(2)}, ${d.toStringAsFixed(2)})';
+  }
+}
+
+/// Abstract class that defines an API for evaluating 2D parametric curves.
+///
+/// [Curve2D] differs from [Curve] in that the values interpolated are [Offset]
+/// values instead of [double] values, hence the "2D" in the name. They both
+/// take a single double `t` that has a range of 0.0 to 1.0, inclusive, as input
+/// to the [transform] function . Unlike [Curve], [Curve2D] is not required to
+/// map `t=0.0` and `t=1.0` to specific output values.
+///
+/// The interpolated `t` value given to [transform] represents the progression
+/// along the curve, but it doesn't necessarily progress at a constant velocity, so
+/// incrementing `t` by, say, 0.1 might move along the curve by quite a lot at one
+/// part of the curve, or hardly at all in another part of the curve, depending
+/// on the definition of the curve.
+///
+/// {@tool dartpad --template=stateless_widget_material_no_null_safety}
+/// This example shows how to use a [Curve2D] to modify the position of a widget
+/// so that it can follow an arbitrary path.
+///
+/// ```dart preamble
+/// // This is the path that the child will follow. It's a CatmullRomSpline so
+/// // that the coordinates can be specified that it must pass through. If the
+/// // tension is set to 1.0, it will linearly interpolate between those points,
+/// // instead of interpolating smoothly.
+/// final CatmullRomSpline path = CatmullRomSpline(
+///   const <Offset>[
+///     Offset(0.05, 0.75),
+///     Offset(0.18, 0.23),
+///     Offset(0.32, 0.04),
+///     Offset(0.73, 0.5),
+///     Offset(0.42, 0.74),
+///     Offset(0.73, 0.01),
+///     Offset(0.93, 0.93),
+///     Offset(0.05, 0.75),
+///   ],
+///   startHandle: Offset(0.93, 0.93),
+///   endHandle: Offset(0.18, 0.23),
+///   tension: 0.0,
+/// );
+///
+/// class FollowCurve2D extends StatefulWidget {
+///   const FollowCurve2D({
+///     Key key,
+///     @required this.path,
+///     this.curve = Curves.easeInOut,
+///     @required this.child,
+///     this.duration = const Duration(seconds: 1),
+///   })  : assert(path != null),
+///         assert(curve != null),
+///         assert(child != null),
+///         assert(duration != null),
+///         super(key: key);
+///
+///   final Curve2D path;
+///   final Curve curve;
+///   final Duration duration;
+///   final Widget child;
+///
+///   @override
+///   _FollowCurve2DState createState() => _FollowCurve2DState();
+/// }
+///
+/// class _FollowCurve2DState extends State<FollowCurve2D> with TickerProviderStateMixin {
+///   // The animation controller for this animation.
+///   AnimationController controller;
+///   // The animation that will be used to apply the widget's animation curve.
+///   Animation<double> animation;
+///
+///   @override
+///   void initState() {
+///     super.initState();
+///     controller = AnimationController(duration: widget.duration, vsync: this);
+///     animation = CurvedAnimation(parent: controller, curve: widget.curve);
+///     // Have the controller repeat indefinitely.  If you want it to "bounce" back
+///     // and forth, set the reverse parameter to true.
+///     controller.repeat(reverse: false);
+///     controller.addListener(() => setState(() {}));
+///   }
+///
+///   @override
+///   void dispose() {
+///     super.dispose();
+///     // Always have to dispose of animation controllers when done.
+///     controller.dispose();
+///   }
+///
+///   @override
+///   Widget build(BuildContext context) {
+///     // Scale the path values to match the -1.0 to 1.0 domain of the Alignment widget.
+///     final Offset position = widget.path.transform(animation.value) * 2.0 - Offset(1.0, 1.0);
+///     return Align(
+///       alignment: Alignment(position.dx, position.dy),
+///       child: widget.child,
+///     );
+///   }
+/// }
+/// ```
+///
+/// ```dart
+///   Widget build(BuildContext context) {
+///     return Container(
+///       color: Colors.white,
+///       alignment: Alignment.center,
+///       child: FollowCurve2D(
+///         path: path,
+///         curve: Curves.easeInOut,
+///         duration: const Duration(seconds: 3),
+///         child: CircleAvatar(
+///           backgroundColor: Colors.yellow,
+///           child: DefaultTextStyle(
+///             style: Theme.of(context).textTheme.headline6,
+///             child: Text("B"), // Buzz, buzz!
+///           ),
+///         ),
+///       ),
+///     );
+///   }
+/// ```
+/// {@end-tool}
+///
+abstract class Curve2D extends ParametricCurve<Offset> {
+  /// Abstract const constructor to enable subclasses to provide const
+  /// constructors so that they can be used in const expressions.
+  const Curve2D();
+
+  /// Generates a list of samples with a recursive subdivision until a tolerance
+  /// of `tolerance` is reached.
+  ///
+  /// Samples are generated in order.
+  ///
+  /// Samples can be used to render a curve efficiently, since the samples
+  /// constitute line segments which vary in size with the curvature of the
+  /// curve. They can also be used to quickly approximate the value of the curve
+  /// by searching for the desired range in X and linearly interpolating between
+  /// samples to obtain an approximation of Y at the desired X value. The
+  /// implementation of [CatmullRomCurve] uses samples for this purpose
+  /// internally.
+  ///
+  /// The tolerance is computed as the area of a triangle formed by a new point
+  /// and the preceding and following point.
+  ///
+  /// See also:
+  ///
+  ///  * Luiz Henrique de Figueire's Graphics Gem on [the algorithm](http://ariel.chronotext.org/dd/defigueiredo93adaptive.pdf).
+  Iterable<Curve2DSample> generateSamples({
+    double start = 0.0,
+    double end = 1.0,
+    double tolerance = 1e-10,
+  }) {
+    // The sampling  algorithm is:
+    // 1. Evaluate the area of the triangle (a proxy for the "flatness" of the
+    //    curve) formed by two points and a test point.
+    // 2. If the area of the triangle is small enough (below tolerance), then
+    //    the two points form the final segment.
+    // 3. If the area is still too large, divide the interval into two parts
+    //    using a random subdivision point to avoid aliasing.
+    // 4. Recursively sample the two parts.
+    //
+    // This algorithm concentrates samples in areas of high curvature.
+    assert(tolerance != null);
+    assert(start != null);
+    assert(end != null);
+    assert(end > start);
+    // We want to pick a random seed that will keep the result stable if
+    // evaluated again, so we use the first non-generated control point.
+    final math.Random rand = math.Random(samplingSeed);
+    bool isFlat(Offset p, Offset q, Offset r) {
+      // Calculates the area of the triangle given by the three points.
+      final Offset pr = p - r;
+      final Offset qr = q - r;
+      final double z = pr.dx * qr.dy - qr.dx * pr.dy;
+      return (z * z) < tolerance;
+    }
+
+    final Curve2DSample first = Curve2DSample(start, transform(start));
+    final Curve2DSample last = Curve2DSample(end, transform(end));
+    final List<Curve2DSample> samples = <Curve2DSample>[first];
+    void sample(Curve2DSample p, Curve2DSample q, {bool forceSubdivide = false}) {
+      // Pick a random point somewhat near the center, which avoids aliasing
+      // problems with periodic curves.
+      final double t = p.t + (0.45 + 0.1 * rand.nextDouble()) * (q.t - p.t);
+      final Curve2DSample r = Curve2DSample(t, transform(t));
+
+      if (!forceSubdivide && isFlat(p.value, q.value, r.value)) {
+        samples.add(q);
+      } else {
+        sample(p, r);
+        sample(r, q);
+      }
+    }
+    // If the curve starts and ends on the same point, then we force it to
+    // subdivide at least once, because otherwise it will terminate immediately.
+    sample(
+      first,
+      last,
+      forceSubdivide: (first.value.dx - last.value.dx).abs() < tolerance && (first.value.dy - last.value.dy).abs() < tolerance,
+    );
+    return samples;
+  }
+
+  /// Returns a seed value used by [generateSamples] to seed a random number
+  /// generator to avoid sample aliasing.
+  ///
+  /// Subclasses should override this and provide a custom seed.
+  ///
+  /// The value returned should be the same each time it is called, unless the
+  /// curve definition changes.
+  @protected
+  int get samplingSeed => 0;
+
+  /// Returns the parameter `t` that corresponds to the given x value of the spline.
+  ///
+  /// This will only work properly for curves which are single-valued in x
+  /// (where every value of `x` maps to only one value in 'y', i.e. the curve
+  /// does not loop or curve back over itself). For curves that are not
+  /// single-valued, it will return the parameter for only one of the values at
+  /// the given `x` location.
+  double findInverse(double x) {
+    assert(x != null);
+    double start = 0.0;
+    double end = 1.0;
+    late double mid;
+    double offsetToOrigin(double pos) => x - transform(pos).dx;
+    // Use a binary search to find the inverse point within 1e-6, or 100
+    // subdivisions, whichever comes first.
+    const double errorLimit = 1e-6;
+    int count = 100;
+    final double startValue = offsetToOrigin(start);
+    while ((end - start) / 2.0 > errorLimit && count > 0) {
+      mid = (end + start) / 2.0;
+      final double value = offsetToOrigin(mid);
+      if (value.sign == startValue.sign) {
+        start = mid;
+      } else {
+        end = mid;
+      }
+      count--;
+    }
+    return mid;
+  }
+}
+
+/// A class that holds a sample of a 2D parametric curve, containing the [value]
+/// (the X, Y coordinates) of the curve at the parametric value [t].
+///
+/// See also:
+///
+///  * [Curve2D.generateSamples], which generates samples of this type.
+///  * [Curve2D], a parametric curve that maps a double parameter to a 2D location.
+class Curve2DSample {
+  /// A const constructor for the sample so that subclasses can be const.
+  ///
+  /// All arguments must not be null.
+  const Curve2DSample(this.t, this.value) : assert(t != null), assert(value != null);
+
+  /// The parametric location of this sample point along the curve.
+  final double t;
+
+  /// The value (the X, Y coordinates) of the curve at parametric value [t].
+  final Offset value;
+
+  @override
+  String toString() {
+    return '[(${value.dx.toStringAsFixed(2)}, ${value.dy.toStringAsFixed(2)}), ${t.toStringAsFixed(2)}]';
+  }
+}
+
+/// A 2D spline that passes smoothly through the given control points using a
+/// centripetal Catmull-Rom spline.
+///
+/// When the curve is evaluated with [transform], the output values will move
+/// smoothly from one control point to the next, passing through the control
+/// points.
+///
+/// {@template flutter.animation.CatmullRomSpline}
+/// Unlike most cubic splines, Catmull-Rom splines have the advantage that their
+/// curves pass through the control points given to them. They are cubic
+/// polynomial representations, and, in fact, Catmull-Rom splines can be
+/// converted mathematically into cubic splines. This class implements a
+/// "centripetal" Catmull-Rom spline. The term centripetal implies that it won't
+/// form loops or self-intersections within a single segment.
+/// {@endtemplate}
+///
+/// See also:
+///  * [Centripetal Catmull–Rom splines](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline)
+///    on Wikipedia.
+///  * [Parameterization and Applications of Catmull-Rom Curves](http://faculty.cs.tamu.edu/schaefer/research/cr_cad.pdf),
+///    a paper on using Catmull-Rom splines.
+///  * [CatmullRomCurve], an animation curve that uses a [CatmullRomSpline] as its
+///    internal representation.
+class CatmullRomSpline extends Curve2D {
+  /// Constructs a centripetal Catmull-Rom spline curve.
+  ///
+  /// The `controlPoints` argument is a list of four or more points that
+  /// describe the points that the curve must pass through.
+  ///
+  /// The optional `tension` argument controls how tightly the curve approaches
+  /// the given `controlPoints`. It must be in the range 0.0 to 1.0, inclusive. It
+  /// defaults to 0.0, which provides the smoothest curve. A value of 1.0
+  /// produces a linear interpolation between points.
+  ///
+  /// The optional `endHandle` and `startHandle` points are the beginning and
+  /// ending handle positions. If not specified, they are created automatically
+  /// by extending the line formed by the first and/or last line segment in the
+  /// `controlPoints`, respectively. The spline will not go through these handle
+  /// points, but they will affect the slope of the line at the beginning and
+  /// end of the spline. The spline will attempt to match the slope of the line
+  /// formed by the start or end handle and the neighboring first or last
+  /// control point. The default is chosen so that the slope of the line at the
+  /// ends matches that of the first or last line segment in the control points.
+  ///
+  /// The `tension` and `controlPoints` arguments must not be null, and the
+  /// `controlPoints` list must contain at least four control points to
+  /// interpolate.
+  ///
+  /// The internal curve data structures are lazily computed the first time
+  /// [transform] is called.  If you would rather pre-compute the structures,
+  /// use [CatmullRomSpline.precompute] instead.
+  CatmullRomSpline(
+      List<Offset> controlPoints, {
+        double tension = 0.0,
+        Offset? startHandle,
+        Offset? endHandle,
+      }) : assert(controlPoints != null),
+           assert(tension != null),
+           assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
+           assert(tension >= 0.0, 'tension $tension must not be negative.'),
+           assert(controlPoints.length > 3, 'There must be at least four control points to create a CatmullRomSpline.'),
+           _controlPoints = controlPoints,
+           _startHandle = startHandle,
+           _endHandle = endHandle,
+           _tension = tension,
+           _cubicSegments = <List<Offset>>[];
+
+  /// Constructs a centripetal Catmull-Rom spline curve.
+  ///
+  /// The same as [new CatmullRomSpline], except that the internal data
+  /// structures are precomputed instead of being computed lazily.
+  CatmullRomSpline.precompute(
+      List<Offset> controlPoints, {
+        double tension = 0.0,
+        Offset? startHandle,
+        Offset? endHandle,
+      }) : assert(controlPoints != null),
+           assert(tension != null),
+           assert(tension <= 1.0, 'tension $tension must not be greater than 1.0.'),
+           assert(tension >= 0.0, 'tension $tension must not be negative.'),
+           assert(controlPoints.length > 3, 'There must be at least four control points to create a CatmullRomSpline.'),
+           _controlPoints = null,
+           _startHandle = null,
+           _endHandle = null,
+           _tension = null,
+           _cubicSegments = _computeSegments(controlPoints, tension, startHandle: startHandle, endHandle: endHandle);
+
+
+  static List<List<Offset>> _computeSegments(
+      List<Offset> controlPoints,
+      double tension, {
+      Offset? startHandle,
+      Offset? endHandle,
+    }) {
+    // If not specified, select the first and last control points (which are
+    // handles: they are not intersected by the resulting curve) so that they
+    // extend the first and last segments, respectively.
+    startHandle ??= controlPoints[0] * 2.0 - controlPoints[1];
+    endHandle ??= controlPoints.last * 2.0 - controlPoints[controlPoints.length - 2];
+    final List<Offset> allPoints = <Offset>[
+      startHandle,
+      ...controlPoints,
+      endHandle,
+    ];
+
+    // An alpha of 0.5 is what makes it a centripetal Catmull-Rom spline. A
+    // value of 0.0 would make it a uniform Catmull-Rom spline, and a value of
+    // 1.0 would make it a chordal Catmull-Rom spline. Non-centripetal values
+    // for alpha can give self-intersecting behavior or looping within a
+    // segment.
+    const double alpha = 0.5;
+    final double reverseTension = 1.0 - tension;
+    final List<List<Offset>> result = <List<Offset>>[];
+    for (int i = 0; i < allPoints.length - 3; ++i) {
+      final List<Offset> curve = <Offset>[allPoints[i], allPoints[i + 1], allPoints[i + 2], allPoints[i + 3]];
+      final Offset diffCurve10 = curve[1] - curve[0];
+      final Offset diffCurve21 = curve[2] - curve[1];
+      final Offset diffCurve32 = curve[3] - curve[2];
+      final double t01 = math.pow(diffCurve10.distance, alpha).toDouble();
+      final double t12 = math.pow(diffCurve21.distance, alpha).toDouble();
+      final double t23 = math.pow(diffCurve32.distance, alpha).toDouble();
+
+      final Offset m1 = (diffCurve21 + (diffCurve10 / t01 - (curve[2] - curve[0]) / (t01 + t12)) * t12) * reverseTension;
+      final Offset m2 = (diffCurve21 + (diffCurve32 / t23 - (curve[3] - curve[1]) / (t12 + t23)) * t12) * reverseTension;
+      final Offset sumM12 = m1 + m2;
+
+      final List<Offset> segment = <Offset>[
+        diffCurve21 * -2.0 + sumM12,
+        diffCurve21 * 3.0 - m1 - sumM12,
+        m1,
+        curve[1],
+      ];
+      result.add(segment);
+    }
+    return result;
+  }
+
+  // The list of control point lists for each cubic segment of the spline.
+  final List<List<Offset>> _cubicSegments;
+
+  // This is non-empty only if the _cubicSegments are being computed lazily.
+  final List<Offset>? _controlPoints;
+  final Offset? _startHandle;
+  final Offset? _endHandle;
+  final double? _tension;
+
+  void _initializeIfNeeded() {
+    if (_cubicSegments.isNotEmpty) {
+      return;
+    }
+    _cubicSegments.addAll(
+      _computeSegments(_controlPoints!, _tension!, startHandle: _startHandle, endHandle: _endHandle),
+    );
+  }
+
+  @override
+  @protected
+  int get samplingSeed {
+    _initializeIfNeeded();
+    final Offset seedPoint = _cubicSegments[0][1];
+    return ((seedPoint.dx + seedPoint.dy) * 10000).round();
+  }
+
+  @override
+  Offset transformInternal(double t) {
+    _initializeIfNeeded();
+    final double length = _cubicSegments.length.toDouble();
+    final double position;
+    final double localT;
+    final int index;
+    if (t < 1.0) {
+      position = t * length;
+      localT = position % 1.0;
+      index = position.floor();
+    } else {
+      position = length;
+      localT = 1.0;
+      index = _cubicSegments.length - 1;
+    }
+    final List<Offset> cubicControlPoints = _cubicSegments[index];
+    final double localT2 = localT * localT;
+    return cubicControlPoints[0] * localT2 * localT
+         + cubicControlPoints[1] * localT2
+         + cubicControlPoints[2] * localT
+         + cubicControlPoints[3];
+  }
+}
+
+/// An animation easing curve that passes smoothly through the given control
+/// points using a centripetal Catmull-Rom spline.
+///
+/// When this curve is evaluated with [transform], the values will interpolate
+/// smoothly from one control point to the next, passing through (0.0, 0.0), the
+/// given points, and then (1.0, 1.0).
+///
+/// {@macro flutter.animation.CatmullRomSpline}
+///
+/// This class uses a centripetal Catmull-Rom curve (a [CatmullRomSpline]) as
+/// its internal representation. The term centripetal implies that it won't form
+/// loops or self-intersections within a single segment, and corresponds to a
+/// Catmull-Rom α (alpha) value of 0.5.
+///
+/// See also:
+///
+///  * [CatmullRomSpline], the 2D spline that this curve uses to generate its values.
+///  * A Wikipedia article on [centripetal Catmull-Rom splines](https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline).
+///  * [new CatmullRomCurve] for a description of the constraints put on the
+///    input control points.
+///  * This [paper on using Catmull-Rom splines](http://faculty.cs.tamu.edu/schaefer/research/cr_cad.pdf).
+class CatmullRomCurve extends Curve {
+  /// Constructs a centripetal [CatmullRomCurve].
+  ///
+  /// It takes a list of two or more points that describe the points that the
+  /// curve must pass through. See [controlPoints] for a description of the
+  /// restrictions placed on control points. In addition to the given
+  /// [controlPoints], the curve will begin with an implicit control point at
+  /// (0.0, 0.0) and end with an implicit control point at (1.0, 1.0), so that
+  /// the curve begins and ends at those points.
+  ///
+  /// The optional [tension] argument controls how tightly the curve approaches
+  /// the given `controlPoints`. It must be in the range 0.0 to 1.0, inclusive. It
+  /// defaults to 0.0, which provides the smoothest curve. A value of 1.0
+  /// is equivalent to a linear interpolation between points.
+  ///
+  /// The internal curve data structures are lazily computed the first time
+  /// [transform] is called.  If you would rather pre-compute the curve, use
+  /// [CatmullRomCurve.precompute] instead.
+  ///
+  /// All of the arguments must not be null.
+  ///
+  /// See also:
+  ///
+  ///  * This [paper on using Catmull-Rom splines](http://faculty.cs.tamu.edu/schaefer/research/cr_cad.pdf).
+  CatmullRomCurve(this.controlPoints, {this.tension = 0.0})
+      : assert(tension != null),
+        assert(() {
+          return validateControlPoints(
+            controlPoints,
+            tension: tension,
+            reasons: _debugAssertReasons..clear(),
+          );
+        }(), 'control points $controlPoints could not be validated:\n  ${_debugAssertReasons.join('\n  ')}'),
+        // Pre-compute samples so that we don't have to evaluate the spline's inverse
+        // all the time in transformInternal.
+        _precomputedSamples = <Curve2DSample>[];
+
+  /// Constructs a centripetal [CatmullRomCurve].
+  ///
+  /// Same as [new CatmullRomCurve], but it precomputes the internal curve data
+  /// structures for a more predictable computation load.
+  CatmullRomCurve.precompute(this.controlPoints, {this.tension = 0.0})
+      : assert(tension != null),
+        assert(() {
+          return validateControlPoints(
+            controlPoints,
+            tension: tension,
+            reasons: _debugAssertReasons..clear(),
+          );
+        }(), 'control points $controlPoints could not be validated:\n  ${_debugAssertReasons.join('\n  ')}'),
+        // Pre-compute samples so that we don't have to evaluate the spline's inverse
+        // all the time in transformInternal.
+        _precomputedSamples = _computeSamples(controlPoints, tension);
+
+  static List<Curve2DSample> _computeSamples(List<Offset> controlPoints, double tension) {
+    return CatmullRomSpline.precompute(
+      // Force the first and last control points for the spline to be (0, 0)
+      // and (1, 1), respectively.
+      <Offset>[Offset.zero, ...controlPoints, const Offset(1.0, 1.0)],
+      tension: tension,
+    ).generateSamples(start: 0.0, end: 1.0, tolerance: 1e-12).toList();
+  }
+
+  /// A static accumulator for assertion failures. Not used in release mode.
+  static final List<String> _debugAssertReasons = <String>[];
+
+  // The precomputed approximation curve, so that evaluation of the curve is
+  // efficient.
+  //
+  // If the curve is constructed lazily, then this will be empty, and will be filled
+  // the first time transform is called.
+  final List<Curve2DSample> _precomputedSamples;
+
+  /// The control points used to create this curve.
+  ///
+  /// The `dx` value of each [Offset] in [controlPoints] represents the
+  /// animation value at which the curve should pass through the `dy` value of
+  /// the same control point.
+  ///
+  /// The [controlPoints] list must meet the following criteria:
+  ///
+  ///  * The list must contain at least two points.
+  ///  * The X value of each point must be greater than 0.0 and less then 1.0.
+  ///  * The X values of each point must be greater than the
+  ///    previous point's X value (i.e. monotonically increasing). The Y values
+  ///    are not constrained.
+  ///  * The resulting spline must be single-valued in X. That is, for each X
+  ///    value, there must be exactly one Y value. This means that the control
+  ///    points must not generated a spline that loops or overlaps itself.
+  ///
+  /// The static function [validateControlPoints] can be used to check that
+  /// these conditions are met, and will return true if they are. In debug mode,
+  /// it will also optionally return a list of reasons in text form. In debug
+  /// mode, the constructor will assert that these conditions are met and print
+  /// the reasons if the assert fires.
+  ///
+  /// When the curve is evaluated with [transform], the values will interpolate
+  /// smoothly from one control point to the next, passing through (0.0, 0.0), the
+  /// given control points, and (1.0, 1.0).
+  final List<Offset> controlPoints;
+
+  /// The "tension" of the curve.
+  ///
+  /// The `tension` attribute controls how tightly the curve approaches the
+  /// given [controlPoints]. It must be in the range 0.0 to 1.0, inclusive. It
+  /// is optional, and defaults to 0.0, which provides the smoothest curve. A
+  /// value of 1.0 is equivalent to a linear interpolation between control
+  /// points.
+  final double tension;
+
+  /// Validates that a given set of control points for a [CatmullRomCurve] is
+  /// well-formed and will not produce a spline that self-intersects.
+  ///
+  /// This method is also used in debug mode to validate a curve to make sure
+  /// that it won't violate the contract for the [new CatmullRomCurve]
+  /// constructor.
+  ///
+  /// If in debug mode, and `reasons` is non-null, this function will fill in
+  /// `reasons` with descriptions of the problems encountered. The `reasons`
+  /// argument is ignored in release mode.
+  ///
+  /// In release mode, this function can be used to decide if a proposed
+  /// modification to the curve will result in a valid curve.
+  static bool validateControlPoints(
+      List<Offset>? controlPoints, {
+      double tension = 0.0,
+      List<String>? reasons,
+    }) {
+    assert(tension != null);
+    if (controlPoints == null) {
+      assert(() {
+        reasons?.add('Supplied control points cannot be null');
+        return true;
+      }());
+      return false;
+    }
+
+    if (controlPoints.length < 2) {
+      assert(() {
+        reasons?.add('There must be at least two points supplied to create a valid curve.');
+        return true;
+      }());
+      return false;
+    }
+
+    controlPoints = <Offset>[Offset.zero, ...controlPoints, const Offset(1.0, 1.0)];
+    final Offset startHandle = controlPoints[0] * 2.0 - controlPoints[1];
+    final Offset endHandle = controlPoints.last * 2.0 - controlPoints[controlPoints.length - 2];
+    controlPoints = <Offset>[startHandle, ...controlPoints, endHandle];
+    double lastX = -double.infinity;
+    for (int i = 0; i < controlPoints.length; ++i) {
+      if (i > 1 &&
+          i < controlPoints.length - 2 &&
+          (controlPoints[i].dx <= 0.0 || controlPoints[i].dx >= 1.0)) {
+        assert(() {
+          reasons?.add('Control points must have X values between 0.0 and 1.0, exclusive. '
+              'Point $i has an x value (${controlPoints![i].dx}) which is outside the range.');
+          return true;
+        }());
+        return false;
+      }
+      if (controlPoints[i].dx <= lastX) {
+        assert(() {
+          reasons?.add('Each X coordinate must be greater than the preceding X coordinate '
+              '(i.e. must be monotonically increasing in X). Point $i has an x value of '
+              '${controlPoints![i].dx}, which is not greater than $lastX');
+          return true;
+        }());
+        return false;
+      }
+      lastX = controlPoints[i].dx;
+    }
+
+    bool success = true;
+
+    // An empirical test to make sure things are single-valued in X.
+    lastX = -double.infinity;
+    const double tolerance = 1e-3;
+    final CatmullRomSpline testSpline = CatmullRomSpline(controlPoints, tension: tension);
+    final double start = testSpline.findInverse(0.0);
+    final double end = testSpline.findInverse(1.0);
+    final Iterable<Curve2DSample> samplePoints = testSpline.generateSamples(start: start, end: end);
+    /// If the first and last points in the samples aren't at (0,0) or (1,1)
+    /// respectively, then the curve is multi-valued at the ends.
+    if (samplePoints.first.value.dy.abs() > tolerance || (1.0 - samplePoints.last.value.dy).abs() > tolerance) {
+      bool bail = true;
+      success = false;
+      assert(() {
+        reasons?.add('The curve has more than one Y value at X = ${samplePoints.first.value.dx}. '
+            'Try moving some control points further away from this value of X, or increasing '
+            'the tension.');
+        // No need to keep going if we're not giving reasons.
+        bail = reasons == null;
+        return true;
+      }());
+      if (bail) {
+        // If we're not in debug mode, then we want to bail immediately
+        // instead of checking everything else.
+        return false;
+      }
+    }
+    for (final Curve2DSample sample in samplePoints) {
+      final Offset point = sample.value;
+      final double t = sample.t;
+      final double x = point.dx;
+      if (t >= start && t <= end && (x < -1e-3 || x > 1.0 + 1e-3)) {
+        bool bail = true;
+        success = false;
+        assert(() {
+          reasons?.add('The resulting curve has an X value ($x) which is outside '
+              'the range [0.0, 1.0], inclusive.');
+          // No need to keep going if we're not giving reasons.
+          bail = reasons == null;
+          return true;
+        }());
+        if (bail) {
+          // If we're not in debug mode, then we want to bail immediately
+          // instead of checking all the segments.
+          return false;
+        }
+      }
+      if (x < lastX) {
+        bool bail = true;
+        success = false;
+        assert(() {
+          reasons?.add('The curve has more than one Y value at x = $x. Try moving '
+            'some control points further apart in X, or increasing the tension.');
+          // No need to keep going if we're not giving reasons.
+          bail = reasons == null;
+          return true;
+        }());
+        if (bail) {
+          // If we're not in debug mode, then we want to bail immediately
+          // instead of checking all the segments.
+          return false;
+        }
+      }
+      lastX = x;
+    }
+    return success;
+  }
+
+  @override
+  double transformInternal(double t) {
+    // Linearly interpolate between the two closest samples generated when the
+    // curve was created.
+    if (_precomputedSamples.isEmpty) {
+      // Compute the samples now if we were constructed lazily.
+      _precomputedSamples.addAll(_computeSamples(controlPoints, tension));
+    }
+    int start = 0;
+    int end = _precomputedSamples.length - 1;
+    int mid;
+    Offset value;
+    Offset startValue = _precomputedSamples[start].value;
+    Offset endValue = _precomputedSamples[end].value;
+    // Use a binary search to find the index of the sample point that is just
+    // before t.
+    while (end - start > 1) {
+      mid = (end + start) ~/ 2;
+      value = _precomputedSamples[mid].value;
+      if (t >= value.dx) {
+        start = mid;
+        startValue = value;
+      } else {
+        end = mid;
+        endValue = value;
+      }
+    }
+
+    // Now interpolate between the found sample and the next one.
+    final double t2 = (t - startValue.dx) / (endValue.dx - startValue.dx);
+    return lerpDouble(startValue.dy, endValue.dy, t2)!;
+  }
+}
+
+/// A curve that is the reversed inversion of its given curve.
+///
+/// This curve evaluates the given curve in reverse (i.e., from 1.0 to 0.0 as t
+/// increases from 0.0 to 1.0) and returns the inverse of the given curve's
+/// value (i.e., 1.0 minus the given curve's value).
+///
+/// This is the class used to implement the [flipped] getter on curves.
+///
+/// This is often useful with [CurvedAnimation.reverseCurve].
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_flipped.mp4}
+///
+/// See also:
+///
+///  * [Curve.flipped], which provides the [FlippedCurve] of a [Curve].
+///  * [ReverseAnimation], which reverses an [Animation] rather than a [Curve].
+///  * [CurvedAnimation], which can take a separate curve and reverse curve.
+class FlippedCurve extends Curve {
+  /// Creates a flipped curve.
+  ///
+  /// The [curve] argument must not be null.
+  const FlippedCurve(this.curve) : assert(curve != null);
+
+  /// The curve that is being flipped.
+  final Curve curve;
+
+  @override
+  double transformInternal(double t) => 1.0 - curve.transform(1.0 - t);
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'FlippedCurve')}($curve)';
+  }
+}
+
+/// A curve where the rate of change starts out quickly and then decelerates; an
+/// upside-down `f(t) = t²` parabola.
+///
+/// This is equivalent to the Android `DecelerateInterpolator` class with a unit
+/// factor (the default factor).
+///
+/// See [Curves.decelerate] for an instance of this class.
+class _DecelerateCurve extends Curve {
+  const _DecelerateCurve._();
+
+  @override
+  double transformInternal(double t) {
+    // Intended to match the behavior of:
+    // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/animation/DecelerateInterpolator.java
+    // ...as of December 2016.
+    t = 1.0 - t;
+    return 1.0 - t * t;
+  }
+}
+
+// BOUNCE CURVES
+
+double _bounce(double t) {
+  if (t < 1.0 / 2.75) {
+    return 7.5625 * t * t;
+  } else if (t < 2 / 2.75) {
+    t -= 1.5 / 2.75;
+    return 7.5625 * t * t + 0.75;
+  } else if (t < 2.5 / 2.75) {
+    t -= 2.25 / 2.75;
+    return 7.5625 * t * t + 0.9375;
+  }
+  t -= 2.625 / 2.75;
+  return 7.5625 * t * t + 0.984375;
+}
+
+/// An oscillating curve that grows in magnitude.
+///
+/// See [Curves.bounceIn] for an instance of this class.
+class _BounceInCurve extends Curve {
+  const _BounceInCurve._();
+
+  @override
+  double transformInternal(double t) {
+    return 1.0 - _bounce(1.0 - t);
+  }
+}
+
+/// An oscillating curve that shrink in magnitude.
+///
+/// See [Curves.bounceOut] for an instance of this class.
+class _BounceOutCurve extends Curve {
+  const _BounceOutCurve._();
+
+  @override
+  double transformInternal(double t) {
+    return _bounce(t);
+  }
+}
+
+/// An oscillating curve that first grows and then shrink in magnitude.
+///
+/// See [Curves.bounceInOut] for an instance of this class.
+class _BounceInOutCurve extends Curve {
+  const _BounceInOutCurve._();
+
+  @override
+  double transformInternal(double t) {
+    if (t < 0.5)
+      return (1.0 - _bounce(1.0 - t * 2.0)) * 0.5;
+    else
+      return _bounce(t * 2.0 - 1.0) * 0.5 + 0.5;
+  }
+}
+
+
+// ELASTIC CURVES
+
+/// An oscillating curve that grows in magnitude while overshooting its bounds.
+///
+/// An instance of this class using the default period of 0.4 is available as
+/// [Curves.elasticIn].
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4}
+class ElasticInCurve extends Curve {
+  /// Creates an elastic-in curve.
+  ///
+  /// Rather than creating a new instance, consider using [Curves.elasticIn].
+  const ElasticInCurve([this.period = 0.4]);
+
+  /// The duration of the oscillation.
+  final double period;
+
+  @override
+  double transformInternal(double t) {
+    final double s = period / 4.0;
+    t = t - 1.0;
+    return -math.pow(2.0, 10.0 * t) * math.sin((t - s) * (math.pi * 2.0) / period);
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'ElasticInCurve')}($period)';
+  }
+}
+
+/// An oscillating curve that shrinks in magnitude while overshooting its bounds.
+///
+/// An instance of this class using the default period of 0.4 is available as
+/// [Curves.elasticOut].
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4}
+class ElasticOutCurve extends Curve {
+  /// Creates an elastic-out curve.
+  ///
+  /// Rather than creating a new instance, consider using [Curves.elasticOut].
+  const ElasticOutCurve([this.period = 0.4]);
+
+  /// The duration of the oscillation.
+  final double period;
+
+  @override
+  double transformInternal(double t) {
+    final double s = period / 4.0;
+    return math.pow(2.0, -10 * t) * math.sin((t - s) * (math.pi * 2.0) / period) + 1.0;
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'ElasticOutCurve')}($period)';
+  }
+}
+
+/// An oscillating curve that grows and then shrinks in magnitude while
+/// overshooting its bounds.
+///
+/// An instance of this class using the default period of 0.4 is available as
+/// [Curves.elasticInOut].
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4}
+class ElasticInOutCurve extends Curve {
+  /// Creates an elastic-in-out curve.
+  ///
+  /// Rather than creating a new instance, consider using [Curves.elasticInOut].
+  const ElasticInOutCurve([this.period = 0.4]);
+
+  /// The duration of the oscillation.
+  final double period;
+
+  @override
+  double transformInternal(double t) {
+    final double s = period / 4.0;
+    t = 2.0 * t - 1.0;
+    if (t < 0.0)
+      return -0.5 * math.pow(2.0, 10.0 * t) * math.sin((t - s) * (math.pi * 2.0) / period);
+    else
+      return math.pow(2.0, -10.0 * t) * math.sin((t - s) * (math.pi * 2.0) / period) * 0.5 + 1.0;
+  }
+
+  @override
+  String toString() {
+    return '${objectRuntimeType(this, 'ElasticInOutCurve')}($period)';
+  }
+}
+
+
+// PREDEFINED CURVES
+
+/// A collection of common animation curves.
+///
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_sine.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quad.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_cubic.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quart.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quint.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_expo.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_circ.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_back.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_sine.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quad.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quart.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quint.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_expo.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_circ.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_back.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_sine.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quad.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quart.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quint.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_expo.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_circ.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_back.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_slow_middle.mp4}
+/// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4}
+///
+/// See also:
+///
+///  * [Curve], the interface implemented by the constants available from the
+///    [Curves] class.
+class Curves {
+  // This class is not meant to be instatiated or extended; this constructor
+  // prevents instantiation and extension.
+  // ignore: unused_element
+  Curves._();
+
+  /// A linear animation curve.
+  ///
+  /// This is the identity map over the unit interval: its [Curve.transform]
+  /// method returns its input unmodified. This is useful as a default curve for
+  /// cases where a [Curve] is required but no actual curve is desired.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear.mp4}
+  static const Curve linear = _Linear._();
+
+  /// A curve where the rate of change starts out quickly and then decelerates; an
+  /// upside-down `f(t) = t²` parabola.
+  ///
+  /// This is equivalent to the Android `DecelerateInterpolator` class with a unit
+  /// factor (the default factor).
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_decelerate.mp4}
+  static const Curve decelerate = _DecelerateCurve._();
+
+  /// A curve that is very steep and linear at the beginning, but quickly flattens out
+  /// and very slowly eases in.
+  ///
+  /// By default is the curve used to animate pages on iOS back to their original
+  /// position if a swipe gesture is ended midway through a swipe.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_linear_to_slow_ease_in.mp4}
+  static const Cubic fastLinearToSlowEaseIn = Cubic(0.18, 1.0, 0.04, 1.0);
+
+  /// A cubic animation curve that speeds up quickly and ends slowly.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease.mp4}
+  static const Cubic ease = Cubic(0.25, 0.1, 0.25, 1.0);
+
+  /// A cubic animation curve that starts slowly and ends quickly.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in.mp4}
+  static const Cubic easeIn = Cubic(0.42, 0.0, 1.0, 1.0);
+
+  /// A cubic animation curve that starts starts slowly and ends linearly.
+  ///
+  /// The symmetric animation to [linearToEaseOut].
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_to_linear.mp4}
+  static const Cubic easeInToLinear = Cubic(0.67, 0.03, 0.65, 0.09);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This is
+  /// similar to [Curves.easeIn], but with sinusoidal easing for a slightly less
+  /// abrupt beginning and end. Nonetheless, the result is quite gentle and is
+  /// hard to distinguish from [Curves.linear] at a glance.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_sine.mp4}
+  static const Cubic easeInSine = Cubic(0.47, 0.0, 0.745, 0.715);
+
+  /// A cubic animation curve that starts slowly and ends quickly. Based on a
+  /// quadratic equation where `f(t) = t²`, this is effectively the inverse of
+  /// [Curves.decelerate].
+  ///
+  /// Compared to [Curves.easeInSine], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quad.mp4}
+  static const Cubic easeInQuad = Cubic(0.55, 0.085, 0.68, 0.53);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve is
+  /// based on a cubic equation where `f(t) = t³`. The result is a safe sweet
+  /// spot when choosing a curve for widgets animating off the viewport.
+  ///
+  /// Compared to [Curves.easeInQuad], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_cubic.mp4}
+  static const Cubic easeInCubic = Cubic(0.55, 0.055, 0.675, 0.19);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve is
+  /// based on a quartic equation where `f(t) = t⁴`.
+  ///
+  /// Animations using this curve or steeper curves will benefit from a longer
+  /// duration to avoid motion feeling unnatural.
+  ///
+  /// Compared to [Curves.easeInCubic], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quart.mp4}
+  static const Cubic easeInQuart = Cubic(0.895, 0.03, 0.685, 0.22);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve is
+  /// based on a quintic equation where `f(t) = t⁵`.
+  ///
+  /// Compared to [Curves.easeInQuart], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_quint.mp4}
+  static const Cubic easeInQuint = Cubic(0.755, 0.05, 0.855, 0.06);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve is
+  /// based on an exponential equation where `f(t) = 2¹⁰⁽ᵗ⁻¹⁾`.
+  ///
+  /// Using this curve can give your animations extra flare, but a longer
+  /// duration may need to be used to compensate for the steepness of the curve.
+  ///
+  /// Compared to [Curves.easeInQuint], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_expo.mp4}
+  static const Cubic easeInExpo = Cubic(0.95, 0.05, 0.795, 0.035);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve is
+  /// effectively the bottom-right quarter of a circle.
+  ///
+  /// Like [Curves.easeInExpo], this curve is fairly dramatic and will reduce
+  /// the clarity of an animation if not given a longer duration.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_circ.mp4}
+  static const Cubic easeInCirc = Cubic(0.6, 0.04, 0.98, 0.335);
+
+  /// A cubic animation curve that starts slowly and ends quickly. This curve
+  /// is similar to [Curves.elasticIn] in that it overshoots its bounds before
+  /// reaching its end. Instead of repeated swinging motions before ascending,
+  /// though, this curve overshoots once, then continues to ascend.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_back.mp4}
+  static const Cubic easeInBack = Cubic(0.6, -0.28, 0.735, 0.045);
+
+  /// A cubic animation curve that starts quickly and ends slowly.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out.mp4}
+  static const Cubic easeOut = Cubic(0.0, 0.0, 0.58, 1.0);
+
+  /// A cubic animation curve that starts linearly and ends slowly.
+  ///
+  /// A symmetric animation to [easeInToLinear].
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_linear_to_ease_out.mp4}
+  static const Cubic linearToEaseOut = Cubic(0.35, 0.91, 0.33, 0.97);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This is
+  /// similar to [Curves.easeOut], but with sinusoidal easing for a slightly
+  /// less abrupt beginning and end. Nonetheless, the result is quite gentle and
+  /// is hard to distinguish from [Curves.linear] at a glance.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_sine.mp4}
+  static const Cubic easeOutSine = Cubic(0.39, 0.575, 0.565, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This is
+  /// effectively the same as [Curves.decelerate], only simulated using a cubic
+  /// bezier function.
+  ///
+  /// Compared to [Curves.easeOutSine], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quad.mp4}
+  static const Cubic easeOutQuad = Cubic(0.25, 0.46, 0.45, 0.94);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// a flipped version of [Curves.easeInCubic].
+  ///
+  /// The result is a safe sweet spot when choosing a curve for animating a
+  /// widget's position entering or already inside the viewport.
+  ///
+  /// Compared to [Curves.easeOutQuad], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_cubic.mp4}
+  static const Cubic easeOutCubic = Cubic(0.215, 0.61, 0.355, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// a flipped version of [Curves.easeInQuart].
+  ///
+  /// Animations using this curve or steeper curves will benefit from a longer
+  /// duration to avoid motion feeling unnatural.
+  ///
+  /// Compared to [Curves.easeOutCubic], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quart.mp4}
+  static const Cubic easeOutQuart = Cubic(0.165, 0.84, 0.44, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// a flipped version of [Curves.easeInQuint].
+  ///
+  /// Compared to [Curves.easeOutQuart], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_quint.mp4}
+  static const Cubic easeOutQuint = Cubic(0.23, 1.0, 0.32, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// a flipped version of [Curves.easeInExpo]. Using this curve can give your
+  /// animations extra flare, but a longer duration may need to be used to
+  /// compensate for the steepness of the curve.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_expo.mp4}
+  static const Cubic easeOutExpo = Cubic(0.19, 1.0, 0.22, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// effectively the top-left quarter of a circle.
+  ///
+  /// Like [Curves.easeOutExpo], this curve is fairly dramatic and will reduce
+  /// the clarity of an animation if not given a longer duration.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_circ.mp4}
+  static const Cubic easeOutCirc = Cubic(0.075, 0.82, 0.165, 1.0);
+
+  /// A cubic animation curve that starts quickly and ends slowly. This curve is
+  /// similar to [Curves.elasticOut] in that it overshoots its bounds before
+  /// reaching its end. Instead of repeated swinging motions after ascending,
+  /// though, this curve only overshoots once.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_out_back.mp4}
+  static const Cubic easeOutBack = Cubic(0.175, 0.885, 0.32, 1.275);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out.mp4}
+  static const Cubic easeInOut = Cubic(0.42, 0.0, 0.58, 1.0);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This is similar to [Curves.easeInOut], but with sinusoidal easing
+  /// for a slightly less abrupt beginning and end.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_sine.mp4}
+  static const Cubic easeInOutSine = Cubic(0.445, 0.05, 0.55, 0.95);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInQuad] as the first
+  /// half, and [Curves.easeOutQuad] as the second.
+  ///
+  /// Compared to [Curves.easeInOutSine], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quad.mp4}
+  static const Cubic easeInOutQuad = Cubic(0.455, 0.03, 0.515, 0.955);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInCubic] as the first
+  /// half, and [Curves.easeOutCubic] as the second.
+  ///
+  /// The result is a safe sweet spot when choosing a curve for a widget whose
+  /// initial and final positions are both within the viewport.
+  ///
+  /// Compared to [Curves.easeInOutQuad], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_cubic.mp4}
+  static const Cubic easeInOutCubic = Cubic(0.645, 0.045, 0.355, 1.0);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInQuart] as the first
+  /// half, and [Curves.easeOutQuart] as the second.
+  ///
+  /// Animations using this curve or steeper curves will benefit from a longer
+  /// duration to avoid motion feeling unnatural.
+  ///
+  /// Compared to [Curves.easeInOutCubic], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quart.mp4}
+  static const Cubic easeInOutQuart = Cubic(0.77, 0.0, 0.175, 1.0);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInQuint] as the first
+  /// half, and [Curves.easeOutQuint] as the second.
+  ///
+  /// Compared to [Curves.easeInOutQuart], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_quint.mp4}
+  static const Cubic easeInOutQuint = Cubic(0.86, 0.0, 0.07, 1.0);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly.
+  ///
+  /// Since this curve is arrived at with an exponential function, the midpoint
+  /// is exceptionally steep. Extra consideration should be taken when designing
+  /// an animation using this.
+  ///
+  /// Compared to [Curves.easeInOutQuint], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_expo.mp4}
+  static const Cubic easeInOutExpo = Cubic(1.0, 0.0, 0.0, 1.0);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInCirc] as the first
+  /// half, and [Curves.easeOutCirc] as the second.
+  ///
+  /// Like [Curves.easeInOutExpo], this curve is fairly dramatic and will reduce
+  /// the clarity of an animation if not given a longer duration.
+  ///
+  /// Compared to [Curves.easeInOutExpo], this curve is slightly steeper.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_circ.mp4}
+  static const Cubic easeInOutCirc = Cubic(0.785, 0.135, 0.15, 0.86);
+
+  /// A cubic animation curve that starts slowly, speeds up, and then ends
+  /// slowly. This curve can be imagined as [Curves.easeInBack] as the first
+  /// half, and [Curves.easeOutBack] as the second.
+  ///
+  /// Since two curves are used as a basis for this curve, the resulting
+  /// animation will overshoot its bounds twice before reaching its end - first
+  /// by exceeding its lower bound, then exceeding its upper bound and finally
+  /// descending to its final position.
+  ///
+  /// Derived from Robert Penner’s easing functions.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_ease_in_out_back.mp4}
+  static const Cubic easeInOutBack = Cubic(0.68, -0.55, 0.265, 1.55);
+
+  /// A curve that starts quickly and eases into its final position.
+  ///
+  /// Over the course of the animation, the object spends more time near its
+  /// final destination. As a result, the user isn’t left waiting for the
+  /// animation to finish, and the negative effects of motion are minimized.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_fast_out_slow_in.mp4}
+  ///
+  /// See also:
+  ///
+  ///  * [standardEasing], the name for this curve in the Material specification.
+  static const Cubic fastOutSlowIn = Cubic(0.4, 0.0, 0.2, 1.0);
+
+  /// A cubic animation curve that starts quickly, slows down, and then ends
+  /// quickly.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_slow_middle.mp4}
+  static const Cubic slowMiddle = Cubic(0.15, 0.85, 0.85, 0.15);
+
+  /// An oscillating curve that grows in magnitude.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in.mp4}
+  static const Curve bounceIn = _BounceInCurve._();
+
+  /// An oscillating curve that first grows and then shrink in magnitude.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_out.mp4}
+  static const Curve bounceOut = _BounceOutCurve._();
+
+  /// An oscillating curve that first grows and then shrink in magnitude.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_bounce_in_out.mp4}
+  static const Curve bounceInOut = _BounceInOutCurve._();
+
+  /// An oscillating curve that grows in magnitude while overshooting its bounds.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in.mp4}
+  static const ElasticInCurve elasticIn = ElasticInCurve();
+
+  /// An oscillating curve that shrinks in magnitude while overshooting its bounds.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_out.mp4}
+  static const ElasticOutCurve elasticOut = ElasticOutCurve();
+
+  /// An oscillating curve that grows and then shrinks in magnitude while overshooting its bounds.
+  ///
+  /// {@animation 464 192 https://flutter.github.io/assets-for-api-docs/assets/animation/curve_elastic_in_out.mp4}
+  static const ElasticInOutCurve elasticInOut = ElasticInOutCurve();
+}
diff --git a/lib/src/animation/listener_helpers.dart b/lib/src/animation/listener_helpers.dart
new file mode 100644
index 0000000..686100b
--- /dev/null
+++ b/lib/src/animation/listener_helpers.dart
@@ -0,0 +1,222 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'package:flute/foundation.dart';
+
+import 'animation.dart';
+
+/// A mixin that helps listen to another object only when this object has registered listeners.
+///
+/// This mixin provides implementations of [didRegisterListener] and [didUnregisterListener],
+/// and therefore can be used in conjunction with mixins that require these methods,
+/// [AnimationLocalListenersMixin] and [AnimationLocalStatusListenersMixin].
+mixin AnimationLazyListenerMixin {
+  int _listenerCounter = 0;
+
+  /// Calls [didStartListening] every time a registration of a listener causes
+  /// an empty list of listeners to become non-empty.
+  ///
+  /// See also:
+  ///
+  ///  * [didUnregisterListener], which may cause the listener list to
+  ///    become empty again, and in turn cause this method to call
+  ///    [didStartListening] again.
+  void didRegisterListener() {
+    assert(_listenerCounter >= 0);
+    if (_listenerCounter == 0)
+      didStartListening();
+    _listenerCounter += 1;
+  }
+
+  /// Calls [didStopListening] when an only remaining listener is unregistered,
+  /// thus making the list empty.
+  ///
+  /// See also:
+  ///
+  ///  * [didRegisterListener], which causes the listener list to become non-empty.
+  void didUnregisterListener() {
+    assert(_listenerCounter >= 1);
+    _listenerCounter -= 1;
+    if (_listenerCounter == 0)
+      didStopListening();
+  }
+
+  /// Called when the number of listeners changes from zero to one.
+  @protected
+  void didStartListening();
+
+  /// Called when the number of listeners changes from one to zero.
+  @protected
+  void didStopListening();
+
+  /// Whether there are any listeners.
+  bool get isListening => _listenerCounter > 0;
+}
+
+/// A mixin that replaces the [didRegisterListener]/[didUnregisterListener] contract
+/// with a dispose contract.
+///
+/// This mixin provides implementations of [didRegisterListener] and [didUnregisterListener],
+/// and therefore can be used in conjunction with mixins that require these methods,
+/// [AnimationLocalListenersMixin] and [AnimationLocalStatusListenersMixin].
+mixin AnimationEagerListenerMixin {
+  /// This implementation ignores listener registrations.
+  void didRegisterListener() { }
+
+  /// This implementation ignores listener registrations.
+  void didUnregisterListener() { }
+
+  /// Release the resources used by this object. The object is no longer usable
+  /// after this method is called.
+  @mustCallSuper
+  void dispose() { }
+}
+
+/// A mixin that implements the [addListener]/[removeListener] protocol and notifies
+/// all the registered listeners when [notifyListeners] is called.
+///
+/// This mixin requires that the mixing class provide methods [didRegisterListener]
+/// and [didUnregisterListener]. Implementations of these methods can be obtained
+/// by mixing in another mixin from this library, such as [AnimationLazyListenerMixin].
+mixin AnimationLocalListenersMixin {
+  final ObserverList<VoidCallback> _listeners = ObserverList<VoidCallback>();
+
+  /// Called immediately before a listener is added via [addListener].
+  ///
+  /// At the time this method is called the registered listener is not yet
+  /// notified by [notifyListeners].
+  void didRegisterListener();
+
+  /// Called immediately after a listener is removed via [removeListener].
+  ///
+  /// At the time this method is called the removed listener is no longer
+  /// notified by [notifyListeners].
+  void didUnregisterListener();
+
+  /// Calls the listener every time the value of the animation changes.
+  ///
+  /// Listeners can be removed with [removeListener].
+  void addListener(VoidCallback listener) {
+    didRegisterListener();
+    _listeners.add(listener);
+  }
+
+  /// Stop calling the listener every time the value of the animation changes.
+  ///
+  /// Listeners can be added with [addListener].
+  void removeListener(VoidCallback listener) {
+    final bool removed = _listeners.remove(listener);
+    if (removed) {
+      didUnregisterListener();
+    }
+  }
+
+  /// Calls all the listeners.
+  ///
+  /// If listeners are added or removed during this function, the modifications
+  /// will not change which listeners are called during this iteration.
+  void notifyListeners() {
+    final List<VoidCallback> localListeners = List<VoidCallback>.from(_listeners);
+    for (final VoidCallback listener in localListeners) {
+      InformationCollector? collector;
+      assert(() {
+        collector = () sync* {
+          yield DiagnosticsProperty<AnimationLocalListenersMixin>(
+            'The $runtimeType notifying listeners was',
+            this,
+            style: DiagnosticsTreeStyle.errorProperty,
+          );
+        };
+        return true;
+      }());
+      try {
+        if (_listeners.contains(listener))
+          listener();
+      } catch (exception, stack) {
+        FlutterError.reportError(FlutterErrorDetails(
+          exception: exception,
+          stack: stack,
+          library: 'animation library',
+          context: ErrorDescription('while notifying listeners for $runtimeType'),
+          informationCollector: collector,
+        ));
+      }
+    }
+  }
+}
+
+/// A mixin that implements the addStatusListener/removeStatusListener protocol
+/// and notifies all the registered listeners when notifyStatusListeners is
+/// called.
+///
+/// This mixin requires that the mixing class provide methods [didRegisterListener]
+/// and [didUnregisterListener]. Implementations of these methods can be obtained
+/// by mixing in another mixin from this library, such as [AnimationLazyListenerMixin].
+mixin AnimationLocalStatusListenersMixin {
+  final ObserverList<AnimationStatusListener> _statusListeners = ObserverList<AnimationStatusListener>();
+
+  /// Called immediately before a status listener is added via [addStatusListener].
+  ///
+  /// At the time this method is called the registered listener is not yet
+  /// notified by [notifyStatusListeners].
+  void didRegisterListener();
+
+  /// Called immediately after a status listener is removed via [removeStatusListener].
+  ///
+  /// At the time this method is called the removed listener is no longer
+  /// notified by [notifyStatusListeners].
+  void didUnregisterListener();
+
+  /// Calls listener every time the status of the animation changes.
+  ///
+  /// Listeners can be removed with [removeStatusListener].
+  void addStatusListener(AnimationStatusListener listener) {
+    didRegisterListener();
+    _statusListeners.add(listener);
+  }
+
+  /// Stops calling the listener every time the status of the animation changes.
+  ///
+  /// Listeners can be added with [addStatusListener].
+  void removeStatusListener(AnimationStatusListener listener) {
+    final bool removed = _statusListeners.remove(listener);
+    if (removed) {
+      didUnregisterListener();
+    }
+  }
+
+  /// Calls all the status listeners.
+  ///
+  /// If listeners are added or removed during this function, the modifications
+  /// will not change which listeners are called during this iteration.
+  void notifyStatusListeners(AnimationStatus status) {
+    final List<AnimationStatusListener> localListeners = List<AnimationStatusListener>.from(_statusListeners);
+    for (final AnimationStatusListener listener in localListeners) {
+      try {
+        if (_statusListeners.contains(listener))
+          listener(status);
+      } catch (exception, stack) {
+        InformationCollector? collector;
+        assert(() {
+          collector = () sync* {
+            yield DiagnosticsProperty<AnimationLocalStatusListenersMixin>(
+              'The $runtimeType notifying status listeners was',
+              this,
+              style: DiagnosticsTreeStyle.errorProperty,
+            );
+          };
+          return true;
+        }());
+        FlutterError.reportError(FlutterErrorDetails(
+          exception: exception,
+          stack: stack,
+          library: 'animation library',
+          context: ErrorDescription('while notifying status listeners for $runtimeType'),
+          informationCollector: collector
+        ));
+      }
+    }
+  }
+}
diff --git a/lib/src/animation/tween.dart b/lib/src/animation/tween.dart
new file mode 100644
index 0000000..714a581
--- /dev/null
+++ b/lib/src/animation/tween.dart
@@ -0,0 +1,488 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/ui.dart' show Color, Size, Rect;
+
+import 'package:flute/foundation.dart';
+
+import 'animation.dart';
+import 'animations.dart';
+import 'curves.dart';
+
+// Examples can assume:
+// late Animation<Offset> _animation;
+// late AnimationController _controller;
+
+/// An object that can produce a value of type `T` given an [Animation<double>]
+/// as input.
+///
+/// Typically, the values of the input animation are nominally in the range 0.0
+/// to 1.0. In principle, however, any value could be provided.
+///
+/// The main subclass of [Animatable] is [Tween].
+abstract class Animatable<T> {
+  /// Abstract const constructor. This constructor enables subclasses to provide
+  /// const constructors so that they can be used in const expressions.
+  const Animatable();
+
+  /// Returns the value of the object at point `t`.
+  ///
+  /// The value of `t` is nominally a fraction in the range 0.0 to 1.0, though
+  /// in practice it may extend outside this range.
+  ///
+  /// See also:
+  ///
+  ///  * [evaluate], which is a shorthand for applying [transform] to the value
+  ///    of an [Animation].
+  ///  * [Curve.transform], a similar method for easing curves.
+  T transform(double t);
+
+  /// The current value of this object for the given [Animation].
+  ///
+  /// This function is implemented by deferring to [transform]. Subclasses that
+  /// want to provide custom behavior should override [transform], not
+  /// [evaluate].
+  ///
+  /// See also:
+  ///
+  ///  * [transform], which is similar but takes a `t` value directly instead of
+  ///    an [Animation].
+  ///  * [animate], which creates an [Animation] out of this object, continually
+  ///    applying [evaluate].
+  T evaluate(Animation<double> animation) => transform(animation.value);
+
+  /// Returns a new [Animation] that is driven by the given animation but that
+  /// takes on values determined by this object.
+  ///
+  /// Essentially this returns an [Animation] that automatically applies the
+  /// [evaluate] method to the parent's value.
+  ///
+  /// See also:
+  ///
+  ///  * [AnimationController.drive], which does the same thing from the
+  ///    opposite starting point.
+  Animation<T> animate(Animation<double> parent) {
+    return _AnimatedEvaluation<T>(parent, this);
+  }
+
+  /// Returns a new [Animatable] whose value is determined by first evaluating
+  /// the given parent and then evaluating this object.
+  ///
+  /// This allows [Tween]s to be chained before obtaining an [Animation].
+  Animatable<T> chain(Animatable<double> parent) {
+    return _ChainedEvaluation<T>(parent, this);
+  }
+}
+
+class _AnimatedEvaluation<T> extends Animation<T> with AnimationWithParentMixin<double> {
+  _AnimatedEvaluation(this.parent, this._evaluatable);
+
+  @override
+  final Animation<double> parent;
+
+  final Animatable<T> _evaluatable;
+
+  @override
+  T get value => _evaluatable.evaluate(parent);
+
+  @override
+  String toString() {
+    return '$parent\u27A9$_evaluatable\u27A9$value';
+  }
+
+  @override
+  String toStringDetails() {
+    return '${super.toStringDetails()} $_evaluatable';
+  }
+}
+
+class _ChainedEvaluation<T> extends Animatable<T> {
+  _ChainedEvaluation(this._parent, this._evaluatable);
+
+  final Animatable<double> _parent;
+  final Animatable<T> _evaluatable;
+
+  @override
+  T transform(double t) {
+    return _evaluatable.transform(_parent.transform(t));
+  }
+
+  @override
+  String toString() {
+    return '$_parent\u27A9$_evaluatable';
+  }
+}
+
+/// A linear interpolation between a beginning and ending value.
+///
+/// [Tween] is useful if you want to interpolate across a range.
+///
+/// To use a [Tween] object with an animation, call the [Tween] object's
+/// [animate] method and pass it the [Animation] object that you want to
+/// modify.
+///
+/// You can chain [Tween] objects together using the [chain] method, so that a
+/// single [Animation] object is configured by multiple [Tween] objects called
+/// in succession. This is different than calling the [animate] method twice,
+/// which results in two separate [Animation] objects, each configured with a
+/// single [Tween].
+///
+/// {@tool snippet}
+///
+/// Suppose `_controller` is an [AnimationController], and we want to create an
+/// [Animation<Offset>] that is controlled by that controller, and save it in
+/// `_animation`. Here are two possible ways of expressing this:
+///
+/// ```dart
+/// _animation = _controller.drive(
+///   Tween<Offset>(
+///     begin: const Offset(100.0, 50.0),
+///     end: const Offset(200.0, 300.0),
+///   ),
+/// );
+/// ```
+/// {@end-tool}
+/// {@tool snippet}
+///
+/// ```dart
+/// _animation = Tween<Offset>(
+///   begin: const Offset(100.0, 50.0),
+///   end: const Offset(200.0, 300.0),
+/// ).animate(_controller);
+/// ```
+/// {@end-tool}
+///
+/// In both cases, the `_animation` variable holds an object that, over the
+/// lifetime of the `_controller`'s animation, returns a value
+/// (`_animation.value`) that depicts a point along the line between the two
+/// offsets above. If we used a [MaterialPointArcTween] instead of a
+/// [Tween<Offset>] in the code above, the points would follow a pleasing curve
+/// instead of a straight line, with no other changes necessary.
+///
+/// ## Performance optimizations
+///
+/// Tweens are mutable; specifically, their [begin] and [end] values can be
+/// changed at runtime. An object created with [Animation.drive] using a [Tween]
+/// will immediately honor changes to that underlying [Tween] (though the
+/// listeners will only be triggered if the [Animation] is actively animating).
+/// This can be used to change an animation on the fly without having to
+/// recreate all the objects in the chain from the [AnimationController] to the
+/// final [Tween].
+///
+/// If a [Tween]'s values are never changed, however, a further optimization can
+/// be applied: the object can be stored in a `static final` variable, so that
+/// the exact same instance is used whenever the [Tween] is needed. This is
+/// preferable to creating an identical [Tween] afresh each time a [State.build]
+/// method is called, for example.
+///
+/// ## Types with special considerations
+///
+/// Classes with [lerp] static methods typically have corresponding dedicated
+/// [Tween] subclasses that call that method. For example, [ColorTween] uses
+/// [Color.lerp] to implement the [ColorTween.lerp] method.
+///
+/// Types that define `+` and `-` operators to combine values (`T + T → T` and
+/// `T - T → T`) and an `*` operator to scale by multiplying with a double (`T *
+/// double → T`) can be directly used with `Tween<T>`.
+///
+/// This does not extend to any type with `+`, `-`, and `*` operators. In
+/// particular, [int] does not satisfy this precise contract (`int * double`
+/// actually returns [num], not [int]). There are therefore two specific classes
+/// that can be used to interpolate integers:
+///
+///  * [IntTween], which is an approximation of a linear interpolation (using
+///    [double.round]).
+///  * [StepTween], which uses [double.floor] to ensure that the result is
+///    never greater than it would be using if a `Tween<double>`.
+///
+/// The relevant operators on [Size] also don't fulfill this contract, so
+/// [SizeTween] uses [Size.lerp].
+///
+/// In addition, some of the types that _do_ have suitable `+`, `-`, and `*`
+/// operators still have dedicated [Tween] subclasses that perform the
+/// interpolation in a more specialized manner. One such class is
+/// [MaterialPointArcTween], which is mentioned above. The [AlignmentTween], and
+/// [AlignmentGeometryTween], and [FractionalOffsetTween] are another group of
+/// [Tween]s that use dedicated `lerp` methods instead of merely relying on the
+/// operators (in particular, this allows them to handle null values in a more
+/// useful manner).
+///
+/// ## Nullability
+///
+/// The [begin] and [end] fields are nullable; a [Tween] does not have to
+/// have non-null values specified when it is created.
+///
+/// If `T` is nullable, then [lerp] and [transform] may return null.
+/// This is typically seen in the case where [begin] is null and `t`
+/// is 0.0, or [end] is null and `t` is 1.0, or both are null (at any
+/// `t` value).
+///
+/// If `T` is not nullable, then [begin] and [end] must both be set to
+/// non-null values before using [lerp] or [transform], otherwise they
+/// will throw.
+class Tween<T extends dynamic> extends Animatable<T> {
+  /// Creates a tween.
+  ///
+  /// The [begin] and [end] properties must be non-null before the tween is
+  /// first used, but the arguments can be null if the values are going to be
+  /// filled in later.
+  Tween({
+    this.begin,
+    this.end,
+  });
+
+  /// The value this variable has at the beginning of the animation.
+  ///
+  /// See the constructor for details about whether this property may be null
+  /// (it varies from subclass to subclass).
+  T? begin;
+
+  /// The value this variable has at the end of the animation.
+  ///
+  /// See the constructor for details about whether this property may be null
+  /// (it varies from subclass to subclass).
+  T? end;
+
+  /// Returns the value this variable has at the given animation clock value.
+  ///
+  /// The default implementation of this method uses the [+], [-], and [*]
+  /// operators on `T`. The [begin] and [end] properties must therefore be
+  /// non-null by the time this method is called.
+  ///
+  /// In general, however, it is possible for this to return null, especially
+  /// when `t`=0.0 and [begin] is null, or `t`=1.0 and [end] is null.
+  @protected
+  T lerp(double t) {
+    assert(begin != null);
+    assert(end != null);
+    return begin + (end - begin) * t as T;
+  }
+
+  /// Returns the interpolated value for the current value of the given animation.
+  ///
+  /// This method returns `begin` and `end` when the animation values are 0.0 or
+  /// 1.0, respectively.
+  ///
+  /// This function is implemented by deferring to [lerp]. Subclasses that want
+  /// to provide custom behavior should override [lerp], not [transform] (nor
+  /// [evaluate]).
+  ///
+  /// See the constructor for details about whether the [begin] and [end]
+  /// properties may be null when this is called. It varies from subclass to
+  /// subclass.
+  @override
+  T transform(double t) {
+    if (t == 0.0)
+      return begin as T;
+    if (t == 1.0)
+      return end as T;
+    return lerp(t);
+  }
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'Animatable')}($begin \u2192 $end)';
+}
+
+/// A [Tween] that evaluates its [parent] in reverse.
+class ReverseTween<T> extends Tween<T> {
+  /// Construct a [Tween] that evaluates its [parent] in reverse.
+  ReverseTween(this.parent)
+    : assert(parent != null),
+      super(begin: parent.end, end: parent.begin);
+
+  /// This tween's value is the same as the parent's value evaluated in reverse.
+  ///
+  /// This tween's [begin] is the parent's [end] and its [end] is the parent's
+  /// [begin]. The [lerp] method returns `parent.lerp(1.0 - t)` and its
+  /// [evaluate] method is similar.
+  final Tween<T> parent;
+
+  @override
+  T lerp(double t) => parent.lerp(1.0 - t);
+}
+
+/// An interpolation between two colors.
+///
+/// This class specializes the interpolation of [Tween<Color>] to use
+/// [Color.lerp].
+///
+/// The values can be null, representing no color (which is distinct to
+/// transparent black, as represented by [Colors.transparent]).
+///
+/// See [Tween] for a discussion on how to use interpolation objects.
+class ColorTween extends Tween<Color?> {
+  /// Creates a [Color] tween.
+  ///
+  /// The [begin] and [end] properties may be null; the null value
+  /// is treated as transparent.
+  ///
+  /// We recommend that you do not pass [Colors.transparent] as [begin]
+  /// or [end] if you want the effect of fading in or out of transparent.
+  /// Instead prefer null. [Colors.transparent] refers to black transparent and
+  /// thus will fade out of or into black which is likely unwanted.
+  ColorTween({ Color? begin, Color? end }) : super(begin: begin, end: end);
+
+  /// Returns the value this variable has at the given animation clock value.
+  @override
+  Color? lerp(double t) => Color.lerp(begin, end, t);
+}
+
+/// An interpolation between two sizes.
+///
+/// This class specializes the interpolation of [Tween<Size>] to use
+/// [Size.lerp].
+///
+/// The values can be null, representing [Size.zero].
+///
+/// See [Tween] for a discussion on how to use interpolation objects.
+class SizeTween extends Tween<Size?> {
+  /// Creates a [Size] tween.
+  ///
+  /// The [begin] and [end] properties may be null; the null value
+  /// is treated as an empty size.
+  SizeTween({ Size? begin, Size? end }) : super(begin: begin, end: end);
+
+  /// Returns the value this variable has at the given animation clock value.
+  @override
+  Size? lerp(double t) => Size.lerp(begin, end, t);
+}
+
+/// An interpolation between two rectangles.
+///
+/// This class specializes the interpolation of [Tween<Rect>] to use
+/// [Rect.lerp].
+///
+/// The values can be null, representing a zero-sized rectangle at the
+/// origin ([Rect.zero]).
+///
+/// See [Tween] for a discussion on how to use interpolation objects.
+class RectTween extends Tween<Rect?> {
+  /// Creates a [Rect] tween.
+  ///
+  /// The [begin] and [end] properties may be null; the null value
+  /// is treated as an empty rect at the top left corner.
+  RectTween({ Rect? begin, Rect? end }) : super(begin: begin, end: end);
+
+  /// Returns the value this variable has at the given animation clock value.
+  @override
+  Rect? lerp(double t) => Rect.lerp(begin, end, t);
+}
+
+/// An interpolation between two integers that rounds.
+///
+/// This class specializes the interpolation of [Tween<int>] to be
+/// appropriate for integers by interpolating between the given begin
+/// and end values and then rounding the result to the nearest
+/// integer.
+///
+/// This is the closest approximation to a linear tween that is possible with an
+/// integer. Compare to [StepTween] and [Tween<double>].
+///
+/// The [begin] and [end] values must be set to non-null values before
+/// calling [lerp] or [transform].
+///
+/// See [Tween] for a discussion on how to use interpolation objects.
+class IntTween extends Tween<int> {
+  /// Creates an int tween.
+  ///
+  /// The [begin] and [end] properties must be non-null before the tween is
+  /// first used, but the arguments can be null if the values are going to be
+  /// filled in later.
+  IntTween({ int? begin, int? end }) : super(begin: begin, end: end);
+
+  // The inherited lerp() function doesn't work with ints because it multiplies
+  // the begin and end types by a double, and int * double returns a double.
+  @override
+  int lerp(double t) => (begin! + (end! - begin!) * t).round();
+}
+
+/// An interpolation between two integers that floors.
+///
+/// This class specializes the interpolation of [Tween<int>] to be
+/// appropriate for integers by interpolating between the given begin
+/// and end values and then using [double.floor] to return the current
+/// integer component, dropping the fractional component.
+///
+/// This results in a value that is never greater than the equivalent
+/// value from a linear double interpolation. Compare to [IntTween].
+///
+/// The [begin] and [end] values must be set to non-null values before
+/// calling [lerp] or [transform].
+///
+/// See [Tween] for a discussion on how to use interpolation objects.
+class StepTween extends Tween<int> {
+  /// Creates an [int] tween that floors.
+  ///
+  /// The [begin] and [end] properties must be non-null before the tween is
+  /// first used, but the arguments can be null if the values are going to be
+  /// filled in later.
+  StepTween({ int? begin, int? end }) : super(begin: begin, end: end);
+
+  // The inherited lerp() function doesn't work with ints because it multiplies
+  // the begin and end types by a double, and int * double returns a double.
+  @override
+  int lerp(double t) => (begin! + (end! - begin!) * t).floor();
+}
+
+/// A tween with a constant value.
+class ConstantTween<T> extends Tween<T> {
+  /// Create a tween whose [begin] and [end] values equal [value].
+  ConstantTween(T value) : super(begin: value, end: value);
+
+  /// This tween doesn't interpolate, it always returns the same value.
+  @override
+  T lerp(double t) => begin as T;
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'ConstantTween')}(value: $begin)';
+}
+
+/// Transforms the value of the given animation by the given curve.
+///
+/// This class differs from [CurvedAnimation] in that [CurvedAnimation] applies
+/// a curve to an existing [Animation] object whereas [CurveTween] can be
+/// chained with another [Tween] prior to receiving the underlying [Animation].
+/// ([CurvedAnimation] also has the additional ability of having different
+/// curves when the animation is going forward vs when it is going backward,
+/// which can be useful in some scenarios.)
+///
+/// {@tool snippet}
+///
+/// The following code snippet shows how you can apply a curve to a linear
+/// animation produced by an [AnimationController] `controller`:
+///
+/// ```dart
+/// final Animation<double> animation = _controller.drive(
+///   CurveTween(curve: Curves.ease),
+/// );
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [CurvedAnimation], for an alternative way of expressing the sample above.
+///  * [AnimationController], for examples of creating and disposing of an
+///    [AnimationController].
+class CurveTween extends Animatable<double> {
+  /// Creates a curve tween.
+  ///
+  /// The [curve] argument must not be null.
+  CurveTween({ required this.curve })
+    : assert(curve != null);
+
+  /// The curve to use when transforming the value of the animation.
+  Curve curve;
+
+  @override
+  double transform(double t) {
+    if (t == 0.0 || t == 1.0) {
+      assert(curve.transform(t).round() == t);
+      return t;
+    }
+    return curve.transform(t);
+  }
+
+  @override
+  String toString() => '${objectRuntimeType(this, 'CurveTween')}(curve: $curve)';
+}
diff --git a/lib/src/animation/tween_sequence.dart b/lib/src/animation/tween_sequence.dart
new file mode 100644
index 0000000..6a6095a
--- /dev/null
+++ b/lib/src/animation/tween_sequence.dart
@@ -0,0 +1,169 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+
+import 'animation.dart';
+import 'tween.dart';
+
+// Examples can assume:
+// late AnimationController myAnimationController;
+
+/// Enables creating an [Animation] whose value is defined by a sequence of
+/// [Tween]s.
+///
+/// Each [TweenSequenceItem] has a weight that defines its percentage of the
+/// animation's duration. Each tween defines the animation's value during the
+/// interval indicated by its weight.
+///
+/// {@tool snippet}
+/// This example defines an animation that uses an easing curve to interpolate
+/// between 5.0 and 10.0 during the first 40% of the animation, remains at 10.0
+/// for the next 20%, and then returns to 5.0 for the final 40%.
+///
+/// ```dart
+/// final Animation<double> animation = TweenSequence(
+///   <TweenSequenceItem<double>>[
+///     TweenSequenceItem<double>(
+///       tween: Tween<double>(begin: 5.0, end: 10.0)
+///         .chain(CurveTween(curve: Curves.ease)),
+///       weight: 40.0,
+///     ),
+///     TweenSequenceItem<double>(
+///       tween: ConstantTween<double>(10.0),
+///       weight: 20.0,
+///     ),
+///     TweenSequenceItem<double>(
+///       tween: Tween<double>(begin: 10.0, end: 5.0)
+///         .chain(CurveTween(curve: Curves.ease)),
+///       weight: 40.0,
+///     ),
+///   ],
+/// ).animate(myAnimationController);
+/// ```
+/// {@end-tool}
+class TweenSequence<T> extends Animatable<T> {
+  /// Construct a TweenSequence.
+  ///
+  /// The [items] parameter must be a list of one or more [TweenSequenceItem]s.
+  ///
+  /// There's a small cost associated with building a `TweenSequence` so it's
+  /// best to reuse one, rather than rebuilding it on every frame, when that's
+  /// possible.
+  TweenSequence(List<TweenSequenceItem<T>> items)
+      : assert(items != null),
+        assert(items.isNotEmpty) {
+    _items.addAll(items);
+
+    double totalWeight = 0.0;
+    for (final TweenSequenceItem<T> item in _items)
+      totalWeight += item.weight;
+    assert(totalWeight > 0.0);
+
+    double start = 0.0;
+    for (int i = 0; i < _items.length; i += 1) {
+      final double end = i == _items.length - 1 ? 1.0 : start + _items[i].weight / totalWeight;
+      _intervals.add(_Interval(start, end));
+      start = end;
+    }
+  }
+
+  final List<TweenSequenceItem<T>> _items = <TweenSequenceItem<T>>[];
+  final List<_Interval> _intervals = <_Interval>[];
+
+  T _evaluateAt(double t, int index) {
+    final TweenSequenceItem<T> element = _items[index];
+    final double tInterval = _intervals[index].value(t);
+    return element.tween.transform(tInterval);
+  }
+
+  @override
+  T transform(double t) {
+    assert(t >= 0.0 && t <= 1.0);
+    if (t == 1.0)
+      return _evaluateAt(t, _items.length - 1);
+    for (int index = 0; index < _items.length; index++) {
+      if (_intervals[index].contains(t))
+        return _evaluateAt(t, index);
+    }
+    // Should be unreachable.
+    throw StateError('TweenSequence.evaluate() could not find an interval for $t');
+  }
+
+  @override
+  String toString() => 'TweenSequence(${_items.length} items)';
+}
+
+/// Enables creating a flipped [Animation] whose value is defined by a sequence
+/// of [Tween]s.
+///
+/// This creates a [TweenSequence] that evaluates to a result that flips the
+/// tween both horizontally and vertically.
+///
+/// This tween sequence assumes that the evaluated result has to be a double
+/// between 0.0 and 1.0.
+class FlippedTweenSequence extends TweenSequence<double> {
+  /// Creates a flipped [TweenSequence].
+  ///
+  /// The [items] parameter must be a list of one or more [TweenSequenceItem]s.
+  ///
+  /// There's a small cost associated with building a `TweenSequence` so it's
+  /// best to reuse one, rather than rebuilding it on every frame, when that's
+  /// possible.
+  FlippedTweenSequence(List<TweenSequenceItem<double>> items)
+    : assert(items != null),
+      super(items);
+
+  @override
+  double transform(double t) => 1 - super.transform(1 - t);
+}
+
+/// A simple holder for one element of a [TweenSequence].
+class TweenSequenceItem<T> {
+  /// Construct a TweenSequenceItem.
+  ///
+  /// The [tween] must not be null and [weight] must be greater than 0.0.
+  const TweenSequenceItem({
+    required this.tween,
+    required this.weight,
+  }) : assert(tween != null),
+       assert(weight != null),
+       assert(weight > 0.0);
+
+  /// Defines the value of the [TweenSequence] for the interval within the
+  /// animation's duration indicated by [weight] and this item's position
+  /// in the list of items.
+  ///
+  /// {@tool snippet}
+  ///
+  /// The value of this item can be "curved" by chaining it to a [CurveTween].
+  /// For example to create a tween that eases from 0.0 to 10.0:
+  ///
+  /// ```dart
+  /// Tween<double>(begin: 0.0, end: 10.0)
+  ///   .chain(CurveTween(curve: Curves.ease))
+  /// ```
+  /// {@end-tool}
+  final Animatable<T> tween;
+
+  /// An arbitrary value that indicates the relative percentage of a
+  /// [TweenSequence] animation's duration when [tween] will be used.
+  ///
+  /// The percentage for an individual item is the item's weight divided by the
+  /// sum of all of the items' weights.
+  final double weight;
+}
+
+class _Interval {
+  const _Interval(this.start, this.end) : assert(end > start);
+
+  final double start;
+  final double end;
+
+  bool contains(double t) => t >= start && t < end;
+
+  double value(double t) => (t - start) / (end - start);
+
+  @override
+  String toString() => '<$start, $end>';
+}
diff --git a/lib/src/cupertino/action_sheet.dart b/lib/src/cupertino/action_sheet.dart
new file mode 100644
index 0000000..badd855
--- /dev/null
+++ b/lib/src/cupertino/action_sheet.dart
@@ -0,0 +1,1431 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/ui.dart' show ImageFilter;
+
+import 'package:flute/foundation.dart';
+import 'package:flute/rendering.dart';
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'interface_level.dart';
+import 'scrollbar.dart';
+import 'theme.dart';
+
+const TextStyle _kActionSheetActionStyle = TextStyle(
+  fontFamily: '.SF UI Text',
+  inherit: false,
+  fontSize: 20.0,
+  fontWeight: FontWeight.w400,
+  textBaseline: TextBaseline.alphabetic,
+);
+
+const TextStyle _kActionSheetContentStyle = TextStyle(
+  fontFamily: '.SF UI Text',
+  inherit: false,
+  fontSize: 13.0,
+  fontWeight: FontWeight.w400,
+  color: _kContentTextColor,
+  textBaseline: TextBaseline.alphabetic,
+);
+
+// Translucent, very light gray that is painted on top of the blurred backdrop
+// as the action sheet's background color.
+// TODO(LongCatIsLooong): https://github.com/flutter/flutter/issues/39272. Use
+// System Materials once we have them.
+// Extracted from https://developer.apple.com/design/resources/.
+const Color _kBackgroundColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xC7F9F9F9),
+  darkColor: Color(0xC7252525),
+);
+
+// Translucent, light gray that is painted on top of the blurred backdrop as
+// the background color of a pressed button.
+// Eye-balled from iOS 13 beta simulator.
+const Color _kPressedColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xFFE1E1E1),
+  darkColor: Color(0xFF2E2E2E),
+);
+
+const Color _kCancelPressedColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xFFECECEC),
+  darkColor: Color(0xFF49494B),
+);
+
+// The gray color used for text that appears in the title area.
+// Extracted from https://developer.apple.com/design/resources/.
+const Color _kContentTextColor = Color(0xFF8F8F8F);
+
+// Translucent gray that is painted on top of the blurred backdrop in the gap
+// areas between the content section and actions section, as well as between
+// buttons.
+// Eye-balled from iOS 13 beta simulator.
+const Color _kButtonDividerColor = _kContentTextColor;
+
+const double _kBlurAmount = 20.0;
+const double _kEdgeHorizontalPadding = 8.0;
+const double _kCancelButtonPadding = 8.0;
+const double _kEdgeVerticalPadding = 10.0;
+const double _kContentHorizontalPadding = 40.0;
+const double _kContentVerticalPadding = 14.0;
+const double _kButtonHeight = 56.0;
+const double _kCornerRadius = 14.0;
+const double _kDividerThickness = 1.0;
+
+/// An iOS-style action sheet.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=U-ao8p4A82k}
+///
+/// An action sheet is a specific style of alert that presents the user
+/// with a set of two or more choices related to the current context.
+/// An action sheet can have a title, an additional message, and a list
+/// of actions. The title is displayed above the message and the actions
+/// are displayed below this content.
+///
+/// This action sheet styles its title and message to match standard iOS action
+/// sheet title and message text style.
+///
+/// To display action buttons that look like standard iOS action sheet buttons,
+/// provide [CupertinoActionSheetAction]s for the [actions] given to this action
+/// sheet.
+///
+/// To include a iOS-style cancel button separate from the other buttons,
+/// provide an [CupertinoActionSheetAction] for the [cancelButton] given to this
+/// action sheet.
+///
+/// An action sheet is typically passed as the child widget to
+/// [showCupertinoModalPopup], which displays the action sheet by sliding it up
+/// from the bottom of the screen.
+///
+/// {@tool snippet}
+/// This sample shows how to use a [CupertinoActionSheet].
+///	The [CupertinoActionSheet] shows an alert with a set of two choices
+/// when [CupertinoButton] is pressed.
+///
+/// ```dart
+/// class MyStatefulWidget extends StatefulWidget {
+///   @override
+///   _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
+/// }
+///
+/// class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+///   @override
+///   Widget build(BuildContext context) {
+///     return CupertinoPageScaffold(
+///       child: Center(
+///         child: CupertinoButton(
+///           onPressed: () {
+///             showCupertinoModalPopup(
+///               context: context,
+///               builder: (BuildContext context) => CupertinoActionSheet(
+///                 title: const Text('Title'),
+///                 message: const Text('Message'),
+///                 actions: [
+///                   CupertinoActionSheetAction(
+///                     child: const Text('Action One'),
+///                     onPressed: () {
+///                       Navigator.pop(context);
+///                     },
+///                   ),
+///                   CupertinoActionSheetAction(
+///                     child: const Text('Action Two'),
+///                     onPressed: () {
+///                       Navigator.pop(context);
+///                     },
+///                   )
+///                 ],
+///               ),
+///             );
+///           },
+///           child: Text('CupertinoActionSheet'),
+///         ),
+///       ),
+///     );
+///   }
+/// }
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [CupertinoActionSheetAction], which is an iOS-style action sheet button.
+///  * <https://developer.apple.com/design/human-interface-guidelines/ios/views/action-sheets/>
+class CupertinoActionSheet extends StatelessWidget {
+  /// Creates an iOS-style action sheet.
+  ///
+  /// An action sheet must have a non-null value for at least one of the
+  /// following arguments: [actions], [title], [message], or [cancelButton].
+  ///
+  /// Generally, action sheets are used to give the user a choice between
+  /// two or more choices for the current context.
+  const CupertinoActionSheet({
+    Key? key,
+    this.title,
+    this.message,
+    this.actions,
+    this.messageScrollController,
+    this.actionScrollController,
+    this.cancelButton,
+  }) : assert(actions != null || title != null || message != null || cancelButton != null,
+          'An action sheet must have a non-null value for at least one of the following arguments: '
+          'actions, title, message, or cancelButton'),
+       super(key: key);
+
+  /// An optional title of the action sheet. When the [message] is non-null,
+  /// the font of the [title] is bold.
+  ///
+  /// Typically a [Text] widget.
+  final Widget? title;
+
+  /// An optional descriptive message that provides more details about the
+  /// reason for the alert.
+  ///
+  /// Typically a [Text] widget.
+  final Widget? message;
+
+  /// The set of actions that are displayed for the user to select.
+  ///
+  /// Typically this is a list of [CupertinoActionSheetAction] widgets.
+  final List<Widget>? actions;
+
+  /// A scroll controller that can be used to control the scrolling of the
+  /// [message] in the action sheet.
+  ///
+  /// This attribute is typically not needed, as alert messages should be
+  /// short.
+  final ScrollController? messageScrollController;
+
+  /// A scroll controller that can be used to control the scrolling of the
+  /// [actions] in the action sheet.
+  ///
+  /// This attribute is typically not needed.
+  final ScrollController? actionScrollController;
+
+  /// The optional cancel button that is grouped separately from the other
+  /// actions.
+  ///
+  /// Typically this is an [CupertinoActionSheetAction] widget.
+  final Widget? cancelButton;
+
+  Widget _buildContent(BuildContext context) {
+    final List<Widget> content = <Widget>[];
+    if (title != null || message != null) {
+      final Widget titleSection = _CupertinoAlertContentSection(
+        title: title,
+        message: message,
+        scrollController: messageScrollController,
+      );
+      content.add(Flexible(child: titleSection));
+    }
+
+    return Container(
+      color: CupertinoDynamicColor.resolve(_kBackgroundColor, context),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        children: content,
+      ),
+    );
+  }
+
+  Widget _buildActions() {
+    if (actions == null || actions!.isEmpty) {
+      return Container(
+        height: 0.0,
+      );
+    }
+    return Container(
+      child: _CupertinoAlertActionSection(
+        children: actions!,
+        scrollController: actionScrollController,
+        hasCancelButton: cancelButton != null,
+      ),
+    );
+  }
+
+  Widget _buildCancelButton() {
+    final double cancelPadding = (actions != null || message != null || title != null)
+        ? _kCancelButtonPadding : 0.0;
+    return Padding(
+      padding: EdgeInsets.only(top: cancelPadding),
+      child: _CupertinoActionSheetCancelButton(
+        child: cancelButton,
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    assert(debugCheckHasMediaQuery(context));
+
+    final List<Widget> children = <Widget>[
+      Flexible(child: ClipRRect(
+          borderRadius: BorderRadius.circular(12.0),
+          child: BackdropFilter(
+            filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
+            child: _CupertinoAlertRenderWidget(
+              contentSection: Builder(builder: _buildContent),
+              actionsSection: _buildActions(),
+            ),
+          ),
+        ),
+      ),
+      if (cancelButton != null) _buildCancelButton(),
+    ];
+
+    final Orientation orientation = MediaQuery.of(context).orientation;
+    final double actionSheetWidth;
+    if (orientation == Orientation.portrait) {
+      actionSheetWidth = MediaQuery.of(context).size.width - (_kEdgeHorizontalPadding * 2);
+    } else {
+      actionSheetWidth = MediaQuery.of(context).size.height - (_kEdgeHorizontalPadding * 2);
+    }
+
+    return SafeArea(
+      child: Semantics(
+        namesRoute: true,
+        scopesRoute: true,
+        explicitChildNodes: true,
+        label: 'Alert',
+        child: CupertinoUserInterfaceLevel(
+          data: CupertinoUserInterfaceLevelData.elevated,
+          child: Container(
+            width: actionSheetWidth,
+            margin: const EdgeInsets.symmetric(
+              horizontal: _kEdgeHorizontalPadding,
+              vertical: _kEdgeVerticalPadding,
+            ),
+            child: Column(
+              children: children,
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.stretch,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+/// A button typically used in a [CupertinoActionSheet].
+///
+/// See also:
+///
+///  * [CupertinoActionSheet], an alert that presents the user with a set of two or
+///    more choices related to the current context.
+class CupertinoActionSheetAction extends StatelessWidget {
+  /// Creates an action for an iOS-style action sheet.
+  ///
+  /// The [child] and [onPressed] arguments must not be null.
+  const CupertinoActionSheetAction({
+    Key? key,
+    required this.onPressed,
+    this.isDefaultAction = false,
+    this.isDestructiveAction = false,
+    required this.child,
+  }) : assert(child != null),
+       assert(onPressed != null),
+       super(key: key);
+
+  /// The callback that is called when the button is tapped.
+  ///
+  /// This attribute must not be null.
+  final VoidCallback onPressed;
+
+  /// Whether this action is the default choice in the action sheet.
+  ///
+  /// Default buttons have bold text.
+  final bool isDefaultAction;
+
+  /// Whether this action might change or delete data.
+  ///
+  /// Destructive buttons have red text.
+  final bool isDestructiveAction;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// Typically a [Text] widget.
+  final Widget child;
+
+  @override
+  Widget build(BuildContext context) {
+    TextStyle style = _kActionSheetActionStyle.copyWith(
+      color: isDestructiveAction
+        ? CupertinoDynamicColor.resolve(CupertinoColors.systemRed, context)
+        : CupertinoTheme.of(context).primaryColor,
+    );
+
+    if (isDefaultAction) {
+      style = style.copyWith(fontWeight: FontWeight.w600);
+    }
+
+    return GestureDetector(
+      onTap: onPressed,
+      behavior: HitTestBehavior.opaque,
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(
+          minHeight: _kButtonHeight,
+        ),
+        child: Semantics(
+          button: true,
+          child: Container(
+            alignment: Alignment.center,
+            padding: const EdgeInsets.symmetric(
+              vertical: 16.0,
+              horizontal: 10.0,
+            ),
+            child: DefaultTextStyle(
+              style: style,
+              child: child,
+              textAlign: TextAlign.center,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+class _CupertinoActionSheetCancelButton extends StatefulWidget {
+  const _CupertinoActionSheetCancelButton({
+    Key? key,
+    this.child,
+  }) : super(key: key);
+
+  final Widget? child;
+
+  @override
+  _CupertinoActionSheetCancelButtonState createState() => _CupertinoActionSheetCancelButtonState();
+}
+
+class _CupertinoActionSheetCancelButtonState extends State<_CupertinoActionSheetCancelButton> {
+  bool isBeingPressed = false;
+
+  void _onTapDown(TapDownDetails event) {
+    setState(() { isBeingPressed = true; });
+  }
+
+  void _onTapUp(TapUpDetails event) {
+    setState(() { isBeingPressed = false; });
+  }
+
+  void _onTapCancel() {
+    setState(() { isBeingPressed = false; });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final Color backgroundColor = isBeingPressed
+      ? _kCancelPressedColor
+      : CupertinoColors.secondarySystemGroupedBackground;
+    return GestureDetector(
+      excludeFromSemantics: true,
+      onTapDown: _onTapDown,
+      onTapUp: _onTapUp,
+      onTapCancel: _onTapCancel,
+      child: Container(
+        decoration: BoxDecoration(
+          color: CupertinoDynamicColor.resolve(backgroundColor, context),
+          borderRadius: BorderRadius.circular(_kCornerRadius),
+        ),
+        child: widget.child,
+      ),
+    );
+  }
+}
+
+class _CupertinoAlertRenderWidget extends RenderObjectWidget {
+  const _CupertinoAlertRenderWidget({
+    Key? key,
+    required this.contentSection,
+    required this.actionsSection,
+  }) : super(key: key);
+
+  final Widget contentSection;
+  final Widget actionsSection;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    assert(debugCheckHasMediaQuery(context));
+    return _RenderCupertinoAlert(
+      dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio,
+      dividerColor: CupertinoDynamicColor.resolve(_kButtonDividerColor, context),
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderCupertinoAlert renderObject) {
+    super.updateRenderObject(context, renderObject);
+    renderObject.dividerColor = CupertinoDynamicColor.resolve(_kButtonDividerColor, context);
+  }
+
+  @override
+  RenderObjectElement createElement() {
+    return _CupertinoAlertRenderElement(this);
+  }
+}
+
+class _CupertinoAlertRenderElement extends RenderObjectElement {
+  _CupertinoAlertRenderElement(_CupertinoAlertRenderWidget widget) : super(widget);
+
+  Element? _contentElement;
+  Element? _actionsElement;
+
+  @override
+  _CupertinoAlertRenderWidget get widget => super.widget as _CupertinoAlertRenderWidget;
+
+  @override
+  _RenderCupertinoAlert get renderObject => super.renderObject as _RenderCupertinoAlert;
+
+  @override
+  void visitChildren(ElementVisitor visitor) {
+    if (_contentElement != null) {
+      visitor(_contentElement!);
+    }
+    if (_actionsElement != null) {
+      visitor(_actionsElement!);
+    }
+  }
+
+  @override
+  void mount(Element? parent, dynamic newSlot) {
+    super.mount(parent, newSlot);
+    _contentElement = updateChild(_contentElement,
+        widget.contentSection, _AlertSections.contentSection);
+    _actionsElement = updateChild(_actionsElement,
+        widget.actionsSection, _AlertSections.actionsSection);
+  }
+
+  @override
+  void insertRenderObjectChild(RenderObject child, _AlertSections slot) {
+    _placeChildInSlot(child, slot);
+  }
+
+  @override
+  void moveRenderObjectChild(RenderObject child, _AlertSections oldSlot, _AlertSections newSlot) {
+    _placeChildInSlot(child, newSlot);
+  }
+
+  @override
+  void update(RenderObjectWidget newWidget) {
+    super.update(newWidget);
+    _contentElement = updateChild(_contentElement,
+        widget.contentSection, _AlertSections.contentSection);
+    _actionsElement = updateChild(_actionsElement,
+        widget.actionsSection, _AlertSections.actionsSection);
+  }
+
+  @override
+  void forgetChild(Element child) {
+    assert(child == _contentElement || child == _actionsElement);
+    if (_contentElement == child) {
+      _contentElement = null;
+    } else if (_actionsElement == child) {
+      _actionsElement = null;
+    }
+    super.forgetChild(child);
+  }
+
+  @override
+  void removeRenderObjectChild(RenderObject child, _AlertSections slot) {
+    assert(child == renderObject.contentSection || child == renderObject.actionsSection);
+    if (renderObject.contentSection == child) {
+      renderObject.contentSection = null;
+    } else if (renderObject.actionsSection == child) {
+      renderObject.actionsSection = null;
+    }
+  }
+
+  void _placeChildInSlot(RenderObject child, _AlertSections slot) {
+    assert(slot != null);
+    switch (slot) {
+      case _AlertSections.contentSection:
+        renderObject.contentSection = child as RenderBox;
+        break;
+      case _AlertSections.actionsSection:
+        renderObject.actionsSection = child as RenderBox;
+        break;
+    }
+  }
+}
+
+// An iOS-style layout policy for sizing an alert's content section and action
+// button section.
+//
+// The policy is as follows:
+//
+// If all content and buttons fit on the screen:
+// The content section and action button section are sized intrinsically.
+//
+// If all content and buttons do not fit on the screen:
+// A minimum height for the action button section is calculated. The action
+// button section will not be rendered shorter than this minimum.  See
+// _RenderCupertinoAlertActions for the minimum height calculation.
+//
+// With the minimum action button section calculated, the content section can
+// take up as much of the remaining space as it needs.
+//
+// After the content section is laid out, the action button section is allowed
+// to take up any remaining space that was not consumed by the content section.
+class _RenderCupertinoAlert extends RenderBox {
+  _RenderCupertinoAlert({
+    RenderBox? contentSection,
+    RenderBox? actionsSection,
+    double dividerThickness = 0.0,
+    required Color dividerColor,
+  }) : assert(dividerColor != null),
+       _contentSection = contentSection,
+       _actionsSection = actionsSection,
+       _dividerThickness = dividerThickness,
+       _dividerPaint = Paint()
+        ..color = dividerColor
+        ..style = PaintingStyle.fill;
+
+  RenderBox? get contentSection => _contentSection;
+  RenderBox? _contentSection;
+  set contentSection(RenderBox? newContentSection) {
+    if (newContentSection != _contentSection) {
+      if (null != _contentSection) {
+        dropChild(_contentSection!);
+      }
+      _contentSection = newContentSection;
+      if (null != _contentSection) {
+        adoptChild(_contentSection!);
+      }
+    }
+  }
+
+  RenderBox? get actionsSection => _actionsSection;
+  RenderBox? _actionsSection;
+  set actionsSection(RenderBox? newActionsSection) {
+    if (newActionsSection != _actionsSection) {
+      if (null != _actionsSection) {
+        dropChild(_actionsSection!);
+      }
+      _actionsSection = newActionsSection;
+      if (null != _actionsSection) {
+        adoptChild(_actionsSection!);
+      }
+    }
+  }
+
+  Color get dividerColor => _dividerPaint.color;
+  set dividerColor(Color value) {
+    if (value == _dividerPaint.color)
+      return;
+    _dividerPaint.color = value;
+    markNeedsPaint();
+  }
+
+  final double _dividerThickness;
+
+  final Paint _dividerPaint;
+
+  @override
+  void attach(PipelineOwner owner) {
+    super.attach(owner);
+    if (null != contentSection) {
+      contentSection!.attach(owner);
+    }
+    if (null != actionsSection) {
+      actionsSection!.attach(owner);
+    }
+  }
+
+  @override
+  void detach() {
+    super.detach();
+    if (null != contentSection) {
+      contentSection!.detach();
+    }
+    if (null != actionsSection) {
+      actionsSection!.detach();
+    }
+  }
+
+  @override
+  void redepthChildren() {
+    if (null != contentSection) {
+      redepthChild(contentSection!);
+    }
+    if (null != actionsSection) {
+      redepthChild(actionsSection!);
+    }
+  }
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! MultiChildLayoutParentData) {
+      child.parentData = MultiChildLayoutParentData();
+    }
+  }
+
+  @override
+  void visitChildren(RenderObjectVisitor visitor) {
+    if (contentSection != null) {
+      visitor(contentSection!);
+    }
+    if (actionsSection != null) {
+      visitor(actionsSection!);
+    }
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() {
+    final List<DiagnosticsNode> value = <DiagnosticsNode>[];
+    if (contentSection != null) {
+      value.add(contentSection!.toDiagnosticsNode(name: 'content'));
+    }
+    if (actionsSection != null) {
+      value.add(actionsSection!.toDiagnosticsNode(name: 'actions'));
+    }
+    return value;
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return constraints.minWidth;
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return constraints.maxWidth;
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
+    final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
+    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
+    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
+
+    if (actionsHeight > 0 || contentHeight > 0)
+      height -= 2 * _kEdgeVerticalPadding;
+    if (height.isFinite)
+      return height;
+    return 0.0;
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
+    final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
+    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
+    double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
+
+    if (actionsHeight > 0 || contentHeight > 0)
+      height -= 2 * _kEdgeVerticalPadding;
+    if (height.isFinite)
+      return height;
+    return 0.0;
+  }
+
+  double _computeDividerThickness(BoxConstraints constraints) {
+    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(constraints.maxWidth) > 0.0
+        && actionsSection!.getMaxIntrinsicHeight(constraints.maxWidth) > 0.0;
+    return hasDivider ? _dividerThickness : 0.0;
+  }
+
+  _AlertSizes _computeSizes({required BoxConstraints constraints, required ChildLayouter layoutChild, required double dividerThickness}) {
+    final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(constraints.maxWidth);
+
+    final Size contentSize = layoutChild(
+      contentSection!,
+      constraints.deflate(EdgeInsets.only(bottom: minActionsHeight + dividerThickness)),
+    );
+
+    final Size actionsSize = layoutChild(
+      actionsSection!,
+      constraints.deflate(EdgeInsets.only(top: contentSize.height + dividerThickness)),
+    );
+
+    final double actionSheetHeight = contentSize.height + dividerThickness + actionsSize.height;
+    return _AlertSizes(
+      size: Size(constraints.maxWidth, actionSheetHeight),
+      contentHeight: contentSize.height,
+    );
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return _computeSizes(
+      constraints: constraints,
+      layoutChild: ChildLayoutHelper.dryLayoutChild,
+      dividerThickness: _computeDividerThickness(constraints),
+    ).size;
+  }
+
+  @override
+  void performLayout() {
+    final BoxConstraints constraints = this.constraints;
+    final double dividerThickness = _computeDividerThickness(constraints);
+    final _AlertSizes alertSizes = _computeSizes(
+      constraints: constraints,
+      layoutChild: ChildLayoutHelper.layoutChild,
+      dividerThickness: dividerThickness,
+    );
+
+    size = alertSizes.size;
+
+    // Set the position of the actions box to sit at the bottom of the alert.
+    // The content box defaults to the top left, which is where we want it.
+    assert(actionsSection!.parentData is MultiChildLayoutParentData);
+    final MultiChildLayoutParentData actionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
+    actionParentData.offset = Offset(0.0, alertSizes.contentHeight + dividerThickness);
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    final MultiChildLayoutParentData contentParentData = contentSection!.parentData! as MultiChildLayoutParentData;
+    contentSection!.paint(context, offset + contentParentData.offset);
+
+    final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
+    if (hasDivider) {
+      _paintDividerBetweenContentAndActions(context.canvas, offset);
+    }
+
+    final MultiChildLayoutParentData actionsParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
+    actionsSection!.paint(context, offset + actionsParentData.offset);
+  }
+
+  void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
+    canvas.drawRect(
+      Rect.fromLTWH(
+        offset.dx,
+        offset.dy + contentSection!.size.height,
+        size.width,
+        _dividerThickness,
+      ),
+      _dividerPaint,
+    );
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+    final MultiChildLayoutParentData contentSectionParentData = contentSection!.parentData! as MultiChildLayoutParentData;
+    final MultiChildLayoutParentData actionsSectionParentData = actionsSection!.parentData! as MultiChildLayoutParentData;
+    return result.addWithPaintOffset(
+             offset: contentSectionParentData.offset,
+             position: position,
+             hitTest: (BoxHitTestResult result, Offset transformed) {
+               assert(transformed == position - contentSectionParentData.offset);
+               return contentSection!.hitTest(result, position: transformed);
+             },
+           )
+        || result.addWithPaintOffset(
+             offset: actionsSectionParentData.offset,
+             position: position,
+             hitTest: (BoxHitTestResult result, Offset transformed) {
+               assert(transformed == position - actionsSectionParentData.offset);
+               return actionsSection!.hitTest(result, position: transformed);
+             },
+           );
+  }
+}
+
+class _AlertSizes {
+  const _AlertSizes({required this.size, required this.contentHeight});
+
+  final Size size;
+  final double contentHeight;
+}
+
+// Visual components of an alert that need to be explicitly sized and
+// laid out at runtime.
+enum _AlertSections {
+  contentSection,
+  actionsSection,
+}
+
+// The "content section" of a CupertinoActionSheet.
+//
+// If title is missing, then only content is added.  If content is
+// missing, then only a title is added. If both are missing, then it returns
+// a SingleChildScrollView with a zero-sized Container.
+class _CupertinoAlertContentSection extends StatelessWidget {
+  const _CupertinoAlertContentSection({
+    Key? key,
+    this.title,
+    this.message,
+    this.scrollController,
+  }) : super(key: key);
+
+  // An optional title of the action sheet. When the message is non-null,
+  // the font of the title is bold.
+  //
+  // Typically a Text widget.
+  final Widget? title;
+
+  // An optional descriptive message that provides more details about the
+  // reason for the alert.
+  //
+  // Typically a Text widget.
+  final Widget? message;
+
+  // A scroll controller that can be used to control the scrolling of the
+  // content in the action sheet.
+  //
+  // Defaults to null, and is typically not needed, since most alert contents
+  // are short.
+  final ScrollController? scrollController;
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> titleContentGroup = <Widget>[];
+
+    if (title != null) {
+      titleContentGroup.add(Padding(
+        padding: const EdgeInsets.only(
+          left: _kContentHorizontalPadding,
+          right: _kContentHorizontalPadding,
+          bottom: _kContentVerticalPadding,
+          top: _kContentVerticalPadding,
+        ),
+        child: DefaultTextStyle(
+          style: message == null ? _kActionSheetContentStyle
+              : _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600),
+          textAlign: TextAlign.center,
+          child: title!,
+        ),
+      ));
+    }
+
+    if (message != null) {
+      titleContentGroup.add(
+        Padding(
+          padding: EdgeInsets.only(
+            left: _kContentHorizontalPadding,
+            right: _kContentHorizontalPadding,
+            bottom: title == null ? _kContentVerticalPadding : 22.0,
+            top: title == null ? _kContentVerticalPadding : 0.0,
+          ),
+          child: DefaultTextStyle(
+            style: title == null ? _kActionSheetContentStyle.copyWith(fontWeight: FontWeight.w600)
+                : _kActionSheetContentStyle,
+            textAlign: TextAlign.center,
+            child: message!,
+          ),
+        ),
+      );
+    }
+
+    if (titleContentGroup.isEmpty) {
+      return SingleChildScrollView(
+        controller: scrollController,
+        child: const SizedBox(
+          width: 0.0,
+          height: 0.0,
+        ),
+      );
+    }
+
+    // Add padding between the widgets if necessary.
+    if (titleContentGroup.length > 1) {
+      titleContentGroup.insert(1, const Padding(padding: EdgeInsets.only(top: 8.0)));
+    }
+
+    return CupertinoScrollbar(
+      child: SingleChildScrollView(
+        controller: scrollController,
+        child: Column(
+          mainAxisSize: MainAxisSize.max,
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: titleContentGroup,
+        ),
+      ),
+    );
+  }
+}
+
+// The "actions section" of a CupertinoActionSheet.
+//
+// See _RenderCupertinoAlertActions for details about action button sizing
+// and layout.
+class _CupertinoAlertActionSection extends StatefulWidget {
+  const _CupertinoAlertActionSection({
+    Key? key,
+    required this.children,
+    this.scrollController,
+    this.hasCancelButton,
+  }) : assert(children != null),
+       super(key: key);
+
+  final List<Widget> children;
+
+  // A scroll controller that can be used to control the scrolling of the
+  // actions in the action sheet.
+  //
+  // Defaults to null, and is typically not needed, since most alerts
+  // don't have many actions.
+  final ScrollController? scrollController;
+
+  final bool? hasCancelButton;
+
+  @override
+  _CupertinoAlertActionSectionState createState() => _CupertinoAlertActionSectionState();
+}
+
+class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSection> {
+  @override
+  Widget build(BuildContext context) {
+    final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
+
+    final List<Widget> interactiveButtons = <Widget>[];
+    for (int i = 0; i < widget.children.length; i += 1) {
+      interactiveButtons.add(
+        _PressableActionButton(
+          child: widget.children[i],
+        ),
+      );
+    }
+
+    return CupertinoScrollbar(
+      child: SingleChildScrollView(
+        controller: widget.scrollController,
+        child: _CupertinoAlertActionsRenderWidget(
+          actionButtons: interactiveButtons,
+          dividerThickness: _kDividerThickness / devicePixelRatio,
+          hasCancelButton: widget.hasCancelButton ?? false,
+        ),
+      ),
+    );
+  }
+}
+
+// A button that updates its render state when pressed.
+//
+// The pressed state is forwarded to an _ActionButtonParentDataWidget. The
+// corresponding _ActionButtonParentData is then interpreted and rendered
+// appropriately by _RenderCupertinoAlertActions.
+class _PressableActionButton extends StatefulWidget {
+  const _PressableActionButton({
+    required this.child,
+  });
+
+  final Widget child;
+
+  @override
+  _PressableActionButtonState createState() => _PressableActionButtonState();
+}
+
+class _PressableActionButtonState extends State<_PressableActionButton> {
+  bool _isPressed = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionButtonParentDataWidget(
+      isPressed: _isPressed,
+      // TODO(mattcarroll): Button press dynamics need overhaul for iOS:
+      //  https://github.com/flutter/flutter/issues/19786
+      child: GestureDetector(
+        excludeFromSemantics: true,
+        behavior: HitTestBehavior.opaque,
+        onTapDown: (TapDownDetails details) => setState(() => _isPressed = true),
+        onTapUp: (TapUpDetails details) => setState(() => _isPressed = false),
+        // TODO(mattcarroll): Cancel is currently triggered when user moves past
+        //  slop instead of off button: https://github.com/flutter/flutter/issues/19783
+        onTapCancel: () => setState(() => _isPressed = false),
+        child: widget.child,
+      ),
+    );
+  }
+}
+
+// ParentDataWidget that updates _ActionButtonParentData for an action button.
+//
+// Each action button requires knowledge of whether or not it is pressed so that
+// the alert can correctly render the button. The pressed state is held within
+// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
+// updating the pressed state of an _ActionButtonParentData based on the
+// incoming isPressed property.
+class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> {
+  const _ActionButtonParentDataWidget({
+    Key? key,
+    required this.isPressed,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  final bool isPressed;
+
+  @override
+  void applyParentData(RenderObject renderObject) {
+    assert(renderObject.parentData is _ActionButtonParentData);
+    final _ActionButtonParentData parentData = renderObject.parentData! as _ActionButtonParentData;
+    if (parentData.isPressed != isPressed) {
+      parentData.isPressed = isPressed;
+
+      // Force a repaint.
+      final AbstractNode? targetParent = renderObject.parent;
+      if (targetParent is RenderObject)
+        targetParent.markNeedsPaint();
+    }
+  }
+
+  @override
+  Type get debugTypicalAncestorWidgetClass => _CupertinoAlertActionsRenderWidget;
+}
+
+// ParentData applied to individual action buttons that report whether or not
+// that button is currently pressed by the user.
+class _ActionButtonParentData extends MultiChildLayoutParentData {
+  _ActionButtonParentData({
+    this.isPressed = false,
+  });
+
+  bool isPressed;
+}
+
+// An iOS-style alert action button layout.
+//
+// See _RenderCupertinoAlertActions for specific layout policy details.
+class _CupertinoAlertActionsRenderWidget extends MultiChildRenderObjectWidget {
+  _CupertinoAlertActionsRenderWidget({
+    Key? key,
+    required List<Widget> actionButtons,
+    double dividerThickness = 0.0,
+    bool hasCancelButton = false,
+  }) : _dividerThickness = dividerThickness,
+       _hasCancelButton = hasCancelButton,
+       super(key: key, children: actionButtons);
+
+  final double _dividerThickness;
+  final bool _hasCancelButton;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderCupertinoAlertActions(
+      dividerThickness: _dividerThickness,
+      dividerColor: CupertinoDynamicColor.resolve(_kButtonDividerColor, context),
+      hasCancelButton: _hasCancelButton,
+      backgroundColor: CupertinoDynamicColor.resolve(_kBackgroundColor, context),
+      pressedColor: CupertinoDynamicColor.resolve(_kPressedColor, context),
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderCupertinoAlertActions renderObject) {
+    renderObject
+      ..dividerThickness = _dividerThickness
+      ..dividerColor = CupertinoDynamicColor.resolve(_kButtonDividerColor, context)
+      ..hasCancelButton = _hasCancelButton
+      ..backgroundColor = CupertinoDynamicColor.resolve(_kBackgroundColor, context)
+      ..pressedColor = CupertinoDynamicColor.resolve(_kPressedColor, context);
+  }
+}
+
+// An iOS-style layout policy for sizing and positioning an action sheet's
+// buttons.
+//
+// The policy is as follows:
+//
+// Action sheet buttons are always stacked vertically. In the case where the
+// content section and the action section combined can not fit on the screen
+// without scrolling, the height of the action section is determined as
+// follows.
+//
+// If the user has included a separate cancel button, the height of the action
+// section can be up to the height of 3 action buttons (i.e., the user can
+// include 1, 2, or 3 action buttons and they will appear without needing to
+// be scrolled). If 4+ action buttons are provided, the height of the action
+// section shrinks to 1.5 buttons tall, and is scrollable.
+//
+// If the user has not included a separate cancel button, the height of the
+// action section is at most 1.5 buttons tall.
+class _RenderCupertinoAlertActions extends RenderBox
+    with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
+        RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
+  _RenderCupertinoAlertActions({
+    List<RenderBox>? children,
+    double dividerThickness = 0.0,
+    required Color dividerColor,
+    bool hasCancelButton = false,
+    required Color backgroundColor,
+    required Color pressedColor,
+  }) : _dividerThickness = dividerThickness,
+       _hasCancelButton = hasCancelButton,
+       _buttonBackgroundPaint = Paint()
+          ..style = PaintingStyle.fill
+          ..color = backgroundColor,
+       _pressedButtonBackgroundPaint = Paint()
+          ..style = PaintingStyle.fill
+          ..color = pressedColor,
+       _dividerPaint = Paint()
+          ..color = dividerColor
+          ..style = PaintingStyle.fill {
+    addAll(children);
+  }
+
+  // The thickness of the divider between buttons.
+  double get dividerThickness => _dividerThickness;
+  double _dividerThickness;
+  set dividerThickness(double newValue) {
+    if (newValue == _dividerThickness) {
+      return;
+    }
+
+    _dividerThickness = newValue;
+    markNeedsLayout();
+  }
+
+  Color get backgroundColor => _buttonBackgroundPaint.color;
+  set backgroundColor(Color newValue) {
+    if (newValue == _buttonBackgroundPaint.color) {
+      return;
+    }
+
+    _buttonBackgroundPaint.color = newValue;
+    markNeedsPaint();
+  }
+
+  Color get pressedColor => _pressedButtonBackgroundPaint.color;
+  set pressedColor(Color newValue) {
+    if (newValue == _pressedButtonBackgroundPaint.color) {
+      return;
+    }
+
+    _pressedButtonBackgroundPaint.color = newValue;
+    markNeedsPaint();
+  }
+
+  Color get dividerColor => _dividerPaint.color;
+  set dividerColor(Color value) {
+    if (value == _dividerPaint.color) {
+      return;
+    }
+    _dividerPaint.color = value;
+    markNeedsPaint();
+  }
+
+  bool _hasCancelButton;
+  bool get hasCancelButton => _hasCancelButton;
+  set hasCancelButton(bool newValue) {
+    if (newValue == _hasCancelButton) {
+      return;
+    }
+
+    _hasCancelButton = newValue;
+    markNeedsLayout();
+  }
+
+  final Paint _buttonBackgroundPaint;
+  final Paint _pressedButtonBackgroundPaint;
+
+  final Paint _dividerPaint;
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! _ActionButtonParentData)
+      child.parentData = _ActionButtonParentData();
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return constraints.minWidth;
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return constraints.maxWidth;
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    if (childCount == 0)
+      return 0.0;
+    if (childCount == 1)
+      return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
+    if (hasCancelButton && childCount < 4)
+      return _computeMinIntrinsicHeightWithCancel(width);
+    return _computeMinIntrinsicHeightWithoutCancel(width);
+  }
+
+  // The minimum height for more than 2-3 buttons when a cancel button is
+  // included is the full height of button stack.
+  double _computeMinIntrinsicHeightWithCancel(double width) {
+    assert(childCount == 2 || childCount == 3);
+    if (childCount == 2) {
+      return firstChild!.getMinIntrinsicHeight(width)
+        + childAfter(firstChild!)!.getMinIntrinsicHeight(width)
+        + dividerThickness;
+    }
+    return firstChild!.getMinIntrinsicHeight(width)
+      + childAfter(firstChild!)!.getMinIntrinsicHeight(width)
+      + childAfter(childAfter(firstChild!)!)!.getMinIntrinsicHeight(width)
+      + (dividerThickness * 2);
+  }
+
+  // The minimum height for more than 2 buttons when no cancel button or 4+
+  // buttons when a cancel button is included is the height of the 1st button
+  // + 50% the height of the 2nd button + 2 dividers.
+  double _computeMinIntrinsicHeightWithoutCancel(double width) {
+    assert(childCount >= 2);
+    return firstChild!.getMinIntrinsicHeight(width)
+      + dividerThickness
+      + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width));
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    if (childCount == 0)
+      return 0.0;
+    if (childCount == 1)
+      return firstChild!.computeMaxIntrinsicHeight(width) + dividerThickness;
+    return _computeMaxIntrinsicHeightStacked(width);
+  }
+
+  // Max height of a stack of buttons is the sum of all button heights + a
+  // divider for each button.
+  double _computeMaxIntrinsicHeightStacked(double width) {
+    assert(childCount >= 2);
+
+    final double allDividersHeight = (childCount - 1) * dividerThickness;
+    double heightAccumulation = allDividersHeight;
+    RenderBox? button = firstChild;
+    while (button != null) {
+      heightAccumulation += button.getMaxIntrinsicHeight(width);
+      button = childAfter(button);
+    }
+    return heightAccumulation;
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return _performLayout(constraints, dry: true);
+  }
+
+  @override
+  void performLayout() {
+    size = _performLayout(constraints, dry: false);
+  }
+
+  Size _performLayout(BoxConstraints constraints, {bool dry = false}) {
+    final BoxConstraints perButtonConstraints = constraints.copyWith(
+      minHeight: 0.0,
+      maxHeight: double.infinity,
+    );
+
+    RenderBox? child = firstChild;
+    int index = 0;
+    double verticalOffset = 0.0;
+    while (child != null) {
+      final Size childSize;
+      if (!dry) {
+        child.layout(
+          perButtonConstraints,
+          parentUsesSize: true,
+        );
+        childSize = child.size;
+        assert(child.parentData is MultiChildLayoutParentData);
+        final MultiChildLayoutParentData parentData = child.parentData! as MultiChildLayoutParentData;
+        parentData.offset = Offset(0.0, verticalOffset);
+      } else {
+        childSize = child.getDryLayout(constraints);
+      }
+
+      verticalOffset += childSize.height;
+      if (index < childCount - 1) {
+        // Add a gap for the next divider.
+        verticalOffset += dividerThickness;
+      }
+
+      index += 1;
+      child = childAfter(child);
+    }
+
+    return constraints.constrain(
+      Size(constraints.maxWidth, verticalOffset)
+    );
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    final Canvas canvas = context.canvas;
+    _drawButtonBackgroundsAndDividersStacked(canvas, offset);
+    _drawButtons(context, offset);
+  }
+
+  void _drawButtonBackgroundsAndDividersStacked(Canvas canvas, Offset offset) {
+    final Offset dividerOffset = Offset(0.0, dividerThickness);
+
+    final Path backgroundFillPath = Path()
+      ..fillType = PathFillType.evenOdd
+      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
+
+    final Path pressedBackgroundFillPath = Path();
+
+    final Path dividersPath = Path();
+
+    Offset accumulatingOffset = offset;
+
+    RenderBox? child = firstChild;
+    RenderBox? prevChild;
+    while (child != null) {
+      assert(child.parentData is _ActionButtonParentData);
+      final _ActionButtonParentData currentButtonParentData = child.parentData! as _ActionButtonParentData;
+      final bool isButtonPressed = currentButtonParentData.isPressed;
+
+      bool isPrevButtonPressed = false;
+      if (prevChild != null) {
+        assert(prevChild.parentData is _ActionButtonParentData);
+        final _ActionButtonParentData previousButtonParentData = prevChild.parentData! as _ActionButtonParentData;
+        isPrevButtonPressed = previousButtonParentData.isPressed;
+      }
+
+      final bool isDividerPresent = child != firstChild;
+      final bool isDividerPainted = isDividerPresent && !(isButtonPressed || isPrevButtonPressed);
+      final Rect dividerRect = Rect.fromLTWH(
+        accumulatingOffset.dx,
+        accumulatingOffset.dy,
+        size.width,
+        _dividerThickness,
+      );
+
+      final Rect buttonBackgroundRect = Rect.fromLTWH(
+        accumulatingOffset.dx,
+        accumulatingOffset.dy + (isDividerPresent ? dividerThickness : 0.0),
+        size.width,
+        child.size.height,
+      );
+
+      // If this button is pressed, then we don't want a white background to be
+      // painted, so we erase this button from the background path.
+      if (isButtonPressed) {
+        backgroundFillPath.addRect(buttonBackgroundRect);
+        pressedBackgroundFillPath.addRect(buttonBackgroundRect);
+      }
+
+      // If this divider is needed, then we erase the divider area from the
+      // background path, and on top of that we paint a translucent gray to
+      // darken the divider area.
+      if (isDividerPainted) {
+        backgroundFillPath.addRect(dividerRect);
+        dividersPath.addRect(dividerRect);
+      }
+
+      accumulatingOffset += (isDividerPresent ? dividerOffset : Offset.zero)
+          + Offset(0.0, child.size.height);
+
+      prevChild = child;
+      child = childAfter(child);
+    }
+
+    canvas.drawPath(backgroundFillPath, _buttonBackgroundPaint);
+    canvas.drawPath(pressedBackgroundFillPath, _pressedButtonBackgroundPaint);
+    canvas.drawPath(dividersPath, _dividerPaint);
+  }
+
+  void _drawButtons(PaintingContext context, Offset offset) {
+    RenderBox? child = firstChild;
+    while (child != null) {
+      final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData;
+      context.paintChild(child, childParentData.offset + offset);
+      child = childAfter(child);
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+    return defaultHitTestChildren(result, position: position);
+  }
+}
diff --git a/lib/src/cupertino/activity_indicator.dart b/lib/src/cupertino/activity_indicator.dart
new file mode 100644
index 0000000..cafd28a
--- /dev/null
+++ b/lib/src/cupertino/activity_indicator.dart
@@ -0,0 +1,199 @@
+// Copyright 2014 The Flutter Authors. 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:flute/widgets.dart';
+
+import 'colors.dart';
+
+const double _kDefaultIndicatorRadius = 10.0;
+
+// Extracted from iOS 13.2 Beta.
+const Color _kActiveTickColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xFF3C3C44),
+  darkColor: Color(0xFFEBEBF5),
+);
+
+/// An iOS-style activity indicator that spins clockwise.
+///
+/// {@youtube 560 315 https://www.youtube.com/watch?v=AENVH-ZqKDQ}
+///
+/// See also:
+///
+///  * <https://developer.apple.com/ios/human-interface-guidelines/controls/progress-indicators/#activity-indicators>
+class CupertinoActivityIndicator extends StatefulWidget {
+  /// Creates an iOS-style activity indicator that spins clockwise.
+  const CupertinoActivityIndicator({
+    Key? key,
+    this.animating = true,
+    this.radius = _kDefaultIndicatorRadius,
+  })  : assert(animating != null),
+        assert(radius != null),
+        assert(radius > 0.0),
+        progress = 1.0,
+        super(key: key);
+
+  /// Creates a non-animated iOS-style activity indicator that displays
+  /// a partial count of ticks based on the value of [progress].
+  ///
+  /// When provided, the value of [progress] must be between 0.0 (zero ticks
+  /// will be shown) and 1.0 (all ticks will be shown) inclusive. Defaults
+  /// to 1.0.
+  const CupertinoActivityIndicator.partiallyRevealed({
+    Key? key,
+    this.radius = _kDefaultIndicatorRadius,
+    this.progress = 1.0,
+  })  : assert(radius != null),
+        assert(radius > 0.0),
+        assert(progress != null),
+        assert(progress >= 0.0),
+        assert(progress <= 1.0),
+        animating = false,
+        super(key: key);
+
+  /// Whether the activity indicator is running its animation.
+  ///
+  /// Defaults to true.
+  final bool animating;
+
+  /// Radius of the spinner widget.
+  ///
+  /// Defaults to 10px. Must be positive and cannot be null.
+  final double radius;
+
+  /// Determines the percentage of spinner ticks that will be shown. Typical usage would
+  /// display all ticks, however, this allows for more fine-grained control such as
+  /// during pull-to-refresh when the drag-down action shows one tick at a time as
+  /// the user continues to drag down.
+  ///
+  /// Defaults to 1.0. Must be between 0.0 and 1.0 inclusive, and cannot be null.
+  final double progress;
+
+  @override
+  _CupertinoActivityIndicatorState createState() =>
+      _CupertinoActivityIndicatorState();
+}
+
+class _CupertinoActivityIndicatorState extends State<CupertinoActivityIndicator>
+    with SingleTickerProviderStateMixin {
+  late AnimationController _controller;
+
+  @override
+  void initState() {
+    super.initState();
+    _controller = AnimationController(
+      duration: const Duration(seconds: 1),
+      vsync: this,
+    );
+
+    if (widget.animating) {
+      _controller.repeat();
+    }
+  }
+
+  @override
+  void didUpdateWidget(CupertinoActivityIndicator oldWidget) {
+    super.didUpdateWidget(oldWidget);
+    if (widget.animating != oldWidget.animating) {
+      if (widget.animating)
+        _controller.repeat();
+      else
+        _controller.stop();
+    }
+  }
+
+  @override
+  void dispose() {
+    _controller.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      height: widget.radius * 2,
+      width: widget.radius * 2,
+      child: CustomPaint(
+        painter: _CupertinoActivityIndicatorPainter(
+          position: _controller,
+          activeColor:
+              CupertinoDynamicColor.resolve(_kActiveTickColor, context),
+          radius: widget.radius,
+          progress: widget.progress,
+        ),
+      ),
+    );
+  }
+}
+
+const double _kTwoPI = math.pi * 2.0;
+
+/// Alpha values extracted from the native component (for both dark and light mode) to
+/// draw the spinning ticks.
+const List<int> _kAlphaValues = <int>[
+  47,
+  47,
+  47,
+  47,
+  72,
+  97,
+  122,
+  147,
+];
+
+/// The alpha value that is used to draw the partially revealed ticks.
+const int _partiallyRevealedAlpha = 147;
+
+class _CupertinoActivityIndicatorPainter extends CustomPainter {
+  _CupertinoActivityIndicatorPainter({
+    required this.position,
+    required this.activeColor,
+    required this.radius,
+    required this.progress,
+  })  : tickFundamentalRRect = RRect.fromLTRBXY(
+          -radius / _kDefaultIndicatorRadius,
+          -radius / 3.0,
+          radius / _kDefaultIndicatorRadius,
+          -radius,
+          radius / _kDefaultIndicatorRadius,
+          radius / _kDefaultIndicatorRadius,
+        ),
+        super(repaint: position);
+
+  final Animation<double> position;
+  final Color activeColor;
+  final double radius;
+  final double progress;
+
+  final RRect tickFundamentalRRect;
+
+  @override
+  void paint(Canvas canvas, Size size) {
+    final Paint paint = Paint();
+    final int tickCount = _kAlphaValues.length;
+
+    canvas.save();
+    canvas.translate(size.width / 2.0, size.height / 2.0);
+
+    final int activeTick = (tickCount * position.value).floor();
+
+    for (int i = 0; i < tickCount * progress; ++i) {
+      final int t = (i - activeTick) % tickCount;
+      paint.color = activeColor
+          .withAlpha(progress < 1 ? _partiallyRevealedAlpha : _kAlphaValues[t]);
+      canvas.drawRRect(tickFundamentalRRect, paint);
+      canvas.rotate(_kTwoPI / tickCount);
+    }
+
+    canvas.restore();
+  }
+
+  @override
+  bool shouldRepaint(_CupertinoActivityIndicatorPainter oldPainter) {
+    return oldPainter.position != position ||
+        oldPainter.activeColor != activeColor ||
+        oldPainter.progress != progress;
+  }
+}
diff --git a/lib/src/cupertino/app.dart b/lib/src/cupertino/app.dart
new file mode 100644
index 0000000..e86a321
--- /dev/null
+++ b/lib/src/cupertino/app.dart
@@ -0,0 +1,469 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/rendering.dart';
+import 'package:flute/widgets.dart';
+
+import 'button.dart';
+import 'colors.dart';
+import 'icons.dart';
+import 'interface_level.dart';
+import 'localizations.dart';
+import 'route.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// // @dart = 2.9
+
+/// An application that uses Cupertino design.
+///
+/// A convenience widget that wraps a number of widgets that are commonly
+/// required for an iOS-design targeting application. It builds upon a
+/// [WidgetsApp] by iOS specific defaulting such as fonts and scrolling
+/// physics.
+///
+/// The [CupertinoApp] configures the top-level [Navigator] to search for routes
+/// in the following order:
+///
+///  1. For the `/` route, the [home] property, if non-null, is used.
+///
+///  2. Otherwise, the [routes] table is used, if it has an entry for the route.
+///
+///  3. Otherwise, [onGenerateRoute] is called, if provided. It should return a
+///     non-null value for any _valid_ route not handled by [home] and [routes].
+///
+///  4. Finally if all else fails [onUnknownRoute] is called.
+///
+/// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
+/// and [builder] is not null, then no [Navigator] is created.
+///
+/// This widget also configures the observer of the top-level [Navigator] (if
+/// any) to perform [Hero] animations.
+///
+/// Use this widget with caution on Android since it may produce behaviors
+/// Android users are not expecting such as:
+///
+///  * Pages will be dismissible via a back swipe.
+///  * Scrolling past extremities will trigger iOS-style spring overscrolls.
+///  * The San Francisco font family is unavailable on Android and can result
+///    in undefined font behavior.
+///
+/// See also:
+///
+///  * [CupertinoPageScaffold], which provides a standard page layout default
+///    with nav bars.
+///  * [Navigator], which is used to manage the app's stack of pages.
+///  * [CupertinoPageRoute], which defines an app page that transitions in an
+///    iOS-specific way.
+///  * [WidgetsApp], which defines the basic app elements but does not depend
+///    on the Cupertino library.
+class CupertinoApp extends StatefulWidget {
+  /// Creates a CupertinoApp.
+  ///
+  /// At least one of [home], [routes], [onGenerateRoute], or [builder] must be
+  /// non-null. If only [routes] is given, it must include an entry for the
+  /// [Navigator.defaultRouteName] (`/`), since that is the route used when the
+  /// application is launched with an intent that specifies an otherwise
+  /// unsupported route.
+  ///
+  /// This class creates an instance of [WidgetsApp].
+  ///
+  /// The boolean arguments, [routes], and [navigatorObservers], must not be null.
+  const CupertinoApp({
+    Key? key,
+    this.navigatorKey,
+    this.home,
+    this.theme,
+    Map<String, Widget Function(BuildContext)> this.routes = const <String, WidgetBuilder>{},
+    this.initialRoute,
+    this.onGenerateRoute,
+    this.onGenerateInitialRoutes,
+    this.onUnknownRoute,
+    List<NavigatorObserver> this.navigatorObservers = const <NavigatorObserver>[],
+    this.builder,
+    this.title = '',
+    this.onGenerateTitle,
+    this.color,
+    this.locale,
+    this.localizationsDelegates,
+    this.localeListResolutionCallback,
+    this.localeResolutionCallback,
+    this.supportedLocales = const <Locale>[Locale('en', 'US')],
+    this.showPerformanceOverlay = false,
+    this.checkerboardRasterCacheImages = false,
+    this.checkerboardOffscreenLayers = false,
+    this.showSemanticsDebugger = false,
+    this.debugShowCheckedModeBanner = true,
+    this.shortcuts,
+    this.actions,
+    this.restorationScopeId,
+  }) : assert(routes != null),
+       assert(navigatorObservers != null),
+       assert(title != null),
+       assert(showPerformanceOverlay != null),
+       assert(checkerboardRasterCacheImages != null),
+       assert(checkerboardOffscreenLayers != null),
+       assert(showSemanticsDebugger != null),
+       assert(debugShowCheckedModeBanner != null),
+       routeInformationProvider = null,
+       routeInformationParser = null,
+       routerDelegate = null,
+       backButtonDispatcher = null,
+       super(key: key);
+
+  /// Creates a [CupertinoApp] that uses the [Router] instead of a [Navigator].
+  const CupertinoApp.router({
+    Key? key,
+    this.routeInformationProvider,
+    required RouteInformationParser<Object> this.routeInformationParser,
+    required RouterDelegate<Object> this.routerDelegate,
+    this.backButtonDispatcher,
+    this.theme,
+    this.builder,
+    this.title = '',
+    this.onGenerateTitle,
+    this.color,
+    this.locale,
+    this.localizationsDelegates,
+    this.localeListResolutionCallback,
+    this.localeResolutionCallback,
+    this.supportedLocales = const <Locale>[Locale('en', 'US')],
+    this.showPerformanceOverlay = false,
+    this.checkerboardRasterCacheImages = false,
+    this.checkerboardOffscreenLayers = false,
+    this.showSemanticsDebugger = false,
+    this.debugShowCheckedModeBanner = true,
+    this.shortcuts,
+    this.actions,
+    this.restorationScopeId,
+  }) : assert(title != null),
+       assert(showPerformanceOverlay != null),
+       assert(checkerboardRasterCacheImages != null),
+       assert(checkerboardOffscreenLayers != null),
+       assert(showSemanticsDebugger != null),
+       assert(debugShowCheckedModeBanner != null),
+       navigatorObservers = null,
+       navigatorKey = null,
+       onGenerateRoute = null,
+       home = null,
+       onGenerateInitialRoutes = null,
+       onUnknownRoute = null,
+       routes = null,
+       initialRoute = null,
+       super(key: key);
+
+  /// {@macro flutter.widgets.widgetsApp.navigatorKey}
+  final GlobalKey<NavigatorState>? navigatorKey;
+
+  /// {@macro flutter.widgets.widgetsApp.home}
+  final Widget? home;
+
+  /// The top-level [CupertinoTheme] styling.
+  ///
+  /// A null [theme] or unspecified [theme] attributes will default to iOS
+  /// system values.
+  final CupertinoThemeData? theme;
+
+  /// The application's top-level routing table.
+  ///
+  /// When a named route is pushed with [Navigator.pushNamed], the route name is
+  /// looked up in this map. If the name is present, the associated
+  /// [WidgetBuilder] is used to construct a [CupertinoPageRoute] that performs
+  /// an appropriate transition, including [Hero] animations, to the new route.
+  ///
+  /// {@macro flutter.widgets.widgetsApp.routes}
+  final Map<String, WidgetBuilder>? routes;
+
+  /// {@macro flutter.widgets.widgetsApp.initialRoute}
+  final String? initialRoute;
+
+  /// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
+  final RouteFactory? onGenerateRoute;
+
+  /// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes}
+  final InitialRouteListFactory? onGenerateInitialRoutes;
+
+  /// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
+  final RouteFactory? onUnknownRoute;
+
+  /// {@macro flutter.widgets.widgetsApp.navigatorObservers}
+  final List<NavigatorObserver>? navigatorObservers;
+
+  /// {@macro flutter.widgets.widgetsApp.routeInformationProvider}
+  final RouteInformationProvider? routeInformationProvider;
+
+  /// {@macro flutter.widgets.widgetsApp.routeInformationParser}
+  final RouteInformationParser<Object>? routeInformationParser;
+
+  /// {@macro flutter.widgets.widgetsApp.routerDelegate}
+  final RouterDelegate<Object>? routerDelegate;
+
+  /// {@macro flutter.widgets.widgetsApp.backButtonDispatcher}
+  final BackButtonDispatcher? backButtonDispatcher;
+
+  /// {@macro flutter.widgets.widgetsApp.builder}
+  final TransitionBuilder? builder;
+
+  /// {@macro flutter.widgets.widgetsApp.title}
+  ///
+  /// This value is passed unmodified to [WidgetsApp.title].
+  final String title;
+
+  /// {@macro flutter.widgets.widgetsApp.onGenerateTitle}
+  ///
+  /// This value is passed unmodified to [WidgetsApp.onGenerateTitle].
+  final GenerateAppTitle? onGenerateTitle;
+
+  /// {@macro flutter.widgets.widgetsApp.color}
+  final Color? color;
+
+  /// {@macro flutter.widgets.widgetsApp.locale}
+  final Locale? locale;
+
+  /// {@macro flutter.widgets.widgetsApp.localizationsDelegates}
+  final Iterable<LocalizationsDelegate<dynamic>>? localizationsDelegates;
+
+  /// {@macro flutter.widgets.widgetsApp.localeListResolutionCallback}
+  ///
+  /// This callback is passed along to the [WidgetsApp] built by this widget.
+  final LocaleListResolutionCallback? localeListResolutionCallback;
+
+  /// {@macro flutter.widgets.LocaleResolutionCallback}
+  ///
+  /// This callback is passed along to the [WidgetsApp] built by this widget.
+  final LocaleResolutionCallback? localeResolutionCallback;
+
+  /// {@macro flutter.widgets.widgetsApp.supportedLocales}
+  ///
+  /// It is passed along unmodified to the [WidgetsApp] built by this widget.
+  final Iterable<Locale> supportedLocales;
+
+  /// Turns on a performance overlay.
+  ///
+  /// See also:
+  ///
+  ///  * <https://flutter.dev/debugging/#performanceoverlay>
+  final bool showPerformanceOverlay;
+
+  /// Turns on checkerboarding of raster cache images.
+  final bool checkerboardRasterCacheImages;
+
+  /// Turns on checkerboarding of layers rendered to offscreen bitmaps.
+  final bool checkerboardOffscreenLayers;
+
+  /// Turns on an overlay that shows the accessibility information
+  /// reported by the framework.
+  final bool showSemanticsDebugger;
+
+  /// {@macro flutter.widgets.widgetsApp.debugShowCheckedModeBanner}
+  final bool debugShowCheckedModeBanner;
+
+  /// {@macro flutter.widgets.widgetsApp.shortcuts}
+  /// {@tool snippet}
+  /// This example shows how to add a single shortcut for
+  /// [LogicalKeyboardKey.select] to the default shortcuts without needing to
+  /// add your own [Shortcuts] widget.
+  ///
+  /// Alternatively, you could insert a [Shortcuts] widget with just the mapping
+  /// you want to add between the [WidgetsApp] and its child and get the same
+  /// effect.
+  ///
+  /// ```dart
+  /// Widget build(BuildContext context) {
+  ///   return WidgetsApp(
+  ///     shortcuts: <LogicalKeySet, Intent>{
+  ///       ... WidgetsApp.defaultShortcuts,
+  ///       LogicalKeySet(LogicalKeyboardKey.select): const ActivateIntent(),
+  ///     },
+  ///     color: const Color(0xFFFF0000),
+  ///     builder: (BuildContext context, Widget child) {
+  ///       return const Placeholder();
+  ///     },
+  ///   );
+  /// }
+  /// ```
+  /// {@end-tool}
+  /// {@macro flutter.widgets.widgetsApp.shortcuts.seeAlso}
+  final Map<LogicalKeySet, Intent>? shortcuts;
+
+  /// {@macro flutter.widgets.widgetsApp.actions}
+  /// {@tool snippet}
+  /// This example shows how to add a single action handling an
+  /// [ActivateAction] to the default actions without needing to
+  /// add your own [Actions] widget.
+  ///
+  /// Alternatively, you could insert a [Actions] widget with just the mapping
+  /// you want to add between the [WidgetsApp] and its child and get the same
+  /// effect.
+  ///
+  /// ```dart
+  /// Widget build(BuildContext context) {
+  ///   return WidgetsApp(
+  ///     actions: <Type, Action<Intent>>{
+  ///       ... WidgetsApp.defaultActions,
+  ///       ActivateAction: CallbackAction(
+  ///         onInvoke: (Intent intent) {
+  ///           // Do something here...
+  ///           return null;
+  ///         },
+  ///       ),
+  ///     },
+  ///     color: const Color(0xFFFF0000),
+  ///     builder: (BuildContext context, Widget child) {
+  ///       return const Placeholder();
+  ///     },
+  ///   );
+  /// }
+  /// ```
+  /// {@end-tool}
+  /// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
+  final Map<Type, Action<Intent>>? actions;
+
+  /// {@macro flutter.widgets.widgetsApp.restorationScopeId}
+  final String? restorationScopeId;
+
+  @override
+  _CupertinoAppState createState() => _CupertinoAppState();
+
+  /// The [HeroController] used for Cupertino page transitions.
+  ///
+  /// Used by [CupertinoTabView] and [CupertinoApp].
+  static HeroController createCupertinoHeroController() =>
+      HeroController(); // Linear tweening.
+}
+
+class _AlwaysCupertinoScrollBehavior extends ScrollBehavior {
+  @override
+  Widget buildViewportChrome(BuildContext context, Widget child, AxisDirection axisDirection) {
+    // Never build any overscroll glow indicators.
+    return child;
+  }
+
+  @override
+  ScrollPhysics getScrollPhysics(BuildContext context) {
+    return const BouncingScrollPhysics();
+  }
+}
+
+class _CupertinoAppState extends State<CupertinoApp> {
+  late HeroController _heroController;
+  bool get _usesRouter => widget.routerDelegate != null;
+
+  @override
+  void initState() {
+    super.initState();
+    _heroController = CupertinoApp.createCupertinoHeroController();
+  }
+
+  // Combine the default localization for Cupertino with the ones contributed
+  // by the localizationsDelegates parameter, if any. Only the first delegate
+  // of a particular LocalizationsDelegate.type is loaded so the
+  // localizationsDelegate parameter can be used to override
+  // _CupertinoLocalizationsDelegate.
+  Iterable<LocalizationsDelegate<dynamic>> get _localizationsDelegates sync* {
+    if (widget.localizationsDelegates != null)
+      yield* widget.localizationsDelegates!;
+    yield DefaultCupertinoLocalizations.delegate;
+  }
+
+  Widget _inspectorSelectButtonBuilder(BuildContext context, VoidCallback onPressed) {
+    return CupertinoButton.filled(
+      child: const Icon(
+        CupertinoIcons.search,
+        size: 28.0,
+        color: CupertinoColors.white,
+      ),
+      padding: EdgeInsets.zero,
+      onPressed: onPressed,
+    );
+  }
+
+  WidgetsApp _buildWidgetApp(BuildContext context) {
+    final CupertinoThemeData effectiveThemeData = CupertinoTheme.of(context);
+    final Color color = CupertinoDynamicColor.resolve(widget.color ?? effectiveThemeData.primaryColor, context);
+
+    if (_usesRouter) {
+      return WidgetsApp.router(
+        key: GlobalObjectKey(this),
+        routeInformationProvider: widget.routeInformationProvider,
+        routeInformationParser: widget.routeInformationParser!,
+        routerDelegate: widget.routerDelegate!,
+        backButtonDispatcher: widget.backButtonDispatcher,
+        builder: widget.builder,
+        title: widget.title,
+        onGenerateTitle: widget.onGenerateTitle,
+        textStyle: effectiveThemeData.textTheme.textStyle,
+        color: color,
+        locale: widget.locale,
+        localizationsDelegates: _localizationsDelegates,
+        localeResolutionCallback: widget.localeResolutionCallback,
+        localeListResolutionCallback: widget.localeListResolutionCallback,
+        supportedLocales: widget.supportedLocales,
+        showPerformanceOverlay: widget.showPerformanceOverlay,
+        checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
+        checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
+        showSemanticsDebugger: widget.showSemanticsDebugger,
+        debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
+        inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
+        shortcuts: widget.shortcuts,
+        actions: widget.actions,
+        restorationScopeId: widget.restorationScopeId,
+      );
+    }
+    return WidgetsApp(
+      key: GlobalObjectKey(this),
+      navigatorKey: widget.navigatorKey,
+      navigatorObservers: widget.navigatorObservers!,
+      pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) {
+        return CupertinoPageRoute<T>(settings: settings, builder: builder);
+      },
+      home: widget.home,
+      routes: widget.routes!,
+      initialRoute: widget.initialRoute,
+      onGenerateRoute: widget.onGenerateRoute,
+      onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
+      onUnknownRoute: widget.onUnknownRoute,
+      builder: widget.builder,
+      title: widget.title,
+      onGenerateTitle: widget.onGenerateTitle,
+      textStyle: effectiveThemeData.textTheme.textStyle,
+      color: color,
+      locale: widget.locale,
+      localizationsDelegates: _localizationsDelegates,
+      localeResolutionCallback: widget.localeResolutionCallback,
+      localeListResolutionCallback: widget.localeListResolutionCallback,
+      supportedLocales: widget.supportedLocales,
+      showPerformanceOverlay: widget.showPerformanceOverlay,
+      checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages,
+      checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers,
+      showSemanticsDebugger: widget.showSemanticsDebugger,
+      debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner,
+      inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
+      shortcuts: widget.shortcuts,
+      actions: widget.actions,
+      restorationScopeId: widget.restorationScopeId,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final CupertinoThemeData effectiveThemeData = widget.theme ?? const CupertinoThemeData();
+
+    return ScrollConfiguration(
+      behavior: _AlwaysCupertinoScrollBehavior(),
+      child: CupertinoUserInterfaceLevel(
+        data: CupertinoUserInterfaceLevelData.base,
+        child: CupertinoTheme(
+          data: effectiveThemeData,
+          child: HeroControllerScope(
+            controller: _heroController,
+            child: Builder(
+              builder: _buildWidgetApp,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/src/cupertino/bottom_tab_bar.dart b/lib/src/cupertino/bottom_tab_bar.dart
new file mode 100644
index 0000000..284307c
--- /dev/null
+++ b/lib/src/cupertino/bottom_tab_bar.dart
@@ -0,0 +1,300 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/ui.dart' show ImageFilter;
+
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'localizations.dart';
+import 'theme.dart';
+
+// Standard iOS 10 tab bar height.
+const double _kTabBarHeight = 50.0;
+
+const Color _kDefaultTabBarBorderColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0x4C000000),
+  darkColor: Color(0x29000000),
+);
+const Color _kDefaultTabBarInactiveColor = CupertinoColors.inactiveGray;
+
+/// An iOS-styled bottom navigation tab bar.
+///
+/// Displays multiple tabs using [BottomNavigationBarItem] with one tab being
+/// active, the first tab by default.
+///
+/// This [StatelessWidget] doesn't store the active tab itself. You must
+/// listen to the [onTap] callbacks and call `setState` with a new [currentIndex]
+/// for the new selection to reflect. This can also be done automatically
+/// by wrapping this with a [CupertinoTabScaffold].
+///
+/// Tab changes typically trigger a switch between [Navigator]s, each with its
+/// own navigation stack, per standard iOS design. This can be done by using
+/// [CupertinoTabView]s inside each tab builder in [CupertinoTabScaffold].
+///
+/// If the given [backgroundColor]'s opacity is not 1.0 (which is the case by
+/// default), it will produce a blurring effect to the content behind it.
+///
+/// When used as [CupertinoTabScaffold.tabBar], by default `CupertinoTabBar` has
+/// its text scale factor set to 1.0 and does not respond to text scale factor
+/// changes from the operating system, to match the native iOS behavior. To override
+/// this behavior, wrap each of the `navigationBar`'s components inside a [MediaQuery]
+/// with the desired [MediaQueryData.textScaleFactor] value. The text scale factor
+/// value from the operating system can be retrieved in many ways, such as querying
+/// [MediaQuery.textScaleFactorOf] against [CupertinoApp]'s [BuildContext].
+///
+/// See also:
+///
+///  * [CupertinoTabScaffold], which hosts the [CupertinoTabBar] at the bottom.
+///  * [BottomNavigationBarItem], an item in a [CupertinoTabBar].
+class CupertinoTabBar extends StatelessWidget implements PreferredSizeWidget {
+  /// Creates a tab bar in the iOS style.
+  const CupertinoTabBar({
+    Key? key,
+    required this.items,
+    this.onTap,
+    this.currentIndex = 0,
+    this.backgroundColor,
+    this.activeColor,
+    this.inactiveColor = _kDefaultTabBarInactiveColor,
+    this.iconSize = 30.0,
+    this.border = const Border(
+      top: BorderSide(
+        color: _kDefaultTabBarBorderColor,
+        width: 0.0, // One physical pixel.
+        style: BorderStyle.solid,
+      ),
+    ),
+  }) : assert(items != null),
+       assert(
+         items.length >= 2,
+         "Tabs need at least 2 items to conform to Apple's HIG",
+       ),
+       assert(currentIndex != null),
+       assert(0 <= currentIndex && currentIndex < items.length),
+       assert(iconSize != null),
+       assert(inactiveColor != null),
+       super(key: key);
+
+  /// The interactive items laid out within the bottom navigation bar.
+  ///
+  /// Must not be null.
+  final List<BottomNavigationBarItem> items;
+
+  /// The callback that is called when a item is tapped.
+  ///
+  /// The widget creating the bottom navigation bar needs to keep track of the
+  /// current index and call `setState` to rebuild it with the newly provided
+  /// index.
+  final ValueChanged<int>? onTap;
+
+  /// The index into [items] of the current active item.
+  ///
+  /// Must not be null and must inclusively be between 0 and the number of tabs
+  /// minus 1.
+  final int currentIndex;
+
+  /// The background color of the tab bar. If it contains transparency, the
+  /// tab bar will automatically produce a blurring effect to the content
+  /// behind it.
+  ///
+  /// Defaults to [CupertinoTheme]'s `barBackgroundColor` when null.
+  final Color? backgroundColor;
+
+  /// The foreground color of the icon and title for the [BottomNavigationBarItem]
+  /// of the selected tab.
+  ///
+  /// Defaults to [CupertinoTheme]'s `primaryColor` if null.
+  final Color? activeColor;
+
+  /// The foreground color of the icon and title for the [BottomNavigationBarItem]s
+  /// in the unselected state.
+  ///
+  /// Defaults to a [CupertinoDynamicColor] that matches the disabled foreground
+  /// color of the native `UITabBar` component. Cannot be null.
+  final Color inactiveColor;
+
+  /// The size of all of the [BottomNavigationBarItem] icons.
+  ///
+  /// This value is used to configure the [IconTheme] for the navigation bar.
+  /// When a [BottomNavigationBarItem.icon] widget is not an [Icon] the widget
+  /// should configure itself to match the icon theme's size and color.
+  ///
+  /// Must not be null.
+  final double iconSize;
+
+  /// The border of the [CupertinoTabBar].
+  ///
+  /// The default value is a one physical pixel top border with grey color.
+  final Border? border;
+
+  @override
+  Size get preferredSize => const Size.fromHeight(_kTabBarHeight);
+
+  /// Indicates whether the tab bar is fully opaque or can have contents behind
+  /// it show through it.
+  bool opaque(BuildContext context) {
+    final Color backgroundColor =
+        this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor;
+    return CupertinoDynamicColor.resolve(backgroundColor, context).alpha == 0xFF;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    assert(debugCheckHasMediaQuery(context));
+    final double bottomPadding = MediaQuery.of(context).padding.bottom;
+
+    final Color backgroundColor = CupertinoDynamicColor.resolve(
+      this.backgroundColor ?? CupertinoTheme.of(context).barBackgroundColor,
+      context,
+    );
+
+    BorderSide resolveBorderSide(BorderSide side) {
+      return side == BorderSide.none
+        ? side
+        : side.copyWith(color: CupertinoDynamicColor.resolve(side.color, context));
+    }
+
+    // Return the border as is when it's a subclass.
+    final Border? resolvedBorder = border == null || border.runtimeType != Border
+      ? border
+      : Border(
+        top: resolveBorderSide(border!.top),
+        left: resolveBorderSide(border!.left),
+        bottom: resolveBorderSide(border!.bottom),
+        right: resolveBorderSide(border!.right),
+      );
+
+    final Color inactive = CupertinoDynamicColor.resolve(inactiveColor, context);
+    Widget result = DecoratedBox(
+      decoration: BoxDecoration(
+        border: resolvedBorder,
+        color: backgroundColor,
+      ),
+      child: SizedBox(
+        height: _kTabBarHeight + bottomPadding,
+        child: IconTheme.merge( // Default with the inactive state.
+          data: IconThemeData(color: inactive, size: iconSize),
+          child: DefaultTextStyle( // Default with the inactive state.
+            style: CupertinoTheme.of(context).textTheme.tabLabelTextStyle.copyWith(color: inactive),
+            child: Padding(
+              padding: EdgeInsets.only(bottom: bottomPadding),
+              child: Semantics(
+                explicitChildNodes: true,
+                child: Row(
+                  // Align bottom since we want the labels to be aligned.
+                  crossAxisAlignment: CrossAxisAlignment.end,
+                  children: _buildTabItems(context),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+
+    if (!opaque(context)) {
+      // For non-opaque backgrounds, apply a blur effect.
+      result = ClipRect(
+        child: BackdropFilter(
+          filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),
+          child: result,
+        ),
+      );
+    }
+
+    return result;
+  }
+
+  List<Widget> _buildTabItems(BuildContext context) {
+    final List<Widget> result = <Widget>[];
+    final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
+
+    for (int index = 0; index < items.length; index += 1) {
+      final bool active = index == currentIndex;
+      result.add(
+        _wrapActiveItem(
+          context,
+          Expanded(
+            child: Semantics(
+              selected: active,
+              hint: localizations.tabSemanticsLabel(
+                tabIndex: index + 1,
+                tabCount: items.length,
+              ),
+              child: GestureDetector(
+                behavior: HitTestBehavior.opaque,
+                onTap: onTap == null ? null : () { onTap!(index); },
+                child: Padding(
+                  padding: const EdgeInsets.only(bottom: 4.0),
+                  child: Column(
+                    mainAxisAlignment: MainAxisAlignment.end,
+                    children: _buildSingleTabItem(items[index], active),
+                  ),
+                ),
+              ),
+            ),
+          ),
+          active: active,
+        ),
+      );
+    }
+
+    return result;
+  }
+
+  List<Widget> _buildSingleTabItem(BottomNavigationBarItem item, bool active) {
+    return <Widget>[
+      Expanded(
+        child: Center(child: active ? item.activeIcon : item.icon),
+      ),
+      if (item.title != null) item.title!,
+      if (item.label != null) Text(item.label!),
+    ];
+  }
+
+  /// Change the active tab item's icon and title colors to active.
+  Widget _wrapActiveItem(BuildContext context, Widget item, { required bool active }) {
+    if (!active)
+      return item;
+
+    final Color activeColor = CupertinoDynamicColor.resolve(
+      this.activeColor ?? CupertinoTheme.of(context).primaryColor,
+      context,
+    );
+    return IconTheme.merge(
+      data: IconThemeData(color: activeColor),
+      child: DefaultTextStyle.merge(
+        style: TextStyle(color: activeColor),
+        child: item,
+      ),
+    );
+  }
+
+  /// Create a clone of the current [CupertinoTabBar] but with provided
+  /// parameters overridden.
+  CupertinoTabBar copyWith({
+    Key? key,
+    List<BottomNavigationBarItem>? items,
+    Color? backgroundColor,
+    Color? activeColor,
+    Color? inactiveColor,
+    double? iconSize,
+    Border? border,
+    int? currentIndex,
+    ValueChanged<int>? onTap,
+  }) {
+    return CupertinoTabBar(
+      key: key ?? this.key,
+      items: items ?? this.items,
+      backgroundColor: backgroundColor ?? this.backgroundColor,
+      activeColor: activeColor ?? this.activeColor,
+      inactiveColor: inactiveColor ?? this.inactiveColor,
+      iconSize: iconSize ?? this.iconSize,
+      border: border ?? this.border,
+      currentIndex: currentIndex ?? this.currentIndex,
+      onTap: onTap ?? this.onTap,
+    );
+  }
+}
diff --git a/lib/src/cupertino/button.dart b/lib/src/cupertino/button.dart
new file mode 100644
index 0000000..144e4e7
--- /dev/null
+++ b/lib/src/cupertino/button.dart
@@ -0,0 +1,273 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'constants.dart';
+import 'theme.dart';
+
+// Measured against iOS 12 in Xcode.
+const EdgeInsets _kButtonPadding = EdgeInsets.all(16.0);
+const EdgeInsets _kBackgroundButtonPadding = EdgeInsets.symmetric(
+  vertical: 14.0,
+  horizontal: 64.0,
+);
+
+/// An iOS-style button.
+///
+/// Takes in a text or an icon that fades out and in on touch. May optionally have a
+/// background.
+///
+/// The [padding] defaults to 16.0 pixels. When using a [CupertinoButton] within
+/// a fixed height parent, like a [CupertinoNavigationBar], a smaller, or even
+/// [EdgeInsets.zero], should be used to prevent clipping larger [child]
+/// widgets.
+///
+/// See also:
+///
+///  * <https://developer.apple.com/ios/human-interface-guidelines/controls/buttons/>
+class CupertinoButton extends StatefulWidget {
+  /// Creates an iOS-style button.
+  const CupertinoButton({
+    Key? key,
+    required this.child,
+    this.padding,
+    this.color,
+    this.disabledColor = CupertinoColors.quaternarySystemFill,
+    this.minSize = kMinInteractiveDimensionCupertino,
+    this.pressedOpacity = 0.4,
+    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
+    required this.onPressed,
+  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
+       assert(disabledColor != null),
+       _filled = false,
+       super(key: key);
+
+  /// Creates an iOS-style button with a filled background.
+  ///
+  /// The background color is derived from the [CupertinoTheme]'s `primaryColor`.
+  ///
+  /// To specify a custom background color, use the [color] argument of the
+  /// default constructor.
+  const CupertinoButton.filled({
+    Key? key,
+    required this.child,
+    this.padding,
+    this.disabledColor = CupertinoColors.quaternarySystemFill,
+    this.minSize = kMinInteractiveDimensionCupertino,
+    this.pressedOpacity = 0.4,
+    this.borderRadius = const BorderRadius.all(Radius.circular(8.0)),
+    required this.onPressed,
+  }) : assert(pressedOpacity == null || (pressedOpacity >= 0.0 && pressedOpacity <= 1.0)),
+       assert(disabledColor != null),
+       color = null,
+       _filled = true,
+       super(key: key);
+
+  /// The widget below this widget in the tree.
+  ///
+  /// Typically a [Text] widget.
+  final Widget child;
+
+  /// The amount of space to surround the child inside the bounds of the button.
+  ///
+  /// Defaults to 16.0 pixels.
+  final EdgeInsetsGeometry? padding;
+
+  /// The color of the button's background.
+  ///
+  /// Defaults to null which produces a button with no background or border.
+  ///
+  /// Defaults to the [CupertinoTheme]'s `primaryColor` when the
+  /// [CupertinoButton.filled] constructor is used.
+  final Color? color;
+
+  /// The color of the button's background when the button is disabled.
+  ///
+  /// Ignored if the [CupertinoButton] doesn't also have a [color].
+  ///
+  /// Defaults to [CupertinoColors.quaternarySystemFill] when [color] is
+  /// specified. Must not be null.
+  final Color disabledColor;
+
+  /// The callback that is called when the button is tapped or otherwise activated.
+  ///
+  /// If this is set to null, the button will be disabled.
+  final VoidCallback? onPressed;
+
+  /// Minimum size of the button.
+  ///
+  /// Defaults to kMinInteractiveDimensionCupertino which the iOS Human
+  /// Interface Guidelines recommends as the minimum tappable area.
+  final double? minSize;
+
+  /// The opacity that the button will fade to when it is pressed.
+  /// The button will have an opacity of 1.0 when it is not pressed.
+  ///
+  /// This defaults to 0.4. If null, opacity will not change on pressed if using
+  /// your own custom effects is desired.
+  final double? pressedOpacity;
+
+  /// The radius of the button's corners when it has a background color.
+  ///
+  /// Defaults to round corners of 8 logical pixels.
+  final BorderRadius? borderRadius;
+
+  final bool _filled;
+
+  /// Whether the button is enabled or disabled. Buttons are disabled by default. To
+  /// enable a button, set its [onPressed] property to a non-null value.
+  bool get enabled => onPressed != null;
+
+  @override
+  _CupertinoButtonState createState() => _CupertinoButtonState();
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    properties.add(FlagProperty('enabled', value: enabled, ifFalse: 'disabled'));
+  }
+}
+
+class _CupertinoButtonState extends State<CupertinoButton> with SingleTickerProviderStateMixin {
+  // Eyeballed values. Feel free to tweak.
+  static const Duration kFadeOutDuration = Duration(milliseconds: 10);
+  static const Duration kFadeInDuration = Duration(milliseconds: 100);
+  final Tween<double> _opacityTween = Tween<double>(begin: 1.0);
+
+  late AnimationController _animationController;
+  late Animation<double> _opacityAnimation;
+
+  @override
+  void initState() {
+    super.initState();
+    _animationController = AnimationController(
+      duration: const Duration(milliseconds: 200),
+      value: 0.0,
+      vsync: this,
+    );
+    _opacityAnimation = _animationController
+      .drive(CurveTween(curve: Curves.decelerate))
+      .drive(_opacityTween);
+    _setTween();
+  }
+
+  @override
+  void didUpdateWidget(CupertinoButton old) {
+    super.didUpdateWidget(old);
+    _setTween();
+  }
+
+  void _setTween() {
+    _opacityTween.end = widget.pressedOpacity ?? 1.0;
+  }
+
+  @override
+  void dispose() {
+    _animationController.dispose();
+    super.dispose();
+  }
+
+  bool _buttonHeldDown = false;
+
+  void _handleTapDown(TapDownDetails event) {
+    if (!_buttonHeldDown) {
+      _buttonHeldDown = true;
+      _animate();
+    }
+  }
+
+  void _handleTapUp(TapUpDetails event) {
+    if (_buttonHeldDown) {
+      _buttonHeldDown = false;
+      _animate();
+    }
+  }
+
+  void _handleTapCancel() {
+    if (_buttonHeldDown) {
+      _buttonHeldDown = false;
+      _animate();
+    }
+  }
+
+  void _animate() {
+    if (_animationController.isAnimating)
+      return;
+    final bool wasHeldDown = _buttonHeldDown;
+    final TickerFuture ticker = _buttonHeldDown
+        ? _animationController.animateTo(1.0, duration: kFadeOutDuration)
+        : _animationController.animateTo(0.0, duration: kFadeInDuration);
+    ticker.then<void>((void value) {
+      if (mounted && wasHeldDown != _buttonHeldDown)
+        _animate();
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final bool enabled = widget.enabled;
+    final CupertinoThemeData themeData = CupertinoTheme.of(context);
+    final Color primaryColor = themeData.primaryColor;
+    final Color? backgroundColor = widget.color == null
+      ? (widget._filled ? primaryColor : null)
+      : CupertinoDynamicColor.maybeResolve(widget.color, context);
+
+    final Color foregroundColor = backgroundColor != null
+      ? themeData.primaryContrastingColor
+      : enabled
+        ? primaryColor
+        : CupertinoDynamicColor.resolve(CupertinoColors.placeholderText, context);
+
+    final TextStyle textStyle = themeData.textTheme.textStyle.copyWith(color: foregroundColor);
+
+    return GestureDetector(
+      behavior: HitTestBehavior.opaque,
+      onTapDown: enabled ? _handleTapDown : null,
+      onTapUp: enabled ? _handleTapUp : null,
+      onTapCancel: enabled ? _handleTapCancel : null,
+      onTap: widget.onPressed,
+      child: Semantics(
+        button: true,
+        child: ConstrainedBox(
+          constraints: widget.minSize == null
+            ? const BoxConstraints()
+            : BoxConstraints(
+                minWidth: widget.minSize!,
+                minHeight: widget.minSize!,
+              ),
+          child: FadeTransition(
+            opacity: _opacityAnimation,
+            child: DecoratedBox(
+              decoration: BoxDecoration(
+                borderRadius: widget.borderRadius,
+                color: backgroundColor != null && !enabled
+                  ? CupertinoDynamicColor.resolve(widget.disabledColor, context)
+                  : backgroundColor,
+              ),
+              child: Padding(
+                padding: widget.padding ?? (backgroundColor != null
+                  ? _kBackgroundButtonPadding
+                  : _kButtonPadding),
+                child: Center(
+                  widthFactor: 1.0,
+                  heightFactor: 1.0,
+                  child: DefaultTextStyle(
+                    style: textStyle,
+                    child: IconTheme(
+                      data: IconThemeData(color: foregroundColor),
+                      child: widget.child,
+                    ),
+                  ),
+                ),
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/src/cupertino/colors.dart b/lib/src/cupertino/colors.dart
new file mode 100644
index 0000000..f78876a
--- /dev/null
+++ b/lib/src/cupertino/colors.dart
@@ -0,0 +1,1138 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/ui.dart' show Color, Brightness;
+
+import '../../foundation.dart';
+import '../widgets/basic.dart';
+import '../widgets/framework.dart';
+import '../widgets/media_query.dart';
+import 'interface_level.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// // @dart = 2.9
+// Widget child;
+// BuildContext context;
+
+/// A palette of [Color] constants that describe colors commonly used when
+/// matching the iOS platform aesthetics.
+class CupertinoColors {
+  // This class is not meant to be instantiated or extended; this constructor
+  // prevents instantiation and extension.
+  // ignore: unused_element
+  CupertinoColors._();
+
+  /// iOS 13's default blue color. Used to indicate active elements such as
+  /// buttons, selected tabs and your own chat bubbles.
+  ///
+  /// This is SystemBlue in the iOS palette.
+  static const CupertinoDynamicColor activeBlue = systemBlue;
+
+  /// iOS 13's default green color. Used to indicate active accents such as
+  /// the switch in its on state and some accent buttons such as the call button
+  /// and Apple Map's 'Go' button.
+  ///
+  /// This is SystemGreen in the iOS palette.
+  static const CupertinoDynamicColor activeGreen = systemGreen;
+
+  /// iOS 13's orange color.
+  ///
+  /// This is SystemOrange in the iOS palette.
+  static const CupertinoDynamicColor activeOrange = systemOrange;
+
+  /// Opaque white color. Used for backgrounds and fonts against dark backgrounds.
+  ///
+  /// This is SystemWhiteColor in the iOS palette.
+  ///
+  /// See also:
+  ///
+  ///  * [material.Colors.white], the same color, in the material design palette.
+  ///  * [black], opaque black in the [CupertinoColors] palette.
+  static const Color white = Color(0xFFFFFFFF);
+
+  /// Opaque black color. Used for texts against light backgrounds.
+  ///
+  /// This is SystemBlackColor in the iOS palette.
+  ///
+  /// See also:
+  ///
+  ///  * [material.Colors.black], the same color, in the material design palette.
+  ///  * [white], opaque white in the [CupertinoColors] palette.
+  static const Color black = Color(0xFF000000);
+
+  /// Used in iOS 10 for light background fills such as the chat bubble background.
+  ///
+  /// This is SystemLightGrayColor in the iOS palette.
+  static const Color lightBackgroundGray = Color(0xFFE5E5EA);
+
+  /// Used in iOS 12 for very light background fills in tables between cell groups.
+  ///
+  /// This is SystemExtraLightGrayColor in the iOS palette.
+  static const Color extraLightBackgroundGray = Color(0xFFEFEFF4);
+
+  /// Used in iOS 12 for very dark background fills in tables between cell groups
+  /// in dark mode.
+  // Value derived from screenshot from the dark themed Apple Watch app.
+  static const Color darkBackgroundGray = Color(0xFF171717);
+
+  /// Used in iOS 13 for unselected selectables such as tab bar items in their
+  /// inactive state or de-emphasized subtitles and details text.
+  ///
+  /// Not the same grey as disabled buttons etc.
+  ///
+  /// This is the disabled color in the iOS palette.
+  static const CupertinoDynamicColor inactiveGray = CupertinoDynamicColor.withBrightness(
+    debugLabel: 'inactiveGray',
+    color: Color(0xFF999999),
+    darkColor: Color(0xFF757575),
+  );
+
+  /// Used for iOS 13 for destructive actions such as the delete actions in
+  /// table view cells and dialogs.
+  ///
+  /// Not the same red as the camera shutter or springboard icon notifications
+  /// or the foreground red theme in various native apps such as HealthKit.
+  ///
+  /// This is SystemRed in the iOS palette.
+  static const Color destructiveRed = systemRed;
+
+  /// A blue color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemBlue](https://developer.apple.com/documentation/uikit/uicolor/3173141-systemblue),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemBlue = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemBlue',
+    color: Color.fromARGB(255, 0, 122, 255),
+    darkColor: Color.fromARGB(255, 10, 132, 255),
+    highContrastColor: Color.fromARGB(255, 0, 64, 221),
+    darkHighContrastColor: Color.fromARGB(255, 64, 156, 255),
+  );
+
+  /// A green color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGreen](https://developer.apple.com/documentation/uikit/uicolor/3173144-systemgreen),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGreen = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGreen',
+    color: Color.fromARGB(255, 52, 199, 89),
+    darkColor: Color.fromARGB(255, 48, 209, 88),
+    highContrastColor: Color.fromARGB(255, 36, 138, 61),
+    darkHighContrastColor: Color.fromARGB(255, 48, 219, 91),
+  );
+
+  /// An indigo color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemIndigo](https://developer.apple.com/documentation/uikit/uicolor/3173146-systemindigo),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemIndigo = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemIndigo',
+    color: Color.fromARGB(255, 88, 86, 214),
+    darkColor: Color.fromARGB(255, 94, 92, 230),
+    highContrastColor: Color.fromARGB(255, 54, 52, 163),
+    darkHighContrastColor: Color.fromARGB(255, 125, 122, 255),
+  );
+
+  /// An orange color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemOrange](https://developer.apple.com/documentation/uikit/uicolor/3173147-systemorange),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemOrange = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemOrange',
+    color: Color.fromARGB(255, 255, 149, 0),
+    darkColor: Color.fromARGB(255, 255, 159, 10),
+    highContrastColor: Color.fromARGB(255, 201, 52, 0),
+    darkHighContrastColor: Color.fromARGB(255, 255, 179, 64),
+  );
+
+  /// A pink color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemPink](https://developer.apple.com/documentation/uikit/uicolor/3173148-systempink),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemPink = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemPink',
+    color: Color.fromARGB(255, 255, 45, 85),
+    darkColor: Color.fromARGB(255, 255, 55, 95),
+    highContrastColor: Color.fromARGB(255, 211, 15, 69),
+    darkHighContrastColor: Color.fromARGB(255, 255, 100, 130),
+  );
+
+  /// A purple color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemPurple](https://developer.apple.com/documentation/uikit/uicolor/3173149-systempurple),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemPurple = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemPurple',
+    color: Color.fromARGB(255, 175, 82, 222),
+    darkColor: Color.fromARGB(255, 191, 90, 242),
+    highContrastColor: Color.fromARGB(255, 137, 68, 171),
+    darkHighContrastColor: Color.fromARGB(255, 218, 143, 255),
+  );
+
+  /// A red color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemRed](https://developer.apple.com/documentation/uikit/uicolor/3173150-systemred),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemRed = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemRed',
+    color: Color.fromARGB(255, 255, 59, 48),
+    darkColor: Color.fromARGB(255, 255, 69, 58),
+    highContrastColor: Color.fromARGB(255, 215, 0, 21),
+    darkHighContrastColor: Color.fromARGB(255, 255, 105, 97),
+  );
+
+  /// A teal color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemTeal](https://developer.apple.com/documentation/uikit/uicolor/3173151-systemteal),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemTeal = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemTeal',
+    color: Color.fromARGB(255, 90, 200, 250),
+    darkColor: Color.fromARGB(255, 100, 210, 255),
+    highContrastColor: Color.fromARGB(255, 0, 113, 164),
+    darkHighContrastColor: Color.fromARGB(255, 112, 215, 255),
+  );
+
+  /// A yellow color that can adapt to the given [BuildContext].
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemYellow](https://developer.apple.com/documentation/uikit/uicolor/3173152-systemyellow),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemYellow = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemYellow',
+    color: Color.fromARGB(255, 255, 204, 0),
+    darkColor: Color.fromARGB(255, 255, 214, 10),
+    highContrastColor: Color.fromARGB(255, 160, 90, 0),
+    darkHighContrastColor: Color.fromARGB(255, 255, 212, 38),
+  );
+
+  /// The base grey color.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray](https://developer.apple.com/documentation/uikit/uicolor/3173143-systemgray),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey',
+    color: Color.fromARGB(255, 142, 142, 147),
+    darkColor: Color.fromARGB(255, 142, 142, 147),
+    highContrastColor: Color.fromARGB(255, 108, 108, 112),
+    darkHighContrastColor: Color.fromARGB(255, 174, 174, 178),
+  );
+
+  /// A second-level shade of grey.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray2](https://developer.apple.com/documentation/uikit/uicolor/3255071-systemgray2),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey2 = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey2',
+    color: Color.fromARGB(255, 174, 174, 178),
+    darkColor: Color.fromARGB(255, 99, 99, 102),
+    highContrastColor: Color.fromARGB(255, 142, 142, 147),
+    darkHighContrastColor: Color.fromARGB(255, 124, 124, 128),
+  );
+
+  /// A third-level shade of grey.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray3](https://developer.apple.com/documentation/uikit/uicolor/3255072-systemgray3),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey3 = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey3',
+    color: Color.fromARGB(255, 199, 199, 204),
+    darkColor: Color.fromARGB(255, 72, 72, 74),
+    highContrastColor: Color.fromARGB(255, 174, 174, 178),
+    darkHighContrastColor: Color.fromARGB(255, 84, 84, 86),
+  );
+
+  /// A fourth-level shade of grey.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray4](https://developer.apple.com/documentation/uikit/uicolor/3255073-systemgray4),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey4 = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey4',
+    color: Color.fromARGB(255, 209, 209, 214),
+    darkColor: Color.fromARGB(255, 58, 58, 60),
+    highContrastColor: Color.fromARGB(255, 188, 188, 192),
+    darkHighContrastColor: Color.fromARGB(255, 68, 68, 70),
+  );
+
+  /// A fifth-level shade of grey.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray5](https://developer.apple.com/documentation/uikit/uicolor/3255074-systemgray5),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey5 = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey5',
+    color: Color.fromARGB(255, 229, 229, 234),
+    darkColor: Color.fromARGB(255, 44, 44, 46),
+    highContrastColor: Color.fromARGB(255, 216, 216, 220),
+    darkHighContrastColor: Color.fromARGB(255, 54, 54, 56),
+  );
+
+  /// A sixth-level shade of grey.
+  ///
+  /// See also:
+  ///
+  ///  * [UIColor.systemGray6](https://developer.apple.com/documentation/uikit/uicolor/3255075-systemgray6),
+  ///    the `UIKit` equivalent.
+  static const CupertinoDynamicColor systemGrey6 = CupertinoDynamicColor.withBrightnessAndContrast(
+    debugLabel: 'systemGrey6',
+    color: Color.fromARGB(255, 242, 242, 247),
+    darkColor: Color.fromARGB(255, 28, 28, 30),
+    highContrastColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastColor: Color.fromARGB(255, 36, 36, 38),
+  );
+
+  /// The color for text labels containing primary content, equivalent to
+  /// [UIColor.label](https://developer.apple.com/documentation/uikit/uicolor/3173131-label).
+  static const CupertinoDynamicColor label = CupertinoDynamicColor(
+    debugLabel: 'label',
+    color: Color.fromARGB(255, 0, 0, 0),
+    darkColor: Color.fromARGB(255, 255, 255, 255),
+    highContrastColor: Color.fromARGB(255, 0, 0, 0),
+    darkHighContrastColor: Color.fromARGB(255, 255, 255, 255),
+    elevatedColor: Color.fromARGB(255, 0, 0, 0),
+    darkElevatedColor: Color.fromARGB(255, 255, 255, 255),
+    highContrastElevatedColor: Color.fromARGB(255, 0, 0, 0),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 255, 255, 255),
+  );
+
+  /// The color for text labels containing secondary content, equivalent to
+  /// [UIColor.secondaryLabel](https://developer.apple.com/documentation/uikit/uicolor/3173136-secondarylabel).
+  static const CupertinoDynamicColor secondaryLabel = CupertinoDynamicColor(
+    debugLabel: 'secondaryLabel',
+    color: Color.fromARGB(153, 60, 60, 67),
+    darkColor: Color.fromARGB(153, 235, 235, 245),
+    highContrastColor: Color.fromARGB(173, 60, 60, 67),
+    darkHighContrastColor: Color.fromARGB(173, 235, 235, 245),
+    elevatedColor: Color.fromARGB(153, 60, 60, 67),
+    darkElevatedColor: Color.fromARGB(153, 235, 235, 245),
+    highContrastElevatedColor: Color.fromARGB(173, 60, 60, 67),
+    darkHighContrastElevatedColor: Color.fromARGB(173, 235, 235, 245),
+);
+
+  /// The color for text labels containing tertiary content, equivalent to
+  /// [UIColor.tertiaryLabel](https://developer.apple.com/documentation/uikit/uicolor/3173153-tertiarylabel).
+  static const CupertinoDynamicColor tertiaryLabel = CupertinoDynamicColor(
+    debugLabel: 'tertiaryLabel',
+    color: Color.fromARGB(76, 60, 60, 67),
+    darkColor: Color.fromARGB(76, 235, 235, 245),
+    highContrastColor: Color.fromARGB(96, 60, 60, 67),
+    darkHighContrastColor: Color.fromARGB(96, 235, 235, 245),
+    elevatedColor: Color.fromARGB(76, 60, 60, 67),
+    darkElevatedColor: Color.fromARGB(76, 235, 235, 245),
+    highContrastElevatedColor: Color.fromARGB(96, 60, 60, 67),
+    darkHighContrastElevatedColor: Color.fromARGB(96, 235, 235, 245),
+  );
+
+  /// The color for text labels containing quaternary content, equivalent to
+  /// [UIColor.quaternaryLabel](https://developer.apple.com/documentation/uikit/uicolor/3173135-quaternarylabel).
+  static const CupertinoDynamicColor quaternaryLabel = CupertinoDynamicColor(
+    debugLabel: 'quaternaryLabel',
+    color: Color.fromARGB(45, 60, 60, 67),
+    darkColor: Color.fromARGB(40, 235, 235, 245),
+    highContrastColor: Color.fromARGB(66, 60, 60, 67),
+    darkHighContrastColor: Color.fromARGB(61, 235, 235, 245),
+    elevatedColor: Color.fromARGB(45, 60, 60, 67),
+    darkElevatedColor: Color.fromARGB(40, 235, 235, 245),
+    highContrastElevatedColor: Color.fromARGB(66, 60, 60, 67),
+    darkHighContrastElevatedColor: Color.fromARGB(61, 235, 235, 245),
+  );
+
+  /// An overlay fill color for thin and small shapes, equivalent to
+  /// [UIColor.systemFill](https://developer.apple.com/documentation/uikit/uicolor/3255070-systemfill).
+  static const CupertinoDynamicColor systemFill = CupertinoDynamicColor(
+    debugLabel: 'systemFill',
+    color: Color.fromARGB(51, 120, 120, 128),
+    darkColor: Color.fromARGB(91, 120, 120, 128),
+    highContrastColor: Color.fromARGB(71, 120, 120, 128),
+    darkHighContrastColor: Color.fromARGB(112, 120, 120, 128),
+    elevatedColor: Color.fromARGB(51, 120, 120, 128),
+    darkElevatedColor: Color.fromARGB(91, 120, 120, 128),
+    highContrastElevatedColor: Color.fromARGB(71, 120, 120, 128),
+    darkHighContrastElevatedColor: Color.fromARGB(112, 120, 120, 128),
+  );
+
+  /// An overlay fill color for medium-size shapes, equivalent to
+  /// [UIColor.secondarySystemFill](https://developer.apple.com/documentation/uikit/uicolor/3255069-secondarysystemfill).
+  static const CupertinoDynamicColor secondarySystemFill = CupertinoDynamicColor(
+    debugLabel: 'secondarySystemFill',
+    color: Color.fromARGB(40, 120, 120, 128),
+    darkColor: Color.fromARGB(81, 120, 120, 128),
+    highContrastColor: Color.fromARGB(61, 120, 120, 128),
+    darkHighContrastColor: Color.fromARGB(102, 120, 120, 128),
+    elevatedColor: Color.fromARGB(40, 120, 120, 128),
+    darkElevatedColor: Color.fromARGB(81, 120, 120, 128),
+    highContrastElevatedColor: Color.fromARGB(61, 120, 120, 128),
+    darkHighContrastElevatedColor: Color.fromARGB(102, 120, 120, 128),
+  );
+
+  /// An overlay fill color for large shapes, equivalent to
+  /// [UIColor.tertiarySystemFill](https://developer.apple.com/documentation/uikit/uicolor/3255076-tertiarysystemfill).
+  static const CupertinoDynamicColor tertiarySystemFill = CupertinoDynamicColor(
+    debugLabel: 'tertiarySystemFill',
+    color: Color.fromARGB(30, 118, 118, 128),
+    darkColor: Color.fromARGB(61, 118, 118, 128),
+    highContrastColor: Color.fromARGB(51, 118, 118, 128),
+    darkHighContrastColor: Color.fromARGB(81, 118, 118, 128),
+    elevatedColor: Color.fromARGB(30, 118, 118, 128),
+    darkElevatedColor: Color.fromARGB(61, 118, 118, 128),
+    highContrastElevatedColor: Color.fromARGB(51, 118, 118, 128),
+    darkHighContrastElevatedColor: Color.fromARGB(81, 118, 118, 128),
+  );
+
+  /// An overlay fill color for large areas containing complex content, equivalent
+  /// to [UIColor.quaternarySystemFill](https://developer.apple.com/documentation/uikit/uicolor/3255068-quaternarysystemfill).
+  static const CupertinoDynamicColor quaternarySystemFill = CupertinoDynamicColor(
+    debugLabel: 'quaternarySystemFill',
+    color: Color.fromARGB(20, 116, 116, 128),
+    darkColor: Color.fromARGB(45, 118, 118, 128),
+    highContrastColor: Color.fromARGB(40, 116, 116, 128),
+    darkHighContrastColor: Color.fromARGB(66, 118, 118, 128),
+    elevatedColor: Color.fromARGB(20, 116, 116, 128),
+    darkElevatedColor: Color.fromARGB(45, 118, 118, 128),
+    highContrastElevatedColor: Color.fromARGB(40, 116, 116, 128),
+    darkHighContrastElevatedColor: Color.fromARGB(66, 118, 118, 128),
+  );
+
+  /// The color for placeholder text in controls or text views, equivalent to
+  /// [UIColor.placeholderText](https://developer.apple.com/documentation/uikit/uicolor/3173134-placeholdertext).
+  static const CupertinoDynamicColor placeholderText = CupertinoDynamicColor(
+    debugLabel: 'placeholderText',
+    color: Color.fromARGB(76, 60, 60, 67),
+    darkColor: Color.fromARGB(76, 235, 235, 245),
+    highContrastColor: Color.fromARGB(96, 60, 60, 67),
+    darkHighContrastColor: Color.fromARGB(96, 235, 235, 245),
+    elevatedColor: Color.fromARGB(76, 60, 60, 67),
+    darkElevatedColor: Color.fromARGB(76, 235, 235, 245),
+    highContrastElevatedColor: Color.fromARGB(96, 60, 60, 67),
+    darkHighContrastElevatedColor: Color.fromARGB(96, 235, 235, 245),
+  );
+
+  /// The color for the main background of your interface, equivalent to
+  /// [UIColor.systemBackground](https://developer.apple.com/documentation/uikit/uicolor/3173140-systembackground).
+  ///
+  /// Typically used for designs that have a white primary background in a light environment.
+  static const CupertinoDynamicColor systemBackground = CupertinoDynamicColor(
+    debugLabel: 'systemBackground',
+    color: Color.fromARGB(255, 255, 255, 255),
+    darkColor: Color.fromARGB(255, 0, 0, 0),
+    highContrastColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastColor: Color.fromARGB(255, 0, 0, 0),
+    elevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkElevatedColor: Color.fromARGB(255, 28, 28, 30),
+    highContrastElevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 36, 36, 38),
+  );
+
+  /// The color for content layered on top of the main background, equivalent to
+  /// [UIColor.secondarySystemBackground](https://developer.apple.com/documentation/uikit/uicolor/3173137-secondarysystembackground).
+  ///
+  /// Typically used for designs that have a white primary background in a light environment.
+  static const CupertinoDynamicColor secondarySystemBackground = CupertinoDynamicColor(
+    debugLabel: 'secondarySystemBackground',
+    color: Color.fromARGB(255, 242, 242, 247),
+    darkColor: Color.fromARGB(255, 28, 28, 30),
+    highContrastColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastColor: Color.fromARGB(255, 36, 36, 38),
+    elevatedColor: Color.fromARGB(255, 242, 242, 247),
+    darkElevatedColor: Color.fromARGB(255, 44, 44, 46),
+    highContrastElevatedColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 54, 54, 56),
+  );
+
+  /// The color for content layered on top of secondary backgrounds, equivalent
+  /// to [UIColor.tertiarySystemBackground](https://developer.apple.com/documentation/uikit/uicolor/3173154-tertiarysystembackground).
+  ///
+  /// Typically used for designs that have a white primary background in a light environment.
+  static const CupertinoDynamicColor tertiarySystemBackground = CupertinoDynamicColor(
+    debugLabel: 'tertiarySystemBackground',
+    color: Color.fromARGB(255, 255, 255, 255),
+    darkColor: Color.fromARGB(255, 44, 44, 46),
+    highContrastColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastColor: Color.fromARGB(255, 54, 54, 56),
+    elevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkElevatedColor: Color.fromARGB(255, 58, 58, 60),
+    highContrastElevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 68, 68, 70),
+  );
+
+  /// The color for the main background of your grouped interface, equivalent to
+  /// [UIColor.systemGroupedBackground](https://developer.apple.com/documentation/uikit/uicolor/3173145-systemgroupedbackground).
+  ///
+  /// Typically used for grouped content, including table views and platter-based designs.
+  static const CupertinoDynamicColor systemGroupedBackground = CupertinoDynamicColor(
+    debugLabel: 'systemGroupedBackground',
+    color: Color.fromARGB(255, 242, 242, 247),
+    darkColor: Color.fromARGB(255, 0, 0, 0),
+    highContrastColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastColor: Color.fromARGB(255, 0, 0, 0),
+    elevatedColor: Color.fromARGB(255, 242, 242, 247),
+    darkElevatedColor: Color.fromARGB(255, 28, 28, 30),
+    highContrastElevatedColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 36, 36, 38),
+  );
+
+  /// The color for content layered on top of the main background of your grouped interface,
+  /// equivalent to [UIColor.secondarySystemGroupedBackground](https://developer.apple.com/documentation/uikit/uicolor/3173138-secondarysystemgroupedbackground).
+  ///
+  /// Typically used for grouped content, including table views and platter-based designs.
+  static const CupertinoDynamicColor secondarySystemGroupedBackground = CupertinoDynamicColor(
+    debugLabel: 'secondarySystemGroupedBackground',
+    color: Color.fromARGB(255, 255, 255, 255),
+    darkColor: Color.fromARGB(255, 28, 28, 30),
+    highContrastColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastColor: Color.fromARGB(255, 36, 36, 38),
+    elevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkElevatedColor: Color.fromARGB(255, 44, 44, 46),
+    highContrastElevatedColor: Color.fromARGB(255, 255, 255, 255),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 54, 54, 56),
+  );
+
+  /// The color for content layered on top of secondary backgrounds of your grouped interface,
+  /// equivalent to [UIColor.tertiarySystemGroupedBackground](https://developer.apple.com/documentation/uikit/uicolor/3173155-tertiarysystemgroupedbackground).
+  ///
+  /// Typically used for grouped content, including table views and platter-based designs.
+  static const CupertinoDynamicColor tertiarySystemGroupedBackground = CupertinoDynamicColor(
+    debugLabel: 'tertiarySystemGroupedBackground',
+    color: Color.fromARGB(255, 242, 242, 247),
+    darkColor: Color.fromARGB(255, 44, 44, 46),
+    highContrastColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastColor: Color.fromARGB(255, 54, 54, 56),
+    elevatedColor: Color.fromARGB(255, 242, 242, 247),
+    darkElevatedColor: Color.fromARGB(255, 58, 58, 60),
+    highContrastElevatedColor: Color.fromARGB(255, 235, 235, 240),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 68, 68, 70),
+  );
+
+  /// The color for thin borders or divider lines that allows some underlying content to be visible,
+  /// equivalent to [UIColor.separator](https://developer.apple.com/documentation/uikit/uicolor/3173139-separator).
+  static const CupertinoDynamicColor separator = CupertinoDynamicColor(
+    debugLabel: 'separator',
+    color: Color.fromARGB(73, 60, 60, 67),
+    darkColor: Color.fromARGB(153, 84, 84, 88),
+    highContrastColor: Color.fromARGB(94, 60, 60, 67),
+    darkHighContrastColor: Color.fromARGB(173, 84, 84, 88),
+    elevatedColor: Color.fromARGB(73, 60, 60, 67),
+    darkElevatedColor: Color.fromARGB(153, 84, 84, 88),
+    highContrastElevatedColor: Color.fromARGB(94, 60, 60, 67),
+    darkHighContrastElevatedColor: Color.fromARGB(173, 84, 84, 88),
+  );
+
+  /// The color for borders or divider lines that hide any underlying content,
+  /// equivalent to [UIColor.opaqueSeparator](https://developer.apple.com/documentation/uikit/uicolor/3173133-opaqueseparator).
+  static const CupertinoDynamicColor opaqueSeparator = CupertinoDynamicColor(
+    debugLabel: 'opaqueSeparator',
+    color: Color.fromARGB(255, 198, 198, 200),
+    darkColor: Color.fromARGB(255, 56, 56, 58),
+    highContrastColor: Color.fromARGB(255, 198, 198, 200),
+    darkHighContrastColor: Color.fromARGB(255, 56, 56, 58),
+    elevatedColor: Color.fromARGB(255, 198, 198, 200),
+    darkElevatedColor: Color.fromARGB(255, 56, 56, 58),
+    highContrastElevatedColor: Color.fromARGB(255, 198, 198, 200),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 56, 56, 58),
+  );
+
+  /// The color for links, equivalent to
+  /// [UIColor.link](https://developer.apple.com/documentation/uikit/uicolor/3173132-link).
+  static const CupertinoDynamicColor link = CupertinoDynamicColor(
+    debugLabel: 'link',
+    color: Color.fromARGB(255, 0, 122, 255),
+    darkColor: Color.fromARGB(255, 9, 132, 255),
+    highContrastColor: Color.fromARGB(255, 0, 122, 255),
+    darkHighContrastColor: Color.fromARGB(255, 9, 132, 255),
+    elevatedColor: Color.fromARGB(255, 0, 122, 255),
+    darkElevatedColor: Color.fromARGB(255, 9, 132, 255),
+    highContrastElevatedColor: Color.fromARGB(255, 0, 122, 255),
+    darkHighContrastElevatedColor: Color.fromARGB(255, 9, 132, 255),
+  );
+}
+
+/// A [Color] subclass that represents a family of colors, and the correct effective
+/// color in the color family.
+///
+/// When used as a regular color, [CupertinoDynamicColor] is equivalent to the
+/// effective color (i.e. [CupertinoDynamicColor.value] will come from the effective
+/// color), which is determined by the [BuildContext] it is last resolved against.
+/// If it has never been resolved, the light, normal contrast, base elevation variant
+/// [CupertinoDynamicColor.color] will be the default effective color.
+///
+/// Sometimes manually resolving a [CupertinoDynamicColor] is not necessary, because
+/// the Cupertino Library provides built-in support for it.
+///
+/// ### Using [CupertinoDynamicColor] in a Cupertino widget
+///
+/// When a Cupertino widget is provided with a [CupertinoDynamicColor], either
+/// directly in its constructor, or from an [InheritedWidget] it depends on (for example,
+/// [DefaultTextStyle]), the widget will automatically resolve the color using
+/// [CupertinoDynamicColor.resolve] against its own [BuildContext], on a best-effort
+/// basis.
+///
+/// {@tool snippet}
+/// By default a [CupertinoButton] has no background color. The following sample
+/// code shows how to build a [CupertinoButton] that appears white in light mode,
+/// and changes automatically to black in dark mode.
+///
+/// ```dart
+/// CupertinoButton(
+///   child: child,
+///   // CupertinoDynamicColor works out of box in a CupertinoButton.
+///   color: CupertinoDynamicColor.withBrightness(
+///     color: CupertinoColors.white,
+///     darkColor: CupertinoColors.black,
+///   ),
+///   onPressed: () { },
+/// )
+/// ```
+/// {@end-tool}
+///
+/// ### Using a [CupertinoDynamicColor] from a [CupertinoTheme]
+///
+/// When referring to a [CupertinoTheme] color, generally the color will already
+/// have adapted to the ambient [BuildContext], because [CupertinoTheme.of]
+/// implicitly resolves all the colors used in the retrieved [CupertinoThemeData],
+/// before returning it.
+///
+/// {@tool snippet}
+/// The following code sample creates a [Container] with the `primaryColor` of the
+/// current theme. If `primaryColor` is a [CupertinoDynamicColor], the container
+/// will be adaptive, thanks to [CupertinoTheme.of]: it will switch to `primaryColor`'s
+/// dark variant once dark mode is turned on, and turns to primaryColor`'s high
+/// contrast variant when [MediaQueryData.highContrast] is requested in the ambient
+/// [MediaQuery], etc.
+///
+/// ```dart
+/// Container(
+///   // Container is not a Cupertino widget, but CupertinoTheme.of implicitly
+///   // resolves colors used in the retrieved CupertinoThemeData.
+///   color: CupertinoTheme.of(context).primaryColor,
+/// )
+/// ```
+/// {@end-tool}
+///
+/// ### Manually Resolving a [CupertinoDynamicColor]
+///
+/// When used to configure a non-Cupertino widget, or wrapped in an object opaque
+/// to the receiving Cupertino component, a [CupertinoDynamicColor] may need to be
+/// manually resolved using [CupertinoDynamicColor.resolve], before it can used
+/// to paint. For example, to use a custom [Border] in a [CupertinoNavigationBar],
+/// the colors used in the [Border] have to be resolved manually before being passed
+/// to [CupertinoNavigationBar]'s constructor.
+///
+/// {@tool snippet}
+///
+/// The following code samples demonstrate two cases where you have to manually
+/// resolve a [CupertinoDynamicColor].
+///
+/// ```dart
+/// CupertinoNavigationBar(
+///   // CupertinoNavigationBar does not know how to resolve colors used in
+///   // a Border class.
+///   border: Border(
+///     bottom: BorderSide(
+///       color: CupertinoDynamicColor.resolve(CupertinoColors.systemBlue, context),
+///     ),
+///   ),
+/// )
+/// ```
+///
+/// ```dart
+/// Container(
+///   // Container is not a Cupertino widget.
+///   color: CupertinoDynamicColor.resolve(CupertinoColors.systemBlue, context),
+/// )
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [CupertinoUserInterfaceLevel], an [InheritedWidget] that may affect color
+///    resolution of a [CupertinoDynamicColor].
+///  * [CupertinoTheme.of], a static method that retrieves the ambient [CupertinoThemeData],
+///    and then resolves [CupertinoDynamicColor]s used in the retrieved data.
+@immutable
+class CupertinoDynamicColor extends Color with Diagnosticable {
+  /// Creates an adaptive [Color] that changes its effective color based on the
+  /// [BuildContext] given. The default effective color is [color].
+  ///
+  /// All the colors must not be null.
+  const CupertinoDynamicColor({
+    String? debugLabel,
+    required Color color,
+    required Color darkColor,
+    required Color highContrastColor,
+    required Color darkHighContrastColor,
+    required Color elevatedColor,
+    required Color darkElevatedColor,
+    required Color highContrastElevatedColor,
+    required Color darkHighContrastElevatedColor,
+  }) : this._(
+         color,
+         color,
+         darkColor,
+         highContrastColor,
+         darkHighContrastColor,
+         elevatedColor,
+         darkElevatedColor,
+         highContrastElevatedColor,
+         darkHighContrastElevatedColor,
+         null,
+         debugLabel,
+       );
+
+  /// Creates an adaptive [Color] that changes its effective color based on the
+  /// given [BuildContext]'s brightness (from [MediaQueryData.platformBrightness]
+  /// or [CupertinoThemeData.brightness]) and accessibility contrast setting
+  /// ([MediaQueryData.highContrast]). The default effective color is [color].
+  ///
+  /// All the colors must not be null.
+  const CupertinoDynamicColor.withBrightnessAndContrast({
+    String? debugLabel,
+    required Color color,
+    required Color darkColor,
+    required Color highContrastColor,
+    required Color darkHighContrastColor,
+  }) : this(
+    debugLabel: debugLabel,
+    color: color,
+    darkColor: darkColor,
+    highContrastColor: highContrastColor,
+    darkHighContrastColor: darkHighContrastColor,
+    elevatedColor: color,
+    darkElevatedColor: darkColor,
+    highContrastElevatedColor: highContrastColor,
+    darkHighContrastElevatedColor: darkHighContrastColor,
+  );
+
+  /// Creates an adaptive [Color] that changes its effective color based on the given
+  /// [BuildContext]'s brightness (from [MediaQueryData.platformBrightness] or
+  /// [CupertinoThemeData.brightness]). The default effective color is [color].
+  ///
+  /// All the colors must not be null.
+  const CupertinoDynamicColor.withBrightness({
+    String? debugLabel,
+    required Color color,
+    required Color darkColor,
+  }) : this(
+    debugLabel: debugLabel,
+    color: color,
+    darkColor: darkColor,
+    highContrastColor: color,
+    darkHighContrastColor: darkColor,
+    elevatedColor: color,
+    darkElevatedColor: darkColor,
+    highContrastElevatedColor: color,
+    darkHighContrastElevatedColor: darkColor,
+  );
+
+  const CupertinoDynamicColor._(
+    this._effectiveColor,
+    this.color,
+    this.darkColor,
+    this.highContrastColor,
+    this.darkHighContrastColor,
+    this.elevatedColor,
+    this.darkElevatedColor,
+    this.highContrastElevatedColor,
+    this.darkHighContrastElevatedColor,
+    this._debugResolveContext,
+    this._debugLabel,
+  ) : assert(color != null),
+      assert(darkColor != null),
+      assert(highContrastColor != null),
+      assert(darkHighContrastColor != null),
+      assert(elevatedColor != null),
+      assert(darkElevatedColor != null),
+      assert(highContrastElevatedColor != null),
+      assert(darkHighContrastElevatedColor != null),
+      assert(_effectiveColor != null),
+      // The super constructor has to be called with a dummy value in order to mark
+      // this constructor const.
+      // The field `value` is overridden in the class implementation.
+      super(0);
+
+  /// The current effective color.
+  ///
+  /// Must not be null. Defaults to [color] if this [CupertinoDynamicColor] has
+  /// never been resolved.
+  final Color _effectiveColor;
+
+  @override
+  int get value => _effectiveColor.value;
+
+  final String? _debugLabel;
+
+  final Element? _debugResolveContext;
+
+  /// The color to use when the [BuildContext] implies a combination of light mode,
+  /// normal contrast, and base interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.light],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.light].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `false`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.base].
+  final Color color;
+
+  /// The color to use when the [BuildContext] implies a combination of dark mode,
+  /// normal contrast, and base interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.dark],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.dark].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `false`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.base].
+  final Color darkColor;
+
+  /// The color to use when the [BuildContext] implies a combination of light mode,
+  /// high contrast, and base interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.light],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.light].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `true`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.base].
+  final Color highContrastColor;
+
+  /// The color to use when the [BuildContext] implies a combination of dark mode,
+  /// high contrast, and base interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.dark],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.dark].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `true`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.base].
+  final Color darkHighContrastColor;
+
+  /// The color to use when the [BuildContext] implies a combination of light mode,
+  /// normal contrast, and elevated interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.light],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.light].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `false`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.elevated].
+  final Color elevatedColor;
+
+  /// The color to use when the [BuildContext] implies a combination of dark mode,
+  /// normal contrast, and elevated interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.dark],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.dark].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `false`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.elevated].
+  final Color darkElevatedColor;
+
+  /// The color to use when the [BuildContext] implies a combination of light mode,
+  /// high contrast, and elevated interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.light],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.light].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `true`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.elevated].
+  final Color highContrastElevatedColor;
+
+  /// The color to use when the [BuildContext] implies a combination of dark mode,
+  /// high contrast, and elevated interface elevation.
+  ///
+  /// In other words, this color will be the effective color of the [CupertinoDynamicColor]
+  /// after it is resolved against a [BuildContext] that:
+  /// - has a [CupertinoTheme] whose [CupertinoThemeData.brightness] is [Brightness.dark],
+  /// or a [MediaQuery] whose [MediaQueryData.platformBrightness] is [Brightness.dark].
+  /// - has a [MediaQuery] whose [MediaQueryData.highContrast] is `true`.
+  /// - has a [CupertinoUserInterfaceLevel] that indicates [CupertinoUserInterfaceLevelData.elevated].
+  final Color darkHighContrastElevatedColor;
+
+  /// Resolves the given [Color] by calling [resolveFrom].
+  ///
+  /// If the given color is already a concrete [Color], it will be returned as is.
+  /// If the given color is a [CupertinoDynamicColor], but the given [BuildContext]
+  /// lacks the dependencies required to the color resolution, the default trait
+  /// value will be used ([Brightness.light] platform brightness, normal contrast,
+  /// [CupertinoUserInterfaceLevelData.base] elevation level).
+  ///
+  /// See also:
+  ///
+  ///  * [maybeResolve], which is similar to this function, but will allow a
+  ///    null `resolvable` color.
+  static Color resolve(Color resolvable, BuildContext context) {
+    assert(context != null);
+    return (resolvable is CupertinoDynamicColor)
+      ? resolvable.resolveFrom(context)
+      : resolvable;
+  }
+
+  /// Resolves the given [Color] by calling [resolveFrom].
+  ///
+  /// If the given color is already a concrete [Color], it will be returned as is.
+  /// If the given color is null, returns null.
+  /// If the given color is a [CupertinoDynamicColor], but the given [BuildContext]
+  /// lacks the dependencies required to the color resolution, the default trait
+  /// value will be used ([Brightness.light] platform brightness, normal contrast,
+  /// [CupertinoUserInterfaceLevelData.base] elevation level).
+  ///
+  /// See also:
+  ///
+  ///  * [resolve], which is similar to this function, but returns a
+  ///    non-nullable value, and does not allow a null `resolvable` color.
+  static Color? maybeResolve(Color? resolvable, BuildContext context) {
+    if (resolvable == null)
+      return null;
+    assert(context != null);
+    return (resolvable is CupertinoDynamicColor)
+      ? resolvable.resolveFrom(context)
+      : resolvable;
+  }
+
+  bool get _isPlatformBrightnessDependent {
+    return color != darkColor
+        || elevatedColor != darkElevatedColor
+        || highContrastColor != darkHighContrastColor
+        || highContrastElevatedColor != darkHighContrastElevatedColor;
+  }
+
+  bool get _isHighContrastDependent {
+    return color != highContrastColor
+        || darkColor != darkHighContrastColor
+        || elevatedColor != highContrastElevatedColor
+        || darkElevatedColor != darkHighContrastElevatedColor;
+  }
+
+  bool get _isInterfaceElevationDependent {
+    return color != elevatedColor
+        || darkColor != darkElevatedColor
+        || highContrastColor != highContrastElevatedColor
+        || darkHighContrastColor != darkHighContrastElevatedColor;
+  }
+
+  /// Resolves this [CupertinoDynamicColor] using the provided [BuildContext].
+  ///
+  /// Calling this method will create a new [CupertinoDynamicColor] that is almost
+  /// identical to this [CupertinoDynamicColor], except the effective color is
+  /// changed to adapt to the given [BuildContext].
+  ///
+  /// For example, if the given [BuildContext] indicates the widgets in the
+  /// subtree should be displayed in dark mode (the surrounding
+  /// [CupertinoTheme]'s [CupertinoThemeData.brightness] or [MediaQuery]'s
+  /// [MediaQueryData.platformBrightness] is [Brightness.dark]), with a high
+  /// accessibility contrast (the surrounding [MediaQuery]'s
+  /// [MediaQueryData.highContrast] is `true`), and an elevated interface
+  /// elevation (the surrounding [CupertinoUserInterfaceLevel]'s `data` is
+  /// [CupertinoUserInterfaceLevelData.elevated]), the resolved
+  /// [CupertinoDynamicColor] will be the same as this [CupertinoDynamicColor],
+  /// except its effective color will be the `darkHighContrastElevatedColor`
+  /// variant from the original [CupertinoDynamicColor].
+  ///
+  /// Calling this function may create dependencies on the closest instance of some
+  /// [InheritedWidget]s that enclose the given [BuildContext]. E.g., if [darkColor]
+  /// is different from [color], this method will call [CupertinoTheme.of], and
+  /// then [MediaQuery.of] if brightness wasn't specified in the theme data retrieved
+  /// from the previous [CupertinoTheme.of] call, in an effort to determine the
+  /// brightness value.
+  ///
+  /// If any of the required dependencies are missing from the given context, the
+  /// default value of that trait will be used ([Brightness.light] platform
+  /// brightness, normal contrast, [CupertinoUserInterfaceLevelData.base] elevation
+  /// level).
+  CupertinoDynamicColor resolveFrom(BuildContext context) {
+    Brightness brightness = Brightness.light;
+    if (_isPlatformBrightnessDependent) {
+      brightness =  CupertinoTheme.maybeBrightnessOf(context) ?? Brightness.light;
+    }
+    bool isHighContrastEnabled = false;
+    if (_isHighContrastDependent) {
+      isHighContrastEnabled = MediaQuery.maybeOf(context)?.highContrast ?? false;
+    }
+
+    final CupertinoUserInterfaceLevelData level = _isInterfaceElevationDependent
+      ? CupertinoUserInterfaceLevel.maybeOf(context) ?? CupertinoUserInterfaceLevelData.base
+      : CupertinoUserInterfaceLevelData.base;
+
+    final Color resolved;
+    switch (brightness) {
+      case Brightness.light:
+        switch (level) {
+          case CupertinoUserInterfaceLevelData.base:
+            resolved = isHighContrastEnabled ? highContrastColor : color;
+            break;
+          case CupertinoUserInterfaceLevelData.elevated:
+            resolved = isHighContrastEnabled ? highContrastElevatedColor : elevatedColor;
+            break;
+        }
+        break;
+      case Brightness.dark:
+        switch (level) {
+          case CupertinoUserInterfaceLevelData.base:
+            resolved = isHighContrastEnabled ? darkHighContrastColor : darkColor;
+            break;
+          case CupertinoUserInterfaceLevelData.elevated:
+            resolved = isHighContrastEnabled ? darkHighContrastElevatedColor : darkElevatedColor;
+            break;
+        }
+    }
+
+    Element? _debugContext;
+    assert(() {
+      _debugContext = context as Element;
+      return true;
+    }());
+    return CupertinoDynamicColor._(
+      resolved,
+      color,
+      darkColor,
+      highContrastColor,
+      darkHighContrastColor,
+      elevatedColor,
+      darkElevatedColor,
+      highContrastElevatedColor,
+      darkHighContrastElevatedColor,
+      _debugContext,
+      _debugLabel,
+    );
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other))
+      return true;
+    if (other.runtimeType != runtimeType)
+      return false;
+    return other is CupertinoDynamicColor
+        && other.value == value
+        && other.color == color
+        && other.darkColor == darkColor
+        && other.highContrastColor == highContrastColor
+        && other.darkHighContrastColor == darkHighContrastColor
+        && other.elevatedColor == elevatedColor
+        && other.darkElevatedColor == darkElevatedColor
+        && other.highContrastElevatedColor == highContrastElevatedColor
+        && other.darkHighContrastElevatedColor == darkHighContrastElevatedColor;
+  }
+
+  @override
+  int get hashCode {
+    return hashValues(
+      value,
+      color,
+      darkColor,
+      highContrastColor,
+      elevatedColor,
+      darkElevatedColor,
+      darkHighContrastColor,
+      darkHighContrastElevatedColor,
+      highContrastElevatedColor,
+    );
+  }
+
+  @override
+  String toString({ DiagnosticLevel minLevel = DiagnosticLevel.info }) {
+    String toString(String name, Color color) {
+      final String marker = color == _effectiveColor ? '*' : '';
+      return '$marker$name = $color$marker';
+    }
+
+    final List<String> xs = <String>[toString('color', color),
+      if (_isPlatformBrightnessDependent) toString('darkColor', darkColor),
+      if (_isHighContrastDependent) toString('highContrastColor', highContrastColor),
+      if (_isPlatformBrightnessDependent && _isHighContrastDependent) toString('darkHighContrastColor', darkHighContrastColor),
+      if (_isInterfaceElevationDependent) toString('elevatedColor', elevatedColor),
+      if (_isPlatformBrightnessDependent && _isInterfaceElevationDependent) toString('darkElevatedColor', darkElevatedColor),
+      if (_isHighContrastDependent && _isInterfaceElevationDependent) toString('highContrastElevatedColor', highContrastElevatedColor),
+      if (_isPlatformBrightnessDependent && _isHighContrastDependent && _isInterfaceElevationDependent) toString('darkHighContrastElevatedColor', darkHighContrastElevatedColor),
+    ];
+
+    return '${_debugLabel ?? objectRuntimeType(this, 'CupertinoDynamicColor')}(${xs.join(', ')}, resolved by: ${_debugResolveContext?.widget ?? "UNRESOLVED"})';
+  }
+
+  @override
+  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
+    super.debugFillProperties(properties);
+    if (_debugLabel != null)
+      properties.add(MessageProperty('debugLabel', _debugLabel!));
+    properties.add(createCupertinoColorProperty('color', color));
+    if (_isPlatformBrightnessDependent)
+      properties.add(createCupertinoColorProperty('darkColor', darkColor));
+    if (_isHighContrastDependent)
+      properties.add(createCupertinoColorProperty('highContrastColor', highContrastColor));
+    if (_isPlatformBrightnessDependent && _isHighContrastDependent)
+      properties.add(createCupertinoColorProperty('darkHighContrastColor', darkHighContrastColor));
+    if (_isInterfaceElevationDependent)
+      properties.add(createCupertinoColorProperty('elevatedColor', elevatedColor));
+    if (_isPlatformBrightnessDependent && _isInterfaceElevationDependent)
+      properties.add(createCupertinoColorProperty('darkElevatedColor', darkElevatedColor));
+    if (_isHighContrastDependent && _isInterfaceElevationDependent)
+      properties.add(createCupertinoColorProperty('highContrastElevatedColor', highContrastElevatedColor));
+    if (_isPlatformBrightnessDependent && _isHighContrastDependent && _isInterfaceElevationDependent)
+      properties.add(createCupertinoColorProperty('darkHighContrastElevatedColor', darkHighContrastElevatedColor));
+
+    if (_debugResolveContext != null)
+      properties.add(DiagnosticsProperty<Element>('last resolved', _debugResolveContext));
+  }
+}
+
+/// Creates a diagnostics property for [CupertinoDynamicColor].
+///
+/// The [showName], [style], and [level] arguments must not be null.
+DiagnosticsProperty<Color> createCupertinoColorProperty(
+  String name,
+  Color? value, {
+  bool showName = true,
+  Object? defaultValue = kNoDefaultValue,
+  DiagnosticsTreeStyle style = DiagnosticsTreeStyle.singleLine,
+  DiagnosticLevel level = DiagnosticLevel.info,
+}) {
+  if (value is CupertinoDynamicColor) {
+    return DiagnosticsProperty<CupertinoDynamicColor>(
+      name,
+      value,
+      description: value._debugLabel,
+      showName: showName,
+      defaultValue: defaultValue,
+      style: style,
+      level: level,
+    );
+  } else {
+    return ColorProperty(
+      name,
+      value,
+      showName: showName,
+      defaultValue: defaultValue,
+      style: style,
+      level: level,
+    );
+  }
+}
diff --git a/lib/src/cupertino/constants.dart b/lib/src/cupertino/constants.dart
new file mode 100644
index 0000000..2831c50
--- /dev/null
+++ b/lib/src/cupertino/constants.dart
@@ -0,0 +1,17 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/// The minimum dimension of any interactive region according to the iOS Human
+/// Interface Guidelines.
+///
+/// This is used to avoid small regions that are hard for the user to interact
+/// with. It applies to both dimensions of a region, so a square of size
+/// kMinInteractiveDimension x kMinInteractiveDimension is the smallest
+/// acceptable region that should respond to gestures.
+///
+/// See also:
+///
+///  * [kMinInteractiveDimension]
+///  * <https://developer.apple.com/ios/human-interface-guidelines/visual-design/adaptivity-and-layout/>
+const double kMinInteractiveDimensionCupertino = 44.0;
diff --git a/lib/src/cupertino/context_menu.dart b/lib/src/cupertino/context_menu.dart
new file mode 100644
index 0000000..7ae7cc8
--- /dev/null
+++ b/lib/src/cupertino/context_menu.dart
@@ -0,0 +1,1290 @@
+// Copyright 2014 The Flutter Authors. 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:flute/ui.dart' as ui;
+import 'package:flute/gestures.dart' show kMinFlingVelocity, kLongPressTimeout;
+import 'package:flute/foundation.dart' show kIsWeb;
+import 'package:flute/rendering.dart';
+import 'package:flute/scheduler.dart';
+import 'package:flute/services.dart';
+import 'package:flute/widgets.dart';
+
+// The scale of the child at the time that the CupertinoContextMenu opens.
+// This value was eyeballed from a physical device running iOS 13.1.2.
+const double _kOpenScale = 1.1;
+
+typedef _DismissCallback = void Function(
+  BuildContext context,
+  double scale,
+  double opacity,
+);
+
+/// A function that produces the preview when the CupertinoContextMenu is open.
+///
+/// Called every time the animation value changes.
+typedef ContextMenuPreviewBuilder = Widget Function(
+  BuildContext context,
+  Animation<double> animation,
+  Widget child,
+);
+
+// A function that proxies to ContextMenuPreviewBuilder without the child.
+typedef _ContextMenuPreviewBuilderChildless = Widget Function(
+  BuildContext context,
+  Animation<double> animation,
+);
+
+// Given a GlobalKey, return the Rect of the corresponding RenderBox's
+// paintBounds in global coordinates.
+Rect _getRect(GlobalKey globalKey) {
+  assert(globalKey.currentContext != null);
+  final RenderBox renderBoxContainer = globalKey.currentContext!.findRenderObject()! as RenderBox;
+  final Offset containerOffset = renderBoxContainer.localToGlobal(
+    renderBoxContainer.paintBounds.topLeft,
+  );
+  return containerOffset & renderBoxContainer.paintBounds.size;
+}
+
+// The context menu arranges itself slightly differently based on the location
+// on the screen of [CupertinoContextMenu.child] before the
+// [CupertinoContextMenu] opens.
+enum _ContextMenuLocation {
+  center,
+  left,
+  right,
+}
+
+/// A full-screen modal route that opens when the [child] is long-pressed.
+///
+/// When open, the [CupertinoContextMenu] shows the child, or the widget returned
+/// by [previewBuilder] if given, in a large full-screen [Overlay] with a list
+/// of buttons specified by [actions]. The child/preview is placed in an
+/// [Expanded] widget so that it will grow to fill the Overlay if its size is
+/// unconstrained.
+///
+/// When closed, the CupertinoContextMenu simply displays the child as if the
+/// CupertinoContextMenu were not there. Sizing and positioning is unaffected.
+/// The menu can be closed like other [PopupRoute]s, such as by tapping the
+/// background or by calling `Navigator.pop(context)`. Unlike PopupRoute, it can
+/// also be closed by swiping downwards.
+///
+/// The [previewBuilder] parameter is most commonly used to display a slight
+/// variation of [child]. See [previewBuilder] for an example of rounding the
+/// child's corners and allowing its aspect ratio to expand, similar to the
+/// Photos app on iOS.
+///
+/// {@tool dartpad --template=stateless_widget_material_no_null_safety}
+///
+/// This sample shows a very simple CupertinoContextMenu for an empty red
+/// 100x100 Container. Simply long press on it to open.
+///
+/// ```dart imports
+/// import 'package:flute/cupertino.dart';
+/// ```
+///
+/// ```dart
+/// Widget build(BuildContext context) {
+///   return Scaffold(
+///     body: Center(
+///       child: Container(
+///         width: 100,
+///         height: 100,
+///         child: CupertinoContextMenu(
+///           child: Container(
+///             color: Colors.red,
+///           ),
+///           actions: <Widget>[
+///             CupertinoContextMenuAction(
+///               child: const Text('Action one'),
+///               onPressed: () {
+///                 Navigator.pop(context);
+///               },
+///             ),
+///             CupertinoContextMenuAction(
+///               child: const Text('Action two'),
+///               onPressed: () {
+///                 Navigator.pop(context);
+///               },
+///             ),
+///           ],
+///         ),
+///       ),
+///     ),
+///   );
+/// }
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+///  * [Apple's HIG for Context Menus](https://developer.apple.com/design/human-interface-guidelines/ios/controls/context-menus/)
+class CupertinoContextMenu extends StatefulWidget {
+  /// Create a context menu.
+  ///
+  /// [actions] is required and cannot be null or empty.
+  ///
+  /// [child] is required and cannot be null.
+  CupertinoContextMenu({
+    Key? key,
+    required this.actions,
+    required this.child,
+    this.previewBuilder,
+  }) : assert(actions != null && actions.isNotEmpty),
+       assert(child != null),
+       super(key: key);
+
+  /// The widget that can be "opened" with the [CupertinoContextMenu].
+  ///
+  /// When the [CupertinoContextMenu] is long-pressed, the menu will open and
+  /// this widget (or the widget returned by [previewBuilder], if provided) will
+  /// be moved to the new route and placed inside of an [Expanded] widget. This
+  /// allows the child to resize to fit in its place in the new route, if it
+  /// doesn't size itself.
+  ///
+  /// When the [CupertinoContextMenu] is "closed", this widget acts like a
+  /// [Container], i.e. it does not constrain its child's size or affect its
+  /// position.
+  ///
+  /// This parameter cannot be null.
+  final Widget child;
+
+  /// The actions that are shown in the menu.
+  ///
+  /// These actions are typically [CupertinoContextMenuAction]s.
+  ///
+  /// This parameter cannot be null or empty.
+  final List<Widget> actions;
+
+  /// A function that returns an alternative widget to show when the
+  /// [CupertinoContextMenu] is open.
+  ///
+  /// If not specified, [child] will be shown.
+  ///
+  /// The preview is often used to show a slight variation of the [child]. For
+  /// example, the child could be given rounded corners in the preview but have
+  /// sharp corners when in the page.
+  ///
+  /// In addition to the current [BuildContext], the function is also called
+  /// with an [Animation] and the [child]. The animation goes from 0 to 1 when
+  /// the CupertinoContextMenu opens, and from 1 to 0 when it closes, and it can
+  /// be used to animate the preview in sync with this opening and closing. The
+  /// child parameter provides access to the child displayed when the
+  /// CupertinoContextMenu is closed.
+  ///
+  /// {@tool snippet}
+  ///
+  /// Below is an example of using `previewBuilder` to show an image tile that's
+  /// similar to each tile in the iOS iPhoto app's context menu. Several of
+  /// these could be used in a GridView for a similar effect.
+  ///
+  /// When opened, the child animates to show its full aspect ratio and has
+  /// rounded corners. The larger size of the open CupertinoContextMenu allows
+  /// the FittedBox to fit the entire image, even when it has a very tall or
+  /// wide aspect ratio compared to the square of a GridView, so this animates
+  /// into view as the CupertinoContextMenu is opened. The preview is swapped in
+  /// right when the open animation begins, which includes the rounded corners.
+  ///
+  /// ```dart
+  /// CupertinoContextMenu(
+  ///   child: FittedBox(
+  ///     fit: BoxFit.cover,
+  ///     child: Image.asset('assets/photo.jpg'),
+  ///   ),
+  ///   // The FittedBox in the preview here allows the image to animate its
+  ///   // aspect ratio when the CupertinoContextMenu is animating its preview
+  ///   // widget open and closed.
+  ///   previewBuilder: (BuildContext context, Animation<double> animation, Widget child) {
+  ///     return FittedBox(
+  ///       fit: BoxFit.cover,
+  ///       // This ClipRRect rounds the corners of the image when the
+  ///       // CupertinoContextMenu is open, even though it's not rounded when
+  ///       // it's closed. It uses the given animation to animate the corners
+  ///       // in sync with the opening animation.
+  ///       child: ClipRRect(
+  ///         borderRadius: BorderRadius.circular(64.0 * animation.value),
+  ///         child: Image.asset('assets/photo.jpg'),
+  ///       ),
+  ///     );
+  ///   },
+  ///   actions: <Widget>[
+  ///     CupertinoContextMenuAction(
+  ///       child: const Text('Action one'),
+  ///       onPressed: () {},
+  ///     ),
+  ///   ],
+  /// ),
+  /// ```
+  ///
+  /// {@end-tool}
+  final ContextMenuPreviewBuilder? previewBuilder;
+
+  @override
+  _CupertinoContextMenuState createState() => _CupertinoContextMenuState();
+}
+
+class _CupertinoContextMenuState extends State<CupertinoContextMenu> with TickerProviderStateMixin {
+  final GlobalKey _childGlobalKey = GlobalKey();
+  bool _childHidden = false;
+  // Animates the child while it's opening.
+  late AnimationController _openController;
+  Rect? _decoyChildEndRect;
+  OverlayEntry? _lastOverlayEntry;
+  _ContextMenuRoute<void>? _route;
+
+  @override
+  void initState() {
+    super.initState();
+    _openController = AnimationController(
+      duration: kLongPressTimeout,
+      vsync: this,
+    );
+    _openController.addStatusListener(_onDecoyAnimationStatusChange);
+  }
+
+  // Determine the _ContextMenuLocation based on the location of the original
+  // child in the screen.
+  //
+  // The location of the original child is used to determine how to horizontally
+  // align the content of the open CupertinoContextMenu. For example, if the
+  // child is near the center of the screen, it will also appear in the center
+  // of the screen when the menu is open, and the actions will be centered below
+  // it.
+  _ContextMenuLocation get _contextMenuLocation {
+    final Rect childRect = _getRect(_childGlobalKey);
+    final double screenWidth = MediaQuery.of(context).size.width;
+
+    final double center = screenWidth / 2;
+    final bool centerDividesChild = childRect.left < center
+      && childRect.right > center;
+    final double distanceFromCenter = (center - childRect.center.dx).abs();
+    if (centerDividesChild && distanceFromCenter <= childRect.width / 4) {
+      return _ContextMenuLocation.center;
+    }
+
+    if (childRect.center.dx > center) {
+      return _ContextMenuLocation.right;
+    }
+
+    return _ContextMenuLocation.left;
+  }
+
+  // Push the new route and open the CupertinoContextMenu overlay.
+  void _openContextMenu() {
+    setState(() {
+      _childHidden = true;
+    });
+
+    _route = _ContextMenuRoute<void>(
+      actions: widget.actions,
+      barrierLabel: 'Dismiss',
+      filter: ui.ImageFilter.blur(
+        sigmaX: 5.0,
+        sigmaY: 5.0,
+      ),
+      contextMenuLocation: _contextMenuLocation,
+      previousChildRect: _decoyChildEndRect!,
+      builder: (BuildContext context, Animation<double> animation) {
+        if (widget.previewBuilder == null) {
+          return widget.child;
+        }
+        return widget.previewBuilder!(context, animation, widget.child);
+      },
+    );
+    Navigator.of(context, rootNavigator: true).push<void>(_route!);
+    _route!.animation!.addStatusListener(_routeAnimationStatusListener);
+  }
+
+  void _onDecoyAnimationStatusChange(AnimationStatus animationStatus) {
+    switch (animationStatus) {
+      case AnimationStatus.dismissed:
+        if (_route == null) {
+          setState(() {
+            _childHidden = false;
+          });
+        }
+        _lastOverlayEntry?.remove();
+        _lastOverlayEntry = null;
+        break;
+
+      case AnimationStatus.completed:
+        setState(() {
+          _childHidden = true;
+        });
+        _openContextMenu();
+        // Keep the decoy on the screen for one extra frame. We have to do this
+        // because _ContextMenuRoute renders its first frame offscreen.
+        // Otherwise there would be a visible flash when nothing is rendered for
+        // one frame.
+        SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
+          _lastOverlayEntry?.remove();
+          _lastOverlayEntry = null;
+          _openController.reset();
+        });
+        break;
+
+      default:
+        return;
+    }
+  }
+
+  // Watch for when _ContextMenuRoute is closed and return to the state where
+  // the CupertinoContextMenu just behaves as a Container.
+  void _routeAnimationStatusListener(AnimationStatus status) {
+    if (status != AnimationStatus.dismissed) {
+      return;
+    }
+    setState(() {
+      _childHidden = false;
+    });
+    _route!.animation!.removeStatusListener(_routeAnimationStatusListener);
+    _route = null;
+  }
+
+  void _onTap() {
+    if (_openController.isAnimating && _openController.value < 0.5) {
+      _openController.reverse();
+    }
+  }
+
+  void _onTapCancel() {
+    if (_openController.isAnimating && _openController.value < 0.5) {
+      _openController.reverse();
+    }
+  }
+
+  void _onTapUp(TapUpDetails details) {
+    if (_openController.isAnimating && _openController.value < 0.5) {
+      _openController.reverse();
+    }
+  }
+
+  void _onTapDown(TapDownDetails details) {
+    setState(() {
+      _childHidden = true;
+    });
+
+    final Rect childRect = _getRect(_childGlobalKey);
+    _decoyChildEndRect = Rect.fromCenter(
+      center: childRect.center,
+      width: childRect.width * _kOpenScale,
+      height: childRect.height * _kOpenScale,
+    );
+
+    // Create a decoy child in an overlay directly on top of the original child.
+    // TODO(justinmc): There is a known inconsistency with native here, due to
+    // doing the bounce animation using a decoy in the top level Overlay. The
+    // decoy will pop on top of the AppBar if the child is partially behind it,
+    // such as a top item in a partially scrolled view. However, if we don't use
+    // an overlay, then the decoy will appear behind its neighboring widget when
+    // it expands. This may be solvable by adding a widget to Scaffold that's
+    // underneath the AppBar.
+    _lastOverlayEntry = OverlayEntry(
+      opaque: false,
+      builder: (BuildContext context) {
+        return _DecoyChild(
+          beginRect: childRect,
+          child: widget.child,
+          controller: _openController,
+          endRect: _decoyChildEndRect,
+        );
+      },
+    );
+    Overlay.of(context)!.insert(_lastOverlayEntry!);
+    _openController.forward();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      onTapCancel: _onTapCancel,
+      onTapDown: _onTapDown,
+      onTapUp: _onTapUp,
+      onTap: _onTap,
+      child: TickerMode(
+        enabled: !_childHidden,
+        child: Opacity(
+          key: _childGlobalKey,
+          opacity: _childHidden ? 0.0 : 1.0,
+          child: widget.child,
+        ),
+      ),
+    );
+  }
+
+  @override
+  void dispose() {
+    _openController.dispose();
+    super.dispose();
+  }
+}
+
+// A floating copy of the CupertinoContextMenu's child.
+//
+// When the child is pressed, but before the CupertinoContextMenu opens, it does
+// a "bounce" animation where it shrinks and then grows. This is implemented
+// by hiding the original child and placing _DecoyChild on top of it in an
+// Overlay. The use of an Overlay allows the _DecoyChild to appear on top of
+// siblings of the original child.
+class _DecoyChild extends StatefulWidget {
+  const _DecoyChild({
+    Key? key,
+    this.beginRect,
+    required this.controller,
+    this.endRect,
+    this.child,
+  }) : super(key: key);
+
+  final Rect? beginRect;
+  final AnimationController controller;
+  final Rect? endRect;
+  final Widget? child;
+
+  @override
+  _DecoyChildState createState() => _DecoyChildState();
+}
+
+class _DecoyChildState extends State<_DecoyChild> with TickerProviderStateMixin {
+  // TODO(justinmc): Dark mode support.
+  // See https://github.com/flutter/flutter/issues/43211.
+  static const Color _lightModeMaskColor = Color(0xFF888888);
+  static const Color _masklessColor = Color(0xFFFFFFFF);
+
+  final GlobalKey _childGlobalKey = GlobalKey();
+  late Animation<Color> _mask;
+  late Animation<Rect?> _rect;
+
+  @override
+  void initState() {
+    super.initState();
+    // Change the color of the child during the initial part of the decoy bounce
+    // animation. The interval was eyeballed from a physical iOS 13.1.2 device.
+    _mask = _OnOffAnimation<Color>(
+      controller: widget.controller,
+      onValue: _lightModeMaskColor,
+      offValue: _masklessColor,
+      intervalOn: 0.0,
+      intervalOff: 0.5,
+    );
+
+    final Rect midRect =  widget.beginRect!.deflate(
+      widget.beginRect!.width * (_kOpenScale - 1.0) / 2,
+    );
+    _rect = TweenSequence<Rect?>(<TweenSequenceItem<Rect?>>[
+      TweenSequenceItem<Rect?>(
+        tween: RectTween(
+          begin: widget.beginRect,
+          end: midRect,
+        ).chain(CurveTween(curve: Curves.easeInOutCubic)),
+        weight: 1.0,
+      ),
+      TweenSequenceItem<Rect?>(
+        tween: RectTween(
+          begin: midRect,
+          end: widget.endRect,
+        ).chain(CurveTween(curve: Curves.easeOutCubic)),
+        weight: 1.0,
+      ),
+    ]).animate(widget.controller);
+    _rect.addListener(_rectListener);
+  }
+
+  // Listen to the _rect animation and vibrate when it reaches the halfway point
+  // and switches from animating down to up.
+  void _rectListener() {
+    if (widget.controller.value < 0.5) {
+      return;
+    }
+    HapticFeedback.selectionClick();
+    _rect.removeListener(_rectListener);
+  }
+
+  @override
+  void dispose() {
+    _rect.removeListener(_rectListener);
+    super.dispose();
+  }
+
+  Widget _buildAnimation(BuildContext context, Widget? child) {
+    final Color color = widget.controller.status == AnimationStatus.reverse
+      ? _masklessColor
+      : _mask.value;
+    return Positioned.fromRect(
+      rect: _rect.value!,
+      // TODO(justinmc): When ShaderMask is supported on web, remove this
+      // conditional and use ShaderMask everywhere.
+      // https://github.com/flutter/flutter/issues/52967.
+      child: kIsWeb
+          ? Container(key: _childGlobalKey, child: widget.child)
+          : ShaderMask(
+            key: _childGlobalKey,
+            shaderCallback: (Rect bounds) {
+              return LinearGradient(
+                begin: Alignment.topLeft,
+                end: Alignment.bottomRight,
+                colors: <Color>[color, color],
+              ).createShader(bounds);
+            },
+            child: widget.child,
+          ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Stack(
+      children: <Widget>[
+        AnimatedBuilder(
+          builder: _buildAnimation,
+          animation: widget.controller,
+        ),
+      ],
+    );
+  }
+}
+
+// The open CupertinoContextMenu modal.
+class _ContextMenuRoute<T> extends PopupRoute<T> {
+  // Build a _ContextMenuRoute.
+  _ContextMenuRoute({
+    required List<Widget> actions,
+    required _ContextMenuLocation contextMenuLocation,
+    this.barrierLabel,
+    _ContextMenuPreviewBuilderChildless? builder,
+    ui.ImageFilter? filter,
+    required Rect previousChildRect,
+    RouteSettings? settings,
+  }) : assert(actions != null && actions.isNotEmpty),
+       assert(contextMenuLocation != null),
+       _actions = actions,
+       _builder = builder,
+       _contextMenuLocation = contextMenuLocation,
+       _previousChildRect = previousChildRect,
+       super(
+         filter: filter,
+         settings: settings,
+       );
+
+  // Barrier color for a Cupertino modal barrier.
+  static const Color _kModalBarrierColor = Color(0x6604040F);
+  // The duration of the transition used when a modal popup is shown. Eyeballed
+  // from a physical device running iOS 13.1.2.
+  static const Duration _kModalPopupTransitionDuration =
+    Duration(milliseconds: 335);
+
+  final List<Widget> _actions;
+  final _ContextMenuPreviewBuilderChildless? _builder;
+  final GlobalKey _childGlobalKey = GlobalKey();
+  final _ContextMenuLocation _contextMenuLocation;
+  bool _externalOffstage = false;
+  bool _internalOffstage = false;
+  Orientation? _lastOrientation;
+  // The Rect of the child at the moment that the CupertinoContextMenu opens.
+  final Rect _previousChildRect;
+  double? _scale = 1.0;
+  final GlobalKey _sheetGlobalKey = GlobalKey();
+
+  static final CurveTween _curve = CurveTween(
+    curve: Curves.easeOutBack,
+  );
+  static final CurveTween _curveReverse = CurveTween(
+    curve: Curves.easeInBack,
+  );
+  static final RectTween _rectTween = RectTween();
+  static final Animatable<Rect?> _rectAnimatable = _rectTween.chain(_curve);
+  static final RectTween _rectTweenReverse = RectTween();
+  static final Animatable<Rect?> _rectAnimatableReverse = _rectTweenReverse
+    .chain(
+      _curveReverse,
+    );
+  static final RectTween _sheetRectTween = RectTween();
+  final Animatable<Rect?> _sheetRectAnimatable = _sheetRectTween.chain(
+    _curve,
+  );
+  final Animatable<Rect?> _sheetRectAnimatableReverse = _sheetRectTween.chain(
+    _curveReverse,
+  );
+  static final Tween<double> _sheetScaleTween = Tween<double>();
+  static final Animatable<double> _sheetScaleAnimatable = _sheetScaleTween
+    .chain(
+      _curve,
+    );
+  static final Animatable<double> _sheetScaleAnimatableReverse =
+    _sheetScaleTween.chain(
+      _curveReverse,
+    );
+  final Tween<double> _opacityTween = Tween<double>(begin: 0.0, end: 1.0);
+  late Animation<double> _sheetOpacity;
+
+  @override
+  final String? barrierLabel;
+
+  @override
+  Color get barrierColor => _kModalBarrierColor;
+
+  @override
+  bool get barrierDismissible => true;
+
+  @override
+  bool get semanticsDismissible => false;
+
+  @override
+  Duration get transitionDuration => _kModalPopupTransitionDuration;
+
+  // Getting the RenderBox doesn't include the scale from the Transform.scale,
+  // so it's manually accounted for here.
+  static Rect _getScaledRect(GlobalKey globalKey, double scale) {
+    final Rect childRect = _getRect(globalKey);
+    final Size sizeScaled = childRect.size * scale;
+    final Offset offsetScaled = Offset(
+      childRect.left + (childRect.size.width - sizeScaled.width) / 2,
+      childRect.top + (childRect.size.height - sizeScaled.height) / 2,
+    );
+    return offsetScaled & sizeScaled;
+  }
+
+  // Get the alignment for the _ContextMenuSheet's Transform.scale based on the
+  // contextMenuLocation.
+  static AlignmentDirectional getSheetAlignment(_ContextMenuLocation contextMenuLocation) {
+    switch (contextMenuLocation) {
+      case _ContextMenuLocation.center:
+        return AlignmentDirectional.topCenter;
+      case _ContextMenuLocation.right:
+        return AlignmentDirectional.topEnd;
+      case _ContextMenuLocation.left:
+        return AlignmentDirectional.topStart;
+    }
+  }
+
+  // The place to start the sheetRect animation from.
+  static Rect _getSheetRectBegin(Orientation? orientation, _ContextMenuLocation contextMenuLocation, Rect childRect, Rect sheetRect) {
+    switch (contextMenuLocation) {
+      case _ContextMenuLocation.center:
+        final Offset target = orientation == Orientation.portrait
+          ? childRect.bottomCenter
+          : childRect.topCenter;
+        final Offset centered = target - Offset(sheetRect.width / 2, 0.0);
+        return centered & sheetRect.size;
+      case _ContextMenuLocation.right:
+        final Offset target = orientation == Orientation.portrait
+          ? childRect.bottomRight
+          : childRect.topRight;
+        return (target - Offset(sheetRect.width, 0.0)) & sheetRect.size;
+      case _ContextMenuLocation.left:
+        final Offset target = orientation == Orientation.portrait
+          ? childRect.bottomLeft
+          : childRect.topLeft;
+        return target & sheetRect.size;
+    }
+  }
+
+  void _onDismiss(BuildContext context, double scale, double opacity) {
+    _scale = scale;
+    _opacityTween.end = opacity;
+    _sheetOpacity = _opacityTween.animate(CurvedAnimation(
+      parent: animation!,
+      curve: const Interval(0.9, 1.0),
+    ));
+    Navigator.of(context).pop();
+  }
+
+  // Take measurements on the child and _ContextMenuSheet and update the
+  // animation tweens to match.
+  void _updateTweenRects() {
+    final Rect childRect = _scale == null
+      ? _getRect(_childGlobalKey)
+      : _getScaledRect(_childGlobalKey, _scale!);
+    _rectTween.begin = _previousChildRect;
+    _rectTween.end = childRect;
+
+    // When opening, the transition happens from the end of the child's bounce
+    // animation to the final state. When closing, it goes from the final state
+    // to the original position before the bounce.
+    final Rect childRectOriginal = Rect.fromCenter(
+      center: _previousChildRect.center,
+      width: _previousChildRect.width / _kOpenScale,
+      height: _previousChildRect.height / _kOpenScale,
+    );
+
+    final Rect sheetRect = _getRect(_sheetGlobalKey);
+    final Rect sheetRectBegin = _getSheetRectBegin(
+      _lastOrientation,
+      _contextMenuLocation,
+      childRectOriginal,
+      sheetRect,
+    );
+    _sheetRectTween.begin = sheetRectBegin;
+    _sheetRectTween.end = sheetRect;
+    _sheetScaleTween.begin = 0.0;
+    _sheetScaleTween.end = _scale;
+
+    _rectTweenReverse.begin = childRectOriginal;
+    _rectTweenReverse.end = childRect;
+  }
+
+  void _setOffstageInternally() {
+    super.offstage = _externalOffstage || _internalOffstage;
+    // It's necessary to call changedInternalState to get the backdrop to
+    // update.
+    changedInternalState();
+  }
+
+  @override
+  bool didPop(T? result) {
+    _updateTweenRects();
+    return super.didPop(result);
+  }
+
+  @override
+  set offstage(bool value) {
+    _externalOffstage = value;
+    _setOffstageInternally();
+  }
+
+  @override
+  TickerFuture didPush() {
+    _internalOffstage = true;
+    _setOffstageInternally();
+
+    // Render one frame offstage in the final position so that we can take
+    // measurements of its layout and then animate to them.
+    SchedulerBinding.instance!.addPostFrameCallback((Duration _) {
+      _updateTweenRects();
+      _internalOffstage = false;
+      _setOffstageInternally();
+    });
+    return super.didPush();
+  }
+
+  @override
+  Animation<double> createAnimation() {
+    final Animation<double> animation = super.createAnimation();
+    _sheetOpacity = _opacityTween.animate(CurvedAnimation(
+      parent: animation,
+      curve: Curves.linear,
+    ));
+    return animation;
+  }
+
+  @override
+  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
+    // This is usually used to build the "page", which is then passed to
+    // buildTransitions as child, the idea being that buildTransitions will
+    // animate the entire page into the scene. In the case of _ContextMenuRoute,
+    // two individual pieces of the page are animated into the scene in
+    // buildTransitions, and a Container is returned here.
+    return Container();
+  }
+
+  @override
+  Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
+    return OrientationBuilder(
+      builder: (BuildContext context, Orientation orientation) {
+        _lastOrientation = orientation;
+
+        // While the animation is running, render everything in a Stack so that
+        // they're movable.
+        if (!animation.isCompleted) {
+          final bool reverse = animation.status == AnimationStatus.reverse;
+          final Rect rect = reverse
+            ? _rectAnimatableReverse.evaluate(animation)!
+            : _rectAnimatable.evaluate(animation)!;
+          final Rect sheetRect = reverse
+            ? _sheetRectAnimatableReverse.evaluate(animation)!
+            : _sheetRectAnimatable.evaluate(animation)!;
+          final double sheetScale = reverse
+            ? _sheetScaleAnimatableReverse.evaluate(animation)
+            : _sheetScaleAnimatable.evaluate(animation);
+          return Stack(
+            children: <Widget>[
+              Positioned.fromRect(
+                rect: sheetRect,
+                child: Opacity(
+                  opacity: _sheetOpacity.value,
+                  child: Transform.scale(
+                    alignment: getSheetAlignment(_contextMenuLocation),
+                    scale: sheetScale,
+                    child: _ContextMenuSheet(
+                      key: _sheetGlobalKey,
+                      actions: _actions,
+                      contextMenuLocation: _contextMenuLocation,
+                      orientation: orientation,
+                    ),
+                  ),
+                ),
+              ),
+              Positioned.fromRect(
+                key: _childGlobalKey,
+                rect: rect,
+                child: _builder!(context, animation),
+              ),
+            ],
+          );
+        }
+
+        // When the animation is done, just render everything in a static layout
+        // in the final position.
+        return _ContextMenuRouteStatic(
+          actions: _actions,
+          child: _builder!(context, animation),
+          childGlobalKey: _childGlobalKey,
+          contextMenuLocation: _contextMenuLocation,
+          onDismiss: _onDismiss,
+          orientation: orientation,
+          sheetGlobalKey: _sheetGlobalKey,
+        );
+      },
+    );
+  }
+}
+
+// The final state of the _ContextMenuRoute after animating in and before
+// animating out.
+class _ContextMenuRouteStatic extends StatefulWidget {
+  const _ContextMenuRouteStatic({
+    Key? key,
+    this.actions,
+    required this.child,
+    this.childGlobalKey,
+    required this.contextMenuLocation,
+    this.onDismiss,
+    required this.orientation,
+    this.sheetGlobalKey,
+  }) : assert(contextMenuLocation != null),
+       assert(orientation != null),
+       super(key: key);
+
+  final List<Widget>? actions;
+  final Widget child;
+  final GlobalKey? childGlobalKey;
+  final _ContextMenuLocation contextMenuLocation;
+  final _DismissCallback? onDismiss;
+  final Orientation orientation;
+  final GlobalKey? sheetGlobalKey;
+
+  @override
+  _ContextMenuRouteStaticState createState() => _ContextMenuRouteStaticState();
+}
+
+class _ContextMenuRouteStaticState extends State<_ContextMenuRouteStatic> with TickerProviderStateMixin {
+  // The child is scaled down as it is dragged down until it hits this minimum
+  // value.
+  static const double _kMinScale = 0.8;
+  // The CupertinoContextMenuSheet disappears at this scale.
+  static const double _kSheetScaleThreshold = 0.9;
+  static const double _kPadding = 20.0;
+  static const double _kDamping = 400.0;
+  static const Duration _kMoveControllerDuration = Duration(milliseconds: 600);
+
+  late Offset _dragOffset;
+  double _lastScale = 1.0;
+  late AnimationController _moveController;
+  late AnimationController _sheetController;
+  late Animation<Offset> _moveAnimation;
+  late Animation<double> _sheetScaleAnimation;
+  late Animation<double> _sheetOpacityAnimation;
+
+  // The scale of the child changes as a function of the distance it is dragged.
+  static double _getScale(Orientation orientation, double maxDragDistance, double dy) {
+    final double dyDirectional = dy <= 0.0 ? dy : -dy;
+    return math.max(
+      _kMinScale,
+      (maxDragDistance + dyDirectional) / maxDragDistance,
+    );
+  }
+
+  void _onPanStart(DragStartDetails details) {
+    _moveController.value = 1.0;
+    _setDragOffset(Offset.zero);
+  }
+
+  void _onPanUpdate(DragUpdateDetails details) {
+    _setDragOffset(_dragOffset + details.delta);
+  }
+
+  void _onPanEnd(DragEndDetails details) {
+    // If flung, animate a bit before handling the potential dismiss.
+    if (details.velocity.pixelsPerSecond.dy.abs() >= kMinFlingVelocity) {
+      final bool flingIsAway = details.velocity.pixelsPerSecond.dy > 0;
+      final double finalPosition = flingIsAway
+        ? _moveAnimation.value.dy + 100.0
+        : 0.0;
+
+      if (flingIsAway && _sheetController.status != AnimationStatus.forward) {
+        _sheetController.forward();
+      } else if (!flingIsAway && _sheetController.status != AnimationStatus.reverse) {
+        _sheetController.reverse();
+      }
+
+      _moveAnimation = Tween<Offset>(
+        begin: Offset(0.0, _moveAnimation.value.dy),
+        end: Offset(0.0, finalPosition),
+      ).animate(_moveController);
+      _moveController.reset();
+      _moveController.duration = const Duration(
+        milliseconds: 64,
+      );
+      _moveController.forward();
+      _moveController.addStatusListener(_flingStatusListener);
+      return;
+    }
+
+    // Dismiss if the drag is enough to scale down all the way.
+    if (_lastScale == _kMinScale) {
+      widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
+      return;
+    }
+
+    // Otherwise animate back home.
+    _moveController.addListener(_moveListener);
+    _moveController.reverse();
+  }
+
+  void _moveListener() {
+    // When the scale passes the threshold, animate the sheet back in.
+    if (_lastScale > _kSheetScaleThreshold) {
+      _moveController.removeListener(_moveListener);
+      if (_sheetController.status != AnimationStatus.dismissed) {
+        _sheetController.reverse();
+      }
+    }
+  }
+
+  void _flingStatusListener(AnimationStatus status) {
+    if (status != AnimationStatus.completed) {
+      return;
+    }
+
+    // Reset the duration back to its original value.
+    _moveController.duration = _kMoveControllerDuration;
+
+    _moveController.removeStatusListener(_flingStatusListener);
+    // If it was a fling back to the start, it has reset itself, and it should
+    // not be dismissed.
+    if (_moveAnimation.value.dy == 0.0) {
+      return;
+    }
+    widget.onDismiss!(context, _lastScale, _sheetOpacityAnimation.value);
+  }
+
+  Alignment _getChildAlignment(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
+    switch (contextMenuLocation) {
+      case _ContextMenuLocation.center:
+        return orientation == Orientation.portrait
+          ? Alignment.bottomCenter
+          : Alignment.topRight;
+      case _ContextMenuLocation.right:
+        return orientation == Orientation.portrait
+          ? Alignment.bottomCenter
+          : Alignment.topLeft;
+      case _ContextMenuLocation.left:
+        return orientation == Orientation.portrait
+          ? Alignment.bottomCenter
+          : Alignment.topRight;
+    }
+  }
+
+  void _setDragOffset(Offset dragOffset) {
+    // Allow horizontal and negative vertical movement, but damp it.
+    final double endX = _kPadding * dragOffset.dx / _kDamping;
+    final double endY = dragOffset.dy >= 0.0
+      ? dragOffset.dy
+      : _kPadding * dragOffset.dy / _kDamping;
+    setState(() {
+      _dragOffset = dragOffset;
+      _moveAnimation = Tween<Offset>(
+        begin: Offset.zero,
+        end: Offset(
+          endX.clamp(-_kPadding, _kPadding),
+          endY,
+        ),
+      ).animate(
+        CurvedAnimation(
+          parent: _moveController,
+          curve: Curves.elasticIn,
+        ),
+      );
+
+      // Fade the _ContextMenuSheet out or in, if needed.
+      if (_lastScale <= _kSheetScaleThreshold
+          && _sheetController.status != AnimationStatus.forward
+          && _sheetScaleAnimation.value != 0.0) {
+        _sheetController.forward();
+      } else if (_lastScale > _kSheetScaleThreshold
+          && _sheetController.status != AnimationStatus.reverse
+          && _sheetScaleAnimation.value != 1.0) {
+        _sheetController.reverse();
+      }
+    });
+  }
+
+  // The order and alignment of the _ContextMenuSheet and the child depend on
+  // both the orientation of the screen as well as the position on the screen of
+  // the original child.
+  List<Widget> _getChildren(Orientation orientation, _ContextMenuLocation contextMenuLocation) {
+    final Expanded child = Expanded(
+      child: Align(
+        alignment: _getChildAlignment(
+          widget.orientation,
+          widget.contextMenuLocation,
+        ),
+        child: AnimatedBuilder(
+          animation: _moveController,
+          builder: _buildChildAnimation,
+          child: widget.child,
+        ),
+      ),
+    );
+    const SizedBox spacer = SizedBox(
+      width: _kPadding,
+      height: _kPadding,
+    );
+    final Expanded sheet = Expanded(
+      child: AnimatedBuilder(
+        animation: _sheetController,
+        builder: _buildSheetAnimation,
+        child: _ContextMenuSheet(
+          key: widget.sheetGlobalKey,
+          actions: widget.actions!,
+          contextMenuLocation: widget.contextMenuLocation,
+          orientation: widget.orientation,
+        ),
+      ),
+    );
+
+    switch (contextMenuLocation) {
+      case _ContextMenuLocation.center:
+        return <Widget>[child, spacer, sheet];
+      case _ContextMenuLocation.right:
+        return orientation == Orientation.portrait
+          ? <Widget>[child, spacer, sheet]
+          : <Widget>[sheet, spacer, child];
+      case _ContextMenuLocation.left:
+        return <Widget>[child, spacer, sheet];
+    }
+  }
+
+  // Build the animation for the _ContextMenuSheet.
+  Widget _buildSheetAnimation(BuildContext context, Widget? child) {
+    return Transform.scale(
+      alignment: _ContextMenuRoute.getSheetAlignment(widget.contextMenuLocation),
+      scale: _sheetScaleAnimation.value,
+      child: Opacity(
+        opacity: _sheetOpacityAnimation.value,
+        child: child,
+      ),
+    );
+  }
+
+  // Build the animation for the child.
+  Widget _buildChildAnimation(BuildContext context, Widget? child) {
+    _lastScale = _getScale(
+      widget.orientation,
+      MediaQuery.of(context).size.height,
+      _moveAnimation.value.dy,
+    );
+    return Transform.scale(
+      key: widget.childGlobalKey,
+      scale: _lastScale,
+      child: child,
+    );
+  }
+
+  // Build the animation for the overall draggable dismissible content.
+  Widget _buildAnimation(BuildContext context, Widget? child) {
+    return Transform.translate(
+      offset: _moveAnimation.value,
+      child: child,
+    );
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    _moveController = AnimationController(
+      duration: _kMoveControllerDuration,
+      value: 1.0,
+      vsync: this,
+    );
+    _sheetController = AnimationController(
+      duration: const Duration(milliseconds: 100),
+      reverseDuration: const Duration(milliseconds: 300),
+      vsync: this,
+    );
+    _sheetScaleAnimation = Tween<double>(
+      begin: 1.0,
+      end: 0.0,
+    ).animate(
+      CurvedAnimation(
+        parent: _sheetController,
+        curve: Curves.linear,
+        reverseCurve: Curves.easeInBack,
+      ),
+    );
+    _sheetOpacityAnimation = Tween<double>(
+      begin: 1.0,
+      end: 0.0,
+    ).animate(_sheetController);
+    _setDragOffset(Offset.zero);
+  }
+
+  @override
+  void dispose() {
+    _moveController.dispose();
+    _sheetController.dispose();
+    super.dispose();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final List<Widget> children = _getChildren(
+      widget.orientation,
+      widget.contextMenuLocation,
+    );
+
+    return SafeArea(
+      child: Padding(
+        padding: const EdgeInsets.all(_kPadding),
+        child: Align(
+          alignment: Alignment.topLeft,
+          child: GestureDetector(
+            onPanEnd: _onPanEnd,
+            onPanStart: _onPanStart,
+            onPanUpdate: _onPanUpdate,
+            child: AnimatedBuilder(
+              animation: _moveController,
+              builder: _buildAnimation,
+              child: widget.orientation == Orientation.portrait
+                ? Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: children,
+                )
+                : Row(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: children,
+                ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
+
+// The menu that displays when CupertinoContextMenu is open. It consists of a
+// list of actions that are typically CupertinoContextMenuActions.
+class _ContextMenuSheet extends StatelessWidget {
+  _ContextMenuSheet({
+    Key? key,
+    required this.actions,
+    required _ContextMenuLocation contextMenuLocation,
+    required Orientation orientation,
+  }) : assert(actions != null && actions.isNotEmpty),
+       assert(contextMenuLocation != null),
+       assert(orientation != null),
+       _contextMenuLocation = contextMenuLocation,
+       _orientation = orientation,
+       super(key: key);
+
+  final List<Widget> actions;
+  final _ContextMenuLocation _contextMenuLocation;
+  final Orientation _orientation;
+
+  // Get the children, whose order depends on orientation and
+  // contextMenuLocation.
+  List<Widget> get children {
+    final Flexible menu = Flexible(
+      fit: FlexFit.tight,
+      flex: 2,
+      child: IntrinsicHeight(
+        child: ClipRRect(
+          borderRadius: BorderRadius.circular(13.0),
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.stretch,
+            children: actions,
+          ),
+        ),
+      ),
+    );
+
+    switch (_contextMenuLocation) {
+      case _ContextMenuLocation.center:
+        return _orientation == Orientation.portrait
+          ? <Widget>[
+            const Spacer(
+              flex: 1,
+            ),
+            menu,
+            const Spacer(
+              flex: 1,
+            ),
+          ]
+        : <Widget>[
+            menu,
+            const Spacer(
+              flex: 1,
+            ),
+          ];
+      case _ContextMenuLocation.right:
+        return <Widget>[
+          const Spacer(
+            flex: 1,
+          ),
+          menu,
+        ];
+      case _ContextMenuLocation.left:
+        return <Widget>[
+          menu,
+          const Spacer(
+            flex: 1,
+          ),
+        ];
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Row(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: children,
+    );
+  }
+}
+
+// An animation that switches between two colors.
+//
+// The transition is immediate, so there are no intermediate values or
+// interpolation. The color switches from offColor to onColor and back to
+// offColor at the times given by intervalOn and intervalOff.
+class _OnOffAnimation<T> extends CompoundAnimation<T> {
+  _OnOffAnimation({
+    required AnimationController controller,
+    required T onValue,
+    required T offValue,
+    required double intervalOn,
+    required double intervalOff,
+  }) : _offValue = offValue,
+       assert(intervalOn >= 0.0 && intervalOn <= 1.0),
+       assert(intervalOff >= 0.0 && intervalOff <= 1.0),
+       assert(intervalOn <= intervalOff),
+       super(
+        first: Tween<T>(begin: offValue, end: onValue).animate(
+          CurvedAnimation(
+            parent: controller,
+            curve: Interval(intervalOn, intervalOn),
+          ),
+        ),
+        next: Tween<T>(begin: onValue, end: offValue).animate(
+          CurvedAnimation(
+            parent: controller,
+            curve: Interval(intervalOff, intervalOff),
+          ),
+        ),
+       );
+
+  final T _offValue;
+
+  @override
+  T get value => next.value == _offValue ? next.value : first.value;
+}
diff --git a/lib/src/cupertino/context_menu_action.dart b/lib/src/cupertino/context_menu_action.dart
new file mode 100644
index 0000000..51abfdd
--- /dev/null
+++ b/lib/src/cupertino/context_menu_action.dart
@@ -0,0 +1,148 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/rendering.dart';
+import 'package:flute/widgets.dart';
+import 'colors.dart';
+
+/// A button in a _ContextMenuSheet.
+///
+/// A typical use case is to pass a [Text] as the [child] here, but be sure to
+/// use [TextOverflow.ellipsis] for the [Text.overflow] field if the text may be
+/// long, as without it the text will wrap to the next line.
+class CupertinoContextMenuAction extends StatefulWidget {
+  /// Construct a CupertinoContextMenuAction.
+  const CupertinoContextMenuAction({
+    Key? key,
+    required this.child,
+    this.isDefaultAction = false,
+    this.isDestructiveAction = false,
+    this.onPressed,
+    this.trailingIcon,
+  }) : assert(child != null),
+       assert(isDefaultAction != null),
+       assert(isDestructiveAction != null),
+       super(key: key);
+
+  /// The widget that will be placed inside the action.
+  final Widget child;
+
+  /// Indicates whether this action should receive the style of an emphasized,
+  /// default action.
+  final bool isDefaultAction;
+
+  /// Indicates whether this action should receive the style of a destructive
+  /// action.
+  final bool isDestructiveAction;
+
+  /// Called when the action is pressed.
+  final VoidCallback? onPressed;
+
+  /// An optional icon to display to the right of the child.
+  ///
+  /// Will be colored in the same way as the [TextStyle] used for [child] (for
+  /// example, if using [isDestructiveAction]).
+  final IconData? trailingIcon;
+
+  @override
+  _CupertinoContextMenuActionState createState() => _CupertinoContextMenuActionState();
+}
+
+class _CupertinoContextMenuActionState extends State<CupertinoContextMenuAction> {
+  static const Color _kBackgroundColor = Color(0xFFEEEEEE);
+  static const Color _kBackgroundColorPressed = Color(0xFFDDDDDD);
+  static const double _kButtonHeight = 56.0;
+  static const TextStyle _kActionSheetActionStyle = TextStyle(
+    fontFamily: '.SF UI Text',
+    inherit: false,
+    fontSize: 20.0,
+    fontWeight: FontWeight.w400,
+    color: CupertinoColors.black,
+    textBaseline: TextBaseline.alphabetic,
+  );
+
+  final GlobalKey _globalKey = GlobalKey();
+  bool _isPressed = false;
+
+  void onTapDown(TapDownDetails details) {
+    setState(() {
+      _isPressed = true;
+    });
+  }
+
+  void onTapUp(TapUpDetails details) {
+    setState(() {
+      _isPressed = false;
+    });
+  }
+
+  void onTapCancel() {
+    setState(() {
+      _isPressed = false;
+    });
+  }
+
+  TextStyle get _textStyle {
+    if (widget.isDefaultAction) {
+      return _kActionSheetActionStyle.copyWith(
+        fontWeight: FontWeight.w600,
+      );
+    }
+    if (widget.isDestructiveAction) {
+      return _kActionSheetActionStyle.copyWith(
+        color: CupertinoColors.destructiveRed,
+      );
+    }
+    return _kActionSheetActionStyle;
+  }
+
+
+  @override
+  Widget build(BuildContext context) {
+    return GestureDetector(
+      key: _globalKey,
+      onTapDown: onTapDown,
+      onTapUp: onTapUp,
+      onTapCancel: onTapCancel,
+      onTap: widget.onPressed,
+      behavior: HitTestBehavior.opaque,
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(
+          minHeight: _kButtonHeight,
+        ),
+        child: Semantics(
+          button: true,
+          child: Container(
+            decoration: BoxDecoration(
+              color: _isPressed ? _kBackgroundColorPressed : _kBackgroundColor,
+              border: const Border(
+                bottom: BorderSide(width: 1.0, color: _kBackgroundColorPressed),
+              ),
+            ),
+            padding: const EdgeInsets.symmetric(
+              vertical: 16.0,
+              horizontal: 10.0,
+            ),
+            child: DefaultTextStyle(
+              style: _textStyle,
+              child: Row(
+                mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                children: <Widget>[
+                  Flexible(
+                    child: widget.child,
+                  ),
+                  if (widget.trailingIcon != null)
+                    Icon(
+                      widget.trailingIcon,
+                      color: _textStyle.color,
+                    ),
+                ],
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+}
diff --git a/lib/src/cupertino/date_picker.dart b/lib/src/cupertino/date_picker.dart
new file mode 100644
index 0000000..fc14d12
--- /dev/null
+++ b/lib/src/cupertino/date_picker.dart
@@ -0,0 +1,2105 @@
+// Copyright 2014 The Flutter Authors. 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:flute/ui.dart' hide TextStyle;
+
+import 'package:flute/scheduler.dart';
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'localizations.dart';
+import 'picker.dart';
+import 'theme.dart';
+
+// Values derived from https://developer.apple.com/design/resources/ and on iOS
+// simulators with "Debug View Hierarchy".
+const double _kItemExtent = 32.0;
+// From the picker's intrinsic content size constraint.
+const double _kPickerWidth = 320.0;
+const double _kPickerHeight = 216.0;
+const bool _kUseMagnifier = true;
+const double _kMagnification = 2.35/2.1;
+const double _kDatePickerPadSize = 12.0;
+// The density of a date picker is different from a generic picker.
+// Eyeballed from iOS.
+const double _kSqueeze = 1.25;
+
+const TextStyle _kDefaultPickerTextStyle = TextStyle(
+  letterSpacing: -0.83,
+);
+
+// The item height is 32 and the magnifier height is 34, from
+// iOS simulators with "Debug View Hierarchy".
+// And the magnified fontSize by [_kTimerPickerMagnification] conforms to the
+// iOS 14 native style by eyeball test.
+const double _kTimerPickerMagnification = 34 / 32;
+// Minimum horizontal padding between [CupertinoTimerPicker]
+//
+// It shouldn't actually be hard-coded for direct use, and the perfect solution
+// should be to calculate the values that match the magnified values by
+// offAxisFraction and _kSqueeze.
+// Such calculations are complex, so we'll hard-code them for now.
+const double _kTimerPickerMinHorizontalPadding = 30;
+// Half of the horizontal padding value between the timer picker's columns.
+const double _kTimerPickerHalfColumnPadding = 4;
+// The horizontal padding between the timer picker's number label and its
+// corresponding unit label.
+const double _kTimerPickerLabelPadSize = 6;
+const double _kTimerPickerLabelFontSize = 17.0;
+
+// The width of each column of the countdown time picker.
+const double _kTimerPickerColumnIntrinsicWidth = 106;
+
+TextStyle _themeTextStyle(BuildContext context, { bool isValid = true }) {
+  final TextStyle style = CupertinoTheme.of(context).textTheme.dateTimePickerTextStyle;
+  return isValid ? style : style.copyWith(color: CupertinoDynamicColor.resolve(CupertinoColors.inactiveGray, context));
+}
+
+void _animateColumnControllerToItem(FixedExtentScrollController controller, int targetItem) {
+  controller.animateToItem(
+    targetItem,
+    curve: Curves.easeInOut,
+    duration: const Duration(milliseconds: 200),
+  );
+}
+
+const Widget _leftSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(capRightEdge: false);
+const Widget _centerSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(capLeftEdge: false, capRightEdge: false,);
+const Widget _rightSelectionOverlay = CupertinoPickerDefaultSelectionOverlay(capLeftEdge: false);
+
+// Lays out the date picker based on how much space each single column needs.
+//
+// Each column is a child of this delegate, indexed from 0 to number of columns - 1.
+// Each column will be padded horizontally by 12.0 both left and right.
+//
+// The picker will be placed in the center, and the leftmost and rightmost
+// column will be extended equally to the remaining width.
+class _DatePickerLayoutDelegate extends MultiChildLayoutDelegate {
+  _DatePickerLayoutDelegate({
+    required this.columnWidths,
+    required this.textDirectionFactor,
+  }) : assert(columnWidths != null),
+       assert(textDirectionFactor != null);
+
+  // The list containing widths of all columns.
+  final List<double> columnWidths;
+
+  // textDirectionFactor is 1 if text is written left to right, and -1 if right to left.
+  final int textDirectionFactor;
+
+  @override
+  void performLayout(Size size) {
+    double remainingWidth = size.width;
+
+    for (int i = 0; i < columnWidths.length; i++)
+      remainingWidth -= columnWidths[i] + _kDatePickerPadSize * 2;
+
+    double currentHorizontalOffset = 0.0;
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      final int index = textDirectionFactor == 1 ? i : columnWidths.length - i - 1;
+
+      double childWidth = columnWidths[index] + _kDatePickerPadSize * 2;
+      if (index == 0 || index == columnWidths.length - 1)
+        childWidth += remainingWidth / 2;
+
+      // We can't actually assert here because it would break things badly for
+      // semantics, which will expect that we laid things out here.
+      assert(() {
+        if (childWidth < 0) {
+          FlutterError.reportError(
+            FlutterErrorDetails(
+              exception: FlutterError(
+                'Insufficient horizontal space to render the '
+                'CupertinoDatePicker because the parent is too narrow at '
+                '${size.width}px.\n'
+                'An additional ${-remainingWidth}px is needed to avoid '
+                'overlapping columns.',
+              ),
+            ),
+          );
+        }
+        return true;
+      }());
+      layoutChild(index, BoxConstraints.tight(Size(math.max(0.0, childWidth), size.height)));
+      positionChild(index, Offset(currentHorizontalOffset, 0.0));
+
+      currentHorizontalOffset += childWidth;
+    }
+  }
+
+  @override
+  bool shouldRelayout(_DatePickerLayoutDelegate oldDelegate) {
+    return columnWidths != oldDelegate.columnWidths
+      || textDirectionFactor != oldDelegate.textDirectionFactor;
+  }
+}
+
+/// Different display modes of [CupertinoDatePicker].
+///
+/// See also:
+///
+///  * [CupertinoDatePicker], the class that implements different display modes
+///    of the iOS-style date picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+enum CupertinoDatePickerMode {
+  /// Mode that shows the date in hour, minute, and (optional) an AM/PM designation.
+  /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: ` 4 | 14 | PM `.
+  time,
+  /// Mode that shows the date in month, day of month, and year.
+  /// Name of month is spelled in full.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: ` July | 13 | 2012 `.
+  date,
+  /// Mode that shows the date as day of the week, month, day of month and
+  /// the time in hour, minute, and (optional) an AM/PM designation.
+  /// The AM/PM designation is shown only if [CupertinoDatePicker] does not use 24h format.
+  /// Column order is subject to internationalization.
+  ///
+  /// Example: ` Fri Jul 13 | 4 | 14 | PM `
+  dateAndTime,
+}
+
+// Different types of column in CupertinoDatePicker.
+enum _PickerColumnType {
+  // Day of month column in date mode.
+  dayOfMonth,
+  // Month column in date mode.
+  month,
+  // Year column in date mode.
+  year,
+  // Medium date column in dateAndTime mode.
+  date,
+  // Hour column in time and dateAndTime mode.
+  hour,
+  // minute column in time and dateAndTime mode.
+  minute,
+  // AM/PM column in time and dateAndTime mode.
+  dayPeriod,
+}
+
+/// A date picker widget in iOS style.
+///
+/// There are several modes of the date picker listed in [CupertinoDatePickerMode].
+///
+/// The class will display its children as consecutive columns. Its children
+/// order is based on internationalization.
+///
+/// Example of the picker in date mode:
+///
+///  * US-English: `| July | 13 | 2012 |`
+///  * Vietnamese: `| 13 | Tháng 7 | 2012 |`
+///
+/// Can be used with [showCupertinoModalPopup] to display the picker modally at
+/// the bottom of the screen.
+///
+/// Sizes itself to its parent and may not render correctly if not given the
+/// full screen width. Content texts are shown with
+/// [CupertinoTextThemeData.dateTimePickerTextStyle].
+///
+/// See also:
+///
+///  * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+class CupertinoDatePicker extends StatefulWidget {
+  /// Constructs an iOS style date picker.
+  ///
+  /// [mode] is one of the mode listed in [CupertinoDatePickerMode] and defaults
+  /// to [CupertinoDatePickerMode.dateAndTime].
+  ///
+  /// [onDateTimeChanged] is the callback called when the selected date or time
+  /// changes and must not be null. When in [CupertinoDatePickerMode.time] mode,
+  /// the year, month and day will be the same as [initialDateTime]. When in
+  /// [CupertinoDatePickerMode.date] mode, this callback will always report the
+  /// start time of the currently selected day.
+  ///
+  /// [initialDateTime] is the initial date time of the picker. Defaults to the
+  /// present date and time and must not be null. The present must conform to
+  /// the intervals set in [minimumDate], [maximumDate], [minimumYear], and
+  /// [maximumYear].
+  ///
+  /// [minimumDate] is the minimum selectable [DateTime] of the picker. When set
+  /// to null, the picker does not limit the minimum [DateTime] the user can pick.
+  /// In [CupertinoDatePickerMode.time] mode, [minimumDate] should typically be
+  /// on the same date as [initialDateTime], as the picker will not limit the
+  /// minimum time the user can pick if it's set to a date earlier than that.
+  ///
+  /// [maximumDate] is the maximum selectable [DateTime] of the picker. When set
+  /// to null, the picker does not limit the maximum [DateTime] the user can pick.
+  /// In [CupertinoDatePickerMode.time] mode, [maximumDate] should typically be
+  /// on the same date as [initialDateTime], as the picker will not limit the
+  /// maximum time the user can pick if it's set to a date later than that.
+  ///
+  /// [minimumYear] is the minimum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
+  ///
+  /// [maximumYear] is the maximum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
+  ///
+  /// [minuteInterval] is the granularity of the minute spinner. Must be a
+  /// positive integer factor of 60.
+  ///
+  /// [use24hFormat] decides whether 24 hour format is used. Defaults to false.
+  CupertinoDatePicker({
+    Key? key,
+    this.mode = CupertinoDatePickerMode.dateAndTime,
+    required this.onDateTimeChanged,
+    DateTime? initialDateTime,
+    this.minimumDate,
+    this.maximumDate,
+    this.minimumYear = 1,
+    this.maximumYear,
+    this.minuteInterval = 1,
+    this.use24hFormat = false,
+    this.backgroundColor,
+  }) : initialDateTime = initialDateTime ?? DateTime.now(),
+       assert(mode != null),
+       assert(onDateTimeChanged != null),
+       assert(minimumYear != null),
+       assert(
+         minuteInterval > 0 && 60 % minuteInterval == 0,
+         'minute interval is not a positive integer factor of 60',
+       ),
+       super(key: key) {
+    assert(this.initialDateTime != null);
+    assert(
+      mode != CupertinoDatePickerMode.dateAndTime || minimumDate == null || !this.initialDateTime.isBefore(minimumDate!),
+      'initial date is before minimum date',
+    );
+    assert(
+      mode != CupertinoDatePickerMode.dateAndTime || maximumDate == null || !this.initialDateTime.isAfter(maximumDate!),
+      'initial date is after maximum date',
+    );
+    assert(
+      mode != CupertinoDatePickerMode.date || (minimumYear >= 1 && this.initialDateTime.year >= minimumYear),
+      'initial year is not greater than minimum year, or minimum year is not positive',
+    );
+    assert(
+      mode != CupertinoDatePickerMode.date || maximumYear == null || this.initialDateTime.year <= maximumYear!,
+      'initial year is not smaller than maximum year',
+    );
+    assert(
+      mode != CupertinoDatePickerMode.date || minimumDate == null || !minimumDate!.isAfter(this.initialDateTime),
+      'initial date ${this.initialDateTime} is not greater than or equal to minimumDate $minimumDate',
+    );
+    assert(
+      mode != CupertinoDatePickerMode.date || maximumDate == null || !maximumDate!.isBefore(this.initialDateTime),
+      'initial date ${this.initialDateTime} is not less than or equal to maximumDate $maximumDate',
+    );
+    assert(
+      this.initialDateTime.minute % minuteInterval == 0,
+      'initial minute is not divisible by minute interval',
+    );
+  }
+
+  /// The mode of the date picker as one of [CupertinoDatePickerMode].
+  /// Defaults to [CupertinoDatePickerMode.dateAndTime]. Cannot be null and
+  /// value cannot change after initial build.
+  final CupertinoDatePickerMode mode;
+
+  /// The initial date and/or time of the picker. Defaults to the present date
+  /// and time and must not be null. The present must conform to the intervals
+  /// set in [minimumDate], [maximumDate], [minimumYear], and [maximumYear].
+  ///
+  /// Changing this value after the initial build will not affect the currently
+  /// selected date time.
+  final DateTime initialDateTime;
+
+  /// The minimum selectable date that the picker can settle on.
+  ///
+  /// When non-null, the user can still scroll the picker to [DateTime]s earlier
+  /// than [minimumDate], but the [onDateTimeChanged] will not be called on
+  /// these [DateTime]s. Once let go, the picker will scroll back to [minimumDate].
+  ///
+  /// In [CupertinoDatePickerMode.time] mode, a time becomes unselectable if the
+  /// [DateTime] produced by combining that particular time and the date part of
+  /// [initialDateTime] is earlier than [minimumDate]. So typically [minimumDate]
+  /// needs to be set to a [DateTime] that is on the same date as [initialDateTime].
+  ///
+  /// Defaults to null. When set to null, the picker does not impose a limit on
+  /// the earliest [DateTime] the user can select.
+  final DateTime? minimumDate;
+
+  /// The maximum selectable date that the picker can settle on.
+  ///
+  /// When non-null, the user can still scroll the picker to [DateTime]s later
+  /// than [maximumDate], but the [onDateTimeChanged] will not be called on
+  /// these [DateTime]s. Once let go, the picker will scroll back to [maximumDate].
+  ///
+  /// In [CupertinoDatePickerMode.time] mode, a time becomes unselectable if the
+  /// [DateTime] produced by combining that particular time and the date part of
+  /// [initialDateTime] is later than [maximumDate]. So typically [maximumDate]
+  /// needs to be set to a [DateTime] that is on the same date as [initialDateTime].
+  ///
+  /// Defaults to null. When set to null, the picker does not impose a limit on
+  /// the latest [DateTime] the user can select.
+  final DateTime? maximumDate;
+
+  /// Minimum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Defaults to 1 and must not be null.
+  final int minimumYear;
+
+  /// Maximum year that the picker can be scrolled to in
+  /// [CupertinoDatePickerMode.date] mode. Null if there's no limit.
+  final int? maximumYear;
+
+  /// The granularity of the minutes spinner, if it is shown in the current mode.
+  /// Must be an integer factor of 60.
+  final int minuteInterval;
+
+  /// Whether to use 24 hour format. Defaults to false.
+  final bool use24hFormat;
+
+  /// Callback called when the selected date and/or time changes. If the new
+  /// selected [DateTime] is not valid, or is not in the [minimumDate] through
+  /// [maximumDate] range, this callback will not be called.
+  ///
+  /// Must not be null.
+  final ValueChanged<DateTime> onDateTimeChanged;
+
+  /// Background color of date picker.
+  ///
+  /// Defaults to null, which disables background painting entirely.
+  final Color? backgroundColor;
+
+  @override
+  State<StatefulWidget> createState() { // ignore: no_logic_in_create_state, https://github.com/flutter/flutter/issues/70499
+    // The `time` mode and `dateAndTime` mode of the picker share the time
+    // columns, so they are placed together to one state.
+    // The `date` mode has different children and is implemented in a different
+    // state.
+    switch (mode) {
+      case CupertinoDatePickerMode.time:
+      case CupertinoDatePickerMode.dateAndTime:
+        return _CupertinoDatePickerDateTimeState();
+      case CupertinoDatePickerMode.date:
+        return _CupertinoDatePickerDateState();
+    }
+  }
+
+  // Estimate the minimum width that each column needs to layout its content.
+  static double _getColumnWidth(
+    _PickerColumnType columnType,
+    CupertinoLocalizations localizations,
+    BuildContext context,
+  ) {
+    String longestText = '';
+
+    switch (columnType) {
+      case _PickerColumnType.date:
+        // Measuring the length of all possible date is impossible, so here
+        // just some dates are measured.
+        for (int i = 1; i <= 12; i++) {
+          // An arbitrary date.
+          final String date =
+              localizations.datePickerMediumDate(DateTime(2018, i, 25));
+          if (longestText.length < date.length)
+            longestText = date;
+        }
+        break;
+      case _PickerColumnType.hour:
+        for (int i = 0; i < 24; i++) {
+          final String hour = localizations.datePickerHour(i);
+          if (longestText.length < hour.length)
+            longestText = hour;
+        }
+        break;
+      case _PickerColumnType.minute:
+        for (int i = 0; i < 60; i++) {
+          final String minute = localizations.datePickerMinute(i);
+          if (longestText.length < minute.length)
+            longestText = minute;
+        }
+        break;
+      case _PickerColumnType.dayPeriod:
+        longestText =
+          localizations.anteMeridiemAbbreviation.length > localizations.postMeridiemAbbreviation.length
+            ? localizations.anteMeridiemAbbreviation
+            : localizations.postMeridiemAbbreviation;
+        break;
+      case _PickerColumnType.dayOfMonth:
+        for (int i = 1; i <=31; i++) {
+          final String dayOfMonth = localizations.datePickerDayOfMonth(i);
+          if (longestText.length < dayOfMonth.length)
+            longestText = dayOfMonth;
+        }
+        break;
+      case _PickerColumnType.month:
+        for (int i = 1; i <=12; i++) {
+          final String month = localizations.datePickerMonth(i);
+          if (longestText.length < month.length)
+            longestText = month;
+        }
+        break;
+      case _PickerColumnType.year:
+        longestText = localizations.datePickerYear(2018);
+        break;
+    }
+
+    assert(longestText != '', 'column type is not appropriate');
+
+    final TextPainter painter = TextPainter(
+      text: TextSpan(
+        style: _themeTextStyle(context),
+        text: longestText,
+      ),
+      textDirection: Directionality.of(context),
+    );
+
+    // This operation is expensive and should be avoided. It is called here only
+    // because there's no other way to get the information we want without
+    // laying out the text.
+    painter.layout();
+
+    return painter.maxIntrinsicWidth;
+  }
+}
+
+typedef _ColumnBuilder = Widget Function(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay);
+
+class _CupertinoDatePickerDateTimeState extends State<CupertinoDatePicker> {
+  // Fraction of the farthest column's vanishing point vs its width. Eyeballed
+  // vs iOS.
+  static const double _kMaximumOffAxisFraction = 0.45;
+
+  late int textDirectionFactor;
+  late CupertinoLocalizations localizations;
+
+  // Alignment based on text direction. The variable name is self descriptive,
+  // however, when text direction is rtl, alignment is reversed.
+  late Alignment alignCenterLeft;
+  late Alignment alignCenterRight;
+
+  // Read this out when the state is initially created. Changes in initialDateTime
+  // in the widget after first build is ignored.
+  late DateTime initialDateTime;
+
+  // The difference in days between the initial date and the currently selected date.
+  // 0 if the current mode does not involve a date.
+  int get selectedDayFromInitial {
+    switch (widget.mode) {
+      case CupertinoDatePickerMode.dateAndTime:
+        return dateController.hasClients ? dateController.selectedItem : 0;
+      case CupertinoDatePickerMode.time:
+        return 0;
+      case CupertinoDatePickerMode.date:
+        break;
+    }
+    assert(
+      false,
+      '$runtimeType is only meant for dateAndTime mode or time mode',
+    );
+    return 0;
+  }
+  // The controller of the date column.
+  late FixedExtentScrollController dateController;
+
+  // The current selection of the hour picker. Values range from 0 to 23.
+  int get selectedHour => _selectedHour(selectedAmPm, _selectedHourIndex);
+  int get _selectedHourIndex => hourController.hasClients ? hourController.selectedItem % 24 : initialDateTime.hour;
+  // Calculates the selected hour given the selected indices of the hour picker
+  // and the meridiem picker.
+  int _selectedHour(int selectedAmPm, int selectedHour) {
+    return _isHourRegionFlipped(selectedAmPm) ? (selectedHour + 12) % 24 : selectedHour;
+  }
+  // The controller of the hour column.
+  late FixedExtentScrollController hourController;
+
+  // The current selection of the minute picker. Values range from 0 to 59.
+  int get selectedMinute {
+    return minuteController.hasClients
+      ? minuteController.selectedItem * widget.minuteInterval % 60
+      : initialDateTime.minute;
+  }
+  // The controller of the minute column.
+  late FixedExtentScrollController minuteController;
+
+  // Whether the current meridiem selection is AM or PM.
+  //
+  // We can't use the selectedItem of meridiemController as the source of truth
+  // because the meridiem picker can be scrolled **animatedly** by the hour picker
+  // (e.g. if you scroll from 12 to 1 in 12h format), but the meridiem change
+  // should take effect immediately, **before** the animation finishes.
+  late int selectedAmPm;
+  // Whether the physical-region-to-meridiem mapping is flipped.
+  bool get isHourRegionFlipped => _isHourRegionFlipped(selectedAmPm);
+  bool _isHourRegionFlipped(int selectedAmPm) => selectedAmPm != meridiemRegion;
+  // The index of the 12-hour region the hour picker is currently in.
+  //
+  // Used to determine whether the meridiemController should start animating.
+  // Valid values are 0 and 1.
+  //
+  // The AM/PM correspondence of the two regions flips when the meridiem picker
+  // scrolls. This variable is to keep track of the selected "physical"
+  // (meridiem picker invariant) region of the hour picker. The "physical" region
+  // of an item of index `i` is `i ~/ 12`.
+  late int meridiemRegion;
+  // The current selection of the AM/PM picker.
+  //
+  // - 0 means AM
+  // - 1 means PM
+  late FixedExtentScrollController meridiemController;
+
+  bool isDatePickerScrolling = false;
+  bool isHourPickerScrolling = false;
+  bool isMinutePickerScrolling = false;
+  bool isMeridiemPickerScrolling = false;
+
+  bool get isScrolling {
+    return isDatePickerScrolling
+        || isHourPickerScrolling
+        || isMinutePickerScrolling
+        || isMeridiemPickerScrolling;
+  }
+
+  // The estimated width of columns.
+  final Map<int, double> estimatedColumnWidths = <int, double>{};
+
+  @override
+  void initState() {
+    super.initState();
+    initialDateTime = widget.initialDateTime;
+
+    // Initially each of the "physical" regions is mapped to the meridiem region
+    // with the same number, e.g., the first 12 items are mapped to the first 12
+    // hours of a day. Such mapping is flipped when the meridiem picker is scrolled
+    // by the user, the first 12 items are mapped to the last 12 hours of a day.
+    selectedAmPm = initialDateTime.hour ~/ 12;
+    meridiemRegion = selectedAmPm;
+
+    meridiemController = FixedExtentScrollController(initialItem: selectedAmPm);
+    hourController = FixedExtentScrollController(initialItem: initialDateTime.hour);
+    minuteController = FixedExtentScrollController(initialItem: initialDateTime.minute ~/ widget.minuteInterval);
+    dateController = FixedExtentScrollController(initialItem: 0);
+
+    PaintingBinding.instance!.systemFonts.addListener(_handleSystemFontsChange);
+  }
+
+  void _handleSystemFontsChange () {
+    setState(() {
+      // System fonts change might cause the text layout width to change.
+      // Clears cached width to ensure that they get recalculated with the
+      // new system fonts.
+      estimatedColumnWidths.clear();
+    });
+  }
+
+  @override
+  void dispose() {
+    dateController.dispose();
+    hourController.dispose();
+    minuteController.dispose();
+    meridiemController.dispose();
+
+    PaintingBinding.instance!.systemFonts.removeListener(_handleSystemFontsChange);
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(CupertinoDatePicker oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    assert(
+      oldWidget.mode == widget.mode,
+      "The $runtimeType's mode cannot change once it's built.",
+    );
+
+    if (!widget.use24hFormat && oldWidget.use24hFormat) {
+      // Thanks to the physical and meridiem region mapping, the only thing we
+      // need to update is the meridiem controller, if it's not previously attached.
+      meridiemController.dispose();
+      meridiemController = FixedExtentScrollController(initialItem: selectedAmPm);
+    }
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
+    localizations = CupertinoLocalizations.of(context);
+
+    alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
+    alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
+
+    estimatedColumnWidths.clear();
+  }
+
+  // Lazily calculate the column width of the column being displayed only.
+  double _getEstimatedColumnWidth(_PickerColumnType columnType) {
+    if (estimatedColumnWidths[columnType.index] == null) {
+      estimatedColumnWidths[columnType.index] =
+          CupertinoDatePicker._getColumnWidth(columnType, localizations, context);
+    }
+
+    return estimatedColumnWidths[columnType.index]!;
+  }
+
+  // Gets the current date time of the picker.
+  DateTime get selectedDateTime {
+    return DateTime(
+      initialDateTime.year,
+      initialDateTime.month,
+      initialDateTime.day + selectedDayFromInitial,
+      selectedHour,
+      selectedMinute,
+    );
+  }
+
+  // Only reports datetime change when the date time is valid.
+  void _onSelectedItemChange(int index) {
+    final DateTime selected = selectedDateTime;
+
+    final bool isDateInvalid = widget.minimumDate?.isAfter(selected) == true
+                            || widget.maximumDate?.isBefore(selected) == true;
+
+    if (isDateInvalid)
+      return;
+
+    widget.onDateTimeChanged(selected);
+  }
+
+  // Builds the date column. The date is displayed in medium date format (e.g. Fri Aug 31).
+  Widget _buildMediumDatePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isDatePickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isDatePickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker.builder(
+        scrollController: dateController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: (int index) {
+          _onSelectedItemChange(index);
+        },
+        itemBuilder: (BuildContext context, int index) {
+          final DateTime rangeStart = DateTime(
+            initialDateTime.year,
+            initialDateTime.month,
+            initialDateTime.day + index,
+          );
+
+          // Exclusive.
+          final DateTime rangeEnd = DateTime(
+            initialDateTime.year,
+            initialDateTime.month,
+            initialDateTime.day + index + 1,
+          );
+
+          final DateTime now = DateTime.now();
+
+          if (widget.minimumDate?.isAfter(rangeEnd) == true)
+            return null;
+          if (widget.maximumDate?.isAfter(rangeStart) == false)
+            return null;
+
+          final String dateText = rangeStart == DateTime(now.year, now.month, now.day)
+            ? localizations.todayLabel
+            : localizations.datePickerMediumDate(rangeStart);
+
+          return itemPositioningBuilder(
+            context,
+            Text(dateText, style: _themeTextStyle(context)),
+          );
+        },
+      ),
+    );
+  }
+
+  // With the meridiem picker set to `meridiemIndex`, and the hour picker set to
+  // `hourIndex`, is it possible to change the value of the minute picker, so
+  // that the resulting date stays in the valid range.
+  bool _isValidHour(int meridiemIndex, int hourIndex) {
+    final DateTime rangeStart = DateTime(
+      initialDateTime.year,
+      initialDateTime.month,
+      initialDateTime.day + selectedDayFromInitial,
+      _selectedHour(meridiemIndex, hourIndex),
+      0,
+    );
+
+    // The end value of the range is exclusive, i.e. [rangeStart, rangeEnd).
+    final DateTime rangeEnd = rangeStart.add(const Duration(hours: 1));
+
+    return (widget.minimumDate?.isBefore(rangeEnd) ?? true)
+        && !(widget.maximumDate?.isBefore(rangeStart) ?? false);
+  }
+
+  Widget _buildHourPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isHourPickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isHourPickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker(
+        scrollController: hourController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: (int index) {
+          final bool regionChanged = meridiemRegion != index ~/ 12;
+          final bool debugIsFlipped = isHourRegionFlipped;
+
+          if (regionChanged) {
+            meridiemRegion = index ~/ 12;
+            selectedAmPm = 1 - selectedAmPm;
+          }
+
+          if (!widget.use24hFormat && regionChanged) {
+            // Scroll the meridiem column to adjust AM/PM.
+            //
+            // _onSelectedItemChanged will be called when the animation finishes.
+            //
+            // Animation values obtained by comparing with iOS version.
+            meridiemController.animateToItem(
+              selectedAmPm,
+              duration: const Duration(milliseconds: 300),
+              curve: Curves.easeOut,
+            );
+          } else {
+            _onSelectedItemChange(index);
+          }
+
+          assert(debugIsFlipped == isHourRegionFlipped);
+        },
+        children: List<Widget>.generate(24, (int index) {
+          final int hour = isHourRegionFlipped ? (index + 12) % 24 : index;
+          final int displayHour = widget.use24hFormat ? hour : (hour + 11) % 12 + 1;
+
+          return itemPositioningBuilder(
+            context,
+            Text(
+              localizations.datePickerHour(displayHour),
+              semanticsLabel: localizations.datePickerHourSemanticsLabel(displayHour),
+              style: _themeTextStyle(context, isValid: _isValidHour(selectedAmPm, index)),
+            ),
+          );
+        }),
+        looping: true,
+      )
+    );
+  }
+
+  Widget _buildMinutePicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isMinutePickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isMinutePickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker(
+        scrollController: minuteController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: _onSelectedItemChange,
+        children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) {
+          final int minute = index * widget.minuteInterval;
+
+          final DateTime date = DateTime(
+            initialDateTime.year,
+            initialDateTime.month,
+            initialDateTime.day + selectedDayFromInitial,
+            selectedHour,
+            minute,
+          );
+
+          final bool isInvalidMinute = (widget.minimumDate?.isAfter(date) ?? false)
+                                    || (widget.maximumDate?.isBefore(date) ?? false);
+
+          return itemPositioningBuilder(
+            context,
+            Text(
+              localizations.datePickerMinute(minute),
+              semanticsLabel: localizations.datePickerMinuteSemanticsLabel(minute),
+              style: _themeTextStyle(context, isValid: !isInvalidMinute),
+            ),
+          );
+        }),
+        looping: true,
+        selectionOverlay: selectionOverlay,
+      ),
+    );
+  }
+
+  Widget _buildAmPmPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isMeridiemPickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isMeridiemPickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker(
+        scrollController: meridiemController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: (int index) {
+          selectedAmPm = index;
+          assert(selectedAmPm == 0 || selectedAmPm == 1);
+          _onSelectedItemChange(index);
+        },
+        children: List<Widget>.generate(2, (int index) {
+          return itemPositioningBuilder(
+            context,
+            Text(
+              index == 0
+                ? localizations.anteMeridiemAbbreviation
+                : localizations.postMeridiemAbbreviation,
+              style: _themeTextStyle(context, isValid: _isValidHour(index, _selectedHourIndex)),
+            ),
+          );
+        }),
+        selectionOverlay: selectionOverlay,
+      ),
+    );
+  }
+
+  // One or more pickers have just stopped scrolling.
+  void _pickerDidStopScrolling() {
+    // Call setState to update the greyed out date/hour/minute/meridiem.
+    setState(() { });
+
+    if (isScrolling)
+      return;
+
+    // Whenever scrolling lands on an invalid entry, the picker
+    // automatically scrolls to a valid one.
+    final DateTime selectedDate = selectedDateTime;
+
+    final bool minCheck = widget.minimumDate?.isAfter(selectedDate) ?? false;
+    final bool maxCheck = widget.maximumDate?.isBefore(selectedDate) ?? false;
+
+    if (minCheck || maxCheck) {
+      // We have minCheck === !maxCheck.
+      final DateTime targetDate = minCheck ? widget.minimumDate! : widget.maximumDate!;
+      _scrollToDate(targetDate, selectedDate);
+    }
+  }
+
+  void _scrollToDate(DateTime newDate, DateTime fromDate) {
+    assert(newDate != null);
+    SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
+      if (fromDate.year != newDate.year || fromDate.month != newDate.month || fromDate.day != newDate.day) {
+        _animateColumnControllerToItem(dateController, selectedDayFromInitial);
+      }
+
+      if (fromDate.hour != newDate.hour) {
+        final bool needsMeridiemChange = !widget.use24hFormat
+                                      && fromDate.hour ~/ 12 != newDate.hour ~/ 12;
+        // In AM/PM mode, the pickers should not scroll all the way to the other hour region.
+        if (needsMeridiemChange) {
+          _animateColumnControllerToItem(meridiemController, 1 - meridiemController.selectedItem);
+
+          // Keep the target item index in the current 12-h region.
+          final int newItem = (hourController.selectedItem ~/ 12) * 12
+                            + (hourController.selectedItem + newDate.hour - fromDate.hour) % 12;
+          _animateColumnControllerToItem(hourController, newItem);
+        } else {
+          _animateColumnControllerToItem(
+            hourController,
+            hourController.selectedItem + newDate.hour - fromDate.hour,
+          );
+        }
+      }
+
+      if (fromDate.minute != newDate.minute) {
+        _animateColumnControllerToItem(minuteController, newDate.minute);
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // Widths of the columns in this picker, ordered from left to right.
+    final List<double> columnWidths = <double>[
+      _getEstimatedColumnWidth(_PickerColumnType.hour),
+      _getEstimatedColumnWidth(_PickerColumnType.minute),
+    ];
+
+    // Swap the hours and minutes if RTL to ensure they are in the correct position.
+    final List<_ColumnBuilder> pickerBuilders = Directionality.of(context) == TextDirection.rtl
+      ? <_ColumnBuilder>[_buildMinutePicker, _buildHourPicker]
+      : <_ColumnBuilder>[_buildHourPicker, _buildMinutePicker];
+
+    // Adds am/pm column if the picker is not using 24h format.
+    if (!widget.use24hFormat) {
+      if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.date_time_dayPeriod
+        || localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date) {
+        pickerBuilders.add(_buildAmPmPicker);
+        columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
+      } else {
+        pickerBuilders.insert(0, _buildAmPmPicker);
+        columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.dayPeriod));
+      }
+    }
+
+    // Adds medium date column if the picker's mode is date and time.
+    if (widget.mode == CupertinoDatePickerMode.dateAndTime) {
+      if (localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.time_dayPeriod_date
+          || localizations.datePickerDateTimeOrder == DatePickerDateTimeOrder.dayPeriod_time_date) {
+        pickerBuilders.add(_buildMediumDatePicker);
+        columnWidths.add(_getEstimatedColumnWidth(_PickerColumnType.date));
+      } else {
+        pickerBuilders.insert(0, _buildMediumDatePicker);
+        columnWidths.insert(0, _getEstimatedColumnWidth(_PickerColumnType.date));
+      }
+    }
+
+    final List<Widget> pickers = <Widget>[];
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      double offAxisFraction = 0.0;
+      Widget selectionOverlay = _centerSelectionOverlay;
+      if (i == 0) {
+        offAxisFraction = -_kMaximumOffAxisFraction * textDirectionFactor;
+        selectionOverlay = _leftSelectionOverlay;
+      } else if (i >= 2 || columnWidths.length == 2)
+        offAxisFraction = _kMaximumOffAxisFraction * textDirectionFactor;
+
+      EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
+      if (i == columnWidths.length - 1) {
+        padding = padding.flipped;
+        selectionOverlay = _rightSelectionOverlay;
+      }
+      if (textDirectionFactor == -1)
+        padding = padding.flipped;
+
+      pickers.add(LayoutId(
+        id: i,
+        child: pickerBuilders[i](
+          offAxisFraction,
+          (BuildContext context, Widget? child) {
+            return Container(
+              alignment: i == columnWidths.length - 1
+                ? alignCenterLeft
+                : alignCenterRight,
+              padding: padding,
+              child: Container(
+                alignment: i == columnWidths.length - 1 ? alignCenterLeft : alignCenterRight,
+                width: i == 0 || i == columnWidths.length - 1
+                  ? null
+                  : columnWidths[i] + _kDatePickerPadSize,
+                child: child,
+              ),
+            );
+          },
+          selectionOverlay,
+        ),
+      ));
+    }
+
+    return MediaQuery(
+      data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+      child: DefaultTextStyle.merge(
+        style: _kDefaultPickerTextStyle,
+        child: CustomMultiChildLayout(
+          delegate: _DatePickerLayoutDelegate(
+            columnWidths: columnWidths,
+            textDirectionFactor: textDirectionFactor,
+          ),
+          children: pickers,
+        ),
+      ),
+    );
+  }
+}
+
+class _CupertinoDatePickerDateState extends State<CupertinoDatePicker> {
+  late int textDirectionFactor;
+  late CupertinoLocalizations localizations;
+
+  // Alignment based on text direction. The variable name is self descriptive,
+  // however, when text direction is rtl, alignment is reversed.
+  late Alignment alignCenterLeft;
+  late Alignment alignCenterRight;
+
+  // The currently selected values of the picker.
+  late int selectedDay;
+  late int selectedMonth;
+  late int selectedYear;
+
+  // The controller of the day picker. There are cases where the selected value
+  // of the picker is invalid (e.g. February 30th 2018), and this dayController
+  // is responsible for jumping to a valid value.
+  late FixedExtentScrollController dayController;
+  late FixedExtentScrollController monthController;
+  late FixedExtentScrollController yearController;
+
+  bool isDayPickerScrolling = false;
+  bool isMonthPickerScrolling = false;
+  bool isYearPickerScrolling = false;
+
+  bool get isScrolling => isDayPickerScrolling || isMonthPickerScrolling || isYearPickerScrolling;
+
+  // Estimated width of columns.
+  Map<int, double> estimatedColumnWidths = <int, double>{};
+
+  @override
+  void initState() {
+    super.initState();
+    selectedDay = widget.initialDateTime.day;
+    selectedMonth = widget.initialDateTime.month;
+    selectedYear = widget.initialDateTime.year;
+
+    dayController = FixedExtentScrollController(initialItem: selectedDay - 1);
+    monthController = FixedExtentScrollController(initialItem: selectedMonth - 1);
+    yearController = FixedExtentScrollController(initialItem: selectedYear);
+
+    PaintingBinding.instance!.systemFonts.addListener(_handleSystemFontsChange);
+  }
+
+  void _handleSystemFontsChange() {
+    setState(() {
+      // System fonts change might cause the text layout width to change.
+      _refreshEstimatedColumnWidths();
+    });
+  }
+
+  @override
+  void dispose() {
+    dayController.dispose();
+    monthController.dispose();
+    yearController.dispose();
+
+    PaintingBinding.instance!.systemFonts.removeListener(_handleSystemFontsChange);
+    super.dispose();
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    textDirectionFactor = Directionality.of(context) == TextDirection.ltr ? 1 : -1;
+    localizations = CupertinoLocalizations.of(context);
+
+    alignCenterLeft = textDirectionFactor == 1 ? Alignment.centerLeft : Alignment.centerRight;
+    alignCenterRight = textDirectionFactor == 1 ? Alignment.centerRight : Alignment.centerLeft;
+
+    _refreshEstimatedColumnWidths();
+  }
+
+  void _refreshEstimatedColumnWidths() {
+    estimatedColumnWidths[_PickerColumnType.dayOfMonth.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.dayOfMonth, localizations, context);
+    estimatedColumnWidths[_PickerColumnType.month.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.month, localizations, context);
+    estimatedColumnWidths[_PickerColumnType.year.index] = CupertinoDatePicker._getColumnWidth(_PickerColumnType.year, localizations, context);
+  }
+
+  // The DateTime of the last day of a given month in a given year.
+  // Let `DateTime` handle the year/month overflow.
+  DateTime _lastDayInMonth(int year, int month) => DateTime(year, month + 1, 0);
+
+  Widget _buildDayPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    final int daysInCurrentMonth = _lastDayInMonth(selectedYear, selectedMonth).day;
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isDayPickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isDayPickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker(
+        scrollController: dayController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: (int index) {
+          selectedDay = index + 1;
+          if (_isCurrentDateValid)
+            widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
+        },
+        children: List<Widget>.generate(31, (int index) {
+          final int day = index + 1;
+          return itemPositioningBuilder(
+            context,
+            Text(
+              localizations.datePickerDayOfMonth(day),
+              style: _themeTextStyle(context, isValid: day <= daysInCurrentMonth),
+            ),
+          );
+        }),
+        looping: true,
+        selectionOverlay: selectionOverlay,
+      ),
+    );
+  }
+
+  Widget _buildMonthPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isMonthPickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isMonthPickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker(
+        scrollController: monthController,
+        offAxisFraction: offAxisFraction,
+        itemExtent: _kItemExtent,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        squeeze: _kSqueeze,
+        onSelectedItemChanged: (int index) {
+          selectedMonth = index + 1;
+          if (_isCurrentDateValid)
+            widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
+        },
+        children: List<Widget>.generate(12, (int index) {
+          final int month = index + 1;
+          final bool isInvalidMonth = (widget.minimumDate?.year == selectedYear && widget.minimumDate!.month > month)
+                                   || (widget.maximumDate?.year == selectedYear && widget.maximumDate!.month < month);
+
+          return itemPositioningBuilder(
+            context,
+            Text(
+              localizations.datePickerMonth(month),
+              style: _themeTextStyle(context, isValid: !isInvalidMonth),
+            ),
+          );
+        }),
+        looping: true,
+        selectionOverlay: selectionOverlay,
+      ),
+    );
+  }
+
+  Widget _buildYearPicker(double offAxisFraction, TransitionBuilder itemPositioningBuilder, Widget selectionOverlay) {
+    return NotificationListener<ScrollNotification>(
+      onNotification: (ScrollNotification notification) {
+        if (notification is ScrollStartNotification) {
+          isYearPickerScrolling = true;
+        } else if (notification is ScrollEndNotification) {
+          isYearPickerScrolling = false;
+          _pickerDidStopScrolling();
+        }
+
+        return false;
+      },
+      child: CupertinoPicker.builder(
+        scrollController: yearController,
+        itemExtent: _kItemExtent,
+        offAxisFraction: offAxisFraction,
+        useMagnifier: _kUseMagnifier,
+        magnification: _kMagnification,
+        backgroundColor: widget.backgroundColor,
+        onSelectedItemChanged: (int index) {
+          selectedYear = index;
+          if (_isCurrentDateValid)
+            widget.onDateTimeChanged(DateTime(selectedYear, selectedMonth, selectedDay));
+        },
+        itemBuilder: (BuildContext context, int year) {
+          if (year < widget.minimumYear)
+            return null;
+
+          if (widget.maximumYear != null && year > widget.maximumYear!)
+            return null;
+
+          final bool isValidYear = (widget.minimumDate == null || widget.minimumDate!.year <= year)
+                                && (widget.maximumDate == null || widget.maximumDate!.year >= year);
+
+          return itemPositioningBuilder(
+            context,
+            Text(
+              localizations.datePickerYear(year),
+              style: _themeTextStyle(context, isValid: isValidYear),
+            ),
+          );
+        },
+        selectionOverlay: selectionOverlay,
+      ),
+    );
+  }
+
+  bool get _isCurrentDateValid {
+    // The current date selection represents a range [minSelectedData, maxSelectDate].
+    final DateTime minSelectedDate = DateTime(selectedYear, selectedMonth, selectedDay);
+    final DateTime maxSelectedDate = DateTime(selectedYear, selectedMonth, selectedDay + 1);
+
+    final bool minCheck = widget.minimumDate?.isBefore(maxSelectedDate) ?? true;
+    final bool maxCheck = widget.maximumDate?.isBefore(minSelectedDate) ?? false;
+
+    return minCheck && !maxCheck && minSelectedDate.day == selectedDay;
+  }
+
+  // One or more pickers have just stopped scrolling.
+  void _pickerDidStopScrolling() {
+    // Call setState to update the greyed out days/months/years, as the currently
+    // selected year/month may have changed.
+    setState(() { });
+
+    if (isScrolling) {
+      return;
+    }
+
+    // Whenever scrolling lands on an invalid entry, the picker
+    // automatically scrolls to a valid one.
+    final DateTime minSelectDate = DateTime(selectedYear, selectedMonth, selectedDay);
+    final DateTime maxSelectDate = DateTime(selectedYear, selectedMonth, selectedDay + 1);
+
+    final bool minCheck = widget.minimumDate?.isBefore(maxSelectDate) ?? true;
+    final bool maxCheck = widget.maximumDate?.isBefore(minSelectDate) ?? false;
+
+    if (!minCheck || maxCheck) {
+      // We have minCheck === !maxCheck.
+      final DateTime targetDate = minCheck ? widget.maximumDate! : widget.minimumDate!;
+      _scrollToDate(targetDate);
+      return;
+    }
+
+    // Some months have less days (e.g. February). Go to the last day of that month
+    // if the selectedDay exceeds the maximum.
+    if (minSelectDate.day != selectedDay) {
+      final DateTime lastDay = _lastDayInMonth(selectedYear, selectedMonth);
+      _scrollToDate(lastDay);
+    }
+  }
+
+  void _scrollToDate(DateTime newDate) {
+    assert(newDate != null);
+    SchedulerBinding.instance!.addPostFrameCallback((Duration timestamp) {
+      if (selectedYear != newDate.year) {
+        _animateColumnControllerToItem(yearController, newDate.year);
+      }
+
+      if (selectedMonth != newDate.month) {
+        _animateColumnControllerToItem(monthController, newDate.month - 1);
+      }
+
+      if (selectedDay != newDate.day) {
+        _animateColumnControllerToItem(dayController, newDate.day - 1);
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    List<_ColumnBuilder> pickerBuilders = <_ColumnBuilder>[];
+    List<double> columnWidths = <double>[];
+
+    switch (localizations.datePickerDateOrder) {
+      case DatePickerDateOrder.mdy:
+        pickerBuilders = <_ColumnBuilder>[_buildMonthPicker, _buildDayPicker, _buildYearPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.month.index]!,
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]!,
+          estimatedColumnWidths[_PickerColumnType.year.index]!,
+        ];
+        break;
+      case DatePickerDateOrder.dmy:
+        pickerBuilders = <_ColumnBuilder>[_buildDayPicker, _buildMonthPicker, _buildYearPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]!,
+          estimatedColumnWidths[_PickerColumnType.month.index]!,
+          estimatedColumnWidths[_PickerColumnType.year.index]!,
+        ];
+        break;
+      case DatePickerDateOrder.ymd:
+        pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildMonthPicker, _buildDayPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.year.index]!,
+          estimatedColumnWidths[_PickerColumnType.month.index]!,
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]!,
+        ];
+        break;
+      case DatePickerDateOrder.ydm:
+        pickerBuilders = <_ColumnBuilder>[_buildYearPicker, _buildDayPicker, _buildMonthPicker];
+        columnWidths = <double>[
+          estimatedColumnWidths[_PickerColumnType.year.index]!,
+          estimatedColumnWidths[_PickerColumnType.dayOfMonth.index]!,
+          estimatedColumnWidths[_PickerColumnType.month.index]!,
+        ];
+        break;
+    }
+
+    final List<Widget> pickers = <Widget>[];
+
+    for (int i = 0; i < columnWidths.length; i++) {
+      final double offAxisFraction = (i - 1) * 0.3 * textDirectionFactor;
+
+      EdgeInsets padding = const EdgeInsets.only(right: _kDatePickerPadSize);
+      if (textDirectionFactor == -1)
+        padding = const EdgeInsets.only(left: _kDatePickerPadSize);
+
+      Widget selectionOverlay = _centerSelectionOverlay;
+      if (i == 0)
+        selectionOverlay = _leftSelectionOverlay;
+      else if (i == columnWidths.length - 1)
+        selectionOverlay = _rightSelectionOverlay;
+
+      pickers.add(LayoutId(
+        id: i,
+        child: pickerBuilders[i](
+          offAxisFraction,
+          (BuildContext context, Widget? child) {
+            return Container(
+              alignment: i == columnWidths.length - 1
+                  ? alignCenterLeft
+                  : alignCenterRight,
+              padding: i == 0 ? null : padding,
+              child: Container(
+                alignment: i == 0 ? alignCenterLeft : alignCenterRight,
+                width: columnWidths[i] + _kDatePickerPadSize,
+                child: child,
+              ),
+            );
+          },
+          selectionOverlay,
+        ),
+      ));
+    }
+
+    return MediaQuery(
+      data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+      child: DefaultTextStyle.merge(
+        style: _kDefaultPickerTextStyle,
+        child: CustomMultiChildLayout(
+          delegate: _DatePickerLayoutDelegate(
+            columnWidths: columnWidths,
+            textDirectionFactor: textDirectionFactor,
+          ),
+          children: pickers,
+        ),
+      ),
+    );
+  }
+}
+
+
+// The iOS date picker and timer picker has their width fixed to 320.0 in all
+// modes. The only exception is the hms mode (which doesn't have a native counterpart),
+// with a fixed width of 330.0 px.
+//
+// For date pickers, if the maximum width given to the picker is greater than
+// 320.0, the leftmost and rightmost column will be extended equally so that the
+// widths match, and the picker is in the center.
+//
+// For timer pickers, if the maximum width given to the picker is greater than
+// its intrinsic width, it will keep its intrinsic size and position itself in the
+// parent using its alignment parameter.
+//
+// If the maximum width given to the picker is smaller than 320.0, the picker's
+// layout will be broken.
+
+
+/// Different modes of [CupertinoTimerPicker].
+///
+/// See also:
+///
+///  * [CupertinoTimerPicker], the class that implements the iOS-style timer picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+enum CupertinoTimerPickerMode {
+  /// Mode that shows the timer duration in hour and minute.
+  ///
+  /// Examples: 16 hours | 14 min.
+  hm,
+  /// Mode that shows the timer duration in minute and second.
+  ///
+  /// Examples: 14 min | 43 sec.
+  ms,
+  /// Mode that shows the timer duration in hour, minute, and second.
+  ///
+  /// Examples: 16 hours | 14 min | 43 sec.
+  hms,
+}
+
+/// A countdown timer picker in iOS style.
+///
+/// This picker shows a countdown duration with hour, minute and second spinners.
+/// The duration is bound between 0 and 23 hours 59 minutes 59 seconds.
+///
+/// There are several modes of the timer picker listed in [CupertinoTimerPickerMode].
+///
+/// The picker has a fixed size of 320 x 216, in logical pixels, with the exception
+/// of [CupertinoTimerPickerMode.hms], which is 330 x 216. If the parent widget
+/// provides more space than it needs, the picker will position itself according
+/// to its [alignment] property.
+///
+/// See also:
+///
+///  * [CupertinoDatePicker], the class that implements different display modes
+///    of the iOS-style date picker.
+///  * [CupertinoPicker], the class that implements a content agnostic spinner UI.
+class CupertinoTimerPicker extends StatefulWidget {
+  /// Constructs an iOS style countdown timer picker.
+  ///
+  /// [mode] is one of the modes listed in [CupertinoTimerPickerMode] and
+  /// defaults to [CupertinoTimerPickerMode.hms].
+  ///
+  /// [onTimerDurationChanged] is the callback called when the selected duration
+  /// changes and must not be null.
+  ///
+  /// [initialTimerDuration] defaults to 0 second and is limited from 0 second
+  /// to 23 hours 59 minutes 59 seconds.
+  ///
+  /// [minuteInterval] is the granularity of the minute spinner. Must be a
+  /// positive integer factor of 60.
+  ///
+  /// [secondInterval] is the granularity of the second spinner. Must be a
+  /// positive integer factor of 60.
+  CupertinoTimerPicker({
+    Key? key,
+    this.mode = CupertinoTimerPickerMode.hms,
+    this.initialTimerDuration = Duration.zero,
+    this.minuteInterval = 1,
+    this.secondInterval = 1,
+    this.alignment = Alignment.center,
+    this.backgroundColor,
+    required this.onTimerDurationChanged,
+  }) : assert(mode != null),
+       assert(onTimerDurationChanged != null),
+       assert(initialTimerDuration >= Duration.zero),
+       assert(initialTimerDuration < const Duration(days: 1)),
+       assert(minuteInterval > 0 && 60 % minuteInterval == 0),
+       assert(secondInterval > 0 && 60 % secondInterval == 0),
+       assert(initialTimerDuration.inMinutes % minuteInterval == 0),
+       assert(initialTimerDuration.inSeconds % secondInterval == 0),
+       assert(alignment != null),
+       super(key: key);
+
+  /// The mode of the timer picker.
+  final CupertinoTimerPickerMode mode;
+
+  /// The initial duration of the countdown timer.
+  final Duration initialTimerDuration;
+
+  /// The granularity of the minute spinner. Must be a positive integer factor
+  /// of 60.
+  final int minuteInterval;
+
+  /// The granularity of the second spinner. Must be a positive integer factor
+  /// of 60.
+  final int secondInterval;
+
+  /// Callback called when the timer duration changes.
+  final ValueChanged<Duration> onTimerDurationChanged;
+
+  /// Defines how the timer picker should be positioned within its parent.
+  ///
+  /// This property must not be null. It defaults to [Alignment.center].
+  final AlignmentGeometry alignment;
+
+  /// Background color of timer picker.
+  ///
+  /// Defaults to null, which disables background painting entirely.
+  final Color? backgroundColor;
+
+  @override
+  State<StatefulWidget> createState() => _CupertinoTimerPickerState();
+}
+
+class _CupertinoTimerPickerState extends State<CupertinoTimerPicker> {
+  late TextDirection textDirection;
+  late CupertinoLocalizations localizations;
+  int get textDirectionFactor {
+    switch (textDirection) {
+      case TextDirection.ltr:
+        return 1;
+      case TextDirection.rtl:
+        return -1;
+    }
+  }
+
+  // The currently selected values of the picker.
+  int? selectedHour;
+  late int selectedMinute;
+  int? selectedSecond;
+
+  // On iOS the selected values won't be reported until the scrolling fully stops.
+  // The values below are the latest selected values when the picker comes to a full stop.
+  int? lastSelectedHour;
+  int? lastSelectedMinute;
+  int? lastSelectedSecond;
+
+  final TextPainter textPainter = TextPainter();
+  final List<String> numbers = List<String>.generate(10, (int i) => '${9 - i}');
+  late double numberLabelWidth;
+  late double numberLabelHeight;
+  late double numberLabelBaseline;
+
+  late double hourLabelWidth;
+  late double minuteLabelWidth;
+  late double secondLabelWidth;
+
+  late double totalWidth;
+  late double pickerColumnWidth;
+
+  @override
+  void initState() {
+    super.initState();
+
+    selectedMinute = widget.initialTimerDuration.inMinutes % 60;
+
+    if (widget.mode != CupertinoTimerPickerMode.ms)
+      selectedHour = widget.initialTimerDuration.inHours;
+
+    if (widget.mode != CupertinoTimerPickerMode.hm)
+      selectedSecond = widget.initialTimerDuration.inSeconds % 60;
+
+    PaintingBinding.instance!.systemFonts.addListener(_handleSystemFontsChange);
+  }
+
+  void _handleSystemFontsChange() {
+    setState(() {
+      // System fonts change might cause the text layout width to change.
+      textPainter.markNeedsLayout();
+      _measureLabelMetrics();
+    });
+  }
+
+  @override
+  void dispose() {
+    PaintingBinding.instance!.systemFonts.removeListener(_handleSystemFontsChange);
+    super.dispose();
+  }
+
+  @override
+  void didUpdateWidget(CupertinoTimerPicker oldWidget) {
+    super.didUpdateWidget(oldWidget);
+
+    assert(
+      oldWidget.mode == widget.mode,
+      "The CupertinoTimerPicker's mode cannot change once it's built",
+    );
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+
+    textDirection = Directionality.of(context);
+    localizations = CupertinoLocalizations.of(context);
+
+    _measureLabelMetrics();
+  }
+
+  void _measureLabelMetrics() {
+    textPainter.textDirection = textDirection;
+    final TextStyle textStyle = _textStyleFrom(context, _kTimerPickerMagnification);
+
+    double maxWidth = double.negativeInfinity;
+    String? widestNumber;
+
+    // Assumes that:
+    // - 2-digit numbers are always wider than 1-digit numbers.
+    // - There's at least one number in 1-9 that's wider than or equal to 0.
+    // - The widest 2-digit number is composed of 2 same 1-digit numbers
+    //   that has the biggest width.
+    // - If two different 1-digit numbers are of the same width, their corresponding
+    //   2 digit numbers are of the same width.
+    for (final String input in numbers) {
+      textPainter.text = TextSpan(
+        text: input,
+        style: textStyle,
+      );
+      textPainter.layout();
+
+      if (textPainter.maxIntrinsicWidth > maxWidth) {
+        maxWidth = textPainter.maxIntrinsicWidth;
+        widestNumber = input;
+      }
+    }
+
+    textPainter.text = TextSpan(
+      text: '$widestNumber$widestNumber',
+      style: textStyle,
+    );
+
+    textPainter.layout();
+    numberLabelWidth = textPainter.maxIntrinsicWidth;
+    numberLabelHeight = textPainter.height;
+    numberLabelBaseline = textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
+
+    minuteLabelWidth =
+        _measureLabelsMaxWidth(localizations.timerPickerMinuteLabels, textStyle);
+
+    if (widget.mode != CupertinoTimerPickerMode.ms)
+      hourLabelWidth =
+          _measureLabelsMaxWidth(localizations.timerPickerHourLabels, textStyle);
+
+    if (widget.mode != CupertinoTimerPickerMode.hm)
+      secondLabelWidth =
+          _measureLabelsMaxWidth(localizations.timerPickerSecondLabels, textStyle);
+  }
+
+  // Measures all possible time text labels and return maximum width.
+  double _measureLabelsMaxWidth(List<String?> labels, TextStyle style) {
+    double maxWidth = double.negativeInfinity;
+    for (int i = 0; i < labels.length; i++) {
+      final String? label = labels[i];
+      if(label == null) {
+        continue;
+      }
+
+      textPainter.text = TextSpan(text: label, style: style);
+      textPainter.layout();
+      textPainter.maxIntrinsicWidth;
+      if (textPainter.maxIntrinsicWidth > maxWidth)
+        maxWidth = textPainter.maxIntrinsicWidth;
+    }
+
+    return maxWidth;
+  }
+
+  // Builds a text label with scale factor 1.0 and font weight semi-bold.
+  // `pickerPadding ` is the additional padding the corresponding picker has to apply
+  // around the `Text`, in order to extend its separators towards the closest
+  // horizontal edge of the encompassing widget.
+  Widget _buildLabel(String text, EdgeInsetsDirectional pickerPadding) {
+    final EdgeInsetsDirectional padding = EdgeInsetsDirectional.only(
+      start: numberLabelWidth
+           + _kTimerPickerLabelPadSize
+           + pickerPadding.start,
+    );
+
+    return IgnorePointer(
+      child: Container(
+        alignment: AlignmentDirectional.centerStart.resolve(textDirection),
+        padding: padding.resolve(textDirection),
+        child: SizedBox(
+          height: numberLabelHeight,
+          child: Baseline(
+            baseline: numberLabelBaseline,
+            baselineType: TextBaseline.alphabetic,
+            child: Text(
+              text,
+              style: const TextStyle(
+                fontSize: _kTimerPickerLabelFontSize,
+                fontWeight: FontWeight.w600,
+              ),
+              maxLines: 1,
+              softWrap: false,
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  // The picker has to be wider than its content, since the separators
+  // are part of the picker.
+  Widget _buildPickerNumberLabel(String text, EdgeInsetsDirectional padding) {
+    return Container(
+      width: _kTimerPickerColumnIntrinsicWidth + padding.horizontal,
+      padding: padding.resolve(textDirection),
+      alignment: AlignmentDirectional.centerStart.resolve(textDirection),
+      child: Container(
+        width: numberLabelWidth,
+        alignment: AlignmentDirectional.centerEnd.resolve(textDirection),
+        child: Text(text, softWrap: false, maxLines: 1, overflow: TextOverflow.visible),
+      ),
+    );
+  }
+
+  Widget _buildHourPicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(initialItem: selectedHour!),
+      magnification: _kMagnification,
+      offAxisFraction: _calculateOffAxisFraction(additionalPadding.start, 0),
+      itemExtent: _kItemExtent,
+      backgroundColor: widget.backgroundColor,
+      squeeze: _kSqueeze,
+      onSelectedItemChanged: (int index) {
+        setState(() {
+          selectedHour = index;
+          widget.onTimerDurationChanged(
+              Duration(
+                  hours: selectedHour!,
+                  minutes: selectedMinute,
+                  seconds: selectedSecond ?? 0));
+        });
+      },
+      children: List<Widget>.generate(24, (int index) {
+        final String label = localizations.timerPickerHourLabel(index) ?? '';
+        final String semanticsLabel = textDirectionFactor == 1
+            ? localizations.timerPickerHour(index) + label
+            : label + localizations.timerPickerHour(index);
+
+        return Semantics(
+          label: semanticsLabel,
+          excludeSemantics: true,
+          child: _buildPickerNumberLabel(localizations.timerPickerHour(index), additionalPadding),
+        );
+      }),
+      selectionOverlay: selectionOverlay,
+    );
+  }
+
+  Widget _buildHourColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    additionalPadding = EdgeInsetsDirectional.only(
+      start: math.max(additionalPadding.start, 0),
+      end: math.max(additionalPadding.end, 0),
+    );
+
+    return Stack(
+      children: <Widget>[
+        NotificationListener<ScrollEndNotification>(
+          onNotification: (ScrollEndNotification notification) {
+            setState(() { lastSelectedHour = selectedHour; });
+            return false;
+          },
+          child: _buildHourPicker(additionalPadding, selectionOverlay),
+        ),
+        _buildLabel(
+          localizations.timerPickerHourLabel(lastSelectedHour ?? selectedHour!) ?? '',
+          additionalPadding,
+        ),
+      ],
+    );
+  }
+
+  Widget _buildMinutePicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(
+        initialItem: selectedMinute ~/ widget.minuteInterval,
+      ),
+      magnification: _kMagnification,
+      offAxisFraction: _calculateOffAxisFraction(
+          additionalPadding.start,
+          widget.mode == CupertinoTimerPickerMode.ms ? 0 : 1
+      ),
+      itemExtent: _kItemExtent,
+      backgroundColor: widget.backgroundColor,
+      squeeze: _kSqueeze,
+      looping: true,
+      onSelectedItemChanged: (int index) {
+        setState(() {
+          selectedMinute = index * widget.minuteInterval;
+          widget.onTimerDurationChanged(
+              Duration(
+                  hours: selectedHour ?? 0,
+                  minutes: selectedMinute,
+                  seconds: selectedSecond ?? 0));
+        });
+      },
+      children: List<Widget>.generate(60 ~/ widget.minuteInterval, (int index) {
+        final int minute = index * widget.minuteInterval;
+        final String label = localizations.timerPickerMinuteLabel(minute) ?? '';
+        final String semanticsLabel = textDirectionFactor == 1
+            ? localizations.timerPickerMinute(minute) + label
+            : label + localizations.timerPickerMinute(minute);
+
+        return Semantics(
+          label: semanticsLabel,
+          excludeSemantics: true,
+          child: _buildPickerNumberLabel(localizations.timerPickerMinute(minute), additionalPadding),
+        );
+      }),
+      selectionOverlay: selectionOverlay,
+    );
+  }
+
+  Widget _buildMinuteColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    additionalPadding = EdgeInsetsDirectional.only(
+      start: math.max(additionalPadding.start, 0),
+      end: math.max(additionalPadding.end, 0),
+    );
+
+    return Stack(
+      children: <Widget>[
+        NotificationListener<ScrollEndNotification>(
+          onNotification: (ScrollEndNotification notification) {
+            setState(() { lastSelectedMinute = selectedMinute; });
+            return false;
+          },
+          child: _buildMinutePicker(additionalPadding, selectionOverlay),
+        ),
+        _buildLabel(
+          localizations.timerPickerMinuteLabel(lastSelectedMinute ?? selectedMinute) ?? '',
+          additionalPadding,
+        ),
+      ],
+    );
+  }
+
+  Widget _buildSecondPicker(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    return CupertinoPicker(
+      scrollController: FixedExtentScrollController(
+        initialItem: selectedSecond! ~/ widget.secondInterval,
+      ),
+      magnification: _kMagnification,
+      offAxisFraction: _calculateOffAxisFraction(
+          additionalPadding.start,
+          widget.mode == CupertinoTimerPickerMode.ms ? 1 : 2
+      ),
+      itemExtent: _kItemExtent,
+      backgroundColor: widget.backgroundColor,
+      squeeze: _kSqueeze,
+      looping: true,
+      onSelectedItemChanged: (int index) {
+        setState(() {
+          selectedSecond = index * widget.secondInterval;
+          widget.onTimerDurationChanged(
+              Duration(
+                  hours: selectedHour ?? 0,
+                  minutes: selectedMinute,
+                  seconds: selectedSecond!));
+        });
+      },
+      children: List<Widget>.generate(60 ~/ widget.secondInterval, (int index) {
+        final int second = index * widget.secondInterval;
+        final String label = localizations.timerPickerSecondLabel(second) ?? '';
+        final String semanticsLabel = textDirectionFactor == 1
+            ? localizations.timerPickerSecond(second) + label
+            : label + localizations.timerPickerSecond(second);
+
+        return Semantics(
+          label: semanticsLabel,
+          excludeSemantics: true,
+          child: _buildPickerNumberLabel(localizations.timerPickerSecond(second), additionalPadding),
+        );
+      }),
+      selectionOverlay: selectionOverlay,
+    );
+  }
+
+  Widget _buildSecondColumn(EdgeInsetsDirectional additionalPadding, Widget selectionOverlay) {
+    additionalPadding = EdgeInsetsDirectional.only(
+      start: math.max(additionalPadding.start, 0),
+      end: math.max(additionalPadding.end, 0),
+    );
+
+    return Stack(
+      children: <Widget>[
+        NotificationListener<ScrollEndNotification>(
+          onNotification: (ScrollEndNotification notification) {
+            setState(() { lastSelectedSecond = selectedSecond; });
+            return false;
+          },
+          child: _buildSecondPicker(additionalPadding, selectionOverlay),
+        ),
+        _buildLabel(
+          localizations.timerPickerSecondLabel(lastSelectedSecond ?? selectedSecond!) ?? '',
+          additionalPadding,
+        ),
+      ],
+    );
+  }
+
+  // Returns [CupertinoTextThemeData.pickerTextStyle] and magnifies the fontSize
+  // by [magnification].
+  TextStyle _textStyleFrom(BuildContext context, [double magnification = 1.0]) {
+    final TextStyle textStyle = CupertinoTheme.of(context).textTheme.pickerTextStyle;
+    return textStyle.copyWith(
+      fontSize: textStyle.fontSize! * magnification
+    );
+  }
+
+  // Calculate the number label center point by padding start and position to
+  // get a reasonable offAxisFraction.
+  double _calculateOffAxisFraction(double paddingStart, int position) {
+    final double centerPoint = paddingStart + (numberLabelWidth / 2);
+
+    // Compute the offAxisFraction needed to be straight within the pickerColumn.
+    final double pickerColumnOffAxisFraction =
+        0.5 - centerPoint / pickerColumnWidth;
+    // Position is to calculate the reasonable offAxisFraction in the picker.
+    final double timerPickerOffAxisFraction =
+        0.5 - (centerPoint + pickerColumnWidth * position) / totalWidth;
+    return (pickerColumnOffAxisFraction - timerPickerOffAxisFraction) * textDirectionFactor;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        // The timer picker can be divided into columns corresponding to hour,
+        // minute, and second. Each column consists of a scrollable and a fixed
+        // label on top of it.
+        List<Widget> columns;
+
+        if (widget.mode == CupertinoTimerPickerMode.hms){
+          // Pad the widget to make it as wide as `_kPickerWidth`.
+          pickerColumnWidth =
+              _kTimerPickerColumnIntrinsicWidth + (_kTimerPickerHalfColumnPadding * 2);
+          totalWidth = pickerColumnWidth * 3;
+        } else {
+          // The default totalWidth for 2-column modes.
+          totalWidth = _kPickerWidth;
+          pickerColumnWidth = totalWidth / 2;
+        }
+
+        if (constraints.maxWidth < totalWidth) {
+          totalWidth = constraints.maxWidth;
+          pickerColumnWidth =
+              totalWidth / (widget.mode == CupertinoTimerPickerMode.hms ? 3 : 2);
+        }
+
+        final double baseLabelContentWidth = numberLabelWidth + _kTimerPickerLabelPadSize;
+        final double minuteLabelContentWidth = baseLabelContentWidth + minuteLabelWidth;
+
+        switch (widget.mode) {
+          case CupertinoTimerPickerMode.hm:
+          // Pad the widget to make it as wide as `_kPickerWidth`.
+            final double hourLabelContentWidth = baseLabelContentWidth + hourLabelWidth;
+            double hourColumnStartPadding =
+                pickerColumnWidth - hourLabelContentWidth - _kTimerPickerHalfColumnPadding;
+            if (hourColumnStartPadding < _kTimerPickerMinHorizontalPadding)
+              hourColumnStartPadding = _kTimerPickerMinHorizontalPadding;
+
+            double minuteColumnEndPadding =
+                pickerColumnWidth - minuteLabelContentWidth - _kTimerPickerHalfColumnPadding;
+            if (minuteColumnEndPadding < _kTimerPickerMinHorizontalPadding)
+              minuteColumnEndPadding = _kTimerPickerMinHorizontalPadding;
+
+            columns = <Widget>[
+              _buildHourColumn(
+                  EdgeInsetsDirectional.only(
+                      start: hourColumnStartPadding,
+                      end: pickerColumnWidth - hourColumnStartPadding - hourLabelContentWidth
+                  ),
+                  _leftSelectionOverlay
+              ),
+              _buildMinuteColumn(
+                  EdgeInsetsDirectional.only(
+                      start: pickerColumnWidth - minuteColumnEndPadding - minuteLabelContentWidth,
+                      end: minuteColumnEndPadding
+                  ),
+                  _rightSelectionOverlay
+              ),
+            ];
+            break;
+          case CupertinoTimerPickerMode.ms:
+            final double secondLabelContentWidth = baseLabelContentWidth + secondLabelWidth;
+            double secondColumnEndPadding =
+                pickerColumnWidth - secondLabelContentWidth - _kTimerPickerHalfColumnPadding;
+            if (secondColumnEndPadding < _kTimerPickerMinHorizontalPadding)
+              secondColumnEndPadding = _kTimerPickerMinHorizontalPadding;
+
+            double minuteColumnStartPadding =
+                pickerColumnWidth - minuteLabelContentWidth - _kTimerPickerHalfColumnPadding;
+            if (minuteColumnStartPadding < _kTimerPickerMinHorizontalPadding)
+              minuteColumnStartPadding = _kTimerPickerMinHorizontalPadding;
+
+            columns = <Widget>[
+              _buildMinuteColumn(
+                  EdgeInsetsDirectional.only(
+                      start: minuteColumnStartPadding,
+                      end: pickerColumnWidth - minuteColumnStartPadding - minuteLabelContentWidth
+                  ),
+                  _leftSelectionOverlay
+              ),
+              _buildSecondColumn(
+                  EdgeInsetsDirectional.only(
+                      start: pickerColumnWidth - secondColumnEndPadding - minuteLabelContentWidth,
+                      end: secondColumnEndPadding
+                  ),
+                  _rightSelectionOverlay
+              ),
+            ];
+            break;
+          case CupertinoTimerPickerMode.hms:
+            final double hourColumnEndPadding =
+                pickerColumnWidth - baseLabelContentWidth - hourLabelWidth - _kTimerPickerMinHorizontalPadding;
+            final double minuteColumnPadding =
+                (pickerColumnWidth - minuteLabelContentWidth) / 2;
+            final double secondColumnStartPadding =
+                pickerColumnWidth - baseLabelContentWidth - secondLabelWidth - _kTimerPickerMinHorizontalPadding;
+
+            columns = <Widget>[
+              _buildHourColumn(
+                  EdgeInsetsDirectional.only(
+                      start: _kTimerPickerMinHorizontalPadding,
+                      end: math.max(hourColumnEndPadding, 0)
+                  ),
+                  _leftSelectionOverlay
+              ),
+              _buildMinuteColumn(
+                  EdgeInsetsDirectional.only(
+                      start: minuteColumnPadding,
+                      end: minuteColumnPadding
+                  ),
+                  _centerSelectionOverlay
+              ),
+              _buildSecondColumn(
+                  EdgeInsetsDirectional.only(
+                      start: math.max(secondColumnStartPadding, 0),
+                      end: _kTimerPickerMinHorizontalPadding
+                  ),
+                  _rightSelectionOverlay
+              ),
+            ];
+            break;
+        }
+        final CupertinoThemeData themeData = CupertinoTheme.of(context);
+        return MediaQuery(
+          // The native iOS picker's text scaling is fixed, so we will also fix it
+          // as well in our picker.
+          data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+          child: CupertinoTheme(
+            data: themeData.copyWith(
+              textTheme: themeData.textTheme.copyWith(
+                pickerTextStyle: _textStyleFrom(context, _kTimerPickerMagnification),
+              ),
+            ),
+            child: Align(
+              alignment: widget.alignment,
+              child: Container(
+                color: CupertinoDynamicColor.maybeResolve(widget.backgroundColor, context),
+                width: totalWidth,
+                height: _kPickerHeight,
+                child: DefaultTextStyle(
+                  style: _textStyleFrom(context),
+                  child: Row(children: columns.map((Widget child) => Expanded(child: child)).toList(growable: false)),
+                ),
+              ),
+            ),
+          ),
+        );
+      },
+    );
+  }
+}
diff --git a/lib/src/cupertino/debug.dart b/lib/src/cupertino/debug.dart
new file mode 100644
index 0000000..ac83642
--- /dev/null
+++ b/lib/src/cupertino/debug.dart
@@ -0,0 +1,46 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/foundation.dart';
+import 'package:flute/widgets.dart';
+
+import 'localizations.dart';
+
+/// Asserts that the given context has a [Localizations] ancestor that contains
+/// a [CupertinoLocalizations] delegate.
+///
+/// To call this function, use the following pattern, typically in the
+/// relevant Widget's build method:
+///
+/// ```dart
+/// assert(debugCheckHasCupertinoLocalizations(context));
+/// ```
+///
+/// Does nothing if asserts are disabled. Always returns true.
+bool debugCheckHasCupertinoLocalizations(BuildContext context) {
+  assert(() {
+    if (Localizations.of<CupertinoLocalizations>(context, CupertinoLocalizations) == null) {
+      throw FlutterError.fromParts(<DiagnosticsNode>[
+        ErrorSummary('No CupertinoLocalizations found.'),
+        ErrorDescription(
+          '${context.widget.runtimeType} widgets require CupertinoLocalizations '
+          'to be provided by a Localizations widget ancestor.'
+        ),
+        ErrorDescription(
+          'The cupertino library uses Localizations to generate messages, '
+          'labels, and abbreviations.'
+        ),
+        ErrorHint(
+          'To introduce a CupertinoLocalizations, either use a '
+          'CupertinoApp at the root of your application to include them '
+          'automatically, or add a Localization widget with a '
+          'CupertinoLocalizations delegate.'
+        ),
+        ...context.describeMissingAncestor(expectedAncestorType: CupertinoLocalizations)
+      ]);
+    }
+    return true;
+  }());
+  return true;
+}
diff --git a/lib/src/cupertino/dialog.dart b/lib/src/cupertino/dialog.dart
new file mode 100644
index 0000000..35ad624
--- /dev/null
+++ b/lib/src/cupertino/dialog.dart
@@ -0,0 +1,1817 @@
+// Copyright 2014 The Flutter Authors. 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:flute/ui.dart' show ImageFilter;
+
+import 'package:flute/foundation.dart';
+import 'package:flute/rendering.dart';
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'interface_level.dart';
+import 'localizations.dart';
+import 'scrollbar.dart';
+
+// TODO(abarth): These constants probably belong somewhere more general.
+
+// Used XD to flutter plugin(https://github.com/AdobeXD/xd-to-flutter-plugin/)
+// to derive values of TextStyle(height and letterSpacing) from
+// Adobe XD template for iOS 13, which can be found in
+// Apple Design Resources(https://developer.apple.com/design/resources/).
+// However the values are not exactly the same as native, so eyeballing is needed.
+const TextStyle _kCupertinoDialogTitleStyle = TextStyle(
+  fontFamily: '.SF UI Display',
+  inherit: false,
+  fontSize: 17.0,
+  fontWeight: FontWeight.w600,
+  height: 1.3,
+  letterSpacing: -0.5,
+  textBaseline: TextBaseline.alphabetic,
+);
+
+const TextStyle _kCupertinoDialogContentStyle = TextStyle(
+  fontFamily: '.SF UI Text',
+  inherit: false,
+  fontSize: 13.0,
+  fontWeight: FontWeight.w400,
+  height: 1.35,
+  letterSpacing: -0.2,
+  textBaseline: TextBaseline.alphabetic,
+);
+
+const TextStyle _kCupertinoDialogActionStyle = TextStyle(
+  fontFamily: '.SF UI Text',
+  inherit: false,
+  fontSize: 16.8,
+  fontWeight: FontWeight.w400,
+  textBaseline: TextBaseline.alphabetic,
+);
+
+// iOS dialogs have a normal display width and another display width that is
+// used when the device is in accessibility mode. Each of these widths are
+// listed below.
+const double _kCupertinoDialogWidth = 270.0;
+const double _kAccessibilityCupertinoDialogWidth = 310.0;
+
+const double _kBlurAmount = 20.0;
+const double _kEdgePadding = 20.0;
+const double _kMinButtonHeight = 45.0;
+const double _kMinButtonFontSize = 10.0;
+const double _kDialogCornerRadius = 14.0;
+const double _kDividerThickness = 1.0;
+
+// A translucent color that is painted on top of the blurred backdrop as the
+// dialog's background color
+// Extracted from https://developer.apple.com/design/resources/.
+const Color _kDialogColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xCCF2F2F2),
+  darkColor: Color(0xBF1E1E1E),
+);
+
+// Translucent light gray that is painted on top of the blurred backdrop as the
+// background color of a pressed button.
+// Eyeballed from iOS 13 beta simulator.
+const Color _kDialogPressedColor = CupertinoDynamicColor.withBrightness(
+  color: Color(0xFFE1E1E1),
+  darkColor: Color(0xFF2E2E2E),
+);
+
+// The alert dialog layout policy changes depending on whether the user is using
+// a "regular" font size vs a "large" font size. This is a spectrum. There are
+// many "regular" font sizes and many "large" font sizes. But depending on which
+// policy is currently being used, a dialog is laid out differently.
+//
+// Empirically, the jump from one policy to the other occurs at the following text
+// scale factors:
+// Largest regular scale factor:  1.3529411764705883
+// Smallest large scale factor:   1.6470588235294117
+//
+// The following constant represents a division in text scale factor beyond which
+// we want to change how the dialog is laid out.
+const double _kMaxRegularTextScaleFactor = 1.4;
+
+// Accessibility mode on iOS is determined by the text scale factor that the
+// user has selected.
+bool _isInAccessibilityMode(BuildContext context) {
+  final MediaQueryData? data = MediaQuery.maybeOf(context);
+  return data != null && data.textScaleFactor > _kMaxRegularTextScaleFactor;
+}
+
+/// An iOS-style alert dialog.
+///
+/// An alert dialog informs the user about situations that require
+/// acknowledgement. An alert dialog has an optional title, optional content,
+/// and an optional list of actions. The title is displayed above the content
+/// and the actions are displayed below the content.
+///
+/// This dialog styles its title and content (typically a message) to match the
+/// standard iOS title and message dialog text style. These default styles can
+/// be overridden by explicitly defining [TextStyle]s for [Text] widgets that
+/// are part of the title or content.
+///
+/// To display action buttons that look like standard iOS dialog buttons,
+/// provide [CupertinoDialogAction]s for the [actions] given to this dialog.
+///
+/// Typically passed as the child widget to [showDialog], which displays the
+/// dialog.
+///
+/// See also:
+///
+///  * [CupertinoPopupSurface], which is a generic iOS-style popup surface that
+///    holds arbitrary content to create custom popups.
+///  * [CupertinoDialogAction], which is an iOS-style dialog button.
+///  * [AlertDialog], a Material Design alert dialog.
+///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
+class CupertinoAlertDialog extends StatelessWidget {
+  /// Creates an iOS-style alert dialog.
+  ///
+  /// The [actions] must not be null.
+  const CupertinoAlertDialog({
+    Key? key,
+    this.title,
+    this.content,
+    this.actions = const <Widget>[],
+    this.scrollController,
+    this.actionScrollController,
+    this.insetAnimationDuration = const Duration(milliseconds: 100),
+    this.insetAnimationCurve = Curves.decelerate,
+  }) : assert(actions != null),
+       super(key: key);
+
+  /// The (optional) title of the dialog is displayed in a large font at the top
+  /// of the dialog.
+  ///
+  /// Typically a [Text] widget.
+  final Widget? title;
+
+  /// The (optional) content of the dialog is displayed in the center of the
+  /// dialog in a lighter font.
+  ///
+  /// Typically a [Text] widget.
+  final Widget? content;
+
+  /// The (optional) set of actions that are displayed at the bottom of the
+  /// dialog.
+  ///
+  /// Typically this is a list of [CupertinoDialogAction] widgets.
+  final List<Widget> actions;
+
+  /// A scroll controller that can be used to control the scrolling of the
+  /// [content] in the dialog.
+  ///
+  /// Defaults to null, and is typically not needed, since most alert messages
+  /// are short.
+  ///
+  /// See also:
+  ///
+  ///  * [actionScrollController], which can be used for controlling the actions
+  ///    section when there are many actions.
+  final ScrollController? scrollController;
+
+  /// A scroll controller that can be used to control the scrolling of the
+  /// actions in the dialog.
+  ///
+  /// Defaults to null, and is typically not needed.
+  ///
+  /// See also:
+  ///
+  ///  * [scrollController], which can be used for controlling the [content]
+  ///    section when it is long.
+  final ScrollController? actionScrollController;
+
+  /// {@macro flutter.material.dialog.insetAnimationDuration}
+  final Duration insetAnimationDuration;
+
+  /// {@macro flutter.material.dialog.insetAnimationCurve}
+  final Curve insetAnimationCurve;
+
+  Widget _buildContent(BuildContext context) {
+    final List<Widget> children = <Widget>[
+      if (title != null || content != null)
+        Flexible(
+          flex: 3,
+          child: _CupertinoAlertContentSection(
+            title: title,
+            content: content,
+            scrollController: scrollController,
+          ),
+        ),
+    ];
+
+    return Container(
+      color: CupertinoDynamicColor.resolve(_kDialogColor, context),
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.stretch,
+        children: children,
+      ),
+    );
+  }
+
+  Widget _buildActions() {
+    Widget actionSection = Container(
+      height: 0.0,
+    );
+    if (actions.isNotEmpty) {
+      actionSection = _CupertinoAlertActionSection(
+        children: actions,
+        scrollController: actionScrollController,
+      );
+    }
+
+    return actionSection;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final CupertinoLocalizations localizations = CupertinoLocalizations.of(context);
+    final bool isInAccessibilityMode = _isInAccessibilityMode(context);
+    final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+    return CupertinoUserInterfaceLevel(
+      data: CupertinoUserInterfaceLevelData.elevated,
+      child: MediaQuery(
+        data: MediaQuery.of(context).copyWith(
+          // iOS does not shrink dialog content below a 1.0 scale factor
+          textScaleFactor: math.max(textScaleFactor, 1.0),
+        ),
+        child: LayoutBuilder(
+          builder: (BuildContext context, BoxConstraints constraints) {
+            return AnimatedPadding(
+              padding: MediaQuery.of(context).viewInsets +
+                  const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
+              duration: insetAnimationDuration,
+              curve: insetAnimationCurve,
+              child: MediaQuery.removeViewInsets(
+                removeLeft: true,
+                removeTop: true,
+                removeRight: true,
+                removeBottom: true,
+                context: context,
+                child: Center(
+                  child: Container(
+                    margin: const EdgeInsets.symmetric(vertical: _kEdgePadding),
+                    width: isInAccessibilityMode
+                      ? _kAccessibilityCupertinoDialogWidth
+                      : _kCupertinoDialogWidth,
+                    child: CupertinoPopupSurface(
+                      isSurfacePainted: false,
+                      child: Semantics(
+                        namesRoute: true,
+                        scopesRoute: true,
+                        explicitChildNodes: true,
+                        label: localizations.alertDialogLabel,
+                        child: _CupertinoDialogRenderWidget(
+                          contentSection: _buildContent(context),
+                          actionsSection: _buildActions(),
+                        ),
+                      ),
+                    ),
+                  ),
+                ),
+              ),
+            );
+          },
+        ),
+      ),
+    );
+  }
+}
+
+/// An iOS-style dialog.
+///
+/// This dialog widget does not have any opinion about the contents of the
+/// dialog. Rather than using this widget directly, consider using
+/// [CupertinoAlertDialog], which implement a specific kind of dialog.
+///
+/// Push with `Navigator.of(..., rootNavigator: true)` when using with
+/// [CupertinoTabScaffold] to ensure that the dialog appears above the tabs.
+///
+/// See also:
+///
+///  * [CupertinoAlertDialog], which is a dialog with title, contents, and
+///    actions.
+///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
+@Deprecated(
+  'Use CupertinoAlertDialog for alert dialogs. Use CupertinoPopupSurface for custom popups. '
+  'This feature was deprecated after v0.2.3.'
+)
+class CupertinoDialog extends StatelessWidget {
+  /// Creates an iOS-style dialog.
+  const CupertinoDialog({
+    Key? key,
+    this.child,
+  }) : super(key: key);
+
+  /// The widget below this widget in the tree.
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    return Center(
+      child: SizedBox(
+        width: _kCupertinoDialogWidth,
+        child: CupertinoPopupSurface(
+          child: child,
+        ),
+      ),
+    );
+  }
+}
+
+/// Rounded rectangle surface that looks like an iOS popup surface, e.g., alert dialog
+/// and action sheet.
+///
+/// A [CupertinoPopupSurface] can be configured to paint or not paint a white
+/// color on top of its blurred area. Typical usage should paint white on top
+/// of the blur. However, the white paint can be disabled for the purpose of
+/// rendering divider gaps for a more complicated layout, e.g., [CupertinoAlertDialog].
+/// Additionally, the white paint can be disabled to render a blurred rounded
+/// rectangle without any color (similar to iOS's volume control popup).
+///
+/// See also:
+///
+///  * [CupertinoAlertDialog], which is a dialog with a title, content, and
+///    actions.
+///  * <https://developer.apple.com/ios/human-interface-guidelines/views/alerts/>
+class CupertinoPopupSurface extends StatelessWidget {
+  /// Creates an iOS-style rounded rectangle popup surface.
+  const CupertinoPopupSurface({
+    Key? key,
+    this.isSurfacePainted = true,
+    this.child,
+  }) : super(key: key);
+
+  /// Whether or not to paint a translucent white on top of this surface's
+  /// blurred background. [isSurfacePainted] should be true for a typical popup
+  /// that contains content without any dividers. A popup that requires dividers
+  /// should set [isSurfacePainted] to false and then paint its own surface area.
+  ///
+  /// Some popups, like iOS's volume control popup, choose to render a blurred
+  /// area without any white paint covering it. To achieve this effect,
+  /// [isSurfacePainted] should be set to false.
+  final bool isSurfacePainted;
+
+  /// The widget below this widget in the tree.
+  final Widget? child;
+
+  @override
+  Widget build(BuildContext context) {
+    return ClipRRect(
+      borderRadius: BorderRadius.circular(_kDialogCornerRadius),
+      child: BackdropFilter(
+        filter: ImageFilter.blur(sigmaX: _kBlurAmount, sigmaY: _kBlurAmount),
+        child: Container(
+          color: isSurfacePainted ? CupertinoDynamicColor.resolve(_kDialogColor, context) : null,
+          child: child,
+        ),
+      ),
+    );
+  }
+}
+
+// iOS style layout policy widget for sizing an alert dialog's content section and
+// action button section.
+//
+// See [_RenderCupertinoDialog] for specific layout policy details.
+class _CupertinoDialogRenderWidget extends RenderObjectWidget {
+  const _CupertinoDialogRenderWidget({
+    Key? key,
+    required this.contentSection,
+    required this.actionsSection,
+  }) : super(key: key);
+
+  final Widget contentSection;
+  final Widget actionsSection;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderCupertinoDialog(
+      dividerThickness: _kDividerThickness / MediaQuery.of(context).devicePixelRatio,
+      isInAccessibilityMode: _isInAccessibilityMode(context),
+      dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderCupertinoDialog renderObject) {
+    renderObject
+      ..isInAccessibilityMode = _isInAccessibilityMode(context)
+      ..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context);
+  }
+
+  @override
+  RenderObjectElement createElement() {
+    return _CupertinoDialogRenderElement(this);
+  }
+}
+
+class _CupertinoDialogRenderElement extends RenderObjectElement {
+  _CupertinoDialogRenderElement(_CupertinoDialogRenderWidget widget) : super(widget);
+
+  Element? _contentElement;
+  Element? _actionsElement;
+
+  @override
+  _CupertinoDialogRenderWidget get widget => super.widget as _CupertinoDialogRenderWidget;
+
+  @override
+  _RenderCupertinoDialog get renderObject => super.renderObject as _RenderCupertinoDialog;
+
+  @override
+  void visitChildren(ElementVisitor visitor) {
+    if (_contentElement != null) {
+      visitor(_contentElement!);
+    }
+    if (_actionsElement != null) {
+      visitor(_actionsElement!);
+    }
+  }
+
+  @override
+  void mount(Element? parent, dynamic newSlot) {
+    super.mount(parent, newSlot);
+    _contentElement = updateChild(_contentElement, widget.contentSection, _AlertDialogSections.contentSection);
+    _actionsElement = updateChild(_actionsElement, widget.actionsSection, _AlertDialogSections.actionsSection);
+  }
+
+  @override
+  void insertRenderObjectChild(RenderObject child, _AlertDialogSections slot) {
+    assert(slot != null);
+    switch (slot) {
+      case _AlertDialogSections.contentSection:
+        renderObject.contentSection = child as RenderBox;
+        break;
+      case _AlertDialogSections.actionsSection:
+        renderObject.actionsSection = child as RenderBox;
+        break;
+    }
+  }
+
+  @override
+  void moveRenderObjectChild(RenderObject child, _AlertDialogSections oldSlot, _AlertDialogSections newSlot) {
+    assert(false);
+  }
+
+  @override
+  void update(RenderObjectWidget newWidget) {
+    super.update(newWidget);
+    _contentElement = updateChild(_contentElement, widget.contentSection, _AlertDialogSections.contentSection);
+    _actionsElement = updateChild(_actionsElement, widget.actionsSection, _AlertDialogSections.actionsSection);
+  }
+
+  @override
+  void forgetChild(Element child) {
+    assert(child == _contentElement || child == _actionsElement);
+    if (_contentElement == child) {
+      _contentElement = null;
+    } else {
+      assert(_actionsElement == child);
+      _actionsElement = null;
+    }
+    super.forgetChild(child);
+  }
+
+  @override
+  void removeRenderObjectChild(RenderObject child, _AlertDialogSections slot) {
+    assert(child == renderObject.contentSection || child == renderObject.actionsSection);
+    if (renderObject.contentSection == child) {
+      renderObject.contentSection = null;
+    } else {
+      assert(renderObject.actionsSection == child);
+      renderObject.actionsSection = null;
+    }
+  }
+}
+
+// iOS style layout policy for sizing an alert dialog's content section and action
+// button section.
+//
+// The policy is as follows:
+//
+// If all content and buttons fit on screen:
+// The content section and action button section are sized intrinsically and centered
+// vertically on screen.
+//
+// If all content and buttons do not fit on screen, and iOS is NOT in accessibility mode:
+// A minimum height for the action button section is calculated. The action
+// button section will not be rendered shorter than this minimum.  See
+// [_RenderCupertinoDialogActions] for the minimum height calculation.
+//
+// With the minimum action button section calculated, the content section can
+// take up as much space as is available, up to the point that it hits the
+// minimum button height at the bottom.
+//
+// After the content section is laid out, the action button section is allowed
+// to take up any remaining space that was not consumed by the content section.
+//
+// If all content and buttons do not fit on screen, and iOS IS in accessibility mode:
+// The button section is given up to 50% of the available height. Then the content
+// section is given whatever height remains.
+class _RenderCupertinoDialog extends RenderBox {
+  _RenderCupertinoDialog({
+    RenderBox? contentSection,
+    RenderBox? actionsSection,
+    double dividerThickness = 0.0,
+    bool isInAccessibilityMode = false,
+    required Color dividerColor,
+  }) : _contentSection = contentSection,
+       _actionsSection = actionsSection,
+       _dividerThickness = dividerThickness,
+       _isInAccessibilityMode = isInAccessibilityMode,
+       _dividerPaint = Paint()
+        ..color = dividerColor
+        ..style = PaintingStyle.fill;
+
+
+  RenderBox? get contentSection => _contentSection;
+  RenderBox? _contentSection;
+  set contentSection(RenderBox? newContentSection) {
+    if (newContentSection != _contentSection) {
+      if (_contentSection != null) {
+        dropChild(_contentSection!);
+      }
+      _contentSection = newContentSection;
+      if (_contentSection != null) {
+        adoptChild(_contentSection!);
+      }
+    }
+  }
+
+  RenderBox? get actionsSection => _actionsSection;
+  RenderBox? _actionsSection;
+  set actionsSection(RenderBox? newActionsSection) {
+    if (newActionsSection != _actionsSection) {
+      if (null != _actionsSection) {
+        dropChild(_actionsSection!);
+      }
+      _actionsSection = newActionsSection;
+      if (null != _actionsSection) {
+        adoptChild(_actionsSection!);
+      }
+    }
+  }
+
+  bool get isInAccessibilityMode => _isInAccessibilityMode;
+  bool _isInAccessibilityMode;
+  set isInAccessibilityMode(bool newValue) {
+    if (newValue != _isInAccessibilityMode) {
+      _isInAccessibilityMode = newValue;
+      markNeedsLayout();
+    }
+  }
+
+  double get _dialogWidth => isInAccessibilityMode
+      ? _kAccessibilityCupertinoDialogWidth
+      : _kCupertinoDialogWidth;
+
+  final double _dividerThickness;
+  final Paint _dividerPaint;
+
+  Color get dividerColor => _dividerPaint.color;
+  set dividerColor(Color newValue) {
+    if (dividerColor == newValue) {
+      return;
+    }
+
+    _dividerPaint.color = newValue;
+    markNeedsPaint();
+  }
+
+  @override
+  void attach(PipelineOwner owner) {
+    super.attach(owner);
+    if (null != contentSection) {
+      contentSection!.attach(owner);
+    }
+    if (null != actionsSection) {
+      actionsSection!.attach(owner);
+    }
+  }
+
+  @override
+  void detach() {
+    super.detach();
+    if (null != contentSection) {
+      contentSection!.detach();
+    }
+    if (null != actionsSection) {
+      actionsSection!.detach();
+    }
+  }
+
+  @override
+  void redepthChildren() {
+    if (null != contentSection) {
+      redepthChild(contentSection!);
+    }
+    if (null != actionsSection) {
+      redepthChild(actionsSection!);
+    }
+  }
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! BoxParentData) {
+      child.parentData = BoxParentData();
+    }
+  }
+
+  @override
+  void visitChildren(RenderObjectVisitor visitor) {
+    if (contentSection != null) {
+      visitor(contentSection!);
+    }
+    if (actionsSection != null) {
+      visitor(actionsSection!);
+    }
+  }
+
+  @override
+  List<DiagnosticsNode> debugDescribeChildren() => <DiagnosticsNode>[
+    if (contentSection != null) contentSection!.toDiagnosticsNode(name: 'content'),
+    if (actionsSection != null) actionsSection!.toDiagnosticsNode(name: 'actions'),
+  ];
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return _dialogWidth;
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return _dialogWidth;
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    final double contentHeight = contentSection!.getMinIntrinsicHeight(width);
+    final double actionsHeight = actionsSection!.getMinIntrinsicHeight(width);
+    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
+    final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
+
+    if (height.isFinite)
+      return height;
+    return 0.0;
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    final double contentHeight = contentSection!.getMaxIntrinsicHeight(width);
+    final double actionsHeight = actionsSection!.getMaxIntrinsicHeight(width);
+    final bool hasDivider = contentHeight > 0.0 && actionsHeight > 0.0;
+    final double height = contentHeight + (hasDivider ? _dividerThickness : 0.0) + actionsHeight;
+
+    if (height.isFinite)
+      return height;
+    return 0.0;
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return _performLayout(
+      constraints: constraints,
+      layoutChild: ChildLayoutHelper.dryLayoutChild,
+    ).size;
+  }
+
+  @override
+  void performLayout() {
+    final _DialogSizes dialogSizes = _performLayout(
+      constraints: constraints,
+      layoutChild: ChildLayoutHelper.layoutChild,
+    );
+    size = dialogSizes.size;
+
+    // Set the position of the actions box to sit at the bottom of the dialog.
+    // The content box defaults to the top left, which is where we want it.
+    assert(actionsSection!.parentData is BoxParentData);
+    final BoxParentData actionParentData = actionsSection!.parentData! as BoxParentData;
+    actionParentData.offset = Offset(0.0, dialogSizes.actionSectionYOffset);
+  }
+
+  _DialogSizes _performLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
+    return isInAccessibilityMode
+        ? performAccessibilityLayout(
+          constraints: constraints,
+          layoutChild: layoutChild,
+        ) : performRegularLayout(
+          constraints: constraints,
+          layoutChild: layoutChild,
+        );
+  }
+
+  // When not in accessibility mode, an alert dialog might reduce the space
+  // for buttons to just over 1 button's height to make room for the content
+  // section.
+  _DialogSizes performRegularLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
+    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0
+        && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0;
+    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;
+
+    final double minActionsHeight = actionsSection!.getMinIntrinsicHeight(_dialogWidth);
+
+    final Size contentSize = layoutChild(
+      contentSection!,
+      constraints.deflate(EdgeInsets.only(bottom: minActionsHeight + dividerThickness)),
+    );
+
+    final Size actionsSize = layoutChild(
+      actionsSection!,
+      constraints.deflate(EdgeInsets.only(top: contentSize.height + dividerThickness)),
+    );
+
+    final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;
+
+    return _DialogSizes(
+      size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
+      actionSectionYOffset: contentSize.height + dividerThickness,
+    );
+  }
+
+  // When in accessibility mode, an alert dialog will allow buttons to take
+  // up to 50% of the dialog height, even if the content exceeds available space.
+  _DialogSizes performAccessibilityLayout({required BoxConstraints constraints, required ChildLayouter layoutChild}) {
+    final bool hasDivider = contentSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0
+        && actionsSection!.getMaxIntrinsicHeight(_dialogWidth) > 0.0;
+    final double dividerThickness = hasDivider ? _dividerThickness : 0.0;
+
+    final double maxContentHeight = contentSection!.getMaxIntrinsicHeight(_dialogWidth);
+    final double maxActionsHeight = actionsSection!.getMaxIntrinsicHeight(_dialogWidth);
+
+    final Size contentSize;
+    final Size actionsSize;
+    if (maxContentHeight + dividerThickness + maxActionsHeight > constraints.maxHeight) {
+      // There isn't enough room for everything. Following iOS's accessibility dialog
+      // layout policy, first we allow the actions to take up to 50% of the dialog
+      // height. Second we fill the rest of the available space with the content
+      // section.
+
+      actionsSize = layoutChild(
+        actionsSection!,
+        constraints.deflate(EdgeInsets.only(top: constraints.maxHeight / 2.0)),
+      );
+
+      contentSize = layoutChild(
+        contentSection!,
+        constraints.deflate(EdgeInsets.only(bottom: actionsSize.height + dividerThickness)),
+      );
+    } else {
+      // Everything fits. Give content and actions all the space they want.
+
+      contentSize = layoutChild(
+        contentSection!,
+        constraints,
+      );
+
+      actionsSize = layoutChild(
+        actionsSection!,
+        constraints.deflate(EdgeInsets.only(top: contentSize.height)),
+      );
+    }
+
+    // Calculate overall dialog height.
+    final double dialogHeight = contentSize.height + dividerThickness + actionsSize.height;
+
+    return _DialogSizes(
+      size: constraints.constrain(Size(_dialogWidth, dialogHeight)),
+      actionSectionYOffset: contentSize.height + dividerThickness,
+    );
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    final BoxParentData contentParentData = contentSection!.parentData! as BoxParentData;
+    contentSection!.paint(context, offset + contentParentData.offset);
+
+    final bool hasDivider = contentSection!.size.height > 0.0 && actionsSection!.size.height > 0.0;
+    if (hasDivider) {
+      _paintDividerBetweenContentAndActions(context.canvas, offset);
+    }
+
+    final BoxParentData actionsParentData = actionsSection!.parentData! as BoxParentData;
+    actionsSection!.paint(context, offset + actionsParentData.offset);
+  }
+
+  void _paintDividerBetweenContentAndActions(Canvas canvas, Offset offset) {
+    canvas.drawRect(
+      Rect.fromLTWH(
+        offset.dx,
+        offset.dy + contentSection!.size.height,
+        size.width,
+        _dividerThickness,
+      ),
+      _dividerPaint,
+    );
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+    final BoxParentData contentSectionParentData = contentSection!.parentData! as BoxParentData;
+    final BoxParentData actionsSectionParentData = actionsSection!.parentData! as BoxParentData;
+    return result.addWithPaintOffset(
+             offset: contentSectionParentData.offset,
+             position: position,
+             hitTest: (BoxHitTestResult result, Offset transformed) {
+               assert(transformed == position - contentSectionParentData.offset);
+               return contentSection!.hitTest(result, position: transformed);
+             },
+           )
+        || result.addWithPaintOffset(
+             offset: actionsSectionParentData.offset,
+             position: position,
+             hitTest: (BoxHitTestResult result, Offset transformed) {
+               assert(transformed == position - actionsSectionParentData.offset);
+               return actionsSection!.hitTest(result, position: transformed);
+             },
+           );
+  }
+}
+
+class _DialogSizes {
+  const _DialogSizes({required this.size, required this.actionSectionYOffset});
+
+  final Size size;
+  final double actionSectionYOffset;
+}
+
+// Visual components of an alert dialog that need to be explicitly sized and
+// laid out at runtime.
+enum _AlertDialogSections {
+  contentSection,
+  actionsSection,
+}
+
+// The "content section" of a CupertinoAlertDialog.
+//
+// If title is missing, then only content is added.  If content is
+// missing, then only title is added. If both are missing, then it returns
+// a SingleChildScrollView with a zero-sized Container.
+class _CupertinoAlertContentSection extends StatelessWidget {
+  const _CupertinoAlertContentSection({
+    Key? key,
+    this.title,
+    this.content,
+    this.scrollController,
+  }) : super(key: key);
+
+  // The (optional) title of the dialog is displayed in a large font at the top
+  // of the dialog.
+  //
+  // Typically a Text widget.
+  final Widget? title;
+
+  // The (optional) content of the dialog is displayed in the center of the
+  // dialog in a lighter font.
+  //
+  // Typically a Text widget.
+  final Widget? content;
+
+  // A scroll controller that can be used to control the scrolling of the
+  // content in the dialog.
+  //
+  // Defaults to null, and is typically not needed, since most alert contents
+  // are short.
+  final ScrollController? scrollController;
+
+  @override
+  Widget build(BuildContext context) {
+    if (title == null && content == null) {
+      return SingleChildScrollView(
+        controller: scrollController,
+        child: const SizedBox(width: 0.0, height: 0.0),
+      );
+    }
+
+    final double textScaleFactor = MediaQuery.of(context).textScaleFactor;
+    final List<Widget> titleContentGroup = <Widget>[
+      if (title != null)
+        Padding(
+          padding: EdgeInsets.only(
+            left: _kEdgePadding,
+            right: _kEdgePadding,
+            bottom: content == null ? _kEdgePadding : 1.0,
+            top: _kEdgePadding * textScaleFactor,
+          ),
+          child: DefaultTextStyle(
+            style: _kCupertinoDialogTitleStyle.copyWith(
+              color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
+            ),
+            textAlign: TextAlign.center,
+            child: title!,
+          ),
+        ),
+      if (content != null)
+        Padding(
+          padding: EdgeInsets.only(
+            left: _kEdgePadding,
+            right: _kEdgePadding,
+            bottom: _kEdgePadding * textScaleFactor,
+            top: title == null ? _kEdgePadding : 1.0,
+          ),
+          child: DefaultTextStyle(
+            style: _kCupertinoDialogContentStyle.copyWith(
+              color: CupertinoDynamicColor.resolve(CupertinoColors.label, context),
+            ),
+            textAlign: TextAlign.center,
+            child: content!,
+          ),
+        ),
+    ];
+
+    return CupertinoScrollbar(
+      child: SingleChildScrollView(
+        controller: scrollController,
+        child: Column(
+          mainAxisSize: MainAxisSize.max,
+          crossAxisAlignment: CrossAxisAlignment.stretch,
+          children: titleContentGroup,
+        ),
+      ),
+    );
+  }
+}
+
+// The "actions section" of a [CupertinoAlertDialog].
+//
+// See [_RenderCupertinoDialogActions] for details about action button sizing
+// and layout.
+class _CupertinoAlertActionSection extends StatefulWidget {
+  const _CupertinoAlertActionSection({
+    Key? key,
+    required this.children,
+    this.scrollController,
+  }) : assert(children != null),
+       super(key: key);
+
+  final List<Widget> children;
+
+  // A scroll controller that can be used to control the scrolling of the
+  // actions in the dialog.
+  //
+  // Defaults to null, and is typically not needed, since most alert dialogs
+  // don't have many actions.
+  final ScrollController? scrollController;
+
+  @override
+  _CupertinoAlertActionSectionState createState() => _CupertinoAlertActionSectionState();
+}
+
+class _CupertinoAlertActionSectionState extends State<_CupertinoAlertActionSection> {
+  @override
+  Widget build(BuildContext context) {
+    final double devicePixelRatio = MediaQuery.of(context).devicePixelRatio;
+
+    final List<Widget> interactiveButtons = <Widget>[];
+    for (int i = 0; i < widget.children.length; i += 1) {
+      interactiveButtons.add(
+        _PressableActionButton(
+          child: widget.children[i],
+        ),
+      );
+    }
+
+    return CupertinoScrollbar(
+      child: SingleChildScrollView(
+        controller: widget.scrollController,
+        child: _CupertinoDialogActionsRenderWidget(
+          actionButtons: interactiveButtons,
+          dividerThickness: _kDividerThickness / devicePixelRatio,
+        ),
+      ),
+    );
+  }
+}
+
+// Button that updates its render state when pressed.
+//
+// The pressed state is forwarded to an _ActionButtonParentDataWidget. The
+// corresponding _ActionButtonParentData is then interpreted and rendered
+// appropriately by _RenderCupertinoDialogActions.
+class _PressableActionButton extends StatefulWidget {
+  const _PressableActionButton({
+    required this.child,
+  });
+
+  final Widget child;
+
+  @override
+  _PressableActionButtonState createState() => _PressableActionButtonState();
+}
+
+class _PressableActionButtonState extends State<_PressableActionButton> {
+  bool _isPressed = false;
+
+  @override
+  Widget build(BuildContext context) {
+    return _ActionButtonParentDataWidget(
+      isPressed: _isPressed,
+      child: MergeSemantics(
+        // TODO(mattcarroll): Button press dynamics need overhaul for iOS:
+        // https://github.com/flutter/flutter/issues/19786
+        child: GestureDetector(
+          excludeFromSemantics: true,
+          behavior: HitTestBehavior.opaque,
+          onTapDown: (TapDownDetails details) => setState(() {
+            _isPressed = true;
+          }),
+          onTapUp: (TapUpDetails details) => setState(() {
+            _isPressed = false;
+          }),
+          // TODO(mattcarroll): Cancel is currently triggered when user moves
+          //  past slop instead of off button: https://github.com/flutter/flutter/issues/19783
+          onTapCancel: () => setState(() => _isPressed = false),
+          child: widget.child,
+        ),
+      ),
+    );
+  }
+}
+
+// ParentDataWidget that updates _ActionButtonParentData for an action button.
+//
+// Each action button requires knowledge of whether or not it is pressed so that
+// the dialog can correctly render the button. The pressed state is held within
+// _ActionButtonParentData. _ActionButtonParentDataWidget is responsible for
+// updating the pressed state of an _ActionButtonParentData based on the
+// incoming [isPressed] property.
+class _ActionButtonParentDataWidget extends ParentDataWidget<_ActionButtonParentData> {
+  const _ActionButtonParentDataWidget({
+    Key? key,
+    required this.isPressed,
+    required Widget child,
+  }) : super(key: key, child: child);
+
+  final bool isPressed;
+
+  @override
+  void applyParentData(RenderObject renderObject) {
+    assert(renderObject.parentData is _ActionButtonParentData);
+    final _ActionButtonParentData parentData = renderObject.parentData! as _ActionButtonParentData;
+    if (parentData.isPressed != isPressed) {
+      parentData.isPressed = isPressed;
+
+      // Force a repaint.
+      final AbstractNode? targetParent = renderObject.parent;
+      if (targetParent is RenderObject)
+        targetParent.markNeedsPaint();
+    }
+  }
+
+  @override
+  Type get debugTypicalAncestorWidgetClass => _CupertinoDialogActionsRenderWidget;
+}
+
+// ParentData applied to individual action buttons that report whether or not
+// that button is currently pressed by the user.
+class _ActionButtonParentData extends MultiChildLayoutParentData {
+  _ActionButtonParentData({
+    this.isPressed = false,
+  });
+
+  bool isPressed;
+}
+
+/// A button typically used in a [CupertinoAlertDialog].
+///
+/// See also:
+///
+///  * [CupertinoAlertDialog], a dialog that informs the user about situations
+///    that require acknowledgement.
+class CupertinoDialogAction extends StatelessWidget {
+  /// Creates an action for an iOS-style dialog.
+  const CupertinoDialogAction({
+    Key? key,
+    this.onPressed,
+    this.isDefaultAction = false,
+    this.isDestructiveAction = false,
+    this.textStyle,
+    required this.child,
+  }) : assert(child != null),
+       assert(isDefaultAction != null),
+       assert(isDestructiveAction != null),
+       super(key: key);
+
+  /// The callback that is called when the button is tapped or otherwise
+  /// activated.
+  ///
+  /// If this is set to null, the button will be disabled.
+  final VoidCallback? onPressed;
+
+  /// Set to true if button is the default choice in the dialog.
+  ///
+  /// Default buttons have bold text. Similar to
+  /// [UIAlertController.preferredAction](https://developer.apple.com/documentation/uikit/uialertcontroller/1620102-preferredaction),
+  /// but more than one action can have this attribute set to true in the same
+  /// [CupertinoAlertDialog].
+  ///
+  /// This parameters defaults to false and cannot be null.
+  final bool isDefaultAction;
+
+  /// Whether this action destroys an object.
+  ///
+  /// For example, an action that deletes an email is destructive.
+  ///
+  /// Defaults to false and cannot be null.
+  final bool isDestructiveAction;
+
+  /// [TextStyle] to apply to any text that appears in this button.
+  ///
+  /// Dialog actions have a built-in text resizing policy for long text. To
+  /// ensure that this resizing policy always works as expected, [textStyle]
+  /// must be used if a text size is desired other than that specified in
+  /// [_kCupertinoDialogActionStyle].
+  final TextStyle? textStyle;
+
+  /// The widget below this widget in the tree.
+  ///
+  /// Typically a [Text] widget.
+  final Widget child;
+
+  /// Whether the button is enabled or disabled. Buttons are disabled by
+  /// default. To enable a button, set its [onPressed] property to a non-null
+  /// value.
+  bool get enabled => onPressed != null;
+
+  double _calculatePadding(BuildContext context) {
+    return 8.0 * MediaQuery.textScaleFactorOf(context);
+  }
+
+  // Dialog action content shrinks to fit, up to a certain point, and if it still
+  // cannot fit at the minimum size, the text content is ellipsized.
+  //
+  // This policy only applies when the device is not in accessibility mode.
+  Widget _buildContentWithRegularSizingPolicy({
+    required BuildContext context,
+    required TextStyle textStyle,
+    required Widget content,
+  }) {
+    final bool isInAccessibilityMode = _isInAccessibilityMode(context);
+    final double dialogWidth = isInAccessibilityMode
+        ? _kAccessibilityCupertinoDialogWidth
+        : _kCupertinoDialogWidth;
+    final double textScaleFactor = MediaQuery.textScaleFactorOf(context);
+    // The fontSizeRatio is the ratio of the current text size (including any
+    // iOS scale factor) vs the minimum text size that we allow in action
+    // buttons. This ratio information is used to automatically scale down action
+    // button text to fit the available space.
+    final double fontSizeRatio = (textScaleFactor * textStyle.fontSize!) / _kMinButtonFontSize;
+    final double padding = _calculatePadding(context);
+
+    return IntrinsicHeight(
+      child: SizedBox(
+        width: double.infinity,
+        child: FittedBox(
+          fit: BoxFit.scaleDown,
+          child: ConstrainedBox(
+            constraints: BoxConstraints(
+              maxWidth: fontSizeRatio * (dialogWidth - (2 * padding)),
+            ),
+            child: Semantics(
+              button: true,
+              onTap: onPressed,
+              child: DefaultTextStyle(
+                style: textStyle,
+                textAlign: TextAlign.center,
+                overflow: TextOverflow.ellipsis,
+                maxLines: 1,
+                child: content,
+              ),
+            ),
+          ),
+        ),
+      ),
+    );
+  }
+
+  // Dialog action content is permitted to be as large as it wants when in
+  // accessibility mode. If text is used as the content, the text wraps instead
+  // of ellipsizing.
+  Widget _buildContentWithAccessibilitySizingPolicy({
+    required TextStyle textStyle,
+    required Widget content,
+  }) {
+    return DefaultTextStyle(
+      style: textStyle,
+      textAlign: TextAlign.center,
+      child: content,
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    TextStyle style = _kCupertinoDialogActionStyle.copyWith(
+      color: CupertinoDynamicColor.resolve(
+        isDestructiveAction ?  CupertinoColors.systemRed : CupertinoColors.systemBlue,
+        context,
+      ),
+    );
+    style = style.merge(textStyle);
+
+    if (isDefaultAction) {
+      style = style.copyWith(fontWeight: FontWeight.w600);
+    }
+
+    if (!enabled) {
+      style = style.copyWith(color: style.color!.withOpacity(0.5));
+    }
+
+    // Apply a sizing policy to the action button's content based on whether or
+    // not the device is in accessibility mode.
+    // TODO(mattcarroll): The following logic is not entirely correct. It is also
+    // the case that if content text does not contain a space, it should also
+    // wrap instead of ellipsizing. We are consciously not implementing that
+    // now due to complexity.
+    final Widget sizedContent = _isInAccessibilityMode(context)
+      ? _buildContentWithAccessibilitySizingPolicy(
+          textStyle: style,
+          content: child,
+        )
+      : _buildContentWithRegularSizingPolicy(
+          context: context,
+          textStyle: style,
+          content: child,
+        );
+
+    return GestureDetector(
+      excludeFromSemantics: true,
+      onTap: onPressed,
+      behavior: HitTestBehavior.opaque,
+      child: ConstrainedBox(
+        constraints: const BoxConstraints(
+          minHeight: _kMinButtonHeight,
+        ),
+        child: Container(
+          alignment: Alignment.center,
+          padding: EdgeInsets.all(_calculatePadding(context)),
+          child: sizedContent,
+        ),
+      ),
+    );
+  }
+}
+
+// iOS style dialog action button layout.
+//
+// [_CupertinoDialogActionsRenderWidget] does not provide any scrolling
+// behavior for its buttons. It only handles the sizing and layout of buttons.
+// Scrolling behavior can be composed on top of this widget, if desired.
+//
+// See [_RenderCupertinoDialogActions] for specific layout policy details.
+class _CupertinoDialogActionsRenderWidget extends MultiChildRenderObjectWidget {
+  _CupertinoDialogActionsRenderWidget({
+    Key? key,
+    required List<Widget> actionButtons,
+    double dividerThickness = 0.0,
+  }) : _dividerThickness = dividerThickness,
+       super(key: key, children: actionButtons);
+
+  final double _dividerThickness;
+
+  @override
+  RenderObject createRenderObject(BuildContext context) {
+    return _RenderCupertinoDialogActions(
+      dialogWidth: _isInAccessibilityMode(context)
+        ? _kAccessibilityCupertinoDialogWidth
+        : _kCupertinoDialogWidth,
+      dividerThickness: _dividerThickness,
+      dialogColor: CupertinoDynamicColor.resolve(_kDialogColor, context),
+      dialogPressedColor: CupertinoDynamicColor.resolve(_kDialogPressedColor, context),
+      dividerColor: CupertinoDynamicColor.resolve(CupertinoColors.separator, context),
+    );
+  }
+
+  @override
+  void updateRenderObject(BuildContext context, _RenderCupertinoDialogActions renderObject) {
+    renderObject
+      ..dialogWidth = _isInAccessibilityMode(context)
+        ? _kAccessibilityCupertinoDialogWidth
+        : _kCupertinoDialogWidth
+      ..dividerThickness = _dividerThickness
+      ..dialogColor = CupertinoDynamicColor.resolve(_kDialogColor, context)
+      ..dialogPressedColor = CupertinoDynamicColor.resolve(_kDialogPressedColor, context)
+      ..dividerColor = CupertinoDynamicColor.resolve(CupertinoColors.separator, context);
+  }
+}
+
+// iOS style layout policy for sizing and positioning an alert dialog's action
+// buttons.
+//
+// The policy is as follows:
+//
+// If a single action button is provided, or if 2 action buttons are provided
+// that can fit side-by-side, then action buttons are sized and laid out in a
+// single horizontal row. The row is exactly as wide as the dialog, and the row
+// is as tall as the tallest action button. A horizontal divider is drawn above
+// the button row. If 2 action buttons are provided, a vertical divider is
+// drawn between them. The thickness of the divider is set by [dividerThickness].
+//
+// If 2 action buttons are provided but they cannot fit side-by-side, then the
+// 2 buttons are stacked vertically. A horizontal divider is drawn above each
+// button. The thickness of the divider is set by [dividerThickness]. The minimum
+// height of this [RenderBox] in the case of 2 stacked buttons is as tall as
+// the 2 buttons stacked. This is different than the 3+ button case where the
+// minimum height is only 1.5 buttons tall. See the 3+ button explanation for
+// more info.
+//
+// If 3+ action buttons are provided then they are all stacked vertically. A
+// horizontal divider is drawn above each button. The thickness of the divider
+// is set by [dividerThickness]. The minimum height of this [RenderBox] in the case
+// of 3+ stacked buttons is as tall as the 1st button + 50% the height of the
+// 2nd button. In other words, the minimum height is 1.5 buttons tall. This
+// minimum height of 1.5 buttons is expected to work in tandem with a surrounding
+// [ScrollView] to match the iOS dialog behavior.
+//
+// Each button is expected to have an _ActionButtonParentData which reports
+// whether or not that button is currently pressed. If a button is pressed,
+// then the dividers above and below that pressed button are not drawn - instead
+// they are filled with the standard white dialog background color. The one
+// exception is the very 1st divider which is always rendered. This policy comes
+// from observation of native iOS dialogs.
+class _RenderCupertinoDialogActions extends RenderBox
+    with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
+        RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
+  _RenderCupertinoDialogActions({
+    List<RenderBox>? children,
+    required double dialogWidth,
+    double dividerThickness = 0.0,
+    required Color dialogColor,
+    required Color dialogPressedColor,
+    required Color dividerColor,
+  }) : _dialogWidth = dialogWidth,
+       _buttonBackgroundPaint = Paint()
+        ..color = dialogColor
+        ..style = PaintingStyle.fill,
+        _pressedButtonBackgroundPaint = Paint()
+          ..color = dialogPressedColor
+          ..style = PaintingStyle.fill,
+        _dividerPaint = Paint()
+          ..color = dividerColor
+          ..style = PaintingStyle.fill,
+       _dividerThickness = dividerThickness {
+    addAll(children);
+  }
+
+  double get dialogWidth => _dialogWidth;
+  double _dialogWidth;
+  set dialogWidth(double newWidth) {
+    if (newWidth != _dialogWidth) {
+      _dialogWidth = newWidth;
+      markNeedsLayout();
+    }
+  }
+
+  // The thickness of the divider between buttons.
+  double get dividerThickness => _dividerThickness;
+  double _dividerThickness;
+  set dividerThickness(double newValue) {
+    if (newValue != _dividerThickness) {
+      _dividerThickness = newValue;
+      markNeedsLayout();
+    }
+  }
+
+  final Paint _buttonBackgroundPaint;
+  set dialogColor(Color value) {
+    if (value == _buttonBackgroundPaint.color)
+      return;
+
+    _buttonBackgroundPaint.color = value;
+    markNeedsPaint();
+  }
+
+  final Paint _pressedButtonBackgroundPaint;
+  set dialogPressedColor(Color value) {
+    if (value == _pressedButtonBackgroundPaint.color)
+      return;
+
+    _pressedButtonBackgroundPaint.color = value;
+    markNeedsPaint();
+  }
+
+  final Paint _dividerPaint;
+  set dividerColor(Color value) {
+    if (value == _dividerPaint.color)
+      return;
+
+    _dividerPaint.color = value;
+    markNeedsPaint();
+  }
+
+  Iterable<RenderBox> get _pressedButtons sync* {
+    RenderBox? currentChild = firstChild;
+    while (currentChild != null) {
+      assert(currentChild.parentData is _ActionButtonParentData);
+      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
+      if (parentData.isPressed) {
+        yield currentChild;
+      }
+      currentChild = childAfter(currentChild);
+    }
+  }
+
+  bool get _isButtonPressed {
+    RenderBox? currentChild = firstChild;
+    while (currentChild != null) {
+      assert(currentChild.parentData is _ActionButtonParentData);
+      final _ActionButtonParentData parentData = currentChild.parentData! as _ActionButtonParentData;
+      if (parentData.isPressed) {
+        return true;
+      }
+      currentChild = childAfter(currentChild);
+    }
+    return false;
+  }
+
+  @override
+  void setupParentData(RenderBox child) {
+    if (child.parentData is! _ActionButtonParentData)
+      child.parentData = _ActionButtonParentData();
+  }
+
+  @override
+  double computeMinIntrinsicWidth(double height) {
+    return dialogWidth;
+  }
+
+  @override
+  double computeMaxIntrinsicWidth(double height) {
+    return dialogWidth;
+  }
+
+  @override
+  double computeMinIntrinsicHeight(double width) {
+    final double minHeight;
+    if (childCount == 0) {
+      minHeight = 0.0;
+    } else if (childCount == 1) {
+      // If only 1 button, display the button across the entire dialog.
+      minHeight = _computeMinIntrinsicHeightSideBySide(width);
+    } else {
+      if (childCount == 2 && _isSingleButtonRow(width)) {
+        // The first 2 buttons fit side-by-side. Display them horizontally.
+        minHeight = _computeMinIntrinsicHeightSideBySide(width);
+      } else {
+        // 3+ buttons are always stacked. The minimum height when stacked is
+        // 1.5 buttons tall.
+        minHeight = _computeMinIntrinsicHeightStacked(width);
+      }
+    }
+    return minHeight;
+  }
+
+  // The minimum height for a single row of buttons is the larger of the buttons'
+  // min intrinsic heights.
+  double _computeMinIntrinsicHeightSideBySide(double width) {
+    assert(childCount >= 1 && childCount <= 2);
+
+    final double minHeight;
+    if (childCount == 1) {
+      minHeight = firstChild!.getMinIntrinsicHeight(width);
+    } else {
+      final double perButtonWidth = (width - dividerThickness) / 2.0;
+      minHeight = math.max(
+        firstChild!.getMinIntrinsicHeight(perButtonWidth),
+        lastChild!.getMinIntrinsicHeight(perButtonWidth),
+      );
+    }
+    return minHeight;
+  }
+
+  // The minimum height for 2+ stacked buttons is the height of the 1st button
+  // + 50% the height of the 2nd button + the divider between the two.
+  double _computeMinIntrinsicHeightStacked(double width) {
+    assert(childCount >= 2);
+
+    return firstChild!.getMinIntrinsicHeight(width)
+      + dividerThickness
+      + (0.5 * childAfter(firstChild!)!.getMinIntrinsicHeight(width));
+  }
+
+  @override
+  double computeMaxIntrinsicHeight(double width) {
+    final double maxHeight;
+    if (childCount == 0) {
+      // No buttons. Zero height.
+      maxHeight = 0.0;
+    } else if (childCount == 1) {
+      // One button. Our max intrinsic height is equal to the button's.
+      maxHeight = firstChild!.getMaxIntrinsicHeight(width);
+    } else if (childCount == 2) {
+      // Two buttons...
+      if (_isSingleButtonRow(width)) {
+        // The 2 buttons fit side by side so our max intrinsic height is equal
+        // to the taller of the 2 buttons.
+        final double perButtonWidth = (width - dividerThickness) / 2.0;
+        maxHeight = math.max(
+          firstChild!.getMaxIntrinsicHeight(perButtonWidth),
+          lastChild!.getMaxIntrinsicHeight(perButtonWidth),
+        );
+      } else {
+        // The 2 buttons do not fit side by side. Measure total height as a
+        // vertical stack.
+        maxHeight = _computeMaxIntrinsicHeightStacked(width);
+      }
+    } else {
+      // Three+ buttons. Stack the buttons vertically with dividers and measure
+      // the overall height.
+      maxHeight = _computeMaxIntrinsicHeightStacked(width);
+    }
+    return maxHeight;
+  }
+
+  // Max height of a stack of buttons is the sum of all button heights + a
+  // divider for each button.
+  double _computeMaxIntrinsicHeightStacked(double width) {
+    assert(childCount >= 2);
+
+    final double allDividersHeight = (childCount - 1) * dividerThickness;
+    double heightAccumulation = allDividersHeight;
+    RenderBox? button = firstChild;
+    while (button != null) {
+      heightAccumulation += button.getMaxIntrinsicHeight(width);
+      button = childAfter(button);
+    }
+    return heightAccumulation;
+  }
+
+  bool _isSingleButtonRow(double width) {
+    final bool isSingleButtonRow;
+    if (childCount == 1) {
+      isSingleButtonRow = true;
+    } else if (childCount == 2) {
+      // There are 2 buttons. If they can fit side-by-side then that's what
+      // we want to do. Otherwise, stack them vertically.
+      final double sideBySideWidth = firstChild!.getMaxIntrinsicWidth(double.infinity)
+          + dividerThickness
+          + lastChild!.getMaxIntrinsicWidth(double.infinity);
+      isSingleButtonRow = sideBySideWidth <= width;
+    } else {
+      isSingleButtonRow = false;
+    }
+    return isSingleButtonRow;
+  }
+
+  @override
+  Size computeDryLayout(BoxConstraints constraints) {
+    return _computeLayout(constraints: constraints, dry: true);
+  }
+
+  @override
+  void performLayout() {
+    size = _computeLayout(constraints: constraints, dry: false);
+  }
+
+  Size _computeLayout({required BoxConstraints constraints, bool dry = false}) {
+    final ChildLayouter layoutChild = dry
+        ? ChildLayoutHelper.dryLayoutChild
+        : ChildLayoutHelper.layoutChild;
+
+    if (_isSingleButtonRow(dialogWidth)) {
+      if (childCount == 1) {
+        // We have 1 button. Our size is the width of the dialog and the height
+        // of the single button.
+        final Size childSize = layoutChild(
+          firstChild!,
+          constraints,
+        );
+
+        return constraints.constrain(
+          Size(dialogWidth, childSize.height)
+        );
+      } else {
+        // Each button gets half the available width, minus a single divider.
+        final BoxConstraints perButtonConstraints = BoxConstraints(
+          minWidth: (constraints.minWidth - dividerThickness) / 2.0,
+          maxWidth: (constraints.maxWidth - dividerThickness) / 2.0,
+          minHeight: 0.0,
+          maxHeight: double.infinity,
+        );
+
+        // Layout the 2 buttons.
+        final Size firstChildSize = layoutChild(
+          firstChild!,
+          perButtonConstraints,
+        );
+        final Size lastChildSize = layoutChild(
+          lastChild!,
+          perButtonConstraints,
+        );
+
+        if (!dry) {
+          // The 2nd button needs to be offset to the right.
+          assert(lastChild!.parentData is MultiChildLayoutParentData);
+          final MultiChildLayoutParentData secondButtonParentData = lastChild!.parentData! as MultiChildLayoutParentData;
+          secondButtonParentData.offset = Offset(firstChildSize.width + dividerThickness, 0.0);
+        }
+
+        // Calculate our size based on the button sizes.
+        return constraints.constrain(
+          Size(
+            dialogWidth,
+            math.max(
+              firstChildSize.height,
+              lastChildSize.height,
+            ),
+          ),
+        );
+      }
+    } else {
+      // We need to stack buttons vertically, plus dividers above each button (except the 1st).
+      final BoxConstraints perButtonConstraints = constraints.copyWith(
+        minHeight: 0.0,
+        maxHeight: double.infinity,
+      );
+
+      RenderBox? child = firstChild;
+      int index = 0;
+      double verticalOffset = 0.0;
+      while (child != null) {
+        final Size childSize = layoutChild(
+          child,
+          perButtonConstraints,
+        );
+
+        if (!dry) {
+          assert(child.parentData is MultiChildLayoutParentData);
+          final MultiChildLayoutParentData parentData = child.parentData! as MultiChildLayoutParentData;
+          parentData.offset = Offset(0.0, verticalOffset);
+        }
+        verticalOffset += childSize.height;
+        if (index < childCount - 1) {
+          // Add a gap for the next divider.
+          verticalOffset += dividerThickness;
+        }
+
+        index += 1;
+        child = childAfter(child);
+      }
+
+      // Our height is the accumulated height of all buttons and dividers.
+      return constraints.constrain(
+        Size(dialogWidth, verticalOffset)
+      );
+    }
+  }
+
+  @override
+  void paint(PaintingContext context, Offset offset) {
+    final Canvas canvas = context.canvas;
+
+    if (_isSingleButtonRow(size.width)) {
+      _drawButtonBackgroundsAndDividersSingleRow(canvas, offset);
+    } else {
+      _drawButtonBackgroundsAndDividersStacked(canvas, offset);
+    }
+
+    _drawButtons(context, offset);
+  }
+
+  void _drawButtonBackgroundsAndDividersSingleRow(Canvas canvas, Offset offset) {
+    // The vertical divider sits between the left button and right button (if
+    // the dialog has 2 buttons).  The vertical divider is hidden if either the
+    // left or right button is pressed.
+    final Rect verticalDivider = childCount == 2 && !_isButtonPressed
+      ? Rect.fromLTWH(
+          offset.dx + firstChild!.size.width,
+          offset.dy,
+          dividerThickness,
+          math.max(
+            firstChild!.size.height,
+            lastChild!.size.height,
+          ),
+        )
+      : Rect.zero;
+
+    final List<Rect> pressedButtonRects = _pressedButtons.map<Rect>((RenderBox pressedButton) {
+      final MultiChildLayoutParentData buttonParentData = pressedButton.parentData! as MultiChildLayoutParentData;
+
+      return Rect.fromLTWH(
+        offset.dx + buttonParentData.offset.dx,
+        offset.dy + buttonParentData.offset.dy,
+        pressedButton.size.width,
+        pressedButton.size.height,
+      );
+    }).toList();
+
+    // Create the button backgrounds path and paint it.
+    final Path backgroundFillPath = Path()
+      ..fillType = PathFillType.evenOdd
+      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height))
+      ..addRect(verticalDivider);
+
+    for (int i = 0; i < pressedButtonRects.length; i += 1) {
+      backgroundFillPath.addRect(pressedButtonRects[i]);
+    }
+
+    canvas.drawPath(
+      backgroundFillPath,
+      _buttonBackgroundPaint,
+    );
+
+    // Create the pressed buttons background path and paint it.
+    final Path pressedBackgroundFillPath = Path();
+    for (int i = 0; i < pressedButtonRects.length; i += 1) {
+      pressedBackgroundFillPath.addRect(pressedButtonRects[i]);
+    }
+
+    canvas.drawPath(
+      pressedBackgroundFillPath,
+      _pressedButtonBackgroundPaint,
+    );
+
+    // Create the dividers path and paint it.
+    final Path dividersPath = Path()
+      ..addRect(verticalDivider);
+
+    canvas.drawPath(
+      dividersPath,
+      _dividerPaint,
+    );
+  }
+
+  void _drawButtonBackgroundsAndDividersStacked(Canvas canvas, Offset offset) {
+    final Offset dividerOffset = Offset(0.0, dividerThickness);
+
+    final Path backgroundFillPath = Path()
+      ..fillType = PathFillType.evenOdd
+      ..addRect(Rect.fromLTWH(0.0, 0.0, size.width, size.height));
+
+    final Path pressedBackgroundFillPath = Path();
+
+    final Path dividersPath = Path();
+
+    Offset accumulatingOffset = offset;
+
+    RenderBox? child = firstChild;
+    RenderBox? prevChild;
+    while (child != null) {
+      assert(child.parentData is _ActionButtonParentData);
+      final _ActionButtonParentData currentButtonParentData = child.parentData! as _ActionButtonParentData;
+      final bool isButtonPressed = currentButtonParentData.isPressed;
+
+      bool isPrevButtonPressed = false;
+      if (prevChild != null) {
+        assert(prevChild.parentData is _ActionButtonParentData);
+        final _ActionButtonParentData previousButtonParentData = prevChild.parentData! as _ActionButtonParentData;
+        isPrevButtonPressed = previousButtonParentData.isPressed;
+      }
+
+      final bool isDividerPresent = child != firstChild;
+      final bool isDividerPainted = isDividerPresent && !(isButtonPressed || isPrevButtonPressed);
+      final Rect dividerRect = Rect.fromLTWH(
+        accumulatingOffset.dx,
+        accumulatingOffset.dy,
+        size.width,
+        dividerThickness,
+      );
+
+      final Rect buttonBackgroundRect = Rect.fromLTWH(
+        accumulatingOffset.dx,
+        accumulatingOffset.dy + (isDividerPresent ? dividerThickness : 0.0),
+        size.width,
+        child.size.height,
+      );
+
+      // If this button is pressed, then we don't want a white background to be
+      // painted, so we erase this button from the background path.
+      if (isButtonPressed) {
+        backgroundFillPath.addRect(buttonBackgroundRect);
+        pressedBackgroundFillPath.addRect(buttonBackgroundRect);
+      }
+
+      // If this divider is needed, then we erase the divider area from the
+      // background path, and on top of that we paint a translucent gray to
+      // darken the divider area.
+      if (isDividerPainted) {
+        backgroundFillPath.addRect(dividerRect);
+        dividersPath.addRect(dividerRect);
+      }
+
+      accumulatingOffset += (isDividerPresent ? dividerOffset : Offset.zero)
+          + Offset(0.0, child.size.height);
+
+      prevChild = child;
+      child = childAfter(child);
+    }
+
+    canvas.drawPath(backgroundFillPath, _buttonBackgroundPaint);
+    canvas.drawPath(pressedBackgroundFillPath, _pressedButtonBackgroundPaint);
+    canvas.drawPath(dividersPath, _dividerPaint);
+  }
+
+  void _drawButtons(PaintingContext context, Offset offset) {
+    RenderBox? child = firstChild;
+    while (child != null) {
+      final MultiChildLayoutParentData childParentData = child.parentData! as MultiChildLayoutParentData;
+      context.paintChild(child, childParentData.offset + offset);
+      child = childAfter(child);
+    }
+  }
+
+  @override
+  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
+    return defaultHitTestChildren(result, position: position);
+  }
+}
diff --git a/lib/src/cupertino/form_row.dart b/lib/src/cupertino/form_row.dart
new file mode 100644
index 0000000..cc9fb5d
--- /dev/null
+++ b/lib/src/cupertino/form_row.dart
@@ -0,0 +1,200 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flute/rendering.dart';
+import 'package:flute/widgets.dart';
+
+import 'colors.dart';
+import 'theme.dart';
+
+// Examples can assume:
+// // @dart = 2.9
+
+// Content padding determined via SwiftUI's `Form` view in the iOS 14.2 SDK.
+const EdgeInsetsGeometry _kDefaultPadding =
+    EdgeInsetsDirectional.fromSTEB(20.0, 6.0, 6.0, 6.0);
+
+/// An iOS-style form row.
+///
+/// Creates an iOS-style split form row with a standard prefix and child widget.
+/// Also provides a space for error and helper widgets that appear underneath.
+///
+/// The [child] parameter is required. This widget is displayed at the end of
+/// the row.
+///
+/// The [prefix] parameter is optional and is displayed at the start of the
+/// row. Standard iOS guidelines encourage passing a [Text] widget to [prefix]
+/// to detail the nature of the row's [child] widget.
+///
+/// The [padding] parameter is used to pad the contents of the row. It defaults
+/// to the standard iOS padding. If no edge insets are intended, explicitly pass
+/// [EdgeInsets.zero] to [padding].
+///
+/// The [helper] and [error] parameters are both optional widgets targeted at
+/// displaying more information about the row. Both widgets are placed
+/// underneath the [prefix] and [child], and