Commit 25223c75 authored by Ian Hickson's avatar Ian Hickson Committed by GitHub

New scrollbars for the scrollable refactor (#7646)

parent 417c2f25
......@@ -2,10 +2,187 @@
// 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:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'theme.dart';
class Scrollbar2 extends StatefulWidget {
Scrollbar2({
Key key,
this.child,
}) : super(key: key);
/// The subtree to place inside the [Scrollbar2]. This should include
/// a source of [ScrollNotification2] notifications, typically a [Scrollable2]
/// widget.
final Widget child;
@override
_Scrollbar2State createState() => new _Scrollbar2State();
}
class _Scrollbar2State extends State<Scrollbar2> with TickerProviderStateMixin {
_ScrollbarController _controller;
@override
void dependenciesChanged() {
super.dependenciesChanged();
_controller ??= new _ScrollbarController(this);
_controller.color = Theme.of(context).highlightColor;
}
bool _handleScrollNotification(ScrollNotification2 notification) {
if (notification is ScrollUpdateNotification ||
notification is OverscrollNotification)
_controller.update(notification.metrics, notification.axisDirection);
return false;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new NotificationListener<ScrollNotification2>(
onNotification: _handleScrollNotification,
// TODO(ianh): Maybe we should try to collapse out these repaint
// boundaries when the scroll bars are invisible.
child: new RepaintBoundary(
child: new CustomPaint(
foregroundPainter: new _ScrollbarPainter(_controller),
child: new RepaintBoundary(
child: config.child,
),
),
),
);
}
}
class _ScrollbarController extends ChangeNotifier {
_ScrollbarController(TickerProvider vsync) {
assert(vsync != null);
_fadeController = new AnimationController(duration: _kThumbFadeDuration, vsync: vsync);
_opacity = new CurvedAnimation(parent: _fadeController, curve: Curves.fastOutSlowIn)
..addListener(notifyListeners);
}
// animation of the main axis direction
AnimationController _fadeController;
Animation<double> _opacity;
// fade-out timer
Timer _fadeOut;
Color get color => _color;
Color _color;
set color(Color value) {
assert(value != null);
if (color == value)
return;
_color = value;
notifyListeners();
}
@override
void dispose() {
_fadeOut?.cancel();
_fadeController.dispose();
super.dispose();
}
ScrollableMetrics _lastMetrics;
AxisDirection _lastAxisDirection;
static const double _kMinThumbExtent = 18.0;
static const double _kThumbGirth = 6.0;
static const Duration _kThumbFadeDuration = const Duration(milliseconds: 300);
static const Duration _kFadeOutTimeout = const Duration(milliseconds: 600);
void update(ScrollableMetrics metrics, AxisDirection axisDirection) {
_lastMetrics = metrics;
_lastAxisDirection = axisDirection;
if (_fadeController.status == AnimationStatus.completed) {
notifyListeners();
} else if (_fadeController.status != AnimationStatus.forward) {
_fadeController.forward();
}
_fadeOut?.cancel();
_fadeOut = new Timer(_kFadeOutTimeout, startFadeOut);
}
void startFadeOut() {
_fadeOut = null;
_fadeController.reverse();
}
Paint get _paint => new Paint()..color = color.withOpacity(_opacity.value);
void _paintVerticalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
final Point thumbOrigin = new Point(size.width - _kThumbGirth, thumbOffset);
final Size thumbSize = new Size(_kThumbGirth, thumbExtent);
canvas.drawRect(thumbOrigin & thumbSize, _paint);
}
void _paintHorizontalThumb(Canvas canvas, Size size, double thumbOffset, double thumbExtent) {
final Point thumbOrigin = new Point(thumbOffset, size.height - _kThumbGirth);
final Size thumbSize = new Size(thumbExtent, _kThumbGirth);
canvas.drawRect(thumbOrigin & thumbSize, _paint);
}
void _paintThumb(double before, double inside, double after, double viewport, Canvas canvas, Size size,
void painter(Canvas canvas, Size size, double thumbOffset, double thumbExtent)) {
final double thumbExtent = math.max(math.min(viewport, _kMinThumbExtent), viewport * inside / (before + inside + after));
final double thumbOffset = before * (viewport - thumbExtent) / (before + after);
painter(canvas, size, thumbOffset, thumbExtent);
}
void paint(Canvas canvas, Size size) {
if (_lastAxisDirection == null || _lastMetrics == null || _opacity.value == 0.0)
return;
switch (_lastAxisDirection) {
case AxisDirection.down:
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.height, canvas, size, _paintVerticalThumb);
break;
case AxisDirection.up:
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.height, canvas, size, _paintVerticalThumb);
break;
case AxisDirection.right:
_paintThumb(_lastMetrics.extentBefore, _lastMetrics.extentInside, _lastMetrics.extentAfter, size.width, canvas, size, _paintHorizontalThumb);
break;
case AxisDirection.left:
_paintThumb(_lastMetrics.extentAfter, _lastMetrics.extentInside, _lastMetrics.extentBefore, size.width, canvas, size, _paintHorizontalThumb);
break;
}
}
}
class _ScrollbarPainter extends CustomPainter {
_ScrollbarPainter(this.controller) : super(repaint: controller);
final _ScrollbarController controller;
@override
void paint(Canvas canvas, Size size) {
controller.paint(canvas, size);
}
@override
bool shouldRepaint(_ScrollbarPainter oldDelegate) {
return oldDelegate.controller != controller;
}
}
// DELETE EVERYTHING BELOW THIS LINE WHEN REMOVING LEGACY SCROLLING CODE
const double _kMinScrollbarThumbExtent = 18.0;
const double _kScrollbarThumbGirth = 6.0;
const Duration _kScrollbarThumbFadeDuration = const Duration(milliseconds: 300);
......
// Copyright 2016 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/material.dart';
import '../rendering/mock_canvas.dart';
void main() {
testWidgets('Viewport2 basic test', (WidgetTester tester) async {
await tester.pumpWidget(new Scrollbar2(
child: new SingleChildScrollView(
child: new SizedBox(width: 4000.0, height: 4000.0),
),
));
expect(find.byType(Scrollbar2), isNot(paints..rect()));
await tester.fling(find.byType(SingleChildScrollView), const Offset(0.0, -10.0), 10.0);
expect(find.byType(Scrollbar2), paints..rect());
});
}
......@@ -16,8 +16,8 @@ void main() {
),
height: 200.0,
width: 300.0,
child: new Scrollbar(
child: new Block(
child: new Scrollbar2(
child: new ScrollView(
children: <Widget>[
new Container(height: 40.0, child: new Text('0')),
new Container(height: 40.0, child: new Text('1')),
......@@ -35,13 +35,13 @@ void main() {
);
SchedulerBinding.instance.debugAssertNoTransientCallbacks('Building a list with a scrollbar triggered an animation.');
await tester.tap(find.byType(Block));
await tester.tap(find.byType(ScrollView));
SchedulerBinding.instance.debugAssertNoTransientCallbacks('Tapping a block with a scrollbar triggered an animation.');
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
await tester.scroll(find.byType(Block), const Offset(0.0, -10.0));
await tester.scroll(find.byType(ScrollView), const Offset(0.0, -10.0));
expect(SchedulerBinding.instance.transientCallbackCount, greaterThan(0));
await tester.pump(const Duration(milliseconds: 200));
await tester.pump(const Duration(milliseconds: 200));
......
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