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
a82005a9
Unverified
Commit
a82005a9
authored
Jul 08, 2020
by
Justin McCandless
Committed by
GitHub
Jul 08, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
InteractiveViewer pan axis locking (#61019)
parent
df64d1b3
Changes
2
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
221 additions
and
3 deletions
+221
-3
interactive_viewer.dart
packages/flutter/lib/src/widgets/interactive_viewer.dart
+49
-3
interactive_viewer_test.dart
packages/flutter/test/widgets/interactive_viewer_test.dart
+172
-0
No files found.
packages/flutter/lib/src/widgets/interactive_viewer.dart
View file @
a82005a9
...
...
@@ -53,6 +53,7 @@ class InteractiveViewer extends StatefulWidget {
/// The [child] parameter must not be null.
InteractiveViewer
({
Key
key
,
this
.
alignPanAxis
=
false
,
this
.
boundaryMargin
=
EdgeInsets
.
zero
,
this
.
constrained
=
true
,
// These default scale values were eyeballed as reasonable limits for common
...
...
@@ -66,7 +67,8 @@ class InteractiveViewer extends StatefulWidget {
this
.
scaleEnabled
=
true
,
this
.
transformationController
,
@required
this
.
child
,
})
:
assert
(
child
!=
null
),
})
:
assert
(
alignPanAxis
!=
null
),
assert
(
child
!=
null
),
assert
(
constrained
!=
null
),
assert
(
minScale
!=
null
),
assert
(
minScale
>
0
),
...
...
@@ -85,6 +87,15 @@ class InteractiveViewer extends StatefulWidget {
&&
boundaryMargin
.
left
.
isFinite
)),
super
(
key:
key
);
/// If true, panning is only allowed in the direction of the horizontal axis
/// or the vertical axis.
///
/// In other words, when this is true, diagonal panning is not allowed. A
/// single gesture begun along one axis cannot also cause panning along the
/// other axis without stopping and beginning a new gesture. This is a common
/// pattern in tables where data is displayed in columns and rows.
final
bool
alignPanAxis
;
/// A margin for the visible boundaries of the child.
///
/// Any transformation that results in the viewport being able to view outside
...
...
@@ -477,6 +488,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
final
GlobalKey
_parentKey
=
GlobalKey
();
Animation
<
Offset
>
_animation
;
AnimationController
_controller
;
Axis
_panAxis
;
// Used with alignPanAxis.
Offset
_referenceFocalPoint
;
// Point where the current gesture began.
double
_scaleStart
;
// Scale value at start of scaling gesture.
double
_rotationStart
=
0.0
;
// Rotation at start of rotation gesture.
...
...
@@ -528,9 +540,13 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
return
matrix
.
clone
();
}
final
Offset
alignedTranslation
=
widget
.
alignPanAxis
&&
_panAxis
!=
null
?
_alignAxis
(
translation
,
_panAxis
)
:
translation
;
final
Matrix4
nextMatrix
=
matrix
.
clone
()..
translate
(
t
ranslation
.
dx
,
t
ranslation
.
dy
,
alignedT
ranslation
.
dx
,
alignedT
ranslation
.
dy
,
);
// Transform the viewport to determine where its four corners will be after
...
...
@@ -683,6 +699,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
}
_gestureType
=
null
;
_panAxis
=
null
;
_scaleStart
=
_transformationController
.
value
.
getMaxScaleOnAxis
();
_referenceFocalPoint
=
_transformationController
.
toScene
(
details
.
localFocalPoint
,
...
...
@@ -710,6 +727,9 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
!
widget
.
scaleEnabled
?
1.0
:
details
.
scale
,
!
_rotateEnabled
?
0.0
:
details
.
rotation
,
);
if
(
_gestureType
==
_GestureType
.
pan
)
{
_panAxis
??=
_getPanAxis
(
_referenceFocalPoint
,
focalPointScene
);
}
if
(!
_gestureIsSupported
(
_gestureType
))
{
return
;
...
...
@@ -800,11 +820,13 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
_controller
.
reset
();
if
(!
_gestureIsSupported
(
_gestureType
))
{
_panAxis
=
null
;
return
;
}
// If the scale ended with enough velocity, animate inertial movement.
if
(
_gestureType
!=
_GestureType
.
pan
||
details
.
velocity
.
pixelsPerSecond
.
distance
<
kMinFlingVelocity
)
{
_panAxis
=
null
;
return
;
}
...
...
@@ -871,6 +893,7 @@ class _InteractiveViewerState extends State<InteractiveViewer> with TickerProvid
// Handle inertia drag animation.
void
_onAnimate
()
{
if
(!
_controller
.
isAnimating
)
{
_panAxis
=
null
;
_animation
?.
removeListener
(
_onAnimate
);
_animation
=
null
;
_controller
.
reset
();
...
...
@@ -1158,3 +1181,26 @@ Offset _round(Offset offset) {
double
.
parse
(
offset
.
dy
.
toStringAsFixed
(
9
)),
);
}
// Align the given offset to the given axis by allowing movement only in the
// axis direction.
Offset
_alignAxis
(
Offset
offset
,
Axis
axis
)
{
switch
(
axis
)
{
case
Axis
.
horizontal
:
return
Offset
(
offset
.
dx
,
0.0
);
case
Axis
.
vertical
:
default
:
return
Offset
(
0.0
,
offset
.
dy
);
}
}
// Given two points, return the axis where the distance between the points is
// greatest. If they are equal, return null.
Axis
_getPanAxis
(
Offset
point1
,
Offset
point2
)
{
if
(
point1
==
point2
)
{
return
null
;
}
final
double
x
=
point2
.
dx
-
point1
.
dx
;
final
double
y
=
point2
.
dy
-
point1
.
dy
;
return
x
.
abs
()
>
y
.
abs
()
?
Axis
.
horizontal
:
Axis
.
vertical
;
}
packages/flutter/test/widgets/interactive_viewer_test.dart
View file @
a82005a9
...
...
@@ -260,6 +260,86 @@ void main() {
expect
(
transformationController
.
value
.
getMaxScaleOnAxis
(),
minScale
);
});
testWidgets
(
'alignPanAxis allows panning in one direction only for diagonal gesture'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
alignPanAxis:
true
,
boundaryMargin:
const
EdgeInsets
.
all
(
double
.
infinity
),
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Perform a diagonal drag gesture.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
20.0
,
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
childInterior
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
// Translation has only happened along the y axis (the default axis when
// a gesture is perfectly at 45 degrees to the axes).
final
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
0.0
);
expect
(
translation
.
y
,
childOffset
.
dy
-
childInterior
.
dy
);
});
testWidgets
(
'alignPanAxis allows panning in one direction only for horizontal leaning gesture'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
alignPanAxis:
true
,
boundaryMargin:
const
EdgeInsets
.
all
(
double
.
infinity
),
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
expect
(
transformationController
.
value
,
equals
(
Matrix4
.
identity
()));
// Perform a horizontally leaning diagonal drag gesture.
final
Offset
childOffset
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
final
Offset
childInterior
=
Offset
(
childOffset
.
dx
+
20.0
,
childOffset
.
dy
+
10.0
,
);
final
TestGesture
gesture
=
await
tester
.
startGesture
(
childInterior
);
addTearDown
(
gesture
.
removePointer
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
childOffset
);
await
tester
.
pump
();
await
gesture
.
up
();
await
tester
.
pumpAndSettle
();
// Translation happened only along the x axis because that's the axis that
// had the greatest movement.
final
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
childOffset
.
dx
-
childInterior
.
dx
);
expect
(
translation
.
y
,
0.0
);
});
testWidgets
(
'inertia fling and boundary sliding'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
boundaryMargin
=
50.0
;
...
...
@@ -406,6 +486,98 @@ void main() {
expect
(
newSceneFocalPoint
.
dy
,
closeTo
(
sceneFocalPoint
.
dy
,
1.0
));
});
testWidgets
(
'Scaling automatically causes a centering translation even when alignPanAxis is set'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
const
double
boundaryMargin
=
50.0
;
const
double
minScale
=
0.1
;
await
tester
.
pumpWidget
(
MaterialApp
(
home:
Scaffold
(
body:
Center
(
child:
InteractiveViewer
(
alignPanAxis:
true
,
boundaryMargin:
const
EdgeInsets
.
all
(
boundaryMargin
),
minScale:
minScale
,
transformationController:
transformationController
,
child:
Container
(
width:
200.0
,
height:
200.0
),
),
),
),
),
);
Vector3
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
0.0
);
expect
(
translation
.
y
,
0.0
);
// Pan into the corner of the boundaries in two gestures, since
// alignPanAxis prevents diagonal panning.
final
Offset
childOffset1
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
const
Offset
flingEnd1
=
Offset
(
20.0
,
0.0
);
await
tester
.
flingFrom
(
childOffset1
,
flingEnd1
,
1000.0
);
await
tester
.
pumpAndSettle
();
await
tester
.
pump
(
const
Duration
(
seconds:
5
));
final
Offset
childOffset2
=
tester
.
getTopLeft
(
find
.
byType
(
Container
));
const
Offset
flingEnd2
=
Offset
(
0.0
,
15.0
);
await
tester
.
flingFrom
(
childOffset2
,
flingEnd2
,
1000.0
);
await
tester
.
pumpAndSettle
();
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
closeTo
(
boundaryMargin
,
.
000000001
));
expect
(
translation
.
y
,
closeTo
(
boundaryMargin
,
.
000000001
));
// Zoom out so the entire child is visible. The child will also be
// translated in order to keep it inside the boundaries.
final
Offset
childCenter
=
tester
.
getCenter
(
find
.
byType
(
Container
));
Offset
scaleStart1
=
Offset
(
childCenter
.
dx
-
40.0
,
childCenter
.
dy
);
Offset
scaleStart2
=
Offset
(
childCenter
.
dx
+
40.0
,
childCenter
.
dy
);
Offset
scaleEnd1
=
Offset
(
childCenter
.
dx
-
10.0
,
childCenter
.
dy
);
Offset
scaleEnd2
=
Offset
(
childCenter
.
dx
+
10.0
,
childCenter
.
dy
);
TestGesture
gesture
=
await
tester
.
createGesture
();
TestGesture
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
expect
(
transformationController
.
value
.
getMaxScaleOnAxis
(),
lessThan
(
1.0
));
translation
=
transformationController
.
value
.
getTranslation
();
expect
(
translation
.
x
,
lessThan
(
boundaryMargin
));
expect
(
translation
.
y
,
lessThan
(
boundaryMargin
));
expect
(
translation
.
x
,
greaterThan
(
0.0
));
expect
(
translation
.
y
,
greaterThan
(
0.0
));
expect
(
translation
.
x
,
closeTo
(
translation
.
y
,
.
000000001
));
// Zoom in on a point that's not the center, and see that it remains at
// roughly the same location in the viewport after the zoom.
scaleStart1
=
Offset
(
childCenter
.
dx
-
50.0
,
childCenter
.
dy
);
scaleStart2
=
Offset
(
childCenter
.
dx
-
30.0
,
childCenter
.
dy
);
scaleEnd1
=
Offset
(
childCenter
.
dx
-
51.0
,
childCenter
.
dy
);
scaleEnd2
=
Offset
(
childCenter
.
dx
-
29.0
,
childCenter
.
dy
);
final
Offset
viewportFocalPoint
=
Offset
(
childCenter
.
dx
-
40.0
-
childOffset1
.
dx
,
childCenter
.
dy
-
childOffset1
.
dy
,
);
final
Offset
sceneFocalPoint
=
transformationController
.
toScene
(
viewportFocalPoint
);
gesture
=
await
tester
.
createGesture
();
gesture2
=
await
tester
.
createGesture
();
await
gesture
.
down
(
scaleStart1
);
await
gesture2
.
down
(
scaleStart2
);
await
tester
.
pump
();
await
gesture
.
moveTo
(
scaleEnd1
);
await
gesture2
.
moveTo
(
scaleEnd2
);
await
tester
.
pump
();
await
gesture
.
up
();
await
gesture2
.
up
();
await
tester
.
pumpAndSettle
();
final
Offset
newSceneFocalPoint
=
transformationController
.
toScene
(
viewportFocalPoint
);
expect
(
newSceneFocalPoint
.
dx
,
closeTo
(
sceneFocalPoint
.
dx
,
1.0
));
expect
(
newSceneFocalPoint
.
dy
,
closeTo
(
sceneFocalPoint
.
dy
,
1.0
));
});
testWidgets
(
'Can scale with mouse'
,
(
WidgetTester
tester
)
async
{
final
TransformationController
transformationController
=
TransformationController
();
await
tester
.
pumpWidget
(
...
...
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