Add RTL support to ListBody (#12414)

Fixes #11930
diff --git a/packages/flutter/lib/rendering.dart b/packages/flutter/lib/rendering.dart
index 3e3a809..af00b83 100644
--- a/packages/flutter/lib/rendering.dart
+++ b/packages/flutter/lib/rendering.dart
@@ -32,7 +32,6 @@
 
 export 'src/rendering/animated_size.dart';
 export 'src/rendering/binding.dart';
-export 'src/rendering/block.dart';
 export 'src/rendering/box.dart';
 export 'src/rendering/custom_layout.dart';
 export 'src/rendering/debug.dart';
@@ -42,6 +41,7 @@
 export 'src/rendering/flow.dart';
 export 'src/rendering/image.dart';
 export 'src/rendering/layer.dart';
+export 'src/rendering/list_body.dart';
 export 'src/rendering/node.dart';
 export 'src/rendering/object.dart';
 export 'src/rendering/paragraph.dart';
diff --git a/packages/flutter/lib/src/material/mergeable_material.dart b/packages/flutter/lib/src/material/mergeable_material.dart
index 3c3fb5a..fa3a4aa 100644
--- a/packages/flutter/lib/src/material/mergeable_material.dart
+++ b/packages/flutter/lib/src/material/mergeable_material.dart
@@ -649,11 +649,15 @@
   final List<MergeableMaterialItem> items;
   final List<BoxShadow> boxShadows;
 
+  AxisDirection _getDirection(BuildContext context) {
+    return getAxisDirectionFromAxisReverseAndDirectionality(context, mainAxis, false);
+  }
+
   @override
   RenderListBody createRenderObject(BuildContext context) {
     return new _RenderMergeableMaterialListBody(
-      mainAxis: mainAxis,
-      boxShadows: boxShadows
+      axisDirection: _getDirection(context),
+      boxShadows: boxShadows,
     );
   }
 
@@ -661,7 +665,7 @@
   void updateRenderObject(BuildContext context, RenderListBody renderObject) {
     final _RenderMergeableMaterialListBody materialRenderListBody = renderObject;
     materialRenderListBody
-      ..mainAxis = mainAxis
+      ..axisDirection = _getDirection(context)
       ..boxShadows = boxShadows;
   }
 }
@@ -669,9 +673,9 @@
 class _RenderMergeableMaterialListBody extends RenderListBody {
   _RenderMergeableMaterialListBody({
     List<RenderBox> children,
-    Axis mainAxis: Axis.vertical,
+    AxisDirection axisDirection: AxisDirection.down,
     this.boxShadows
-  }) : super(children: children, mainAxis: mainAxis);
+  }) : super(children: children, axisDirection: axisDirection);
 
   List<BoxShadow> boxShadows;
 
diff --git a/packages/flutter/lib/src/painting/basic_types.dart b/packages/flutter/lib/src/painting/basic_types.dart
index aaddbd7..ee157f8 100644
--- a/packages/flutter/lib/src/painting/basic_types.dart
+++ b/packages/flutter/lib/src/painting/basic_types.dart
@@ -2,6 +2,8 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'dart:ui' show TextDirection;
+
 export 'dart:ui' show
   BlendMode,
   BlurStyle,
@@ -157,3 +159,108 @@
   /// The "start" is at the top, the "end" is at the bottom.
   down,
 }
+
+/// A direction along either the horizontal or vertical [Axis].
+enum AxisDirection {
+  /// Zero is at the bottom and positive values are above it: ⇈
+  ///
+  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
+  /// the bottom and the Z at the top. This is an unusual configuration.
+  up,
+
+  /// Zero is on the left and positive values are to the right of it: ⇉
+  ///
+  /// Alphabetical content with a [GrowthDirection.forward] would have the A on
+  /// the left and the Z on the right. This is the ordinary reading order for a
+  /// horizontal set of tabs in an English application, for example.
+  right,
+
+  /// Zero is at the top and positive values are below it: ⇊
+  ///
+  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
+  /// the top and the Z at the bottom. This is the ordinary reading order for a
+  /// vertical list.
+  down,
+
+  /// Zero is to the right and positive values are to the left of it: ⇇
+  ///
+  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
+  /// the right and the Z at the left. This is the ordinary reading order for a
+  /// horizontal set of tabs in a Hebrew application, for example.
+  left,
+}
+
+/// Returns the [Axis] that contains the given [AxisDirection].
+///
+/// Specifically, returns [Axis.vertical] for [AxisDirection.up] and
+/// [AxisDirection.down] and returns [Axis.horizontal] for [AxisDirection.left]
+/// and [AxisDirection.right].
+Axis axisDirectionToAxis(AxisDirection axisDirection) {
+  assert(axisDirection != null);
+  switch (axisDirection) {
+    case AxisDirection.up:
+    case AxisDirection.down:
+      return Axis.vertical;
+    case AxisDirection.left:
+    case AxisDirection.right:
+      return Axis.horizontal;
+  }
+  return null;
+}
+
+/// Returns the [AxisDirection] in which reading occurs in the given [TextDirection].
+///
+/// Specifically, returns [AxisDirection.left] for [TextDirection.rtl] and
+/// [AxisDirection.right] for [TextDirection.ltr].
+AxisDirection textDirectionToAxisDirection(TextDirection textDirection) {
+  assert(textDirection != null);
+  switch (textDirection) {
+    case TextDirection.rtl:
+      return AxisDirection.left;
+    case TextDirection.ltr:
+      return AxisDirection.right;
+  }
+  return null;
+}
+
+/// Returns the opposite of the given [AxisDirection].
+///
+/// Specifically, returns [AxisDirection.up] for [AxisDirection.down] (and
+/// vice versa), as well as [AxisDirection.left] for [AxisDirection.right] (and
+/// vice versa).
+///
+/// See also:
+///
+///  * [flipAxis], which does the same thing for [Axis] values.
+AxisDirection flipAxisDirection(AxisDirection axisDirection) {
+  assert(axisDirection != null);
+  switch (axisDirection) {
+    case AxisDirection.up:
+      return AxisDirection.down;
+    case AxisDirection.right:
+      return AxisDirection.left;
+    case AxisDirection.down:
+      return AxisDirection.up;
+    case AxisDirection.left:
+      return AxisDirection.right;
+  }
+  return null;
+}
+
+/// Returns whether travelling along the given axis direction visits coordinates
+/// along that axis in numerically decreasing order.
+///
+/// Specifically, returns true for [AxisDirection.up] and [AxisDirection.left]
+/// and false for [AxisDirection.down] for [AxisDirection.right].
+bool axisDirectionIsReversed(AxisDirection axisDirection) {
+  assert(axisDirection != null);
+  switch (axisDirection) {
+    case AxisDirection.up:
+    case AxisDirection.left:
+      return true;
+    case AxisDirection.down:
+    case AxisDirection.right:
+      return false;
+  }
+  return null;
+}
diff --git a/packages/flutter/lib/src/rendering/block.dart b/packages/flutter/lib/src/rendering/list_body.dart
similarity index 63%
rename from packages/flutter/lib/src/rendering/block.dart
rename to packages/flutter/lib/src/rendering/list_body.dart
index 4e13b94..cfe3cb2 100644
--- a/packages/flutter/lib/src/rendering/block.dart
+++ b/packages/flutter/lib/src/rendering/list_body.dart
@@ -31,8 +31,9 @@
   /// By default, children are arranged along the vertical axis.
   RenderListBody({
     List<RenderBox> children,
-    Axis mainAxis: Axis.vertical,
-  }) : _mainAxis = mainAxis {
+    AxisDirection axisDirection: AxisDirection.down,
+  }) : assert(axisDirection != null),
+       _axisDirection = axisDirection {
     addAll(children);
   }
 
@@ -42,41 +43,17 @@
       child.parentData = new ListBodyParentData();
   }
 
-  /// The direction to use as the main axis.
-  Axis get mainAxis => _mainAxis;
-  Axis _mainAxis;
-  set mainAxis(Axis value) {
-    if (_mainAxis != value) {
-      _mainAxis = value;
-      markNeedsLayout();
-    }
+  AxisDirection get axisDirection => _axisDirection;
+  AxisDirection _axisDirection;
+  set axisDirection(AxisDirection value) {
+    assert(value != null);
+    if (_axisDirection == value)
+      return;
+    _axisDirection = value;
+    markNeedsLayout();
   }
 
-  BoxConstraints _getInnerConstraints(BoxConstraints constraints) {
-    assert(_mainAxis != null);
-    switch (_mainAxis) {
-      case Axis.horizontal:
-        return new BoxConstraints.tightFor(height: constraints.maxHeight);
-      case Axis.vertical:
-        return new BoxConstraints.tightFor(width: constraints.maxWidth);
-    }
-    return null;
-  }
-
-  double get _mainAxisExtent {
-    final RenderBox child = lastChild;
-    if (child == null)
-      return 0.0;
-    final BoxParentData parentData = child.parentData;
-    assert(mainAxis != null);
-    switch (mainAxis) {
-      case Axis.horizontal:
-        return parentData.offset.dx + child.size.width;
-      case Axis.vertical:
-        return parentData.offset.dy + child.size.height;
-    }
-    return null;
-  }
+  Axis get mainAxis => axisDirectionToAxis(axisDirection);
 
   @override
   void performLayout() {
@@ -124,41 +101,81 @@
         'This is relatively expensive, however.' // (that's why we don't do it automatically)
       );
     }());
-    final BoxConstraints innerConstraints = _getInnerConstraints(constraints);
-    double position = 0.0;
+    double mainAxisExtent = 0.0;
     RenderBox child = firstChild;
-    while (child != null) {
-      child.layout(innerConstraints, parentUsesSize: true);
-      final ListBodyParentData childParentData = child.parentData;
-      switch (mainAxis) {
-        case Axis.horizontal:
-          childParentData.offset = new Offset(position, 0.0);
-          position += child.size.width;
-          break;
-        case Axis.vertical:
-          childParentData.offset = new Offset(0.0, position);
-          position += child.size.height;
-          break;
+    switch (axisDirection) {
+    case AxisDirection.right:
+      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(height: constraints.maxHeight);
+      while (child != null) {
+        child.layout(innerConstraints, parentUsesSize: true);
+        final ListBodyParentData childParentData = child.parentData;
+        childParentData.offset = new Offset(mainAxisExtent, 0.0);
+        mainAxisExtent += child.size.width;
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
       }
-      assert(child.parentData == childParentData);
-      child = childParentData.nextSibling;
+      size = constraints.constrain(new Size(mainAxisExtent, constraints.maxHeight));
+      break;
+    case AxisDirection.left:
+      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(height: constraints.maxHeight);
+      while (child != null) {
+        child.layout(innerConstraints, parentUsesSize: true);
+        final ListBodyParentData childParentData = child.parentData;
+        mainAxisExtent += child.size.width;
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
+      }
+      double position = 0.0;
+      child = firstChild;
+      while (child != null) {
+        final ListBodyParentData childParentData = child.parentData;
+        position += child.size.width;
+        childParentData.offset = new Offset(mainAxisExtent - position, 0.0);
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
+      }
+      size = constraints.constrain(new Size(mainAxisExtent, constraints.maxHeight));
+      break;
+    case AxisDirection.down:
+      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.maxWidth);
+      while (child != null) {
+        child.layout(innerConstraints, parentUsesSize: true);
+        final ListBodyParentData childParentData = child.parentData;
+        childParentData.offset = new Offset(0.0, mainAxisExtent);
+          mainAxisExtent += child.size.height;
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
+      }
+      size = constraints.constrain(new Size(constraints.maxWidth, mainAxisExtent));
+      break;
+    case AxisDirection.up:
+      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.maxWidth);
+      while (child != null) {
+        child.layout(innerConstraints, parentUsesSize: true);
+        final ListBodyParentData childParentData = child.parentData;
+        mainAxisExtent += child.size.height;
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
+      }
+      double position = 0.0;
+      child = firstChild;
+      while (child != null) {
+        final ListBodyParentData childParentData = child.parentData;
+        position += child.size.height;
+        childParentData.offset = new Offset(0.0, mainAxisExtent - position);
+        assert(child.parentData == childParentData);
+        child = childParentData.nextSibling;
+      }
+      size = constraints.constrain(new Size(constraints.maxWidth, mainAxisExtent));
+      break;
     }
-    switch (mainAxis) {
-      case Axis.horizontal:
-        size = constraints.constrain(new Size(_mainAxisExtent, constraints.maxHeight));
-        break;
-      case Axis.vertical:
-        size = constraints.constrain(new Size(constraints.maxWidth, _mainAxisExtent));
-        break;
-    }
-
     assert(size.isFinite);
   }
 
   @override
   void debugFillProperties(DiagnosticPropertiesBuilder description) {
     super.debugFillProperties(description);
-    description.add(new EnumProperty<Axis>('mainAxis', mainAxis));
+    description.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection));
   }
 
   double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
diff --git a/packages/flutter/lib/src/rendering/sliver.dart b/packages/flutter/lib/src/rendering/sliver.dart
index ea810c3..7588744 100644
--- a/packages/flutter/lib/src/rendering/sliver.dart
+++ b/packages/flutter/lib/src/rendering/sliver.dart
@@ -41,111 +41,6 @@
   reverse,
 }
 
-/// A direction along either the horizontal or vertical [Axis].
-enum AxisDirection {
-  /// Zero is at the bottom and positive values are above it: ⇈
-  ///
-  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
-  /// the bottom and the Z at the top. This is an unusual configuration.
-  up,
-
-  /// Zero is on the left and positive values are to the right of it: ⇉
-  ///
-  /// Alphabetical content with a [GrowthDirection.forward] would have the A on
-  /// the left and the Z on the right. This is the ordinary reading order for a
-  /// horizontal set of tabs in an English application, for example.
-  right,
-
-  /// Zero is at the top and positive values are below it: ⇊
-  ///
-  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
-  /// the top and the Z at the bottom. This is the ordinary reading order for a
-  /// vertical list.
-  down,
-
-  /// Zero is to the right and positive values are to the left of it: ⇇
-  ///
-  /// Alphabetical content with a [GrowthDirection.forward] would have the A at
-  /// the right and the Z at the left. This is the ordinary reading order for a
-  /// horizontal set of tabs in a Hebrew application, for example.
-  left,
-}
-
-/// Returns the [Axis] that contains the given [AxisDirection].
-///
-/// Specifically, returns [Axis.vertical] for [AxisDirection.up] and
-/// [AxisDirection.down] and returns [Axis.horizontal] for [AxisDirection.left]
-/// and [AxisDirection.right].
-Axis axisDirectionToAxis(AxisDirection axisDirection) {
-  assert(axisDirection != null);
-  switch (axisDirection) {
-    case AxisDirection.up:
-    case AxisDirection.down:
-      return Axis.vertical;
-    case AxisDirection.left:
-    case AxisDirection.right:
-      return Axis.horizontal;
-  }
-  return null;
-}
-
-/// Returns the [AxisDirection] in which reading occurs in the given [TextDirection].
-///
-/// Specifically, returns [AxisDirection.left] for [TextDirection.rtl] and
-/// [AxisDirection.right] for [TextDirection.ltr].
-AxisDirection textDirectionToAxisDirection(TextDirection textDirection) {
-  assert(textDirection != null);
-  switch (textDirection) {
-    case TextDirection.rtl:
-      return AxisDirection.left;
-    case TextDirection.ltr:
-      return AxisDirection.right;
-  }
-  return null;
-}
-
-/// Returns the opposite of the given [AxisDirection].
-///
-/// Specifically, returns [AxisDirection.up] for [AxisDirection.down] (and
-/// vice versa), as well as [AxisDirection.left] for [AxisDirection.right] (and
-/// vice versa).
-///
-/// See also:
-///
-///  * [flipAxis], which does the same thing for [Axis] values.
-AxisDirection flipAxisDirection(AxisDirection axisDirection) {
-  assert(axisDirection != null);
-  switch (axisDirection) {
-    case AxisDirection.up:
-      return AxisDirection.down;
-    case AxisDirection.right:
-      return AxisDirection.left;
-    case AxisDirection.down:
-      return AxisDirection.up;
-    case AxisDirection.left:
-      return AxisDirection.right;
-  }
-  return null;
-}
-
-/// Returns whether travelling along the given axis direction visits coordinates
-/// along that axis in numerically decreasing order.
-///
-/// Specifically, returns true for [AxisDirection.up] and [AxisDirection.left]
-/// and false for [AxisDirection.down] for [AxisDirection.right].
-bool axisDirectionIsReversed(AxisDirection axisDirection) {
-  assert(axisDirection != null);
-  switch (axisDirection) {
-    case AxisDirection.up:
-    case AxisDirection.left:
-      return true;
-    case AxisDirection.down:
-    case AxisDirection.right:
-      return false;
-  }
-  return null;
-}
-
 /// Flips the [AxisDirection] if the [GrowthDirection] is [GrowthDirection.reverse].
 ///
 /// Specifically, returns `axisDirection` if `growthDirection` is
diff --git a/packages/flutter/lib/src/widgets/basic.dart b/packages/flutter/lib/src/widgets/basic.dart
index 3a455cf..6e4de19 100644
--- a/packages/flutter/lib/src/widgets/basic.dart
+++ b/packages/flutter/lib/src/widgets/basic.dart
@@ -2116,6 +2116,42 @@
 
 // LAYOUT NODES
 
+/// Returns the [AxisDirection] in the given [Axis] in the current
+/// [Directionality] (or the reverse if `reverse` is true).
+///
+/// If `axis` is [Axis.vertical], this function returns [AxisDirection.down]
+/// unless `reverse` is true, in which case this function returns
+/// [AxisDirection.up].
+///
+/// If `axis` is [Axis.horizontal], this function checks the current
+/// [Directionality]. If the current [Directionality] is right-to-left, then
+/// this function returns [AxisDirection.left] (unless `reverse` is true, in
+/// which case it returns [AxisDirection.right]). Similarly, if the current
+/// [Directionality] is left-to-right, then this function returns
+/// [AxisDirection.right] (unless `reverse` is true, in which case it returns
+/// [AxisDirection.left]).
+///
+/// This function is used by a number of scrolling widgets (e.g., [ListView],
+/// [GridView], [PageView], and [SingleChildScrollView]) as well as [ListBody]
+/// to translate their [Axis] and `reverse` properties into a concrete
+/// [AxisDirection].
+AxisDirection getAxisDirectionFromAxisReverseAndDirectionality(
+  BuildContext context,
+  Axis axis,
+  bool reverse,
+) {
+  switch (axis) {
+    case Axis.horizontal:
+      assert(debugCheckHasDirectionality(context));
+      final TextDirection textDirection = Directionality.of(context);
+      final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
+      return reverse ? flipAxisDirection(axisDirection) : axisDirection;
+    case Axis.vertical:
+      return reverse ? AxisDirection.up : AxisDirection.down;
+  }
+  return null;
+}
+
 /// A widget that arranges its children sequentially along a given axis, forcing
 /// them to the dimension of the parent in the other axis.
 ///
@@ -2142,6 +2178,7 @@
   ListBody({
     Key key,
     this.mainAxis: Axis.vertical,
+    this.reverse: false,
     List<Widget> children: const <Widget>[],
   }) : assert(mainAxis != null),
        super(key: key, children: children);
@@ -2149,12 +2186,32 @@
   /// The direction to use as the main axis.
   final Axis mainAxis;
 
+  /// Whether the list body positions children in the reading direction.
+  ///
+  /// For example, if the reading direction is left-to-right and
+  /// [mainAxis] is [Axis.horizontal], then the list body positions children
+  /// from left to right when [reverse] is false and from right to left when
+  /// [reverse] is true.
+  ///
+  /// Similarly, if [mainAxis] is [Axis.vertical], then the list body positions
+  /// from top to bottom when [reverse] is false and from bottom to top when
+  /// [reverse] is true.
+  ///
+  /// Defaults to false.
+  final bool reverse;
+
+  AxisDirection _getDirection(BuildContext context) {
+    return getAxisDirectionFromAxisReverseAndDirectionality(context, mainAxis, reverse);
+  }
+
   @override
-  RenderListBody createRenderObject(BuildContext context) => new RenderListBody(mainAxis: mainAxis);
+  RenderListBody createRenderObject(BuildContext context) {
+    return new RenderListBody(axisDirection: _getDirection(context));
+  }
 
   @override
   void updateRenderObject(BuildContext context, RenderListBody renderObject) {
-    renderObject.mainAxis = mainAxis;
+    renderObject.axisDirection = _getDirection(context);
   }
 }
 
diff --git a/packages/flutter/lib/src/widgets/scroll_view.dart b/packages/flutter/lib/src/widgets/scroll_view.dart
index a827e61..d2b5030 100644
--- a/packages/flutter/lib/src/widgets/scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/scroll_view.dart
@@ -6,7 +6,6 @@
 import 'package:flutter/rendering.dart';
 
 import 'basic.dart';
-import 'debug.dart';
 import 'framework.dart';
 import 'primary_scroll_controller.dart';
 import 'scroll_controller.dart';
@@ -179,16 +178,7 @@
   /// [AxisDirection.right].
   @protected
   AxisDirection getDirection(BuildContext context) {
-    switch (scrollDirection) {
-      case Axis.horizontal:
-        assert(debugCheckHasDirectionality(context));
-        final TextDirection textDirection = Directionality.of(context);
-        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
-        return reverse ? flipAxisDirection(axisDirection) : axisDirection;
-      case Axis.vertical:
-        return reverse ? AxisDirection.up : AxisDirection.down;
-    }
-    return null;
+    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
   }
 
   /// Subclasses should override this method to build the slivers for the inside
diff --git a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart
index e2bf2bf..a9c513e 100644
--- a/packages/flutter/lib/src/widgets/single_child_scroll_view.dart
+++ b/packages/flutter/lib/src/widgets/single_child_scroll_view.dart
@@ -8,7 +8,6 @@
 import 'package:flutter/rendering.dart';
 
 import 'basic.dart';
-import 'debug.dart';
 import 'framework.dart';
 import 'primary_scroll_controller.dart';
 import 'scroll_controller.dart';
@@ -70,7 +69,7 @@
   /// left to right when [reverse] is false and from right to left when
   /// [reverse] is true.
   ///
-  /// Similarly, if [scrollDirection] is [Axis.vertical], then scroll view
+  /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
   /// scrolls from top to bottom when [reverse] is false and from bottom to top
   /// when [reverse] is true.
   ///
@@ -116,16 +115,7 @@
   final Widget child;
 
   AxisDirection _getDirection(BuildContext context) {
-    switch (scrollDirection) {
-      case Axis.horizontal:
-        assert(debugCheckHasDirectionality(context));
-        final TextDirection textDirection = Directionality.of(context);
-        final AxisDirection axisDirection = textDirectionToAxisDirection(textDirection);
-        return reverse ? flipAxisDirection(axisDirection) : axisDirection;
-      case Axis.vertical:
-        return reverse ? AxisDirection.up : AxisDirection.down;
-    }
-    return null;
+    return getAxisDirectionFromAxisReverseAndDirectionality(context, scrollDirection, reverse);
   }
 
   @override
diff --git a/packages/flutter/test/rendering/paragraph_intrinsics_test.dart b/packages/flutter/test/rendering/paragraph_intrinsics_test.dart
index 644d709..75e9db9 100644
--- a/packages/flutter/test/rendering/paragraph_intrinsics_test.dart
+++ b/packages/flutter/test/rendering/paragraph_intrinsics_test.dart
@@ -17,7 +17,7 @@
     final RenderListBody testBlock = new RenderListBody(
       children: <RenderBox>[
         paragraph,
-      ]
+      ],
     );
 
     final double textWidth = paragraph.getMaxIntrinsicWidth(double.INFINITY);
@@ -52,7 +52,7 @@
     expect(testBlock.getMaxIntrinsicHeight(0.0), equals(manyLinesTextHeight));
 
     // horizontal block (same expectations again)
-    testBlock.mainAxis = Axis.horizontal;
+    testBlock.axisDirection = AxisDirection.right;
     expect(testBlock.getMinIntrinsicWidth(double.INFINITY), equals(wrappedTextWidth));
     expect(testBlock.getMaxIntrinsicWidth(double.INFINITY), equals(textWidth));
     expect(testBlock.getMinIntrinsicHeight(double.INFINITY), equals(oneLineTextHeight));
diff --git a/packages/flutter/test/widgets/list_body_test.dart b/packages/flutter/test/widgets/list_body_test.dart
new file mode 100644
index 0000000..734d7f1
--- /dev/null
+++ b/packages/flutter/test/widgets/list_body_test.dart
@@ -0,0 +1,109 @@
+// 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 'package:flutter_test/flutter_test.dart';
+import 'package:flutter/widgets.dart';
+
+final List<Widget> children = <Widget>[
+  new Container(width: 200.0, height: 150.0),
+  new Container(width: 200.0, height: 150.0),
+  new Container(width: 200.0, height: 150.0),
+  new Container(width: 200.0, height: 150.0),
+];
+
+void expectRects(WidgetTester tester, List<Rect> expected) {
+  final Finder finder = find.byType(Container);
+  finder.precache();
+  final List<Rect> actual = <Rect>[];
+  for (int i = 0; i < expected.length; ++i) {
+    final Finder current = finder.at(i);
+    expect(current, findsOneWidget);
+    actual.add(tester.getRect(finder.at(i)));
+  }
+  expect(() => finder.at(expected.length), throwsRangeError);
+  expect(actual, equals(expected));
+}
+
+void main() {
+
+  testWidgets('ListBody down', (WidgetTester tester) async {
+    await tester.pumpWidget(new Flex(
+      direction: Axis.vertical,
+      children: <Widget>[ new ListBody(children: children) ],
+    ));
+
+    expectRects(
+      tester,
+      <Rect>[
+        new Rect.fromLTWH(0.0, 0.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 150.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 300.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 450.0, 800.0, 150.0),
+      ],
+    );
+  });
+
+  testWidgets('ListBody up', (WidgetTester tester) async {
+    await tester.pumpWidget(new Flex(
+      direction: Axis.vertical,
+      children: <Widget>[ new ListBody(reverse: true, children: children) ],
+    ));
+
+    expectRects(
+      tester,
+      <Rect>[
+        new Rect.fromLTWH(0.0, 450.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 300.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 150.0, 800.0, 150.0),
+        new Rect.fromLTWH(0.0, 0.0, 800.0, 150.0),
+      ],
+    );
+  });
+
+  testWidgets('ListBody right', (WidgetTester tester) async {
+    await tester.pumpWidget(new Flex(
+      textDirection: TextDirection.ltr,
+      direction: Axis.horizontal,
+      children: <Widget>[
+        new Directionality(
+          textDirection: TextDirection.ltr,
+          child: new ListBody(mainAxis: Axis.horizontal, children: children),
+        ),
+      ],
+    ));
+
+    expectRects(
+      tester,
+      <Rect>[
+        new Rect.fromLTWH(0.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(200.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(400.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(600.0, 0.0, 200.0, 600.0),
+      ],
+    );
+  });
+
+  testWidgets('ListBody left', (WidgetTester tester) async {
+    await tester.pumpWidget(new Flex(
+      textDirection: TextDirection.ltr,
+      direction: Axis.horizontal,
+      children: <Widget>[
+        new Directionality(
+          textDirection: TextDirection.rtl,
+          child: new ListBody(mainAxis: Axis.horizontal, children: children),
+        ),
+      ],
+    ));
+
+    expectRects(
+      tester,
+      <Rect>[
+        new Rect.fromLTWH(600.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(400.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(200.0, 0.0, 200.0, 600.0),
+        new Rect.fromLTWH(0.0, 0.0, 200.0, 600.0),
+      ],
+    );
+  });
+}
diff --git a/packages/flutter_test/lib/src/finders.dart b/packages/flutter_test/lib/src/finders.dart
index e462965..718e3a5 100644
--- a/packages/flutter_test/lib/src/finders.dart
+++ b/packages/flutter_test/lib/src/finders.dart
@@ -285,6 +285,10 @@
   /// matched by this finder.
   Finder get last => new _LastFinder(this);
 
+  /// Returns a variant of this finder that only matches the element at the
+  /// given index matched by this finder.
+  Finder at(int index) => new _IndexFinder(this, index);
+
   /// Returns a variant of this finder that only matches elements reachable by
   /// a hit test.
   ///
@@ -335,6 +339,22 @@
   }
 }
 
+class _IndexFinder extends Finder {
+  _IndexFinder(this.parent, this.index);
+
+  final Finder parent;
+
+  final int index;
+
+  @override
+  String get description => '${parent.description} (ignoring all but index $index)';
+
+  @override
+  Iterable<Element> apply(Iterable<Element> candidates) sync* {
+    yield parent.apply(candidates).elementAt(index);
+  }
+}
+
 class _HitTestableFinder extends Finder {
   _HitTestableFinder(this.parent, this.alignment);