App 高级感营造之 局部动画
效果
源代码
import 'package:flutter/material.dart';
class LoginPage extends StatefulWidget {
const LoginPage({super.key});
@override
State<StatefulWidget> createState() {
return LoginPageState();
}
}
class LoginPageState extends State<LoginPage> with TickerProviderStateMixin {
late AnimationController _lightController;
late Animation<double> _lightAnimation;
late AnimationController _btnController;
late Animation<double> _btnAnimation;
@override
void initState() {
super.initState();
_lightController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 2000));
_lightAnimation = Tween<double>(begin: -100, end: 0)
.animate(_lightController)
..addStatusListener((status) {});
_btnController = AnimationController(
vsync: this, duration: const Duration(milliseconds: 2000));
_btnAnimation = Tween<double>(begin: 0, end: 1).animate(_lightController)
..addStatusListener((status) {});
_lightController.forward();
_btnController.forward();
}
List<Widget> _lights() {
return [
AnimatedBuilder(
animation: _lightController,
builder: (BuildContext context, Widget? child) {
return Positioned(
top: _lightAnimation.value + 40,
right: 40,
width: 70,
child: Image.asset('assets/images/clock.png'),
);
},
),
AnimatedBuilder(
animation: _lightController,
builder: (BuildContext context, Widget? child) {
return Positioned(
top: _lightAnimation.value,
left: 20,
child: Image.asset('assets/images/light-1.png', width: 90));
},
),
AnimatedBuilder(
animation: _lightController,
builder: (BuildContext context, Widget? child) {
return Positioned(
top: _lightAnimation.value,
left: 140,
child: Image.asset('assets/images/light-2.png', width: 50));
},
)
];
}
List<Widget> _input() {
return [
// 用户名
const UserNameInputField(),
// 分隔线
Container(
color: Colors.grey[200],
height: 1,
margin: const EdgeInsets.symmetric(horizontal: 30)),
// 密码
const PasswordInputField()
];
}
List<Widget> _btn() {
return [
AnimatedBuilder(
animation: _btnController,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _btnAnimation.value,
child: Container(
width: double.infinity,
height: 50,
margin: const EdgeInsets.symmetric(vertical: 30, horizontal: 30),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: Colors.deepPurple[200],
),
child: const Center(
child: Text(
'登录',
style: TextStyle(color: Colors.white, fontSize: 18),
)),
),
);
},
),
AnimatedBuilder(
animation: _btnController,
builder: (BuildContext context, Widget? child) {
return Opacity(
opacity: _btnAnimation.value,
child: Text(
'忘记密码',
style: TextStyle(color: Colors.grey[500]),
),
);
},
),
];
}
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: true,
body: SingleChildScrollView(
child: Stack(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset('assets/images/background.png'),
const SizedBox(height: 20),
..._input(),
..._btn(),
],
),
..._lights()
],
),
),
);
}
@override
bool get wantKeepAlive => true;
}
class UserNameInputField extends StatefulWidget {
const UserNameInputField({super.key});
@override
UserNameInputFieldState createState() => UserNameInputFieldState();
}
class UserNameInputFieldState extends State<UserNameInputField> {
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(.1),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(5, 7))
],
),
child: TextFormField(
decoration: InputDecoration(
hintText: '用户名',
hintStyle: const TextStyle(color: Colors.grey),
border: const OutlineInputBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomRight: Radius.circular(0),
bottomLeft: Radius.circular(0)),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[300],
),
),
);
}
}
class PasswordInputField extends StatefulWidget {
const PasswordInputField({super.key});
@override
PasswordInputFieldState createState() => PasswordInputFieldState();
}
class PasswordInputFieldState extends State<PasswordInputField> {
bool _obscureText = true;
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 30),
decoration: BoxDecoration(boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(.1),
spreadRadius: 1,
blurRadius: 2,
offset: const Offset(5, 7))
]),
child: TextFormField(
obscureText: _obscureText,
decoration: InputDecoration(
hintText: '密码',
hintStyle: const TextStyle(color: Colors.grey),
suffixIcon: IconButton(
icon: Icon(_obscureText ? Icons.visibility : Icons.visibility_off),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
border: const OutlineInputBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomRight: Radius.circular(10),
bottomLeft: Radius.circular(10)),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.grey[300],
),
),
);
}
}
实现原理
登录页面上的部分元素施加动态效果
- 给 需要动态效果的组件包裹 AnimatedBuilder
- AnimatedBuilder内部使用 animation.value
- 适当的时机调用 animationController.forward启动动画
转载自:https://juejin.cn/post/7242198873865781305