Flutter开发者

Chip

2018-09-29

上期回顾

在前面的文章中我们看了下Tooltip的用法,在文章的最后也给大家留了一个问题,自定义自己的Tooltip。

可以看到在上图中,同样是Tooltip,我们修改了Tooltip的背景颜色,以及装饰器的边框弧度,还有Tooltip的宽高比。

其实针对上面的界面,我们只需要修改Tooltip源码中BoxDecoration的color属性以及borderRadius属性就可以实现背景颜色以及边弧度的修改。

修改Center的 widthFactor以及heightFactor属性就可以完成对Tooltip宽高比的修改。

/// The amount of vertical distance bet
final double verticalOffset;

/// borderRadius of BoxDecoration
final double borderRadius;

///widthFactor of tip
final double widthFactor;

///heightFactor of tip
final double heightFactor;

///backgroundColor of tip
final Color backgroundColor;

但是,同样为了便于扩展,我们可以把这几个属性也抽离出来,添加入我们自定Tooltip的构造方法中即可。

下面还是来看下整个代码实现吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327

// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';

const Duration _kFadeDuration = Duration(milliseconds: 200);
const Duration _kShowDuration = Duration(milliseconds: 1500);

class MyTooltip extends StatefulWidget {
/// Creates a tooltip.
///
/// By default, tooltips prefer to appear below the [child] widget when the
/// user long presses on the widget.
///
/// The [message] argument must not be null.
const MyTooltip({
Key key,
@required this.message,
this.height = 32.0,
this.padding = const EdgeInsets.symmetric(horizontal: 16.0),
this.verticalOffset = 24.0,
this.borderRadius = 2.0,
this.widthFactor = 1.0,
this.heightFactor = 1.0,
this.preferBelow = true,
this.backgroundColor = Colors.grey,
this.excludeFromSemantics = false,
this.child,
}) : assert(message != null),
assert(height != null),
assert(padding != null),
assert(verticalOffset != null),
assert(preferBelow != null),
assert(excludeFromSemantics != null),
super(key: key);

/// The text to display in the tooltip.
final String message;

/// The amount of vertical space the tooltip should occupy (inside its padding).
final double height;

/// The amount of space by which to inset the child.
///
/// Defaults to 16.0 logical pixels in each direction.
final EdgeInsetsGeometry padding;

/// The amount of vertical distance between the widget and the displayed tooltip.
final double verticalOffset;

/// borderRadius of BoxDecoration
final double borderRadius;

///widthFactor of tip
final double widthFactor;

///heightFactor of tip
final double heightFactor;

///backgroundColor of tip
final Color backgroundColor;

/// Whether the tooltip defaults to being displayed below the widget.
///
/// Defaults to true. If there is insufficient space to display the tooltip in
/// the preferred direction, the tooltip will be displayed in the opposite
/// direction.
final bool preferBelow;

/// Whether the tooltip's [message] should be excluded from the semantics
/// tree.
final bool excludeFromSemantics;

/// The widget below this widget in the tree.
///
/// {@macro flutter.widgets.child}
final Widget child;

@override
_TooltipState createState() => _TooltipState();

@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(StringProperty('message', message, showName: false));
properties.add(DoubleProperty('vertical offset', verticalOffset));
properties.add(FlagProperty('position',
value: preferBelow, ifTrue: 'below', ifFalse: 'above', showName: true));
}
}

class _TooltipState extends State<MyTooltip>
with SingleTickerProviderStateMixin {
AnimationController _controller;
OverlayEntry _entry;
Timer _timer;

@override
void initState() {
super.initState();
_controller = AnimationController(duration: _kFadeDuration, vsync: this)
..addStatusListener(_handleStatusChanged);
}

void _handleStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.dismissed) _removeEntry();
}

/// Shows the tooltip if it is not already visible.
///
/// Returns `false` when the tooltip was already visible.
bool ensureTooltipVisible() {
if (_entry != null) {
_timer?.cancel();
_timer = null;
_controller.forward();
return false; // Already visible.
}
final RenderBox box = context.findRenderObject();
final Offset target = box.localToGlobal(box.size.center(Offset.zero));
// We create this widget outside of the overlay entry's builder to prevent
// updated values from happening to leak into the overlay when the overlay
// rebuilds.
final Widget overlay = _TooltipOverlay(
message: widget.message,
height: widget.height,
padding: widget.padding,
animation:
CurvedAnimation(parent: _controller, curve: Curves.fastOutSlowIn),
target: target,
verticalOffset: widget.verticalOffset,
preferBelow: widget.preferBelow,
borderRadius: widget.borderRadius,
backgroundColor: widget.backgroundColor,
widthFactor: widget.widthFactor,
heightFactor: widget.heightFactor,

);
_entry = OverlayEntry(builder: (BuildContext context) => overlay);
Overlay.of(context, debugRequiredFor: widget).insert(_entry);
GestureBinding.instance.pointerRouter.addGlobalRoute(_handlePointerEvent);
SemanticsService.tooltip(widget.message);
_controller.forward();
return true;
}

void _removeEntry() {
assert(_entry != null);
_timer?.cancel();
_timer = null;
_entry.remove();
_entry = null;
GestureBinding.instance.pointerRouter
.removeGlobalRoute(_handlePointerEvent);
}

void _handlePointerEvent(PointerEvent event) {
assert(_entry != null);
if (event is PointerUpEvent || event is PointerCancelEvent)
_timer ??= Timer(_kShowDuration, _controller.reverse);
else if (event is PointerDownEvent) _controller.reverse();
}

@override
void deactivate() {
if (_entry != null) _controller.reverse();
super.deactivate();
}

@override
void dispose() {
if (_entry != null) _removeEntry();
_controller.dispose();
super.dispose();
}

void _handleLongPress() {
final bool tooltipCreated = ensureTooltipVisible();
if (tooltipCreated) Feedback.forLongPress(context);
}

@override
Widget build(BuildContext context) {
assert(Overlay.of(context, debugRequiredFor: widget) != null);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onLongPress: _handleLongPress,
excludeFromSemantics: true,
child: Semantics(
label: widget.excludeFromSemantics ? null : widget.message,
child: widget.child,
),
);
}
}

/// A delegate for computing the layout of a tooltip to be displayed above or
/// bellow a target specified in the global coordinate system.
class _TooltipPositionDelegate extends SingleChildLayoutDelegate {
/// Creates a delegate for computing the layout of a tooltip.
///
/// The arguments must not be null.
_TooltipPositionDelegate({
@required this.target,
@required this.verticalOffset,
@required this.preferBelow,
}) : assert(target != null),
assert(verticalOffset != null),
assert(preferBelow != null);

/// The offset of the target the tooltip is positioned near in the global
/// coordinate system.
final Offset target;

/// The amount of vertical distance between the target and the displayed
/// tooltip.
final double verticalOffset;

/// Whether the tooltip defaults to being displayed below the widget.
///
/// If there is insufficient space to display the tooltip in the preferred
/// direction, the tooltip will be displayed in the opposite direction.
final bool preferBelow;

@override
BoxConstraints getConstraintsForChild(BoxConstraints constraints) =>
constraints.loosen();

@override
Offset getPositionForChild(Size size, Size childSize) {
return positionDependentBox(
size: size,
childSize: childSize,
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
);
}

@override
bool shouldRelayout(_TooltipPositionDelegate oldDelegate) {
return target != oldDelegate.target ||
verticalOffset != oldDelegate.verticalOffset ||
preferBelow != oldDelegate.preferBelow;
}
}

class _TooltipOverlay extends StatelessWidget {
const _TooltipOverlay({
Key key,
this.message,
this.height,
this.padding,
this.animation,
this.target,
this.verticalOffset,
this.preferBelow,
this.borderRadius,
this.widthFactor,
this.heightFactor,
this.backgroundColor,
}) : super(key: key);

final String message;
final double height;
final EdgeInsetsGeometry padding;
final Animation<double> animation;
final Offset target;
final double verticalOffset;
final bool preferBelow;

final double borderRadius;
final double widthFactor;
final double heightFactor;
final Color backgroundColor;

@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final ThemeData darkTheme = ThemeData(
brightness: Brightness.dark,
textTheme: theme.brightness == Brightness.dark
? theme.textTheme
: theme.primaryTextTheme,
platform: theme.platform,
);
return Positioned.fill(
child: IgnorePointer(
child: CustomSingleChildLayout(
delegate: _TooltipPositionDelegate(
target: target,
verticalOffset: verticalOffset,
preferBelow: preferBelow,
),
child: FadeTransition(
opacity: animation,
child: Opacity(
opacity: 0.9,
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: height),
child: Container(
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(borderRadius),
),
padding: padding,
child: Center(
widthFactor: widthFactor,
heightFactor: heightFactor,
child: Text(message, style: darkTheme.textTheme.body1),
),
),
),
),
),
),
),
);
}
}

代码稍微有点长,还是主要看_TooltipOverlay部分即可。

下面我们可以尝试修改我们刚才自定义的几个属性来看下效果:

1
2
3
4
5
6
7
8
9
10
11
12

MyTooltip(
message: "点击删除",
preferBelow: false,
backgroundColor: Colors.lightGreenAccent,
borderRadius: 10.0,
widthFactor: 3.0,
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 15.0),
child: Icon(
Icons.delete,
size: 50.0,
))

当然大家也可以根据自己的需要给tip添加背景图片,这里就不在具体演示了。

Chip

中文翻译为碎片的意思,一般也是用作商品或者一些东西的标签。

好吧,还是看看怎么使用吧

构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Chip({
Key key,
this.avatar,//标签左侧Widget,一般为小图标
@required this.label,//标签
this.labelStyle,
this.labelPadding,//padding
this.deleteIcon//删除图标,
this.onDeleted//删除回调,为空时不显示删除图标,
this.deleteIconColor//删除图标的颜色,
this.deleteButtonTooltipMessage//删除按钮的tip文字,
this.shape//形状,
this.clipBehavior = Clip.none,
this.backgroundColor//背景颜色,
this.padding,
this.materialTapTargetSize//删除图标material点击区域大小,
})

说了这么多,还是真的不如先来个最简单的Chip看下什么效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

import 'package:flutter/material.dart';

void main() {
runApp(MaterialApp(
home: MyApp(),
));
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Chip"),
),
body: Center(
child: Chip(
label: Text("flyou"),
),
),
);
}
}

纳尼,这是什么鬼

这跟前面讲的ToolTip显示效果也好像啊,像吗?确实很像,确实也不是很像,接着往下看吧。

好吧,刚才构造方法中有那么多的属性,我们来看下怎么使用吧。

我们给我们的Chip加上一个 avatar再来看下效果

avatar: Icon(Icons.add_location, color: Colors.lightBlue)

那么再来看下这几个与delete相关的属性吧

我们先仅仅给Chip添加 onDeleted属性

onDeleted: (){}

可以看到,我们仅仅给Chip添加了一个onDeleted属性它便给我们多出了一个删除的按钮和长按的Tooltip提示(没错,就是我们上篇文章讲到的Tooltip,感兴趣的童鞋可以去看下源码哈)。

那么,我们尝试修改与delete相关的其他属性再来看下效果

1
2
3
4

deleteIcon: Icon(Icons.delete_forever),
deleteIconColor: Colors.red,
deleteButtonTooltipMessage: "删除该条",

修改背景颜色

backgroundColor: Colors.lightGreenAccent

或许你对Chip的显示样式还不是很满意,那么你就真的可以试试shape属性了

shape:RoundedRectangleBorder(borderRadius: BorderRadius.circular(3.0))

当然,你还可以修改labelStyle来修改文字显示效果,修改padding来控制chip的大小。

小结

  • Chip是一个很常见的标签组件
  • 使用Chip的一些属性可以很方便的完成对Chip效果的自定义
  • Chip自带删除按钮和删除监听,可以方便做其他操作

试一试

根据以前讲到过的东东,试下如下效果(从来不要求你跟我一样,哈)

使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

打赏请备注姓名或者昵称,方便我后期统计哦

关注公众号,及时查阅最新文章