likes
comments
collection
share

App 高级感营造之 局部动画

作者站长头像
站长
· 阅读数 48

效果

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],
        ),
      ),
    );
  }
}

实现原理

登录页面上的部分元素施加动态效果

  1. 给 需要动态效果的组件包裹 AnimatedBuilder
  2. AnimatedBuilder内部使用 animation.value
  3. 适当的时机调用 animationController.forward启动动画
转载自:https://juejin.cn/post/7242198873865781305
评论
请登录