Flutter堆叠布局插件mix_stack的使用

Flutter堆叠布局插件mix_stack的使用

MixStack 是一个能够帮助你在混合开发(Hybrid Development)中顺畅地连接 Flutter 和原生页面的库。它支持多标签嵌入式 Flutter 视图、动态标签更改等功能,使得从旧的原生代码迁移到 Flutter 变得更加简单。

MixStack基本结构

如上图所示,每个由 MixStack 提供的原生 Flutter 容器都包含一个独立的 Flutter 导航器来管理页面,通过 Flutter 中的 Stack 小部件让 Flutter 渲染当前原生 Flutter 容器所属的 Flutter 页面栈。这种方式使得 Flutter 和原生页面导航及各种视图交互成为可能且易于管理。

开始使用

混合开发有时会显得有些复杂,请耐心一点。

安装

pubspec.yaml 文件中添加 mixstack 依赖:

dependencies:
  mix_stack: <lastest_version>

运行以下命令以获取依赖:

flutter pub get

在 iOS 文件夹中运行以下命令:

pod install

在 Android 的 build.gradle 文件中添加以下代码:

implementation rootProject.findProject(":mix_stack")

如何集成

在 Flutter 端

在你的 Flutter 项目中找到根小部件,并在初始小部件的构建方法中添加 MixStackApp,将路由生成函数传递给 MixStackApp,例如:

class MyApp extends StatelessWidget {
  void defineRoutes(Router router) {
    router.define('/test', handler: Handler(handlerFunc: (ctx, params) => TestPage()));
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    defineRoutes(router);
    return MixStackApp(routeBuilder: (context, path) {
      return router.matchRoute(context, path).route; // 传回 route
    });
  }
}

这样,Flutter 部分就完成了。详细用法请参见 Flutter 侧用法详情

在 iOS 端

当 FlutterEngine 执行运行后,将引擎设置为 MXStackExchange:

// AppDelegate.m
[flutterEngine run];
[MXStackExchange shared].engine = flutterEngine;

这样,iOS 部分就完成了。详细用法请参见 iOS 侧用法详情

在 Android 端

确保在应用的 onCreate() 方法中执行以下操作:

MXStackService.init(application);

这样,Android 部分就完成了。详细用法请参见 Android 侧用法详情

Flutter 侧用法

监听容器的导航器

如果你需要监听容器内的导航器,可以在 MixStackApp 初始化时添加额外的观察者构建器:

MixStackApp(
  routeBuilder: (context, path) {
    return router.matchRoute(context, path).route;
  },
  observersBuilder: () {
    return [CustomObserver()];
  },
)
控制原生 UI 显示

当你有原生视图与 Flutter 容器混合在一起时,有时你可能希望隐藏这些原生视图,比如当你在 Flutter 中推入新页面时,就像下图所示:

你可以通过使用 NativeOverlayReplacer 来实现这一点。

当你需要这样做时,只需将页面的根小部件包裹在 NativeOverlayReplacer 中,并填写你在原生端注册的原生叠加层视图名称:

[@override](/user/override)
Widget build(BuildContext context) {
  return NativeOverlayReplacer(child: Container(), autoHidesOverlayNames: [MXOverlayNameTabBar, 'NativeOverlay1', 'NativeOverlay2']);
}

我们还提供了一个简单的接口来隐藏 MixStack 提供的原生标签栏,以便简化常见的需求,即你嵌入了一个 Flutter 标签页,该标签页有自己的导航栈,当推入新页面时,需要隐藏原生标签栏:

[@override](/user/override)
Widget build(BuildContext context) {
  return NativeOverlayReplacer.autoHidesTabBar(child: Container());
}
``

如果你想要更精细的控制,可以调整 `persist` 属性:

```dart
List<String> names = await MixStack.getOverlayNames(context); // 获取当前原生叠加层名称
names = names.where((element) => element.contains('tabBar')).toList();
NativeOverlayReplacer.of(context).registerAutoPushHiding(names, persist: false); // 如果我们希望这个注册只对单次推入动作有效,我们将持久化设为 false,如果希望每次推入页面时都有效,将其设为 true

在上述代码设置之后,每次页面推入操作都会触发原生 UI 组件的设置,并在 Flutter 中提供原生 UI 组件快照,以提供平滑动画并让用户忽略混合结构。

直接弹出当前 Flutter 容器
MixStack.popNative(context);
强制调整当前 Flutter 容器的返回手势状态

MixStack 一直很好地处理这个问题,但有时你可能需要这种能力,请确保你不会自断手脚:

MixStack.enableNativePanGensture(context, true);
Flutter 响应当前容器事件

有时你可能需要 Flutter 代码响应特定的原生调用,这可以通过以下方式实现:

// 某些小部件代码
void initState() {
  super.initState();
  // 根页面注册一个导航器 popToRoot 动作
  if (!Navigator.of(context).canPop()) {
    PageContainer.of(context).addListener('popToTab', (query) {
      Navigator.of(context).popUntil((route) {
        return route.isFirst;
      });
    });
  }
}
手动调整原生叠加层
NativeOverlayReplacer.of(ctx)
  .configOverlays([NativeOverlayConfig(name: MXOverlayNameTabBar, hidden: false, alpha: 1)]);
进阶:订阅 Flutter 应用生命周期

MixStack 提供了整个 Flutter 应用生命周期的监听功能,由于其结构与原始 Flutter 生命周期有所不同,如果你需要进行可见性检查或其他操作,请考虑使用此功能:

MixStack.lifecycleNotifier.addListener(() {
  print('Lifecycle:${MixStack.lifecycleNotifier.value}');
});
进阶:订阅当前容器的安全区域边距变化

如果 Flutter 侧 UI 组件想了解原生容器边距的变化,可以执行以下操作:

PageContainerInfo containerInfo;
[@override](/user/override)
void initState() {
  super.initState();
  // 获取当前容器信息,并订阅更改
  containerInfo = PageContainer.of(context).info;
  bottomInset = containerInfo.insets.bottom;
  containerInfo.addListener(updateBottomInset);
}

[@override](/user/override)
void dispose() {
  // 取消订阅
  containerInfo.removeListener(updateBottomInset);
  super.dispose();
}

void updateBottomInset() {
  bottomInset = PageContainer.of(context).info.insets.bottom;
}
``

请注意,此订阅仅传递安全区域边距更改,如果你想了解更多关于原生容器的信息,请使用 `MixStack.overlayInfos` 获取信息。

```dart
double bottomInset = 0;
[@override](/user/override)
Widget build(BuildContext context) {
  // 获取原生叠加层信息以调整 UI 边距
  MixStack.overlayInfos(context, [MXOverlayNameTabBar], delay: Duration(milliseconds: 400)).then((value) {
    if (value.keys.length == 0) {
      return;
    }
    final info = value[MXOverlayNameTabBar];
    double newInset = info.hidden == false ? info.rect.height : 0;
    if (bottomInset != newInset) {
      setState(() {
        bottomInset = newInset;
      });
    }
  });
  return Stack(
    children: [
      Positioned(
        bottom: bottomInset + 10,
        right: 20,
        child: FloatingActionButton(
          onPressed: () {
            print("Floating button follow insets move");
          },
        ),
      ),
    ],
  );
}

iOS 侧用法

在 TabBarController 中放置多个 Flutter 页面

MixStack 提供了 MXAbstractTabBarController 类用于子类化,主要为了调整标签页边距和处理标签页可见性。如果你需要更多功能,可以自行实现。

当你想添加一个或多个 Flutter 视图时,请使用 MXContainerViewController 作为标签页的子视图控制器。如果某个 MXContainerViewController 要嵌入到另一个视图控制器中,请标记根视图控制器为我们的自定义标签:

vc.containsFlutter = YES;

将 Flutter 页面添加到 Tab 的示例代码如下:

TabViewController *tabs = [[TabViewController alloc] init];
UITabBarItem *item1 = [[UITabBarItem alloc] initWithTitle:@"Demo1" image:[UIImage imageNamed:@"icon1"] tag:0];
UITabBarItem *item2 = [[UITabBarItem alloc] initWithTitle:@"Demo2" image:[UIImage imageNamed:@"icon2"] tag:0];
MXContainerViewController *flutterVC1 = [[MXContainerViewController alloc] initWithRoute:@"/test" barItem:item1];
MXContainerViewController *flutterVC2 = [[MXContainerViewController alloc] initWithRoute:@"/test_2" barItem:item2];
SomeNativeVC *nativeVC = [[SomeNativeVC alloc] init]; // 当然你也可以添加正常的原生视图 ^_^
[tabs setViewControllers:@[flutterVC1, flutterVC2, nativeVC]];
将 Flutter 容器作为正常视图控制器使用

MixStack 的 MXContainerViewController 可以在正常场景中使用。只需传递与目标页面匹配的 Flutter 路由:

MXContainerViewController *flutterVC = [[MXContainerViewController alloc] initWithRoute:@"/test"];
[self.navigationController pushViewController:flutterVC animated:YES];
弹出 Flutter 容器

假设当前页面是一个 Flutter 容器,且你不确定是否需要弹出此视图控制器,或者你需要弹出容器内包含的一个页面,你可以尝试如下方法:

[[MXStackExchange shared] popPage:^(BOOL popSuccess) {
  if (!popSuccess) {
    [self.navigationController popViewControllerAnimated:YES];
  }
}];

如果结果为真,意味着 Flutter 容器内的导航栈中有一个页面被弹出,并且还有页面存在。如果结果为假,则意味着当前容器内的导航栈只剩下一个根页面,因此你可以安全地弹出整个容器。

向 Flutter 容器的 Flutter 栈提交事件
MXContainerViewController *flutterVC = (MXContainerViewController *)self.tab.viewControllers.lastObject;
[flutterVC sendEvent:@"popToTab" query:@{ @"hello" : @"World" }];
获取当前 Flutter 容器的导航历史

如果返回空,则意味着当前容器内无页面。

MXContainerViewController *flutterVC = ...;
[flutterVC currentHistory:^(NSArray<NSString *> *_Nullable history) {
  NSLog(@"%@", history);
}];
锁定当前容器引擎渲染

有时我们需要在 Flutter 容器上方显示一些 UI,如一些弹出窗口。由于 Flutter 渲染机制,在这样做时,Flutter 视图会变黑。因此,我们提供了一个快照机制,当你将 showSnapshot 设置为 true 时,整个视图将被快照并冻结。当你完成业务后,可以将其恢复为 false。

MXContainerViewController *fc = ...
fc.showSnapshot = YES;

iOS 进阶用法 & Q&A

多窗口使用中的潜在问题

在某些情况下,iOS 端可能会使用多窗口方式管理 UI,可能会出现两个窗口都包含 Flutter 容器的情况,并且已知不同窗口的视图控制器不会接收可见性回调。当你遇到这种情况时,可以使用以下代码手动引导 MixStack 将 FlutterEngine 的视图控制器放回到正确的业务窗口:

[MXStackExchange shared].engineViewControllerMissing = ^id<MXViewControllerProtocol> _Nonnull(NSString *_Nonnull previousRootRoute) {
  return someFlutterVCFitsYourBusiness;
};
自定义支持 NativeOverlayReplacer

确保 Flutter 容器视图控制器实现了 MXOverlayHandlerProtocol 接口。你可以参考相关示例代码,如 MXAbstractTabBarController

什么是 ignoreSafeareaInsetsConfig

在 MixStack 的 MXOverlayHandlerProtocol 中,有一个名为 ignoreSafeareaInsetsConfig 的方法,基于一个事实,即在大多数情况下,MixStack 建议将导致 SafeAreaInsets 为零的叠加层设置为零。也就是说,Flutter 渲染层应该不知道原生 UI 的 SafeAreaInsets 更改,建议在特定容器中实现 ignoreSafeareaInsetsConfig,然后在 Flutter 中使用 MixStack.of(context).overlayInfos 获取叠加层更改信息以调整 UI。

Android 用法

使用 MXFlutterActivity

我们提供了 MXFlutterActivity 供直接使用,只需传递在 Flutter 中注册的目标页面路由:

Intent intent = new Intent(getActivity(), MXFlutterActivity.class);
intent.putExtra(ROUTE, "/test_blue"); // 传递在 Flutter 中注册的目标页面路由
startActivity(intent);
使用 MXFlutterFragment 在 Activity 和 Tab 中

我们提供了 MXFlutterFragment 供片段使用,类似于 MXFlutterActivity,也需要传递在 Flutter 中注册的目标页面路由:

MXFlutterFragment flutterFragment = new MXFlutterFragment();
Bundle bundle = new Bundle();
bundle.putString(MXFlutterFragment.ROUTE, "/test_blue");
flutterFragment.setArguments(bundle);

对于 Tab 切换,我们需要对 MXFlutterFragment 进行控制,对于 MXFlutterFragment 的宿主 Activity,你需要实现 IMXPageManager 接口,它只有一个函数 getPageManager(),主要用于在 Activity 中获取 MXPageManager,这有两个目的:

  • 控制 MXFlutterFragment 页面生命周期
  • 提供 Flutter 页面的原生 UI 控制能力

经典情况:活动中有多个标签页,每个指向不同的片段,你需要 IMXPageManager 来控制显示哪个片段,并正确设置 MXFlutterFragment 的生命周期,如下所示:

private void showFragment(Fragment fg) {
  FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
  if (currentFragment != fg) {
    transaction.hide(currentFragment);
  }
  if (!fg.isAdded()) {
    transaction.add(R.id.fl_main_container, fg, fg.getClass().getName());
  } else {
    transaction.show(fg);
  }
  transaction.commit();
  currentFragment = fg;

  pageManager.setCurrentShowPage(currentFragment); // 告诉 MixStack 显示哪个 MXFlutterFragment
}

由于 Flutter 与 Android 的机制不同,我们需要覆盖返回逻辑。因此在 MXFlutterFragment 的宿主 Activity 中,我们需要添加以下代码:

@Override
public void onBackPressed() {
  if (pageManager.checkIsFlutterCanPop()) {
    pageManager.onBackPressed(this);
  } else {
    super.onBackPressed();
  }
}

当 Activity 被销毁时,我们也需要通过 IMXPageManager 通知 MixStack:

@Override
protected void onDestroy() {
  super.onDestroy();
  pageManager.onDestroy();
}
管理 Flutter 容器的原生 UI

我们还使用 PageManager 来实现这一点,如果你不需要管理原生 UI,可以直接初始化一个,否则需要重写方法,有四个方法需要重写:

  • overlayViewsForNames 获取视图和名称之间的映射
  • configOverlay 配置如何显示、动画化原生叠加层视图,默认有动画
  • overlayNames 获取可用名称
  • overlayView 通过名称获取叠加层视图

示例如下:

MXPageManager pageManager = new MXPageManager() {
  @Override
  public List<String> overlayNames() {
    List<String> overlayNames = new ArrayList<>();
    overlayNames.add("tabBar");
    return overlayNames;
  }

  @Override
  public View overlayView(String viewName) {
    if ("tabBar".equals(viewName)) {
      return mBottomBar;
    }
    return null;
  }
};
向 FlutterFragment/FlutterActivity 提交事件
flutterFragment.sendEvent("popToTab", query);
flutterActivity.sendEvent("popToTab", query);
获取 Flutter 容器的导航栈历史
pageManager.getPageHistory(new PageHistoryListener() {
  @Override
  public void pageHistory(List<String> history) {
  }
});
销毁

在 MainActivity 的 onDestroy 被调用时销毁

MXStackService.getInstance().destroy();

更多关于Flutter堆叠布局插件mix_stack的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter堆叠布局插件mix_stack的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,mix_stack 是一个用于 Flutter 的堆叠布局插件,它提供了比原生 Stack 更强大和灵活的布局功能。下面是一个关于如何使用 mix_stack 的代码示例,以展示其基本用法。

首先,确保在你的 pubspec.yaml 文件中添加 mix_stack 依赖:

dependencies:
  flutter:
    sdk: flutter
  mix_stack: ^最新版本号  # 请替换为实际最新版本号

然后,运行 flutter pub get 来获取依赖。

以下是一个使用 mix_stack 的示例代码:

import 'package:flutter/material.dart';
import 'package:mix_stack/mix_stack.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('MixStack Demo'),
        ),
        body: Center(
          child: MixStack(
            alignment: Alignment.center,
            children: [
              // 第一个子组件:一个红色的圆形
              MixStackItem(
                child: Container(
                  width: 100,
                  height: 100,
                  decoration: BoxDecoration(
                    color: Colors.red,
                    shape: BoxShape.circle,
                  ),
                ),
                // 设置堆叠顺序,数值越小越在上层
                zIndex: 1,
              ),

              // 第二个子组件:一个蓝色的矩形
              MixStackItem(
                child: Container(
                  width: 150,
                  height: 150,
                  decoration: BoxDecoration(
                    color: Colors.blue.withOpacity(0.5),
                    borderRadius: BorderRadius.circular(10),
                  ),
                ),
                // 设置堆叠顺序,数值比上面的大,所以在下层
                zIndex: 2,
              ),

              // 第三个子组件:一个带有文本的白色矩形
              MixStackItem(
                child: Container(
                  width: 200,
                  height: 50,
                  decoration: BoxDecoration(
                    color: Colors.white,
                    borderRadius: BorderRadius.circular(10),
                  ),
                  child: Center(
                    child: Text(
                      'Hello MixStack!',
                      style: TextStyle(color: Colors.black, fontSize: 20),
                    ),
                  ),
                ),
                // 设置堆叠顺序,数值最大,在最下层
                zIndex: 3,
                // 可以设置相对于父容器的偏移量
                position: Offset(0, -50),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

在这个示例中,我们使用了 MixStack 来堆叠三个不同的组件:

  1. 一个红色的圆形。
  2. 一个半透明的蓝色矩形。
  3. 一个带有文本的白色矩形,并通过 position 属性设置了相对于父容器的偏移量。

每个子组件都被包裹在 MixStackItem 中,并通过 zIndex 属性来控制它们的堆叠顺序。数值越小,组件越在上层。

希望这个示例能够帮助你理解如何使用 mix_stack 插件进行堆叠布局。如果你有更具体的需求或问题,欢迎进一步提问!

回到顶部