Dart 如何创建扩展到全屏的可拖动元素

Dart 如何创建扩展到全屏的可拖动元素,dart,flutter,gesture,flutter-animation,Dart,Flutter,Gesture,Flutter Animation,我想在Flatter中构建一个典型的音频播放器,在屏幕底部可以看到一个小型播放器,可以拖动或点击它以变成全屏,就像内置的iOS音乐应用程序或spotify: 顶部的元素不应该在中间停止,因此,如果用户拖动了一半,那么它应该再次向下移动,或者完成转换(取决于它当前所在的位置) 也可以再次将其向下拖动 旁注:我使用的是CupertinoApp而不是MaterialApp。。。不确定这是否有区别。你试过背景元素吗?您可以查看这篇文章,看看如何实现它。您可以根据需要定制和使用它。我用下面的例子大致实现

我想在Flatter中构建一个典型的音频播放器,在屏幕底部可以看到一个小型播放器,可以拖动或点击它以变成全屏,就像内置的iOS音乐应用程序或spotify:

顶部的元素不应该在中间停止,因此,如果用户拖动了一半,那么它应该再次向下移动,或者完成转换(取决于它当前所在的位置)

也可以再次将其向下拖动


旁注:我使用的是
CupertinoApp
而不是
MaterialApp
。。。不确定这是否有区别。

你试过背景元素吗?您可以查看这篇文章,看看如何实现它。您可以根据需要定制和使用它。我用下面的例子大致实现了它,我正在使用
CupertinoApp
(这是我第一次):)

import'包装:flift/cupertino.dart';
进口“包装:颤振/材料.省道”;
导入'dart:math'作为数学;
导入“package:meta/meta.dart”;
void main()=>runApp(MyApp());
类MyApp扩展了无状态小部件{
@凌驾
小部件构建(构建上下文){
返回CupertinoApp(
标题:“颤振演示”,
路线:{
“/”:(上下文)=>SimpleExample(),
},
);
}
}
类SimpleExample扩展了无状态小部件{
@凌驾
小部件构建(构建上下文)=>
脚手架(主体:安全区域(儿童:面板());
}
类面板扩展了无状态小部件{
最终frontPanelVisible=ValueNotifier(假);
@凌驾
小部件构建(构建上下文){
返回背景(
frontLayer:FrontPanel(),
背层:背面板(
前面板打开:前面板可见,
),
frontHeader:FrontPanelTitle(),
面板可见:前面板可见,
前面板开口高度:40.0,
前人头重量:48.0,
Frontheader:是的,
);
}
}
类FrontPanelTitle扩展了无状态小部件{
@凌驾
小部件构建(构建上下文){
返回填充(
填充:仅限常量边集(顶部:16.0,左侧:16.0),
子:文本(
“轻拍我”,
风格:Theme.of(context).textTheme.subhead,
),
);
}
}
类FrontPanel扩展了无状态小部件{
@凌驾
小部件构建(构建上下文){
返回容器(
颜色:主题.of(上下文).cardColor,
child:Center(child:Text('helloworld'));
}
}
类BackPanel扩展StatefulWidget{
背板({@required this.frontPanelOpen});
最终估价通知人前面板打开;
@凌驾
createState()=>_BackPanelState();
}
类_BackPanelState扩展了状态{
布尔公开赛;
@凌驾
initState(){
super.initState();
panelOpen=widget.frontPanelOpen.value;
widget.frontPanelOpen.addListener(_subscribeToValueNotifier);
}
void\u subscribeToValueNotifier()=>
setState(()=>panelOpen=widget.frontPanelOpen.value);
///发生热重新加载时重新订阅所需
@凌驾
void didUpdateWidget(后面板oldWidget){
super.didUpdateWidget(oldWidget);
oldWidget.frontPanelOpen.RemovelListener(_subscribeToValueNotifier);
widget.frontPanelOpen.addListener(_subscribeToValueNotifier);
}
@凌驾
小部件构建(构建上下文){
返回列(
mainAxisAlignment:mainAxisAlignment.spaceBetween,
crossAxisAlignment:crossAxisAlignment.stretch,
儿童:[
居中(
孩子:填充(
填充:仅限常量边设置(
排名:10.0,
),
子项:Text('前面板为${panelOpen?“open”:“closed”}),
)),
居中(
孩子:升起按钮(
孩子:文本(“点击我”),
已按下:(){
widget.frontPanelOpen.value=true;
},
)),
//不可见;由前面板覆盖
居中(子项:文本(“面板底部”),
]);
}
}
常数kFlingVelocity=2.0;
类_BackdropPanel扩展了无状态小部件{
常量后置面板({
关键点,
这个.onTap,
这是垂直排水管,
这是垂直排水,
这个名字,
这个孩子,
这是我的权利,
这个,这个,,
}):super(key:key);
最终确认为onTap;
最终手势DragUpdateCallback on垂直DragUpdate;
垂直方向上的最终姿势;
最终小部件标题;
最后一个孩子;
最后的双重所有权;
最终边缘填充;
@凌驾
小部件构建(构建上下文){
返回填充(
填充:填充,
儿童:材料(
标高:12.0,
borderRadius:仅限borderRadius(
左上:半径。圆形(16.0),
右上角:半径。圆形(16.0),
),
子:列(
crossAxisAlignment:crossAxisAlignment.stretch,
儿童:[
手势检测器(
行为:HitTestBehavior.不透明,
onVerticalDragUpdate:onVerticalDragUpdate,
在垂直方向上:在垂直方向上,
onTap:onTap,
子项:容器(高度:标题灯,子项:标题),
),
分隔器(
高度:1.0,
),
扩大(
孩子:孩子,
),
],
),
),
);
}
}
///建立一个背景。
///
///背景小部件有两个面板,前面板和后面板。前面板如图所示
///默认情况下,并向下滑动以显示后面板,用户可以从中
///你可以选择。用户还可以为以下内容配置标题:
///显示前面板或后面板。
类扩展StatefulWidget{
最后一层;
最后一个窗口小部件的底层;
最终窗口小部件前端标题;
最终双前面板开口高度;
最后的双前卫;
最终bool前端服务关闭;
最终边缘设置前面板衬垫;
最终的
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'dart:math' as math;

import 'package:meta/meta.dart';
void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return CupertinoApp(
      title: 'Flutter Demo',
      routes: {
        '/': (context) => SimpleExample(),
      },
    );
  }
}

class SimpleExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) =>
      Scaffold(body: SafeArea(child: Panels()));
}

class Panels extends StatelessWidget {
  final frontPanelVisible = ValueNotifier<bool>(false);

  @override
  Widget build(BuildContext context) {
    return Backdrop(
      frontLayer: FrontPanel(),
      backLayer: BackPanel(
        frontPanelOpen: frontPanelVisible,
      ),
      frontHeader: FrontPanelTitle(),
      panelVisible: frontPanelVisible,
      frontPanelOpenHeight: 40.0,
      frontHeaderHeight: 48.0,
      frontHeaderVisibleClosed: true,
    );
  }
}

class FrontPanelTitle extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(top: 16.0, left: 16.0),
      child: Text(
        'Tap Me',
        style: Theme.of(context).textTheme.subhead,
      ),
    );
  }
}

class FrontPanel extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
        color: Theme.of(context).cardColor,
        child: Center(child: Text('Hello world')));
  }
}

class BackPanel extends StatefulWidget {
  BackPanel({@required this.frontPanelOpen});
  final ValueNotifier<bool> frontPanelOpen;

  @override
  createState() => _BackPanelState();
}

class _BackPanelState extends State<BackPanel> {
  bool panelOpen;

  @override
  initState() {
    super.initState();
    panelOpen = widget.frontPanelOpen.value;
    widget.frontPanelOpen.addListener(_subscribeToValueNotifier);
  }

  void _subscribeToValueNotifier() =>
      setState(() => panelOpen = widget.frontPanelOpen.value);

  /// Required for resubscribing when hot reload occurs
  @override
  void didUpdateWidget(BackPanel oldWidget) {
    super.didUpdateWidget(oldWidget);
    oldWidget.frontPanelOpen.removeListener(_subscribeToValueNotifier);
    widget.frontPanelOpen.addListener(_subscribeToValueNotifier);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          Center(
              child: Padding(
                padding: const EdgeInsets.only(
                  top: 10.0,
                ),
                child: Text('Front panel is ${panelOpen ? "open" : "closed"}'),
              )),
          Center(
              child: RaisedButton(
                child: Text('Tap Me'),
                onPressed: () {
                  widget.frontPanelOpen.value = true;
                },
              )),
          // will not be seen; covered by front panel
          Center(child: Text('Bottom of Panel')),
        ]);
  }
}

const _kFlingVelocity = 2.0;

class _BackdropPanel extends StatelessWidget {
  const _BackdropPanel({
    Key key,
    this.onTap,
    this.onVerticalDragUpdate,
    this.onVerticalDragEnd,
    this.title,
    this.child,
    this.titleHeight,
    this.padding,
  }) : super(key: key);

  final VoidCallback onTap;
  final GestureDragUpdateCallback onVerticalDragUpdate;
  final GestureDragEndCallback onVerticalDragEnd;
  final Widget title;
  final Widget child;
  final double titleHeight;
  final EdgeInsets padding;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: padding,
      child: Material(
        elevation: 12.0,
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(16.0),
          topRight: Radius.circular(16.0),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: <Widget>[
            GestureDetector(
              behavior: HitTestBehavior.opaque,
              onVerticalDragUpdate: onVerticalDragUpdate,
              onVerticalDragEnd: onVerticalDragEnd,
              onTap: onTap,
              child: Container(height: titleHeight, child: title),
            ),
            Divider(
              height: 1.0,
            ),
            Expanded(
              child: child,
            ),
          ],
        ),
      ),
    );
  }
}

/// Builds a Backdrop.
///
/// A Backdrop widget has two panels, front and back. The front panel is shown
/// by default, and slides down to show the back panel, from which a user
/// can make a selection. The user can also configure the titles for when the
/// front or back panel is showing.
class Backdrop extends StatefulWidget {
  final Widget frontLayer;
  final Widget backLayer;
  final Widget frontHeader;
  final double frontPanelOpenHeight;
  final double frontHeaderHeight;
  final bool frontHeaderVisibleClosed;
  final EdgeInsets frontPanelPadding;
  final ValueNotifier<bool> panelVisible;

  Backdrop(
      {@required this.frontLayer,
        @required this.backLayer,
        this.frontPanelOpenHeight = 0.0,
        this.frontHeaderHeight = 48.0,
        this.frontPanelPadding = const EdgeInsets.all(0.0),
        this.frontHeaderVisibleClosed = true,
        this.panelVisible,
        this.frontHeader})
      : assert(frontLayer != null),
        assert(backLayer != null);

  @override
  createState() => _BackdropState();
}

class _BackdropState extends State<Backdrop>
    with SingleTickerProviderStateMixin {
  final _backdropKey = GlobalKey(debugLabel: 'Backdrop');
  AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: Duration(milliseconds: 300),
      // value of 0 hides the panel; value of 1 fully shows the panel
      value: (widget.panelVisible?.value ?? true) ? 1.0 : 0.0,
      vsync: this,
    );

    // Listen on the toggle value notifier if it's not null

    widget.panelVisible?.addListener(_subscribeToValueNotifier);

    // Ensure that the value notifier is updated when the panel is opened or closed
    if (widget.panelVisible != null) {
      _controller.addStatusListener((status) {
        if (status == AnimationStatus.completed)
          widget.panelVisible.value = true;
        else if (status == AnimationStatus.dismissed)
          widget.panelVisible.value = false;
      });
    }
  }

  void _subscribeToValueNotifier() {
    if (widget.panelVisible.value != _backdropPanelVisible)
      _toggleBackdropPanelVisibility();
  }

  /// Required for resubscribing when hot reload occurs
  @override
  void didUpdateWidget(Backdrop oldWidget) {
    super.didUpdateWidget(oldWidget);
    oldWidget.panelVisible?.removeListener(_subscribeToValueNotifier);
    widget.panelVisible?.addListener(_subscribeToValueNotifier);
  }

  @override
  void dispose() {
    _controller.dispose();
    widget.panelVisible?.dispose();
    super.dispose();
  }

  bool get _backdropPanelVisible =>
      _controller.status == AnimationStatus.completed ||
          _controller.status == AnimationStatus.forward;

  void _toggleBackdropPanelVisibility() => _controller.fling(
      velocity: _backdropPanelVisible ? -_kFlingVelocity : _kFlingVelocity);

  double get _backdropHeight {
    final RenderBox renderBox = _backdropKey.currentContext.findRenderObject();
    return renderBox.size.height;
  }

  void _handleDragUpdate(DragUpdateDetails details) {
    if (!_controller.isAnimating)
      _controller.value -= details.primaryDelta / _backdropHeight;
  }

  void _handleDragEnd(DragEndDetails details) {
    if (_controller.isAnimating ||
        _controller.status == AnimationStatus.completed) return;

    final double flingVelocity =
        details.velocity.pixelsPerSecond.dy / _backdropHeight;
    if (flingVelocity < 0.0)
      _controller.fling(velocity: math.max(_kFlingVelocity, -flingVelocity));
    else if (flingVelocity > 0.0)
      _controller.fling(velocity: math.min(-_kFlingVelocity, -flingVelocity));
    else
      _controller.fling(
          velocity:
          _controller.value < 0.5 ? -_kFlingVelocity : _kFlingVelocity);
  }

  @override
  Widget build(BuildContext context) {
    return LayoutBuilder(builder: (context, constraints) {
      final panelSize = constraints.biggest;
      final closedPercentage = widget.frontHeaderVisibleClosed
          ? (panelSize.height - widget.frontHeaderHeight) / panelSize.height
          : 1.0;
      final openPercentage = widget.frontPanelOpenHeight / panelSize.height;

      final panelDetailsPosition = Tween<Offset>(
        begin: Offset(0.0, closedPercentage),
        end: Offset(0.0, openPercentage),
      ).animate(_controller.view);

      return Container(
        key: _backdropKey,
        child: Stack(
          children: <Widget>[
            widget.backLayer,
            SlideTransition(
              position: panelDetailsPosition,
              child: _BackdropPanel(
                onTap: _toggleBackdropPanelVisibility,
                onVerticalDragUpdate: _handleDragUpdate,
                onVerticalDragEnd: _handleDragEnd,
                title: widget.frontHeader,
                titleHeight: widget.frontHeaderHeight,
                child: widget.frontLayer,
                padding: widget.frontPanelPadding,
              ),
            ),
          ],
        ),
      );
    });
  }
}