先看效果實作

需求分析
這個是使用 PopupRoute這個路由類進行實作
大概原理就是利用PopupRpute這個類進行改造,然后自定義一個頁面,頁面內鑲嵌一個影片類,用來實作縮放影片
大概分為三部分,PopupRoute改造,彈出頁面設定,影片類設定,
為什么選擇PopupRoute?
可以鑲嵌在flutter本身的路由管理之中
也就是邏輯操作都是正常的頁面管理,可以手動管理,也可以用路由回傳直接關掉,不會影響原有頁面和布局
第一步,改造PopupRoute類
import 'package:flutter/material.dart';class Popup extends PopupRoute { final Duration _duration = Duration(milliseconds: 300); Widget child;
Popup({@required this.child});
@override Color get barrierColor => null;
@override bool get barrierDismissible => true;
@override String get barrierLabel => null;
@override Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) { return child; }
@override Duration get transitionDuration => _duration; }
第二步,新建一個彈窗頁面
頁面分兩部分

一個是頁面的背景,一個是頁面的內容
注意,彈窗影片的代碼在下方
class Model extends StatefulWidget { final double left; //距離左邊位置 彈窗的x軸定位 final double top; //距離上面位置 彈窗的y軸定位 final bool otherClose; //點擊背景關閉頁面 final Widget child; //傳入彈窗的樣式 final Function fun; // 把關閉的函式回傳給父組件 參考vue的$emit final Offset offset; // 彈窗影片的起點Model({ @required this.child, this.left = 0, this.top = 0, this.otherClose = false, this.fun, this.offset, });
@override _ModelState createState() => _ModelState(); }
class _ModelState extends State<Model> { AnimationController animateController;
@override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: Stack( children: <Widget>[ Positioned( child: GestureDetector( child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, color: Colors.transparent, ), onTap: () async { if (widget.otherClose) { } else { closeModel(); } }, ), ), Positioned( /// 這個是彈窗影片 在下方,我把他分離 防止太長 child: ZoomInOffset( duration: Duration(milliseconds: 180), offset: widget.offset, controller: (controller) { animateController = controller; widget.fun(closeModel); }, child: widget.child, ), left: widget.left, top: widget.top, ), ], ), ); }
///關閉頁面影片 Future closeModel() async { await animateController.reverse(); Navigator.pop(context); } }
影片代碼
我是直接復制 animate_do: ^2.0.0 這個版本的ZoomIn的影片類
這個插件本身就是依賴flutter 自帶的影片來完成的,很簡潔,使用很方便, 不過默認構造的時候沒有影片的啟動方向,默認是最中心, 但是可以添加個引數,我把原始碼復制出來自己改造了一下, 這個類在構造的時候有個 controller 引數,型別的函式,帶一個AnimationController的引數 把控制器通過函式傳遞出去到Model類,可以在Model類里面進行控制影片開啟和關閉 后續我在Model類里面把影片關閉和回傳退出PopupRoute層封裝成一個函式 傳遞到Model里面的fun引數里面回傳出去 可以在最外部進行組件通信,進而控制這些子組件 import 'package:flutter/material.dart';class ZoomInOffset extends StatefulWidget { final Key key; final Widget child; final Duration duration; final Duration delay;
///把控制器通過函式傳遞出去,可以在父組件進行控制 final Function(AnimationController) controller; final bool manualTrigger; final bool animate; final double from;
///這是我自己寫的 起點 final Offset offset;
ZoomInOffset( {this.key, this.child, this.duration = const Duration(milliseconds: 500), this.delay = const Duration(milliseconds: 0), this.controller, this.manualTrigger = false, this.animate = true, this.offset, this.from = 1.0}) : super(key: key) { if (manualTrigger == true && controller == null) { throw FlutterError('If you want to use manualTrigger:true, \n\n' 'Then you must provide the controller property, that is a callback like:\n\n' ' ( controller: AnimationController) => yourController = controller \n\n'); } }
@override _ZoomInState createState() => _ZoomInState(); }
/// State class, where the magic happens class _ZoomInState extends State<ZoomInOffset> with SingleTickerProviderStateMixin { AnimationController controller; bool disposed = false; Animation<double> fade; Animation<double> opacity;
@override void dispose() async { disposed = true; controller.dispose(); super.dispose(); }
@override void initState() { super.initState();
controller = AnimationController(duration: widget.duration, vsync: this); fade = Tween(begin: 0.0, end: widget.from) .animate(CurvedAnimation(curve: Curves.easeOut, parent: controller));
opacity = Tween<double>(begin: 0.0, end: 1) .animate(CurvedAnimation(parent: controller, curve: Interval(0, 0.65)));
if (!widget.manualTrigger && widget.animate) { Future.delayed(widget.delay, () { if (!disposed) { controller?.forward(); } }); }
if (widget.controller is Function) { widget.controller(controller); } }
@override Widget build(BuildContext context) { if (widget.animate && widget.delay.inMilliseconds == 0) { controller?.forward(); }
return AnimatedBuilder( animation: fade, builder: (BuildContext context, Widget child) { /// 這個transform有origin的可選構造引數,我們可以手動添加 return Transform.scale( origin: widget.offset, scale: fade.value, child: Opacity( opacity: opacity.value, child: widget.child, ), ); }, ); } }
最后頁面呼叫
我用stack類進行堆疊組件,堆疊出上面箭頭
其實可以抽成一個方向設定不過太麻煩了我沒寫,畢竟能用就行
import 'package:flutter/material.dart'; import 'package:one/widget/Model.dart'; import 'package:one/widget/Popup.dart';void main() { runApp(MyApp()); }
class MyApp extends StatefulWidget { @override _MyAppState createState() => _MyAppState(); }
class _MyAppState extends State<MyApp> { ///給獲取詳細資訊的widget設定一個key GlobalKey iconkey = GlobalKey();
///獲取位置,給后續彈窗設定位置 Offset iconOffset;
///獲取size 后續計算彈出位置 Size iconSize;
///接受彈窗類構造成功傳遞來的關閉引數 Function closeModel;
@override Widget build(BuildContext context) { ///等待widget初始化完成 WidgetsBinding.instance.addPostFrameCallback((duration) { ///通過key獲取到widget的位置 RenderBox box = iconkey.currentContext.findRenderObject();
///獲取widget的高寬 iconSize = box.size;
///獲取位置 iconOffset = box.localToGlobal(Offset.zero); });
return MaterialApp( home: Builder( builder: (context) => Scaffold( appBar: AppBar( actions: [ IconButton( key: iconkey, icon: Icon( Icons.favorite, color: Colors.red, ), onPressed: () { showModel(context); }, ), ], ), body: Column( children: [], ), ), ), ); }
///播放影片 void showModel(BuildContext context) { /// 設定傳入彈窗的高寬 double _width = 130; double _height = 230;
Navigator.push( context, Popup( child: Model( left: iconOffset.dx - _width + iconSize.width / 1.2, top: iconOffset.dy + iconSize.height / 1.3, offset: Offset(_width / 2, -_height / 2), child: Container( width: _width, height: _height, child: buildMenu(), ), fun: (close) { closeModel = close; }, ), ), ); }
///構造傳入的widget Widget buildMenu() { ///構造List List _list = [1, 2, 3, 4, 5];
return Container( height: 160, width: 230, child: Stack( children: [ Positioned( right: 4, top: 17, child: Container( width: 20, height: 20, transform: Matrix4.rotationZ(45 * 3.14 / 180), decoration: BoxDecoration( color: Color.fromRGBO(46, 53, 61, 1), borderRadius: BorderRadius.circular(5), ), ), ),
///選單內容 Positioned( bottom: 0, child: Container( padding: EdgeInsets.only( top: 20, bottom: 20, left: 10, right: 10, ), width: 130, height: 200, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: Color.fromRGBO(46, 53, 61, 1), ), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: _list .map<Widget>((e) => InkWell( child: Container( width: double.infinity, alignment: Alignment.center, child: Text( '這應該是選項${e.toString()}', style: TextStyle( color: Colors.white70, fontSize: 14, ), ), ), onTap: () async { print('這是點擊了選項${e.toString()}'); await Future.delayed(Duration(milliseconds: 500)) .then((value) => print('開始')); await closeModel(); print('結束'); }, )) .toList(), ), ), ), ], ), ); } }
然后就能實作我們的彈窗影片了,如果想要其他效果的影片,可以手動替換影片類,或者自己手寫個新的 最后我自己的專案修飾效果,還有demo的代碼 代碼 倉庫地址:https://github.com/mannaoz/one
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/286127.html
標籤:Android
下一篇:詳講自定義影片的實作
