Flutter与OpenHarmony打卡步进器组件

前言

步进器是打卡工具类应用中用于数值调整的常用组件。在设置打卡目标次数、每日饮水杯数、运动时长等场景中,步进器提供了直观的数值增减操作。本文将详细介绍如何在Flutter和OpenHarmony平台上实现功能完善的步进器组件。

步进器的设计需要考虑数值范围、步进值、边界处理和视觉反馈。我们将实现支持自定义范围和步进值的步进器组件,同时提供良好的交互体验。

Flutter步进器实现

首先创建基础步进器组件:

dart 复制代码
class Stepper extends StatelessWidget {
  final int value;
  final int min;
  final int max;
  final int step;
  final ValueChanged<int> onChanged;
  final String? unit;

  const Stepper({
    Key? key,
    required this.value,
    this.min = 0,
    this.max = 100,
    this.step = 1,
    required this.onChanged,
    this.unit,
  }) : super(key: key);

  void _increment() {
    if (value + step <= max) {
      onChanged(value + step);
    }
  }

  void _decrement() {
    if (value - step >= min) {
      onChanged(value - step);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        _buildButton(Icons.remove, _decrement, value <= min),
        Container(
          width: 80,
          alignment: Alignment.center,
          child: Text(
            unit != null ? '$value$unit' : '$value',
            style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
        ),
        _buildButton(Icons.add, _increment, value >= max),
      ],
    );
  }

  Widget _buildButton(IconData icon, VoidCallback onPressed, bool disabled) {
    return GestureDetector(
      onTap: disabled ? null : onPressed,
      child: Container(
        width: 40,
        height: 40,
        decoration: BoxDecoration(
          color: disabled ? Colors.grey.shade200 : Colors.blue,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Icon(
          icon,
          color: disabled ? Colors.grey : Colors.white,
        ),
      ),
    );
  }
}

Stepper组件提供了完整的数值调整功能。min和max定义数值范围,step定义每次调整的步进值,unit可选地显示单位。按钮在达到边界时自动禁用,通过颜色变化提供视觉反馈。数值显示在中间,使用大号粗体字确保清晰可读。

实现带动画效果的步进器:

dart 复制代码
class AnimatedStepper extends StatefulWidget {
  final int value;
  final int min;
  final int max;
  final ValueChanged<int> onChanged;

  const AnimatedStepper({
    Key? key,
    required this.value,
    this.min = 0,
    this.max = 100,
    required this.onChanged,
  }) : super(key: key);

  @override
  State<AnimatedStepper> createState() => _AnimatedStepperState();
}

class _AnimatedStepperState extends State<AnimatedStepper>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _scaleAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(milliseconds: 100),
      vsync: this,
    );
    _scaleAnimation = Tween<double>(begin: 1.0, end: 1.2).animate(_controller);
  }

  void _animateValue() {
    _controller.forward().then((_) => _controller.reverse());
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        _buildButton(Icons.remove, () {
          if (widget.value > widget.min) {
            widget.onChanged(widget.value - 1);
            _animateValue();
          }
        }),
        AnimatedBuilder(
          animation: _scaleAnimation,
          builder: (context, child) {
            return Transform.scale(
              scale: _scaleAnimation.value,
              child: Container(
                width: 80,
                alignment: Alignment.center,
                child: Text(
                  '${widget.value}',
                  style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
                ),
              ),
            );
          },
        ),
        _buildButton(Icons.add, () {
          if (widget.value < widget.max) {
            widget.onChanged(widget.value + 1);
            _animateValue();
          }
        }),
      ],
    );
  }

  Widget _buildButton(IconData icon, VoidCallback onPressed) {
    return IconButton(
      icon: Icon(icon),
      onPressed: onPressed,
      iconSize: 28,
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

动画步进器在数值变化时添加缩放动画效果,让交互更加生动。AnimationController控制动画,forward和reverse实现放大后缩小的弹性效果。这种微妙的动画反馈能够增强用户的操作确认感。

OpenHarmony步进器实现

在鸿蒙系统中创建步进器组件:

typescript 复制代码
@Component
struct NumberStepper {
  @Prop value: number = 0
  @Prop min: number = 0
  @Prop max: number = 100
  @Prop step: number = 1
  @Prop unit: string = ''
  private onChanged: (value: number) => void = () => {}
  @State scale: number = 1

  increment() {
    if (this.value + this.step <= this.max) {
      this.onChanged(this.value + this.step)
      this.animateValue()
    }
  }

  decrement() {
    if (this.value - this.step >= this.min) {
      this.onChanged(this.value - this.step)
      this.animateValue()
    }
  }

  animateValue() {
    animateTo({ duration: 50 }, () => { this.scale = 1.2 })
    setTimeout(() => {
      animateTo({ duration: 50 }, () => { this.scale = 1 })
    }, 50)
  }

  build() {
    Row() {
      this.StepButton('-', () => this.decrement(), this.value <= this.min)
      
      Text(this.unit ? `${this.value}${this.unit}` : `${this.value}`)
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .width(80)
        .textAlign(TextAlign.Center)
        .scale({ x: this.scale, y: this.scale })
      
      this.StepButton('+', () => this.increment(), this.value >= this.max)
    }
  }

  @Builder
  StepButton(text: string, onClick: () => void, disabled: boolean) {
    Text(text)
      .width(40)
      .height(40)
      .fontSize(24)
      .fontWeight(FontWeight.Bold)
      .textAlign(TextAlign.Center)
      .fontColor(disabled ? '#999999' : Color.White)
      .backgroundColor(disabled ? '#E0E0E0' : '#007AFF')
      .borderRadius(8)
      .onClick(() => {
        if (!disabled) onClick()
      })
  }
}

鸿蒙的步进器使用相同的逻辑结构。animateTo函数实现数值变化时的缩放动画,setTimeout控制动画的时序。disabled状态通过颜色变化提供视觉反馈,禁用时按钮变灰且不响应点击。

目标设置步进器

实现打卡目标设置的步进器:

dart 复制代码
class GoalStepper extends StatelessWidget {
  final String label;
  final int value;
  final int min;
  final int max;
  final String unit;
  final ValueChanged<int> onChanged;
  final List<int>? quickValues;

  const GoalStepper({
    Key? key,
    required this.label,
    required this.value,
    this.min = 1,
    this.max = 100,
    required this.unit,
    required this.onChanged,
    this.quickValues,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(label, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
        const SizedBox(height: 12),
        Row(
          children: [
            Expanded(
              child: Stepper(
                value: value,
                min: min,
                max: max,
                unit: unit,
                onChanged: onChanged,
              ),
            ),
          ],
        ),
        if (quickValues != null) ...[
          const SizedBox(height: 12),
          Wrap(
            spacing: 8,
            children: quickValues!.map((v) => ChoiceChip(
              label: Text('$v$unit'),
              selected: value == v,
              onSelected: (_) => onChanged(v),
            )).toList(),
          ),
        ],
      ],
    );
  }
}

GoalStepper是打卡目标设置的专用组件,包含标签、步进器和快捷选择。quickValues提供常用数值的快捷按钮,用户可以直接点击选择而无需多次点击加减按钮。这种设计大大提升了设置效率。

使用示例:

dart 复制代码
GoalStepper(
  label: '每日饮水目标',
  value: waterGoal,
  min: 1,
  max: 20,
  unit: '杯',
  quickValues: [4, 6, 8, 10],
  onChanged: (value) => setState(() => waterGoal = value),
)

时长步进器

实现时长设置的步进器:

dart 复制代码
class DurationStepper extends StatelessWidget {
  final int minutes;
  final ValueChanged<int> onChanged;

  const DurationStepper({
    Key? key,
    required this.minutes,
    required this.onChanged,
  }) : super(key: key);

  String get formattedDuration {
    if (minutes < 60) return '$minutes分钟';
    final hours = minutes ~/ 60;
    final mins = minutes % 60;
    return mins > 0 ? '$hours小时$mins分钟' : '$hours小时';
  }

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        IconButton(
          icon: const Icon(Icons.remove_circle_outline),
          onPressed: minutes > 5 ? () => onChanged(minutes - 5) : null,
        ),
        Container(
          width: 120,
          alignment: Alignment.center,
          child: Text(
            formattedDuration,
            style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
          ),
        ),
        IconButton(
          icon: const Icon(Icons.add_circle_outline),
          onPressed: minutes < 480 ? () => onChanged(minutes + 5) : null,
        ),
      ],
    );
  }
}

时长步进器专门用于设置时间长度,以5分钟为步进单位。formattedDuration方法将分钟数格式化为易读的时长文本,超过60分钟时显示小时和分钟的组合。最大值480分钟(8小时)覆盖了大多数习惯的时长需求。

总结

本文详细介绍了在Flutter和OpenHarmony平台上实现步进器组件的完整方案。步进器通过直观的加减按钮和清晰的数值显示,为用户提供了便捷的数值调整体验。动画效果增强了交互反馈,快捷选择提升了设置效率。两个平台的实现都注重边界处理和视觉反馈,确保步进器操作准确可靠。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

相关推荐
莓有烦恼吖2 小时前
基于AI图像识别与智能推荐的校园食堂评价系统研究 05-审核机制模块
java·服务器·python
开开心心就好2 小时前
OCR识别工具可加AI接口,快捷键截图翻译便捷
java·网络·windows·随机森林·电脑·excel·推荐算法
小高0072 小时前
🔥3 kB 换 120 ms 阻塞? Axios 还是 fetch?
前端·javascript·面试
爬山算法2 小时前
Hibernate(15)Hibernate中如何定义一个实体的主键?
java·后端·hibernate
廋到被风吹走2 小时前
【Spring】Spring AMQP 详细介绍
java·spring·wpf
一起养小猫3 小时前
LeetCode100天Day6-回文数与加一
java·leetcode
POLITE33 小时前
Leetcode 234.回文链表 JavaScript (Day 9)
javascript·leetcode·链表
消失的旧时光-19433 小时前
Flutter API 设计最佳实践(终极版)
flutter
程序员小假3 小时前
我们来说一下 MySQL 的慢查询日志
java·后端