Skip to content

Commit

Permalink
Allow dragging to reorder tabs to the last position. #35
Browse files Browse the repository at this point in the history
Allow dragging tabs between different TabbedView instances. #32
  • Loading branch information
caduandrade committed Jul 14, 2023
1 parent f8c169a commit be6f423
Show file tree
Hide file tree
Showing 25 changed files with 566 additions and 417 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
## 1.18.0

* Highlighting the tab's drop position.
* Allow dragging to reorder tabs to the last position.
* Allow dragging tabs between different `TabbedView` instances.

### Changes

* `OnDraggableBuild`
* From: `(int tabIndex, TabData tabData)`
* To: `(TabbedViewController controller, int tabIndex, TabData tabData)`
* `Draggable` will always be `DraggableData` type: `Draggable<DraggableData>`

## 1.17.0

Expand All @@ -23,6 +32,8 @@
* `TabbedView`
* The `draggableTabBuilder` has been replaced by `onDraggableBuild`
* Automatic creation of a `Draggable<TabData>`
* `TabData`
* The `uniqueKey` attribute has been renamed to `key`.
* Minimum sdk version required: 2.19.0

### Migrating custom drag feedback
Expand Down
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -169,4 +169,4 @@ packages:
version: "2.1.4"
sdks:
dart: ">=3.0.0-0 <4.0.0"
flutter: ">=1.17.0"
flutter: ">=2.17.0"
2 changes: 1 addition & 1 deletion lib/src/content_area.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class ContentArea extends StatelessWidget {
child = Offstage(offstage: !selectedTab, child: child);
}
children.add(Positioned.fill(
key: tab.uniqueKey,
key: tab.key,
child:
Container(child: child, padding: contentAreaTheme.padding)));
}
Expand Down
5 changes: 4 additions & 1 deletion lib/src/draggable_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ class DraggableConfig {
this.onDragUpdate,
this.onDraggableCanceled,
this.onDragEnd,
this.onDragCompleted});
this.onDragCompleted,
this.canDrag = true});

static const DraggableConfig defaultConfig = DraggableConfig();

Expand Down Expand Up @@ -80,4 +81,6 @@ class DraggableConfig {
/// This function will only be called while this widget is still mounted to
/// the tree (i.e. [State.mounted] is true).
final DragEndCallback? onDragEnd;

final bool canDrag;
}
9 changes: 9 additions & 0 deletions lib/src/draggable_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import 'package:tabbed_view/src/tab_data.dart';
import 'package:tabbed_view/src/tabbed_view_controller.dart';

class DraggableData {
DraggableData(this.controller, this.tabData);

final TabbedViewController controller;
final TabData tabData;
}
9 changes: 8 additions & 1 deletion lib/src/internal/tabbed_view_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import 'package:meta/meta.dart';
import 'package:tabbed_view/src/tabbed_view.dart';
import 'package:tabbed_view/src/tabbed_view_controller.dart';
import 'package:tabbed_view/src/tabbed_view_menu_item.dart';
import 'package:tabbed_view/src/typedefs/on_before_drop_accept.dart';
import 'package:tabbed_view/src/typedefs/on_draggable_build.dart';
import 'package:tabbed_view/src/typedefs/can_drop.dart';

/// Propagates parameters to internal widgets.
@internal
Expand All @@ -22,7 +25,9 @@ class TabbedViewProvider {
required this.menuItemsUpdater,
required this.onTabDrag,
required this.draggingTabIndex,
required this.onDraggableBuild});
required this.onDraggableBuild,
required this.canDrop,
required this.onBeforeDropAccept});

final TabbedViewController controller;
final bool contentClip;
Expand All @@ -39,6 +44,8 @@ class TabbedViewProvider {
final OnTabDrag onTabDrag;
final int? draggingTabIndex;
final OnDraggableBuild? onDraggableBuild;
final CanDrop? canDrop;
final OnBeforeDropAccept? onBeforeDropAccept;
}

/// Updater for menu items
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:tabbed_view/src/draggable_data.dart';
import 'package:tabbed_view/src/internal/tabbed_view_provider.dart';
import 'package:tabbed_view/src/tab_data.dart';

@internal
class DropTabWidget extends StatefulWidget {
Expand All @@ -15,6 +15,8 @@ class DropTabWidget extends StatefulWidget {
final Widget child;
final int newIndex;

static const double dropWidth = 8;

@override
State<StatefulWidget> createState() => DropTabWidgetState();
}
Expand All @@ -32,7 +34,7 @@ class DropTabWidgetState extends State<DropTabWidget> {

@override
Widget build(BuildContext context) {
return DragTarget<TabData>(
return DragTarget<DraggableData>(
builder: (
BuildContext context,
List<dynamic> accepted,
Expand All @@ -45,31 +47,63 @@ class DropTabWidgetState extends State<DropTabWidget> {
return widget.child;
},
onMove: (details) {
if (_over == false) {
if (_over == false && _canDrop(details.data)) {
setState(() {
_over = true;
});
}
},
onLeave: (data) {
setState(() {
_over = false;
});
if (_over) {
setState(() {
_over = false;
});
}
},
onWillAccept: (data) {
if (data != null) {
return _canDrop(data);
}
return false;
},
onAccept: (TabData tabData) {
widget.provider.controller.reorderTab(tabData.index, widget.newIndex);
onAccept: (DraggableData data) {
if (widget.provider.onBeforeDropAccept != null) {
if (widget.provider.onBeforeDropAccept!(
data, widget.provider.controller) ==
false) {
setState(() {
_over = false;
});
return;
}
}
if (widget.provider.controller == data.controller) {
widget.provider.controller
.reorderTab(data.tabData.index, widget.newIndex);
} else {
data.controller.removeTab(data.tabData.index);
widget.provider.controller.insertTab(widget.newIndex, data.tabData);
}
},
);
}

bool _canDrop(DraggableData source) {
if (widget.provider.canDrop == null) {
return true;
}
return widget.provider.canDrop!(source, widget.provider.controller);
}
}

class _CustomPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..color = Colors.black.withOpacity(.7)
..style = PaintingStyle.fill;
canvas.drawRect(Rect.fromLTWH(0, 0, 3, size.height), paint);
canvas.drawRect(
Rect.fromLTWH(0, 0, DropTabWidget.dropWidth, size.height), paint);
}

@override
Expand Down
27 changes: 27 additions & 0 deletions lib/src/internal/tabs_area/hidden_tabs.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:collection';

import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

/// Holds the hidden tab indexes.
@internal
class HiddenTabs extends ChangeNotifier {
List<int> _indexes = [];

bool _hasHiddenTabs = false;
bool get hasHiddenTabs => _hasHiddenTabs;

List<int> get indexes {
_indexes.sort();
return UnmodifiableListView(_indexes);
}

void update(List<int> hiddenIndexes) {
_indexes = hiddenIndexes;
bool hasHiddenTabs = _indexes.isNotEmpty;
if (_hasHiddenTabs != hasHiddenTabs) {
_hasHiddenTabs = hasHiddenTabs;
Future.microtask(() => notifyListeners());
}
}
}
97 changes: 97 additions & 0 deletions lib/src/internal/tabs_area/tabs_area_buttons_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:tabbed_view/src/internal/tabs_area/hidden_tabs.dart';
import 'package:tabbed_view/src/internal/tabbed_view_provider.dart';
import 'package:tabbed_view/src/tab_button.dart';
import 'package:tabbed_view/src/tab_button_widget.dart';
import 'package:tabbed_view/src/tab_data.dart';
import 'package:tabbed_view/src/tabbed_view_menu_item.dart';
import 'package:tabbed_view/src/theme/tabbed_view_theme_data.dart';
import 'package:tabbed_view/src/theme/tabs_area_theme_data.dart';
import 'package:tabbed_view/src/theme/theme_widget.dart';

/// Area for buttons like the hidden tabs menu button.
@internal
class TabsAreaButtonsWidget extends StatelessWidget {
final TabbedViewProvider provider;
final HiddenTabs hiddenTabs;

const TabsAreaButtonsWidget(
{super.key, required this.provider, required this.hiddenTabs});

@override
Widget build(BuildContext context) {
final TabbedViewThemeData theme = TabbedViewTheme.of(context);
final TabsAreaThemeData tabsAreaTheme = theme.tabsArea;

List<TabButton> buttons = [];
if (provider.tabsAreaButtonsBuilder != null) {
buttons = provider.tabsAreaButtonsBuilder!(
context, provider.controller.tabs.length);
}
if (hiddenTabs.hasHiddenTabs) {
buttons.insert(
0,
TabButton(
icon: tabsAreaTheme.menuIcon,
menuBuilder: _hiddenTabsMenuBuilder));
}

List<Widget> children = [];

for (int i = 0; i < buttons.length; i++) {
EdgeInsets? padding;
if (i > 0 && tabsAreaTheme.buttonsGap > 0) {
padding = EdgeInsets.only(left: tabsAreaTheme.buttonsGap);
}
final TabButton tabButton = buttons[i];
children.add(Container(
child: TabButtonWidget(
provider: provider,
button: tabButton,
enabled: provider.draggingTabIndex == null,
normalColor: tabsAreaTheme.normalButtonColor,
hoverColor: tabsAreaTheme.hoverButtonColor,
disabledColor: tabsAreaTheme.disabledButtonColor,
normalBackground: tabsAreaTheme.normalButtonBackground,
hoverBackground: tabsAreaTheme.hoverButtonBackground,
disabledBackground: tabsAreaTheme.disabledButtonBackground,
iconSize: tabButton.iconSize != null
? tabButton.iconSize!
: tabsAreaTheme.buttonIconSize,
themePadding: tabsAreaTheme.buttonPadding),
padding: padding));
}

Widget buttonsArea = Row(children: children);

EdgeInsetsGeometry? margin;
if (tabsAreaTheme.buttonsOffset > 0) {
margin = EdgeInsets.only(left: tabsAreaTheme.buttonsOffset);
}

if (children.isNotEmpty &&
(tabsAreaTheme.buttonsAreaDecoration != null ||
tabsAreaTheme.buttonsAreaPadding != null ||
margin != null)) {
buttonsArea = Container(
child: buttonsArea,
decoration: tabsAreaTheme.buttonsAreaDecoration,
padding: tabsAreaTheme.buttonsAreaPadding,
margin: margin);
}
return buttonsArea;
}

/// Builder for hidden tabs menu.
List<TabbedViewMenuItem> _hiddenTabsMenuBuilder(BuildContext context) {
List<TabbedViewMenuItem> list = [];
for (int index in hiddenTabs.indexes) {
TabData tab = provider.controller.tabs[index];
list.add(TabbedViewMenuItem(
text: tab.text,
onSelection: () => provider.controller.selectedIndex = index));
}
return list;
}
}
35 changes: 35 additions & 0 deletions lib/src/internal/tabs_area/tabs_area_corner.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';
import 'package:tabbed_view/src/internal/tabbed_view_provider.dart';
import 'package:tabbed_view/src/internal/tabs_area/drop_tab_widget.dart';
import 'package:tabbed_view/src/internal/tabs_area/hidden_tabs.dart';
import 'package:tabbed_view/src/internal/tabs_area/tabs_area_buttons_widget.dart';

@internal
class TabsAreaCorner extends StatelessWidget {
final TabbedViewProvider provider;
final HiddenTabs hiddenTabs;

const TabsAreaCorner(
{super.key, required this.provider, required this.hiddenTabs});

@override
Widget build(BuildContext context) {
return ListenableBuilder(listenable: hiddenTabs, builder: _builder);
}

Widget _builder(BuildContext context, Widget? child) {
return DropTabWidget(
provider: provider,
newIndex: provider.controller.length,
child: Container(
padding: EdgeInsets.only(left: DropTabWidget.dropWidth),
child: Row(
children: [
TabsAreaButtonsWidget(
provider: provider, hiddenTabs: hiddenTabs)
],
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.end)));
}
}
21 changes: 21 additions & 0 deletions lib/src/internal/tabs_area/tabs_area_layout_parent_data.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';

/// Parent data for [_TabsAreaLayoutRenderBox] class.
@internal
class TabsAreaLayoutParentData extends ContainerBoxParentData<RenderBox> {
bool visible = false;
bool selected = false;

double leftBorderHeight = 0;
double rightBorderHeight = 0;

/// Resets all values.
void reset() {
visible = false;
selected = false;

leftBorderHeight = 0;
rightBorderHeight = 0;
}
}
Loading

0 comments on commit be6f423

Please sign in to comment.