Flutter组件封装:标签拖拽排序 NDragSortWrap

一、需求来源

最近需要实现一个可拖拽标签需求,实现之后顺手封装一下,效果如下:

二、使用示例

less 复制代码
//
//  DraggableDemo.dart
//  flutter_templet_project
//
//  Created by shang on 6/2/21 5:37 PM.
//  Copyright © 6/2/21 shang. All rights reserved.
//

import 'package:flutter/material.dart';
import 'package:flutter_templet_project/basicWidget/n_drag_sort_wrap.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';

class DraggableDemo extends StatefulWidget {
  final String? title;

  const DraggableDemo({Key? key, this.title}) : super(key: key);

  @override
  _DraggableDemoState createState() => _DraggableDemoState();
}

class _DraggableDemoState extends State<DraggableDemo> with TickerProviderStateMixin {
  final scrollController = ScrollController();


  List<String> tags = List.generate(20, (i) => "标签$i");
  late List<String> others = List.generate(10, (i) => "其他${i + tags.length}");

  late var tabController = TabController(length: tags.length, vsync: this);

  bool canEdit = true;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title ?? "$widget"),
      ),
      body: buildBody(),
    );
  }

  Widget buildBody() {
    return Scrollbar(
      controller: scrollController,
      child: SingleChildScrollView(
        controller: scrollController,
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            buildDragSortWrap(),
          ],
        ),
      ),
    );
  }

  Widget buildDragSortWrap() {
    return StatefulBuilder(
      builder: (BuildContext context, StateSetter setState) {
        tabController = TabController(length: tags.length, vsync: this);
        return Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Material(
              child: Row(
                children: [
                  Expanded(
                    child: TabBar(
                      controller: tabController,
                      isScrollable: true,
                      tabs: tags.map((e) => Tab(text: e)).toList(),
                      labelColor: Colors.black87,
                      unselectedLabelColor: Colors.black38,
                      indicatorColor: Colors.red,
                      indicatorSize: TabBarIndicatorSize.label,
                      indicatorPadding: EdgeInsets.symmetric(horizontal: 16),
                    ),
                  ),
                  GestureDetector(
                    onTap: () {
                      DLog.d("more");
                    },
                    child: Container(
                      padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                      child: Icon(Icons.keyboard_arrow_down),
                    ),
                  )
                ],
              ),
            ),
            buildTagBar(
              onEdit: () {
                canEdit = !canEdit;
                setState(() {});
              },
            ),
            NDragSortWrap<String>(
              spacing: 12,
              runSpacing: 8,
              items: tags,
              itemBuilder: (context, item, isDragging) {
                return buildItem(
                  isDragging: isDragging,
                  item: item,
                  isTopRightVisible: canEdit,
                  topRight: GestureDetector(
                    onTap: () {
                      DLog.d(item);
                      tags.remove(item);
                      setState(() {});
                    },
                    child: Icon(Icons.remove, size: 14, color: Colors.white),
                  ),
                );
              },
              onChanged: (newList) {
                tags = newList;
                setState(() {});
              },
            ),
            Divider(height: 16),
            Wrap(
              spacing: 12,
              runSpacing: 8,
              children: [
                ...others.map(
                  (item) {
                    return buildItem(
                      isDragging: false,
                      item: item,
                      isTopRightVisible: canEdit,
                      topRight: GestureDetector(
                        onTap: () {
                          DLog.d(item);
                          others.remove(item);
                          tags.add(item);
                          setState(() {});
                        },
                        child: Icon(Icons.add, size: 14, color: Colors.white),
                      ),
                    );
                  },
                ),
              ],
            )
          ],
        );
      },
    );
  }

  Widget buildItem({
    required bool isDragging,
    required String item,
    bool isTopRightVisible = true,
    required Widget topRight,
  }) {
    return Badge(
      backgroundColor: Colors.red,
      textColor: Colors.white,
      offset: Offset(4, -4),
      isLabelVisible: isTopRightVisible,
      label: topRight,
      child: AnimatedContainer(
        duration: Duration(milliseconds: 150),
        padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        decoration: BoxDecoration(
          color: isDragging ? Colors.green.withOpacity(0.6) : Colors.green,
          borderRadius: BorderRadius.circular(8),
        ),
        child: Text(
          item,
          style: TextStyle(color: Colors.white),
        ),
      ),
    );
  }

  Widget buildTagBar({required VoidCallback onEdit}) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
      clipBehavior: Clip.antiAlias,
      decoration: BoxDecoration(),
      child: Row(
        mainAxisSize: MainAxisSize.min,
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Container(
            clipBehavior: Clip.antiAlias,
            decoration: BoxDecoration(),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              mainAxisAlignment: MainAxisAlignment.start,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  '我的频道',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: const Color(0xFF303034),
                    fontSize: 15,
                    fontWeight: FontWeight.w600,
                  ),
                ),
                Text(
                  ' (点击编辑可排序)',
                  textAlign: TextAlign.center,
                  style: TextStyle(
                    color: const Color(0xFF7C7C85),
                    fontSize: 12,
                  ),
                ),
              ],
            ),
          ),
          GestureDetector(
            onTap: onEdit,
            child: Text(
              '编辑',
              textAlign: TextAlign.center,
              style: TextStyle(
                color: const Color(0xFF303034),
                fontSize: 14,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

三、源码

组件 NDragSortWrap

scala 复制代码
import 'package:flutter/material.dart';
import 'package:flutter_templet_project/extension/extension_local.dart';

class NDragSortWrap<T extends Object> extends StatefulWidget {
  const NDragSortWrap({
    super.key,
    required this.items,
    required this.itemBuilder,
    this.onChanged,
    this.spacing = 8,
    this.runSpacing = 8,
  });

  final List<T> items;
  final Widget Function(BuildContext context, T item, bool isDragging) itemBuilder;
  final void Function(List<T> newList)? onChanged;
  final double spacing;
  final double runSpacing;

  @override
  State<NDragSortWrap<T>> createState() => _NDragSortWrapState<T>();
}

class _NDragSortWrapState<T extends Object> extends State<NDragSortWrap<T>> {
  late List<T> _list;

  @override
  void initState() {
    super.initState();
    _list = List<T>.from(widget.items);
  }

  @override
  void didUpdateWidget(covariant NDragSortWrap<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    _list = List<T>.from(widget.items);
    setState(() {});
  }

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: widget.spacing,
      runSpacing: widget.runSpacing,
      children: [
        for (int i = 0; i < _list.length; i++) _buildDraggableItem(context, i),
      ],
    );
  }

  Widget _buildDraggableItem(BuildContext context, int index) {
    final item = _list[index];

    return LongPressDraggable<T>(
      data: item,
      feedback: Material(
        color: Colors.transparent,
        child: widget.itemBuilder(context, item, true),
      ),
      childWhenDragging: Opacity(
        opacity: 0.3,
        child: widget.itemBuilder(context, item, false),
      ),
      onDragCompleted: () {},
      onDraggableCanceled: (_, __) {},
      child: DragTarget<T>(
        onAcceptWithDetails: (details) {
          final draggedItem = details.data;
          final oldIndex = _list.indexOf(draggedItem);
          final newIndex = index;

          final item = _list.removeAt(oldIndex);
          _list.insert(newIndex, item);
          setState(() {});

          widget.onChanged?.call(_list);
        },
        builder: (context, _, __) {
          return widget.itemBuilder(context, item, false);
        },
      ),
    );
  }
}

最后、总结

实现起来并不复杂,就是依赖官方组件 LongPressDraggable 提供的各种状态做数据处理,然后刷新页面即可。

github

相关推荐
এ慕ོ冬℘゜几秒前
前端实战:使用 jQuery 与 CSS3 打造动态数据表格与滑块交互
前端·css3·jquery
Csvn1 分钟前
✂️ AbortController:一个 API 统一取消 fetch、事件监听与 AI 流式请求
前端
invicinble4 分钟前
对于vue2转vue3相关技术整合
前端
MacroZheng13 分钟前
腾讯又开源了一个新项目,用起来真优雅!
前端·vue.js·人工智能
如此这般英俊28 分钟前
手搓Claude Code-第十三章 background_tasks
前端·人工智能·chrome·python·算法·语言模型·自然语言处理
人间凡尔赛30 分钟前
Angular 20 告别 Zone.js:Zoneless 无 Zone 时代正式开启,Signals 驱动的响应式新范式(附完整代码)
前端·angular
雪芽蓝域zzs33 分钟前
(五)Vue3 管理后台:权限控制原理 + 完整流程梳理
前端
meilindehuzi_a34 分钟前
React JWT 登录鉴权实战:从无状态 HTTP 到路由守卫、Zustand 与 Axios 拦截器
前端·react.js·http
爱酱丶44 分钟前
VS Code 快速生成Vue3 + TypeScript + Setup 基础空模板
前端·javascript·typescript