普通视图

发现新文章,点击刷新页面。
今天 — 2025年11月22日首页

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

作者 SoaringHeart
2025年11月22日 12:53

一、需求来源

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

Simulator Screenshot - iPhone 16 - 2025-11-22 at 12.44.53.png

二、使用示例

//
//  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

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

❌
❌