| // Copyright 2013 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:flutter/widgets.dart'; |
| import 'breakpoints.dart'; |
| |
| /// A Widget that takes a mapping of [SlotLayoutConfig]s to [Breakpoint]s and |
| /// adds the appropriate Widget based on the current screen size. |
| /// |
| /// See also: |
| /// * [AdaptiveLayout], where [SlotLayout]s are assigned to placements on the |
| /// screen called "slots". |
| class SlotLayout extends StatefulWidget { |
| /// Creates a [SlotLayout] widget. |
| const SlotLayout({required this.config, super.key}); |
| |
| /// Given a context and a config, it returns the [SlotLayoutConfig] that will g |
| /// be chosen from the config under the context's conditions. |
| static SlotLayoutConfig? pickWidget( |
| BuildContext context, Map<Breakpoint, SlotLayoutConfig?> config) { |
| SlotLayoutConfig? chosenWidget; |
| config.forEach((Breakpoint breakpoint, SlotLayoutConfig? pickedWidget) { |
| if (breakpoint.isActive(context)) { |
| chosenWidget = pickedWidget; |
| } |
| }); |
| return chosenWidget; |
| } |
| |
| /// Maps [Breakpoint]s to [SlotLayoutConfig]s to determine what Widget to |
| /// display on which condition of screens. |
| /// |
| /// The [SlotLayoutConfig]s in this map are nullable since some breakpoints |
| /// apply to more open ranges and the nullability allows one to override the |
| /// value at that Breakpoint to be null. |
| /// |
| /// [SlotLayout] picks the last [SlotLayoutConfig] whose corresponding |
| /// [Breakpoint.isActive] returns true. |
| /// |
| /// If two [Breakpoint]s are active concurrently then the latter one defined |
| /// in the map takes priority. |
| final Map<Breakpoint, SlotLayoutConfig?> config; |
| |
| /// A wrapper for the children passed to [SlotLayout] to provide appropriate |
| /// config information. |
| /// |
| /// Acts as a delegate to the abstract class [SlotLayoutConfig]. |
| /// It first takes a builder which returns the child Widget that [SlotLayout] |
| /// eventually displays with an animation. |
| /// |
| /// It also takes an inAnimation and outAnimation to describe how the Widget |
| /// should be animated as it is switched in or out from [SlotLayout]. These |
| /// are both defined as functions that takes a [Widget] and an [Animation] and |
| /// return a [Widget]. These functions are passed to the [AnimatedSwitcher] |
| /// inside [SlotLayout] and are to be played when the child enters/exits. |
| /// |
| /// Last, it takes a required key. The key should be kept constant but unique |
| /// as this key is what is used to let the [SlotLayout] know that a change has |
| /// been made to its child. |
| /// |
| /// If you define a given animation phase, there may be multiple |
| /// widgets being displayed depending on the phases you have chosen to animate. |
| /// If you are using GlobalKeys, this may cause issues with the |
| /// [AnimatedSwitcher]. |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedWidget] and [ImplicitlyAnimatedWidget], which are commonly used |
| /// as the returned widget for the inAnimation and outAnimation functions. |
| /// * [AnimatedSwitcher.defaultTransitionBuilder], which is what takes the |
| /// inAnimation and outAnimation. |
| static SlotLayoutConfig from({ |
| WidgetBuilder? builder, |
| Widget Function(Widget, Animation<double>)? inAnimation, |
| Widget Function(Widget, Animation<double>)? outAnimation, |
| required Key key, |
| }) => |
| SlotLayoutConfig._( |
| builder: builder, |
| inAnimation: inAnimation, |
| outAnimation: outAnimation, |
| key: key, |
| ); |
| |
| @override |
| State<SlotLayout> createState() => _SlotLayoutState(); |
| } |
| |
| class _SlotLayoutState extends State<SlotLayout> |
| with SingleTickerProviderStateMixin { |
| SlotLayoutConfig? chosenWidget; |
| |
| @override |
| Widget build(BuildContext context) { |
| chosenWidget = SlotLayout.pickWidget(context, widget.config); |
| bool hasAnimation = false; |
| return AnimatedSwitcher( |
| duration: const Duration(milliseconds: 1000), |
| layoutBuilder: (Widget? currentChild, List<Widget> previousChildren) { |
| final Stack elements = Stack( |
| children: <Widget>[ |
| if (hasAnimation) ...previousChildren, |
| if (currentChild != null) currentChild, |
| ], |
| ); |
| return elements; |
| }, |
| transitionBuilder: (Widget child, Animation<double> animation) { |
| final SlotLayoutConfig configChild = child as SlotLayoutConfig; |
| if (child.key == chosenWidget?.key) { |
| return (configChild.inAnimation != null) |
| ? child.inAnimation!(child, animation) |
| : child; |
| } else { |
| if (configChild.outAnimation != null) { |
| hasAnimation = true; |
| } |
| return (configChild.outAnimation != null) |
| ? child.outAnimation!(child, ReverseAnimation(animation)) |
| : child; |
| } |
| }, |
| child: chosenWidget ?? SlotLayoutConfig.empty()); |
| } |
| } |
| |
| /// Defines how [SlotLayout] should display under a certain [Breakpoint]. |
| class SlotLayoutConfig extends StatelessWidget { |
| /// Creates a new [SlotLayoutConfig]. |
| /// |
| /// Returns the child widget as is but holds properties to be accessed by other |
| /// classes. |
| const SlotLayoutConfig._({ |
| super.key, |
| required this.builder, |
| this.inAnimation, |
| this.outAnimation, |
| }); |
| |
| /// The child Widget that [SlotLayout] eventually returns with an animation. |
| final WidgetBuilder? builder; |
| |
| /// A function that provides the animation to be wrapped around the builder |
| /// child as it is being moved in during a switch in [SlotLayout]. |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedWidget] and [ImplicitlyAnimatedWidget], which are commonly used |
| /// as the returned widget. |
| final Widget Function(Widget, Animation<double>)? inAnimation; |
| |
| /// A function that provides the animation to be wrapped around the builder |
| /// child as it is being moved in during a switch in [SlotLayout]. |
| /// |
| /// See also: |
| /// |
| /// * [AnimatedWidget] and [ImplicitlyAnimatedWidget], which are commonly used |
| /// as the returned widget. |
| final Widget Function(Widget, Animation<double>)? outAnimation; |
| |
| /// An empty [SlotLayoutConfig] to be placed in a slot to indicate that the slot |
| /// should show nothing. |
| static SlotLayoutConfig empty() { |
| return const SlotLayoutConfig._(key: Key(''), builder: null); |
| } |
| |
| @override |
| Widget build(BuildContext context) { |
| return (builder != null) ? builder!(context) : const SizedBox.shrink(); |
| } |
| } |