【问题标题】:How to create a draggable element that expands to full screen如何创建扩展为全屏的可拖动元素
【发布时间】:2018-10-30 16:27:17
【问题描述】:

我想在 Flutter 中构建一个典型的音频播放器,它在屏幕底部有一个可见的迷你播放器,可以拖动或点击变成全屏,就像内置的 iOS 音乐应用或 Spotify:

顶部的元素不能在两者之间停下来,所以如果用户拖到一半,那么它应该要么再次向下移动,要么完成过渡(取决于它当前的位置)。

应该还可以再往下拉。

旁注:我使用的是CupertinoApp 而不是MaterialApp...不确定这是否会有所不同。

【问题讨论】:

    标签: dart flutter gesture flutter-animation


    【解决方案1】:

    我强烈推荐这个包miniplayer 0.6.1 它基本上可以携带您需要的一切,还提供可定制的迷你播放器和完整播放器。但是您必须阅读本机代码并进行一些更改。

    除此之外,动画部分还可以使用Flutter Heroes。它也适用于这种情况。

    【讨论】:

    • 感谢您推荐我的包裹!
    【解决方案2】:

    你试过背景元素吗?您可以查看此really cool article 以了解如何实现它。您可以根据需要对其进行自定义和使用。我用下面的例子大致实现了它,我正在使用CupertinoApp(这是我的第一次):)

    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,
                  ),
                ),
              ],
            ),
          );
        });
      }
    }
    

    【讨论】:

    • 看起来和我需要的完全一样。非常感谢!
    【解决方案3】:

    您可以使用DraggableScrollableSheet 小部件。这个article 也将帮助您开始使用小部件。

    【讨论】:

      猜你喜欢
      • 2013-09-18
      • 1970-01-01
      • 2015-05-24
      • 1970-01-01
      • 1970-01-01
      • 2020-12-07
      • 1970-01-01
      • 1970-01-01
      • 2013-06-06
      相关资源
      最近更新 更多