Commit 921d4320 authored by Hans Muller's avatar Hans Muller

Added Painter, ScrollingListPainter, MaterialScrollbarPainter

Enabled displaying a scrollbar in ScrollingLists. The scrollbar is painted as an "overlay", i.e. it's painted on top of the scrolling list's visible children.

Added an abstract Painter base class that encapsulates a paint method and the renderer that it's attached to. RenderBlockViewport and HomogenousViewport now support an overlayPainter property. If specified, RenderBlockViewport attaches itself to the overlayPainter when it's attached to the rendering tree. RenderBlockViewport now calls overlayPainter.paint() after it has painted its children.

Added an abstract ScrollingListPainter class that exposes ScrollingList's state which might be needed for painting. Like its scroll direction and scrollOffset. The ScrollingListPainter is notified when a scroll starts and ends.

Defined a Material-specific ScrollingListPainter that renders a scrollbar. The scrollbar thumb is faded in/out when the scroll starts/ends.

Added onScrollStart and onScrollEnd listeners to Scrollable.
parent faa7f818
// 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:intl/intl.dart';
import 'package:flutter/material.dart';
class ScrollbarApp extends StatefulComponent {
ScrollbarApp({ this.navigator });
final NavigatorState navigator;
ScrollbarAppState createState() => new ScrollbarAppState();
}
class ScrollbarAppState extends State<ScrollbarApp> {
final int _itemCount = 20;
final double _itemExtent = 50.0;
final ScrollbarPainter _scrollbarPainter = new ScrollbarPainter();
Widget _buildMenu(BuildContext context) {
NumberFormat dd = new NumberFormat("00", "en_US");
return new ScrollableList<int>(
items: new List<int>.generate(_itemCount, (int i) => i),
itemExtent: _itemExtent,
itemBuilder: (BuildContext _, int i) => new Text('Item ${dd.format(i)}', style: Theme.of(context).text.title),
scrollableListPainter: _scrollbarPainter
);
}
Widget build(BuildContext context) {
Widget scrollable = new Container(
margin: new EdgeDims.symmetric(horizontal: 6.0), // TODO(hansmuller) 6.0 should be based on _kScrollbarThumbWidth
child: new Center(
shrinkWrap: ShrinkWrap.both,
child: new Container(
width: 80.0,
height: _itemExtent * 5.0,
child: _buildMenu(context)
)
)
);
return new Scaffold(
toolBar: new ToolBar(center: new Text('Scrollbar Demo')),
body: new Container(
decoration: new BoxDecoration(backgroundColor: Theme.of(context).primarySwatch[50]),
padding: new EdgeDims.all(12.0),
child: new Center(child: new Card(child: scrollable))
)
);
}
}
void main() {
runApp(new MaterialApp(
title: 'ScrollbarApp',
theme: new ThemeData(
brightness: ThemeBrightness.light,
primarySwatch: Colors.blue,
accentColor: Colors.redAccent[200]
),
routes: {
'/': (RouteArguments args) => new ScrollbarApp(navigator: args.navigator),
}
));
}
......@@ -33,6 +33,7 @@ export 'src/material/progress_indicator.dart';
export 'src/material/radio.dart';
export 'src/material/raised_button.dart';
export 'src/material/scaffold.dart';
export 'src/material/scrollbar_painter.dart';
export 'src/material/shadows.dart';
export 'src/material/snack_bar.dart';
export 'src/material/switch.dart';
......
// 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 'dart:ui' as ui;
import 'package:flutter/animation.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
const double _kMinScrollbarThumbLength = 18.0;
const double _kScrollbarThumbGirth = 6.0;
const Duration _kScrollbarThumbFadeDuration = const Duration(milliseconds: 300);
class ScrollbarPainter extends ScrollableListPainter {
double _opacity = 0.0;
int get _alpha => (_opacity * 0xFF).round();
// TODO(hansmuller): thumb color should come from ThemeData.
Color get thumbColor => const Color(0xFF9E9E9E);
void paintThumb(PaintingContext context, Rect thumbBounds) {
final Paint paint = new Paint()..color = thumbColor.withAlpha(_alpha);
context.canvas.drawRect(thumbBounds, paint);
}
void paintScrollbar(PaintingContext context, Offset offset) {
final Rect viewportBounds = offset & viewportSize;
Point thumbOrigin;
Size thumbSize;
if (isVertical) {
double thumbHeight = viewportBounds.height * viewportBounds.height / contentExtent;
thumbHeight = thumbHeight.clamp(_kMinScrollbarThumbLength, viewportBounds.height);
final double maxThumbTop = viewportBounds.height - thumbHeight;
double thumbTop = (scrollOffset / (contentExtent - viewportBounds.height)) * maxThumbTop;
thumbTop = viewportBounds.top + thumbTop.clamp(0.0, maxThumbTop);
thumbOrigin = new Point(viewportBounds.right - _kScrollbarThumbGirth, thumbTop);
thumbSize = new Size(_kScrollbarThumbGirth, thumbHeight);
} else {
double thumbWidth = viewportBounds.width * viewportBounds.width / contentExtent;
thumbWidth = thumbWidth.clamp(_kMinScrollbarThumbLength, viewportBounds.width);
final double maxThumbLeft = viewportBounds.width - thumbWidth;
double thumbLeft = (scrollOffset / (contentExtent - viewportBounds.width)) * maxThumbLeft;
thumbLeft = viewportBounds.left + thumbLeft.clamp(0.0, maxThumbLeft);
thumbOrigin = new Point(thumbLeft, viewportBounds.height - _kScrollbarThumbGirth);
thumbSize = new Size(thumbWidth, _kScrollbarThumbGirth);
}
paintThumb(context, thumbOrigin & thumbSize);
}
void paint(PaintingContext context, Offset offset) {
if (_alpha == 0)
return;
paintScrollbar(context, offset);
}
ValuePerformance<double> _fade;
Future scrollStarted() {
_fade ??= new ValuePerformance<double>()
..duration = _kScrollbarThumbFadeDuration
..variable = new AnimatedValue<double>(0.0, end: 1.0, curve: ease)
..addListener(() {
_opacity = _fade.value;
renderer?.markNeedsPaint();
});
return _fade.forward();
}
Future scrollEnded() {
return _fade.reverse();
}
void detach() {
super.detach();
_fade?.stop();
}
}
......@@ -237,6 +237,7 @@ class RenderBlockViewport extends RenderBlockBase {
ExtentCallback totalExtentCallback,
ExtentCallback maxCrossAxisDimensionCallback,
ExtentCallback minCrossAxisDimensionCallback,
Painter overlayPainter,
BlockDirection direction: BlockDirection.vertical,
double itemExtent,
double minExtent: 0.0,
......@@ -246,6 +247,7 @@ class RenderBlockViewport extends RenderBlockBase {
_totalExtentCallback = totalExtentCallback,
_maxCrossAxisExtentCallback = maxCrossAxisDimensionCallback,
_minCrossAxisExtentCallback = minCrossAxisDimensionCallback,
_overlayPainter = overlayPainter,
_startOffset = startOffset,
super(children: children, direction: direction, itemExtent: itemExtent, minExtent: minExtent);
......@@ -298,6 +300,27 @@ class RenderBlockViewport extends RenderBlockBase {
markNeedsLayout();
}
Painter get overlayPainter => _overlayPainter;
Painter _overlayPainter;
void set overlayPainter(Painter value) {
if (_overlayPainter == value)
return;
_overlayPainter?.detach();
_overlayPainter = value;
_overlayPainter?.attach(this);
markNeedsPaint();
}
void attach() {
super.attach();
_overlayPainter?.attach(this);
}
void detach() {
super.detach();
_overlayPainter?.detach();
}
/// The offset at which to paint the first child
///
/// Note: you can modify this property from within [callback], if necessary.
......@@ -377,11 +400,15 @@ class RenderBlockViewport extends RenderBlockBase {
void paint(PaintingContext context, Offset offset) {
context.canvas.save();
context.canvas.clipRect(offset & size);
if (isVertical)
defaultPaint(context, offset.translate(0.0, startOffset));
else
defaultPaint(context, offset.translate(startOffset, 0.0));
overlayPainter?.paint(context, offset);
context.canvas.restore();
}
......
......@@ -394,6 +394,27 @@ class PaintingContext {
}
/// An encapsulation of a renderer and a paint() method.
///
/// A renderer may allow its paint() method to be augmented or redefined by
/// providing a Painter. See for example overlayPainter in BlockViewport.
abstract class Painter {
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;
void attach(RenderObject renderObject) {
assert(_renderObject == null);
_renderObject = renderObject;
}
void detach() {
assert(_renderObject != null);
_renderObject = null;
}
void paint(PaintingContext context, Offset offset);
}
/// An abstract set of layout constraints
///
/// Concrete layout models (such as box) will create concrete subclasses to
......
......@@ -18,7 +18,8 @@ class HomogeneousViewport extends RenderObjectWidget {
this.itemExtent, // required
this.itemCount, // optional, but you cannot shrink-wrap this class or otherwise use its intrinsic dimensions if you don't specify it
this.direction: ScrollDirection.vertical,
this.startOffset: 0.0
this.startOffset: 0.0,
this.overlayPainter
}) : super(key: key) {
assert(itemExtent != null);
}
......@@ -29,6 +30,7 @@ class HomogeneousViewport extends RenderObjectWidget {
final int itemCount;
final ScrollDirection direction;
final double startOffset;
final Painter overlayPainter;
_HomogeneousViewportElement createElement() => new _HomogeneousViewportElement(this);
......@@ -70,6 +72,7 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
renderObject.totalExtentCallback = getTotalExtent;
renderObject.minCrossAxisExtentCallback = getMinCrossAxisExtent;
renderObject.maxCrossAxisExtentCallback = getMaxCrossAxisExtent;
renderObject.overlayPainter = widget.overlayPainter;
}
void unmount() {
......@@ -77,6 +80,7 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
renderObject.totalExtentCallback = null;
renderObject.minCrossAxisExtentCallback = null;
renderObject.maxCrossAxisExtentCallback = null;
renderObject.overlayPainter = null;
super.unmount();
}
......@@ -134,6 +138,7 @@ class _HomogeneousViewportElement extends RenderObjectElement<HomogeneousViewpor
renderObject.itemExtent = widget.itemExtent;
renderObject.minExtent = getTotalExtent(null);
renderObject.startOffset = offset;
renderObject.overlayPainter = widget.overlayPainter;
});
}
......
......@@ -32,7 +32,9 @@ abstract class Scrollable extends StatefulComponent {
Key key,
this.initialScrollOffset,
this.scrollDirection: ScrollDirection.vertical,
this.onScrollStart,
this.onScroll,
this.onScrollEnd,
this.snapOffsetCallback,
this.snapAlignmentOffset: 0.0
}) : super(key: key) {
......@@ -42,7 +44,9 @@ abstract class Scrollable extends StatefulComponent {
final double initialScrollOffset;
final ScrollDirection scrollDirection;
final ScrollListener onScrollStart;
final ScrollListener onScroll;
final ScrollListener onScrollEnd;
final SnapOffsetCallback snapOffsetCallback;
final double snapAlignmentOffset;
}
......@@ -74,6 +78,12 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return _scrollBehavior;
}
GestureDragStartCallback _getDragStartHandler(ScrollDirection direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
return _handleDragStart;
}
GestureDragUpdateCallback _getDragUpdateHandler(ScrollDirection direction) {
if (config.scrollDirection != direction || !scrollBehavior.isScrollable)
return null;
......@@ -88,8 +98,10 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
Widget build(BuildContext context) {
return new GestureDetector(
onVerticalDragStart: _getDragStartHandler(ScrollDirection.vertical),
onVerticalDragUpdate: _getDragUpdateHandler(ScrollDirection.vertical),
onVerticalDragEnd: _getDragEndHandler(ScrollDirection.vertical),
onHorizontalDragStart: _getDragStartHandler(ScrollDirection.horizontal),
onHorizontalDragUpdate: _getDragUpdateHandler(ScrollDirection.horizontal),
onHorizontalDragEnd: _getDragEndHandler(ScrollDirection.horizontal),
child: new Listener(
......@@ -199,12 +211,22 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
return _startToEndAnimation();
}
void dispatchOnScrollStart() {
if (config.onScrollStart != null)
config.onScrollStart(_scrollOffset);
}
// Derived classes can override this method and call super.dispatchOnScroll()
void dispatchOnScroll() {
if (config.onScroll != null)
config.onScroll(_scrollOffset);
}
void dispatchOnScrollEnd() {
if (config.onScrollEnd != null)
config.onScrollEnd(_scrollOffset);
}
double _scrollVelocity(ui.Offset velocity) {
double scrollVelocity = config.scrollDirection == ScrollDirection.horizontal
? -velocity.dx
......@@ -216,14 +238,20 @@ abstract class ScrollableState<T extends Scrollable> extends State<T> {
_animation.stop();
}
void _handleDragStart() {
scheduleMicrotask(dispatchOnScrollStart);
}
void _handleDragUpdate(double delta) {
// We negate the delta here because a positive scroll offset moves the
// the content up (or to the left) rather than down (or the right).
scrollBy(-delta);
}
void _handleDragEnd(Offset velocity) {
fling(velocity);
Future _handleDragEnd(Offset velocity) {
return fling(velocity).then((_) {
dispatchOnScrollEnd();
});
}
}
......@@ -379,6 +407,52 @@ class Block extends StatelessComponent {
}
}
abstract class ScrollableListPainter extends Painter {
void attach(RenderObject renderObject) {
assert(renderObject is RenderBlockViewport);
super.attach(renderObject);
}
RenderBlockViewport get renderer => renderObject;
bool get isVertical => renderer.isVertical;
Size get viewportSize => renderer.size;
double get contentExtent => _contentExtent;
double _contentExtent = 0.0;
void set contentExtent (double value) {
assert(value != null);
assert(value >= 0.0);
if (_contentExtent == value)
return;
_contentExtent = value;
renderer?.markNeedsPaint();
}
double get scrollOffset => _scrollOffset;
double _scrollOffset = 0.0;
void set scrollOffset (double value) {
assert(value != null);
assert(value >= 0.0 && value <= 1.0);
if (_scrollOffset == value)
return;
_scrollOffset = value;
renderer?.markNeedsPaint();
}
/// Called when a scroll starts. Subclasses may override this method to
/// initialize some state or to play an animation. The returned Future should
/// complete when the computation triggered by this method has finished.
Future scrollStarted() => new Future.value();
/// Similar to scrollStarted(). Called when a scroll ends. For fling scrolls
/// "ended" means that the scroll animation either stopped of its own accord
/// or was canceled by the user.
Future scrollEnded() => new Future.value();
}
/// An optimized scrollable widget for a large number of children that are all
/// the same size (extent) in the scrollDirection. For example for
/// ScrollDirection.vertical itemExtent is the height of each item. Use this
......@@ -394,7 +468,8 @@ abstract class ScrollableWidgetList extends Scrollable {
double snapAlignmentOffset: 0.0,
this.itemsWrap: false,
this.itemExtent,
this.padding
this.padding,
this.scrollableListPainter
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
......@@ -409,6 +484,7 @@ abstract class ScrollableWidgetList extends Scrollable {
final bool itemsWrap;
final double itemExtent;
final EdgeDims padding;
final ScrollableListPainter scrollableListPainter;
}
abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends ScrollableState<T> {
......@@ -480,18 +556,40 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
return new EdgeDims.only(top: padding.top, bottom: padding.bottom);
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
double get _contentExtent {
double contentExtent = config.itemExtent * itemCount;
if (config.padding != null)
contentExtent += _leadingPadding + _trailingPadding;
return contentExtent;
}
void _updateScrollBehavior() {
// if you don't call this from build(), you must call it from setState().
if (config.scrollableListPainter != null)
config.scrollableListPainter.contentExtent = _contentExtent;
scrollTo(scrollBehavior.updateExtents(
contentExtent: contentExtent,
contentExtent: _contentExtent,
containerExtent: _containerExtent,
scrollOffset: scrollOffset
));
}
void dispatchOnScrollStart() {
super.dispatchOnScrollStart();
config.scrollableListPainter?.scrollStarted();
}
void dispatchOnScroll() {
super.dispatchOnScroll();
if (config.scrollableListPainter != null)
config.scrollableListPainter.scrollOffset = scrollOffset;
}
void dispatchOnScrollEnd() {
super.dispatchOnScrollEnd();
config.scrollableListPainter?.scrollEnded();
}
Widget buildContent(BuildContext context) {
if (itemCount != _previousItemCount) {
_previousItemCount = itemCount;
......@@ -508,7 +606,8 @@ abstract class ScrollableWidgetListState<T extends ScrollableWidgetList> extends
itemExtent: config.itemExtent,
itemCount: itemCount,
direction: config.scrollDirection,
startOffset: scrollOffset - _leadingPadding
startOffset: scrollOffset - _leadingPadding,
overlayPainter: config.scrollableListPainter
)
)
);
......@@ -541,7 +640,8 @@ class ScrollableList<T> extends ScrollableWidgetList {
this.itemBuilder,
itemsWrap: false,
double itemExtent,
EdgeDims padding
EdgeDims padding,
ScrollableListPainter scrollableListPainter
}) : super(
key: key,
initialScrollOffset: initialScrollOffset,
......@@ -551,7 +651,9 @@ class ScrollableList<T> extends ScrollableWidgetList {
snapAlignmentOffset: snapAlignmentOffset,
itemsWrap: itemsWrap,
itemExtent: itemExtent,
padding: padding);
padding: padding,
scrollableListPainter: scrollableListPainter
);
final List<T> items;
final ItemBuilder<T> itemBuilder;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment