From f4fbbe7aeb775eb0f7438fe0044d1adba1860c75 Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Thu, 5 Jun 2025 13:41:58 +1000 Subject: [PATCH 1/2] Inline adaptive packages for `step_01` --- .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 ++++++++++++++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++++++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 +++++++++++++++ boring_to_beautiful/step_01/pubspec.yaml | 5 +- 6 files changed, 684 insertions(+), 5 deletions(-) create mode 100644 boring_to_beautiful/step_01/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_01/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_01/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_01/lib/src/utils/adaptive_container.dart diff --git a/boring_to_beautiful/step_01/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_01/lib/src/features/home/view/home_screen.dart index 9330ffbac5..ff61f22d8c 100644 --- a/boring_to_beautiful/step_01/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_01/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_01/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_01/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_01/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_01/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_01/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_01/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_01/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_01/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_01/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_01/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_01/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_01/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_01/pubspec.yaml b/boring_to_beautiful/step_01/pubspec.yaml index 2112905498..23cc063b14 100644 --- a/boring_to_beautiful/step_01/pubspec.yaml +++ b/boring_to_beautiful/step_01/pubspec.yaml @@ -1,6 +1,6 @@ name: myartist description: "A new Flutter project." -publish_to: 'none' +publish_to: "none" version: 0.1.0 environment: @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 From 2aa51d147a39b46ae0bff09165684b388aa71ad3 Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Thu, 5 Jun 2025 14:30:15 +1000 Subject: [PATCH 2/2] Add vendored code to rebuild script --- boring_to_beautiful/codelab_rebuild.yaml | 705 +++++++++++++++++- .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../final/lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/final/pubspec.yaml | 3 - boring_to_beautiful/step_01/pubspec.yaml | 2 +- .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_02/pubspec.yaml | 3 - .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_03/pubspec.yaml | 3 - .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_04/pubspec.yaml | 3 - .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_05/pubspec.yaml | 3 - .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_06/pubspec.yaml | 3 - .../src/features/home/view/home_screen.dart | 2 +- .../lib/src/utils/adaptive_breakpoints.dart | 281 +++++++ .../lib/src/utils/adaptive_column.dart | 151 ++++ .../lib/src/utils/adaptive_components.dart | 6 + .../lib/src/utils/adaptive_container.dart | 244 ++++++ boring_to_beautiful/step_07/pubspec.yaml | 3 - 44 files changed, 5483 insertions(+), 33 deletions(-) create mode 100644 boring_to_beautiful/final/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/final/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/final/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/final/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_02/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_02/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_02/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_02/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_03/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_03/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_03/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_03/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_04/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_04/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_04/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_04/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_05/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_05/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_05/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_05/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_06/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_06/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_06/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_06/lib/src/utils/adaptive_container.dart create mode 100644 boring_to_beautiful/step_07/lib/src/utils/adaptive_breakpoints.dart create mode 100644 boring_to_beautiful/step_07/lib/src/utils/adaptive_column.dart create mode 100644 boring_to_beautiful/step_07/lib/src/utils/adaptive_components.dart create mode 100644 boring_to_beautiful/step_07/lib/src/utils/adaptive_container.dart diff --git a/boring_to_beautiful/codelab_rebuild.yaml b/boring_to_beautiful/codelab_rebuild.yaml index 4433d60b22..adb96fcf8a 100644 --- a/boring_to_beautiful/codelab_rebuild.yaml +++ b/boring_to_beautiful/codelab_rebuild.yaml @@ -33,9 +33,9 @@ steps: } ] } - - name: Add adaptive_breakpoints adaptive_components adaptive_navigation animations ... + - name: Add dependencies path: myartist - flutter: pub add adaptive_breakpoints adaptive_components adaptive_navigation animations collection cupertino_icons desktop_window dynamic_color english_words flutter_bloc freezed_annotation go_router material_color_utilities:any universal_platform url_launcher + flutter: pub add animations collection cupertino_icons desktop_window dynamic_color english_words flutter_bloc freezed_annotation go_router material_color_utilities:any universal_platform url_launcher - name: Add build_runner flutter_lints freezed path: myartist flutter: pub add --dev build_runner flutter_lints freezed @@ -44,7 +44,7 @@ steps: patch-u: | --- b/boring_to_beautiful/step_01/pubspec.yaml +++ a/boring_to_beautiful/step_01/pubspec.yaml - @@ -34,3 +34,9 @@ dev_dependencies: + @@ -31,3 +31,9 @@ dev_dependencies: flutter: uses-material-design: true @@ -145,6 +145,9 @@ steps: - name: mkdir lib/src/shared/views path: myartist mkdir: lib/src/shared/views + - name: mkdir lib/src/utils + path: myartist + mkdir: lib/src/utils - name: Add lib/src/features/home/home.dart path: myartist/lib/src/features/home/home.dart replace-contents: | @@ -405,13 +408,13 @@ steps: // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; + import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; @@ -4486,6 +4489,700 @@ steps: ); } } + - name: Add lib/src/utils/adaptive_breakpoints.dart + path: myartist/lib/src/utils/adaptive_breakpoints.dart + replace-contents: | + // Copyright 2020, the Flutter project authors. Please see the AUTHORS file + // for details. 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/material.dart'; + + /// Adaptive Window in Material has five different window sizes. Each window size + /// represents a range of devices. + /// + /// Extra small represents phones and small tablets in portrait view. + /// Small represents tablets in portrait view and phones in landscape view. + /// Medium represents large tablets in landscape view. + /// Large represents computer screens. + /// Extra large represents large computer screens. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; + } + + /// This class represents the Material breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; + } + + /// This list represents the material breakpoint system. + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + /// + /// This list is in sequential order. + const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + ]; + + /// Returns the [AdaptiveWindowType] to the user. + /// + /// This is useful when the user wants to compare the MediaQuery to the current + /// window size. + AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; + } + + /// Returns the [BreakpointSystemEntry] to the user. + /// + /// Typically the developer will use the getWindowType function. Using this + /// function gives the developer access to the specific breakpoint entry and + /// it's variables. + BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); + } + - name: Add lib/src/utils/adaptive_column.dart + path: myartist/lib/src/utils/adaptive_column.dart + replace-contents: | + // Copyright 2020, the Flutter project authors. Please see the AUTHORS file + // for details. 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/material.dart'; + + import 'adaptive_breakpoints.dart'; + import 'adaptive_container.dart'; + + /// The [AdaptiveColumn] follows the Material guideline for responsive grids. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + /// + /// This widget intigrates with the [AdaptiveContainer] widget. + /// The [AdaptiveContainer] widget has a parameter called columns which represents + /// the amount of columns it should take according to the breakpoints package. + /// + /// So if the user has 6 adaptive container and each container represents two columns + /// then the 6 adaptive container would all fit within one Row on a extra large screen + /// because extra large screens have 12 columns per row. + /// + /// Learn more about the breakpoint system: + /// + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } + } + - name: Add lib/src/utils/adaptive_components.dart + path: myartist/lib/src/utils/adaptive_components.dart + replace-contents: | + // Copyright 2020, the Flutter project authors. Please see the AUTHORS file + // for details. All rights reserved. Use of this source code is governed by a + // BSD-style license that can be found in the LICENSE file. + + export 'adaptive_column.dart'; + export 'adaptive_container.dart'; + - name: Add lib/src/utils/adaptive_container.dart + path: myartist/lib/src/utils/adaptive_container.dart + replace-contents: | + // Copyright 2020, the Flutter project authors. Please see the AUTHORS file + // for details. 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/material.dart'; + + import 'adaptive_breakpoints.dart'; + + /// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. + /// + /// The AdaptiveContainer does everything a normal container does but with + /// adaptive constraints. For more information go to one of the links below. + /// + /// https://api.flutter.dev/flutter/widgets/Container-class.html + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + /// + /// This class is useful whenever you want a container to only be active in + /// certain [AdaptiveWindowType]. + class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } + } + + /// Used to see if a range of [AdaptiveWindowType] should be shown in the window. + /// If the user sets one of the variables below to true than that window type + /// should be shown within the [AdaptiveContainer]. + class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } + } - name: mkdir assets/images/albums path: myartist mkdir: assets/images/albums diff --git a/boring_to_beautiful/final/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/final/lib/src/features/home/view/home_screen.dart index 90d0354e29..b3a5e330dc 100644 --- a/boring_to_beautiful/final/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/final/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/final/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/final/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/final/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/final/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/final/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/final/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/final/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/final/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/final/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/final/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/final/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/final/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/final/pubspec.yaml b/boring_to_beautiful/final/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/final/pubspec.yaml +++ b/boring_to_beautiful/final/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_01/pubspec.yaml b/boring_to_beautiful/step_01/pubspec.yaml index 23cc063b14..f5f445fc3a 100644 --- a/boring_to_beautiful/step_01/pubspec.yaml +++ b/boring_to_beautiful/step_01/pubspec.yaml @@ -1,6 +1,6 @@ name: myartist description: "A new Flutter project." -publish_to: "none" +publish_to: 'none' version: 0.1.0 environment: diff --git a/boring_to_beautiful/step_02/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_02/lib/src/features/home/view/home_screen.dart index 9330ffbac5..ff61f22d8c 100644 --- a/boring_to_beautiful/step_02/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_02/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_02/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_02/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_02/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_02/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_02/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_02/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_02/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_02/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_02/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_02/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_02/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_02/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_02/pubspec.yaml b/boring_to_beautiful/step_02/pubspec.yaml index 2112905498..f5f445fc3a 100644 --- a/boring_to_beautiful/step_02/pubspec.yaml +++ b/boring_to_beautiful/step_02/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_03/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_03/lib/src/features/home/view/home_screen.dart index 9330ffbac5..ff61f22d8c 100644 --- a/boring_to_beautiful/step_03/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_03/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_03/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_03/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_03/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_03/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_03/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_03/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_03/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_03/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_03/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_03/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_03/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_03/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_03/pubspec.yaml b/boring_to_beautiful/step_03/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/step_03/pubspec.yaml +++ b/boring_to_beautiful/step_03/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_04/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_04/lib/src/features/home/view/home_screen.dart index 9330ffbac5..ff61f22d8c 100644 --- a/boring_to_beautiful/step_04/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_04/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_04/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_04/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_04/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_04/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_04/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_04/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_04/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_04/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_04/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_04/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_04/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_04/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_04/pubspec.yaml b/boring_to_beautiful/step_04/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/step_04/pubspec.yaml +++ b/boring_to_beautiful/step_04/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_05/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_05/lib/src/features/home/view/home_screen.dart index 7238df2fbb..e7fc8a876d 100644 --- a/boring_to_beautiful/step_05/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_05/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_05/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_05/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_05/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_05/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_05/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_05/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_05/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_05/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_05/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_05/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_05/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_05/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_05/pubspec.yaml b/boring_to_beautiful/step_05/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/step_05/pubspec.yaml +++ b/boring_to_beautiful/step_05/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_06/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_06/lib/src/features/home/view/home_screen.dart index 90d0354e29..b3a5e330dc 100644 --- a/boring_to_beautiful/step_06/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_06/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_06/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_06/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_06/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_06/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_06/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_06/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_06/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_06/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_06/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_06/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_06/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_06/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_06/pubspec.yaml b/boring_to_beautiful/step_06/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/step_06/pubspec.yaml +++ b/boring_to_beautiful/step_06/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8 diff --git a/boring_to_beautiful/step_07/lib/src/features/home/view/home_screen.dart b/boring_to_beautiful/step_07/lib/src/features/home/view/home_screen.dart index 90d0354e29..b3a5e330dc 100644 --- a/boring_to_beautiful/step_07/lib/src/features/home/view/home_screen.dart +++ b/boring_to_beautiful/step_07/lib/src/features/home/view/home_screen.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:adaptive_components/adaptive_components.dart'; import 'package:flutter/material.dart'; import '../../../shared/classes/classes.dart'; import '../../../shared/extensions.dart'; import '../../../shared/providers/providers.dart'; import '../../../shared/views/views.dart'; +import '../../../utils/adaptive_components.dart'; import '../../playlists/view/playlist_songs.dart'; import 'view.dart'; diff --git a/boring_to_beautiful/step_07/lib/src/utils/adaptive_breakpoints.dart b/boring_to_beautiful/step_07/lib/src/utils/adaptive_breakpoints.dart new file mode 100644 index 0000000000..3b19ea96de --- /dev/null +++ b/boring_to_beautiful/step_07/lib/src/utils/adaptive_breakpoints.dart @@ -0,0 +1,281 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +/// Adaptive Window in Material has five different window sizes. Each window size +/// represents a range of devices. +/// +/// Extra small represents phones and small tablets in portrait view. +/// Small represents tablets in portrait view and phones in landscape view. +/// Medium represents large tablets in landscape view. +/// Large represents computer screens. +/// Extra large represents large computer screens. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveWindowType { + const AdaptiveWindowType._({ + required this.name, + required this.relativeSize, + required this.widthRangeValues, + required this.heightLandscapeRangeValues, + required this.heightPortraitRangeValues, + }); + + /// Name based on the [AdaptiveWindowType]. + /// + /// Can be: xsmall, small, medium, large or xlarge + final String name; + + /// Used to set custom comparison operators for the [AdaptiveWindowType] enum. + final int relativeSize; + + /// Valid range of width for this window type. + final RangeValues widthRangeValues; + + /// Valid range of height for this window type on landscape mode. + final RangeValues heightLandscapeRangeValues; + + /// Valid range of height for this window type on portrait mode. + final RangeValues heightPortraitRangeValues; + + static const AdaptiveWindowType xsmall = AdaptiveWindowType._( + name: 'xsmall', + relativeSize: 0, + widthRangeValues: RangeValues(0, 599), + heightLandscapeRangeValues: RangeValues(0, 359), + heightPortraitRangeValues: RangeValues(0, 959), + ); + + static const AdaptiveWindowType small = AdaptiveWindowType._( + name: 'small', + relativeSize: 1, + widthRangeValues: RangeValues(600, 1023), + heightLandscapeRangeValues: RangeValues(360, 719), + heightPortraitRangeValues: RangeValues(360, 1599), + ); + + static const AdaptiveWindowType medium = AdaptiveWindowType._( + name: 'medium', + relativeSize: 2, + widthRangeValues: RangeValues(1024, 1439), + heightLandscapeRangeValues: RangeValues(720, 959), + heightPortraitRangeValues: RangeValues(720, 1919), + ); + + static const AdaptiveWindowType large = AdaptiveWindowType._( + name: 'large', + relativeSize: 3, + widthRangeValues: RangeValues(1440, 1919), + heightLandscapeRangeValues: RangeValues(960, 1279), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + static const AdaptiveWindowType xlarge = AdaptiveWindowType._( + name: 'xlarge', + relativeSize: 4, + widthRangeValues: RangeValues(1920, double.infinity), + heightLandscapeRangeValues: RangeValues(1280, double.infinity), + heightPortraitRangeValues: RangeValues(1920, double.infinity), + ); + + bool operator <=(AdaptiveWindowType other) => + relativeSize <= other.relativeSize; + + bool operator <(AdaptiveWindowType other) => + relativeSize < other.relativeSize; + + bool operator >=(AdaptiveWindowType other) => + relativeSize >= other.relativeSize; + + bool operator >(AdaptiveWindowType other) => + relativeSize > other.relativeSize; +} + +/// This class represents the Material breakpoint system entry. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class BreakpointSystemEntry { + const BreakpointSystemEntry({ + required this.range, + this.portrait, + this.landscape, + required this.adaptiveWindowType, + required this.columns, + required this.margin, + required this.gutter, + }); + + /// The breakpoint range values represents a width range. + final RangeValues range; + + /// Type of device which uses this breakpoint range in portrait view. + final String? portrait; + + /// Type of device which uses this breakpoint range in landscape view. + final String? landscape; + + /// Material generalizes the device size into five different windows: extra + /// small, small, medium, large, and extra large. + /// + /// The adaptive window represents a set of similar devices. For example, if + /// you want to create an adaptive layout for phones and small tablets you + /// would check if your window width is within the range of xsmall and s. If your + /// user has a bigger window size than you would create a different layout for + /// larger screens. + final AdaptiveWindowType adaptiveWindowType; + + /// The number of columns in this breakpoint system entry. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final int columns; + + /// The size of margins in pixels in this breakpoint system entry. + /// Typically the same as gutters. + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double margin; + + /// The size of gutters in pixels in this breakpoint system entry. Typically + /// the same as margins. + /// + /// Gutters represents the space between the columns. + /// + /// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins + final double gutter; +} + +/// This list represents the material breakpoint system. +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This list is in sequential order. +const List breakpointSystem = [ + BreakpointSystemEntry( + range: RangeValues(0, 359), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(360, 399), + portrait: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(400, 479), + portrait: 'large handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(480, 599), + portrait: 'large handset', + landscape: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xsmall, + columns: 4, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(600, 719), + portrait: 'small tablet', + landscape: 'medium handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 16.0, + gutter: 16.0, + ), + BreakpointSystemEntry( + range: RangeValues(720, 839), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 8, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(840, 959), + portrait: 'large tablet', + landscape: 'large handset', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(960, 1023), + landscape: 'small tablet', + adaptiveWindowType: AdaptiveWindowType.small, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1024, 1279), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1280, 1439), + landscape: 'large tablet', + adaptiveWindowType: AdaptiveWindowType.medium, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1440, 1599), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1600, 1919), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.large, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), + BreakpointSystemEntry( + range: RangeValues(1920, double.infinity), + portrait: 'small handset', + adaptiveWindowType: AdaptiveWindowType.xlarge, + columns: 12, + margin: 24.0, + gutter: 24.0, + ), +]; + +/// Returns the [AdaptiveWindowType] to the user. +/// +/// This is useful when the user wants to compare the MediaQuery to the current +/// window size. +AdaptiveWindowType getWindowType(BuildContext context) { + return getBreakpointEntry(context).adaptiveWindowType; +} + +/// Returns the [BreakpointSystemEntry] to the user. +/// +/// Typically the developer will use the getWindowType function. Using this +/// function gives the developer access to the specific breakpoint entry and +/// it's variables. +BreakpointSystemEntry getBreakpointEntry(BuildContext context) { + double width = MediaQuery.sizeOf(context).width; + for (BreakpointSystemEntry entry in breakpointSystem) { + if (entry.range.start <= width && width < entry.range.end + 1) { + return entry; + } + } + throw AssertionError('Something unexpected happened'); +} diff --git a/boring_to_beautiful/step_07/lib/src/utils/adaptive_column.dart b/boring_to_beautiful/step_07/lib/src/utils/adaptive_column.dart new file mode 100644 index 0000000000..3c8df2f48d --- /dev/null +++ b/boring_to_beautiful/step_07/lib/src/utils/adaptive_column.dart @@ -0,0 +1,151 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; +import 'adaptive_container.dart'; + +/// The [AdaptiveColumn] follows the Material guideline for responsive grids. +/// +/// https://material.io/design/layout/responsive-layout-grid.html#columns-gutters-and-margins +/// +/// This widget intigrates with the [AdaptiveContainer] widget. +/// The [AdaptiveContainer] widget has a parameter called columns which represents +/// the amount of columns it should take according to the breakpoints package. +/// +/// So if the user has 6 adaptive container and each container represents two columns +/// then the 6 adaptive container would all fit within one Row on a extra large screen +/// because extra large screens have 12 columns per row. +/// +/// Learn more about the breakpoint system: +/// +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +class AdaptiveColumn extends StatelessWidget { + /// Creates a vertical array of children. Going from left to right and then + /// top to bottom. + /// + /// This class has a responsive layout that is based of the adaptive breakpoints + /// package. The user puts in [AdaptiveContainer] widgets as children and each + /// child has a columm parameter. This represents the amount of columns it takes + /// up in its current row. So if the child has three [AdaptiveContainer] widgets + /// with each column set to 4 than on an extra-small screen each container would use up + /// the entire width of the device. On an extra-large screen the three containers + /// would fit across the row. This is because extra large devices allow up to + /// 12 columns to fit within the space. + /// + /// To see an example visit: + /// https://adaptive-components.web.app/#/ + const AdaptiveColumn({ + this.gutter, + this.margin, + required this.children, + super.key, + }) : assert(margin == null || margin >= 0), + assert(gutter == null || gutter >= 0); + + /// Empty space at the left and right of this widget. + /// + /// By default the margins will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? margin; + + /// Represents the space between children. + /// + /// By default the gutter will be set differently on different devices. This + /// double value will be dependent of the breakpoint entry of the current screen. + /// + /// Learn more at https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final double? gutter; + + /// The List of [AdaptiveContainer]. Adaptive container neeeds to be used + /// because the widget has a columnSpan parameter. This parameter represents the + /// amount of columns this widget should incompass. + /// + /// By default it is set to 1. + final List children; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + BreakpointSystemEntry entry = getBreakpointEntry(context); + final double effectiveMargin = margin ?? entry.margin; + final double effectiveGutter = gutter ?? entry.gutter; + + return Container( + margin: EdgeInsets.symmetric(horizontal: effectiveMargin), + constraints: BoxConstraints( + minWidth: entry.adaptiveWindowType.widthRangeValues.start, + maxWidth: entry.adaptiveWindowType.widthRangeValues.end, + ), + child: Wrap( + runSpacing: 8.0, + children: () { + int currentColumns = 0; + int totalGutters = 0; + List children = []; + final List row = []; + + for (AdaptiveContainer child in this.children) { + // The if statement checks if the adaptiveContainer child fits + // within the adaptive constraints. + if (child.constraints.withinAdaptiveConstraint(context)) { + row.add(child); + currentColumns += child.columnSpan; + + if (currentColumns < entry.columns) { + totalGutters++; + } else { + if (currentColumns > entry.columns) { + totalGutters--; + } + int rowGutters = 0; + for (AdaptiveContainer rowItem in row) { + // Periodic width is the width of 1 column + 1 gutter. + double periodicWidth = + (MediaQuery.sizeOf(context).width - + effectiveMargin * 2 + + effectiveGutter) / + entry.columns; + + // For a row item with a column span of k, its width is + // k * column + (k - 1) * gutter, which equals + // k * (column + gutter) - gutter, which is + // k * periodicWidth - gutter. + double maxWidth = + periodicWidth * rowItem.columnSpan - effectiveGutter; + children.add( + ConstrainedBox( + constraints: BoxConstraints( + minWidth: maxWidth, + maxWidth: maxWidth, + ), + child: rowItem, + ), + ); + + if (rowGutters < totalGutters && 1 < row.length) { + children.add( + SizedBox(width: effectiveGutter, child: Container()), + ); + rowGutters++; + } + } + totalGutters = 0; + currentColumns = 0; + row.clear(); + } + } + } + return children; + }(), + ), + ); + }, + ); + } +} diff --git a/boring_to_beautiful/step_07/lib/src/utils/adaptive_components.dart b/boring_to_beautiful/step_07/lib/src/utils/adaptive_components.dart new file mode 100644 index 0000000000..de398a446c --- /dev/null +++ b/boring_to_beautiful/step_07/lib/src/utils/adaptive_components.dart @@ -0,0 +1,6 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +export 'adaptive_column.dart'; +export 'adaptive_container.dart'; diff --git a/boring_to_beautiful/step_07/lib/src/utils/adaptive_container.dart b/boring_to_beautiful/step_07/lib/src/utils/adaptive_container.dart new file mode 100644 index 0000000000..b3a30c48ed --- /dev/null +++ b/boring_to_beautiful/step_07/lib/src/utils/adaptive_container.dart @@ -0,0 +1,244 @@ +// Copyright 2020, the Flutter project authors. Please see the AUTHORS file +// for details. 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/material.dart'; + +import 'adaptive_breakpoints.dart'; + +/// [AdaptiveContainer] lets you create a [Container] with adaptive constraints. +/// +/// The AdaptiveContainer does everything a normal container does but with +/// adaptive constraints. For more information go to one of the links below. +/// +/// https://api.flutter.dev/flutter/widgets/Container-class.html +/// https://material.io/design/layout/responsive-layout-grid.html#breakpoints +/// +/// This class is useful whenever you want a container to only be active in +/// certain [AdaptiveWindowType]. +class AdaptiveContainer extends StatelessWidget { + /// Creates a widget that combines common painting, positioning, and sizing widgets. + /// + /// The `color` and `decoration` arguments cannot both be supplied, since + /// it would potentially result in the decoration drawing over the background + /// color. To supply a decoration with a color, use `decoration: + /// BoxDecoration(color: color)`. + AdaptiveContainer({ + super.key, + this.alignment, + this.padding, + this.color, + this.decoration, + this.foregroundDecoration, + this.margin, + this.transform, + this.height, + this.child, + this.clipBehavior = Clip.none, + constraints, + this.columnSpan = 1, + }) : assert(margin == null || margin.isNonNegative), + assert(padding == null || padding.isNonNegative), + assert(decoration == null || decoration.debugAssertIsValid()), + assert( + color == null || decoration == null, + 'Cannot provide both a color and a decoration\n' + 'To provide both, use "decoration: BoxDecoration(color: color)".', + ) { + this.constraints = constraints ?? const AdaptiveConstraints(); + } + + /// The [child] contained by the container. + /// + /// If null, and if the [constraints] are unbounded or also null, the + /// container will expand to fill all available space in its parent, unless + /// the parent provides unbounded constraints, in which case the container + /// will attempt to be as small as possible. + final Widget? child; + + /// Represents how height the container should be. + final double? height; + + /// Creates constraints for adaptive windows. + /// + /// This is used by the builder to see what type of screen the user wants this + /// [AdaptiveContainer] to fit within. + late final AdaptiveConstraints constraints; + + /// columnSpan is used with [AdaptiveColumn] to represent + /// the amount of columns that this widget will fill up within a certain [Flex] + /// range. + /// + /// By default the columns will only represent one column space. If you want + /// this content of this widget to be shown increase it. There can be at most + /// 12 columns per flex range. + /// + /// Learn more by visiting the Material website: + /// https://material.io/design/layout/responsive-layout-grid.html#breakpoints + final int columnSpan; + + /// Align the [child] within the container. + /// + /// If non-null, the container will expand to fill its parent and position its + /// child within itself according to the given value. If the incoming + /// constraints are unbounded, then the child will be shrink-wrapped instead. + /// + /// Ignored if [child] is null. + /// + /// See also: + /// + /// * [Alignment], a class with convenient constants typically used to + /// specify an [AlignmentGeometry]. + /// * [AlignmentDirectional], like [Alignment] for specifying alignments + /// relative to text direction. + final AlignmentGeometry? alignment; + + /// Empty space to inscribe inside the [decoration]. The [child], if any, is + /// placed inside this padding. + /// + /// This padding is in addition to any padding inherent in the [decoration]; + /// see [Decoration.padding]. + final EdgeInsetsGeometry? padding; + + /// The color to paint behind the [child]. + /// + /// This property should be preferred when the background is a simple color. + /// For other cases, such as gradients or images, use the [decoration] + /// property. + /// + /// If the [decoration] is used, this property must be null. A background + /// color may still be painted by the [decoration] even if this property is + /// null. + final Color? color; + + /// The decoration to paint behind the [child]. + /// + /// Use the [color] property to specify a simple solid color. + /// + /// The [child] is not clipped to the decoration. To clip a child to the shape + /// of a particular [ShapeDecoration], consider using a [ClipPath] widget. + final Decoration? decoration; + + /// The decoration to paint in front of the [child]. + final Decoration? foregroundDecoration; + + /// Empty space to surround the [decoration] and [child]. + final EdgeInsetsGeometry? margin; + + /// The transformation matrix to apply before painting the container. + final Matrix4? transform; + + /// The clip behavior when [Container.decoration] has a clipPath. + /// + /// Defaults to [Clip.none]. + final Clip clipBehavior; + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, boxConstraints) { + if (constraints.withinAdaptiveConstraint(context)) { + return Container( + alignment: alignment, + padding: padding, + color: color, + decoration: decoration, + foregroundDecoration: foregroundDecoration, + transform: transform, + clipBehavior: clipBehavior, + height: height, + margin: margin, + child: child, + ); + } else { + /// Since this container is not within the adaptive constraints. + /// No widget must be returned but since you can't return no widget we + /// are returning a [LimitedBox] which is a very efficent widget. + return LimitedBox( + maxWidth: 0.0, + maxHeight: 0.0, + child: ConstrainedBox(constraints: const BoxConstraints.expand()), + ); + } + }, + ); + } +} + +/// Used to see if a range of [AdaptiveWindowType] should be shown in the window. +/// If the user sets one of the variables below to true than that window type +/// should be shown within the [AdaptiveContainer]. +class AdaptiveConstraints { + const AdaptiveConstraints({ + this.xsmall = true, + this.small = true, + this.medium = true, + this.large = true, + this.xlarge = true, + }); + + const AdaptiveConstraints.xsmall({ + this.xsmall = true, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.small({ + this.xsmall = false, + this.small = true, + this.medium = false, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.medium({ + this.xsmall = false, + this.small = false, + this.medium = true, + this.large = false, + this.xlarge = false, + }); + + const AdaptiveConstraints.large({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = true, + this.xlarge = false, + }); + + const AdaptiveConstraints.xlarge({ + this.xsmall = false, + this.small = false, + this.medium = false, + this.large = false, + this.xlarge = true, + }); + + final bool xsmall; + final bool small; + final bool medium; + final bool large; + final bool xlarge; + + bool withinAdaptiveConstraint(BuildContext context) { + AdaptiveWindowType currentEntry = getWindowType(context); + + switch (currentEntry) { + case AdaptiveWindowType.xsmall: + return xsmall; + case AdaptiveWindowType.small: + return small; + case AdaptiveWindowType.medium: + return medium; + case AdaptiveWindowType.large: + return large; + case AdaptiveWindowType.xlarge: + return xlarge; + default: + throw AssertionError('Unsupported AdaptiveWindowType'); + } + } +} diff --git a/boring_to_beautiful/step_07/pubspec.yaml b/boring_to_beautiful/step_07/pubspec.yaml index 406605614c..e6cd6dfab6 100644 --- a/boring_to_beautiful/step_07/pubspec.yaml +++ b/boring_to_beautiful/step_07/pubspec.yaml @@ -9,9 +9,6 @@ environment: dependencies: flutter: sdk: flutter - adaptive_breakpoints: ^0.1.7 - adaptive_components: ^0.0.10 - adaptive_navigation: ^0.0.10 animations: ^2.0.11 collection: ^1.19.1 cupertino_icons: ^1.0.8