├── .gitignore ├── 1 - 注册表单的实现 - 20201127 ├── README.md ├── alex_main.dart ├── end_main.dart ├── laelluo_main.dart ├── lc_main.dart ├── leonard_login.dart ├── lvyan_main.dart ├── ray_main.dart ├── sun_main.dart └── yifang_main.dart ├── 2 - 百格齐放 - 20201202 ├── README.md ├── alex_main.dart ├── end_main.dart ├── laelluo_main.dart ├── lvyan_main.dart ├── ray_main.dart └── yifang_main.dart ├── 3 - 活动入口网格 - 20210404 ├── README.md ├── leo_activity_page.dart ├── lycstar_main.dart ├── main_sun.dart └── zengqiang_main.dart ├── CODEOWNERS ├── README.md ├── _config.yml └── pubspec.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | **/ios/Flutter/.last_build_id 26 | .dart_tool/ 27 | .flutter-plugins 28 | .flutter-plugins-dependencies 29 | .packages 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Web related 35 | lib/generated_plugin_registrant.dart 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | 48 | .env 49 | pubspec.lock -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/README.md: -------------------------------------------------------------------------------- 1 | # 注册表单的实现 - 2020/11/27 2 | 3 | ![](https://tva1.sinaimg.cn/large/0081Kckwgy1gl43x5id6gj30fo06yq37.jpg) 4 | 5 | 实现一个注册表单,包含 **两个输入框**、**一个登录按钮** 和 **两行文字**。输入框分别对应“登录”和“密码”两个输入条目: 6 | 7 | * 登录输入框 (20 + 5) 8 | * 起始处显示一个图标:`Icons.supervisor_account`; 9 | * 未输入时显示提示文字:`请输入用户名`; 10 | * (加分项)当输入内容后,尾部展示一个图标按钮,图标为:`Icons.close`。点击可以清空用户名,并且图标消失。 11 | * 密码输入框 (30 + 10) 12 | * 起始处显示一个图标:`Icons.lock`; 13 | * 未输入时显示提示文字:`请输入密码`: 14 | * 尾部显示一个图标按钮,用于控制密码的是否显示为掩码,图标为:`Icons.visibility` / `Icons.visibility_off`。 15 | * (加分项)输入不能少于8位,复杂度不低于6(唯一文字超过6个)。 16 | * 登录按钮显示文字“登录”,颜色为默认主题色。登录按钮点击后,将已填入的用户名和密码显示在两行文字上。 (10) 17 | * 文字格式为 `用户名:xxx`,未填写的时候显示 `用户名:未填写`。用户名以下划线装饰,密码以斜体装饰。 (5) 18 | * (加分项)用户名未输入 **或** 密码输入不符合要求时,登录按钮置灰且无法点击。 (10) 19 | * (加分项)点击登录后,按钮文字变为圆形加载进度条,在三秒后恢复。 (10) 20 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/alex_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() => runApp(MyApp()); 4 | 5 | class MyApp extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return MaterialApp( 9 | title: 'Test App Demo', 10 | theme: ThemeData(primarySwatch: Colors.blue), 11 | home: const TestTextFieldPage(), 12 | ); 13 | } 14 | } 15 | 16 | class TestTextFieldPage extends StatefulWidget { 17 | const TestTextFieldPage({Key key}) : super(key: key); 18 | 19 | @override 20 | _TestTextFieldPageState createState() => _TestTextFieldPageState(); 21 | } 22 | 23 | class _TestTextFieldPageState extends State { 24 | final TextEditingController usernameController = TextEditingController(); 25 | final ValueNotifier usernameNotifier = ValueNotifier(''); 26 | final ValueNotifier passwordNotifier = ValueNotifier(''); 27 | final ValueNotifier isLoginNotifier = ValueNotifier(false); 28 | final ValueNotifier isObscureNotifier = ValueNotifier(true); 29 | 30 | @override 31 | void dispose() { 32 | // 所有监听需要在 dispose 时销毁。 33 | usernameController.dispose(); 34 | usernameNotifier.dispose(); 35 | passwordNotifier.dispose(); 36 | isLoginNotifier.dispose(); 37 | isObscureNotifier.dispose(); 38 | super.dispose(); 39 | } 40 | 41 | /// 通过所有的数据,判断是否可以登录 42 | bool canLogin(String username, String password, bool isLogin) { 43 | return username.isNotEmpty && // 用户名不为空 44 | password.length >= 8 && // 密码长度超过 8 位 45 | password.codeUnits.toSet().length >= 6 && // 密码复杂度超过 6 46 | !isLogin; // 未在登录 47 | } 48 | 49 | /// 登录操作 50 | /// 51 | /// 延时三秒后切换回未登录状态 52 | void login() { 53 | isLoginNotifier.value = true; 54 | Future.delayed(const Duration(seconds: 3), () { 55 | isLoginNotifier.value = false; 56 | }); 57 | } 58 | 59 | @override 60 | Widget build(BuildContext context) { 61 | return Scaffold( 62 | appBar: AppBar(), 63 | body: Column( 64 | mainAxisAlignment: MainAxisAlignment.center, 65 | children: [ 66 | ValueListenableBuilder( 67 | valueListenable: usernameNotifier, 68 | builder: (_, String u, __) => TextField( 69 | decoration: InputDecoration( 70 | hintText: '请输入用户名', 71 | icon: const Icon(Icons.supervisor_account), 72 | suffixIcon: u.isNotEmpty 73 | ? GestureDetector( 74 | onTap: () { 75 | // 通过 controller 清除内容 76 | usernameController.clear(); 77 | // 清除时不会触发 onChanged,所以手动设置 78 | // 当然,如果通过 controller 设置新值是可以触发的 79 | usernameNotifier.value = ''; 80 | }, 81 | child: const Icon(Icons.clear), 82 | ) 83 | : null, 84 | ), 85 | onChanged: (String u) => usernameNotifier.value = u, 86 | ), 87 | ), 88 | ValueListenableBuilder( 89 | valueListenable: isObscureNotifier, 90 | builder: (_, bool isObscure, __) => TextField( 91 | decoration: InputDecoration( 92 | hintText: '请输入密码', 93 | icon: const Icon(Icons.lock), 94 | suffixIcon: GestureDetector( 95 | onTap: () { 96 | isObscureNotifier.value = !isObscureNotifier.value; 97 | }, 98 | child: const Icon(Icons.visibility), 99 | ), 100 | ), 101 | obscureText: isObscure, 102 | onChanged: (String p) => passwordNotifier.value = p, 103 | ), 104 | ), 105 | ValueListenableBuilder3( 106 | firstNotifier: usernameNotifier, 107 | secondNotifier: passwordNotifier, 108 | thirdNotifier: isLoginNotifier, 109 | builder: (_, String u, String p, bool isLogin, __) { 110 | return RaisedButton( 111 | onPressed: canLogin(u, p, isLogin) ? login : null, 112 | color: Theme.of(context).accentColor, 113 | child: isLogin 114 | ? const CircularProgressIndicator() 115 | : const Text('登录'), 116 | ); 117 | }, 118 | ), 119 | ], 120 | ), 121 | ); 122 | } 123 | } 124 | 125 | /// 将三个 [ValueNotifier] 整合在一起的 builder widget。套中套中套。 126 | class ValueListenableBuilder3 extends StatelessWidget { 127 | const ValueListenableBuilder3({ 128 | Key key, 129 | @required this.firstNotifier, 130 | @required this.secondNotifier, 131 | @required this.thirdNotifier, 132 | @required this.builder, 133 | }) : super(key: key); 134 | 135 | final ValueNotifier firstNotifier; 136 | final ValueNotifier secondNotifier; 137 | final ValueNotifier thirdNotifier; 138 | final Widget Function(BuildContext, A, B, C, Widget) builder; 139 | 140 | @override 141 | Widget build(BuildContext context) { 142 | return ValueListenableBuilder( 143 | valueListenable: firstNotifier, 144 | builder: (_, A first, __) { 145 | return ValueListenableBuilder( 146 | valueListenable: secondNotifier, 147 | builder: (_, B second, __) { 148 | return ValueListenableBuilder( 149 | valueListenable: thirdNotifier, 150 | builder: (_, C third, __) { 151 | return builder(context, first, second, third, __); 152 | }, 153 | ); 154 | }, 155 | ); 156 | }, 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/end_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | 4 | void main() => runApp(new MyApp()); 5 | 6 | class MyApp extends StatelessWidget { 7 | @override 8 | Widget build(BuildContext context) { 9 | return new MaterialApp( 10 | title: '注册表单', 11 | home: new RegisgtForm(), 12 | ); 13 | } 14 | } 15 | 16 | class RegisgtForm extends StatefulWidget { 17 | const RegisgtForm({Key key}) : super(key: key); 18 | @override 19 | createState() => RegisgtFormState(); 20 | } 21 | 22 | class RegisgtFormState extends State { 23 | bool isMask = true; 24 | bool isUser = false; 25 | bool isPwd = false; 26 | String userName = ''; 27 | String password = ''; 28 | bool isLogin = false; 29 | Timer timer; 30 | 31 | Widget buildTextField(TextEditingController controller, String label, Icon icon, [bool obscure]) { 32 | return TextField( 33 | controller: controller, 34 | decoration: InputDecoration( 35 | filled: true, 36 | labelText: label, 37 | prefixIcon: icon, 38 | ), 39 | obscureText: obscure != null ? obscure : false, 40 | ); 41 | } 42 | 43 | final controller = TextEditingController(); 44 | final pwdController = TextEditingController(); 45 | 46 | @override 47 | void initState() { 48 | super.initState(); 49 | controller.addListener(() { 50 | final user = controller.text; 51 | setState(() { 52 | userName = user; 53 | isUser = !user.isEmpty; 54 | }); 55 | // print('user ${controller.text}'); 56 | }); 57 | pwdController.addListener(() { 58 | final pwd = pwdController.text; 59 | RegExp reg = RegExp(r"(?![0-9]+$)(?![a-zA-Z]+$)[0-9A-Za-z]{8,16}$"); 60 | setState(() { 61 | password = pwd; 62 | isPwd = reg.hasMatch(pwd); 63 | }); 64 | // print('password ${pwdController.text}'); 65 | }); 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return Scaffold( 71 | appBar: AppBar( 72 | title: Text('注册表单'), 73 | ), 74 | body: Column( 75 | crossAxisAlignment: CrossAxisAlignment.start, 76 | children: [ 77 | Stack( 78 | children: [ 79 | Padding( 80 | padding: const EdgeInsets.all(20.0), 81 | child: buildTextField(controller, '请输入用户名', Icon(Icons.supervisor_account)), 82 | ), 83 | Positioned( 84 | child: Offstage( 85 | offstage: !isUser, 86 | child: IconButton( 87 | icon: Icon(Icons.close), 88 | tooltip: 'Clear text', 89 | onPressed: () { 90 | print('pressed'); 91 | controller.clear(); 92 | }, 93 | ), 94 | ), 95 | top: 24, 96 | right: 20, 97 | ), 98 | ], 99 | ), 100 | Stack( 101 | children: [ 102 | Padding( 103 | padding: const EdgeInsets.all(20.0), 104 | child: buildTextField(pwdController, '请输入密码', Icon(Icons.lock), isMask), 105 | ), 106 | Positioned( 107 | child: IconButton( 108 | icon: Icon(Icons.visibility), 109 | tooltip: 'visibility', 110 | onPressed: _isMask, 111 | ), 112 | top: 24, 113 | right: 20, 114 | ), 115 | ], 116 | ), 117 | Center( 118 | child: RaisedButton( 119 | onPressed: isUser && isPwd ? _confirm : null, 120 | // child: Text('登录'), 121 | child: !isLogin ? Text('登录') : Container( 122 | width: 20, 123 | height: 20, 124 | child: CircularProgressIndicator( 125 | valueColor: AlwaysStoppedAnimation(Colors.white), 126 | ) 127 | ), 128 | color: Colors.blue, 129 | textColor: Colors.white, 130 | ) 131 | ), 132 | Text(isLogin ? userName : ''), 133 | Text(isLogin ? password : ''), 134 | Center( 135 | child: Padding( 136 | padding: const EdgeInsets.all(20.0), 137 | child: RaisedButton( 138 | onPressed: _pushBaige, 139 | child: Text('百格齐放') 140 | ) 141 | ) 142 | ) 143 | ] 144 | ) 145 | ); 146 | } 147 | 148 | void _clearUserText(controller) { 149 | controller.clear(); 150 | print('clear text'); 151 | } 152 | 153 | void _isMask() { 154 | setState(() { 155 | isMask = !isMask; 156 | }); 157 | print('whether to show mask ${isMask}'); 158 | } 159 | 160 | void _confirm() { 161 | setState(() { 162 | isLogin = true; 163 | }); 164 | timer = Timer(Duration(seconds: 3), () { 165 | setState(() { 166 | isLogin = false; 167 | }); 168 | }); 169 | } 170 | 171 | void _pushBaige() { 172 | Navigator.of(context).push( 173 | MaterialPageRoute( 174 | builder: (context) { 175 | // return BaigeWidget(); 176 | return const SizedBox.shrink(); 177 | } 178 | ) 179 | ); 180 | } 181 | } 182 | 183 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/laelluo_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() => runApp(Application()); 4 | 5 | class Application extends StatelessWidget { 6 | @override 7 | Widget build(BuildContext context) { 8 | return MaterialApp(home: MainPage()); 9 | } 10 | } 11 | 12 | class MainPage extends StatefulWidget { 13 | const MainPage({ 14 | Key key, 15 | }) : super(key: key); 16 | 17 | @override 18 | _MainPageState createState() => _MainPageState(); 19 | } 20 | 21 | class _MainPageState extends State { 22 | TextEditingController usernameController; 23 | TextEditingController passwordController; 24 | 25 | /// 使用`ValueNotifier`和`ValueListenableBuilder`实现局部刷新 26 | ValueNotifier obscureTextState; 27 | ValueNotifier canSubmitState; 28 | ValueNotifier loadingState; 29 | ValueNotifier usernameState; 30 | ValueNotifier passwordState; 31 | 32 | @override 33 | void initState() { 34 | super.initState(); 35 | usernameController = TextEditingController(); 36 | usernameController.addListener(canSubmitUpdateListener); 37 | passwordController = TextEditingController(); 38 | passwordController.addListener(canSubmitUpdateListener); 39 | obscureTextState = ValueNotifier(true); 40 | canSubmitState = ValueNotifier(false); 41 | loadingState = ValueNotifier(false); 42 | usernameState = ValueNotifier(''); 43 | passwordState = ValueNotifier(''); 44 | } 45 | 46 | @override 47 | void dispose() { 48 | usernameController.removeListener(canSubmitUpdateListener); 49 | passwordController.removeListener(canSubmitUpdateListener); 50 | usernameController.dispose(); 51 | passwordController.dispose(); 52 | obscureTextState.dispose(); 53 | canSubmitState.dispose(); 54 | loadingState.dispose(); 55 | usernameState.dispose(); 56 | passwordState.dispose(); 57 | super.dispose(); 58 | } 59 | 60 | void canSubmitUpdateListener() { 61 | canSubmitState.value = usernameController.text.isNotEmpty && 62 | passwordController.text.length >= 8 && 63 | passwordController.text.characters.toSet().length >= 6; 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return Scaffold( 69 | body: SafeArea( 70 | child: SingleChildScrollView( 71 | child: Column( 72 | children: [ 73 | usernameTextField, 74 | passwordTextField, 75 | submitButton, 76 | usernameRichText, 77 | passwordRichText, 78 | ], 79 | ), 80 | ), 81 | ), 82 | ); 83 | } 84 | 85 | ValueListenableBuilder get passwordRichText { 86 | return ValueListenableBuilder( 87 | valueListenable: passwordState, 88 | builder: (BuildContext context, String value, Widget child) { 89 | return Text.rich(TextSpan( 90 | text: '密码:', 91 | children: [ 92 | TextSpan( 93 | text: value.isEmpty ? '未填写' : value, 94 | style: TextStyle(fontStyle: FontStyle.italic), 95 | ), 96 | ], 97 | )); 98 | }, 99 | ); 100 | } 101 | 102 | ValueListenableBuilder get usernameRichText { 103 | return ValueListenableBuilder( 104 | valueListenable: usernameState, 105 | builder: (BuildContext context, String value, Widget child) { 106 | return Text.rich(TextSpan( 107 | text: '用户名:', 108 | children: [ 109 | TextSpan( 110 | text: value.isEmpty ? '未填写' : value, 111 | style: TextStyle(decoration: TextDecoration.underline), 112 | ), 113 | ], 114 | )); 115 | }, 116 | ); 117 | } 118 | 119 | ValueListenableBuilder get submitButton { 120 | return ValueListenableBuilder( 121 | valueListenable: canSubmitState, 122 | builder: (BuildContext context, bool value, Widget child) { 123 | VoidCallback onPressed; 124 | if (value) { 125 | onPressed = () { 126 | if (loadingState.value) return; 127 | loadingState.value = true; 128 | Future.delayed( 129 | Duration(seconds: 3), 130 | () => loadingState.value = false, 131 | ); 132 | usernameState.value = usernameController.text; 133 | passwordState.value = passwordController.text; 134 | }; 135 | } else { 136 | onPressed = null; 137 | } 138 | return RaisedButton( 139 | child: ValueListenableBuilder( 140 | valueListenable: loadingState, 141 | builder: (BuildContext context, bool value, Widget child) { 142 | if (value) 143 | return CircularProgressIndicator( 144 | valueColor: AlwaysStoppedAnimation(Colors.white)); 145 | return child; 146 | }, 147 | child: Text('登陆'), 148 | ), 149 | color: Theme.of(context).primaryColor, 150 | onPressed: onPressed, 151 | ); 152 | }, 153 | ); 154 | } 155 | 156 | ValueListenableBuilder get passwordTextField { 157 | return ValueListenableBuilder( 158 | builder: (BuildContext context, value, Widget child) { 159 | return TextField( 160 | controller: passwordController, 161 | decoration: InputDecoration( 162 | prefixIcon: Icon(Icons.lock), 163 | hintText: '请输入密码', 164 | suffixIcon: IconButton( 165 | icon: Icon(value ? Icons.visibility_off : Icons.visibility), 166 | onPressed: () => obscureTextState.value = !obscureTextState.value, 167 | ), 168 | ), 169 | obscureText: value, 170 | ); 171 | }, 172 | valueListenable: obscureTextState, 173 | ); 174 | } 175 | 176 | TextField get usernameTextField { 177 | return TextField( 178 | controller: usernameController, 179 | decoration: InputDecoration( 180 | prefixIcon: Icon(Icons.supervisor_account), 181 | hintText: '请输入用户名', 182 | suffixIcon: ValueListenableBuilder( 183 | valueListenable: usernameController, 184 | builder: 185 | (BuildContext context, TextEditingValue value, Widget child) { 186 | if (value.text.isEmpty) return SizedBox.shrink(); 187 | return IconButton( 188 | icon: Icon(Icons.close), 189 | onPressed: () => usernameController.clear(), 190 | ); 191 | }, 192 | ), 193 | ), 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/lc_main.dart: -------------------------------------------------------------------------------- 1 | // import 'dart:html'; 2 | 3 | import 'dart:async'; 4 | 5 | import 'package:flutter/material.dart'; 6 | import 'package:flutter/cupertino.dart'; 7 | 8 | void main() { 9 | runApp(MyApp()); 10 | } 11 | 12 | class MyApp extends StatelessWidget { 13 | // This widget is the root of your application. 14 | @override 15 | Widget build(BuildContext context) { 16 | return MaterialApp( 17 | title: 'Flutter Demo', 18 | theme: ThemeData( 19 | primarySwatch: Colors.blue, 20 | visualDensity: VisualDensity.adaptivePlatformDensity, 21 | ), 22 | home: MyHomePage(title: 'Flutter Demo Home Page'), 23 | ); 24 | } 25 | } 26 | 27 | class MyHomePage extends StatefulWidget { 28 | MyHomePage({Key key, this.title}) : super(key: key); 29 | 30 | final String title; 31 | 32 | @override 33 | _MyHomePageState createState() => _MyHomePageState(); 34 | } 35 | 36 | class _MyHomePageState extends State { 37 | bool passflag = true; 38 | bool canSub = false; 39 | bool isSub = false; 40 | bool subEnd = false; 41 | String name = ''; 42 | String password = ''; 43 | String userErr; 44 | String passErr; 45 | Timer timer; 46 | List passlist; 47 | 48 | @override 49 | Widget build(BuildContext context) { 50 | return Scaffold( 51 | appBar: AppBar( 52 | title: Text(widget.title), 53 | ), 54 | body: Center( 55 | child: Column( 56 | mainAxisAlignment: MainAxisAlignment.center, 57 | children: [ 58 | Form( 59 | child: Column(children: [ 60 | Flex( 61 | direction: Axis.horizontal, 62 | mainAxisSize: MainAxisSize.min, 63 | children: [ 64 | new Icon(Icons.supervisor_account), 65 | new Container( 66 | width: 350, 67 | margin: EdgeInsets.only(left: 20.0), 68 | child: new TextField( 69 | obscureText: false, 70 | onSubmitted: (val) => { 71 | this.setState(() { 72 | name = val; 73 | subEnd = false; 74 | }) 75 | }, 76 | controller: new TextEditingController(text: name), 77 | decoration: InputDecoration( 78 | errorText: 79 | name.replaceAll(" ", '') == "" ? "用户名不能为空" : null, 80 | hintText: "请输入用户名", 81 | suffixIcon: name != '' 82 | ? InkWell( 83 | child: Icon(Icons.close), 84 | onTap: () => { 85 | this.setState(() { 86 | name = ''; 87 | }) 88 | }) 89 | : null), 90 | ), 91 | ), 92 | ], 93 | ), 94 | Flex( 95 | direction: Axis.horizontal, 96 | mainAxisSize: MainAxisSize.min, 97 | children: [ 98 | new Icon(Icons.lock), 99 | Container( 100 | width: 350, 101 | margin: EdgeInsets.only(left: 20.0), 102 | child: TextField( 103 | obscureText: passflag, 104 | onSubmitted: (val) => { 105 | passlist = [], 106 | for (var i = 0; i < val.length; i++) 107 | { 108 | if (passlist.indexOf(val[i]) == -1 && 109 | passlist.length < 6) 110 | {passlist.add(val[i])} 111 | }, 112 | if (val.length > 7 && val.length < 20) 113 | { 114 | if (passlist.length == 6) 115 | { 116 | { 117 | this.setState(() { 118 | password = val; 119 | subEnd = false; 120 | passErr = null; 121 | }) 122 | } 123 | } 124 | else 125 | { 126 | this.setState(() { 127 | password = val; 128 | subEnd = false; 129 | passErr = '请输入6位以上不同字符组成的密码'; 130 | }) 131 | } 132 | } 133 | else 134 | { 135 | this.setState(() { 136 | password = val; 137 | subEnd = false; 138 | passErr = "请输入8-20位的密码"; 139 | }) 140 | } 141 | }, 142 | controller: new TextEditingController(text: password), 143 | decoration: InputDecoration( 144 | hintText: "请输入密码", 145 | errorText: passErr, 146 | suffixIcon: InkWell( 147 | child: passflag 148 | ? Icon(Icons.visibility_off) 149 | : Icon(Icons.visibility), 150 | onTap: () => { 151 | this.setState(() { 152 | passflag = !passflag; 153 | }) 154 | }), 155 | ), 156 | ), 157 | ), 158 | ], 159 | ), 160 | ])), 161 | Padding( 162 | padding: const EdgeInsets.symmetric(vertical: 12.0), 163 | child: !isSub 164 | ? RaisedButton( 165 | color: name != "" && passErr == null 166 | ? Colors.lightBlue 167 | : Colors.grey, 168 | onPressed: () => { 169 | if (name != "" && passErr == null) 170 | { 171 | this.setState(() { 172 | isSub = true; 173 | }), 174 | timer = new Timer(new Duration(seconds: 3), () { 175 | this.setState(() { 176 | isSub = false; 177 | subEnd = true; 178 | }); 179 | }), 180 | } 181 | }, 182 | child: Text("登录"), 183 | ) 184 | : CupertinoActivityIndicator()), 185 | Column( 186 | children: [ 187 | Row( 188 | children: [ 189 | Text("用户名:"), 190 | Text( 191 | name != "" && subEnd ? name : "未填写", 192 | style: TextStyle(decoration: TextDecoration.underline), 193 | ) 194 | ], 195 | ), 196 | Row( 197 | children: [ 198 | Text("密码:"), 199 | Text(password != "" && subEnd ? password : "未填写", 200 | style: TextStyle(fontStyle: FontStyle.italic)) 201 | ], 202 | ) 203 | ], 204 | ) 205 | ], 206 | ), 207 | ), 208 | ); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/leonard_login.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | 4 | class LoginApp extends StatelessWidget { 5 | @override 6 | Widget build(BuildContext context) { 7 | return MaterialApp( 8 | title: 'Flutter Login Demo', 9 | theme: ThemeData( 10 | primarySwatch: Colors.blue, 11 | visualDensity: VisualDensity.adaptivePlatformDensity, 12 | ), 13 | home: LoginHomePage(title: 'Login Page'), 14 | ); 15 | } 16 | } 17 | 18 | class LoginHomePage extends StatefulWidget { 19 | LoginHomePage({Key key, this.title}) : super(key: key); 20 | 21 | final String title; 22 | 23 | @override 24 | _LoginHomePageState createState() => _LoginHomePageState(); 25 | } 26 | 27 | class _LoginHomePageState extends State { 28 | final TextEditingController usernameController = TextEditingController(); 29 | final TextEditingController passwordController = TextEditingController(); 30 | final ValueNotifier usernameCloseValue = ValueNotifier(true); 31 | final ValueNotifier passwordHiddenValue = ValueNotifier(true); 32 | final ValueNotifier textShowValue = ValueNotifier(false); 33 | final ValueNotifier buttonEnabled = ValueNotifier(false); 34 | 35 | void checkInputText() { 36 | buttonEnabled.value = (usernameController.text.isNotEmpty && 37 | passwordController.text.length >= 8); 38 | } 39 | 40 | @override 41 | Widget build(BuildContext context) { 42 | return Scaffold( 43 | body: Padding( 44 | padding: EdgeInsets.only(top: 60, left: 15, right: 15), 45 | child: Column( 46 | children: [ 47 | TextField( 48 | autofocus: true, 49 | controller: usernameController, 50 | decoration: InputDecoration( 51 | hintText: '请输入用户名', 52 | prefixIcon: Icon(Icons.supervisor_account), 53 | suffix: usernameCloseValue.value 54 | ? SizedBox.shrink() 55 | : TextSuffixButton( 56 | suffixIcon: Icon( 57 | Icons.close, 58 | color: Colors.blue, 59 | ), 60 | clearTextTap: () { 61 | usernameController.text = ''; 62 | setState(() {}); 63 | }), 64 | ), 65 | onChanged: (String text) { 66 | checkInputText(); 67 | usernameCloseValue.value = text.length == 0; 68 | setState(() {}); 69 | }, 70 | ), 71 | TextField( 72 | controller: passwordController, 73 | obscureText: passwordHiddenValue.value, 74 | inputFormatters: [ 75 | FilteringTextInputFormatter(RegExp("[a-zA-Z]|[0-9]"), 76 | allow: true), 77 | ], 78 | decoration: InputDecoration( 79 | hintText: '请输入密码', 80 | prefixIcon: Icon(Icons.lock), 81 | suffixIcon: passwordHiddenValue.value 82 | ? TextSuffixButton( 83 | suffixIcon: Icon( 84 | Icons.visibility, 85 | ), 86 | clearTextTap: () { 87 | print('open'); 88 | passwordHiddenValue.value = false; 89 | setState(() {}); 90 | }) 91 | : TextSuffixButton( 92 | suffixIcon: Icon(Icons.visibility_off), 93 | clearTextTap: () { 94 | print('hidden'); 95 | passwordHiddenValue.value = true; 96 | setState(() {}); 97 | }, 98 | ), 99 | ), 100 | onChanged: (String text) { 101 | checkInputText(); 102 | setState(() {}); 103 | }, 104 | ), 105 | Padding( 106 | padding: EdgeInsets.only(top: 30, left: 30, right: 30), 107 | child: GestureDetector( 108 | child: Container( 109 | height: 44, 110 | alignment: Alignment.center, 111 | color: buttonEnabled.value ? Colors.blue : Colors.grey, 112 | child: Text( 113 | '登录', 114 | ), 115 | ), 116 | onTap: buttonEnabled.value 117 | ? () { 118 | textShowValue.value = true; 119 | setState(() {}); 120 | } 121 | : null, 122 | ), 123 | ), 124 | Padding( 125 | padding: EdgeInsets.only(top: 46), 126 | child: textShowValue.value 127 | ? Text( 128 | '用户名:' + 129 | (usernameController.text.isEmpty 130 | ? '未填写' 131 | : usernameController.text), 132 | ) 133 | : SizedBox.shrink(), 134 | ), 135 | textShowValue.value 136 | ? Text( 137 | '密码:' + 138 | (passwordController.text.isEmpty 139 | ? '未填写' 140 | : passwordController.text), 141 | ) 142 | : SizedBox.shrink(), 143 | ], 144 | ), 145 | ), 146 | ); 147 | } 148 | } 149 | 150 | class TextSuffixButton extends StatelessWidget { 151 | const TextSuffixButton({ 152 | Key key, 153 | @required this.suffixIcon, 154 | @required this.clearTextTap, 155 | }) : assert(suffixIcon != null), 156 | assert(clearTextTap != null), 157 | super(key: key); 158 | 159 | final Icon suffixIcon; 160 | final Function clearTextTap; 161 | 162 | @override 163 | Widget build(BuildContext context) { 164 | return GestureDetector( 165 | child: suffixIcon, 166 | onTap: () { 167 | clearTextTap(); 168 | }, 169 | ); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/lvyan_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | void main() { 6 | runApp(MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return MaterialApp( 13 | title: 'Flutter Demo', 14 | theme: ThemeData( 15 | primarySwatch: Colors.blue, 16 | visualDensity: VisualDensity.adaptivePlatformDensity, 17 | ), 18 | home: const MyHomePage(title: 'Flutter Demo Home Page'), 19 | ); 20 | } 21 | } 22 | 23 | class MyHomePage extends StatefulWidget { 24 | const MyHomePage({Key key, this.title}) : super(key: key); 25 | 26 | final String title; 27 | 28 | @override 29 | _MyHomePageState createState() => _MyHomePageState(); 30 | } 31 | 32 | class _MyHomePageState extends State { 33 | final TextEditingController _usernameController = TextEditingController(); 34 | 35 | bool _visible = false; 36 | 37 | bool _isLogin = false; 38 | 39 | /// 使用流监听文本变化结合StreamBuilder可以减少setState的使用并控制控件刷新范围 40 | final StreamController _usernameStream = StreamController(); 41 | final StreamController _passwordStream = StreamController(); 42 | 43 | /// 不知道怎么合并两个流的结果 😄 44 | final StreamController _loginStream = StreamController(); 45 | 46 | Timer _loginTimer; 47 | 48 | String _password = ''; 49 | 50 | @override 51 | void initState() { 52 | super.initState(); 53 | 54 | _usernameController.addListener( 55 | () => _listenChanged(_usernameStream, _usernameController.text)); 56 | } 57 | 58 | /// 将控件抽取成变量能减少build方法内控件嵌套造成的爬楼梯现象,方便阅读代码 59 | /// 这与在build内部抽取的final变量除了作用域的区别之外并无其他太大区别 60 | Widget get _clearIcon => StreamBuilder( 61 | stream: _usernameStream.stream, 62 | builder: (_, AsyncSnapshot snapshot) { 63 | Widget child = const SizedBox.shrink(); 64 | 65 | if (snapshot.hasData && snapshot.data.isNotEmpty) { 66 | child = IconButton( 67 | icon: const Icon(Icons.clear), 68 | onPressed: () => _usernameController.clear(), 69 | ); 70 | } 71 | 72 | return child; 73 | }, 74 | ); 75 | 76 | @override 77 | Widget build(BuildContext context) { 78 | final Widget usernameField = TextField( 79 | controller: _usernameController, 80 | maxLength: 20, 81 | decoration: InputDecoration( 82 | prefixIcon: const Icon(Icons.supervisor_account), 83 | hintText: '请输入用户名', 84 | counterText: '', 85 | border: const UnderlineInputBorder(), 86 | suffixIcon: _clearIcon, 87 | ), 88 | ); 89 | 90 | final Widget visibilityIcon = IconButton( 91 | icon: Icon(_visible ? Icons.visibility_off : Icons.visibility), 92 | onPressed: () => setState(() => _visible = !_visible), 93 | ); 94 | 95 | final Widget passwordField = StreamBuilder( 96 | stream: _passwordStream.stream, 97 | builder: (_, AsyncSnapshot snapshot) { 98 | String helperText; 99 | 100 | final String password = snapshot.data; 101 | if (snapshot.hasData && password.isNotEmpty) { 102 | if (password.length < 8) { 103 | helperText = '密码不能少于8位'; 104 | } else if (password.split('').toSet().length < 6) { 105 | // 利用Set集合的特性判断密码复杂度 106 | helperText = '密码复杂度不能低于6'; 107 | } 108 | } 109 | 110 | return TextField( 111 | maxLength: 20, 112 | onChanged: (String text) { 113 | _password = text; 114 | _listenChanged(_passwordStream, text); 115 | }, 116 | obscureText: !_visible, 117 | obscuringCharacter: '*', 118 | decoration: InputDecoration( 119 | prefixIcon: const Icon(Icons.lock), 120 | hintText: '请输入密码', 121 | counterText: '', 122 | helperText: helperText, 123 | border: const UnderlineInputBorder(), 124 | suffixIcon: visibilityIcon, 125 | ), 126 | ); 127 | }, 128 | ); 129 | 130 | final Widget loginButton = StreamBuilder( 131 | stream: _loginStream.stream, 132 | builder: (_, AsyncSnapshot snapshot) { 133 | bool canLogin = false; 134 | if (snapshot.hasData) { 135 | canLogin = snapshot.data; 136 | } 137 | return RaisedButton( 138 | child: _isLogin 139 | ? const SizedBox( 140 | child: CircularProgressIndicator(strokeWidth: 2), 141 | width: 20, 142 | height: 20, 143 | ) 144 | : const Text('登录'), 145 | color: Theme.of(context).primaryColor, 146 | onPressed: canLogin && !_isLogin 147 | ? () { 148 | setState(() { 149 | _isLogin = true; 150 | 151 | _loginTimer = Timer(const Duration(seconds: 3), 152 | () => setState(() => _isLogin = false)); 153 | }); 154 | } 155 | : null, 156 | ); 157 | }, 158 | ); 159 | 160 | final Widget displayNameText = Text.rich( 161 | TextSpan( 162 | children: [ 163 | const TextSpan(text: '用户名:'), 164 | TextSpan( 165 | text: _usernameController.text.isEmpty ? '未填写' : _usernameController.text, 166 | style: const TextStyle(decoration: TextDecoration.underline), 167 | ), 168 | ], 169 | ), 170 | ); 171 | 172 | final Widget displayPasswordText = Text.rich( 173 | TextSpan( 174 | children: [ 175 | const TextSpan(text: '密码:'), 176 | TextSpan( 177 | text: _password.isEmpty ? '未填写' : _password, 178 | style: const TextStyle(fontStyle: FontStyle.italic), 179 | ), 180 | ], 181 | ), 182 | ); 183 | 184 | return Scaffold( 185 | appBar: AppBar(title: Text(widget.title)), 186 | body: Center( 187 | child: Column( 188 | mainAxisAlignment: MainAxisAlignment.center, 189 | children: [ 190 | usernameField, 191 | passwordField, 192 | loginButton, 193 | const SizedBox(height: 20), 194 | displayNameText, 195 | displayPasswordText, 196 | ], 197 | ), 198 | ), 199 | ); 200 | } 201 | 202 | void _listenChanged( 203 | StreamController streamController, 204 | String text, 205 | ) { 206 | streamController.sink.add(text); 207 | 208 | _loginStream.sink.add(_usernameController.text.isNotEmpty && 209 | _password.length >= 8 && 210 | _password.split('').toSet().length >= 6); 211 | } 212 | 213 | @override 214 | void dispose() { 215 | // 记得释放资源 216 | _usernameStream.close(); 217 | _passwordStream.close(); 218 | _loginStream.close(); 219 | _loginTimer?.cancel(); 220 | _usernameController.dispose(); 221 | super.dispose(); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/ray_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() { 4 | runApp(MyApp()); 5 | } 6 | 7 | class MyApp extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return MaterialApp( 11 | title: 'Flutter Demo', 12 | theme: ThemeData( 13 | primarySwatch: Colors.blue, 14 | visualDensity: VisualDensity.adaptivePlatformDensity, 15 | ), 16 | home: MyHomePage(title: 'Flutter Demo Home Page'), 17 | ); 18 | } 19 | } 20 | 21 | class MyHomePage extends StatefulWidget { 22 | MyHomePage({Key key, this.title}) : super(key: key); 23 | 24 | final String title; 25 | 26 | @override 27 | _MyHomePageState createState() => _MyHomePageState(); 28 | } 29 | 30 | class _MyHomePageState extends State { 31 | TextEditingController _accountController; 32 | TextEditingController _pwdController; 33 | String account = '未填写'; 34 | String pwd = '未填写'; 35 | bool obscureText = false; 36 | bool enabled = false; 37 | bool submitting = false; 38 | 39 | @override 40 | void initState() { 41 | super.initState(); 42 | _accountController = TextEditingController()..addListener(_checkInput); 43 | _pwdController = TextEditingController()..addListener(_checkInput); 44 | } 45 | 46 | void _checkInput() { 47 | var account = _accountController.text; 48 | var pwd = _pwdController.text; 49 | bool enable = account.isNotEmpty && _checkPwd(pwd); 50 | if (enabled != enable) { 51 | setState(() { 52 | enabled = enable; 53 | }); 54 | } 55 | } 56 | 57 | bool _checkPwd(String pwd) { 58 | if (pwd == null || pwd.length < 8) { 59 | return false; 60 | } 61 | return pwd.split('').toSet().length >= 6; 62 | } 63 | 64 | void _onSubmit() { 65 | if (submitting) { 66 | return; 67 | } 68 | setState(() { 69 | submitting = true; 70 | account = _accountController.text; 71 | pwd = _pwdController.text; 72 | Future.delayed( 73 | Duration(seconds: 3), 74 | ).then( 75 | (value) => mounted ? setState(() { 76 | submitting = false; 77 | }) : null, 78 | ); 79 | }); 80 | } 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | return Scaffold( 85 | appBar: AppBar( 86 | title: Text(widget.title), 87 | ), 88 | body: Column( 89 | children: [ 90 | Text( 91 | '用户名:$account', 92 | style: TextStyle(decoration: TextDecoration.underline), 93 | ), 94 | Text( 95 | '密码:$pwd', 96 | style: TextStyle(fontStyle: FontStyle.italic), 97 | ), 98 | TextField( 99 | controller: _accountController, 100 | decoration: InputDecoration( 101 | prefixIcon: Icon( 102 | Icons.supervisor_account, 103 | ), 104 | suffixIcon: ValueListenableBuilder( 105 | valueListenable: _accountController, 106 | builder: (_, value, __) { 107 | bool empty = value.text.isEmpty; 108 | return Visibility( 109 | visible: !empty, 110 | child: GestureDetector( 111 | onTap: () => _accountController.text = '', 112 | behavior: HitTestBehavior.opaque, 113 | child: Icon( 114 | Icons.close, 115 | ), 116 | ), 117 | ); 118 | }, 119 | ), 120 | hintText: '请输入用户名', 121 | border: UnderlineInputBorder(), 122 | ), 123 | ), 124 | TextField( 125 | controller: _pwdController, 126 | obscureText: obscureText, 127 | decoration: InputDecoration( 128 | prefixIcon: Icon( 129 | Icons.lock, 130 | ), 131 | suffixIcon: GestureDetector( 132 | onTap: () => setState(() => obscureText = !obscureText), 133 | behavior: HitTestBehavior.opaque, 134 | child: Icon( 135 | obscureText ? Icons.visibility_off : Icons.visibility, 136 | ), 137 | ), 138 | hintText: '请输入密码', 139 | border: UnderlineInputBorder(), 140 | ), 141 | ), 142 | FlatButton( 143 | onPressed: enabled ? _onSubmit : null, 144 | color: Theme.of(context).primaryColor, 145 | disabledColor: Colors.grey, 146 | child: submitting 147 | ? Container( 148 | width: 25, 149 | height: 25, 150 | child: CircularProgressIndicator( 151 | backgroundColor: Colors.redAccent, 152 | ), 153 | ) 154 | : Text('登录'), 155 | ), 156 | ], 157 | ), 158 | ); 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/sun_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | // This widget is the root of your application. 10 | @override 11 | Widget build(BuildContext context) { 12 | return MaterialApp( 13 | title: 'Flutter Demo', 14 | theme: ThemeData( 15 | // This is the theme of your application. 16 | // 17 | // Try running your application with "flutter run". You'll see the 18 | // application has a blue toolbar. Then, without quitting the app, try 19 | // changing the primarySwatch below to Colors.green and then invoke 20 | // "hot reload" (press "r" in the console where you ran "flutter run", 21 | // or simply save your changes to "hot reload" in a Flutter IDE). 22 | // Notice that the counter didn't reset back to zero; the application 23 | // is not restarted. 24 | primarySwatch: Colors.blue, 25 | // This makes the visual density adapt to the platform that you run 26 | // the app on. For desktop platforms, the controls will be smaller and 27 | // closer together (more dense) than on mobile platforms. 28 | visualDensity: VisualDensity.adaptivePlatformDensity, 29 | ), 30 | home: MyHomePage(title: 'Flutter Test'), 31 | // RegisterPage(), 32 | ); 33 | } 34 | } 35 | 36 | class RegisterPage extends StatelessWidget { 37 | @override 38 | Widget build(BuildContext context) { 39 | // TODO: implement build 40 | return Scaffold( 41 | // appBar: AppBar( 42 | // title: Text("首页"), 43 | // ), 44 | body: Center( 45 | child: Column( 46 | mainAxisAlignment: MainAxisAlignment.center, 47 | children: [ 48 | Row( 49 | mainAxisAlignment: MainAxisAlignment.center, 50 | children: [ 51 | Padding( 52 | padding: const EdgeInsets.only(left: 10.0), 53 | child: Icon(Icons.supervisor_account), 54 | ), 55 | Expanded( 56 | child: TextField( 57 | autofocus: true, 58 | decoration: InputDecoration( 59 | hintText: "请输入用户名", 60 | border: InputBorder.none, 61 | ), 62 | ), 63 | ), 64 | CupertinoButton( 65 | child: Icon( 66 | Icons.clear, 67 | color: Colors.grey, 68 | ), 69 | onPressed: () {}), 70 | ], 71 | ), 72 | Container(), 73 | Row( 74 | mainAxisAlignment: MainAxisAlignment.center, 75 | children: [ 76 | Padding( 77 | padding: EdgeInsets.only(left: 10.0), 78 | child: Icon(Icons.lock), 79 | ), 80 | Expanded( 81 | child: TextField( 82 | autofocus: false, 83 | obscureText: true, 84 | onChanged: ((v) { 85 | print("value: $v"); 86 | }), 87 | decoration: InputDecoration( 88 | hintText: "请输入密码", 89 | border: InputBorder.none, 90 | // prefixIcon: Icon(Icons.lock), 91 | ), 92 | ), 93 | ), 94 | CupertinoButton( 95 | child: Icon( 96 | Icons.visibility, 97 | color: Colors.grey, 98 | ), 99 | onPressed: () {}), 100 | ], 101 | ), 102 | CupertinoButton( 103 | onPressed: () {}, 104 | child: Text("登录"), 105 | color: Colors.blue, 106 | ) 107 | ], 108 | ), 109 | ), 110 | ); 111 | } 112 | } 113 | 114 | class MyHomePage extends StatefulWidget { 115 | MyHomePage({Key key, this.title}) : super(key: key); 116 | 117 | // This widget is the home page of your application. It is stateful, meaning 118 | // that it has a State object (defined below) that contains fields that affect 119 | // how it looks. 120 | 121 | // This class is the configuration for the state. It holds the values (in this 122 | // case the title) provided by the parent (in this case the App widget) and 123 | // used by the build method of the State. Fields in a Widget subclass are 124 | // always marked "final". 125 | 126 | final String title; 127 | 128 | @override 129 | _MyHomePageState createState() => _MyHomePageState(); 130 | } 131 | 132 | class _MyHomePageState extends State { 133 | /// 是否展示密码 134 | bool _isOpen = false; 135 | bool _isShowContent = false; 136 | bool _isLoginEnabled = false; 137 | TextEditingController _userCtrl = new TextEditingController(); 138 | TextEditingController _pwdCtrl = new TextEditingController(); 139 | 140 | void _login() { 141 | if (!_isLoginEnabled) { 142 | return; 143 | } 144 | setState(() { 145 | _isShowContent = true; 146 | }); 147 | } 148 | 149 | void _openEye() { 150 | setState(() { 151 | _isOpen = !_isOpen; 152 | }); 153 | } 154 | 155 | void _changed(String v) { 156 | setState(() { 157 | if (_userCtrl.text.isEmpty && _pwdCtrl.text.length < 8) { 158 | _isLoginEnabled = false; 159 | } else { 160 | _isLoginEnabled = true; 161 | } 162 | }); 163 | print('$v'); 164 | } 165 | 166 | @override 167 | Widget build(BuildContext context) { 168 | String v; 169 | return Scaffold( 170 | // appBar: AppBar( 171 | // title: Text("首页"), 172 | // ), 173 | body: Center( 174 | child: Column( 175 | mainAxisAlignment: MainAxisAlignment.center, 176 | children: [ 177 | Row( 178 | mainAxisAlignment: MainAxisAlignment.center, 179 | children: [ 180 | Padding( 181 | padding: const EdgeInsets.only(left: 10.0), 182 | child: Icon(Icons.supervisor_account), 183 | ), 184 | Expanded( 185 | child: TextField( 186 | autofocus: true, 187 | controller: _userCtrl, 188 | decoration: InputDecoration( 189 | hintText: "请输入用户名", 190 | border: InputBorder.none, 191 | ), 192 | // onChanged: ((v) { 193 | // // print("value: $v"); 194 | // setState(() { 195 | // if (_userCtrl.text.isEmpty || 196 | // _pwdCtrl.text.length < 8) { 197 | // _isLoginEnabled = false; 198 | // } else { 199 | // _isLoginEnabled = true; 200 | // } 201 | // }); 202 | // }), 203 | onChanged: _changed, 204 | ), 205 | ), 206 | CupertinoButton( 207 | child: Icon( 208 | Icons.clear, 209 | color: Colors.grey, 210 | ), 211 | onPressed: () { 212 | _userCtrl.text = ""; 213 | }), 214 | ], 215 | ), 216 | Container( 217 | height: 1, 218 | margin: EdgeInsets.only(left: 35, right: 10, top: 0), 219 | color: Colors.grey, 220 | ), 221 | Row( 222 | mainAxisAlignment: MainAxisAlignment.center, 223 | children: [ 224 | Padding( 225 | padding: EdgeInsets.only(left: 10.0), 226 | child: Icon(Icons.lock), 227 | ), 228 | Expanded( 229 | child: TextField( 230 | autofocus: false, 231 | controller: _pwdCtrl, 232 | obscureText: _isOpen ? false : true, 233 | onChanged: (v) => _changed(v), 234 | decoration: InputDecoration( 235 | hintText: "请输入密码", 236 | border: InputBorder.none, 237 | // prefixIcon: Icon(Icons.lock), 238 | ), 239 | ), 240 | ), 241 | CupertinoButton( 242 | child: Icon( 243 | _isOpen ? Icons.visibility : Icons.visibility_off, 244 | color: Colors.grey, 245 | ), 246 | onPressed: _openEye, 247 | ), 248 | ], 249 | ), 250 | Container( 251 | height: 1, 252 | margin: EdgeInsets.only(left: 35, right: 10), 253 | color: Colors.grey, 254 | ), 255 | 256 | Padding( 257 | padding: EdgeInsets.only(top: 10), 258 | child: CupertinoButton( 259 | onPressed: _login, 260 | child: Text("登录"), 261 | color: _isLoginEnabled ? Colors.blue : Colors.grey, 262 | ), 263 | ), 264 | 265 | // result 266 | Padding( 267 | padding: EdgeInsets.only(top: 20), 268 | child: Text( 269 | _isShowContent 270 | ? (_userCtrl.text.isEmpty 271 | ? "用户名:未填写" 272 | : "用户名:" + _userCtrl.text) 273 | : "", 274 | style: TextStyle(decoration: TextDecoration.underline), 275 | ), 276 | ), 277 | 278 | Text( 279 | _isShowContent 280 | ? ((_pwdCtrl.text.length < 8) 281 | ? "密码要大于8位" 282 | : "密码:" + _pwdCtrl.text) 283 | : "", 284 | style: TextStyle(fontStyle: FontStyle.italic), 285 | ), 286 | ], 287 | ), 288 | ), 289 | ); 290 | } 291 | } 292 | -------------------------------------------------------------------------------- /1 - 注册表单的实现 - 20201127/yifang_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | void main() { 5 | runApp(MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | title: 'Flutter Demo', 13 | home: HomePage(), 14 | ); 15 | } 16 | } 17 | 18 | class HomePage extends StatefulWidget { 19 | @override 20 | HomePageState createState() => HomePageState(); 21 | } 22 | 23 | class HomePageState extends State { 24 | TextEditingController _accountController; 25 | TextEditingController _codeController; 26 | String _account = '未填写'; 27 | String _code = '未填写'; 28 | String _errorText; 29 | bool _empty = true; 30 | bool _obscureCode = true; 31 | bool _isLoad = false; 32 | bool _isConform = false; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | _accountController = TextEditingController(); 38 | _codeController = TextEditingController(); 39 | 40 | _accountController.addListener(_accountListener); 41 | _codeController.addListener(_codeListener); 42 | } 43 | 44 | @override 45 | void dispose() { 46 | _accountController.removeListener(_accountListener); 47 | _codeController.removeListener(_codeListener); 48 | _accountController.dispose(); 49 | _codeController.dispose(); 50 | super.dispose(); 51 | } 52 | 53 | void _accountListener() { 54 | if (_accountController.text == '') { 55 | if (!_empty) { 56 | setState(() { 57 | _empty = true; 58 | _isConform = false; 59 | }); 60 | } 61 | } else { 62 | if (_empty) { 63 | _empty = false; 64 | setState(() {}); 65 | } 66 | } 67 | _codeListener(); 68 | } 69 | 70 | void _codeListener() { 71 | if (!_empty) { 72 | final bool res = _codeVerify(_codeController.text); 73 | if (res != _isConform) { 74 | setState(() { 75 | _isConform = res; 76 | }); 77 | } 78 | } 79 | } 80 | 81 | bool _codeVerify(String text) { 82 | if (text.length < 8) return false; 83 | 84 | final bool res = text.split('').toSet().length >= 6; 85 | return res; 86 | } 87 | 88 | void _login() { 89 | setState(() { 90 | _account = _accountController.text; 91 | _code = _codeController.text; 92 | _isLoad = true; 93 | }); 94 | Future.delayed( 95 | const Duration(seconds: 3), 96 | ).then((value) { 97 | if (mounted) { 98 | setState(() { 99 | _isLoad = false; 100 | }); 101 | } 102 | }); 103 | } 104 | 105 | @override 106 | Widget build(BuildContext context) 107 | { 108 | return Scaffold( 109 | body: Center( 110 | child: Column( 111 | mainAxisSize: MainAxisSize.min, 112 | children: [ 113 | TextField( 114 | controller: _accountController, 115 | textInputAction: TextInputAction.next, 116 | decoration: InputDecoration( 117 | icon: const Icon(Icons.supervisor_account), 118 | hintText: '请输入用户名', 119 | suffixIcon: _empty 120 | ? null 121 | : IconButton( 122 | icon: const Icon(Icons.close), 123 | onPressed: () { 124 | _accountController.text = ''; 125 | setState(() { 126 | _empty = false; 127 | _isConform = false; 128 | }); 129 | }, 130 | ), 131 | ), 132 | ), 133 | TextField( 134 | controller: _codeController, 135 | textInputAction: TextInputAction.done, 136 | onSubmitted: (value){ 137 | 138 | }, 139 | obscureText: _obscureCode, 140 | decoration: InputDecoration( 141 | icon: const Icon(Icons.lock), 142 | hintText: '请输入密码', 143 | errorText: _errorText, 144 | suffixIcon: IconButton( 145 | icon: _obscureCode 146 | ? const Icon(Icons.visibility_off) 147 | : const Icon(Icons.visibility), 148 | onPressed: () { 149 | setState(() { 150 | _obscureCode = !_obscureCode; 151 | }); 152 | }, 153 | ), 154 | ), 155 | ), 156 | FlatButton( 157 | onPressed: _isConform ? _login : null, 158 | child: _isLoad 159 | ? const CupertinoActivityIndicator() 160 | : const Text('登录'), 161 | color: Theme.of(context).primaryColor, 162 | disabledColor: Colors.grey, 163 | ), 164 | Text( 165 | '用户名: $_account', 166 | style: const TextStyle(decoration: TextDecoration.underline), 167 | ), 168 | Text( 169 | '密码: $_code', 170 | style: const TextStyle(fontStyle: FontStyle.italic), 171 | ), 172 | ], 173 | ), 174 | ), 175 | ); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/README.md: -------------------------------------------------------------------------------- 1 | # 百格齐放 - 2020/12/02 2 | 3 | ![](http://pic.alexv525.com/2020-12-02-123329.png?) 4 | 5 | 同时实现两种布局,要求如下: 6 | * 上下 **各占一半** 的应用布局空间; 7 | * 上半部分: 8 | * 横向显示 **9** 个比例条,比例条的 **每一个部分** 的 **中间** 显示该部分的比例; 9 | * 每一条的总比例为 **10**,上部分比例分别为:`[2, 5, 8, 3, 6, 9, 1, 4, 7]`,下部分比例为剩余数。 10 | * 下半部分: 11 | * 从 **左上** 至 **右下** 依次层叠 **7** 个方块; 12 | * 下一个方块的 **左上角顶点** 为上一个方块的 **中心点**; 13 | * 最后一个方块的右下角与区域右下角 **顶点重合**。 14 | * 每个部分显示随机颜色。 15 | * 不能使用 `SizedBox`、`Container` 的 `width` 或 `height`、`ConstrainedBox`。 16 | 17 | 随机颜色生成器: 18 | 19 | ```dart 20 | import 'dart:math' as math; 21 | 22 | class RandomColor { 23 | const RandomColor._(); 24 | 25 | static final math.Random _random = math.Random(); 26 | 27 | static Color getColor() { 28 | return Color.fromARGB( 29 | 255, 30 | _random.nextInt(255), 31 | _random.nextInt(255), 32 | _random.nextInt(255), 33 | ); 34 | } 35 | } 36 | ``` 37 | 38 | 加分项: 39 | 40 | * 当 **横屏** 时,区域显示为 **左右** 两块;**竖屏** 时,显示为 **上下** 两块; 41 | * 上半部分: 42 | * 支持总条数为 **m**,且每条比例条的区域数为 **n** 的比例条,并要求每个区域的比例均可以通过数组控制; 43 | * 下半部分: 44 | * 支持总数为 **x** 个方块的层叠; 45 | * 在原有基础上,从 **右上** 至 **左下** 以同样的方式再层叠一组方块。 46 | 47 | ![](http://pic.alexv525.com/2020-12-02-2C4E536FE41DEDD98371A665E86F26A7.jpg) 48 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/alex_main.dart: -------------------------------------------------------------------------------- 1 | /// 2 | /// [Author] Alex (https://github.com/AlexV525) 3 | /// [Date] 2020/12/27 14:25 4 | /// 5 | import 'dart:math' as math; 6 | 7 | import 'package:flutter/material.dart'; 8 | 9 | void main() => runApp(MyApp()); 10 | 11 | class MyApp extends StatelessWidget { 12 | @override 13 | Widget build(BuildContext context) { 14 | return MaterialApp( 15 | title: 'Test App Demo', 16 | theme: ThemeData(primarySwatch: Colors.blue), 17 | home: const BlossomPage(), 18 | ); 19 | } 20 | } 21 | 22 | class BlossomPage extends StatelessWidget { 23 | const BlossomPage({Key key}) : super(key: key); 24 | 25 | List> get flexLists { 26 | return >[ 27 | [2, 7, 1], 28 | [4, 3, 1, 2], 29 | [3, 1, 2, 4], 30 | [3, 4, 2, 1], 31 | [1, 2, 1, 2, 1, 2, 1], 32 | ]; 33 | } 34 | 35 | int get stackedRectangles => 10; 36 | 37 | Widget get _part1 { 38 | return Row( 39 | children: List.generate( 40 | flexLists.length, 41 | (int i) { 42 | final List list = flexLists[i]; 43 | return Expanded( 44 | child: Column( 45 | children: List.generate( 46 | list.length, 47 | (int j) => _part1ItemWidget(list[j]), 48 | ), 49 | ), 50 | ); 51 | }, 52 | ), 53 | ); 54 | } 55 | 56 | Widget get _part2 { 57 | return LayoutBuilder( 58 | builder: (BuildContext context, BoxConstraints constraints) { 59 | final double _w = constraints.maxWidth / (stackedRectangles + 1); 60 | final double _h = constraints.maxHeight / (stackedRectangles + 1); 61 | return Stack( 62 | children: [ 63 | ...List.generate(stackedRectangles, (int i) { 64 | return _part2ItemWidget(i, _w, _h, true); 65 | }), 66 | ...List.generate(stackedRectangles, (int i) { 67 | return _part2ItemWidget(i, _w, _h, false); 68 | }), 69 | ], 70 | ); 71 | }, 72 | ); 73 | } 74 | 75 | Widget _part1ItemWidget(int flex) { 76 | return Expanded( 77 | flex: flex, 78 | child: Container( 79 | alignment: Alignment.center, 80 | color: RandomColor.getColor(), 81 | child: Text('$flex'), 82 | ), 83 | ); 84 | } 85 | 86 | Widget _part2ItemWidget(int i, double w, double h, bool left) { 87 | return Positioned( 88 | left: left ? i * w : null, 89 | right: left ? null : i * w, 90 | top: i * h, 91 | width: w * 2, 92 | height: h * 2, 93 | child: ColoredBox(color: RandomColor.getColor()), 94 | ); 95 | } 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Material( 100 | child: OrientationBuilder( 101 | builder: (BuildContext c, Orientation o) => Flex( 102 | direction: 103 | o == Orientation.landscape ? Axis.horizontal : Axis.vertical, 104 | children: [Expanded(child: _part1), Expanded(child: _part2)], 105 | ), 106 | ), 107 | ); 108 | } 109 | } 110 | 111 | class RandomColor { 112 | const RandomColor._(); 113 | 114 | static final math.Random _random = math.Random(); 115 | 116 | static Color getColor() { 117 | return Color.fromARGB( 118 | 255, 119 | _random.nextInt(255), 120 | _random.nextInt(255), 121 | _random.nextInt(255), 122 | ); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/end_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math' as math; 3 | 4 | class RandomColor { 5 | const RandomColor._(); 6 | 7 | static final math.Random _random = math.Random(); 8 | 9 | static Color getColor() { 10 | return Color.fromARGB( 11 | 255, 12 | _random.nextInt(255), 13 | _random.nextInt(255), 14 | _random.nextInt(255), 15 | ); 16 | } 17 | } 18 | 19 | class BaigeWidget extends StatelessWidget { 20 | final gridList = [2, 5, 8, 3, 6, 9, 1, 4, 7]; 21 | final cubeList = [1, 2, 3, 4, 5, 6, 7]; 22 | 23 | @override 24 | void initState() {} 25 | 26 | Widget grid(int flex) { 27 | return Expanded( 28 | flex: flex, 29 | child: Container( 30 | child: Center( 31 | child: Text( 32 | '${flex}', 33 | textAlign: TextAlign.center, 34 | ) 35 | ), 36 | color: RandomColor.getColor(), 37 | ), 38 | ); 39 | } 40 | 41 | Widget scaleBar() { 42 | return Expanded( 43 | child: Row( 44 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 45 | children: gridList.asMap().keys.map((item) => Expanded( 46 | child: Column( 47 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 48 | mainAxisSize: MainAxisSize.max, 49 | children: [ 50 | grid(gridList[item]), 51 | grid(10 - gridList[item]), 52 | ] 53 | ) 54 | )).toList(), 55 | ), 56 | ); 57 | } 58 | Widget cube() { 59 | return Expanded( 60 | child: LayoutBuilder( 61 | builder: (BuildContext context, BoxConstraints constraints) { 62 | final width = constraints.maxWidth / 8; 63 | final height = constraints.maxWidth / 8; 64 | return Stack( 65 | children: cubeList.asMap().keys.map((item) => Positioned( 66 | child: Container( 67 | color: RandomColor.getColor(), 68 | ), 69 | left: item * width, 70 | right: (6.0 - item) * width, 71 | top: item * height, 72 | bottom: (6.0 - item) * height, 73 | )).toList(), 74 | ); 75 | } 76 | ), 77 | ); 78 | } 79 | 80 | @override 81 | Widget build(BuildContext context) { 82 | return Scaffold( 83 | appBar: AppBar( 84 | title: Text('百格齐放'), 85 | ), 86 | body: OrientationBuilder( 87 | builder: (contet, orientation) { 88 | if (orientation == Orientation.portrait) { 89 | return Column( 90 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 91 | children: [ 92 | scaleBar(), 93 | cube(), 94 | ] 95 | ); 96 | } else { 97 | return Row( 98 | mainAxisAlignment: MainAxisAlignment.spaceBetween, 99 | children: [ 100 | scaleBar(), 101 | cube(), 102 | ] 103 | ); 104 | } 105 | } 106 | ), 107 | ); 108 | } 109 | } -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/laelluo_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math show Random, max; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | void main() => runApp(Application()); 6 | 7 | class Application extends StatelessWidget { 8 | @override 9 | Widget build(BuildContext context) { 10 | return MaterialApp(home: MainPage()); 11 | } 12 | } 13 | 14 | class MainPage extends StatelessWidget { 15 | const MainPage({ 16 | Key key, 17 | }) : super(key: key); 18 | 19 | Color get randomColor => RandomColor.getColor(); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | /// m = 5, n = 2 or 3 的数据列表 24 | final dataList = [ 25 | [1, 2], 26 | [3, 4], 27 | [5, 6], 28 | [7, 8, 9], 29 | [1, 2, 3], 30 | ]; 31 | return Material( 32 | /// 监听屏幕方向 33 | child: OrientationBuilder( 34 | builder: (BuildContext context, Orientation orientation) { 35 | if (orientation == Orientation.landscape) { 36 | return Row(children: [buildList(dataList), buildStack(true, 8)]); 37 | } 38 | return Column( 39 | children: [buildList(dataList), buildStack(false, 7)], 40 | ); 41 | }, 42 | ), 43 | ); 44 | } 45 | 46 | /// 构建单个小方块 47 | Expanded buildStack(bool isLandscape, int x) { 48 | final children = []; 49 | for (var index = 0; index < x; ++index) { 50 | children.add(buildFractionallySizedBox( 51 | Alignment(-1, -1), 52 | Alignment(1, 1), 53 | index, 54 | x, 55 | )); 56 | if (isLandscape) { 57 | children.add(buildFractionallySizedBox( 58 | Alignment(1, -1), 59 | Alignment(-1, 1), 60 | index, 61 | x, 62 | )); 63 | } 64 | } 65 | 66 | return Expanded( 67 | child: Stack( 68 | fit: StackFit.expand, 69 | children: children, 70 | ), 71 | ); 72 | } 73 | 74 | /// 构建单个小方块 75 | FractionallySizedBox buildFractionallySizedBox( 76 | Alignment begin, 77 | Alignment end, 78 | int index, 79 | int x, 80 | ) { 81 | /// 计算单个小方块占据父组件宽高的比例 82 | final factor = 1 / ((x + 1) / 2); 83 | return FractionallySizedBox( 84 | widthFactor: factor, 85 | heightFactor: factor, 86 | /// 计算小方块所在位置 87 | alignment: AlignmentTween( 88 | begin: begin, 89 | end: end, 90 | ).lerp(index / (x - 1)), 91 | child: DecoratedBox( 92 | decoration: BoxDecoration(color: randomColor), 93 | ), 94 | ); 95 | } 96 | 97 | /// 根据dataList构建所有比例条 98 | Expanded buildList(List> data) { 99 | return Expanded( 100 | child: Row( 101 | children: data.map((e) { 102 | return Expanded( 103 | child: Column(children: e.map(buildBox).toList()), 104 | ); 105 | }).toList(), 106 | ), 107 | ); 108 | } 109 | 110 | /// 构建比例条的单个小方块 111 | Expanded buildBox(int e) { 112 | return Expanded( 113 | flex: e, 114 | child: DecoratedBox( 115 | decoration: BoxDecoration(color: randomColor), 116 | child: Center(child: Text(e.toString())), 117 | ), 118 | ); 119 | } 120 | } 121 | 122 | class RandomColor { 123 | const RandomColor._(); 124 | 125 | static final math.Random _random = math.Random(); 126 | 127 | static Color getColor() { 128 | return Color.fromARGB( 129 | 255, 130 | _random.nextInt(255), 131 | _random.nextInt(255), 132 | _random.nextInt(255), 133 | ); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/lvyan_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:math' as math; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() { 7 | runApp(MyApp()); 8 | } 9 | 10 | class MyApp extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'Flutter Demo', 15 | theme: ThemeData( 16 | primarySwatch: Colors.blue, 17 | visualDensity: VisualDensity.adaptivePlatformDensity, 18 | ), 19 | home: MyHomePage(), 20 | ); 21 | } 22 | } 23 | 24 | class MyHomePage extends StatelessWidget { 25 | final int _totalRatio = 10; 26 | final List _stripRatios = [2, 5, 8, 3, 6, 9, 1, 4, 7]; 27 | 28 | final int _gridCount = 7; 29 | 30 | Widget _buildStrip(int ratio) => Expanded( 31 | flex: ratio, 32 | child: Container( 33 | color: RandomColor.getColor(), 34 | child: Center(child: Text('$ratio')), 35 | ), 36 | ); 37 | 38 | Widget get _firstPart => Row( 39 | children: _stripRatios 40 | .map( 41 | (int e) => Expanded( 42 | child: Column( 43 | children: [ 44 | _buildStrip(e), 45 | _buildStrip(_totalRatio - e) 46 | ], 47 | ), 48 | ), 49 | ) 50 | .toList(), 51 | ); 52 | 53 | Widget get _secondPart => LayoutBuilder( 54 | builder: (BuildContext context, BoxConstraints constraints) { 55 | final List grids = []; 56 | final List mirrorGrids = []; 57 | 58 | final double maxWidth = constraints.maxWidth; 59 | final double maxHeight = constraints.maxHeight; 60 | final double width = (maxWidth / (_gridCount + 1)) * 2; 61 | final double height = (maxHeight / (_gridCount + 1)) * 2; 62 | 63 | for (int i = 0; i < _gridCount; i++) { 64 | final double offsetX = i * width / 2; 65 | final double offsetY = i * height / 2; 66 | 67 | final Widget grid = _buildGrid( 68 | width, 69 | height, 70 | offsetX, 71 | offsetY, 72 | ); 73 | grids.add(grid); 74 | 75 | final Widget mirrorGrid = _buildGrid( 76 | width, 77 | height, 78 | maxWidth - offsetX - width, 79 | offsetY, 80 | ); 81 | mirrorGrids.add(mirrorGrid); 82 | } 83 | 84 | return Stack(children: [...grids, ...mirrorGrids]); 85 | }, 86 | ); 87 | 88 | Widget _buildGrid( 89 | double width, 90 | double height, 91 | double left, 92 | double top, 93 | ) => 94 | Positioned( 95 | left: left, 96 | top: top, 97 | width: width, 98 | height: height, 99 | child: ColoredBox(color: RandomColor.getColor()), 100 | ); 101 | 102 | @override 103 | Widget build(BuildContext context) => Material( 104 | child: OrientationBuilder( 105 | builder: (_, Orientation orientation) { 106 | final List children = [ 107 | Expanded(child: _firstPart), 108 | Expanded(child: _secondPart), 109 | ]; 110 | 111 | Axis direction; 112 | if (orientation == Orientation.portrait) { 113 | direction = Axis.vertical; 114 | } else { 115 | direction = Axis.horizontal; 116 | } 117 | 118 | return Flex( 119 | direction: direction, 120 | children: children, 121 | ); 122 | }, 123 | ), 124 | ); 125 | } 126 | 127 | class RandomColor { 128 | const RandomColor._(); 129 | 130 | static final math.Random _random = math.Random(); 131 | 132 | static Color getColor() { 133 | return Color.fromARGB( 134 | 255, 135 | _random.nextInt(255), 136 | _random.nextInt(255), 137 | _random.nextInt(255), 138 | ); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/ray_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:math' as math; 3 | 4 | void main() { 5 | runApp(MyApp()); 6 | } 7 | 8 | class MyApp extends StatelessWidget { 9 | @override 10 | Widget build(BuildContext context) { 11 | return MaterialApp( 12 | title: 'Flutter Demo', 13 | theme: ThemeData( 14 | primarySwatch: Colors.blue, 15 | visualDensity: VisualDensity.adaptivePlatformDensity, 16 | ), 17 | home: MyHomePage(title: 'Flutter Demo Home Page'), 18 | ); 19 | } 20 | } 21 | 22 | class MyHomePage extends StatefulWidget { 23 | MyHomePage({Key key, this.title}) : super(key: key); 24 | 25 | final String title; 26 | 27 | @override 28 | _MyHomePageState createState() => _MyHomePageState(); 29 | } 30 | 31 | class _MyHomePageState extends State { 32 | @override 33 | Widget build(BuildContext context) { 34 | return Scaffold( 35 | body: OrientationBuilder( 36 | builder: (_, orientation) { 37 | List children = [ 38 | Expanded( 39 | child: PercentBoxWidget( 40 | max: 10, 41 | data: [2, 5, 8, 3, 6, 9, 1, 4, 7], 42 | ), 43 | ), 44 | Expanded( 45 | child: StackBoxWidget( 46 | boxCount: 10, 47 | ), 48 | ), 49 | ]; 50 | if (orientation == Orientation.landscape) { 51 | return Row( 52 | children: children, 53 | ); 54 | } else { 55 | return Column( 56 | children: children, 57 | ); 58 | } 59 | }, 60 | ), 61 | ); 62 | } 63 | } 64 | 65 | class RandomColor { 66 | const RandomColor._(); 67 | 68 | static final math.Random _random = math.Random(); 69 | 70 | static Color getColor() { 71 | return Color.fromARGB( 72 | 255, 73 | _random.nextInt(255), 74 | _random.nextInt(255), 75 | _random.nextInt(255), 76 | ); 77 | } 78 | } 79 | 80 | class PercentBoxWidget extends LeafRenderObjectWidget { 81 | final int max; 82 | final List data; 83 | 84 | PercentBoxWidget({ 85 | this.max = 10, 86 | this.data = const [], 87 | }) : assert(max != null && max > 1), 88 | assert(() { 89 | if (data == null) { 90 | return false; 91 | } 92 | data.forEach((element) { 93 | assert(element > 0 && element < max); 94 | }); 95 | return true; 96 | }()); 97 | 98 | @override 99 | RenderPercentBox createRenderObject(BuildContext context) { 100 | return RenderPercentBox(max, data); 101 | } 102 | 103 | @override 104 | void updateRenderObject( 105 | BuildContext context, covariant RenderPercentBox renderPercentBox) { 106 | renderPercentBox 107 | ..data = data 108 | ..max = max; 109 | } 110 | } 111 | 112 | class RenderPercentBox extends RenderBox { 113 | int max; 114 | List data; 115 | final Paint bgPaint = Paint()..isAntiAlias = true; //背景画笔 116 | final TextPainter textPainter = 117 | TextPainter(textDirection: TextDirection.ltr); //文字画笔 118 | 119 | RenderPercentBox(this.max, this.data); 120 | 121 | @override 122 | void performLayout() { 123 | size = constraints.constrain(Size.infinite); //填满 124 | } 125 | 126 | @override 127 | void paint(PaintingContext context, Offset offset) { 128 | if (data.isNotEmpty) { 129 | Canvas canvas = context.canvas; 130 | canvas.save(); //保存当前canvas 131 | canvas.translate(offset.dx, offset.dy); //画布移动到当前的便宜位置 132 | double width = size.width / data.length; 133 | for (int i = 0; i < data.length; i++) { 134 | int value = data[i]; 135 | bgPaint..color = RandomColor.getColor(); 136 | double topHeight = size.height * (value / max); 137 | Rect topRect = Rect.fromLTWH(0, 0, width, topHeight); 138 | canvas.drawRect(topRect, bgPaint); 139 | textPainter 140 | ..text = TextSpan(text: value.toString()) //设置文字 141 | ..layout(maxWidth: width) //布局 142 | ..paint( 143 | canvas, 144 | Offset( 145 | topRect.center.dx - textPainter.width / 2, 146 | topRect.center.dy - textPainter.height / 2, 147 | ), 148 | ); //绘制 149 | Rect bottomRect = 150 | Rect.fromLTWH(0, topHeight, width, size.height - topHeight); 151 | bgPaint..color = RandomColor.getColor(); 152 | canvas.drawRect(bottomRect, bgPaint); 153 | textPainter 154 | ..text = TextSpan(text: (max - value).toString()) 155 | ..layout(maxWidth: width) 156 | ..paint( 157 | canvas, 158 | Offset( 159 | bottomRect.center.dx - textPainter.width / 2, 160 | bottomRect.center.dy - textPainter.height / 2, 161 | ), 162 | ); 163 | canvas.translate(width, 0); //移动画布 164 | } 165 | canvas.restore(); //恢复canvas 166 | } 167 | } 168 | } 169 | 170 | ///层叠box 171 | class StackBoxWidget extends LeafRenderObjectWidget { 172 | final int boxCount; 173 | 174 | const StackBoxWidget({this.boxCount = 7}) 175 | : assert(boxCount != null && boxCount > 0); 176 | 177 | @override 178 | RenderStackBox createRenderObject(BuildContext context) { 179 | return RenderStackBox(boxCount); 180 | } 181 | 182 | @override 183 | void updateRenderObject( 184 | BuildContext context, covariant RenderStackBox renderStackBox) { 185 | renderStackBox.boxCount = boxCount; 186 | } 187 | } 188 | 189 | class RenderStackBox extends RenderBox { 190 | int boxCount; 191 | final Paint boxPaint = Paint()..isAntiAlias = true; 192 | 193 | RenderStackBox(this.boxCount); 194 | 195 | @override 196 | void performLayout() { 197 | size = constraints.constrain(Size.infinite); //填满 198 | } 199 | 200 | @override 201 | void paint(PaintingContext context, Offset offset) { 202 | double width = size.width / ((boxCount + 1) / 2); 203 | double halfWidth = width / 2; 204 | Canvas canvas = context.canvas; 205 | canvas.save(); 206 | canvas.translate(offset.dx, offset.dy); 207 | double ltrLeft = 0; //左上角开始的方块的left 208 | double top = 0; 209 | double rtlLeft = size.width - width; //右上角开始的方块的left 210 | for (int i = 0; i < boxCount; i++) { 211 | boxPaint.color = RandomColor.getColor(); 212 | canvas.drawRect( 213 | Rect.fromLTWH(ltrLeft, top, width, width), boxPaint); //左上角开始的方块 214 | boxPaint.color = RandomColor.getColor(); 215 | canvas.drawRect( 216 | Rect.fromLTWH(rtlLeft, top, width, width), boxPaint); //右上角开始的方块 217 | ltrLeft += halfWidth; 218 | rtlLeft -= halfWidth; 219 | top += halfWidth; 220 | } 221 | canvas.restore(); 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /2 - 百格齐放 - 20201202/yifang_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:flutter/cupertino.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | void main() { 7 | runApp(MyApp()); 8 | } 9 | 10 | class MyApp extends StatelessWidget { 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'Flutter Demo', 15 | home: HomePage(), 16 | ); 17 | } 18 | } 19 | 20 | class HomePage extends StatefulWidget { 21 | @override 22 | HomePageState createState() => HomePageState(); 23 | } 24 | 25 | class HomePageState extends State { 26 | List upRatio = [2, 5, 8, 3, 6, 9, 1, 4, 7]; 27 | List> radioList = []; 28 | int belowCount = 7; 29 | 30 | @override 31 | void initState() { 32 | super.initState(); 33 | //初始化比例数组 34 | for (final int radio in upRatio) { 35 | radioList.add([radio, 10 - radio]); 36 | } 37 | } 38 | 39 | @override 40 | void dispose() { 41 | super.dispose(); 42 | } 43 | 44 | List _upList() { 45 | //支持总条数为 m,且每条比例条的区域数为 n 的比例条,并要求每个区域的比例均可以通过数组控制; 46 | 47 | final List tList = []; 48 | for (final List list in radioList) { 49 | tList.add( 50 | Expanded( 51 | child: Column( 52 | children: _item(list), 53 | ), 54 | ), 55 | ); 56 | } 57 | return tList; 58 | } 59 | 60 | List _item(List list) { 61 | final List itemList = []; 62 | for (int i = 0; i < list.length; i++) { 63 | itemList.add( 64 | Expanded( 65 | child: Container( 66 | color: RandomColor.getColor(), 67 | child: Align( 68 | alignment: Alignment.center, 69 | child: Text( 70 | list[i].toString(), 71 | ), 72 | ), 73 | ), 74 | flex: list[i], 75 | ), 76 | ); 77 | } 78 | return itemList; 79 | } 80 | 81 | @override 82 | Widget build(BuildContext context) { 83 | return Scaffold( 84 | body: Flex( 85 | direction: MediaQuery.of(context).orientation == Orientation.portrait ? Axis.vertical : Axis.horizontal, 86 | children: [ 87 | Expanded( 88 | child: Row( 89 | children: _upList(), 90 | ), 91 | ), 92 | Expanded( 93 | child: LayoutBuilder( 94 | builder: (BuildContext context, BoxConstraints constraints) { 95 | print('maxHeight: ${constraints.maxHeight}'); 96 | print('maxWidth: ${constraints.maxWidth}'); 97 | return CustomPaint( 98 | size: Size(constraints.maxWidth, constraints.maxHeight), 99 | painter: SPainter(belowCount), 100 | ); 101 | }, 102 | ), 103 | ), 104 | ], 105 | ), 106 | ); 107 | } 108 | } 109 | 110 | class RandomColor { 111 | const RandomColor._(); 112 | 113 | static final math.Random _random = math.Random(); 114 | 115 | static Color getColor() { 116 | return Color.fromARGB( 117 | 255, 118 | _random.nextInt(255), 119 | _random.nextInt(255), 120 | _random.nextInt(255), 121 | ); 122 | } 123 | } 124 | 125 | class SPainter extends CustomPainter { 126 | SPainter(this.count); 127 | 128 | int count; 129 | 130 | @override 131 | void paint(Canvas canvas, Size size) { 132 | final Paint paint = Paint() 133 | ..style = PaintingStyle.stroke 134 | ..color = Colors.black 135 | ..strokeWidth = 1.0 136 | ..isAntiAlias = true; 137 | 138 | final double h = size.height; 139 | final double w = size.width; 140 | final double divWidth = w / (count + 1); 141 | final double divHeight = h / (count + 1); 142 | // //辅助线 143 | // for(int i = 1; i <= count; i++){ 144 | // final double dy = divHeight * i; 145 | // canvas.drawLine(Offset(0, dy), Offset(w, dy), paint); 146 | // } 147 | // for(int i = 1; i <= count; i++){ 148 | // final double dx = divWidth * i; 149 | // canvas.drawLine(Offset(dx, 0), Offset(dx, h), paint); 150 | // } 151 | paint.style = PaintingStyle.fill; 152 | //从左上到右下 153 | for (int i = 1; i <= count; i++) { 154 | paint.color = RandomColor.getColor(); 155 | canvas.drawRect( 156 | Rect.fromCenter( 157 | center: Offset(divWidth * i, divHeight * i), 158 | width: divWidth * 2, 159 | height: divHeight * 2, 160 | ), 161 | paint, 162 | ); 163 | } 164 | //从右上到左下 165 | for (int i = 1; i <= count; i++) { 166 | paint.color = RandomColor.getColor(); 167 | canvas.drawRect( 168 | Rect.fromCenter( 169 | center: Offset(divWidth * (count + 1 - i), divHeight * i), 170 | width: divWidth * 2, 171 | height: divHeight * 2, 172 | ), 173 | paint, 174 | ); 175 | } 176 | } 177 | 178 | @override 179 | bool shouldRepaint(covariant CustomPainter oldDelegate) => true; 180 | } 181 | -------------------------------------------------------------------------------- /3 - 活动入口网格 - 20210404/README.md: -------------------------------------------------------------------------------- 1 | # 活动入口网格 - 2021/04/04 2 | 3 | ![](https://pic.alexv525.com/2021-04-04-095238.png) 4 | 5 | 实现一个类似「饿了么」和「美团」首页的活动入口网格布局,要求如下: 6 | * **不能使用 `GridView`**; 7 | * 支持 **左右滑动切换页面**; 8 | * 支持 **自定义横向和纵向的 item 数量**; 9 | * item 支持 **自定义图标和文字**; 10 | * item 支持 **自定义高度**,布局高度依据 item 高度确定; 11 | * item 支持 **点击事件**。 12 | 13 | 加分项: 14 | * 在底部实现一个页数指示器; 15 | * item 支持 **自定义各自的图标和文字的大小及样式**,同时保证布局高度自适应; 16 | * 布局可以根据不同页面的 item 数量,**在切换时平滑调整高度**; 17 | * item 基于类 (class) 组织内容,布局提供接口用于自定义 `itemBuilder`: `Widget Function(_ItemData item)`; 18 | * 开放实现自定义页数指示器的接口 `PreferredSizeWidget Function(PageController controller, int pageCount)? indicatorBuilder`。 19 | 20 | ![](https://pic.alexv525.com/2021-04-04-Kapture%202021-04-04%20at%2018.02.20.gif) 21 | -------------------------------------------------------------------------------- /3 - 活动入口网格 - 20210404/leo_activity_page.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui' as ui; 2 | import 'package:flutter/material.dart'; 3 | 4 | class Screens { 5 | const Screens._(); 6 | 7 | static MediaQueryData get mediaQuery => MediaQueryData.fromWindow(ui.window); 8 | 9 | static double get width => mediaQuery.size.width; 10 | 11 | static double get height => mediaQuery.size.height; 12 | } 13 | 14 | class ItemModel { 15 | ItemModel({ 16 | IconData icon, 17 | String name, 18 | }) { 19 | _icon = icon; 20 | _name = name; 21 | } 22 | 23 | IconData _icon; 24 | IconData get icon => _icon; 25 | String _name; 26 | String get name => _name; 27 | } 28 | 29 | class ActivityEnterPage extends StatefulWidget { 30 | const ActivityEnterPage({ 31 | Key key, 32 | @required this.items, 33 | this.rowCount = 4, 34 | this.columnCount = 2, 35 | this.itemHeight = 60, 36 | this.itemBuilder, 37 | this.itemCallback, 38 | }) : super(key: key); 39 | 40 | final List items; 41 | 42 | /// 横向item数量 默认为4 43 | final int rowCount; 44 | 45 | /// 纵向item数量 默认为2 46 | final int columnCount; 47 | 48 | /// 单个item的高度 49 | final double itemHeight; 50 | 51 | /// 自定义的item 52 | final Widget Function(ItemModel model) itemBuilder; 53 | 54 | /// 点击item的回调 55 | final Function(int index) itemCallback; 56 | 57 | @override 58 | ActivityEnterPageState createState() => ActivityEnterPageState(); 59 | } 60 | 61 | class ActivityEnterPageState extends State { 62 | /// 默认页数 63 | get pageCount => 64 | (widget.items.length / (widget.rowCount * widget.columnCount)).ceil(); 65 | 66 | /// 当前页码 67 | final ValueNotifier currentPage = ValueNotifier(0); 68 | 69 | /// 当前页码下实际的column数 70 | final ValueNotifier currentColumn = ValueNotifier(1); 71 | 72 | @override 73 | void initState() { 74 | super.initState(); 75 | if ((widget.items.length / widget.rowCount).ceil() > widget.columnCount) { 76 | currentColumn.value = widget.columnCount; 77 | } else { 78 | currentColumn.value = (widget.items.length / widget.rowCount).ceil(); 79 | } 80 | } 81 | 82 | /// 获取当前页上的items数据 83 | List _currentPageItems(int index) { 84 | final int _listIndex = widget.rowCount * widget.columnCount * index; 85 | final int _nextIndex = widget.rowCount * widget.columnCount * (index + 1); 86 | List _pageItems; 87 | if (widget.items.length > _nextIndex) { 88 | _pageItems = widget.items.sublist(_listIndex, _nextIndex); 89 | } else { 90 | _pageItems = widget.items.sublist(_listIndex, widget.items.length); 91 | } 92 | return _pageItems; 93 | } 94 | 95 | @override 96 | Widget build(BuildContext context) { 97 | return Scaffold( 98 | appBar: AppBar( 99 | title: Text('activity'), 100 | ), 101 | backgroundColor: Colors.blue, 102 | body: ValueListenableBuilder( 103 | valueListenable: currentColumn, 104 | builder: (_, int column, __) { 105 | return AnimatedContainer( 106 | color: Colors.white, 107 | duration: kThemeAnimationDuration, 108 | height: column * widget.itemHeight + (column * 10) + 20, 109 | child: Stack( 110 | children: [ 111 | PageView.builder( 112 | onPageChanged: (int index) { 113 | final List _items = _currentPageItems(index); 114 | currentColumn.value = 115 | (_items.length / widget.rowCount).ceil(); 116 | currentPage.value = index; 117 | }, 118 | itemCount: pageCount, 119 | itemBuilder: (BuildContext context, int index) { 120 | return _GirdViewItem( 121 | items: _currentPageItems(index), 122 | row: widget.rowCount, 123 | column: widget.columnCount, 124 | itemHeight: widget.itemHeight, 125 | tapCallback: (int tapIndex) { 126 | int _resultIndex = tapIndex; 127 | if (index > 0) { 128 | final int _listIndex = 129 | widget.rowCount * widget.columnCount * index; 130 | _resultIndex = _listIndex + _resultIndex; 131 | } 132 | widget.itemCallback?.call(_resultIndex); 133 | }, 134 | ); 135 | }, 136 | ), 137 | if (pageCount > 1) 138 | ValueListenableBuilder( 139 | valueListenable: currentPage, 140 | builder: (_, int page, __) { 141 | return Positioned( 142 | bottom: 10, 143 | left: 0, 144 | right: 0, 145 | child: Row( 146 | mainAxisAlignment: MainAxisAlignment.center, 147 | children: List.generate( 148 | pageCount, 149 | (index) { 150 | return Container( 151 | margin: EdgeInsets.symmetric(horizontal: 5), 152 | height: 10, 153 | width: 10, 154 | color: 155 | index == page ? Colors.blue : Colors.grey, 156 | ); 157 | }, 158 | ).toList(), 159 | ), 160 | ); 161 | }, 162 | ), 163 | ], 164 | ), 165 | ); 166 | }, 167 | ), 168 | ); 169 | } 170 | } 171 | 172 | class _GirdViewItem extends StatelessWidget { 173 | const _GirdViewItem({ 174 | Key key, 175 | @required this.items, 176 | @required this.row, 177 | @required this.column, 178 | @required this.itemHeight, 179 | @required this.tapCallback, 180 | this.itemBuilder, 181 | }) : super(key: key); 182 | 183 | final List items; 184 | final int row; 185 | final int column; 186 | final double itemHeight; 187 | final Function(int index) tapCallback; 188 | final Widget Function(ItemModel model) itemBuilder; 189 | final double _horizontalMargin = 30; 190 | final double _verticalMargin = 20; 191 | final double _spacing = 10; 192 | final double _runSpacing = 10; 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | final double _width = 197 | (Screens.width - _horizontalMargin - (row - 1) * _spacing) / row; 198 | return Container( 199 | padding: EdgeInsets.symmetric( 200 | horizontal: _horizontalMargin / 2, vertical: _verticalMargin / 2), 201 | child: Wrap( 202 | spacing: _spacing, 203 | runSpacing: _runSpacing, 204 | children: List.generate( 205 | items.length, 206 | (int index) { 207 | if (itemBuilder != null) 208 | return itemBuilder.call(items[index]); 209 | else 210 | return _WrapItem( 211 | index: index, 212 | itemWidth: _width, 213 | itemHeight: itemHeight, 214 | model: items[index], 215 | tapCallback: tapCallback, 216 | ); 217 | }, 218 | ), 219 | ), 220 | ); 221 | } 222 | } 223 | 224 | class _WrapItem extends StatelessWidget { 225 | const _WrapItem({ 226 | Key key, 227 | @required this.index, 228 | @required this.itemWidth, 229 | @required this.model, 230 | @required this.itemHeight, 231 | @required this.tapCallback, 232 | }) : super(key: key); 233 | 234 | final int index; 235 | final double itemHeight; 236 | final double itemWidth; 237 | final ItemModel model; 238 | final Function(int index) tapCallback; 239 | 240 | @override 241 | Widget build(BuildContext context) { 242 | return GestureDetector( 243 | behavior: HitTestBehavior.opaque, 244 | onTap: () { 245 | tapCallback(index); 246 | }, 247 | child: Container( 248 | width: itemWidth, 249 | height: itemHeight ?? 50, 250 | child: Column( 251 | children: [ 252 | Icon(model.icon), 253 | const Spacer(), 254 | Text( 255 | model.name, 256 | overflow: TextOverflow.ellipsis, 257 | ), 258 | ], 259 | ), 260 | ), 261 | ); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /3 - 活动入口网格 - 20210404/lycstar_main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | void main() { 6 | runApp(MyApp()); 7 | } 8 | 9 | class PageGridItem { 10 | final String label; 11 | final dynamic image; 12 | 13 | PageGridItem({this.label, this.image}); 14 | } 15 | 16 | class MyApp extends StatelessWidget { 17 | @override 18 | Widget build(BuildContext context) { 19 | return MaterialApp( 20 | title: 'Flutter Demo', 21 | theme: ThemeData( 22 | primarySwatch: Colors.blue, 23 | ), 24 | home: TestPage(title: 'Flutter Demo Home Page'), 25 | ); 26 | } 27 | } 28 | 29 | class TestPage extends StatelessWidget { 30 | final String title; 31 | 32 | const TestPage({this.title}); 33 | 34 | @override 35 | Widget build(BuildContext context) { 36 | return Scaffold( 37 | appBar: AppBar(title: Text(this.title)), 38 | body: Column( 39 | children: [ 40 | PageGridView( 41 | color: Colors.blue, 42 | mainAxisCount: 3, 43 | crossAxisCount: 5, 44 | children: [ 45 | PageGridItem(image: Icons.ac_unit, label: "label1"), 46 | PageGridItem(image: Icons.backup, label: "label2"), 47 | PageGridItem(image: Icons.calendar_today, label: "label3"), 48 | PageGridItem(image: Icons.delete_forever, label: "label4"), 49 | PageGridItem(image: Icons.ac_unit, label: "label1"), 50 | PageGridItem(image: Icons.backup, label: "label2"), 51 | PageGridItem(image: Icons.calendar_today, label: "label3"), 52 | PageGridItem(image: Icons.delete_forever, label: "label4"), 53 | PageGridItem(image: Icons.ac_unit, label: "label1"), 54 | PageGridItem(image: Icons.backup, label: "label2"), 55 | PageGridItem(image: Icons.calendar_today, label: "label3"), 56 | PageGridItem(image: Icons.delete_forever, label: "label4"), 57 | PageGridItem(image: Icons.ac_unit, label: "label1"), 58 | PageGridItem(image: Icons.backup, label: "label2"), 59 | PageGridItem(image: Icons.calendar_today, label: "label3"), 60 | PageGridItem(image: Icons.delete_forever, label: "label4"), 61 | PageGridItem(image: Icons.ac_unit, label: "label1"), 62 | PageGridItem(image: Icons.backup, label: "label2"), 63 | PageGridItem(image: Icons.calendar_today, label: "label3"), 64 | PageGridItem(image: Icons.delete_forever, label: "label4"), 65 | PageGridItem(image: Icons.ac_unit, label: "label1"), 66 | PageGridItem(image: Icons.backup, label: "label2"), 67 | PageGridItem(image: Icons.calendar_today, label: "label3"), 68 | PageGridItem(image: Icons.delete_forever, label: "label4"), 69 | PageGridItem(image: Icons.ac_unit, label: "label1"), 70 | PageGridItem(image: Icons.backup, label: "label2"), 71 | PageGridItem(image: Icons.calendar_today, label: "label3"), 72 | PageGridItem(image: Icons.delete_forever, label: "label4"), 73 | PageGridItem(image: Icons.ac_unit, label: "label1"), 74 | PageGridItem(image: Icons.backup, label: "label2"), 75 | PageGridItem(image: Icons.calendar_today, label: "label3"), 76 | PageGridItem(image: Icons.delete_forever, label: "label4"), 77 | ], 78 | itemBuilder: (PageGridItem pageGridItem) { 79 | return GestureDetector( 80 | behavior: HitTestBehavior.opaque, 81 | onTap: () { 82 | print(pageGridItem.label); 83 | }, 84 | child: Column( 85 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 86 | children: [ 87 | Icon(pageGridItem.image), 88 | Text(pageGridItem.label), 89 | ], 90 | ), 91 | ); 92 | }, 93 | ), 94 | ], 95 | ), 96 | ); 97 | } 98 | } 99 | 100 | class PageGridView extends StatefulWidget { 101 | final Color color; 102 | 103 | final List children; 104 | 105 | final int mainAxisCount; 106 | 107 | final int crossAxisCount; 108 | 109 | final double itemHeight; 110 | 111 | final Widget Function(PageGridItem item) itemBuilder; 112 | 113 | final Widget Function(double page, int pageCount) indicatorBuilder; 114 | 115 | const PageGridView({ 116 | Key key, 117 | this.color = Colors.transparent, 118 | this.children, 119 | this.mainAxisCount = 2, 120 | this.crossAxisCount = 4, 121 | this.itemHeight = 80.0, 122 | this.itemBuilder, 123 | this.indicatorBuilder, 124 | }) : assert(children != null), 125 | assert(itemBuilder != null), 126 | super(key: key); 127 | 128 | @override 129 | _PageGridViewState createState() => _PageGridViewState(); 130 | } 131 | 132 | class _PageGridViewState extends State { 133 | final ValueNotifier _page = ValueNotifier(0); 134 | 135 | int get _pageCount => (widget.children.length / _pageItemCount).ceil(); 136 | 137 | int get _pageItemCount => widget.mainAxisCount * widget.crossAxisCount; 138 | 139 | int get _totalItemCount => widget.children.length; 140 | 141 | double get _pageHeight => widget.mainAxisCount * widget.itemHeight; 142 | 143 | bool get _hasAlonePage => (_totalItemCount % _pageItemCount) > 0; 144 | 145 | double get _endPageHeight => 146 | ((_totalItemCount % _pageItemCount) / widget.crossAxisCount).ceil() * 147 | widget.itemHeight; 148 | 149 | PageController _pageController = PageController(); 150 | 151 | @override 152 | void initState() { 153 | super.initState(); 154 | _pageController.addListener(() { 155 | _page.value = _pageController.page; 156 | }); 157 | } 158 | 159 | List _getCurPageItems(int index) { 160 | return widget.children.sublist( 161 | index * _pageItemCount, 162 | (index + 1) * _pageItemCount > _totalItemCount 163 | ? _totalItemCount 164 | : (index + 1) * _pageItemCount, 165 | ); 166 | } 167 | 168 | Widget buildDefaultIndicator(double page, int pageCount) { 169 | return Row( 170 | mainAxisAlignment: MainAxisAlignment.center, 171 | children: List.generate(pageCount, (index) { 172 | Color color; 173 | if (pageCount == 1) { 174 | color = Colors.red; 175 | } else if (page.floor() == index) { 176 | color = Color.lerp( 177 | Colors.red, 178 | Colors.white, 179 | page - page.floor(), 180 | ); 181 | } else if (page.floor() == index - 1) { 182 | color = Color.lerp( 183 | Colors.white, 184 | Colors.red, 185 | page - page.floor(), 186 | ); 187 | } else 188 | color = Colors.white; 189 | return Container( 190 | margin: EdgeInsets.symmetric(vertical: 15.0, horizontal: 6.0), 191 | decoration: BoxDecoration( 192 | shape: BoxShape.circle, 193 | color: color, 194 | ), 195 | width: 12.0, 196 | height: 12.0, 197 | ); 198 | }), 199 | ); 200 | } 201 | 202 | @override 203 | Widget build(BuildContext context) { 204 | return ValueListenableBuilder( 205 | valueListenable: _page, 206 | builder: (_, double page, Widget child) { 207 | return Container( 208 | color: widget.color, 209 | child: Column( 210 | mainAxisSize: MainAxisSize.min, 211 | children: [ 212 | Container( 213 | constraints: BoxConstraints.tightFor( 214 | height: _hasAlonePage 215 | ? page.ceil() == _pageCount - 1 216 | ? lerpDouble( 217 | _pageHeight, 218 | _endPageHeight, 219 | page - page.floor() == 0 220 | ? 1 221 | : page - page.floor(), 222 | ) 223 | : _pageHeight 224 | : _pageHeight, 225 | ), 226 | child: child, 227 | ), 228 | widget.indicatorBuilder == null 229 | ? buildDefaultIndicator(page, _pageCount) 230 | : widget.indicatorBuilder(page, _pageCount) 231 | ], 232 | ), 233 | ); 234 | }, 235 | child: LayoutBuilder( 236 | builder: (context, constraints) { 237 | return PageView.builder( 238 | controller: _pageController, 239 | itemCount: _pageCount, 240 | itemBuilder: (context, index) { 241 | var list = _getCurPageItems(index); 242 | return Wrap( 243 | children: list 244 | .map( 245 | (item) => SizedBox( 246 | width: constraints.maxWidth / widget.crossAxisCount, 247 | height: widget.itemHeight, 248 | child: widget.itemBuilder(item), 249 | ), 250 | ) 251 | .toList(), 252 | ); 253 | }, 254 | ); 255 | }, 256 | ), 257 | ); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /3 - 活动入口网格 - 20210404/main_sun.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ffi'; 2 | 3 | import 'package:flutter/material.dart'; 4 | 5 | void main() { 6 | runApp(MyApp()); 7 | } 8 | 9 | class MyApp extends StatelessWidget { 10 | // This widget is the root of your application. 11 | @override 12 | Widget build(BuildContext context) { 13 | return MaterialApp( 14 | title: 'Flutter Demo', 15 | theme: ThemeData( 16 | // This is the theme of your application. 17 | // 18 | // Try running your application with "flutter run". You'll see the 19 | // application has a blue toolbar. Then, without quitting the app, try 20 | // changing the primarySwatch below to Colors.green and then invoke 21 | // "hot reload" (press "r" in the console where you ran "flutter run", 22 | // or simply save your changes to "hot reload" in a Flutter IDE). 23 | // Notice that the counter didn't reset back to zero; the application 24 | // is not restarted. 25 | primarySwatch: Colors.blue, 26 | // This makes the visual density adapt to the platform that you run 27 | // the app on. For desktop platforms, the controls will be smaller and 28 | // closer together (more dense) than on mobile platforms. 29 | visualDensity: VisualDensity.adaptivePlatformDensity, 30 | ), 31 | home: MyHomePage(title: 'Flutter Demo Home Page'), 32 | ); 33 | } 34 | } 35 | 36 | class MyHomePage extends StatefulWidget { 37 | MyHomePage({Key key, this.title}) : super(key: key); 38 | 39 | // This widget is the home page of your application. It is stateful, meaning 40 | // that it has a State object (defined below) that contains fields that affect 41 | // how it looks. 42 | 43 | // This class is the configuration for the state. It holds the values (in this 44 | // case the title) provided by the parent (in this case the App widget) and 45 | // used by the build method of the State. Fields in a Widget subclass are 46 | // always marked "final". 47 | 48 | final String title; 49 | 50 | @override 51 | _MyHomePageState createState() => _MyHomePageState(); 52 | } 53 | 54 | class _MyHomePageState extends State { 55 | int rows = 2; 56 | int cols = 6; 57 | List array = [ 58 | 'A', 59 | 'B', 60 | 'C', 61 | 'D', 62 | 'E', 63 | 'F', 64 | 'G', 65 | 'H', 66 | 'I', 67 | 'J', 68 | 'K', 69 | 'L', 70 | 'M', 71 | 'N', 72 | 'O', 73 | 'P', 74 | 'Q', 75 | 'R', 76 | 'S', 77 | 'T', 78 | 'U', 79 | 'V', 80 | 'W', 81 | 'X', 82 | 'Y', 83 | 'Z', 84 | ]; 85 | 86 | @override 87 | Widget build(BuildContext context) { 88 | return Scaffold( 89 | appBar: AppBar( 90 | title: Text(widget.title), 91 | ), 92 | body: ColoredBox( 93 | color: Colors.blueGrey, 94 | child: _pageView(context), 95 | ), 96 | // This trailing comma makes auto-formatting nicer for build methods. 97 | ); 98 | } 99 | 100 | Widget _pageView(BuildContext context) { 101 | int count = array.length % (rows * cols) == 0 102 | ? array.length ~/ (rows * cols) 103 | : array.length ~/ (rows * cols) + 1; 104 | List> subArrays = List>(); 105 | for (int i = 0; i < count; i++) { 106 | int start = i * rows * cols; 107 | int end = (i + 1) * rows * cols - 1; 108 | if (end > array.length) { 109 | end = array.length - 1; 110 | } 111 | subArrays.add(array.sublist(start, end + 1)); 112 | } 113 | 114 | return PageView( 115 | scrollDirection: Axis.horizontal, 116 | reverse: false, 117 | controller: PageController( 118 | initialPage: 0, 119 | viewportFraction: 1, 120 | keepPage: true, 121 | ), 122 | physics: BouncingScrollPhysics(), 123 | pageSnapping: true, 124 | onPageChanged: (index) { 125 | //监听事件 126 | print('index=====$index'); 127 | }, 128 | children: subArrays 129 | .map( 130 | (e) => Wrap( 131 | children: e.map((s) => _gridItem(context, title: s)).toList(), 132 | ), 133 | ) 134 | .toList(), 135 | ); 136 | } 137 | 138 | Widget _gridItem(BuildContext context, 139 | {double width, Image icon, String title, Double height}) { 140 | double screenW = MediaQuery.of(context).size.width; 141 | 142 | return GestureDetector( 143 | onTap: () { 144 | print(title); 145 | }, 146 | child: Container( 147 | color: Colors.orange, 148 | width: screenW / cols, 149 | height: height ?? (screenW / cols), 150 | child: Column( 151 | children: [ 152 | Icon(Icons.android), 153 | Text(title), 154 | ], 155 | ), 156 | ), 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /3 - 活动入口网格 - 20210404/zengqiang_main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() => runApp(MyApp()); 4 | 5 | class MyApp extends StatelessWidget { 6 | // This widget is the root of your application. 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | debugShowCheckedModeBanner: false, 11 | title: '活动网格', 12 | theme: ThemeData( 13 | // This is the theme of your application. 14 | // 15 | // Try running your application with "flutter run". You'll see the 16 | // application has a blue toolbar. Then, without quitting the app, try 17 | // changing the primarySwatch below to Colors.green and then invoke 18 | // "hot reload" (press "r" in the console where you ran "flutter run", 19 | // or simply save your changes to "hot reload" in a Flutter IDE). 20 | // Notice that the counter didn't reset back to zero; the acpplication 21 | // is not restarted. 22 | primaryColor: Colors.red, 23 | primaryColorBrightness: Brightness.dark, 24 | primaryColorDark: Colors.grey, 25 | primaryColorLight: Colors.white, 26 | appBarTheme: AppBarTheme( 27 | color: Colors.blue, 28 | // centerTitle: false, 29 | // shadowColor: Colors.blue, 30 | brightness: Brightness.dark), 31 | ), 32 | home: HomePage(), 33 | ); 34 | } 35 | } 36 | 37 | //实体 38 | class ItemEntity { 39 | String name; 40 | IconData icon; 41 | 42 | ItemEntity(this.name, this.icon); 43 | } 44 | 45 | class HomePage extends StatefulWidget { 46 | @override 47 | State createState() { 48 | return GridItemPageState(); 49 | } 50 | } 51 | 52 | class GridItemPageState extends State { 53 | var data = []; 54 | 55 | //页面高度 56 | var _pageHeight; 57 | 58 | //页面数 59 | var pages = -1; 60 | 61 | //每行item数量 62 | var rowCount = 5; 63 | 64 | //行数 65 | var columnCount = 2; 66 | 67 | //item size 68 | var _itemSize; 69 | 70 | var isFirst = true; 71 | 72 | @override 73 | void initState() { 74 | super.initState(); 75 | for (int i = 0; i < 13; i++) { 76 | data.add(ItemEntity('测试:${i + 1}', Icons.android)); 77 | } 78 | //计算页数 79 | pages = data.length ~/ (rowCount * columnCount) + 80 | (data.length % (rowCount * columnCount) == 0 ? 0 : 1); 81 | } 82 | 83 | List _generateData() { 84 | //组件list 85 | var list = []; 86 | 87 | //生成每页数据 88 | for (int i = 0; i < pages; i++) { 89 | list.add( 90 | //每页的父布局 91 | Container( 92 | color: Colors.white, 93 | width: double.infinity, 94 | alignment: Alignment.topLeft, 95 | child: Wrap( 96 | direction: Axis.horizontal, 97 | // crossAxisAlignment: WrapCrossAlignment.center, 98 | children: data 99 | .sublist( 100 | rowCount * columnCount * i, 101 | (i + 1) * rowCount * columnCount > data.length 102 | ? data.length 103 | : (i + 1) * rowCount * columnCount) 104 | .map((e) => GestureDetector( 105 | onTap: () { 106 | print('点击${e.name}'); 107 | }, 108 | child: Container( 109 | width: _itemSize, 110 | height: _itemSize, 111 | child: Column( 112 | mainAxisAlignment: MainAxisAlignment.center, 113 | crossAxisAlignment: CrossAxisAlignment.center, 114 | children: [Icon(e.icon), Text(e.name)], 115 | )), 116 | )) 117 | .toList(), 118 | ), 119 | )); 120 | } 121 | return list; 122 | } 123 | 124 | @override 125 | Widget build(BuildContext context) { 126 | _itemSize = MediaQuery.of(context).size.width / rowCount; 127 | if (isFirst) { 128 | isFirst = false; 129 | if (pages > 1) { 130 | _pageHeight = columnCount * _itemSize; 131 | } else { 132 | //不够一页的item数量 133 | var moreItemCount = data.length % (rowCount * columnCount); 134 | var lineCount = 135 | moreItemCount ~/ rowCount + moreItemCount % rowCount == 0 ? 0 : 1; 136 | _pageHeight = lineCount * _itemSize; 137 | } 138 | } 139 | 140 | return Scaffold( 141 | backgroundColor: Colors.green, 142 | appBar: AppBar( 143 | title: Text('自定义Grid分页Item'), 144 | ), 145 | body: Container( 146 | width: double.infinity, 147 | height: _pageHeight, 148 | child: PageView( 149 | scrollDirection: Axis.horizontal, 150 | pageSnapping: true, 151 | onPageChanged: (index) { 152 | setState(() { 153 | if ((pages - 1) == index) { 154 | //不够一页的item数量 155 | var moreItemCount = data.length % (rowCount * columnCount); 156 | var lineCount = 157 | moreItemCount ~/ rowCount + moreItemCount % rowCount == 0 158 | ? 0 159 | : 1; 160 | _pageHeight = lineCount * _itemSize; 161 | print("if:$_pageHeight"); 162 | } else { 163 | _pageHeight = columnCount * _itemSize; 164 | } 165 | }); 166 | }, 167 | children: _generateData(), 168 | ), 169 | ), 170 | ); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @AlexV525 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter Learning Test 2 | 3 | ## 介绍 4 | 学习 Flutter 之路上的点点滴滴及课堂小测~ 5 | 6 | ## 题目索引 7 | 8 | 1. 2020-11-27 [《注册表单的实现》](1%20-%20注册表单的实现%20-%2020201127) 9 | 2. 2020-12-02 [《百格齐放》](2%20-%20百格齐放%20-%2020201202) 10 | 2. 2021-04-04 [《活动入口网格》](3%20-%20活动入口网格%20-%2020210404) 11 | 12 | ## 提交说明 13 | 14 | * 提交一个完整可运行的 `main.dart`。 15 | * 文件以 `人名英文缩写或英文名_main.dart` 放置。 16 | * 可以在 [DartPad](https://dartpad.cn/) 进行实践,无需安装本地环境。 17 | 18 | ## 参考资料 19 | 20 | * Flutter 中文社区:https://flutter.cn/ 21 | * Flutter 中文网:https://book.flutterchina.com/ 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_learning_test 2 | description: A Flutter project that contains multiple learning tests. 3 | version: 1.0.0+1 4 | 5 | environment: 6 | sdk: '>=2.10.0 <2.12.0' 7 | flutter: '>=1.22.0' 8 | 9 | dependencies: 10 | flutter: 11 | sdk: flutter 12 | 13 | flutter: 14 | uses-material-design: true 15 | --------------------------------------------------------------------------------