App高级感营造之 3D按钮

229 阅读1分钟

效果图

GIF 2023-5-31 9-41-55.gif

源代码

以下代码基于flutter 3.3.3 版本,不同之间版本稍有差别

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Colors.grey[300],
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
            child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: const [
              Pretty3DButton(
                text: "Press Me!",
                height: 300,
                width: 300,
                blurRadius: 5,
                spreadRadius: 1,
                offset: 4,
              ),
            ])));
  }
}

class Pretty3DButton extends StatefulWidget {
  final String text;
  final double height;
  final double width;
  final double blurRadius;
  final double offset;
  final double spreadRadius;

  const Pretty3DButton(
      {super.key,
      required this.text,
      required this.height,
      required this.width,
      required this.blurRadius,
      required this.offset,
      required this.spreadRadius});

  @override
  State<StatefulWidget> createState() {
    return Pretty3DButtonState();
  }
}

class Pretty3DButtonState extends State<Pretty3DButton> {
  bool _isElevated = true;
  final animationDuration = const Duration(milliseconds: 80);

  @override
  void initState() {
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
        onTapDown: (TapDownDetails details) async {
          setState(() {
            _isElevated = false;
          });
        },
        onTapUp: (TapUpDetails details) async {
          setState(() {
            _isElevated = true;
          });
        },
        child: AnimatedContainer(
          duration: animationDuration,
          height: widget.height,
          width: widget.width,
          decoration: BoxDecoration(
              color: Colors.blue[300],
              borderRadius: BorderRadius.circular(10),
              boxShadow: _isElevated
                  ? [
                      BoxShadow(
                        color: Colors.blue[500]!,
                        offset: Offset(widget.offset, widget.offset), // 偏移量
                        blurRadius: widget.blurRadius, // 模糊半径
                        spreadRadius: widget.spreadRadius, // 扩散半径
                      ),
                      BoxShadow(
                        color: Colors.white,
                        offset: Offset(-widget.offset, -widget.offset),
                        blurRadius: widget.blurRadius,
                        spreadRadius: widget.spreadRadius,
                      )
                    ]
                  : null),
          child: Center(
              child: Text(
            widget.text,
            style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: _isElevated ? Colors.white : Colors.grey[300]),
          )),
        ));
  }
}

实现原理

  • 定义全局状态码 _isElevated 区分按下与弹起的状态

  • 利用 Container自带的Decoration中 boxShadow 属性,先实现 多级阴影,做出立体效果

  • 将Container替换成 AnimatedContainer ,支持状态之间通过一定的动画时长去切换

  • GestureDetector 响应按下和弹起事件,分别设置 _isElevated 状态值为 true 和 false并刷新 组件状态

支持的属性

属性名类型说明
textString内部文案
heightdouble组件高度
widthdouble组件宽度
blurRadiusdouble阴影模糊半径
spreadRadiusdouble阴影扩散半径
offsetdouble阴影相对于Container自身的偏移量