mirror of
https://github.com/flutter/samples.git
synced 2025-11-13 00:08:24 +00:00
413 lines
11 KiB
Dart
413 lines
11 KiB
Dart
// Copyright 2019 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:flutter/services.dart';
|
|
import 'package:gallery/data/gallery_options.dart';
|
|
import 'package:gallery/l10n/gallery_localizations.dart';
|
|
import 'package:gallery/layout/adaptive.dart';
|
|
import 'package:gallery/layout/text_scale.dart';
|
|
import 'package:gallery/pages/home.dart';
|
|
import 'package:gallery/studies/rally/colors.dart';
|
|
import 'package:gallery/layout/focus_traversal_policy.dart';
|
|
|
|
class LoginPage extends StatefulWidget {
|
|
@override
|
|
_LoginPageState createState() => _LoginPageState();
|
|
}
|
|
|
|
class _LoginPageState extends State<LoginPage> {
|
|
final TextEditingController _usernameController = TextEditingController();
|
|
final TextEditingController _passwordController = TextEditingController();
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final backButtonFocusNode =
|
|
InheritedFocusNodes.of(context).backButtonFocusNode;
|
|
|
|
return DefaultFocusTraversal(
|
|
policy: EdgeChildrenFocusTraversalPolicy(
|
|
firstFocusNodeOutsideScope: backButtonFocusNode,
|
|
lastFocusNodeOutsideScope: backButtonFocusNode,
|
|
focusScope: FocusScope.of(context),
|
|
),
|
|
child: ApplyTextOptions(
|
|
child: Scaffold(
|
|
body: SafeArea(
|
|
child: _MainView(
|
|
usernameController: _usernameController,
|
|
passwordController: _passwordController,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_usernameController.dispose();
|
|
_passwordController.dispose();
|
|
super.dispose();
|
|
}
|
|
}
|
|
|
|
class _MainView extends StatelessWidget {
|
|
const _MainView({
|
|
Key key,
|
|
this.usernameController,
|
|
this.passwordController,
|
|
}) : super(key: key);
|
|
|
|
final TextEditingController usernameController;
|
|
final TextEditingController passwordController;
|
|
|
|
void _login(BuildContext context) {
|
|
Navigator.pop(context);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDesktop = isDisplayDesktop(context);
|
|
List<Widget> listViewChildren;
|
|
|
|
if (isDesktop) {
|
|
final desktopMaxWidth = 400.0 + 100.0 * (cappedTextScale(context) - 1);
|
|
listViewChildren = [
|
|
_UsernameInput(
|
|
maxWidth: desktopMaxWidth,
|
|
usernameController: usernameController,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_PasswordInput(
|
|
maxWidth: desktopMaxWidth,
|
|
passwordController: passwordController,
|
|
),
|
|
_LoginButton(
|
|
maxWidth: desktopMaxWidth,
|
|
onTap: () {
|
|
_login(context);
|
|
},
|
|
),
|
|
];
|
|
} else {
|
|
listViewChildren = [
|
|
_SmallLogo(),
|
|
_UsernameInput(
|
|
usernameController: usernameController,
|
|
),
|
|
const SizedBox(height: 12),
|
|
_PasswordInput(
|
|
passwordController: passwordController,
|
|
),
|
|
_ThumbButton(
|
|
onTap: () {
|
|
_login(context);
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
return Column(
|
|
children: [
|
|
if (isDesktop) _TopBar(),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: isDesktop ? Alignment.center : Alignment.topCenter,
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
children: listViewChildren,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TopBar extends StatelessWidget {
|
|
const _TopBar({
|
|
Key key,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final spacing = const SizedBox(width: 30);
|
|
return Container(
|
|
width: double.infinity,
|
|
margin: const EdgeInsets.only(top: 8),
|
|
padding: EdgeInsets.symmetric(horizontal: 30),
|
|
child: Wrap(
|
|
alignment: WrapAlignment.spaceBetween,
|
|
children: [
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ExcludeSemantics(
|
|
child: SizedBox(
|
|
height: 80,
|
|
child: Image.asset(
|
|
'logo.png',
|
|
package: 'rally_assets',
|
|
),
|
|
),
|
|
),
|
|
spacing,
|
|
Text(
|
|
GalleryLocalizations.of(context).rallyLoginLoginToRally,
|
|
style: Theme.of(context).textTheme.body2.copyWith(
|
|
fontSize: 35 / reducedTextScale(context),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
GalleryLocalizations.of(context).rallyLoginNoAccount,
|
|
style: Theme.of(context).textTheme.subhead,
|
|
),
|
|
spacing,
|
|
_BorderButton(
|
|
text: GalleryLocalizations.of(context).rallyLoginSignUp,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SmallLogo extends StatelessWidget {
|
|
const _SmallLogo({
|
|
Key key,
|
|
}) : super(key: key);
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 64),
|
|
child: SizedBox(
|
|
height: 160,
|
|
child: ExcludeSemantics(
|
|
child: Image.asset(
|
|
'logo.png',
|
|
package: 'rally_assets',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _UsernameInput extends StatelessWidget {
|
|
const _UsernameInput({
|
|
Key key,
|
|
this.maxWidth,
|
|
this.usernameController,
|
|
}) : super(key: key);
|
|
|
|
final double maxWidth;
|
|
final TextEditingController usernameController;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Align(
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
|
child: TextField(
|
|
controller: usernameController,
|
|
decoration: InputDecoration(
|
|
labelText: GalleryLocalizations.of(context).rallyLoginUsername,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PasswordInput extends StatelessWidget {
|
|
const _PasswordInput({
|
|
Key key,
|
|
this.maxWidth,
|
|
this.passwordController,
|
|
}) : super(key: key);
|
|
|
|
final double maxWidth;
|
|
final TextEditingController passwordController;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Align(
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
|
child: TextField(
|
|
controller: passwordController,
|
|
decoration: InputDecoration(
|
|
labelText: GalleryLocalizations.of(context).rallyLoginPassword,
|
|
),
|
|
obscureText: true,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _ThumbButton extends StatefulWidget {
|
|
_ThumbButton({
|
|
@required this.onTap,
|
|
});
|
|
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
_ThumbButtonState createState() => _ThumbButtonState();
|
|
}
|
|
|
|
class _ThumbButtonState extends State<_ThumbButton> {
|
|
BoxDecoration borderDecoration;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Semantics(
|
|
button: true,
|
|
enabled: true,
|
|
label: GalleryLocalizations.of(context).rallyLoginLabelLogin,
|
|
child: GestureDetector(
|
|
onTap: widget.onTap,
|
|
child: Focus(
|
|
onKey: (node, event) {
|
|
if (event is RawKeyDownEvent) {
|
|
if (event.logicalKey == LogicalKeyboardKey.enter ||
|
|
event.logicalKey == LogicalKeyboardKey.space) {
|
|
widget.onTap();
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
onFocusChange: (hasFocus) {
|
|
if (hasFocus) {
|
|
setState(() {
|
|
borderDecoration = BoxDecoration(
|
|
border: Border.all(
|
|
color: Colors.white.withOpacity(0.5),
|
|
width: 2,
|
|
),
|
|
);
|
|
});
|
|
} else {
|
|
setState(() {
|
|
borderDecoration = null;
|
|
});
|
|
}
|
|
},
|
|
child: Container(
|
|
decoration: borderDecoration,
|
|
height: 120,
|
|
child: ExcludeSemantics(
|
|
child: Image.asset(
|
|
'thumb.png',
|
|
package: 'rally_assets',
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LoginButton extends StatelessWidget {
|
|
const _LoginButton({
|
|
Key key,
|
|
@required this.onTap,
|
|
this.maxWidth,
|
|
}) : super(key: key);
|
|
|
|
final double maxWidth;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Align(
|
|
alignment: Alignment.center,
|
|
child: Container(
|
|
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
|
padding: const EdgeInsets.symmetric(vertical: 30),
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.check_circle_outline, color: RallyColors.buttonColor),
|
|
const SizedBox(width: 12),
|
|
Text(GalleryLocalizations.of(context).rallyLoginRememberMe),
|
|
const Expanded(child: SizedBox.shrink()),
|
|
_FilledButton(
|
|
text: GalleryLocalizations.of(context).rallyLoginButtonLogin,
|
|
onTap: onTap,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _BorderButton extends StatelessWidget {
|
|
const _BorderButton({Key key, @required this.text}) : super(key: key);
|
|
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OutlineButton(
|
|
borderSide: const BorderSide(color: RallyColors.buttonColor),
|
|
color: RallyColors.buttonColor,
|
|
highlightedBorderColor: RallyColors.buttonColor,
|
|
focusColor: RallyColors.buttonColor.withOpacity(0.8),
|
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 24),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
textColor: Colors.white,
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
},
|
|
child: Text(text),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FilledButton extends StatelessWidget {
|
|
const _FilledButton({Key key, @required this.text, @required this.onTap})
|
|
: super(key: key);
|
|
|
|
final String text;
|
|
final VoidCallback onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FlatButton(
|
|
color: RallyColors.buttonColor,
|
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 24),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
onPressed: onTap,
|
|
child: Row(
|
|
children: [
|
|
Icon(Icons.lock),
|
|
const SizedBox(width: 6),
|
|
Text(text),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|