乐趣区

关于flutter:Flutter实现电影院选座效果

导语

接到了一个仿电影院的需要,上周简直是找遍了百度,谷歌,stackoverflow。均没有找到用 flutter 实现的成果,那只能本人写一个了。本文只讲思路,具体实现还需各位看官本人入手。只有看懂了上面的思路,实现起来非常简单。

间接上效果图

竖屏:

横屏:

初始化自适应屏幕的放大放大成果:

布局剖析

两头的座位 => 矩阵 ,通过Column 嵌套 Row 实现,不能通过 GridView 实现(滑动抵触,下文会阐明)

左侧导航条=> 一个简略的 Column(不能用 ListView,同样会造成滑动抵触)

交互剖析 & 实现

放大放大拖动成果:

对于放大放大拖动的成果,Flutter 当初有自带的组件 InteractiveViewer

通过这个组件可完满实现放大放大成果。组件属性这边不开展解释,比较简单,可点击下面链接自行理解。

这里讲下两个重点属性:

1、回调事件

  • 交互开始 onInteractionStart
  • 交互更新 onInteractionUpdate
  • 交互完结 onInteractionEnd

2、变换控制器transformationController

能够通过这个类来通过代码管制放大放大成果

导航条追随座位表放大放大拖动:

右边导航条追随两头座位的放大放大,以及行数定位不偏离:

下面讲的那些货色个别大家都能想到,也很好实现。这个交互成果的真正难点是这个 追随滑动成果

因为右边的导航条是固定在最左侧的,而座位表能够全屏拖动,所以这座位表和导航条不能放在一个缩放组件里,不然座位表放大的时候,间接将导航条放大出屏幕了。所以咱们的思路就是将导航条和座位表作为 Stack 的子组件,而后座位表实现放大放大成果,并且让导航条能追随座位表进行放大放大。笔者在这试了很多办法:

办法一:

左侧导航栏和两头座位表均应用InteractiveViewer

而后通过InteractiveViewe r 的回调事件和变换控器来实现成果同步

后果:

失败,transformationController的原理是 Matrix4 泛型的ValueNotifier(四维矩阵),简略的挪动放大还能实现,齐全克隆一个放大放大拖动成果,笔者做不到。。各位如果线性代数十分牛逼的能够试试。

办法二:

flutter 有一个同步滚动组件叫linked_scroll_controller

他能将两个 scrollController 绑定在一起,实现同步滚动。

所以让左侧导航栏应用 ListView, 两头座位表应用 InteractiveViewer 嵌套 GridView, 而后将 ListViewGridViewScrollController 绑定在一起实现同步滚动。

后果:

失败,InteractiveViewer的滑动是通过 Matrix4 实现的,和 ListView 的滑动抵触。

同步滚动实现了,然而放大放大的拖动无奈执行。

办法三:

应用 InteractiveViewer 是逃不过的,不然本人实现放大放大成果太头疼,如果能像下面的 linked_scroll_controller 一样,将 InteractiveViewer 的缩放成果复制到另外一个 InteractiveViewer 中去,那就完满了。

就是办法一的思路,然而用 InteractiveViewer 凋谢的接口和控制器,无奈实现,这个时候就须要去浏览了解 InteractiveViewer 的源码,看看有没有什么启发。

@override
  Widget build(BuildContext context) {
    Widget child = Transform(
      transform: _transformationController.value,
      child: KeyedSubtree(
        key: _childKey,
        child: widget.child,
      ),
    );

    if (!widget.constrained) {
      child = OverflowBox(
        alignment: Alignment.topLeft,
        minWidth: 0.0,
        minHeight: 0.0,
        maxWidth: double.infinity,
        maxHeight: double.infinity,
        // maxHeight: 220.w,
        child: child,
      );
    }

    if (widget.clipBehavior != Clip.none) {
      child = ClipRRect(
        clipBehavior: widget.clipBehavior,
        child: child,
      );
    }

    // A GestureDetector allows the detection of panning and zooming gestures on
    // the child.
    return Listener(
      key: _parentKey,
      onPointerSignal: _receivedPointerSignal,
      child: GestureDetector(
        behavior: HitTestBehavior.opaque,
        // Necessary when panning off screen.
        dragStartBehavior: DragStartBehavior.start,
        onScaleEnd: onScaleEnd,
        onScaleStart: onScaleStart,
        onScaleUpdate: onScaleUpdate,
        child: child,
      ),
    );
  }

不看不晓得,一看吓一跳,其实 InteractiveViewer 曾经将所有的办法都替咱们封装好了。

留神下面的 GestureDetector,整个InteractiveViewer 的手势交互办法,其实就是 onScaleEndonScaleStartonScaleUpdate 这三个办法。

那咱们只须要将座位表组件回调的的这三个办法中的参数,传入到导航条组件中去就行,而后删掉导航条组件的GestureDetector,让导航条组件只承受来自座位表组件的手势交互参数。

咱们只需重写两个InteractiveViewer,一个为主组件(座位表),一个为从组件(导航条),并凋谢 InteractiveViewerState,当座位表组件回调手势的三个办法时,通过 key 将三个办法的参数传入导航条组件就 OK。

_onInteractionUpdate(ScaleUpdateDetails details) {if (controller.fromInteractiveViewKey.currentState != null) {controller.fromInteractiveViewKey.currentState.onScaleUpdate(details);
    }
  }

  _onInteractionStart(ScaleStartDetails details) {if (controller.fromInteractiveViewKey.currentState != null) {controller.fromInteractiveViewKey.currentState.onScaleStart(details);
    }
  }

  _onInteractionEnd(ScaleEndDetails details) {if (controller.fromInteractiveViewKey.currentState != null) {controller.fromInteractiveViewKey.currentState.onScaleEnd(details);
    }
  }

齐全无需任何加工,将参数照搬照抄的传入导航条组件。咱们就能实现同步缩放拖动的成果!

这里必须特地留神:座位表和导航条组件的单个 item 的高度必须完全相同,包含 margin,padding,不然还是会呈现错位景象

至此,最大的难点同步缩放和滑动就解决了。

底部弹框悬浮在座位表上方:

点击座位后弹出底部弹框,遮蔽局部座位表,然而座位表能继续向上拖动显示完最初一行的数据

这个乍一看没啥难的,但细细一想也有点简单。首先,明确座位表的显示区域是蕴含底部弹框的,因为底部弹框是悬浮在座位表下面的,那么咱们就只能应用 margin 而不是 padding,所以依据设计图底部弹框的height,咱们将marginBottom 设成这个 height 就行,然而会有个问题:

当整个座位表放大 margin 局部也会同步放大,这样就会导致放的越大,座位表间隔上面空出的间距就越大。

解决思路:

咱们须要拿到以后放大的倍数,动静调整 margin,以后放大 X 倍,原始 margin 为 Y,则以后放大后的 margin=Y/X,Y 已知,咱们只须要晓得 X 就行。然而在_onInteractionUpdate 接口中,X 并非以后放大几倍,而是较上次缩放后的缩放倍数。即:

  • 初始 1.0 倍。
  • 第一次放大至 2 倍,接口回调的放大倍数为 2
  • 第二次放大至 3 倍,接口回调的放大倍数为 1.5(较第一次又放大了 1.5 倍)。

并且更重大的是当放大到 maxScale 后,接口仍会继续回调放大倍数。这就很困扰咱们,起初浏览源码后发现,咱们所要的较原始放大倍数的以后放大倍数参数在 InteractiveViewer 类中的。

// Return a new matrix representing the given matrix after applying the given
  // scale.
  Matrix4 _matrixScale(Matrix4 matrix, double scale) {if (scale == 1.0) {return matrix.clone();
    }
    assert(scale != 0.0);

    // Don't allow a scale that results in an overall scale beyond min/max
    // scale.
    final double currentScale =
        _transformationController.value.getMaxScaleOnAxis();
    final double totalScale =currentScale * scale;
    // 改了算法
    // final double totalScale = math.max(
    //   currentScale * scale,
    //   // Ensure that the scale cannot make the child so big that it can't fit
    //   // inside the boundaries (in either direction).
    //   math.max(
    //     _viewport.width / _boundaryRect.width,
    //     _viewport.height / _boundaryRect.height,
    //   ),
    // );
    final double clampedTotalScale = totalScale.clamp(
      widget.minScale,
      widget.maxScale,
    );

    widget.scaleCallback?.call(clampedTotalScale);

    final double clampedScale = clampedTotalScale / currentScale;
    return matrix.clone()..scale(clampedScale);
  }

留神下面的 scaleCallback,这是笔者本人实现的回调办法,其中的 clampedTotalScale 就是咱们想要的较初始缩放倍数的以后放大倍数,即:初始 1.0 倍,第一次放大至 2 倍,接口回调的放大倍数为 2,第二次放大至 3 倍,接口回调的放大倍数为 3(较初始放大了 3 倍)。

且 clampedTotalScale 永远在 minScale 和 maxScale 的区间内。拿来即用十分不便。

下面代码中有一段算法被我正文掉了,这段代码的成果是:

InteractiveViewer 中的 child 曾经齐全显示的时候,则无奈再放大,即 minScale 不仅仅取决于咱们设置的值,还取决于 InteractiveViewer 的 child 显示成果,这里我不须要这个限度,则将他正文掉了。

其实如果要完满实现 UI 给出的成果,有很多中央要用到 margin, 比方座位表的上下左右 margin, 只有拿到了下面的clampedTotalScale,均能够动静计算,很不便。

横竖屏适配成果

下面的 gif 图有横屏成果,横竖屏切换用的也是官网 API,OrientationBuilder,这个用起来也很简略。这里讲一个 UI 适配的注意事项:

因为笔者我的项目用了 ScreenUtil(UI 自适应),所以在竖屏的时候,传入竖屏的 UI 尺寸图,且尺寸结尾应用.w 进行适配,当横屏时,传入横屏的 UI 尺寸图(其实就是将竖屏的 width 和 height 倒置),而后尺寸结尾应用.h 进行适配。这样就根本能完满适配横竖屏,残余的细节就能够微调。

初始放大倍数

如下面的效果图,在第一次进入或横竖屏切换时,当座位表布局过多(默认显示不下时),尽可能放大以显示更多的内容(上限放大至minScale),当座位表布局过少(默认显示时屏幕很空),尽可能放大直至显示满屏幕(下限放大至maxScale)。

下面成果可总结为:在尽可能显示齐全的前提下尽可能大。

InteractiveViewer并没有初始放大倍数参数,默认进入都是放大 1.0 倍。这里就须要咱们本人来算出这个初始放大倍数。

计算

如果有用 screenUtil,以下计算留神辨别横竖屏, 横屏时适配结尾用.w, 竖屏用.h,其中异形屏的padding 不必辨别横竖屏,零碎会主动更改

  • 1、整个座位表的显示区域:

屏幕高 - 异形屏高低 padding- 竖屏时底部悬浮框的 height(横屏悬浮框如果不在底部,则为 0)- 标题栏高度以及本人加的一些其余布局的高度。屏幕宽 - 异形屏左右 padding- 横屏时右侧悬浮框 width(竖屏时悬浮框如不在右侧,则为 0)- 导航条宽度(这个导航栏宽度也须要依据放大放大倍数动静计算)- 其余本人加的布局宽。

  • 2、计算初始放大倍数(1.0)下的座位表 item 的 width 和 height 以及 padding, 这个间接按设计图的就行。
  • 3、取得以后座位表的 x 轴和 y 轴。即每行几个座位,一共有几行座位。
  • 4、计算假如要将所有座位表显示下,每个 item 的 width 和 height。

即用下面 1. 所得的座位表显示区域的宽高别离除以座位表的 x 和 y,

  • 5、将 2. 的 width 除以 4.width,即如 X 轴齐全显示下须要缩放的值 SX,

将 2. 的 height 除以 4.height,即如 Y 轴齐全显示下须要缩放的值 SY,

  • 6、比拟 SX 和 SY 两值,取小值 defaultS(在尽可能显示齐全的前提下尽可能大)
  • 7、如果 defaultS 在 minScale 和 maxScale 区间内,则取 defaultS,反之取区间边界值。

缩放

transformationControllerInteractiveViewer缩放到defaultS

// 座位表
mainTransformationController.value = Matrix4.identity()..scale(defaultS);
// 导航条
fromTransformationController.value = Matrix4.identity()..scale(defaultS);

这里留神座位表和导航条都要进行缩放。

缩放动静 margin

最初别忘记将各种须要动静计算的 margin 也缩放到 defaultS 值。

如果有横竖屏切换成果的,在每次横竖屏切换的时候都动静计算初始放大值,须要留神,每次计算的时候都要将动静计算的 margin 置为初始值(即当缩放大小为 1.0 时的 margin 值)。

有时候想不进去就看源码,立马就会醍醐灌顶。

Android 高级开发零碎进阶笔记、最新面试温习笔记 PDF,我的 GitHub

文末

您的点赞珍藏就是对我最大的激励!
欢送关注我,分享 Android 干货,交换 Android 技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!

退出移动版