1// Copyright 2019 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5import 'package:flutter/foundation.dart'; 6import 'package:flutter/painting.dart'; 7 8import 'basic.dart'; 9import 'binding.dart'; 10import 'focus_manager.dart'; 11import 'framework.dart'; 12 13/// A direction along either the horizontal or vertical axes. 14/// 15/// This is used by the [DirectionalFocusTraversalPolicyMixin] to indicate which 16/// direction to traverse in. 17enum TraversalDirection { 18 /// Indicates a direction above the currently focused widget. 19 up, 20 21 /// Indicates a direction to the right of the currently focused widget. 22 /// 23 /// This direction is unaffected by the [Directionality] of the current 24 /// context. 25 right, 26 27 /// Indicates a direction below the currently focused widget. 28 down, 29 30 /// Indicates a direction to the left of the currently focused widget. 31 /// 32 /// This direction is unaffected by the [Directionality] of the current 33 /// context. 34 left, 35 36 // TODO(gspencer): Add diagonal traversal directions used by TV remotes and 37 // game controllers when we support them. 38} 39 40/// An object used to specify a focus traversal policy used for configuring a 41/// [DefaultFocusTraversal] widget. 42/// 43/// The focus traversal policy is what determines which widget is "next", 44/// "previous", or in a direction from the currently focused [FocusNode]. 45/// 46/// One of the pre-defined subclasses may be used, or define a custom policy to 47/// create a unique focus order. 48/// 49/// See also: 50/// 51/// * [FocusNode], for a description of the focus system. 52/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the 53/// [Focus] nodes below it in the widget hierarchy. 54/// * [FocusNode], which is affected by the traversal policy. 55/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget 56/// creation order to describe the order of traversal. 57/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the 58/// natural "reading order" for the current [Directionality]. 59/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements 60/// focus traversal in a direction. 61abstract class FocusTraversalPolicy { 62 /// Returns the node that should receive focus if there is no current focus 63 /// in the [FocusScopeNode] that [currentNode] belongs to. 64 /// 65 /// This is used by [next]/[previous]/[inDirection] to determine which node to 66 /// focus if they are called, but no node is currently focused. 67 /// 68 /// It is also used by the [FocusManager] to know which node to focus 69 /// initially if no nodes are focused. 70 /// 71 /// If the [direction] is null, then it should find the appropriate first node 72 /// for next/previous, and if direction is non-null, should find the 73 /// appropriate first node in that direction. 74 /// 75 /// The [currentNode] argument must not be null. 76 FocusNode findFirstFocus(FocusNode currentNode); 77 78 /// Returns the node in the given [direction] that should receive focus if 79 /// there is no current focus in the scope to which the [currentNode] belongs. 80 /// 81 /// This is typically used by [inDirection] to determine which node to focus 82 /// if it is called, but no node is currently focused. 83 /// 84 /// All arguments must not be null. 85 FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction); 86 87 /// Clears the data associated with the given [FocusScopeNode] for this object. 88 /// 89 /// This is used to indicate that the focus policy has changed its mode, and 90 /// so any cached policy data should be invalidated. For example, changing the 91 /// direction in which focus is moving, or changing from directional to 92 /// next/previous navigation modes. 93 /// 94 /// The default implementation does nothing. 95 @mustCallSuper 96 @protected 97 void invalidateScopeData(FocusScopeNode node) {} 98 99 /// This is called whenever the given [node] is reparented into a new scope, 100 /// so that the policy has a chance to update or invalidate any cached data 101 /// that it maintains per scope about the node. 102 /// 103 /// The [oldScope] is the previous scope that this node belonged to, if any. 104 /// 105 /// The default implementation does nothing. 106 @mustCallSuper 107 void changedScope({FocusNode node, FocusScopeNode oldScope}) {} 108 109 /// Focuses the next widget in the focus scope that contains the given 110 /// [currentNode]. 111 /// 112 /// This should determine what the next node to receive focus should be by 113 /// inspecting the node tree, and then calling [FocusNode.requestFocus] on 114 /// the node that has been selected. 115 /// 116 /// Returns true if it successfully found a node and requested focus. 117 /// 118 /// The [currentNode] argument must not be null. 119 bool next(FocusNode currentNode); 120 121 /// Focuses the previous widget in the focus scope that contains the given 122 /// [currentNode]. 123 /// 124 /// This should determine what the previous node to receive focus should be by 125 /// inspecting the node tree, and then calling [FocusNode.requestFocus] on 126 /// the node that has been selected. 127 /// 128 /// Returns true if it successfully found a node and requested focus. 129 /// 130 /// The [currentNode] argument must not be null. 131 bool previous(FocusNode currentNode); 132 133 /// Focuses the next widget in the given [direction] in the focus scope that 134 /// contains the given [currentNode]. 135 /// 136 /// This should determine what the next node to receive focus in the given 137 /// [direction] should be by inspecting the node tree, and then calling 138 /// [FocusNode.requestFocus] on the node that has been selected. 139 /// 140 /// Returns true if it successfully found a node and requested focus. 141 /// 142 /// All arguments must not be null. 143 bool inDirection(FocusNode currentNode, TraversalDirection direction); 144} 145 146/// A policy data object for use by the [DirectionalFocusTraversalPolicyMixin] 147class _DirectionalPolicyDataEntry { 148 const _DirectionalPolicyDataEntry({@required this.direction, @required this.node}) 149 : assert(direction != null), 150 assert(node != null); 151 152 final TraversalDirection direction; 153 final FocusNode node; 154} 155 156class _DirectionalPolicyData { 157 const _DirectionalPolicyData({@required this.history}) : assert(history != null); 158 159 /// A queue of entries that describe the path taken to the current node. 160 final List<_DirectionalPolicyDataEntry> history; 161} 162 163/// A mixin class that provides an implementation for finding a node in a 164/// particular direction. 165/// 166/// This can be mixed in to other [FocusTraversalPolicy] implementations that 167/// only want to implement new next/previous policies. 168/// 169/// Since hysteresis in the navigation order is undesirable, this implementation 170/// maintains a stack of previous locations that have been visited on the 171/// [policyData] for the affected [FocusScopeNode]. If the previous direction 172/// was the opposite of the current direction, then the this policy will request 173/// focus on the previously focused node. Change to another direction other than 174/// the current one or its opposite will clear the stack. 175/// 176/// For instance, if the focus moves down, down, down, and then up, up, up, it 177/// will follow the same path through the widgets in both directions. However, 178/// if it moves down, down, down, left, right, and then up, up, up, it may not 179/// follow the same path on the way up as it did on the way down. 180/// 181/// See also: 182/// 183/// * [FocusNode], for a description of the focus system. 184/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the 185/// [Focus] nodes below it in the widget hierarchy. 186/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget 187/// creation order to describe the order of traversal. 188/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the 189/// natural "reading order" for the current [Directionality]. 190mixin DirectionalFocusTraversalPolicyMixin on FocusTraversalPolicy { 191 final Map<FocusScopeNode, _DirectionalPolicyData> _policyData = <FocusScopeNode, _DirectionalPolicyData>{}; 192 193 @override 194 void invalidateScopeData(FocusScopeNode node) { 195 super.invalidateScopeData(node); 196 _policyData.remove(node); 197 } 198 199 @override 200 void changedScope({FocusNode node, FocusScopeNode oldScope}) { 201 super.changedScope(node: node, oldScope: oldScope); 202 if (oldScope != null) { 203 _policyData[oldScope]?.history?.removeWhere((_DirectionalPolicyDataEntry entry) { 204 return entry.node == node; 205 }); 206 } 207 } 208 209 @override 210 FocusNode findFirstFocusInDirection(FocusNode currentNode, TraversalDirection direction) { 211 assert(direction != null); 212 assert(currentNode != null); 213 switch (direction) { 214 case TraversalDirection.up: 215 // Find the bottom-most node so we can go up from there. 216 return _sortAndFindInitial(currentNode, vertical: true, first: false); 217 case TraversalDirection.down: 218 // Find the top-most node so we can go down from there. 219 return _sortAndFindInitial(currentNode, vertical: true, first: true); 220 case TraversalDirection.left: 221 // Find the right-most node so we can go left from there. 222 return _sortAndFindInitial(currentNode, vertical: false, first: false); 223 case TraversalDirection.right: 224 // Find the left-most node so we can go right from there. 225 return _sortAndFindInitial(currentNode, vertical: false, first: true); 226 } 227 return null; 228 } 229 230 FocusNode _sortAndFindInitial(FocusNode currentNode, { bool vertical, bool first }) { 231 final Iterable<FocusNode> nodes = currentNode.nearestScope.traversalDescendants; 232 final List<FocusNode> sorted = nodes.toList(); 233 sorted.sort((FocusNode a, FocusNode b) { 234 if (vertical) { 235 if (first) { 236 return a.rect.top.compareTo(b.rect.top); 237 } else { 238 return b.rect.bottom.compareTo(a.rect.bottom); 239 } 240 } else { 241 if (first) { 242 return a.rect.left.compareTo(b.rect.left); 243 } else { 244 return b.rect.right.compareTo(a.rect.right); 245 } 246 } 247 }); 248 return sorted.first; 249 } 250 251 // Sorts nodes from left to right horizontally, and removes nodes that are 252 // either to the right of the left side of the target node if we're going 253 // left, or to the left of the right side of the target node if we're going 254 // right. 255 // 256 // This doesn't need to take into account directionality because it is 257 // typically intending to actually go left or right, not in a reading 258 // direction. 259 Iterable<FocusNode> _sortAndFilterHorizontally( 260 TraversalDirection direction, 261 Rect target, 262 FocusNode nearestScope, 263 ) { 264 assert(direction == TraversalDirection.left || direction == TraversalDirection.right); 265 final Iterable<FocusNode> nodes = nearestScope.traversalDescendants; 266 assert(!nodes.contains(nearestScope)); 267 final List<FocusNode> sorted = nodes.toList(); 268 sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dx.compareTo(b.rect.center.dx)); 269 Iterable<FocusNode> result; 270 switch (direction) { 271 case TraversalDirection.left: 272 result = sorted.where((FocusNode node) => node.rect != target && node.rect.center.dx <= target.left); 273 break; 274 case TraversalDirection.right: 275 result = sorted.where((FocusNode node) => node.rect != target && node.rect.center.dx >= target.right); 276 break; 277 case TraversalDirection.up: 278 case TraversalDirection.down: 279 break; 280 } 281 return result; 282 } 283 284 // Sorts nodes from top to bottom vertically, and removes nodes that are 285 // either below the top of the target node if we're going up, or above the 286 // bottom of the target node if we're going down. 287 Iterable<FocusNode> _sortAndFilterVertically( 288 TraversalDirection direction, 289 Rect target, 290 Iterable<FocusNode> nodes, 291 ) { 292 final List<FocusNode> sorted = nodes.toList(); 293 sorted.sort((FocusNode a, FocusNode b) => a.rect.center.dy.compareTo(b.rect.center.dy)); 294 switch (direction) { 295 case TraversalDirection.up: 296 return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy <= target.top); 297 case TraversalDirection.down: 298 return sorted.where((FocusNode node) => node.rect != target && node.rect.center.dy >= target.bottom); 299 case TraversalDirection.left: 300 case TraversalDirection.right: 301 break; 302 } 303 assert(direction == TraversalDirection.up || direction == TraversalDirection.down); 304 return null; 305 } 306 307 // Updates the policy data to keep the previously visited node so that we can 308 // avoid hysteresis when we change directions in navigation. 309 // 310 // Returns true if focus was requested on a previous node. 311 bool _popPolicyDataIfNeeded(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) { 312 final _DirectionalPolicyData policyData = _policyData[nearestScope]; 313 if (policyData != null && policyData.history.isNotEmpty && policyData.history.first.direction != direction) { 314 if (policyData.history.last.node.parent == null) { 315 // If a node has been removed from the tree, then we should stop 316 // referencing it and reset the scope data so that we don't try and 317 // request focus on it. This can happen in slivers where the rendered node 318 // has been unmounted. This has the side effect that hysteresis might not 319 // be avoided when items that go off screen get unmounted. 320 invalidateScopeData(nearestScope); 321 return false; 322 } 323 switch (direction) { 324 case TraversalDirection.down: 325 case TraversalDirection.up: 326 switch (policyData.history.first.direction) { 327 case TraversalDirection.left: 328 case TraversalDirection.right: 329 // Reset the policy data if we change directions. 330 invalidateScopeData(nearestScope); 331 break; 332 case TraversalDirection.up: 333 case TraversalDirection.down: 334 policyData.history.removeLast().node.requestFocus(); 335 return true; 336 } 337 break; 338 case TraversalDirection.left: 339 case TraversalDirection.right: 340 switch (policyData.history.first.direction) { 341 case TraversalDirection.left: 342 case TraversalDirection.right: 343 policyData.history.removeLast().node.requestFocus(); 344 return true; 345 case TraversalDirection.up: 346 case TraversalDirection.down: 347 // Reset the policy data if we change directions. 348 invalidateScopeData(nearestScope); 349 break; 350 } 351 } 352 } 353 if (policyData != null && policyData.history.isEmpty) { 354 // Reset the policy data if we change directions. 355 invalidateScopeData(nearestScope); 356 } 357 return false; 358 } 359 360 void _pushPolicyData(TraversalDirection direction, FocusScopeNode nearestScope, FocusNode focusedChild) { 361 final _DirectionalPolicyData policyData = _policyData[nearestScope]; 362 if (policyData != null && policyData is! _DirectionalPolicyData) { 363 return; 364 } 365 final _DirectionalPolicyDataEntry newEntry = _DirectionalPolicyDataEntry(node: focusedChild, direction: direction); 366 if (policyData != null) { 367 policyData.history.add(newEntry); 368 } else { 369 _policyData[nearestScope] = _DirectionalPolicyData(history: <_DirectionalPolicyDataEntry>[newEntry]); 370 } 371 } 372 373 /// Focuses the next widget in the given [direction] in the [FocusScope] that 374 /// contains the [currentNode]. 375 /// 376 /// This determines what the next node to receive focus in the given 377 /// [direction] will be by inspecting the node tree, and then calling 378 /// [FocusNode.requestFocus] on it. 379 /// 380 /// Returns true if it successfully found a node and requested focus. 381 /// 382 /// Maintains a stack of previous locations that have been visited on the 383 /// [policyData] for the affected [FocusScopeNode]. If the previous direction 384 /// was the opposite of the current direction, then the this policy will 385 /// request focus on the previously focused node. Change to another direction 386 /// other than the current one or its opposite will clear the stack. 387 /// 388 /// If this function returns true when called by a subclass, then the subclass 389 /// should return true and not request focus from any node. 390 @mustCallSuper 391 @override 392 bool inDirection(FocusNode currentNode, TraversalDirection direction) { 393 final FocusScopeNode nearestScope = currentNode.nearestScope; 394 final FocusNode focusedChild = nearestScope.focusedChild; 395 if (focusedChild == null) { 396 final FocusNode firstFocus = findFirstFocusInDirection(currentNode, direction); 397 (firstFocus ?? currentNode).requestFocus(); 398 return true; 399 } 400 if (_popPolicyDataIfNeeded(direction, nearestScope, focusedChild)) { 401 return true; 402 } 403 FocusNode found; 404 switch (direction) { 405 case TraversalDirection.down: 406 case TraversalDirection.up: 407 final Iterable<FocusNode> eligibleNodes = _sortAndFilterVertically( 408 direction, 409 focusedChild.rect, 410 nearestScope.traversalDescendants, 411 ); 412 if (eligibleNodes.isEmpty) { 413 break; 414 } 415 List<FocusNode> sorted = eligibleNodes.toList(); 416 if (direction == TraversalDirection.up) { 417 sorted = sorted.reversed.toList(); 418 } 419 // Find any nodes that intersect the band of the focused child. 420 final Rect band = Rect.fromLTRB(focusedChild.rect.left, -double.infinity, focusedChild.rect.right, double.infinity); 421 final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty); 422 if (inBand.isNotEmpty) { 423 // The inBand list is already sorted by horizontal distance, so pick the closest one. 424 found = inBand.first; 425 break; 426 } 427 // Only out-of-band targets remain, so pick the one that is closest the to the center line horizontally. 428 sorted.sort((FocusNode a, FocusNode b) { 429 return (a.rect.center.dx - focusedChild.rect.center.dx).abs().compareTo((b.rect.center.dx - focusedChild.rect.center.dx).abs()); 430 }); 431 found = sorted.first; 432 break; 433 case TraversalDirection.right: 434 case TraversalDirection.left: 435 final Iterable<FocusNode> eligibleNodes = _sortAndFilterHorizontally(direction, focusedChild.rect, nearestScope); 436 if (eligibleNodes.isEmpty) { 437 break; 438 } 439 List<FocusNode> sorted = eligibleNodes.toList(); 440 if (direction == TraversalDirection.left) { 441 sorted = sorted.reversed.toList(); 442 } 443 // Find any nodes that intersect the band of the focused child. 444 final Rect band = Rect.fromLTRB(-double.infinity, focusedChild.rect.top, double.infinity, focusedChild.rect.bottom); 445 final Iterable<FocusNode> inBand = sorted.where((FocusNode node) => !node.rect.intersect(band).isEmpty); 446 if (inBand.isNotEmpty) { 447 // The inBand list is already sorted by vertical distance, so pick the closest one. 448 found = inBand.first; 449 break; 450 } 451 // Only out-of-band targets remain, so pick the one that is closest the to the center line vertically. 452 sorted.sort((FocusNode a, FocusNode b) { 453 return (a.rect.center.dy - focusedChild.rect.center.dy).abs().compareTo((b.rect.center.dy - focusedChild.rect.center.dy).abs()); 454 }); 455 found = sorted.first; 456 break; 457 } 458 if (found != null) { 459 _pushPolicyData(direction, nearestScope, focusedChild); 460 found.requestFocus(); 461 return true; 462 } 463 return false; 464 } 465} 466 467/// A [FocusTraversalPolicy] that traverses the focus order in widget hierarchy 468/// order. 469/// 470/// This policy is used when the order desired is the order in which widgets are 471/// created in the widget hierarchy. 472/// 473/// See also: 474/// 475/// * [FocusNode], for a description of the focus system. 476/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the 477/// [Focus] nodes below it in the widget hierarchy. 478/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the 479/// natural "reading order" for the current [Directionality]. 480/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements 481/// focus traversal in a direction. 482class WidgetOrderFocusTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { 483 /// Creates a const [WidgetOrderFocusTraversalPolicy]. 484 WidgetOrderFocusTraversalPolicy(); 485 486 @override 487 FocusNode findFirstFocus(FocusNode currentNode) { 488 assert(currentNode != null); 489 final FocusScopeNode scope = currentNode.nearestScope; 490 // Start with the candidate focus as the focused child of this scope, if 491 // there is one. Otherwise start with this node itself. Keep going down 492 // through scopes until an ultimately focusable item is found, a scope 493 // doesn't have a focusedChild, or a non-scope is encountered. 494 FocusNode candidate = scope.focusedChild; 495 if (candidate == null) { 496 if (scope.traversalChildren.isNotEmpty) { 497 candidate = scope.traversalChildren.first; 498 } else { 499 candidate = currentNode; 500 } 501 } 502 while (candidate is FocusScopeNode && candidate.focusedChild != null) { 503 final FocusScopeNode candidateScope = candidate; 504 candidate = candidateScope.focusedChild; 505 } 506 return candidate; 507 } 508 509 // Moves the focus to the next or previous node, depending on whether forward 510 // is true or not. 511 bool _move(FocusNode currentNode, {@required bool forward}) { 512 if (currentNode == null) { 513 return false; 514 } 515 final FocusScopeNode nearestScope = currentNode.nearestScope; 516 invalidateScopeData(nearestScope); 517 final FocusNode focusedChild = nearestScope.focusedChild; 518 if (focusedChild == null) { 519 final FocusNode firstFocus = findFirstFocus(currentNode); 520 if (firstFocus != null) { 521 firstFocus.requestFocus(); 522 return true; 523 } 524 } 525 FocusNode previousNode; 526 FocusNode firstNode; 527 FocusNode lastNode; 528 bool visit(FocusNode node) { 529 for (FocusNode visited in node.traversalChildren) { 530 firstNode ??= visited; 531 if (!visit(visited)) { 532 return false; 533 } 534 if (forward) { 535 if (previousNode == focusedChild) { 536 visited.requestFocus(); 537 return false; // short circuit the traversal. 538 } 539 } else { 540 if (previousNode != null && visited == focusedChild) { 541 previousNode.requestFocus(); 542 return false; // short circuit the traversal. 543 } 544 } 545 previousNode = visited; 546 lastNode = visited; 547 } 548 return true; // continue traversal 549 } 550 551 if (visit(nearestScope)) { 552 if (forward) { 553 if (firstNode != null) { 554 firstNode.requestFocus(); 555 return true; 556 } 557 } else { 558 if (lastNode != null) { 559 lastNode.requestFocus(); 560 return true; 561 } 562 } 563 return false; 564 } 565 return true; 566 } 567 568 @override 569 bool next(FocusNode currentNode) => _move(currentNode, forward: true); 570 571 @override 572 bool previous(FocusNode currentNode) => _move(currentNode, forward: false); 573} 574 575class _SortData { 576 _SortData(this.node) : rect = node.rect; 577 578 final Rect rect; 579 final FocusNode node; 580} 581 582/// Traverses the focus order in "reading order". 583/// 584/// By default, reading order traversal goes in the reading direction, and then 585/// down, using this algorithm: 586/// 587/// 1. Find the node rectangle that has the highest `top` on the screen. 588/// 2. Find any other nodes that intersect the infinite horizontal band defined 589/// by the highest rectangle's top and bottom edges. 590/// 3. Pick the closest to the beginning of the reading order from among the 591/// nodes discovered above. 592/// 593/// It uses the ambient directionality in the context for the enclosing scope to 594/// determine which direction is "reading order". 595/// 596/// See also: 597/// 598/// * [FocusNode], for a description of the focus system. 599/// * [DefaultFocusTraversal], a widget that imposes a traversal policy on the 600/// [Focus] nodes below it in the widget hierarchy. 601/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget 602/// creation order to describe the order of traversal. 603/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements 604/// focus traversal in a direction. 605class ReadingOrderTraversalPolicy extends FocusTraversalPolicy with DirectionalFocusTraversalPolicyMixin { 606 @override 607 FocusNode findFirstFocus(FocusNode currentNode) { 608 assert(currentNode != null); 609 final FocusScopeNode scope = currentNode.nearestScope; 610 FocusNode candidate = scope.focusedChild; 611 if (candidate == null && scope.traversalChildren.isNotEmpty) { 612 candidate = _sortByGeometry(scope).first; 613 } 614 615 // If we still didn't find any candidate, use the current node as a 616 // fallback. 617 candidate ??= currentNode; 618 candidate ??= WidgetsBinding.instance.focusManager.rootScope; 619 return candidate; 620 } 621 622 // Sorts the list of nodes based on their geometry into the desired reading 623 // order based on the directionality of the context for each node. 624 Iterable<FocusNode> _sortByGeometry(FocusScopeNode scope) { 625 final Iterable<FocusNode> nodes = scope.traversalDescendants; 626 if (nodes.length <= 1) { 627 return nodes; 628 } 629 630 Iterable<_SortData> inBand(_SortData current, Iterable<_SortData> candidates) { 631 final Rect wide = Rect.fromLTRB(double.negativeInfinity, current.rect.top, double.infinity, current.rect.bottom); 632 return candidates.where((_SortData item) { 633 return !item.rect.intersect(wide).isEmpty; 634 }); 635 } 636 637 final TextDirection textDirection = scope.context == null ? TextDirection.ltr : Directionality.of(scope.context); 638 _SortData pickFirst(List<_SortData> candidates) { 639 int compareBeginningSide(_SortData a, _SortData b) { 640 return textDirection == TextDirection.ltr ? a.rect.left.compareTo(b.rect.left) : -a.rect.right.compareTo(b.rect.right); 641 } 642 643 int compareTopSide(_SortData a, _SortData b) { 644 return a.rect.top.compareTo(b.rect.top); 645 } 646 647 // Get the topmost 648 candidates.sort(compareTopSide); 649 final _SortData topmost = candidates.first; 650 // If there are any others in the band of the topmost, then pick the 651 // leftmost one. 652 final List<_SortData> inBandOfTop = inBand(topmost, candidates).toList(); 653 inBandOfTop.sort(compareBeginningSide); 654 if (inBandOfTop.isNotEmpty) { 655 return inBandOfTop.first; 656 } 657 return topmost; 658 } 659 660 final List<_SortData> data = <_SortData>[]; 661 for (FocusNode node in nodes) { 662 data.add(_SortData(node)); 663 } 664 665 // Pick the initial widget as the one that is leftmost in the band of the 666 // topmost, or the topmost, if there are no others in its band. 667 final List<_SortData> sortedList = <_SortData>[]; 668 final List<_SortData> unplaced = data.toList(); 669 _SortData current = pickFirst(unplaced); 670 sortedList.add(current); 671 unplaced.remove(current); 672 673 while (unplaced.isNotEmpty) { 674 final _SortData next = pickFirst(unplaced); 675 current = next; 676 sortedList.add(current); 677 unplaced.remove(current); 678 } 679 return sortedList.map((_SortData item) => item.node); 680 } 681 682 // Moves the focus forward or backward in reading order, depending on the 683 // value of the forward argument. 684 bool _move(FocusNode currentNode, {@required bool forward}) { 685 final FocusScopeNode nearestScope = currentNode.nearestScope; 686 invalidateScopeData(nearestScope); 687 final FocusNode focusedChild = nearestScope.focusedChild; 688 if (focusedChild == null) { 689 final FocusNode firstFocus = findFirstFocus(currentNode); 690 if (firstFocus != null) { 691 firstFocus.requestFocus(); 692 return true; 693 } 694 } 695 final List<FocusNode> sortedNodes = _sortByGeometry(nearestScope).toList(); 696 if (forward && focusedChild == sortedNodes.last) { 697 sortedNodes.first.requestFocus(); 698 return true; 699 } 700 if (!forward && focusedChild == sortedNodes.first) { 701 sortedNodes.last.requestFocus(); 702 return true; 703 } 704 705 final Iterable<FocusNode> maybeFlipped = forward ? sortedNodes : sortedNodes.reversed; 706 FocusNode previousNode; 707 for (FocusNode node in maybeFlipped) { 708 if (previousNode == focusedChild) { 709 node.requestFocus(); 710 return true; 711 } 712 previousNode = node; 713 } 714 return false; 715 } 716 717 @override 718 bool next(FocusNode currentNode) => _move(currentNode, forward: true); 719 720 @override 721 bool previous(FocusNode currentNode) => _move(currentNode, forward: false); 722} 723 724/// A widget that describes the inherited focus policy for focus traversal. 725/// 726/// By default, traverses in widget order using 727/// [ReadingOrderFocusTraversalPolicy]. 728/// 729/// See also: 730/// 731/// * [FocusNode], for a description of the focus system. 732/// * [WidgetOrderFocusTraversalPolicy], a policy that relies on the widget 733/// creation order to describe the order of traversal. 734/// * [ReadingOrderTraversalPolicy], a policy that describes the order as the 735/// natural "reading order" for the current [Directionality]. 736/// * [DirectionalFocusTraversalPolicyMixin] a mixin class that implements 737/// focus traversal in a direction. 738class DefaultFocusTraversal extends InheritedWidget { 739 /// Creates a [DefaultFocusTraversal] object. 740 /// 741 /// The [child] argument must not be null. 742 const DefaultFocusTraversal({ 743 Key key, 744 this.policy, 745 @required Widget child, 746 }) : super(key: key, child: child); 747 748 /// The policy used to move the focus from one focus node to another. 749 /// 750 /// If not specified, traverses in reading order using 751 /// [ReadingOrderTraversalPolicy]. 752 /// 753 /// See also: 754 /// 755 /// * [FocusTraversalPolicy] for the API used to impose traversal order 756 /// policy. 757 /// * [WidgetOrderFocusTraversalPolicy] for a traversal policy that traverses 758 /// nodes in the order they are added to the widget tree. 759 /// * [ReadingOrderTraversalPolicy] for a traversal policy that traverses 760 /// nodes in the reading order defined in the widget tree, and then top to 761 /// bottom. 762 final FocusTraversalPolicy policy; 763 764 /// Returns the [FocusTraversalPolicy] that most tightly encloses the given 765 /// [BuildContext]. 766 /// 767 /// The [context] argument must not be null. 768 static FocusTraversalPolicy of(BuildContext context, { bool nullOk = false }) { 769 assert(context != null); 770 final DefaultFocusTraversal inherited = context.inheritFromWidgetOfExactType(DefaultFocusTraversal); 771 assert(() { 772 if (nullOk) { 773 return true; 774 } 775 if (inherited == null) { 776 throw FlutterError('Unable to find a DefaultFocusTraversal widget in the context.\n' 777 'DefaultFocusTraversal.of() was called with a context that does not contain a ' 778 'DefaultFocusTraversal.\n' 779 'No DefaultFocusTraversal ancestor could be found starting from the context that was ' 780 'passed to DefaultFocusTraversal.of(). This can happen because there is not a ' 781 'WidgetsApp or MaterialApp widget (those widgets introduce a DefaultFocusTraversal), ' 782 'or it can happen if the context comes from a widget above those widgets.\n' 783 'The context used was:\n' 784 ' $context'); 785 } 786 return true; 787 }()); 788 return inherited?.policy ?? ReadingOrderTraversalPolicy(); 789 } 790 791 @override 792 bool updateShouldNotify(DefaultFocusTraversal oldWidget) => policy != oldWidget.policy; 793} 794