Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
5d39f15c
Unverified
Commit
5d39f15c
authored
Feb 23, 2018
by
amirh
Committed by
GitHub
Feb 23, 2018
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Smooth Floating Action Button notch (#14851)
parent
7f03b9e4
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
145 additions
and
95 deletions
+145
-95
floating_action_button.dart
...ages/flutter/lib/src/material/floating_action_button.dart
+78
-48
floating_action_button_test.dart
...es/flutter/test/material/floating_action_button_test.dart
+67
-47
No files found.
packages/flutter/lib/src/material/floating_action_button.dart
View file @
5d39f15c
...
...
@@ -227,65 +227,95 @@ class _FloatingActionButtonState extends State<FloatingActionButton> {
}
Path
_computeNotch
(
Rect
host
,
Rect
guest
,
Offset
start
,
Offset
end
)
{
assert
(()
{
if
(
end
.
dy
!=
host
.
top
)
throw
new
FlutterError
(
'The floating action button
\'
s notch maker must only be used for a notch in the top edge of the host.
\n
'
'The notch
\'
s path end point:
$end
is not in the top edge of
$host
'
);
if
(
start
.
dy
!=
host
.
top
)
throw
new
FlutterError
(
'The floating action button
\'
s notch maker must only be used for a notch in the top edge the host.
\n
'
'The notch
\'
s path start point:
$start
is not in the top edge of
$host
'
);
return
true
;
}());
assert
(()
{
if
(!
host
.
overlaps
(
guest
))
throw
new
FlutterError
(
'Notch host must intersect with its guest'
);
return
true
;
}());
// The FAB's shape is a circle bounded by the guest rectangle.
// So the FAB's radius is half the guest width.
final
double
fabRadius
=
guest
.
width
/
2.0
;
final
double
notchRadius
=
fabRadius
+
widget
.
notchMargin
;
assert
(()
{
if
(
guest
.
center
.
dx
-
notchRadius
<
start
.
dx
)
throw
new
FlutterError
(
'The notch
\'
s path start point must be to the left of the notch.
\n
'
'Start point was
$start
, guest was
$guest
, notchMargin was
${widget.notchMargin}
.'
);
if
(
guest
.
center
.
dx
+
notchRadius
>
end
.
dx
)
throw
new
FlutterError
(
'The notch
\'
s end point must be to the right of the guest.
\n
'
'End point was
$start
, notch was
$guest
, notchMargin was
${widget.notchMargin}
.'
);
return
true
;
}());
// We find the intersection of the notch's circle with the top edge of the host
// using the Pythagorean theorem for the right triangle that connects the
// center of the notch and the intersection of the notch's circle and the host's
// top edge.
//
// The hypotenuse of this triangle equals the notch's radius, and one side
// (a) is the distance from the notch's center to the top edge.
assert
(
_notchAssertions
(
host
,
guest
,
start
,
end
,
fabRadius
,
notchRadius
));
// If there's no overlap between the guest's margin boundary and the host,
// don't make a notch, just return a straight line from start to end.
if
(!
host
.
overlaps
(
guest
.
inflate
(
widget
.
notchMargin
)))
return
new
Path
()..
lineTo
(
end
.
dx
,
end
.
dy
);
// We build a path for the notch from 3 segments:
// Segment A - a Bezier curve from the host's top edge to segment B.
// Segment B - an arc with radius notchRadius.
// Segment C - a Bezier curver from segment B back to the host's top edge.
//
// The other side (b) would be the distance on the horizontal axis between the
// notch's center and the intersection points with it's top edge.
final
double
a
=
host
.
top
-
guest
.
center
.
dy
;
final
double
b
=
math
.
sqrt
(
notchRadius
*
notchRadius
-
a
*
a
);
// A detailed explanation and the derivation of the formulas below is
// available at: https://goo.gl/Ufzrqn
const
double
s1
=
15.0
;
const
double
s2
=
1.0
;
final
double
r
=
notchRadius
;
final
double
a
=
-
1.0
*
r
-
s2
;
final
double
b
=
host
.
top
-
guest
.
center
.
dy
;
final
double
n2
=
math
.
sqrt
(
b
*
b
*
r
*
r
*
(
a
*
a
+
b
*
b
-
r
*
r
));
final
double
p2xA
=
((
a
*
r
*
r
)
-
n2
)
/
(
a
*
a
+
b
*
b
);
final
double
p2xB
=
((
a
*
r
*
r
)
+
n2
)
/
(
a
*
a
+
b
*
b
);
final
double
p2yA
=
math
.
sqrt
(
r
*
r
-
p2xA
*
p2xA
);
final
double
p2yB
=
math
.
sqrt
(
r
*
r
-
p2xB
*
p2xB
);
final
List
<
Offset
>
p
=
new
List
<
Offset
>(
6
);
// p0, p1, and p2 are the control points for segment A.
p
[
0
]
=
new
Offset
(
a
-
s1
,
b
);
p
[
1
]
=
new
Offset
(
a
,
b
);
final
double
cmp
=
b
<
0
?
-
1.0
:
1.0
;
p
[
2
]
=
cmp
*
p2yA
>
cmp
*
p2yB
?
new
Offset
(
p2xA
,
p2yA
)
:
new
Offset
(
p2xB
,
p2yB
);
// p3, p4, and p5 are the control points for segment B, which is a mirror
// of segment A around the y axis.
p
[
3
]
=
new
Offset
(-
1.0
*
p
[
2
].
dx
,
p
[
2
].
dy
);
p
[
4
]
=
new
Offset
(-
1.0
*
p
[
1
].
dx
,
p
[
1
].
dy
);
p
[
5
]
=
new
Offset
(-
1.0
*
p
[
0
].
dx
,
p
[
0
].
dy
);
// translate all points back to the absolute coordinate system.
for
(
int
i
=
0
;
i
<
p
.
length
;
i
+=
1
)
p
[
i
]
+=
guest
.
center
;
return
new
Path
()
..
lineTo
(
guest
.
center
.
dx
-
b
,
host
.
top
)
..
lineTo
(
p
[
0
].
dx
,
p
[
0
].
dy
)
..
quadraticBezierTo
(
p
[
1
].
dx
,
p
[
1
].
dy
,
p
[
2
].
dx
,
p
[
2
].
dy
)
..
arcToPoint
(
new
Offset
(
guest
.
center
.
dx
+
b
,
host
.
top
)
,
p
[
3
]
,
radius:
new
Radius
.
circular
(
notchRadius
),
clockwise:
false
,
)
..
quadraticBezierTo
(
p
[
4
].
dx
,
p
[
4
].
dy
,
p
[
5
].
dx
,
p
[
5
].
dy
)
..
lineTo
(
end
.
dx
,
end
.
dy
);
}
bool
_notchAssertions
(
Rect
host
,
Rect
guest
,
Offset
start
,
Offset
end
,
double
fabRadius
,
double
notchRadius
)
{
if
(
end
.
dy
!=
host
.
top
)
throw
new
FlutterError
(
'The notch of the floating action button must end at the top edge of the host.
\n
'
'The notch
\'
s path end point:
$end
is not in the top edge of
$host
'
);
if
(
start
.
dy
!=
host
.
top
)
throw
new
FlutterError
(
'The notch of the floating action button must start at the top edge of the host.
\n
'
'The notch
\'
s path start point:
$start
is not in the top edge of
$host
'
);
if
(
guest
.
center
.
dx
-
notchRadius
<
start
.
dx
)
throw
new
FlutterError
(
'The notch
\'
s path start point must be to the left of the floating action button.
\n
'
'Start point was
$start
, guest was
$guest
, notchMargin was
${widget.notchMargin}
.'
);
if
(
guest
.
center
.
dx
+
notchRadius
>
end
.
dx
)
throw
new
FlutterError
(
'The notch
\'
s end point must be to the right of the floating action button.
\n
'
'End point was
$start
, notch was
$guest
, notchMargin was
${widget.notchMargin}
.'
);
return
true
;
}
}
packages/flutter/test/material/floating_action_button_test.dart
View file @
5d39f15c
...
...
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:math'
as
math
;
import
'dart:ui'
;
import
'package:flutter/foundation.dart'
;
...
...
@@ -255,36 +256,15 @@ void main() {
final
Offset
end
=
const
Offset
(
220.0
,
100.0
);
final
Path
actualNotch
=
computeNotch
(
host
,
guest
,
start
,
end
);
final
Path
expectedNotch
=
new
Path
()
..
lineTo
(
190.0
,
100.0
)
..
arcToPoint
(
const
Offset
(
210.0
,
100.0
),
radius:
const
Radius
.
circular
(
10.0
),
clockwise:
false
)
..
lineTo
(
220.0
,
100.0
);
expect
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
),
coversSameAreaAs
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
expectedNotch
),
areaToCompare:
host
.
inflate
(
10.0
)
)
);
final
Path
notchedRectangle
=
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
);
expect
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
),
coversSameAreaAs
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
expectedNotch
),
areaToCompare:
guest
.
inflate
(
10.0
),
sampleSize:
50
,
)
);
expect
(
pathDoesNotContainCircle
(
notchedRectangle
,
guest
),
isTrue
);
});
testWidgets
(
'notch with margin'
,
(
WidgetTester
tester
)
async
{
final
ComputeNotch
computeNotch
=
await
fetchComputeNotch
(
tester
,
const
FloatingActionButton
(
onPressed:
null
,
notchMargin:
4.0
)
const
FloatingActionButton
(
onPressed:
null
,
notchMargin:
4.0
)
);
final
Rect
host
=
new
Rect
.
fromLTRB
(
0.0
,
100.0
,
300.0
,
300.0
);
final
Rect
guest
=
new
Rect
.
fromLTRB
(
190.0
,
90.0
,
210.0
,
110.0
);
...
...
@@ -292,33 +272,58 @@ void main() {
final
Offset
end
=
const
Offset
(
220.0
,
100.0
);
final
Path
actualNotch
=
computeNotch
(
host
,
guest
,
start
,
end
);
final
Path
expectedNotch
=
new
Path
()
..
lineTo
(
186.0
,
100.0
)
..
arcToPoint
(
const
Offset
(
214.0
,
100.0
),
radius:
const
Radius
.
circular
(
14.0
),
clockwise:
false
)
..
lineTo
(
220.0
,
100.0
);
expect
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
),
coversSameAreaAs
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
expectedNotch
),
areaToCompare:
host
.
inflate
(
10.0
)
)
final
Path
notchedRectangle
=
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
);
expect
(
pathDoesNotContainCircle
(
notchedRectangle
,
guest
.
inflate
(
4.0
)),
isTrue
);
});
testWidgets
(
'notch circle center above BAB'
,
(
WidgetTester
tester
)
async
{
final
ComputeNotch
computeNotch
=
await
fetchComputeNotch
(
tester
,
const
FloatingActionButton
(
onPressed:
null
,
notchMargin:
4.0
)
);
final
Rect
host
=
new
Rect
.
fromLTRB
(
0.0
,
100.0
,
300.0
,
300.0
);
final
Rect
guest
=
new
Rect
.
fromLTRB
(
190.0
,
85.0
,
210.0
,
105.0
);
final
Offset
start
=
const
Offset
(
180.0
,
100.0
);
final
Offset
end
=
const
Offset
(
220.0
,
100.0
);
expect
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
),
coversSameAreaAs
(
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
expectedNotch
),
areaToCompare:
guest
.
inflate
(
10.0
),
sampleSize:
50
,
)
final
Path
actualNotch
=
computeNotch
(
host
,
guest
,
start
,
end
);
final
Path
notchedRectangle
=
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
);
expect
(
pathDoesNotContainCircle
(
notchedRectangle
,
guest
.
inflate
(
4.0
)),
isTrue
);
});
testWidgets
(
'notch circle center below BAB'
,
(
WidgetTester
tester
)
async
{
final
ComputeNotch
computeNotch
=
await
fetchComputeNotch
(
tester
,
const
FloatingActionButton
(
onPressed:
null
,
notchMargin:
4.0
)
);
final
Rect
host
=
new
Rect
.
fromLTRB
(
0.0
,
100.0
,
300.0
,
300.0
);
final
Rect
guest
=
new
Rect
.
fromLTRB
(
190.0
,
95.0
,
210.0
,
115.0
);
final
Offset
start
=
const
Offset
(
180.0
,
100.0
);
final
Offset
end
=
const
Offset
(
220.0
,
100.0
);
final
Path
actualNotch
=
computeNotch
(
host
,
guest
,
start
,
end
);
final
Path
notchedRectangle
=
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
);
expect
(
pathDoesNotContainCircle
(
notchedRectangle
,
guest
.
inflate
(
4.0
)),
isTrue
);
});
testWidgets
(
'no notch when there is no overlap'
,
(
WidgetTester
tester
)
async
{
final
ComputeNotch
computeNotch
=
await
fetchComputeNotch
(
tester
,
const
FloatingActionButton
(
onPressed:
null
,
notchMargin:
4.0
)
);
final
Rect
host
=
new
Rect
.
fromLTRB
(
0.0
,
100.0
,
300.0
,
300.0
);
final
Rect
guest
=
new
Rect
.
fromLTRB
(
190.0
,
40.0
,
210.0
,
60.0
);
final
Offset
start
=
const
Offset
(
180.0
,
100.0
);
final
Offset
end
=
const
Offset
(
220.0
,
100.0
);
final
Path
actualNotch
=
computeNotch
(
host
,
guest
,
start
,
end
);
final
Path
notchedRectangle
=
createNotchedRectangle
(
host
,
start
.
dx
,
end
.
dx
,
actualNotch
);
expect
(
pathDoesNotContainCircle
(
notchedRectangle
,
guest
.
inflate
(
4.0
)),
isTrue
);
});
});
}
Path
createNotchedRectangle
(
Rect
container
,
double
startX
,
double
endX
,
Path
notch
)
{
...
...
@@ -393,3 +398,18 @@ class GeometryCachePainter extends CustomPainter {
return
true
;
}
}
bool
pathDoesNotContainCircle
(
Path
path
,
Rect
circleBounds
)
{
assert
(
circleBounds
.
width
==
circleBounds
.
height
);
final
double
radius
=
circleBounds
.
width
/
2.0
;
for
(
double
theta
=
0.0
;
theta
<=
2.0
*
math
.
PI
;
theta
+=
math
.
PI
/
20.0
)
{
for
(
double
i
=
0.0
;
i
<
1
;
i
+=
0.01
)
{
final
double
x
=
i
*
radius
*
math
.
cos
(
theta
);
final
double
y
=
i
*
radius
*
math
.
sin
(
theta
);
if
(
path
.
contains
(
new
Offset
(
x
,
y
)
+
circleBounds
.
center
))
return
false
;
}
}
return
true
;
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment