blob: f01a5836d589add511963493c6563e9df2795f2a [file] [log] [blame]
// Copyright 2018 The Fuchsia 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:ui' show Offset, Size;
import 'package:flutter/foundation.dart' show ValueListenable;
import 'package:flutter/material.dart';
import 'package:lib.widgets/model.dart' show SpringModel;
import 'conditional_builder.dart';
/// A Container for a Widget that's position is determined by a
/// [ScreenPositionModel].
class ScreenContainer extends StatelessWidget {
/// Determines the position of the [child].
final ScreenPositionModel screenPositionModel;
/// The elevation the [child] should have.
/// Defaults to 0.0.
final double elevation;
/// The color to use as the background of the child.
/// Defaults to Colors.red[900].
final Color childBackgroundColor;
/// The width of the gesture area for collecting drags.
/// Defaults to 32.0.
final double gestureWidth;
/// The child widget to display.
final Widget child;
/// If true, the child will be removed completely from the Widget tree when
/// the screen is offscreen.
final bool removeChildWhenOffscreen;
/// Constructor.
const ScreenContainer({
this.removeChildWhenOffscreen = true,
this.screenPositionModel,
this.elevation,
this.childBackgroundColor,
this.gestureWidth,
this.child,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: screenPositionModel,
child: PhysicalModel(
color: childBackgroundColor ?? Colors.red[900],
elevation: elevation ?? 0.0,
child: Stack(
children: <Widget>[
Positioned.fill(
child: child,
),
Positioned(
top: 0.0,
bottom: 0.0,
left: 0.0,
width: gestureWidth ?? 32.0,
child: GestureDetector(
behavior: HitTestBehavior.translucent,
onHorizontalDragUpdate: (DragUpdateDetails details) {
screenPositionModel.update(delta: details.delta.dx);
},
onHorizontalDragEnd: (DragEndDetails details) {
screenPositionModel.end(
horizontalPixelsPerSecond:
details.velocity.pixelsPerSecond.dx,
);
},
),
)
],
),
),
builder: (BuildContext context, Widget child) {
if (removeChildWhenOffscreen) {
return ConditionalBuilder(
condition: !screenPositionModel.isOffscreen,
builder: (_) {
return Transform.translate(
offset: screenPositionModel.offset,
child: child,
);
},
);
} else {
return Transform.translate(
offset: screenPositionModel.offset,
child: child,
);
}
},
);
}
}
const _resetDistance = 64.0;
const _flingMinPixelsPerSecond = 100.0;
/// Handles the position of a full screen element. This implementation comes in
/// from the right and can be dismissed by dragging/flinging the left edge to
/// the right.
class ScreenPositionModel extends SpringModel {
/// The screen size.
final ValueListenable<Size> screenSize;
/// Constructor.
ScreenPositionModel({this.screenSize}) {
screenSize.addListener(notifyListeners);
}
/// The offset the screen should have from its default position.
Offset get offset => Offset(value, 0.0);
/// Called by a gesture detector on the left edge of the Container this
/// Position is associated with.
void update({double delta}) {
if (delta == 0.0) {
return;
}
jump(value + delta);
notifyListeners();
}
/// Called by a gesture detector on the left edge of the Container this
/// Position is associated with.
void end({double horizontalPixelsPerSecond}) {
/// If offset greater than threshold or velocity is a fling rightward, hide.
/// Otherwise show.
if (value < _resetDistance ||
horizontalPixelsPerSecond < -_flingMinPixelsPerSecond) {
show();
} else {
hide();
}
if (value != target) {
velocity = (horizontalPixelsPerSecond / (value - target)).abs();
startTicking();
}
}
/// True if the offset indicates the screen is offscreen.
bool get isOffscreen => offset.dx >= screenSize.value.width;
/// Causes the screen to be shown.
void show() {
target = 0.0;
}
/// Causes the screen to be hidden.
void hide() {
target = screenSize.value.width;
}
/// True if the screen is showing or in the process of showing.
bool get isShowing => target == 0.0;
/// True if the screen is hiding or in the process of hiding.
bool get isHiding => target == screenSize.value.width;
}