1
0
mirror of https://github.com/flutter/samples.git synced 2026-03-28 15:21:30 +00:00

[linting_tool] Add Adaptive Layout with Theming (#852)

This commit is contained in:
Abdullah Deshmukh
2021-07-14 03:01:14 +05:30
committed by GitHub
parent 35f1670098
commit 8e73c73f4b
25 changed files with 984 additions and 80 deletions

View File

@@ -0,0 +1,40 @@
// Copyright 2021 The Flutter team. 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 'package:linting_tool/theme/app_theme.dart';
import 'package:linting_tool/widgets/adaptive_nav.dart';
import 'package:linting_tool/routes.dart' as routes;
class LintingTool extends StatefulWidget {
const LintingTool({Key? key}) : super(key: key);
static const String homeRoute = routes.homeRoute;
@override
_LintingToolState createState() => _LintingToolState();
}
class _LintingToolState extends State<LintingTool> {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Linting Tool',
theme: AppTheme.buildReplyLightTheme(context),
darkTheme: AppTheme.buildReplyDarkTheme(context),
themeMode: ThemeMode.light,
initialRoute: LintingTool.homeRoute,
onGenerateRoute: (settings) {
switch (settings.name) {
case LintingTool.homeRoute:
return MaterialPageRoute<void>(
builder: (context) => const AdaptiveNav(),
settings: settings,
);
}
return null;
},
);
}
}

View File

@@ -0,0 +1,23 @@
// Copyright 2021 The Flutter team. 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:adaptive_breakpoints/adaptive_breakpoints.dart';
import 'package:flutter/material.dart';
/// Returns a boolean value whether the window is considered medium or large size.
/// Used to build adaptive and responsive layouts.
bool isDisplayLarge(BuildContext context) =>
getWindowType(context) >= AdaptiveWindowType.medium;
/// Returns boolean value whether the window is considered medium size.
/// Used to build adaptive and responsive layouts.
bool isDisplayMedium(BuildContext context) {
return getWindowType(context) == AdaptiveWindowType.medium;
}
/// Returns boolean value whether the window is considered small size.
/// Used to build adaptive and responsive layouts.
bool isDisplaySmall(BuildContext context) {
return getWindowType(context) <= AdaptiveWindowType.small;
}

View File

@@ -3,67 +3,8 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:linting_tool/app.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Linting Tool',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Linting Tool'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({Key? key, required this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
runApp(const LintingTool());
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Flutter team. 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';
class DefaultLintsPage extends StatelessWidget {
const DefaultLintsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO(abd99): Implement DefaultLintsPage, showing a list of default lint rules.
return const Text('Default Profiles');
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Flutter team. 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';
class HomePage extends StatelessWidget {
const HomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO(abd99): Implement HomePage, showing a list of supported lint rules.
return const Text('Home');
}
}

View File

@@ -0,0 +1,15 @@
// Copyright 2021 The Flutter team. 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';
class SavedLintsPage extends StatelessWidget {
const SavedLintsPage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO(abd99): Implement SavedLintsPage, showing a list of saved lint rules profiles.
return const Text('Saved Profiles');
}
}

View File

@@ -0,0 +1,6 @@
// Copyright 2021 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
const String homeRoute = '/home';
// TODO(abd99): Add new routes.

View File

@@ -0,0 +1,208 @@
// Copyright 2021 The Flutter team. 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 'package:google_fonts/google_fonts.dart';
import 'package:linting_tool/theme/colors.dart';
class AppTheme {
static ThemeData buildReplyLightTheme(BuildContext context) {
final base = ThemeData.light();
return base.copyWith(
bottomAppBarColor: AppColors.blue700,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColors.blue700,
modalBackgroundColor: Colors.white.withOpacity(0.7),
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: AppColors.blue700,
selectedIconTheme: const IconThemeData(color: AppColors.orange500),
selectedLabelTextStyle:
GoogleFonts.workSansTextTheme().headline5!.copyWith(
color: AppColors.orange500,
),
unselectedIconTheme: const IconThemeData(color: AppColors.blue200),
unselectedLabelTextStyle:
GoogleFonts.workSansTextTheme().headline5!.copyWith(
color: AppColors.blue200,
),
),
canvasColor: AppColors.white50,
cardColor: AppColors.white50,
chipTheme: _buildChipTheme(
AppColors.blue700,
AppColors.lightChipBackground,
Brightness.light,
),
colorScheme: ColorScheme.fromSwatch(primarySwatch: Colors.blueGrey),
textTheme: _buildReplyLightTextTheme(base.textTheme),
scaffoldBackgroundColor: AppColors.blue50,
);
}
static ThemeData buildReplyDarkTheme(BuildContext context) {
final base = ThemeData.dark();
return base.copyWith(
bottomAppBarColor: AppColors.darkBottomAppBarBackground,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: AppColors.darkDrawerBackground,
modalBackgroundColor: Colors.black.withOpacity(0.7),
),
navigationRailTheme: NavigationRailThemeData(
backgroundColor: AppColors.darkBottomAppBarBackground,
selectedIconTheme: const IconThemeData(color: AppColors.orange300),
selectedLabelTextStyle:
GoogleFonts.workSansTextTheme().headline5!.copyWith(
color: AppColors.orange300,
),
unselectedIconTheme: const IconThemeData(color: AppColors.greyLabel),
unselectedLabelTextStyle:
GoogleFonts.workSansTextTheme().headline5!.copyWith(
color: AppColors.greyLabel,
),
),
canvasColor: AppColors.black900,
cardColor: AppColors.darkCardBackground,
chipTheme: _buildChipTheme(
AppColors.blue200,
AppColors.darkChipBackground,
Brightness.dark,
),
colorScheme: const ColorScheme.dark(
primary: AppColors.blue200,
primaryVariant: AppColors.blue300,
secondary: AppColors.orange300,
secondaryVariant: AppColors.orange300,
surface: AppColors.black800,
error: AppColors.red200,
onPrimary: AppColors.black900,
onSecondary: AppColors.black900,
onBackground: AppColors.white50,
onSurface: AppColors.white50,
onError: AppColors.black900,
background: AppColors.black900Alpha087,
),
textTheme: _buildReplyDarkTextTheme(base.textTheme),
scaffoldBackgroundColor: AppColors.black900,
);
}
static ChipThemeData _buildChipTheme(
Color primaryColor,
Color chipBackground,
Brightness brightness,
) {
return ChipThemeData(
backgroundColor: primaryColor.withOpacity(0.12),
disabledColor: primaryColor.withOpacity(0.87),
selectedColor: primaryColor.withOpacity(0.05),
secondarySelectedColor: chipBackground,
padding: const EdgeInsets.all(4),
shape: const StadiumBorder(),
labelStyle: GoogleFonts.workSansTextTheme().bodyText2!.copyWith(
color: brightness == Brightness.dark
? AppColors.white50
: AppColors.black900,
),
secondaryLabelStyle: GoogleFonts.workSansTextTheme().bodyText2!,
brightness: brightness,
);
}
static TextTheme _buildReplyLightTextTheme(TextTheme base) {
return base.copyWith(
headline4: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 34,
letterSpacing: 0.4,
height: 0.9,
color: AppColors.black900,
),
headline5: GoogleFonts.workSans(
fontWeight: FontWeight.bold,
fontSize: 24,
letterSpacing: 0.27,
color: AppColors.black900,
),
headline6: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 20,
letterSpacing: 0.18,
color: AppColors.black900,
),
subtitle2: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 14,
letterSpacing: -0.04,
color: AppColors.black900,
),
bodyText1: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 18,
letterSpacing: 0.2,
color: AppColors.black900,
),
bodyText2: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 14,
letterSpacing: -0.05,
color: AppColors.black900,
),
caption: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 12,
letterSpacing: 0.2,
color: AppColors.black900,
),
);
}
static TextTheme _buildReplyDarkTextTheme(TextTheme base) {
return base.copyWith(
headline4: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 34,
letterSpacing: 0.4,
height: 0.9,
color: AppColors.white50,
),
headline5: GoogleFonts.workSans(
fontWeight: FontWeight.bold,
fontSize: 24,
letterSpacing: 0.27,
color: AppColors.white50,
),
headline6: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 20,
letterSpacing: 0.18,
color: AppColors.white50,
),
subtitle2: GoogleFonts.workSans(
fontWeight: FontWeight.w600,
fontSize: 14,
letterSpacing: -0.04,
color: AppColors.white50,
),
bodyText1: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 18,
letterSpacing: 0.2,
color: AppColors.white50,
),
bodyText2: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 14,
letterSpacing: -0.05,
color: AppColors.white50,
),
caption: GoogleFonts.workSans(
fontWeight: FontWeight.normal,
fontSize: 12,
letterSpacing: 0.2,
color: AppColors.white50,
),
);
}
}

View File

@@ -0,0 +1,42 @@
// Copyright 2021 The Flutter team. 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';
class AppColors {
static const Color white50 = Color(0xFFFFFFFF);
static const Color black800 = Color(0xFF121212);
static const Color black900 = Color(0xFF000000);
static const Color blue50 = Color(0xFFEEF0F2);
static const Color blue100 = Color(0xFFD2DBE0);
static const Color blue200 = Color(0xFFADBBC4);
static const Color blue300 = Color(0xFF8CA2AE);
static const Color blue600 = Color(0xFF4A6572);
static const Color blue700 = Color(0xFF344955);
static const Color blue800 = Color(0xFF232F34);
static const Color orange300 = Color(0xFFFBD790);
static const Color orange400 = Color(0xFFF9BE64);
static const Color orange500 = Color(0xFFF9AA33);
static const Color red200 = Color(0xFFCF7779);
static const Color red400 = Color(0xFFFF4C5D);
static const Color white50Alpha060 = Color(0x99FFFFFF);
static const Color blue50Alpha060 = Color(0x99EEF0F2);
static const Color black900Alpha020 = Color(0x33000000);
static const Color black900Alpha087 = Color(0xDE000000);
static const Color black900Alpha060 = Color(0x99000000);
static const Color greyLabel = Color(0xFFAEAEAE);
static const Color darkBottomAppBarBackground = Color(0xFF2D2D2D);
static const Color darkDrawerBackground = Color(0xFF353535);
static const Color darkCardBackground = Color(0xFF1E1E1E);
static const Color darkChipBackground = Color(0xFF2A2A2A);
static const Color lightChipBackground = Color(0xFFE5E5E5);
}

View File

@@ -0,0 +1,334 @@
// Copyright 2021 The Flutter team. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:linting_tool/pages/default_lints_page.dart';
import 'package:linting_tool/pages/home_page.dart';
import 'package:linting_tool/pages/saved_lints_page.dart';
import 'package:linting_tool/layout/adaptive.dart';
import 'package:linting_tool/theme/colors.dart';
final navKey = GlobalKey<NavigatorState>();
class AdaptiveNav extends StatefulWidget {
const AdaptiveNav({Key? key}) : super(key: key);
@override
_AdaptiveNavState createState() => _AdaptiveNavState();
}
class _AdaptiveNavState extends State<AdaptiveNav> {
@override
Widget build(BuildContext context) {
final isDesktop = isDisplayLarge(context);
const _navigationDestinations = <_Destination>[
_Destination(
textLabel: 'Home',
icon: Icons.home_outlined,
selectedIcon: Icons.home,
destination: HomePage(),
),
_Destination(
textLabel: 'Saved Profiles',
icon: Icons.save_outlined,
selectedIcon: Icons.save,
destination: SavedLintsPage(),
),
_Destination(
textLabel: 'Default Profiles',
icon: Icons.featured_play_list_outlined,
selectedIcon: Icons.featured_play_list,
destination: DefaultLintsPage(),
),
];
final _trailing = <String, IconData>{
'About': Icons.info_outline,
};
return _NavView(
extended: isDesktop,
destinations: _navigationDestinations,
trailing: _trailing,
);
}
}
class _NavView extends StatefulWidget {
const _NavView({
Key? key,
required this.extended,
required this.destinations,
this.trailing,
}) : super(key: key);
final bool extended;
final List<_Destination> destinations;
final Map<String, IconData>? trailing;
@override
_NavViewState createState() => _NavViewState();
}
class _NavViewState extends State<_NavView> {
late ValueNotifier<bool?> _isExtended;
var _selectedIndex = 0;
@override
void initState() {
super.initState();
_isExtended = ValueNotifier<bool?>(widget.extended);
}
void _onDestinationSelected(int index) {
setState(() {
_selectedIndex = index;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Row(
children: [
LayoutBuilder(
builder: (context, constraints) {
return SingleChildScrollView(
clipBehavior: Clip.antiAlias,
child: ConstrainedBox(
constraints: BoxConstraints(
minHeight: constraints.maxHeight,
),
child: IntrinsicHeight(
child: ValueListenableBuilder<bool?>(
valueListenable: _isExtended,
builder: (context, value, child) {
var isSmallDisplay = isDisplaySmall(context);
return NavigationRail(
destinations: [
for (var destination in widget.destinations)
NavigationRailDestination(
icon: Icon(destination.icon),
selectedIcon: destination.selectedIcon != null
? Icon(destination.selectedIcon)
: null,
label: Text(destination.textLabel),
),
],
extended: _isExtended.value! && !isSmallDisplay,
labelType: NavigationRailLabelType.none,
leading: _NavigationRailHeader(
extended: _isExtended,
),
trailing: _NavigationRailTrailingSection(
trailingDestinations: widget.trailing!,
),
selectedIndex: _selectedIndex,
onDestinationSelected: _onDestinationSelected,
);
},
),
),
),
);
},
),
const VerticalDivider(thickness: 1, width: 1),
Expanded(
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 1340),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
switchOutCurve: Curves.easeOut,
switchInCurve: Curves.easeIn,
child: widget.destinations[_selectedIndex].destination,
),
),
),
),
],
),
);
}
}
class _NavigationRailHeader extends StatelessWidget {
const _NavigationRailHeader({
required this.extended,
});
final ValueNotifier<bool?> extended;
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
final animation = NavigationRail.extendedAnimation(context);
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Align(
alignment: AlignmentDirectional.centerStart,
widthFactor: animation.value,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
height: 56,
child: Row(
children: [
const SizedBox(width: 6),
InkWell(
key: const ValueKey('ReplyLogo'),
borderRadius: const BorderRadius.all(Radius.circular(16)),
onTap: () {
extended.value = !extended.value!;
},
child: Row(
children: [
Transform.rotate(
angle: animation.value * math.pi,
child: const Icon(
Icons.arrow_left,
color: AppColors.white50,
size: 16,
),
),
const FlutterLogo(),
const SizedBox(width: 10),
Align(
alignment: AlignmentDirectional.centerStart,
widthFactor: animation.value,
child: Opacity(
opacity: animation.value,
child: Text(
'Linting Tool',
style: textTheme.bodyText1!.copyWith(
color: AppColors.white50,
),
),
),
),
SizedBox(width: 18 * animation.value),
],
),
),
],
),
),
const SizedBox(height: 8),
],
),
);
},
);
}
}
class _NavigationRailTrailingSection extends StatelessWidget {
const _NavigationRailTrailingSection({
required this.trailingDestinations,
});
final Map<String, IconData> trailingDestinations;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final textTheme = theme.textTheme;
final navigationRailTheme = theme.navigationRailTheme;
final animation = NavigationRail.extendedAnimation(context);
return AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Visibility(
maintainAnimation: true,
maintainState: true,
visible: animation.value > 0,
child: Opacity(
opacity: animation.value,
child: Align(
widthFactor: animation.value,
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
height: 485,
width: 256,
child: ListView(
padding: const EdgeInsets.all(12),
physics: const NeverScrollableScrollPhysics(),
children: [
const Divider(
color: AppColors.blue200,
thickness: 0.4,
indent: 14,
endIndent: 16,
),
const SizedBox(height: 8),
for (var item in trailingDestinations.keys)
InkWell(
borderRadius: const BorderRadius.all(
Radius.circular(36),
),
onTap: () => _onTapped(context, item),
child: Column(
children: [
Row(
children: [
const SizedBox(width: 12),
Icon(
trailingDestinations[item],
color: AppColors.blue300,
),
const SizedBox(width: 24),
Text(
item,
style: textTheme.bodyText1!.copyWith(
color: navigationRailTheme
.unselectedLabelTextStyle!.color,
),
),
const SizedBox(height: 72),
],
),
],
),
),
],
),
),
),
),
);
},
);
}
void _onTapped(BuildContext context, String key) {
switch (key) {
case 'About':
showAboutDialog(context: context);
break;
default:
break;
}
}
}
class _Destination {
const _Destination({
required this.destination,
required this.textLabel,
required this.icon,
this.selectedIcon,
});
final String textLabel;
final IconData icon;
final IconData? selectedIcon;
final Widget destination;
}