导语

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

间接上效果图

竖屏:

横屏:

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

布局剖析

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

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

交互剖析&实现

放大放大拖动成果:

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

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

这里讲下两个重点属性:

1、回调事件

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

2、变换控制器transformationController

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

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

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

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

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

办法一:

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

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

后果:

失败,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技术。
对文章有何见解,或者有何技术问题,欢送在评论区一起留言探讨!