Commit 832193c1 authored by nightfallsad's avatar nightfallsad Committed by GitHub

Merge pull request #5 from alibaba/master

merge code
parents ad1b99c2 2f523fd6
---
name: Bug report
about: Describe this issue template's purpose here.
title: ''
labels: ''
assignees: ''
---
<!-- Thank you for using Flutter Boost!
If you are looking for support about how to get start, please read README first
-->
## Steps to Reproduce
**A small application to reproduce the bug(最小化可复现的demo)**
<!--
Please tell us exactly how to reproduce the problem you are running into.
Please attach a small application (ideally just one main.dart file) that
reproduces the problem. You could use https://gist.github.com/ for this.
If the problem is with your application's rendering, then please attach
a screenshot and explain what the problem is.
-->
1. ...
2. ...
3. ...
<!--
Please tell us which target platform(s) the problem occurs (Android / iOS / Web / macOS / Linux / Windows)
Which target OS version, for Web, browser, is the test system running?
Does the problem occur on emulator/simulator as well as on physical devices?
-->
**Flutter Boost Version**
**Target Platform:**
**Target OS version/browser:**
**Devices:**
## Logs
add your crash log or something else.
<!-- Finally, paste the output of running `flutter doctor -v` here. -->
```
```
# Configuration for probot-no-response - https://github.com/probot/no-response
# Number of days of inactivity before an Issue is closed for lack of response
daysUntilClose: 21
# Label requiring a response
responseRequiredLabel: more-information-needed
# Comment to post when closing an Issue for lack of response. Set to `false` to disable
closeComment: >
This issue has been automatically closed because there has been no response
to our request for more information from the original author. With only the
information that is currently in the issue, we don't have enough information
to take action. Please reach out if you have or find the answers we need so
that we can investigate further.
Thanks for your contribution.
...@@ -17,4 +17,3 @@ example/android/app/.classpath ...@@ -17,4 +17,3 @@ example/android/app/.classpath
example/android/app/.project example/android/app/.project
example/android/.project example/android/.project
flutter_boost flutter_boost
example
...@@ -9,6 +9,43 @@ The main changes are as following: ...@@ -9,6 +9,43 @@ The main changes are as following:
4. We did some code refactoring, the main logic became more straightforward. 4. We did some code refactoring, the main logic became more straightforward.
## 0.1.60
A better implementation to support Flutter v1.9.1+hotfixes
Change the content
android:
1. based on the v1.9.1+hotfixes branch of flutter
2. Solve major bugs, such as page parameter passing
3. Support platformview
4. Support androidx branch :feature/flutter_1.9_androidx_upgrade
5. Resolve memory leaks
6. Rewrite part of the code
7. API changes
8. Improved demo and added many demo cases
ios:
1.based on the v1.9.1+hotfixes branch of flutter
2.bugfixed
## 0.1.61
android:
Fixed bugs
iOS:
no change
## 0.1.63
android:
Fixed bugs
iOS:
no change
### API changes ### API changes
From the point of API changes, we did some refactoring as following: From the point of API changes, we did some refactoring as following:
#### iOS API changes #### iOS API changes
......
### 在FlutterBoost下如何管理Flutter页面的生命周期?原生的Flutter的AppLifecycleState事件会不一致,比如ViewAppear会导致app状态suspending或者paused。混合栈怎么处理?
回答:在混合栈下,页面事件基于以下自定义的事件:
```dart
enum ContainerLifeCycle {
Init,
Appear,
WillDisappear,
Disappear,
Destroy,
Background,
Foreground
}
```
对于页面事件重复,请参考下面的FAQ。
### 如何判断flutter的widget或者container是当前可见的?
回答:有个api可以判断当前页面是否可见:
```dart
bool isTopContainer = FlutterBoost.BoostContainer.of(context).onstage
```
传入你widget的context,就能判断你的widget是否是可见的
基于这个API,可以判断你的widget是否可见,从而避免接收一些重复的生命周期消息。参考这个issue:https://github.com/alibaba/flutter_boost/issues/498
### 您好,我想请教一下flutter_boost有关的问题:ABC三个都是flutter页面,从 A页面 -> B页面 -> C页面,当打开C页面时希望自动关掉B页面,当从C页面返回时直接返回A页面,可有什么方法?
回答:你只需要操作Native层的UINavigationController里的vc数组就可以了。就如同平时你操作普通的UIViewController一样。因为FlutterBoost对Native层的FlutterViewController和Dart层的flutter page的生命周期管理是一致的,当FlutterViewController被销毁,其在dart层管理的flutter page也会自动被销毁。
### 在ios中voice over打开,demo在点击交互会crash
回答:无障碍模式下目前Flutter Engine有bug,已经提交issue和PR给flutter啦。请参考这个issue:https://github.com/alibaba/flutter_boost/issues/488及其分析。提交给flutter的PR见这里:https://github.com/flutter/engine/pull/14155
### 似乎官方已经提供了混合栈的功能,参考这里:https://flutter.dev/docs/development/add-to-app; FlutterBoost是否有存在的必要?
回答:官方的解决方案仅仅是在native侧对FlutterViewController和Flutterengine进行解耦,如此可以一个FlutterEngine切换不同的FlutterViewController或者Activity进行渲染。但其并未解决Native和Flutter页面混合的问题,无法保证两侧的页面生命周期一致。即使是Flutter官方针对这个问题也是建议使用FlutterBoost。
其差别主要有:
|*|FlutterBoost1.5 |Flutter官方方案 |其他框架|
|----|----|----|----|
|是否支持混合页面之间随意跳转 |Y |N |Y|
|一致的页面生命周期管理(多Flutter页面) |Y |N |?|
|是否支持页面间数据传递(回传等) |Y |N |N|
|是否支持测滑手势 |Y |Y |Y|
|是否支持跨页的hero动画 |N |Y |N|
|内存等资源占用是否可控 |Y |Y |Y|
|是否提供一致的页面route方案 |Y |Y |N|
|iOS和Android能力及接口是否一致 |Y |N |N|
|框架是否稳定,支持Flutter1.9 |Y |N |?|
|是否已经支持到View级别混合 |N |N |N|
同时FlutterBoost也提供了一次性创建混合工程的命令:flutterboot。代码参考:https://github.com/alibaba-flutter/flutter-boot
...@@ -5,319 +5,87 @@ ...@@ -5,319 +5,87 @@
<a href="https://mp.weixin.qq.com/s?__biz=MzU4MDUxOTI5NA==&mid=2247484367&idx=1&sn=fcbc485f068dae5de9f68d52607ea08f&chksm=fd54d7deca235ec86249a9e3714ec18be8b2d6dc580cae19e4e5113533a6c5b44dfa5813c4c3&scene=0&subscene=131&clicktime=1551942425&ascene=7&devicetype=android-28&version=2700033b&nettype=ctnet&abtest_cookie=BAABAAoACwASABMABAAklx4AVpkeAMSZHgDWmR4AAAA%3D&lang=zh_CN&pass_ticket=1qvHqOsbLBHv3wwAcw577EHhNjg6EKXqTfnOiFbbbaw%3D&wx_header=1">中文介绍</a> <a href="https://mp.weixin.qq.com/s?__biz=MzU4MDUxOTI5NA==&mid=2247484367&idx=1&sn=fcbc485f068dae5de9f68d52607ea08f&chksm=fd54d7deca235ec86249a9e3714ec18be8b2d6dc580cae19e4e5113533a6c5b44dfa5813c4c3&scene=0&subscene=131&clicktime=1551942425&ascene=7&devicetype=android-28&version=2700033b&nettype=ctnet&abtest_cookie=BAABAAoACwASABMABAAklx4AVpkeAMSZHgDWmR4AAAA%3D&lang=zh_CN&pass_ticket=1qvHqOsbLBHv3wwAcw577EHhNjg6EKXqTfnOiFbbbaw%3D&wx_header=1">中文介绍</a>
</p> </p>
# Latest News
Currently, version 1.9 of flutter is supported.
flutter branch:v1.9.1-hotfixes
FlutterBoost branch:feature/flutter_1.9_upgrade
FlutterBoost for androidx branch:feature/flutter_1.9_androidx_upgrade
```java
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: 'feature/flutter_1.9_upgrade'
```
dingding group:
<img width="200" src="https://img.alicdn.com/tfs/TB1JSzVeYY1gK0jSZTEXXXDQVXa-892-1213.jpg">
# Release Note # Release Note
Please checkout the release note for the latest 0.1.54 to see changes [0.1.54 release note](https://github.com/alibaba/flutter_boost/releases) Please checkout the release note for the latest 0.1.63 to see changes [0.1.63 release note](https://github.com/alibaba/flutter_boost/releases)
# FlutterBoost # FlutterBoost
A next-generation Flutter-Native hybrid solution. FlutterBoost is a Flutter plugin which enables hybrid integration of Flutter for your existing native apps with minimum efforts.The philosophy of FlutterBoost is to use Flutter as easy as using a WebView. Managing Native pages and Flutter pages at the same time is non-trivial in an existing App. FlutterBoost takes care of page resolution for you. The only thing you need to care about is the name of the page(usually could be an URL).  A next-generation Flutter-Native hybrid solution. FlutterBoost is a Flutter plugin which enables hybrid integration of Flutter for your existing native apps with minimum efforts.The philosophy of FlutterBoost is to use Flutter as easy as using a WebView. Managing Native pages and Flutter pages at the same time is non-trivial in an existing App. FlutterBoost takes care of page resolution for you. The only thing you need to care about is the name of the page(usually could be an URL). 
<a name="bf647454"></a> <a name="bf647454"></a>
# Prerequisites # Prerequisites
You need to add Flutter to your project before moving on.The version of the flutter SDK requires v1.5.4-hotfixes, or it will compile error. You need to add Flutter to your project before moving on.The version of the flutter SDK requires v1.9.1+hotfixes, or it will compile error.
# Getting Started
## Add a dependency in you Flutter project.
Open you pubspec.yaml and add the following line to dependencies:
```java
flutter_boost: ^0.1.54
```
or you could rely directly on a Github project tag, for example(recommended)
```java
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '0.1.54'
```
## Integration with Flutter code.
Add init code to you App
```dart
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
///register page widget builders,the key is pageName
FlutterBoost.singleton.registerPageBuilders({
'sample://firstPage': (pageName, params, _) => FirstRouteWidget(),
'sample://secondPage': (pageName, params, _) => SecondRouteWidget(),
});
}
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Flutter Boost example',
builder: FlutterBoost.init(), ///init container manager
home: Container());
}
```
## Integration with iOS code.
Note: You need to add libc++ into "Linked Frameworks and Libraries"
Use FLBFlutterAppDelegate as the superclass of your AppDelegate # boost version description
```objc
@interface AppDelegate : FLBFlutterAppDelegate <UIApplicationDelegate>
@end
```
Implement FLBPlatform protocol methods for your App.
```objc
@interface PlatformRouterImp : NSObject<FLBPlatform>
@property (nonatomic,strong) UINavigationController *navigationController;
+ (PlatformRouterImp *)sharedRouter;
@end
@implementation PlatformRouterImp
- (void)openPage:(NSString *)name
params:(NSDictionary *)params
animated:(BOOL)animated
completion:(void (^)(BOOL))completion
{
if([params[@"present"] boolValue]){
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController presentViewController:vc animated:animated completion:^{}];
}else{
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController pushViewController:vc animated:animated];
}
}
1. 0.1.50 is based on the flutter v1.5.4-hotfixes branch, android if other flutter versions or branches will compile incorrectly
- (void)closePage:(NSString *)uid animated:(BOOL)animated params:(NSDictionary *)params completion:(void (^)(BOOL))completion 2. 0.1.51--0.1.54 is a bugfix for 0.1.50
{
FLBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;
if([vc isKindOfClass:FLBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: uid]){
[vc dismissViewControllerAnimated:animated completion:^{}];
}else{
[self.navigationController popViewControllerAnimated:animated];
}
}
@end
```
Initialize FlutterBoost with FLBPlatform at the beginning of your App, such as AppDelegate. 3. 0.1.60 is based on the flutter v1.9.1-hotfixes branch. Android does not support andriodx if other flutter branches will compile incorrectly
```objc 4. 0.1.61--0.1.62 is a bugfix for 0.1.60
PlatformRouterImp *router = [PlatformRouterImp new];
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatform:router
onStart:^(FlutterEngine *engine) {
}];
```
## Integration with Android code.
Init FlutterBoost in Application.onCreate() 
```java
public class MyApplication extends FlutterApplication {
@Override
public void onCreate() {
super.onCreate();
FlutterBoostPlugin.init(new IPlatform() {
@Override
public Application getApplication() {
return MyApplication.this;
}
@Override
public boolean isDebug() {
return true;
}
@Override
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
//native open url
}
@Override
public IFlutterEngineProvider engineProvider() {
return new BoostEngineProvider(){
@Override
public BoostFlutterEngine createEngine(Context context) {
return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint(
context.getResources().getAssets(),
FlutterMain.findAppBundlePath(context),
"main"),"/");
}
};
}
@Override
public int whenEngineStart() {
return ANY_ACTIVITY_CREATED;
}
});
}
```
5. Statement of support for androidx
# Basic Usage Current androidx branch is v0.1.61-androidx-hotfixes
## Concepts
All page routing requests are being sent to the native router. Native router communicates with Native Container Manager, Native Container Manager takes care of building and destroying of Native Containers.  Is based on flutter v1.9.1-hotfixes branch, if other branches will compile incorrectly
Synchronize with the 0.1.63 code, and bugfix also merge to this branch.
## Use Flutter Boost Native Container to show a Flutter page in native code.
iOS
```objc # Getting Started
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController presentViewController:vc animated:animated completion:^{}];
```
However, in this way, you cannot get the page data result after the page finished. We suggest you implement the platform page router like the way mentioned above. And finally open/close the VC as following:
```objc
//push the page
[FlutterBoostPlugin open:@"first" urlParams:@{kPageCallBackId:@"MycallbackId#1"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
NSLog(@"call me when page finished, and your result is:%@", result);
} completion:^(BOOL f) {
NSLog(@"page is opened");
}];
//prsent the page
[FlutterBoostPlugin open:@"second" urlParams:@{@"present":@(YES),kPageCallBackId:@"MycallbackId#2"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
NSLog(@"call me when page finished, and your result is:%@", result);
} completion:^(BOOL f) {
NSLog(@"page is presented");
}];
//close the page
[FlutterBoostPlugin close:yourUniqueId result:yourdata exts:exts completion:nil];
```
Android
```java
public class FlutterPageActivity extends BoostFlutterActivity {
## Add a dependency in you Flutter project.
@Override Open you pubspec.yaml and add the following line to dependencies:
public String getContainerUrl() {
//specify the page name register in FlutterBoost
return "sample://firstPage";
}
@Override support branch
public Map getContainerUrlParams() { ```json
//params of the page flutter_boost:
Map<String,String> params = new HashMap<>(); git:
params.put("key","value"); url: 'https://github.com/alibaba/flutter_boost.git'
return params; ref: '0.1.63'
}
}
``` ```
androidx branch
or ```json
flutter_boost:
```java git:
url: 'https://github.com/alibaba/flutter_boost.git'
public class FlutterFragment extends BoostFlutterFragment { ref: 'v0.1.61-androidx-hotfixes'
@Override
public String getContainerUrl() {
return "flutterFragment";
}
@Override
public Map getContainerUrlParams() {
Map<String,String> params = new HashMap<>();
params.put("tag",getArguments().getString("tag"));
return params;
}
}
``` ```
## Use Flutter Boost to open a page in dart code.
Dart # Boost Integration
```objc Please see the boost example for details.
FlutterBoost.singleton # FAQ
.open("pagename") please read this document:
<a href="Frequently Asked Question.md">FAQ</a>
```
## Use Flutter Boost to close a page in dart code.
```objc
FlutterBoost.singleton.close(uniqueId); # License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
```
# Running the Demo # Problem feedback group( dingding group)
Please see the example for details.
<img width="200" src="https://img.alicdn.com/tfs/TB1JSzVeYY1gK0jSZTEXXXDQVXa-892-1213.jpg">
# License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
## 关于我们 ## 关于我们
阿里巴巴-闲鱼技术是国内最早也是最大规模线上运行Flutter的团队。 阿里巴巴-闲鱼技术是国内最早也是最大规模线上运行Flutter的团队。
我们在公众号中为你精选了Flutter独家干货,全面而深入。 我们在公众号中为你精选了Flutter独家干货,全面而深入。
......
...@@ -2,31 +2,10 @@ ...@@ -2,31 +2,10 @@
<img src="flutter_boost.png"> <img src="flutter_boost.png">
</p> </p>
# 项目最新动态
目前已经支持flutter 1.9版本。
对应的flutter 版本是:v1.9.1-hotfixes
flutter boost分支是:feature/flutter_1.9_upgrade
flutter boost androidx 分支是:feature/flutter_1.9_androidx_upgrade
代码引入方式:
```java
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: 'feature/flutter_1.9_upgrade'
```
# Release Note # Release Note
请查看最新版本0.1.50的release note 确认变更,[0.1.50 release note](https://github.com/alibaba/flutter_boost/releases) 请查看最新版本0.1.63的release note 确认变更,[0.1.63 release note](https://github.com/alibaba/flutter_boost/releases)
# FlutterBoost # FlutterBoost
...@@ -34,274 +13,79 @@ flutter boost androidx 分支是:feature/flutter_1.9_androidx_upgrade ...@@ -34,274 +13,79 @@ flutter boost androidx 分支是:feature/flutter_1.9_androidx_upgrade
# 前置条件 # 前置条件
在继续之前,您需要将Flutter集成到你现有的项目中。flutter sdk 的版本需要 v1.5.4-hotfixes,否则会编译失败.
# 安装
## 在Flutter项目中添加依赖项。
打开pubspec.yaml并将以下行添加到依赖项:
```json
flutter_boost: ^0.1.54
```
或者可以直接依赖github的项目的版本,Tag,pub发布会有延迟,推荐直接依赖Github项目
```java
flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '0.1.54'
```
## Dart代码的集成
将init代码添加到App App
```dart
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
void initState() {
super.initState();
///register page widget builders,the key is pageName
FlutterBoost.singleton.registerPageBuilders({
'sample://firstPage': (pageName, params, _) => FirstRouteWidget(),
'sample://secondPage': (pageName, params, _) => SecondRouteWidget(),
});
}
@override
Widget build(BuildContext context) => MaterialApp(
title: 'Flutter Boost example',
builder: FlutterBoost.init(), ///init container manager
home: Container());
}
```
## iOS代码集成。 在继续之前,您需要将Flutter集成到你现有的项目中。flutter sdk 的版本需要 v1.9.1-hotfixes,否则会编译失败.
注意:需要将libc++ 加入 "Linked Frameworks and Libraries" 中。 # FAQ
请阅读这篇文章:
<a href="Frequently Asked Question.md">FAQ</a>
使用FLBFlutterAppDelegate作为AppDelegate的超类
```objectivec # boost 版本说明
@interface AppDelegate : FLBFlutterAppDelegate <UIApplicationDelegate>
@end
```
为您的应用程序实现FLBPlatform协议方法。
```objectivec
@interface PlatformRouterImp : NSObject<FLBPlatform>
@property (nonatomic,strong) UINavigationController *navigationController; 1. 0.1.50 是基于flutter v1.5.4-hotfixes 分支,android 如果其他flutter版本或者分支 会编译错误
+ (PlatformRouterImp *)sharedRouter; 2. 0.1.51--0.1.54 是对0.1.50的bugfix
@end
3. 0.1.60 是基于flutter v1.9.1-hotfixes 分支,android如果其他flutter分支会编译错误,该版本不支持andriodx
@implementation PlatformRouterImp 4. 0.1.61-- 0.1.63 是对0.1.60 的bugfix
- (void)openPage:(NSString *)name
params:(NSDictionary *)params
animated:(BOOL)animated
completion:(void (^)(BOOL))completion
{
if([params[@"present"] boolValue]){
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController presentViewController:vc animated:animated completion:^{}];
}else{
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController pushViewController:vc animated:animated];
}
}
5. 关于androidx 的支持声明
- (void)closePage:(NSString *)uid animated:(BOOL)animated params:(NSDictionary *)params completion:(void (^)(BOOL))completion 目前androidx 分支为 v0.1.61-androidx-hotfixes
{
FLBFlutterViewContainer *vc = (id)self.navigationController.presentedViewController;
if([vc isKindOfClass:FLBFlutterViewContainer.class] && [vc.uniqueIDString isEqual: uid]){
[vc dismissViewControllerAnimated:animated completion:^{}];
}else{
[self.navigationController popViewControllerAnimated:animated];
}
}
@end 是基于flutter v1.9.1-hotfixes 分支,如果其他分支会编译错误
```
和0.1.60代码同步, bugfix 也会合入该分支。
在应用程序开头使用FLBPlatform初始化FlutterBoost。
```objc
PlatformRouterImp *router = [PlatformRouterImp new];
[FlutterBoostPlugin.sharedInstance startFlutterWithPlatformrouter
onStart^FlutterEngine *engine{
}]; # 安装
```
## Android代码集成。 ## 在Flutter项目中添加依赖项。
在Application.onCreate()中初始化FlutterBoost
```java
public class MyApplication extends FlutterApplication {
@Override
public void onCreate() {
super.onCreate();
FlutterBoostPlugin.init(new IPlatform() {
@Override
public Application getApplication() {
return MyApplication.this;
}
@Override
public boolean isDebug() {
return true;
}
@Override
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
PageRouter.openPageByUrl(context,url,urlParams,requestCode);
}
@Override
public IFlutterEngineProvider engineProvider() {
return new BoostEngineProvider(){
@Override
public BoostFlutterEngine createEngine(Context context) {
return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint(
context.getResources().getAssets(),
FlutterMain.findAppBundlePath(context),
"main"),"/");
}
};
}
@Override
public int whenEngineStart() {
return ANY_ACTIVITY_CREATED;
}
});
}
```
# 基本用法 打开pubspec.yaml并将以下行添加到依赖项:
## 概念
所有页面路由请求都将发送到Native路由器。Native路由器与Native Container Manager通信,Native Container Manager负责构建和销毁Native Containers。 support分支
```json
## 使用Flutter Boost Native Container用Native代码打开Flutter页面。 flutter_boost:
git:
url: 'https://github.com/alibaba/flutter_boost.git'
ref: '0.1.63'
```objc
FLBFlutterViewContainer *vc = FLBFlutterViewContainer.new;
[vc setName:name params:params];
[self.navigationController presentViewController:vc animated:animated completion:^{}];
``` ```
但是,这种方式无法获取页面返回的数据,建议你按上面的example实现类似于PlatformRouterImp这样的路由器,然后通过以下方式来打开/关闭页面 androidx分支
```json
```objc flutter_boost:
//push the page git:
[FlutterBoostPlugin open:@"first" urlParams:@{kPageCallBackId:@"MycallbackId#1"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) { url: 'https://github.com/alibaba/flutter_boost.git'
NSLog(@"call me when page finished, and your result is:%@", result); ref: 'v0.1.61-androidx-hotfixes'
} completion:^(BOOL f) {
NSLog(@"page is opened");
}];
//prsent the page
[FlutterBoostPlugin open:@"second" urlParams:@{@"present":@(YES),kPageCallBackId:@"MycallbackId#2"} exts:@{@"animated":@(YES)} onPageFinished:^(NSDictionary *result) {
NSLog(@"call me when page finished, and your result is:%@", result);
} completion:^(BOOL f) {
NSLog(@"page is presented");
}];
//close the page
[FlutterBoostPlugin close:yourUniqueId result:yourdata exts:exts completion:nil];
```
Android
```java
public class FlutterPageActivity extends BoostFlutterActivity {
@Override
public String getContainerUrl() {
//specify the page name register in FlutterBoost
return "sample://firstPage";
}
@Override
public Map getContainerUrlParams() {
//params of the page
Map<String,String> params = new HashMap<>();
params.put("key","value");
return params;
}
}
``` ```
或者用Fragment
```java
public class FlutterFragment extends BoostFlutterFragment {
@Override ## boost集成
public String getContainerUrl() {
return "sample://firstPage";
}
@Override
public Map getContainerUrlParams() {
Map<String,String> params = new HashMap<>();
params.put("key","value");
return params;
}
}
```
集成请看boost的Examples
## 使用Flutter Boost在dart代码打开页面。
Dart
```java
FlutterBoost.singleton
.open("sample://flutterFragmentPage")
```
# 问题反馈群(钉钉群)
## 使用Flutter Boost在dart代码关闭页面。 <img width="200" src="https://img.alicdn.com/tfs/TB1JSzVeYY1gK0jSZTEXXXDQVXa-892-1213.jpg">
```java
FlutterBoost.singleton.close(uniqueId);
```
# Examples
更详细的使用例子请参考Demo
# 许可证 # 许可证
该项目根据MIT许可证授权 - 有关详细信息,请参阅[LICENSE.md](LICENSE.md)文件 该项目根据MIT许可证授权 - 有关详细信息,请参阅[LICENSE.md](LICENSE.md)文件
<a name="Acknowledgments"> </a> <a name="Acknowledgments"> </a>
# 问题反馈群(钉钉群)
<img width="200" src="https://img.alicdn.com/tfs/TB1JSzVeYY1gK0jSZTEXXXDQVXa-892-1213.jpg">
## 关于我们 ## 关于我们
......
...@@ -26,6 +26,7 @@ android { ...@@ -26,6 +26,7 @@ android {
buildToolsVersion '27.0.3' buildToolsVersion '27.0.3'
defaultConfig { defaultConfig {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 28
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
lintOptions { lintOptions {
...@@ -34,9 +35,12 @@ android { ...@@ -34,9 +35,12 @@ android {
} }
dependencies { dependencies {
implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:support-v4:28.0.0'
implementation 'android.arch.lifecycle:common-java8:1.1.1'
implementation 'com.alibaba:fastjson:1.2.41' implementation 'com.alibaba:fastjson:1.2.41'
implementation 'com.android.support:support-v4:26.1.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
} }
ext { ext {
......
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.idlefish.flutterboost"> package="com.idlefish.flutterboost">
<application>
<activity android:name="com.idlefish.flutterboost.containers.BoostFlutterDefaultActivity" />
</application>
</manifest> </manifest>
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost;
import android.content.Context;
import com.idlefish.flutterboost.interfaces.IFlutterEngineProvider;
import com.idlefish.flutterboost.interfaces.IStateListener;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.view.FlutterMain;
public class BoostEngineProvider implements IFlutterEngineProvider {
private BoostFlutterEngine mEngine = null;
public BoostEngineProvider() {}
@Override
public BoostFlutterEngine createEngine(Context context) {
return new BoostFlutterEngine(context.getApplicationContext());
}
@Override
public BoostFlutterEngine provideEngine(Context context) {
Utils.assertCallOnMainThread();
if (mEngine == null) {
FlutterShellArgs flutterShellArgs = new FlutterShellArgs(new String[0]);
FlutterMain.ensureInitializationComplete(
context.getApplicationContext(), flutterShellArgs.toArray());
mEngine = createEngine(context.getApplicationContext());
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if(stateListener != null) {
stateListener.onEngineCreated(mEngine);
}
}
return mEngine;
}
@Override
public BoostFlutterEngine tryGetEngine() {
return mEngine;
}
public static void assertEngineRunning(){
final BoostFlutterEngine engine = FlutterBoost.singleton().engineProvider().tryGetEngine();
if(engine == null || !engine.isRunning()) {
throw new RuntimeException("engine is not running yet!");
}
}
}
package com.idlefish.flutterboost;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.Surface;
import android.view.View;
import com.idlefish.flutterboost.interfaces.IContainerRecord;
import com.idlefish.flutterboost.interfaces.IStateListener;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.nio.ByteBuffer;
import io.flutter.app.FlutterPluginRegistry;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterJNI;
import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformViewRegistry;
import io.flutter.view.FlutterMain;
import io.flutter.view.FlutterView;
import io.flutter.view.TextureRegistry;
public class BoostFlutterEngine extends FlutterEngine {
protected final Context mContext;
protected final BoostPluginRegistry mBoostPluginRegistry;
protected final DartExecutor.DartEntrypoint mEntrypoint;
protected final String mInitRoute;
private final FakeRender mFakeRender;
protected WeakReference<Activity> mCurrentActivityRef;
public BoostFlutterEngine(@NonNull Context context) {
this(context, null, null);
}
public BoostFlutterEngine(@NonNull Context context, DartExecutor.DartEntrypoint entrypoint, String initRoute) {
super(context);
mContext = context.getApplicationContext();
mBoostPluginRegistry = new BoostPluginRegistry(this, context);
if (entrypoint != null) {
mEntrypoint = entrypoint;
} else {
mEntrypoint = defaultDartEntrypoint(context);
}
if (initRoute != null) {
mInitRoute = initRoute;
} else {
mInitRoute = defaultInitialRoute(context);
}
FlutterJNI flutterJNI = null;
try {
Field field = FlutterEngine.class.getDeclaredField("flutterJNI");
field.setAccessible(true);
flutterJNI = (FlutterJNI) field.get(this);
} catch (Throwable t) {
try {
for(Field field:FlutterEngine.class.getDeclaredFields()) {
field.setAccessible(true);
Object o = field.get(this);
if(o instanceof FlutterJNI) {
flutterJNI = (FlutterJNI)o;
}
}
if(flutterJNI == null) {
throw new RuntimeException("FlutterJNI not found");
}
}catch (Throwable it){
Debuger.exception(it);
}
}
mFakeRender = new FakeRender(flutterJNI);
}
public void startRun(@Nullable Activity activity) {
mCurrentActivityRef = new WeakReference<>(activity);
if (!getDartExecutor().isExecutingDart()) {
Debuger.log("engine start running...");
getNavigationChannel().setInitialRoute(mInitRoute);
getDartExecutor().executeDartEntrypoint(mEntrypoint);
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if (stateListener != null) {
stateListener.onEngineStarted(this);
}
FlutterBoost.singleton().platform().registerPlugins(mBoostPluginRegistry);
if(activity != null) {
FlutterRenderer.ViewportMetrics metrics = new FlutterRenderer.ViewportMetrics();
metrics.devicePixelRatio = activity.getResources().getDisplayMetrics().density;
final View decor = activity.getWindow().getDecorView();
if(decor != null) {
metrics.width = decor.getWidth();
metrics.height = decor.getHeight();
}
if (metrics.width <= 0 || metrics.height <= 0) {
metrics.width = Utils.getMetricsWidth(activity);
metrics.height = Utils.getMetricsHeight(activity);
}
metrics.paddingTop = Utils.getStatusBarHeight(activity);
metrics.paddingRight = 0;
metrics.paddingBottom = 0;
metrics.paddingLeft = 0;
metrics.viewInsetTop = 0;
metrics.viewInsetRight = 0;
metrics.viewInsetBottom = 0;
metrics.viewInsetLeft = 0;
getRenderer().setViewportMetrics(metrics);
}
}
}
protected DartExecutor.DartEntrypoint defaultDartEntrypoint(Context context) {
return new DartExecutor.DartEntrypoint(
context.getResources().getAssets(),
FlutterMain.findAppBundlePath(context),
"main");
}
protected String defaultInitialRoute(Context context) {
return "/";
}
public BoostPluginRegistry getBoostPluginRegistry() {
return mBoostPluginRegistry;
}
public boolean isRunning() {
return getDartExecutor().isExecutingDart();
}
@NonNull
@Override
public FlutterRenderer getRenderer() {
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
boolean hit = false;
for (StackTraceElement st : stackTrace) {
if (st.getMethodName().equals("sendViewportMetricsToFlutter")) {
hit = true;
break;
}
}
if (hit) {
return mFakeRender;
} else {
return super.getRenderer();
}
}
public class BoostPluginRegistry extends FlutterPluginRegistry {
private final FlutterEngine mEngine;
public BoostPluginRegistry(FlutterEngine engine, Context context) {
super(engine, context);
mEngine = engine;
}
public PluginRegistry.Registrar registrarFor(String pluginKey) {
return new BoostRegistrar(mEngine, super.registrarFor(pluginKey));
}
}
public class BoostRegistrar implements PluginRegistry.Registrar {
private final PluginRegistry.Registrar mRegistrar;
private final FlutterEngine mEngine;
BoostRegistrar(FlutterEngine engine, PluginRegistry.Registrar registrar) {
mRegistrar = registrar;
mEngine = engine;
}
@Override
public Activity activity() {
Activity activity;
IContainerRecord record;
record = FlutterBoost.singleton().containerManager().getCurrentTopRecord();
if (record == null) {
record = FlutterBoost.singleton().containerManager().getLastGenerateRecord();
}
if (record == null) {
activity = FlutterBoost.singleton().currentActivity();
} else {
activity = record.getContainer().getContextActivity();
}
if (activity == null && mCurrentActivityRef != null) {
activity = mCurrentActivityRef.get();
}
if (activity == null) {
throw new RuntimeException("current has no valid Activity yet");
}
return activity;
}
@Override
public Context context() {
return mRegistrar.context();
}
@Override
public Context activeContext() {
return mRegistrar.activeContext();
}
@Override
public BinaryMessenger messenger() {
return mEngine.getDartExecutor();
}
@Override
public TextureRegistry textures() {
return mEngine.getRenderer();
}
@Override
public PlatformViewRegistry platformViewRegistry() {
return mRegistrar.platformViewRegistry();
}
@Override
public FlutterView view() {
throw new RuntimeException("should not use!!!");
}
@Override
public String lookupKeyForAsset(String s) {
return mRegistrar.lookupKeyForAsset(s);
}
@Override
public String lookupKeyForAsset(String s, String s1) {
return mRegistrar.lookupKeyForAsset(s, s1);
}
@Override
public PluginRegistry.Registrar publish(Object o) {
return mRegistrar.publish(o);
}
@Override
public PluginRegistry.Registrar addRequestPermissionsResultListener(PluginRegistry.RequestPermissionsResultListener requestPermissionsResultListener) {
return mRegistrar.addRequestPermissionsResultListener(requestPermissionsResultListener);
}
@Override
public PluginRegistry.Registrar addActivityResultListener(PluginRegistry.ActivityResultListener activityResultListener) {
return mRegistrar.addActivityResultListener(activityResultListener);
}
@Override
public PluginRegistry.Registrar addNewIntentListener(PluginRegistry.NewIntentListener newIntentListener) {
return mRegistrar.addNewIntentListener(newIntentListener);
}
@Override
public PluginRegistry.Registrar addUserLeaveHintListener(PluginRegistry.UserLeaveHintListener userLeaveHintListener) {
return mRegistrar.addUserLeaveHintListener(userLeaveHintListener);
}
@Override
public PluginRegistry.Registrar addViewDestroyListener(PluginRegistry.ViewDestroyListener viewDestroyListener) {
return mRegistrar.addViewDestroyListener(viewDestroyListener);
}
}
private boolean viewportMetricsEqual(FlutterRenderer.ViewportMetrics a, FlutterRenderer.ViewportMetrics b) {
return a != null && b != null &&
a.height == b.height &&
a.width == b.width &&
a.devicePixelRatio == b.devicePixelRatio &&
a.paddingBottom == b.paddingBottom &&
a.paddingLeft == b.paddingLeft &&
a.paddingRight == b.paddingRight &&
a.paddingTop == b.paddingTop &&
a.viewInsetLeft == b.viewInsetLeft &&
a.viewInsetRight == b.viewInsetRight &&
a.viewInsetTop == b.viewInsetTop &&
a.viewInsetBottom == b.viewInsetBottom;
}
class FakeRender extends FlutterRenderer {
private ViewportMetrics last;
public FakeRender(FlutterJNI flutterJNI) {
super(flutterJNI);
}
@Override
public void setViewportMetrics(@NonNull ViewportMetrics viewportMetrics) {
if (viewportMetrics.width > 0 && viewportMetrics.height > 0 /*&& !viewportMetricsEqual(last, viewportMetrics)*/) {
last = viewportMetrics;
Debuger.log("setViewportMetrics w:" + viewportMetrics.width + " h:" + viewportMetrics.height);
super.setViewportMetrics(viewportMetrics);
}
}
@Override
public void attachToRenderSurface(@NonNull RenderSurface renderSurface) {
Debuger.exception("should never called!");
}
@Override
public void detachFromRenderSurface() {
Debuger.exception("should never called!");
}
@Override
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
Debuger.exception("should never called!");
}
@Override
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
Debuger.exception("should never called!");
}
@Override
public SurfaceTextureEntry createSurfaceTexture() {
Debuger.exception("should never called!");
return null;
}
@Override
public void surfaceCreated(Surface surface) {
Debuger.exception("should never called!");
}
@Override
public void surfaceChanged(int width, int height) {
Debuger.exception("should never called!");
}
@Override
public void surfaceDestroyed() {
Debuger.exception("should never called!");
}
@Override
public Bitmap getBitmap() {
Debuger.exception("should never called!");
return null;
}
@Override
public void dispatchPointerDataPacket(ByteBuffer buffer, int position) {
Debuger.exception("should never called!");
}
@Override
public boolean isSoftwareRenderingEnabled() {
Debuger.exception("should never called!");
return false;
}
@Override
public void setAccessibilityFeatures(int flags) {
Debuger.exception("should never called!");
}
@Override
public void setSemanticsEnabled(boolean enabled) {
Debuger.exception("should never called!");
}
@Override
public void dispatchSemanticsAction(int id, int action, ByteBuffer args, int argsPosition) {
Debuger.exception("should never called!");
}
}
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.os.Bundle;
import android.support.v4.view.ViewCompat;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityNodeProvider;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.idlefish.flutterboost.interfaces.IStateListener;
import java.lang.reflect.Method;
import java.util.LinkedList;
import java.util.List;
import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.view.AccessibilityBridge;
public class BoostFlutterView extends FrameLayout {
private BoostFlutterEngine mFlutterEngine;
private XFlutterView mFlutterView;
private PlatformPlugin mPlatformPlugin;
private Bundle mArguments;
private RenderingProgressCoverCreator mRenderingProgressCoverCreator;
private View mRenderingProgressCover;
private final List<OnFirstFrameRenderedListener> mFirstFrameRenderedListeners = new LinkedList<>();
private boolean mEngineAttached = false;
private boolean mNeedSnapshotWhenDetach = false;
private SnapshotView mSnapshot;
private final io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener mOnFirstFrameRenderedListener =
new io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
Debuger.log("BoostFlutterView onFirstFrameRendered");
if(mRenderingProgressCover != null && mRenderingProgressCover.getParent() != null) {
((ViewGroup)mRenderingProgressCover.getParent()).removeView(mRenderingProgressCover);
}
if(mNeedSnapshotWhenDetach) {
mSnapshot.dismissSnapshot(BoostFlutterView.this);
}
final Object[] listeners = mFirstFrameRenderedListeners.toArray();
for (Object obj : listeners) {
((OnFirstFrameRenderedListener) obj).onFirstFrameRendered(BoostFlutterView.this);
}
}
};
private final ViewTreeObserver.OnGlobalLayoutListener mGlobalLayoutListener = new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
ViewCompat.requestApplyInsets(mFlutterView);
}
};
public BoostFlutterView(Context context, BoostFlutterEngine engine, Bundle args, RenderingProgressCoverCreator creator) {
super(context);
mFlutterEngine = engine;
mArguments = args;
mRenderingProgressCoverCreator = creator;
init();
}
private void init() {
if (mFlutterEngine == null) {
mFlutterEngine = createFlutterEngine(getContext());
}
if (mArguments == null) {
mArguments = new Bundle();
}
mPlatformPlugin = new PlatformPlugin((Activity) getContext(), mFlutterEngine.getPlatformChannel());
mFlutterView = new XFlutterView(getContext(), getRenderMode(), getTransparencyMode());
addView(mFlutterView, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
mSnapshot = new SnapshotView(getContext());
if(mRenderingProgressCoverCreator != null) {
mRenderingProgressCover = mRenderingProgressCoverCreator
.createRenderingProgressCover(getContext());
}else{
mRenderingProgressCover = createRenderingProgressCorver();
}
if(mRenderingProgressCover != null) {
addView(mRenderingProgressCover, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
}
mFlutterView.addOnFirstFrameRenderedListener(mOnFirstFrameRenderedListener);
mFlutterEngine.startRun((Activity)getContext());
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if(stateListener != null) {
stateListener.onFlutterViewInited(mFlutterEngine,this);
}
checkAssert();
}
private void checkAssert(){
try {
Method method = FlutterView.class.getDeclaredMethod("sendViewportMetricsToFlutter");
if(method == null) {
throw new Exception("method: FlutterView.sendViewportMetricsToFlutter not found!");
}
}catch (Throwable t){
Debuger.exception(t);
}
}
protected View createRenderingProgressCorver(){
FrameLayout frameLayout = new FrameLayout(getContext());
frameLayout.setBackgroundColor(Color.WHITE);
LinearLayout linearLayout = new LinearLayout(getContext());
linearLayout.setOrientation(LinearLayout.VERTICAL);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.CENTER;
frameLayout.addView(linearLayout,layoutParams);
ProgressBar progressBar = new ProgressBar(getContext());
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
linearLayout.addView(progressBar,params);
TextView textView = new TextView(getContext());
params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
textView.setText("Frame Rendering...");
linearLayout.addView(textView,params);
return frameLayout;
}
protected BoostFlutterEngine createFlutterEngine(Context context) {
return FlutterBoost.singleton().engineProvider().provideEngine(context);
}
public void addFirstFrameRendered(OnFirstFrameRenderedListener listener) {
mFirstFrameRenderedListeners.add(listener);
}
public void removeFirstFrameRendered(OnFirstFrameRenderedListener listener) {
mFirstFrameRenderedListeners.remove(listener);
}
protected FlutterView.RenderMode getRenderMode() {
String renderModeName = mArguments.getString("flutterview_render_mode", FlutterView.RenderMode.surface.name());
return FlutterView.RenderMode.valueOf(renderModeName);
}
protected FlutterView.TransparencyMode getTransparencyMode() {
String transparencyModeName = mArguments.getString("flutterview_transparency_mode", FlutterView.TransparencyMode.transparent.name());
return FlutterView.TransparencyMode.valueOf(transparencyModeName);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
ViewCompat.requestApplyInsets(this);
getViewTreeObserver().addOnGlobalLayoutListener(mGlobalLayoutListener);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
getViewTreeObserver().removeOnGlobalLayoutListener(mGlobalLayoutListener);
onDetach();
flutterEngine.getPluginRegistry().getPlatformViewsController().onFlutterViewDestroyed();
}
public BoostFlutterEngine getEngine(){
return mFlutterEngine;
}
public void onResume() {
Debuger.log("BoostFlutterView onResume");
// mFlutterEngine.getLifecycleChannel().appIsResumed();
}
// public void onPostResume() {
// Debuger.log("BoostFlutterView onPostResume");
// mPlatformPlugin.onPostResume();
// }
public void onPause() {
Debuger.log("BoostFlutterView onPause");
// mFlutterEngine.getLifecycleChannel().appIsInactive();
}
public void onStop() {
Debuger.log("BoostFlutterView onStop");
// mFlutterEngine.getLifecycleChannel().appIsPaused();
}
public void onAttach() {
Debuger.log("BoostFlutterView onAttach");
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if(stateListener != null) {
stateListener.beforeEngineAttach(mFlutterEngine,this);
}
mFlutterView.attachToFlutterEngine(mFlutterEngine);
mEngineAttached = true;
if(stateListener != null) {
stateListener.afterEngineAttached(mFlutterEngine,this);
}
}
public void toggleSnapshot() {
mSnapshot.toggleSnapshot(this);
}
public void toggleAttach() {
if(mEngineAttached) {
onDetach();
}else{
onAttach();
}
}
public void onDetach() {
Debuger.log("BoostFlutterView onDetach");
if(mNeedSnapshotWhenDetach) {
mSnapshot.showSnapshot(BoostFlutterView.this);
}
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if(stateListener != null) {
stateListener.beforeEngineDetach(mFlutterEngine,this);
}
mFlutterView.detachFromFlutterEngine();
mEngineAttached = false;
if(stateListener != null) {
stateListener.afterEngineDetached(mFlutterEngine,this);
}
}
public void onDestroy() {
Debuger.log("BoostFlutterView onDestroy");
mFlutterView.removeOnFirstFrameRenderedListener(mOnFirstFrameRenderedListener);
mFlutterView.release();
}
//混合栈的返回和原来Flutter的返回逻辑不同
public void onBackPressed() {
// Debuger.log("onBackPressed()");
// if (mFlutterEngine != null) {
// mFlutterEngine.getNavigationChannel().popRoute();
// } else {
// Debuger.log("Invoked onBackPressed() before BoostFlutterView was attached to an Activity.");
// }
}
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
if (mFlutterEngine != null) {
mFlutterEngine.getPluginRegistry().onRequestPermissionsResult(requestCode, permissions, grantResults);
} else {
Debuger.log("onRequestPermissionResult() invoked before BoostFlutterView was attached to an Activity.");
}
}
public void onNewIntent(Intent intent) {
if (mFlutterEngine != null) {
mFlutterEngine.getPluginRegistry().onNewIntent(intent);
} else {
Debuger.log("onNewIntent() invoked before BoostFlutterView was attached to an Activity.");
}
}
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (mFlutterEngine != null) {
mFlutterEngine.getPluginRegistry().onActivityResult(requestCode, resultCode, data);
} else {
Debuger.log("onActivityResult() invoked before BoostFlutterView was attached to an Activity.");
}
}
public void onUserLeaveHint() {
if (mFlutterEngine != null) {
mFlutterEngine.getPluginRegistry().onUserLeaveHint();
} else {
Debuger.log("onUserLeaveHint() invoked before BoostFlutterView was attached to an Activity.");
}
}
public void onTrimMemory(int level) {
if (mFlutterEngine != null) {
if (level == 10) {
mFlutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
} else {
Debuger.log("onTrimMemory() invoked before BoostFlutterView was attached to an Activity.");
}
}
public void onLowMemory() {
mFlutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
public static class Builder {
private Context context;
private BoostFlutterEngine engine;
private FlutterView.RenderMode renderMode;
private FlutterView.TransparencyMode transparencyMode;
private RenderingProgressCoverCreator renderingProgressCoverCreator;
public Builder(Context ctx) {
this.context = ctx;
renderMode = FlutterView.RenderMode.surface;
transparencyMode = FlutterView.TransparencyMode.transparent;
}
public Builder flutterEngine(BoostFlutterEngine engine) {
this.engine = engine;
return this;
}
public Builder renderMode(FlutterView.RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
public Builder renderingProgressCoverCreator(RenderingProgressCoverCreator creator) {
this.renderingProgressCoverCreator = creator;
return this;
}
public Builder transparencyMode(FlutterView.TransparencyMode transparencyMode) {
this.transparencyMode = transparencyMode;
return this;
}
public BoostFlutterView build() {
Bundle args = new Bundle();
args.putString("flutterview_render_mode", renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name());
args.putString("flutterview_transparency_mode", transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name());
return new BoostFlutterView(context, engine, args,renderingProgressCoverCreator);
}
}
public interface OnFirstFrameRenderedListener {
void onFirstFrameRendered(BoostFlutterView view);
}
public interface RenderingProgressCoverCreator {
View createRenderingProgressCover(Context context);
}
}
package com.idlefish.flutterboost;
import io.flutter.Log;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.PluginRegistry;
import java.util.*;
public class BoostPluginRegistry implements PluginRegistry {
private static final String TAG = "ShimPluginRegistry";
private final FlutterEngine flutterEngine;
private final Map<String, Object> pluginMap = new HashMap();
private final BoostRegistrarAggregate shimRegistrarAggregate;
public BoostRegistrarAggregate getRegistrarAggregate() {
return shimRegistrarAggregate;
}
public BoostPluginRegistry(FlutterEngine flutterEngine) {
this.flutterEngine = flutterEngine;
this.shimRegistrarAggregate = new BoostRegistrarAggregate();
this.flutterEngine.getPlugins().add(this.shimRegistrarAggregate);
}
public Registrar registrarFor(String pluginKey) {
Log.v("ShimPluginRegistry", "Creating plugin Registrar for '" + pluginKey + "'");
if (this.pluginMap.containsKey(pluginKey)) {
throw new IllegalStateException("Plugin key " + pluginKey + " is already in use");
} else {
this.pluginMap.put(pluginKey, (Object) null);
BoostRegistrar registrar = new BoostRegistrar(pluginKey, this.pluginMap);
this.shimRegistrarAggregate.addPlugin(registrar);
return registrar;
}
}
public boolean hasPlugin(String pluginKey) {
return this.pluginMap.containsKey(pluginKey);
}
public Object valuePublishedByPlugin(String pluginKey) {
return this.pluginMap.get(pluginKey);
}
public static class BoostRegistrarAggregate implements FlutterPlugin, ActivityAware {
private final Set<BoostRegistrar> shimRegistrars;
private FlutterPluginBinding flutterPluginBinding;
private ActivityPluginBinding activityPluginBinding;
public ActivityPluginBinding getActivityPluginBinding() {
return activityPluginBinding;
}
private BoostRegistrarAggregate() {
this.shimRegistrars = new HashSet();
}
public void addPlugin(BoostRegistrar shimRegistrar) {
this.shimRegistrars.add(shimRegistrar);
if (this.flutterPluginBinding != null) {
shimRegistrar.onAttachedToEngine(this.flutterPluginBinding);
}
if (this.activityPluginBinding != null) {
shimRegistrar.onAttachedToActivity(this.activityPluginBinding);
}
}
public void onAttachedToEngine(FlutterPluginBinding binding) {
this.flutterPluginBinding = binding;
Iterator var2 = this.shimRegistrars.iterator();
while (var2.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var2.next();
shimRegistrar.onAttachedToEngine(binding);
}
}
public void onDetachedFromEngine(FlutterPluginBinding binding) {
Iterator var2 = this.shimRegistrars.iterator();
while (var2.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var2.next();
shimRegistrar.onDetachedFromEngine(binding);
}
this.flutterPluginBinding = null;
}
public void onAttachedToActivity(ActivityPluginBinding binding) {
this.activityPluginBinding = binding;
Iterator var2 = this.shimRegistrars.iterator();
while (var2.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var2.next();
shimRegistrar.onAttachedToActivity(binding);
}
}
public void onDetachedFromActivityForConfigChanges() {
Iterator var1 = this.shimRegistrars.iterator();
while (var1.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var1.next();
shimRegistrar.onDetachedFromActivity();
}
this.activityPluginBinding = null;
}
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
Iterator var2 = this.shimRegistrars.iterator();
while (var2.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var2.next();
shimRegistrar.onReattachedToActivityForConfigChanges(binding);
}
}
public void onDetachedFromActivity() {
Iterator var1 = this.shimRegistrars.iterator();
while (var1.hasNext()) {
BoostRegistrar shimRegistrar = (BoostRegistrar) var1.next();
shimRegistrar.onDetachedFromActivity();
}
}
}
}
package com.idlefish.flutterboost;
import android.app.Activity;
import android.content.Context;
import android.support.annotation.NonNull;
import io.flutter.Log;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.PluginRegistry.ActivityResultListener;
import io.flutter.plugin.common.PluginRegistry.NewIntentListener;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener;
import io.flutter.plugin.common.PluginRegistry.UserLeaveHintListener;
import io.flutter.plugin.common.PluginRegistry.ViewDestroyListener;
import io.flutter.plugin.platform.PlatformViewRegistry;
import io.flutter.view.FlutterMain;
import io.flutter.view.FlutterNativeView;
import io.flutter.view.FlutterView;
import io.flutter.view.TextureRegistry;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
class BoostRegistrar implements Registrar, FlutterPlugin, ActivityAware {
private static final String TAG = "ShimRegistrar";
private final Map<String, Object> globalRegistrarMap;
private final String pluginId;
private final Set<ViewDestroyListener> viewDestroyListeners = new HashSet();
private final Set<RequestPermissionsResultListener> requestPermissionsResultListeners = new HashSet();
private final Set<ActivityResultListener> activityResultListeners = new HashSet();
private final Set<NewIntentListener> newIntentListeners = new HashSet();
private final Set<UserLeaveHintListener> userLeaveHintListeners = new HashSet();
private FlutterPluginBinding pluginBinding;
private ActivityPluginBinding activityPluginBinding;
public BoostRegistrar(@NonNull String pluginId, @NonNull Map<String, Object> globalRegistrarMap) {
this.pluginId = pluginId;
this.globalRegistrarMap = globalRegistrarMap;
}
public Activity activity() {
if(this.activityPluginBinding != null){
return this.activityPluginBinding.getActivity();
}
if(FlutterBoost.instance().currentActivity()!=null){
return FlutterBoost.instance().currentActivity();
}
return null;
}
public Context context() {
return this.pluginBinding != null ? this.pluginBinding.getApplicationContext() : null;
}
public Context activeContext() {
return (Context)(this.activityPluginBinding == null ? this.context() : this.activity());
}
public BinaryMessenger messenger() {
return this.pluginBinding != null ? this.pluginBinding.getFlutterEngine().getDartExecutor() : null;
}
public TextureRegistry textures() {
return this.pluginBinding != null ? this.pluginBinding.getFlutterEngine().getRenderer() : null;
}
public PlatformViewRegistry platformViewRegistry() {
return this.pluginBinding != null ? this.pluginBinding.getFlutterEngine().getPlatformViewsController().getRegistry() : null;
}
public FlutterView view() {
throw new UnsupportedOperationException("The new embedding does not support the old FlutterView.");
}
public String lookupKeyForAsset(String asset) {
return FlutterMain.getLookupKeyForAsset(asset);
}
public String lookupKeyForAsset(String asset, String packageName) {
return FlutterMain.getLookupKeyForAsset(asset, packageName);
}
public Registrar publish(Object value) {
this.globalRegistrarMap.put(this.pluginId, value);
return this;
}
public Registrar addRequestPermissionsResultListener(RequestPermissionsResultListener listener) {
this.requestPermissionsResultListeners.add(listener);
if (this.activityPluginBinding != null) {
this.activityPluginBinding.addRequestPermissionsResultListener(listener);
}
return this;
}
public Registrar addActivityResultListener(ActivityResultListener listener) {
this.activityResultListeners.add(listener);
if (this.activityPluginBinding != null) {
this.activityPluginBinding.addActivityResultListener(listener);
}
return this;
}
public Registrar addNewIntentListener(NewIntentListener listener) {
this.newIntentListeners.add(listener);
if (this.activityPluginBinding != null) {
this.activityPluginBinding.addOnNewIntentListener(listener);
}
return this;
}
public Registrar addUserLeaveHintListener(UserLeaveHintListener listener) {
this.userLeaveHintListeners.add(listener);
if (this.activityPluginBinding != null) {
this.activityPluginBinding.addOnUserLeaveHintListener(listener);
}
return this;
}
@NonNull
public Registrar addViewDestroyListener(@NonNull ViewDestroyListener listener) {
this.viewDestroyListeners.add(listener);
return this;
}
public void onAttachedToEngine(@NonNull FlutterPluginBinding binding) {
Log.v("ShimRegistrar", "Attached to FlutterEngine.");
this.pluginBinding = binding;
}
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
Log.v("ShimRegistrar", "Detached from FlutterEngine.");
Iterator var2 = this.viewDestroyListeners.iterator();
while(var2.hasNext()) {
ViewDestroyListener listener = (ViewDestroyListener)var2.next();
listener.onViewDestroy((FlutterNativeView)null);
}
this.pluginBinding = null;
}
public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
Log.v("ShimRegistrar", "Attached to an Activity.");
this.activityPluginBinding = binding;
this.addExistingListenersToActivityPluginBinding();
}
public void onDetachedFromActivityForConfigChanges() {
Log.v("ShimRegistrar", "Detached from an Activity for config changes.");
this.activityPluginBinding = null;
}
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
Log.v("ShimRegistrar", "Reconnected to an Activity after config changes.");
this.activityPluginBinding = binding;
this.addExistingListenersToActivityPluginBinding();
}
public void onDetachedFromActivity() {
Log.v("ShimRegistrar", "Detached from an Activity.");
this.activityPluginBinding = null;
}
private void addExistingListenersToActivityPluginBinding() {
Iterator var1 = this.requestPermissionsResultListeners.iterator();
while(var1.hasNext()) {
RequestPermissionsResultListener listener = (RequestPermissionsResultListener)var1.next();
this.activityPluginBinding.addRequestPermissionsResultListener(listener);
}
var1 = this.activityResultListeners.iterator();
while(var1.hasNext()) {
ActivityResultListener listener = (ActivityResultListener)var1.next();
this.activityPluginBinding.addActivityResultListener(listener);
}
var1 = this.newIntentListeners.iterator();
while(var1.hasNext()) {
NewIntentListener listener = (NewIntentListener)var1.next();
this.activityPluginBinding.addOnNewIntentListener(listener);
}
var1 = this.userLeaveHintListeners.iterator();
while(var1.hasNext()) {
UserLeaveHintListener listener = (UserLeaveHintListener)var1.next();
this.activityPluginBinding.addOnUserLeaveHintListener(listener);
}
}
}
...@@ -69,14 +69,13 @@ public class ContainerRecord implements IContainerRecord { ...@@ -69,14 +69,13 @@ public class ContainerRecord implements IContainerRecord {
@Override @Override
public void onCreate() { public void onCreate() {
Utils.assertCallOnMainThread(); Utils.assertCallOnMainThread();
BoostEngineProvider.assertEngineRunning();
if (mState != STATE_UNKNOW) { if (mState != STATE_UNKNOW) {
Debuger.exception("state error"); Debuger.exception("state error");
} }
mState = STATE_CREATED; mState = STATE_CREATED;
mContainer.getBoostFlutterView().onResume(); // mContainer.getBoostFlutterView().onResume();
mProxy.create(); mProxy.create();
} }
...@@ -92,9 +91,10 @@ public class ContainerRecord implements IContainerRecord { ...@@ -92,9 +91,10 @@ public class ContainerRecord implements IContainerRecord {
mManager.pushRecord(this); mManager.pushRecord(this);
mProxy.appear();
mContainer.getBoostFlutterView().onAttach(); mContainer.getBoostFlutterView().onAttach();
mProxy.appear();
} }
@Override @Override
...@@ -129,15 +129,15 @@ public class ContainerRecord implements IContainerRecord { ...@@ -129,15 +129,15 @@ public class ContainerRecord implements IContainerRecord {
mProxy.destroy(); mProxy.destroy();
mContainer.getBoostFlutterView().onDestroy(); // mContainer.getBoostFlutterView().onDestroy();
mManager.removeRecord(this); mManager.removeRecord(this);
mManager.setContainerResult(this,-1,-1,null); mManager.setContainerResult(this,-1,-1,null);
if (!mManager.hasContainerAppear()) { if (!mManager.hasContainerAppear()) {
mContainer.getBoostFlutterView().onPause(); // mContainer.getBoostFlutterView().onPause();
mContainer.getBoostFlutterView().onStop(); // mContainer.getBoostFlutterView().onStop();
} }
} }
...@@ -154,44 +154,45 @@ public class ContainerRecord implements IContainerRecord { ...@@ -154,44 +154,45 @@ public class ContainerRecord implements IContainerRecord {
map.put("name", mContainer.getContainerUrl()); map.put("name", mContainer.getContainerUrl());
map.put("uniqueId", mUniqueId); map.put("uniqueId", mUniqueId);
FlutterBoost.singleton().channel().sendEvent("lifecycle", map); FlutterBoost.instance().channel().sendEvent("lifecycle", map);
mContainer.getBoostFlutterView().onBackPressed(); // mContainer.getBoostFlutterView().onBackPressed();
} }
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
mContainer.getBoostFlutterView().onRequestPermissionsResult(requestCode, permissions, grantResults);
} }
@Override @Override
public void onNewIntent(Intent intent) { public void onNewIntent(Intent intent) {
mContainer.getBoostFlutterView().onNewIntent(intent);
} }
@Override @Override
public void onActivityResult(int requestCode, int resultCode, Intent data) { public void onActivityResult(int requestCode, int resultCode, Intent data) {
mContainer.getBoostFlutterView().onActivityResult(requestCode, resultCode, data);
} }
@Override @Override
public void onContainerResult(int requestCode, int resultCode, Map<String, Object> result) { public void onContainerResult(int requestCode, int resultCode, Map<String, Object> result) {
mManager.setContainerResult(this, requestCode,resultCode, result); mManager.setContainerResult(this, requestCode,resultCode, result);
} }
@Override @Override
public void onUserLeaveHint() { public void onUserLeaveHint() {
mContainer.getBoostFlutterView().onUserLeaveHint();
} }
@Override @Override
public void onTrimMemory(int level) { public void onTrimMemory(int level) {
mContainer.getBoostFlutterView().onTrimMemory(level);
} }
@Override @Override
public void onLowMemory() { public void onLowMemory() {
mContainer.getBoostFlutterView().onLowMemory();
} }
...@@ -252,7 +253,7 @@ public class ContainerRecord implements IContainerRecord { ...@@ -252,7 +253,7 @@ public class ContainerRecord implements IContainerRecord {
args.put("pageName", url); args.put("pageName", url);
args.put("params", params); args.put("params", params);
args.put("uniqueId", uniqueId); args.put("uniqueId", uniqueId);
FlutterBoost.singleton().channel().invokeMethod(method, args); FlutterBoost.instance().channel().invokeMethod(method, args);
} }
public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) { public void invokeChannelUnsafe(String method, String url, Map params, String uniqueId) {
...@@ -260,7 +261,7 @@ public class ContainerRecord implements IContainerRecord { ...@@ -260,7 +261,7 @@ public class ContainerRecord implements IContainerRecord {
args.put("pageName", url); args.put("pageName", url);
args.put("params", params); args.put("params", params);
args.put("uniqueId", uniqueId); args.put("uniqueId", uniqueId);
FlutterBoost.singleton().channel().invokeMethodUnsafe(method, args); FlutterBoost.instance().channel().invokeMethodUnsafe(method, args);
} }
} }
......
...@@ -60,7 +60,7 @@ public class Debuger { ...@@ -60,7 +60,7 @@ public class Debuger {
public static boolean isDebug(){ public static boolean isDebug(){
try { try {
return FlutterBoost.singleton().platform().isDebug(); return FlutterBoost.instance().platform().isDebug();
}catch (Throwable t){ }catch (Throwable t){
return false; return false;
} }
......
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost; package com.idlefish.flutterboost;
import android.app.Activity; import android.app.Activity;
import android.app.Application; import android.app.Application;
import android.content.Intent; import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.NonNull;
import com.idlefish.flutterboost.interfaces.*;
import com.idlefish.flutterboost.interfaces.IContainerManager; import io.flutter.embedding.android.FlutterView;
import com.idlefish.flutterboost.interfaces.IContainerRecord; import io.flutter.embedding.engine.FlutterEngine;
import com.idlefish.flutterboost.interfaces.IFlutterEngineProvider; import io.flutter.embedding.engine.FlutterShellArgs;
import com.idlefish.flutterboost.interfaces.IFlutterViewContainer; import io.flutter.embedding.engine.dart.DartExecutor;
import com.idlefish.flutterboost.interfaces.IPlatform; import io.flutter.plugin.common.PluginRegistry;
import com.idlefish.flutterboost.interfaces.IStateListener; import io.flutter.view.FlutterMain;
import java.lang.reflect.Method;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class FlutterBoost { public class FlutterBoost {
private Platform mPlatform;
private FlutterViewContainerManager mManager;
private FlutterEngine mEngine;
private Activity mCurrentActiveActivity;
private PluginRegistry mRegistry;
static FlutterBoost sInstance = null; static FlutterBoost sInstance = null;
public static synchronized void init(IPlatform platform) { private long FlutterPostFrameCallTime = 0;
if (sInstance == null) { private Application.ActivityLifecycleCallbacks mActivityLifecycleCallbacks;
sInstance = new FlutterBoost(platform);
}
if (platform.whenEngineStart() == IPlatform.IMMEDIATELY) { public long getFlutterPostFrameCallTime() {
sInstance.mEngineProvider return FlutterPostFrameCallTime;
.provideEngine(platform.getApplication())
.startRun(null);
} }
public void setFlutterPostFrameCallTime(long FlutterPostFrameCallTime) {
this.FlutterPostFrameCallTime = FlutterPostFrameCallTime;
} }
public static FlutterBoost singleton() { public static FlutterBoost instance() {
if (sInstance == null) { if (sInstance == null) {
throw new RuntimeException("FlutterBoost not init yet"); sInstance = new FlutterBoost();
} }
return sInstance; return sInstance;
} }
private final IPlatform mPlatform; public void init(Platform platform) {
private final FlutterViewContainerManager mManager;
private final IFlutterEngineProvider mEngineProvider;
IStateListener mStateListener;
Activity mCurrentActiveActivity;
private FlutterBoost(IPlatform platform) {
mPlatform = platform; mPlatform = platform;
mManager = new FlutterViewContainerManager(); mManager = new FlutterViewContainerManager();
IFlutterEngineProvider provider = platform.engineProvider(); mActivityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
if(provider == null) {
provider = new BoostEngineProvider();
}
mEngineProvider = provider;
platform.getApplication().registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks());
BoostChannel.addActionAfterRegistered(new BoostChannel.ActionAfterRegistered() {
@Override @Override
public void onChannelRegistered(BoostChannel channel) { public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
channel.addMethodCallHandler(new BoostMethodHandler()); mCurrentActiveActivity = activity;
} if (mPlatform.whenEngineStart() == ConfigBuilder.ANY_ACTIVITY_CREATED) {
}); doInitialFlutter();
} boostPluginRegistry();
public IFlutterEngineProvider engineProvider() {
return sInstance.mEngineProvider;
}
public IContainerManager containerManager() {
return sInstance.mManager;
}
public IPlatform platform() {
return sInstance.mPlatform;
}
public BoostChannel channel() {
return BoostChannel.singleton();
}
public Activity currentActivity() {
return sInstance.mCurrentActiveActivity;
}
public IFlutterViewContainer findContainerById(String id) {
return mManager.findContainerById(id);
} }
if (mPlatform.whenEngineStart() == ConfigBuilder.IMMEDIATELY) {
boostPluginRegistry();
public void setStateListener(@Nullable IStateListener listener){
mStateListener = listener;
} }
class ActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
if (platform().whenEngineStart() == IPlatform.ANY_ACTIVITY_CREATED) {
sInstance.mEngineProvider
.provideEngine(activity)
.startRun(activity);
}
} }
@Override @Override
...@@ -135,10 +73,10 @@ public class FlutterBoost { ...@@ -135,10 +73,10 @@ public class FlutterBoost {
if (mCurrentActiveActivity == null) { if (mCurrentActiveActivity == null) {
Debuger.log("Application entry foreground"); Debuger.log("Application entry foreground");
if (mEngineProvider.tryGetEngine() != null) { if (createEngine() != null) {
HashMap<String, String> map = new HashMap<>(); HashMap<String, String> map = new HashMap<>();
map.put("type", "foreground"); map.put("type", "foreground");
channel().sendEvent("lifecycle",map); channel().sendEvent("lifecycle", map);
} }
} }
mCurrentActiveActivity = activity; mCurrentActiveActivity = activity;
...@@ -159,10 +97,10 @@ public class FlutterBoost { ...@@ -159,10 +97,10 @@ public class FlutterBoost {
if (mCurrentActiveActivity == activity) { if (mCurrentActiveActivity == activity) {
Debuger.log("Application entry background"); Debuger.log("Application entry background");
if (mEngineProvider.tryGetEngine() != null) { if (createEngine() != null) {
HashMap<String, String> map = new HashMap<>(); HashMap<String, String> map = new HashMap<>();
map.put("type", "background"); map.put("type", "background");
channel().sendEvent("lifecycle",map); channel().sendEvent("lifecycle", map);
} }
mCurrentActiveActivity = null; mCurrentActiveActivity = null;
} }
...@@ -178,106 +116,244 @@ public class FlutterBoost { ...@@ -178,106 +116,244 @@ public class FlutterBoost {
if (mCurrentActiveActivity == activity) { if (mCurrentActiveActivity == activity) {
Debuger.log("Application entry background"); Debuger.log("Application entry background");
if (mEngineProvider.tryGetEngine() != null) { if (createEngine() != null) {
HashMap<String, String> map = new HashMap<>(); HashMap<String, String> map = new HashMap<>();
map.put("type", "background"); map.put("type", "background");
channel().sendEvent("lifecycle",map); channel().sendEvent("lifecycle", map);
} }
mCurrentActiveActivity = null; mCurrentActiveActivity = null;
} }
} }
};
platform.getApplication().registerActivityLifecycleCallbacks(mActivityLifecycleCallbacks);
if (mPlatform.whenEngineStart() == ConfigBuilder.IMMEDIATELY) {
doInitialFlutter();
} }
class BoostMethodHandler implements MethodChannel.MethodCallHandler {
@Override }
public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
switch (methodCall.method) {
case "pageOnStart":
{
Map<String, Object> pageInfo = new HashMap<>();
try { public void doInitialFlutter() {
IContainerRecord record = mManager.getCurrentTopRecord();
if (record == null) {
record = mManager.getLastGenerateRecord(); if (mEngine != null) return;
FlutterEngine flutterEngine = createEngine();
if (mPlatform.lifecycleListener != null) {
mPlatform.lifecycleListener.onEngineCreated();
}
if (flutterEngine.getDartExecutor().isExecutingDart()) {
return;
}
if (mPlatform.initialRoute() != null) {
flutterEngine.getNavigationChannel().setInitialRoute(mPlatform.initialRoute());
} }
DartExecutor.DartEntrypoint entrypoint = new DartExecutor.DartEntrypoint(
FlutterMain.findAppBundlePath(),
"main"
);
flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint);
mRegistry = new BoostPluginRegistry(createEngine());
if(record != null) {
pageInfo.put("name", record.getContainer().getContainerUrl());
pageInfo.put("params", record.getContainer().getContainerUrlParams());
pageInfo.put("uniqueId", record.uniqueId());
} }
result.success(pageInfo); public void boostPluginRegistry(){
} catch (Throwable t) { if(mRegistry!=null&& !mRegistry.hasPlugin("boostPluginRegistry")){
result.error("no flutter page found!",t.getMessage(),t); mPlatform.registerPlugins(mRegistry);
mRegistry.registrarFor("boostPluginRegistry");
} }
}
public static class ConfigBuilder {
public static final String DEFAULT_DART_ENTRYPOINT = "main";
public static final String DEFAULT_INITIAL_ROUTE = "/";
public static int IMMEDIATELY = 0; //立即启动引擎
public static int ANY_ACTIVITY_CREATED = 1; //当有任何Activity创建时,启动引擎
public static int FLUTTER_ACTIVITY_CREATED = 2; //当有flutterActivity创建时,启动引擎
public static int APP_EXit = 0; //所有flutter Activity destory 时,销毁engine
public static int All_FLUTTER_ACTIVITY_DESTROY = 1; //所有flutter Activity destory 时,销毁engine
private String dartEntrypoint = DEFAULT_DART_ENTRYPOINT;
private String initialRoute = DEFAULT_INITIAL_ROUTE;
private int whenEngineStart = ANY_ACTIVITY_CREATED;
private int whenEngineDestory = APP_EXit;
private boolean isDebug = false;
private FlutterView.RenderMode renderMode = FlutterView.RenderMode.texture;
private Application mApp;
private INativeRouter router = null;
private BoostLifecycleListener lifecycleListener;
private BoostPluginsRegister boostPluginsRegister;
public ConfigBuilder(Application app, INativeRouter router) {
this.router = router;
this.mApp = app;
}
public ConfigBuilder renderMode(FlutterView.RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
public ConfigBuilder dartEntrypoint(@NonNull String dartEntrypoint) {
this.dartEntrypoint = dartEntrypoint;
return this;
}
public ConfigBuilder initialRoute(@NonNull String initialRoute) {
this.initialRoute = initialRoute;
return this;
}
public ConfigBuilder isDebug(boolean isDebug) {
this.isDebug = isDebug;
return this;
}
public ConfigBuilder whenEngineStart(int whenEngineStart) {
this.whenEngineStart = whenEngineStart;
return this;
}
public ConfigBuilder lifecycleListener(BoostLifecycleListener lifecycleListener) {
this.lifecycleListener = lifecycleListener;
return this;
}
public ConfigBuilder pluginsRegister(BoostPluginsRegister boostPluginsRegister) {
this.boostPluginsRegister = boostPluginsRegister;
return this;
}
public Platform build() {
Platform platform = new Platform() {
public Application getApplication() {
return ConfigBuilder.this.mApp;
}
public boolean isDebug() {
return ConfigBuilder.this.isDebug;
} }
break;
case "openPage":
{
try {
Map<String,Object> params = methodCall.argument("urlParams");
Map<String,Object> exts = methodCall.argument("exts");
String url = methodCall.argument("url");
mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
@Override @Override
public void onResult(Map<String, Object> rlt) { public String initialRoute() {
if (result != null) { return ConfigBuilder.this.initialRoute;
result.success(rlt);
} }
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
router.openContainer(context, url, urlParams, requestCode, exts);
} }
});
}catch (Throwable t){
result.error("open page error",t.getMessage(),t); public int whenEngineStart() {
return ConfigBuilder.this.whenEngineStart;
} }
public FlutterView.RenderMode renderMode() {
return ConfigBuilder.this.renderMode;
} }
break; };
case "closePage":
{ platform.lifecycleListener = this.lifecycleListener;
try { platform.pluginsRegister=this.boostPluginsRegister;
String uniqueId = methodCall.argument("uniqueId"); return platform;
Map<String,Object> resultData = methodCall.argument("result");
Map<String,Object> exts = methodCall.argument("exts");
mManager.closeContainer(uniqueId, resultData,exts);
result.success(true);
}catch (Throwable t){
result.error("close page error",t.getMessage(),t);
} }
} }
break;
case "onShownContainerChanged":
{
try {
String newId = methodCall.argument("newName");
String oldId = methodCall.argument("oldName");
mManager.onShownContainerChanged(newId,oldId); public IContainerManager containerManager() {
result.success(true); return sInstance.mManager;
}catch (Throwable t){
result.error("onShownContainerChanged",t.getMessage(),t);
} }
public Platform platform() {
return sInstance.mPlatform;
}
public FlutterBoostPlugin channel() {
return FlutterBoostPlugin.singleton();
} }
break;
default: public Activity currentActivity() {
{ return sInstance.mCurrentActiveActivity;
result.notImplemented();
} }
public IFlutterViewContainer findContainerById(String id) {
return mManager.findContainerById(id);
} }
public PluginRegistry getPluginRegistry() {
return mRegistry;
} }
private FlutterEngine createEngine() {
if (mEngine == null) {
FlutterMain.startInitialization(mPlatform.getApplication());
FlutterShellArgs flutterShellArgs = new FlutterShellArgs(new String[0]);
FlutterMain.ensureInitializationComplete(
mPlatform.getApplication().getApplicationContext(), flutterShellArgs.toArray());
mEngine = new FlutterEngine(mPlatform.getApplication().getApplicationContext());
} }
return mEngine;
public static void setBoostResult(Activity activity, HashMap result) {
Intent intent = new Intent();
if (result != null) {
intent.putExtra(IFlutterViewContainer.RESULT_KEY, result);
} }
activity.setResult(Activity.RESULT_OK, intent);
public FlutterEngine engineProvider() {
return mEngine;
} }
}
public void boostDestroy() {
if (mEngine != null) {
mEngine.destroy();
}
if (mPlatform.lifecycleListener != null) {
mPlatform.lifecycleListener.onEngineDestroy();
}
mEngine = null;
mRegistry = null;
mCurrentActiveActivity = null;
}
public interface BoostLifecycleListener {
void onEngineCreated();
void onPluginsRegistered();
void onEngineDestroy();
}
public interface BoostPluginsRegister {
void registerPlugins(PluginRegistry mRegistry);
}
}
...@@ -2,64 +2,54 @@ package com.idlefish.flutterboost; ...@@ -2,64 +2,54 @@ package com.idlefish.flutterboost;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import com.idlefish.flutterboost.interfaces.IStateListener; import com.idlefish.flutterboost.interfaces.IContainerRecord;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.*;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry;
public class BoostChannel { public class FlutterBoostPlugin {
private static BoostChannel sInstance; private static FlutterBoostPlugin sInstance;
private final MethodChannel mMethodChannel; private final MethodChannel mMethodChannel;
private final Set<MethodChannel.MethodCallHandler> mMethodCallHandlers = new HashSet<>(); private final Set<MethodChannel.MethodCallHandler> mMethodCallHandlers = new HashSet<>();
private final Map<String,Set<EventListener>> mEventListeners = new HashMap<>(); private final Map<String, Set<EventListener>> mEventListeners = new HashMap<>();
private static final Set<ActionAfterRegistered> sActions = new HashSet<>(); private static final Set<ActionAfterRegistered> sActions = new HashSet<>();
public static BoostChannel singleton() { public static FlutterBoostPlugin singleton() {
if (sInstance == null) { if (sInstance == null) {
throw new RuntimeException("BoostChannel not register yet"); throw new RuntimeException("FlutterBoostPlugin not register yet");
} }
return sInstance; return sInstance;
} }
public static void addActionAfterRegistered(ActionAfterRegistered action) { public static void addActionAfterRegistered(ActionAfterRegistered action) {
if(action == null) return; if (action == null) return;
if(sInstance == null) { if (sInstance == null) {
sActions.add(action); sActions.add(action);
}else{ } else {
action.onChannelRegistered(sInstance); action.onChannelRegistered(sInstance);
} }
} }
public static void registerWith(PluginRegistry.Registrar registrar) { public static void registerWith(PluginRegistry.Registrar registrar) {
sInstance = new BoostChannel(registrar); sInstance = new FlutterBoostPlugin(registrar);
for(ActionAfterRegistered a : sActions) { for (ActionAfterRegistered a : sActions) {
a.onChannelRegistered(sInstance); a.onChannelRegistered(sInstance);
} }
if(FlutterBoost.sInstance != null) {
final IStateListener stateListener = FlutterBoost.sInstance.mStateListener;
if (stateListener != null) {
stateListener.onChannelRegistered(registrar, sInstance);
}
}
sActions.clear(); sActions.clear();
} }
private BoostChannel(PluginRegistry.Registrar registrar){ private FlutterBoostPlugin(PluginRegistry.Registrar registrar) {
mMethodChannel = new MethodChannel(registrar.messenger(), "flutter_boost"); mMethodChannel = new MethodChannel(registrar.messenger(), "flutter_boost");
mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() { mMethodChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
...@@ -78,26 +68,29 @@ public class BoostChannel { ...@@ -78,26 +68,29 @@ public class BoostChannel {
} }
} }
if(listeners != null) { if (listeners != null) {
for(Object o:listeners) { for (Object o : listeners) {
((EventListener)o).onEvent(name,args); ((EventListener) o).onEvent(name, args);
} }
} }
}else{ } else {
Object[] handlers; Object[] handlers;
synchronized (mMethodCallHandlers) { synchronized (mMethodCallHandlers) {
handlers = mMethodCallHandlers.toArray(); handlers = mMethodCallHandlers.toArray();
} }
for(Object o:handlers) { for (Object o : handlers) {
((MethodChannel.MethodCallHandler)o).onMethodCall(methodCall,result); ((MethodChannel.MethodCallHandler) o).onMethodCall(methodCall, result);
} }
} }
} }
}); });
addMethodCallHandler(new BoostMethodHandler());
} }
public void invokeMethodUnsafe(final String name,Serializable args){ public void invokeMethodUnsafe(final String name, Serializable args) {
invokeMethod(name, args, new MethodChannel.Result() { invokeMethod(name, args, new MethodChannel.Result() {
@Override @Override
public void success(@Nullable Object o) { public void success(@Nullable Object o) {
...@@ -106,17 +99,17 @@ public class BoostChannel { ...@@ -106,17 +99,17 @@ public class BoostChannel {
@Override @Override
public void error(String s, @Nullable String s1, @Nullable Object o) { public void error(String s, @Nullable String s1, @Nullable Object o) {
Debuger.log("invoke method "+name+" error:"+s+" | "+s1); Debuger.log("invoke method " + name + " error:" + s + " | " + s1);
} }
@Override @Override
public void notImplemented() { public void notImplemented() {
Debuger.log("invoke method "+name+" notImplemented"); Debuger.log("invoke method " + name + " notImplemented");
} }
}); });
} }
public void invokeMethod(final String name,Serializable args){ public void invokeMethod(final String name, Serializable args) {
invokeMethod(name, args, new MethodChannel.Result() { invokeMethod(name, args, new MethodChannel.Result() {
@Override @Override
public void success(@Nullable Object o) { public void success(@Nullable Object o) {
...@@ -125,18 +118,18 @@ public class BoostChannel { ...@@ -125,18 +118,18 @@ public class BoostChannel {
@Override @Override
public void error(String s, @Nullable String s1, @Nullable Object o) { public void error(String s, @Nullable String s1, @Nullable Object o) {
Debuger.exception("invoke method "+name+" error:"+s+" | "+s1); Debuger.exception("invoke method " + name + " error:" + s + " | " + s1);
} }
@Override @Override
public void notImplemented() { public void notImplemented() {
Debuger.exception("invoke method "+name+" notImplemented"); Debuger.exception("invoke method " + name + " notImplemented");
} }
}); });
} }
public void invokeMethod(final String name,Serializable args,MethodChannel.Result result){ public void invokeMethod(final String name, Serializable args, MethodChannel.Result result) {
if("__event__".equals(name)) { if ("__event__".equals(name)) {
Debuger.exception("method name should not be __event__"); Debuger.exception("method name should not be __event__");
} }
...@@ -144,7 +137,7 @@ public class BoostChannel { ...@@ -144,7 +137,7 @@ public class BoostChannel {
} }
public void addMethodCallHandler(MethodChannel.MethodCallHandler handler) { public void addMethodCallHandler(MethodChannel.MethodCallHandler handler) {
synchronized (mMethodCallHandlers){ synchronized (mMethodCallHandlers) {
mMethodCallHandlers.add(handler); mMethodCallHandlers.add(handler);
} }
} }
...@@ -156,29 +149,29 @@ public class BoostChannel { ...@@ -156,29 +149,29 @@ public class BoostChannel {
} }
public void addEventListener(String name, EventListener listener) { public void addEventListener(String name, EventListener listener) {
synchronized (mEventListeners){ synchronized (mEventListeners) {
Set<EventListener> set = mEventListeners.get(name); Set<EventListener> set = mEventListeners.get(name);
if(set == null) { if (set == null) {
set = new HashSet<>(); set = new HashSet<>();
} }
set.add(listener); set.add(listener);
mEventListeners.put(name,set); mEventListeners.put(name, set);
} }
} }
public void removeEventListener(EventListener listener) { public void removeEventListener(EventListener listener) {
synchronized (mEventListeners) { synchronized (mEventListeners) {
for(Set<EventListener> set:mEventListeners.values()) { for (Set<EventListener> set : mEventListeners.values()) {
set.remove(listener); set.remove(listener);
} }
} }
} }
public void sendEvent(String name,Map args){ public void sendEvent(String name, Map args) {
Map event = new HashMap(); Map event = new HashMap();
event.put("name",name); event.put("name", name);
event.put("arguments",args); event.put("arguments", args);
mMethodChannel.invokeMethod("__event__",event); mMethodChannel.invokeMethod("__event__", event);
} }
public interface EventListener { public interface EventListener {
...@@ -186,6 +179,90 @@ public class BoostChannel { ...@@ -186,6 +179,90 @@ public class BoostChannel {
} }
public interface ActionAfterRegistered { public interface ActionAfterRegistered {
void onChannelRegistered(BoostChannel channel); void onChannelRegistered(FlutterBoostPlugin channel);
}
class BoostMethodHandler implements MethodChannel.MethodCallHandler {
@Override
public void onMethodCall(MethodCall methodCall, final MethodChannel.Result result) {
FlutterViewContainerManager mManager = (FlutterViewContainerManager) FlutterBoost.instance().containerManager();
switch (methodCall.method) {
case "pageOnStart": {
Map<String, Object> pageInfo = new HashMap<>();
try {
IContainerRecord record = mManager.getCurrentTopRecord();
if (record == null) {
record = mManager.getLastGenerateRecord();
}
if (record != null) {
pageInfo.put("name", record.getContainer().getContainerUrl());
pageInfo.put("params", record.getContainer().getContainerUrlParams());
pageInfo.put("uniqueId", record.uniqueId());
}
result.success(pageInfo);
FlutterBoost.instance().setFlutterPostFrameCallTime(new Date().getTime());
} catch (Throwable t) {
result.error("no flutter page found!", t.getMessage(), t);
}
}
break;
case "openPage": {
try {
Map<String, Object> params = methodCall.argument("urlParams");
Map<String, Object> exts = methodCall.argument("exts");
String url = methodCall.argument("url");
mManager.openContainer(url, params, exts, new FlutterViewContainerManager.OnResult() {
@Override
public void onResult(Map<String, Object> rlt) {
if (result != null) {
result.success(rlt);
}
}
});
} catch (Throwable t) {
result.error("open page error", t.getMessage(), t);
}
}
break;
case "closePage": {
try {
String uniqueId = methodCall.argument("uniqueId");
Map<String, Object> resultData = methodCall.argument("result");
Map<String, Object> exts = methodCall.argument("exts");
mManager.closeContainer(uniqueId, resultData, exts);
result.success(true);
} catch (Throwable t) {
result.error("close page error", t.getMessage(), t);
}
}
break;
case "onShownContainerChanged": {
try {
String newId = methodCall.argument("newName");
String oldId = methodCall.argument("oldName");
mManager.onShownContainerChanged(newId, oldId);
result.success(true);
} catch (Throwable t) {
result.error("onShownContainerChanged", t.getMessage(), t);
}
}
break;
default: {
result.notImplemented();
}
}
}
} }
} }
...@@ -25,7 +25,6 @@ package com.idlefish.flutterboost; ...@@ -25,7 +25,6 @@ package com.idlefish.flutterboost;
import android.content.Context; import android.content.Context;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.SparseArray;
import com.idlefish.flutterboost.interfaces.IContainerManager; import com.idlefish.flutterboost.interfaces.IContainerManager;
import com.idlefish.flutterboost.interfaces.IContainerRecord; import com.idlefish.flutterboost.interfaces.IContainerRecord;
...@@ -41,7 +40,6 @@ import java.util.LinkedHashMap; ...@@ -41,7 +40,6 @@ import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.Stack; import java.util.Stack;
import java.util.concurrent.atomic.AtomicInteger;
public class FlutterViewContainerManager implements IContainerManager { public class FlutterViewContainerManager implements IContainerManager {
...@@ -84,6 +82,11 @@ public class FlutterViewContainerManager implements IContainerManager { ...@@ -84,6 +82,11 @@ public class FlutterViewContainerManager implements IContainerManager {
void removeRecord(IContainerRecord record) { void removeRecord(IContainerRecord record) {
mRecordStack.remove(record); mRecordStack.remove(record);
mRecordMap.remove(record.getContainer()); mRecordMap.remove(record.getContainer());
// if(mRecordMap.isEmpty()){
//
// }
} }
void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) { void setContainerResult(IContainerRecord record,int requestCode, int resultCode, Map<String,Object> result) {
...@@ -111,9 +114,9 @@ public class FlutterViewContainerManager implements IContainerManager { ...@@ -111,9 +114,9 @@ public class FlutterViewContainerManager implements IContainerManager {
} }
void openContainer(String url, Map<String, Object> urlParams, Map<String, Object> exts,OnResult onResult) { void openContainer(String url, Map<String, Object> urlParams, Map<String, Object> exts,OnResult onResult) {
Context context = FlutterBoost.singleton().currentActivity(); Context context = FlutterBoost.instance().currentActivity();
if(context == null) { if(context == null) {
context = FlutterBoost.singleton().platform().getApplication(); context = FlutterBoost.instance().platform().getApplication();
} }
if(urlParams == null) { if(urlParams == null) {
...@@ -128,11 +131,13 @@ public class FlutterViewContainerManager implements IContainerManager { ...@@ -128,11 +131,13 @@ public class FlutterViewContainerManager implements IContainerManager {
final String uniqueId = ContainerRecord.genUniqueId(url); final String uniqueId = ContainerRecord.genUniqueId(url);
urlParams.put(IContainerRecord.UNIQ_KEY,uniqueId); urlParams.put(IContainerRecord.UNIQ_KEY,uniqueId);
IContainerRecord currentTopRecord = getCurrentTopRecord();
if(onResult != null) { if(onResult != null) {
mOnResults.put(uniqueId,onResult); mOnResults.put(currentTopRecord.uniqueId(),onResult);
} }
FlutterBoost.singleton().platform().openContainer(context,url,urlParams,requestCode,exts); FlutterBoost.instance().platform().openContainer(context,url,urlParams,requestCode,exts);
} }
IContainerRecord closeContainer(String uniqueId, Map<String, Object> result,Map<String,Object> exts) { IContainerRecord closeContainer(String uniqueId, Map<String, Object> result,Map<String,Object> exts) {
...@@ -148,7 +153,7 @@ public class FlutterViewContainerManager implements IContainerManager { ...@@ -148,7 +153,7 @@ public class FlutterViewContainerManager implements IContainerManager {
Debuger.exception("closeContainer can not find uniqueId:" + uniqueId); Debuger.exception("closeContainer can not find uniqueId:" + uniqueId);
} }
FlutterBoost.singleton().platform().closeContainer(targetRecord,result,exts); FlutterBoost.instance().platform().closeContainer(targetRecord,result,exts);
return targetRecord; return targetRecord;
} }
......
package com.idlefish.flutterboost; package com.idlefish.flutterboost;
import android.app.Application;
import android.content.Context;
import android.util.Log;
import com.idlefish.flutterboost.interfaces.IContainerRecord; import com.idlefish.flutterboost.interfaces.IContainerRecord;
import com.idlefish.flutterboost.interfaces.IFlutterEngineProvider;
import com.idlefish.flutterboost.interfaces.IPlatform;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.Map; import java.util.Map;
import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry;
public abstract class Platform implements IPlatform { public abstract class Platform {
@Override public abstract Application getApplication();
public boolean isDebug() {
return false; public abstract void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts);
}
public abstract int whenEngineStart();
public abstract FlutterView.RenderMode renderMode();
public abstract boolean isDebug();
public abstract String initialRoute();
public FlutterBoost.BoostLifecycleListener lifecycleListener;
public FlutterBoost.BoostPluginsRegister pluginsRegister;
@Override
public void closeContainer(IContainerRecord record, Map<String, Object> result, Map<String, Object> exts) { public void closeContainer(IContainerRecord record, Map<String, Object> result, Map<String, Object> exts) {
if(record == null) return; if (record == null) return;
record.getContainer().finishContainer(result); record.getContainer().finishContainer(result);
} }
@Override
public IFlutterEngineProvider engineProvider() {
return new BoostEngineProvider();
}
@Override public void registerPlugins(PluginRegistry mRegistry) {
public void registerPlugins(PluginRegistry registry) {
try { if(pluginsRegister!=null){
Class clz = Class.forName("io.flutter.plugins.GeneratedPluginRegistrant"); pluginsRegister.registerPlugins(mRegistry);
Method method = clz.getDeclaredMethod("registerWith",PluginRegistry.class);
method.invoke(null,registry);
}catch (Throwable t){
throw new RuntimeException(t);
}
} }
@Override if (lifecycleListener!= null) {
public int whenEngineStart() { lifecycleListener.onPluginsRegistered();
return ANY_ACTIVITY_CREATED; }
} }
} }
package com.idlefish.flutterboost;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import java.util.LinkedList;
import java.util.List;
public class SnapshotView extends FrameLayout {
private ImageView mImg;
public SnapshotView(@NonNull Context context) {
super(context);
init();
}
public SnapshotView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init();
}
public SnapshotView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init(){
setBackgroundColor(Color.RED);
mImg = new ImageView(getContext());
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
params.gravity = Gravity.CENTER;
mImg.setScaleType(ImageView.ScaleType.FIT_XY);
mImg.setLayoutParams(params);
addView(mImg);
}
public void toggleSnapshot(BoostFlutterView flutterView){
if (!dismissSnapshot(flutterView)) {
showSnapshot(flutterView);
}
}
public boolean showSnapshot(BoostFlutterView flutterView){
if(flutterView == null) return false;
dismissSnapshot(flutterView);
final Bitmap bitmap = flutterView.getEngine().getRenderer().getBitmap();
if(bitmap == null || bitmap.isRecycled()) {
return false;
}
mImg.setImageBitmap(bitmap);
flutterView.addView(this,new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
Debuger.log("showSnapshot");
return true;
}
public boolean dismissSnapshot(BoostFlutterView flutterView){
List<View> snapshots = new LinkedList<>();
for(int index = 0;index < flutterView.getChildCount();index++){
View view = flutterView.getChildAt(index);
if(view instanceof SnapshotView) {
snapshots.add(view);
}
}
if(snapshots.isEmpty()) {
return false;
}else{
for(View v:snapshots) {
flutterView.removeView(v);
}
Debuger.log("dismissSnapshot");
return true;
}
}
}
package com.idlefish.flutterboost;
import com.idlefish.flutterboost.interfaces.IStateListener;
import io.flutter.plugin.common.PluginRegistry;
public class StateListener implements IStateListener {
@Override
public void onEngineCreated(BoostFlutterEngine engine) {
Debuger.log(">>onEngineCreated");
}
@Override
public void onEngineStarted(BoostFlutterEngine engine) {
Debuger.log(">>onEngineStarted");
}
@Override
public void onChannelRegistered(PluginRegistry.Registrar registrar, BoostChannel channel) {
Debuger.log(">>onFlutterViewInited");
}
@Override
public void onFlutterViewInited(BoostFlutterEngine engine, BoostFlutterView flutterView) {
Debuger.log(">>onFlutterViewInited");
}
@Override
public void beforeEngineAttach(BoostFlutterEngine engine, BoostFlutterView flutterView) {
Debuger.log(">>beforeEngineAttach");
}
@Override
public void afterEngineAttached(BoostFlutterEngine engine, BoostFlutterView flutterView) {
Debuger.log(">>afterEngineAttached");
}
@Override
public void beforeEngineDetach(BoostFlutterEngine engine, BoostFlutterView flutterView) {
Debuger.log(">>beforeEngineDetach");
}
@Override
public void afterEngineDetached(BoostFlutterEngine engine, BoostFlutterView flutterView) {
Debuger.log(">>afterEngineDetached");
}
}
...@@ -35,14 +35,14 @@ import android.view.View; ...@@ -35,14 +35,14 @@ import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import com.alibaba.fastjson.JSON;
import java.io.BufferedReader; import java.io.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.List;
import java.util.Map;
public class Utils { public class Utils {
...@@ -263,7 +263,7 @@ public class Utils { ...@@ -263,7 +263,7 @@ public class Utils {
return; return;
} }
String [] arr = new String[]{"mLastSrvView", "mServedView", "mNextServedView"}; String [] arr = new String[]{"mLastSrvView","mServedView", "mNextServedView"};
Field f = null; Field f = null;
Object obj_get = null; Object obj_get = null;
for (int i = 0;i < arr.length;i ++) { for (int i = 0;i < arr.length;i ++) {
...@@ -272,21 +272,71 @@ public class Utils { ...@@ -272,21 +272,71 @@ public class Utils {
f = imm.getClass().getDeclaredField(param); f = imm.getClass().getDeclaredField(param);
if (f.isAccessible() == false) { if (f.isAccessible() == false) {
f.setAccessible(true); f.setAccessible(true);
} // author: sodino mail:sodino@qq.com }
obj_get = f.get(imm); obj_get = f.get(imm);
if (obj_get != null && obj_get instanceof View) { if (obj_get != null && obj_get instanceof View) {
View v_get = (View) obj_get; View v_get = (View) obj_get;
if (v_get.getContext() == destContext) { // 被InputMethodManager持有引用的context是想要目标销毁的 if (v_get.getContext() == destContext) {
f.set(imm, null); // 置空,破坏掉path to gc节点 f.set(imm, null);
} else { } else {
// 不是想要目标销毁的,即为又进了另一层界面了,不要处理,避免影响原逻辑,也就不用继续for循环了
break; break;
} }
} }
}catch(Throwable t){ }catch(Throwable t){
t.printStackTrace(); // t.printStackTrace();
}
}
}
public static String assembleUrl(String url,Map<String, Object> urlParams){
StringBuilder targetUrl = new StringBuilder(url);
if(urlParams != null && !urlParams.isEmpty()) {
if(!targetUrl.toString().contains("?")){
targetUrl.append("?");
}
for(Map.Entry entry:urlParams.entrySet()) {
if(entry.getValue() instanceof Map ) {
Map<String,Object> params = (Map<String,Object> )entry.getValue();
for(Map.Entry param:params.entrySet()) {
String key = (String)param.getKey();
String value = null;
if(param.getValue() instanceof Map || param.getValue() instanceof List) {
try {
value = URLEncoder.encode(JSON.toJSONString(param.getValue()), "UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}else{
value = (param.getValue()==null?null:URLEncoder.encode( String.valueOf(param.getValue())));
}
if(value==null){
continue;
}
if(targetUrl.toString().endsWith("?")){
targetUrl.append(key).append("=").append(value);
}else{
targetUrl.append("&").append(key).append("=").append(value);
}
} }
} }
} }
}
return targetUrl.toString();
}
} }
\ No newline at end of file
package com.idlefish.flutterboost;
import android.content.Context;
import android.graphics.SurfaceTexture;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.Surface;
import android.view.TextureView;
import io.flutter.Log;
import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class XFlutterTextureView extends TextureView implements FlutterRenderer.RenderSurface {
private static final String TAG = "XFlutterTextureView";
private boolean isSurfaceAvailableForRendering;
private boolean isAttachedToFlutterRenderer;
@Nullable
private FlutterRenderer flutterRenderer;
@NonNull
private Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners;
private final SurfaceTextureListener surfaceTextureListener;
public XFlutterTextureView(@NonNull Context context) {
this(context, (AttributeSet)null);
}
public XFlutterTextureView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
this.isSurfaceAvailableForRendering = false;
this.isAttachedToFlutterRenderer = false;
this.onFirstFrameRenderedListeners = new HashSet();
this.surfaceTextureListener = new SurfaceTextureListener() {
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
Log.v("FlutterTextureView", "SurfaceTextureListener.onSurfaceTextureAvailable()");
XFlutterTextureView.this.isSurfaceAvailableForRendering = true;
if (XFlutterTextureView.this.isAttachedToFlutterRenderer) {
XFlutterTextureView.this.connectSurfaceToRenderer();
}
}
public void onSurfaceTextureSizeChanged(@NonNull SurfaceTexture surface, int width, int height) {
Log.v("FlutterTextureView", "SurfaceTextureListener.onSurfaceTextureSizeChanged()");
if (XFlutterTextureView.this.isAttachedToFlutterRenderer) {
XFlutterTextureView.this.changeSurfaceSize(width, height);
}
}
public void onSurfaceTextureUpdated(@NonNull SurfaceTexture surface) {
}
public boolean onSurfaceTextureDestroyed(@NonNull SurfaceTexture surface) {
Log.v("FlutterTextureView", "SurfaceTextureListener.onSurfaceTextureDestroyed()");
XFlutterTextureView.this.isSurfaceAvailableForRendering = false;
if (XFlutterTextureView.this.isAttachedToFlutterRenderer) {
XFlutterTextureView.this.disconnectSurfaceFromRenderer();
}
return true;
}
};
this.init();
}
private void init() {
this.setSurfaceTextureListener(this.surfaceTextureListener);
}
public void attachToRenderer(@NonNull FlutterRenderer flutterRenderer) {
Log.v("FlutterTextureView", "Attaching to FlutterRenderer.");
if (this.flutterRenderer != null) {
Log.v("FlutterTextureView", "Already connected to a FlutterRenderer. Detaching from old one and attaching to new one.");
this.flutterRenderer.detachFromRenderSurface();
}
this.flutterRenderer = flutterRenderer;
this.isAttachedToFlutterRenderer = true;
if (this.isSurfaceAvailableForRendering) {
Log.v("FlutterTextureView", "Surface is available for rendering. Connecting FlutterRenderer to Android surface.");
this.connectSurfaceToRenderer();
}
}
public void detachFromRenderer() {
if (this.flutterRenderer != null) {
if (this.getWindowToken() != null) {
Log.v("FlutterTextureView", "Disconnecting FlutterRenderer from Android surface.");
this.disconnectSurfaceFromRenderer();
}
this.flutterRenderer = null;
this.isAttachedToFlutterRenderer = false;
} else {
Log.w("FlutterTextureView", "detachFromRenderer() invoked when no FlutterRenderer was attached.");
}
}
private void connectSurfaceToRenderer() {
if (this.flutterRenderer != null && this.getSurfaceTexture() != null) {
Surface surface= new Surface(this.getSurfaceTexture());
this.flutterRenderer.surfaceCreated(surface);
surface.release();
} else {
throw new IllegalStateException("connectSurfaceToRenderer() should only be called when flutterRenderer and getSurfaceTexture() are non-null.");
}
}
private void changeSurfaceSize(int width, int height) {
if (this.flutterRenderer == null) {
throw new IllegalStateException("changeSurfaceSize() should only be called when flutterRenderer is non-null.");
} else {
Log.v("FlutterTextureView", "Notifying FlutterRenderer that Android surface size has changed to " + width + " x " + height);
this.flutterRenderer.surfaceChanged(width, height);
}
}
private void disconnectSurfaceFromRenderer() {
if (this.flutterRenderer == null) {
throw new IllegalStateException("disconnectSurfaceFromRenderer() should only be called when flutterRenderer is non-null.");
} else {
this.flutterRenderer.surfaceDestroyed();
}
}
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
this.onFirstFrameRenderedListeners.add(listener);
}
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
this.onFirstFrameRenderedListeners.remove(listener);
}
public void onFirstFrameRendered() {
Log.v("FlutterTextureView", "onFirstFrameRendered()");
Iterator var1 = this.onFirstFrameRenderedListeners.iterator();
while(var1.hasNext()) {
OnFirstFrameRenderedListener listener = (OnFirstFrameRenderedListener)var1.next();
listener.onFirstFrameRendered();
}
}
}
\ No newline at end of file
package com.idlefish.flutterboost; package com.idlefish.flutterboost;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.app.Activity; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration; import android.content.res.Configuration;
//import android.graphics.Insets;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.LocaleList; import android.os.LocaleList;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
import android.text.format.DateFormat; import android.text.format.DateFormat;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent; import android.view.KeyEvent;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View;
import android.view.WindowInsets; import android.view.WindowInsets;
import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeProvider; import android.view.accessibility.AccessibilityNodeProvider;
...@@ -22,24 +25,46 @@ import android.view.inputmethod.EditorInfo; ...@@ -22,24 +25,46 @@ import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import java.lang.reflect.Field;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Set;
import io.flutter.embedding.android.AndroidKeyProcessor; import io.flutter.Log;
import io.flutter.embedding.android.AndroidTouchProcessor; import io.flutter.embedding.android.*;
import io.flutter.embedding.android.FlutterSurfaceView;
import io.flutter.embedding.android.FlutterTextureView;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.engine.FlutterEngine; import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.FlutterRenderer; import io.flutter.embedding.engine.renderer.FlutterRenderer;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener; import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.editing.TextInputPlugin; import io.flutter.plugin.editing.TextInputPlugin;
import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.plugin.platform.PlatformViewsController;
import io.flutter.view.AccessibilityBridge; import io.flutter.view.AccessibilityBridge;
/**
* Displays a Flutter UI on an Android device.
* <p>
* A {@code FlutterView}'s UI is painted by a corresponding {@link FlutterEngine}.
* <p>
* A {@code FlutterView} can operate in 2 different {@link RenderMode}s:
* <ol>
* <li>{@link RenderMode#surface}, which paints a Flutter UI to a {@link android.view.SurfaceView}.
* This mode has the best performance, but a {@code FlutterView} in this mode cannot be positioned
* between 2 other Android {@code View}s in the z-index, nor can it be animated/transformed.
* Unless the special capabilities of a {@link android.graphics.SurfaceTexture} are required,
* developers should strongly prefer this render mode.</li>
* <li>{@link RenderMode#texture}, which paints a Flutter UI to a {@link android.graphics.SurfaceTexture}.
* This mode is not as performant as {@link RenderMode#surface}, but a {@code FlutterView} in this
* mode can be animated and transformed, as well as positioned in the z-index between 2+ other
* Android {@code Views}. Unless the special capabilities of a {@link android.graphics.SurfaceTexture}
* are required, developers should strongly prefer the {@link RenderMode#surface} render mode.</li>
* </ol>
* See <a>https://source.android.com/devices/graphics/arch-tv#surface_or_texture</a> for more
* information comparing {@link android.view.SurfaceView} and {@link android.view.TextureView}.
*/
public class XFlutterView extends FrameLayout { public class XFlutterView extends FrameLayout {
private static final String TAG = "XFlutterView"; private static final String TAG = "FlutterView";
// Behavior configuration of this FlutterView. // Behavior configuration of this FlutterView.
@NonNull @NonNull
...@@ -50,10 +75,14 @@ public class XFlutterView extends FrameLayout { ...@@ -50,10 +75,14 @@ public class XFlutterView extends FrameLayout {
// Internal view hierarchy references. // Internal view hierarchy references.
@Nullable @Nullable
private FlutterRenderer.RenderSurface renderSurface; private FlutterRenderer.RenderSurface renderSurface;
private final Set<OnFirstFrameRenderedListener> onFirstFrameRenderedListeners = new HashSet<>();
private boolean didRenderFirstFrame;
// Connections to a Flutter execution context. // Connections to a Flutter execution context.
@Nullable @Nullable
private FlutterEngine flutterEngine; private FlutterEngine flutterEngine;
@NonNull
private final Set<FlutterView.FlutterEngineAttachmentListener> flutterEngineAttachmentListeners = new HashSet<>();
// Components that process various types of Android View input and events, // Components that process various types of Android View input and events,
// possibly storing intermediate state, and communicating those events to Flutter. // possibly storing intermediate state, and communicating those events to Flutter.
...@@ -69,6 +98,8 @@ public class XFlutterView extends FrameLayout { ...@@ -69,6 +98,8 @@ public class XFlutterView extends FrameLayout {
@Nullable @Nullable
private AccessibilityBridge accessibilityBridge; private AccessibilityBridge accessibilityBridge;
private boolean hasAddFirstFrameRenderedListener=false;
// Directly implemented View behavior that communicates with Flutter. // Directly implemented View behavior that communicates with Flutter.
private final FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics(); private final FlutterRenderer.ViewportMetrics viewportMetrics = new FlutterRenderer.ViewportMetrics();
...@@ -79,17 +110,51 @@ public class XFlutterView extends FrameLayout { ...@@ -79,17 +110,51 @@ public class XFlutterView extends FrameLayout {
} }
}; };
private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
@Override
public void onFirstFrameRendered() {
didRenderFirstFrame = true;
for (OnFirstFrameRenderedListener listener : onFirstFrameRenderedListeners) {
listener.onFirstFrameRendered();
}
}
};
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes.
* <p>
* <ul>
* <li>{@link #renderMode} defaults to {@link RenderMode#surface}.</li>
* <li>{@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.</li>
* </ul>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context) { public XFlutterView(@NonNull Context context) {
this(context, null, null, null); this(context, null, null, null);
} }
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes,
* and allows selection of a {@link #renderMode}.
* <p>
* {@link #transparencyMode} defaults to {@link TransparencyMode#opaque}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode) { public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode) {
this(context, null, renderMode, null); this(context, null, renderMode, null);
} }
/**
* Constructs a {@code FlutterView} programmatically, without any XML attributes,
* assumes the use of {@link RenderMode#surface}, and allows selection of a {@link #transparencyMode}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/
public XFlutterView(@NonNull Context context, @NonNull FlutterView.TransparencyMode transparencyMode) { public XFlutterView(@NonNull Context context, @NonNull FlutterView.TransparencyMode transparencyMode) {
this(context, null, FlutterView.RenderMode.surface, transparencyMode); this(context, null, FlutterView.RenderMode.surface, transparencyMode);
} }
...@@ -97,6 +162,9 @@ public class XFlutterView extends FrameLayout { ...@@ -97,6 +162,9 @@ public class XFlutterView extends FrameLayout {
/** /**
* Constructs a {@code FlutterView} programmatically, without any XML attributes, and allows * Constructs a {@code FlutterView} programmatically, without any XML attributes, and allows
* a selection of {@link #renderMode} and {@link #transparencyMode}. * a selection of {@link #renderMode} and {@link #transparencyMode}.
* <p>
* {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/ */
public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode, @NonNull FlutterView.TransparencyMode transparencyMode) { public XFlutterView(@NonNull Context context, @NonNull FlutterView.RenderMode renderMode, @NonNull FlutterView.TransparencyMode transparencyMode) {
this(context, null, renderMode, transparencyMode); this(context, null, renderMode, transparencyMode);
...@@ -104,9 +172,11 @@ public class XFlutterView extends FrameLayout { ...@@ -104,9 +172,11 @@ public class XFlutterView extends FrameLayout {
/** /**
* Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner. * Constructs a {@code FlutterSurfaceView} in an XML-inflation-compliant manner.
* * <p>
* // TODO(mattcarroll): expose renderMode in XML when build system supports R.attr * {@code FlutterView} requires an {@code Activity} instead of a generic {@code Context}
* to be compatible with {@link PlatformViewsController}.
*/ */
// TODO(mattcarroll): expose renderMode in XML when build system supports R.attr
public XFlutterView(@NonNull Context context, @Nullable AttributeSet attrs) { public XFlutterView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, null, null); this(context, attrs, null, null);
} }
...@@ -121,18 +191,18 @@ public class XFlutterView extends FrameLayout { ...@@ -121,18 +191,18 @@ public class XFlutterView extends FrameLayout {
} }
private void init() { private void init() {
Log.d(TAG, "Initializing FlutterView"); Log.v(TAG, "Initializing FlutterView");
switch (renderMode) { switch (renderMode) {
case surface: case surface:
Log.d(TAG, "Internally creating a FlutterSurfaceView."); Log.v(TAG, "Internally using a FlutterSurfaceView.");
FlutterSurfaceView flutterSurfaceView = new FlutterSurfaceView(getContext(), transparencyMode == FlutterView.TransparencyMode.transparent); FlutterSurfaceView flutterSurfaceView = new FlutterSurfaceView(getContext(), transparencyMode == FlutterView.TransparencyMode.transparent);
renderSurface = flutterSurfaceView; renderSurface = flutterSurfaceView;
addView(flutterSurfaceView); addView(flutterSurfaceView);
break; break;
case texture: case texture:
Log.d(TAG, "Internally creating a FlutterTextureView."); Log.v(TAG, "Internally using a FlutterTextureView.");
FlutterTextureView flutterTextureView = new FlutterTextureView(getContext()); XFlutterTextureView flutterTextureView = new XFlutterTextureView(getContext());
renderSurface = flutterTextureView; renderSurface = flutterTextureView;
addView(flutterTextureView); addView(flutterTextureView);
break; break;
...@@ -143,12 +213,32 @@ public class XFlutterView extends FrameLayout { ...@@ -143,12 +213,32 @@ public class XFlutterView extends FrameLayout {
setFocusableInTouchMode(true); setFocusableInTouchMode(true);
} }
/**
* Returns true if an attached {@link FlutterEngine} has rendered at least 1 frame to this
* {@code FlutterView}.
* <p>
* Returns false if no {@link FlutterEngine} is attached.
* <p>
* This flag is specific to a given {@link FlutterEngine}. The following hypothetical timeline
* demonstrates how this flag changes over time.
* <ol>
* <li>{@code flutterEngineA} is attached to this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineA} renders its first frame to this {@code FlutterView}: returns true</li>
* <li>{@code flutterEngineA} is detached from this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineB} is attached to this {@code FlutterView}: returns false</li>
* <li>{@code flutterEngineB} renders its first frame to this {@code FlutterView}: returns true</li>
* </ol>
*/
public boolean hasRenderedFirstFrame() {
return didRenderFirstFrame;
}
/** /**
* Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's * Adds the given {@code listener} to this {@code FlutterView}, to be notified upon Flutter's
* first rendered frame. * first rendered frame.
*/ */
public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { public void addOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.addOnFirstFrameRenderedListener(listener); onFirstFrameRenderedListeners.add(listener);
} }
/** /**
...@@ -156,7 +246,7 @@ public class XFlutterView extends FrameLayout { ...@@ -156,7 +246,7 @@ public class XFlutterView extends FrameLayout {
* {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}. * {@link #addOnFirstFrameRenderedListener(OnFirstFrameRenderedListener)}.
*/ */
public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) { public void removeOnFirstFrameRenderedListener(@NonNull OnFirstFrameRenderedListener listener) {
renderSurface.removeOnFirstFrameRenderedListener(listener); onFirstFrameRenderedListeners.remove(listener);
} }
//------- Start: Process View configuration that Flutter cares about. ------ //------- Start: Process View configuration that Flutter cares about. ------
...@@ -168,9 +258,11 @@ public class XFlutterView extends FrameLayout { ...@@ -168,9 +258,11 @@ public class XFlutterView extends FrameLayout {
* change, device language change, device text scale factor change, etc. * change, device language change, device text scale factor change, etc.
*/ */
@Override @Override
protected void onConfigurationChanged(Configuration newConfig) { protected void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig); super.onConfigurationChanged(newConfig);
Log.v(TAG, "Configuration changed. Sending locales and user settings to Flutter.");
try { try {
sendLocalesToFlutter(newConfig); sendLocalesToFlutter(newConfig);
sendUserSettingsToFlutter(); sendUserSettingsToFlutter();
}catch (Throwable e){ }catch (Throwable e){
...@@ -194,6 +286,9 @@ public class XFlutterView extends FrameLayout { ...@@ -194,6 +286,9 @@ public class XFlutterView extends FrameLayout {
@Override @Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight); super.onSizeChanged(width, height, oldWidth, oldHeight);
Log.v(TAG, "Size changed. Sending Flutter new viewport metrics. FlutterView was "
+ oldWidth + " x " + oldHeight
+ ", it is now " + width + " x " + height);
viewportMetrics.width = width; viewportMetrics.width = width;
viewportMetrics.height = height; viewportMetrics.height = height;
sendViewportMetricsToFlutter(); sendViewportMetricsToFlutter();
...@@ -212,7 +307,12 @@ public class XFlutterView extends FrameLayout { ...@@ -212,7 +307,12 @@ public class XFlutterView extends FrameLayout {
@Override @Override
@TargetApi(20) @TargetApi(20)
@RequiresApi(20) @RequiresApi(20)
public final WindowInsets onApplyWindowInsets(WindowInsets insets) { // The annotations to suppress "InlinedApi" and "NewApi" lints prevent lint warnings
// caused by usage of Android Q APIs. These calls are safe because they are
// guarded.
@SuppressLint({"InlinedApi", "NewApi"})
@NonNull
public final WindowInsets onApplyWindowInsets(@NonNull WindowInsets insets) {
WindowInsets newInsets = super.onApplyWindowInsets(insets); WindowInsets newInsets = super.onApplyWindowInsets(insets);
// Status bar (top) and left/right system insets should partially obscure the content (padding). // Status bar (top) and left/right system insets should partially obscure the content (padding).
...@@ -226,6 +326,23 @@ public class XFlutterView extends FrameLayout { ...@@ -226,6 +326,23 @@ public class XFlutterView extends FrameLayout {
viewportMetrics.viewInsetRight = 0; viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = insets.getSystemWindowInsetBottom(); viewportMetrics.viewInsetBottom = insets.getSystemWindowInsetBottom();
viewportMetrics.viewInsetLeft = 0; viewportMetrics.viewInsetLeft = 0;
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Insets systemGestureInsets = insets.getSystemGestureInsets();
// viewportMetrics.systemGestureInsetTop = systemGestureInsets.top;
// viewportMetrics.systemGestureInsetRight = systemGestureInsets.right;
// viewportMetrics.systemGestureInsetBottom = systemGestureInsets.bottom;
// viewportMetrics.systemGestureInsetLeft = systemGestureInsets.left;
// }
Log.v(TAG, "Updating window insets (onApplyWindowInsets()):\n"
+ "Status bar insets: Top: " + viewportMetrics.paddingTop
+ ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
+ "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
+ ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight
+ "System Gesture Insets - Left: " + viewportMetrics.systemGestureInsetLeft + ", Top: " + viewportMetrics.systemGestureInsetTop
+ ", Right: " + viewportMetrics.systemGestureInsetRight + ", Bottom: " + viewportMetrics.viewInsetBottom);
sendViewportMetricsToFlutter(); sendViewportMetricsToFlutter();
return newInsets; return newInsets;
...@@ -240,7 +357,7 @@ public class XFlutterView extends FrameLayout { ...@@ -240,7 +357,7 @@ public class XFlutterView extends FrameLayout {
*/ */
@Override @Override
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
protected boolean fitSystemWindows(Rect insets) { protected boolean fitSystemWindows(@NonNull Rect insets) {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) {
// Status bar, left/right system insets partially obscure content (padding). // Status bar, left/right system insets partially obscure content (padding).
viewportMetrics.paddingTop = insets.top; viewportMetrics.paddingTop = insets.top;
...@@ -253,6 +370,13 @@ public class XFlutterView extends FrameLayout { ...@@ -253,6 +370,13 @@ public class XFlutterView extends FrameLayout {
viewportMetrics.viewInsetRight = 0; viewportMetrics.viewInsetRight = 0;
viewportMetrics.viewInsetBottom = insets.bottom; viewportMetrics.viewInsetBottom = insets.bottom;
viewportMetrics.viewInsetLeft = 0; viewportMetrics.viewInsetLeft = 0;
Log.v(TAG, "Updating window insets (fitSystemWindows()):\n"
+ "Status bar insets: Top: " + viewportMetrics.paddingTop
+ ", Left: " + viewportMetrics.paddingLeft + ", Right: " + viewportMetrics.paddingRight + "\n"
+ "Keyboard insets: Bottom: " + viewportMetrics.viewInsetBottom
+ ", Left: " + viewportMetrics.viewInsetLeft + ", Right: " + viewportMetrics.viewInsetRight);
sendViewportMetricsToFlutter(); sendViewportMetricsToFlutter();
return true; return true;
} else { } else {
...@@ -276,7 +400,8 @@ public class XFlutterView extends FrameLayout { ...@@ -276,7 +400,8 @@ public class XFlutterView extends FrameLayout {
* rather than spread that logic throughout this {@code FlutterView}. * rather than spread that logic throughout this {@code FlutterView}.
*/ */
@Override @Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) { @Nullable
public InputConnection onCreateInputConnection(@NonNull EditorInfo outAttrs) {
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
return super.onCreateInputConnection(outAttrs); return super.onCreateInputConnection(outAttrs);
} }
...@@ -284,6 +409,21 @@ public class XFlutterView extends FrameLayout { ...@@ -284,6 +409,21 @@ public class XFlutterView extends FrameLayout {
return textInputPlugin.createInputConnection(this, outAttrs); return textInputPlugin.createInputConnection(this, outAttrs);
} }
/**
* Allows a {@code View} that is not currently the input connection target to invoke commands on
* the {@link android.view.inputmethod.InputMethodManager}, which is otherwise disallowed.
* <p>
* Returns true to allow non-input-connection-targets to invoke methods on
* {@code InputMethodManager}, or false to exclusively allow the input connection target to invoke
* such methods.
*/
@Override
public boolean checkInputConnectionProxy(View view) {
return flutterEngine != null
? flutterEngine.getPlatformViewsController().checkInputConnectionProxy(view)
: super.checkInputConnectionProxy(view);
}
/** /**
* Invoked when key is released. * Invoked when key is released.
* *
...@@ -292,13 +432,13 @@ public class XFlutterView extends FrameLayout { ...@@ -292,13 +432,13 @@ public class XFlutterView extends FrameLayout {
* software keyboard is used, though a software keyboard may choose to invoke * software keyboard is used, though a software keyboard may choose to invoke
* this method in some situations. * this method in some situations.
* *
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} * {@link KeyEvent}s are sent from Android to Flutter. {@link }
* may do some additional work with the given {@link KeyEvent}, e.g., combine this * may do some additional work with the given {@link KeyEvent}, e.g., combine this
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
* character. * character.
*/ */
@Override @Override
public boolean onKeyUp(int keyCode, KeyEvent event) { public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
return super.onKeyUp(keyCode, event); return super.onKeyUp(keyCode, event);
} }
...@@ -315,13 +455,13 @@ public class XFlutterView extends FrameLayout { ...@@ -315,13 +455,13 @@ public class XFlutterView extends FrameLayout {
* software keyboard is used, though a software keyboard may choose to invoke * software keyboard is used, though a software keyboard may choose to invoke
* this method in some situations. * this method in some situations.
* *
* {@link KeyEvent}s are sent from Android to Flutter. {@link AndroidKeyProcessor} * {@link KeyEvent}s are sent from Android to Flutter. {@link }
* may do some additional work with the given {@link KeyEvent}, e.g., combine this * may do some additional work with the given {@link KeyEvent}, e.g., combine this
* {@code keyCode} with the previous {@code keyCode} to generate a unicode combined * {@code keyCode} with the previous {@code keyCode} to generate a unicode combined
* character. * character.
*/ */
@Override @Override
public boolean onKeyDown(int keyCode, KeyEvent event) { public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
return super.onKeyDown(keyCode, event); return super.onKeyDown(keyCode, event);
} }
...@@ -337,7 +477,7 @@ public class XFlutterView extends FrameLayout { ...@@ -337,7 +477,7 @@ public class XFlutterView extends FrameLayout {
* method forwards all {@link MotionEvent} data from Android to Flutter. * method forwards all {@link MotionEvent} data from Android to Flutter.
*/ */
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
return super.onTouchEvent(event); return super.onTouchEvent(event);
} }
...@@ -362,7 +502,7 @@ public class XFlutterView extends FrameLayout { ...@@ -362,7 +502,7 @@ public class XFlutterView extends FrameLayout {
* method forwards all {@link MotionEvent} data from Android to Flutter. * method forwards all {@link MotionEvent} data from Android to Flutter.
*/ */
@Override @Override
public boolean onGenericMotionEvent(MotionEvent event) { public boolean onGenericMotionEvent(@NonNull MotionEvent event) {
boolean handled = isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event); boolean handled = isAttachedToFlutterEngine() && androidTouchProcessor.onGenericMotionEvent(event);
return handled ? true : super.onGenericMotionEvent(event); return handled ? true : super.onGenericMotionEvent(event);
} }
...@@ -379,7 +519,7 @@ public class XFlutterView extends FrameLayout { ...@@ -379,7 +519,7 @@ public class XFlutterView extends FrameLayout {
* processed here for accessibility purposes. * processed here for accessibility purposes.
*/ */
@Override @Override
public boolean onHoverEvent(MotionEvent event) { public boolean onHoverEvent(@NonNull MotionEvent event) {
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
return super.onHoverEvent(event); return super.onHoverEvent(event);
} }
...@@ -395,6 +535,7 @@ public class XFlutterView extends FrameLayout { ...@@ -395,6 +535,7 @@ public class XFlutterView extends FrameLayout {
//-------- Start: Accessibility ------- //-------- Start: Accessibility -------
@Override @Override
@Nullable
public AccessibilityNodeProvider getAccessibilityNodeProvider() { public AccessibilityNodeProvider getAccessibilityNodeProvider() {
if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) { if (accessibilityBridge != null && accessibilityBridge.isAccessibilityEnabled()) {
return accessibilityBridge; return accessibilityBridge;
...@@ -406,12 +547,8 @@ public class XFlutterView extends FrameLayout { ...@@ -406,12 +547,8 @@ public class XFlutterView extends FrameLayout {
} }
} }
// TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments. // TODO(mattcarroll): Confer with Ian as to why we need this method. Delete if possible, otherwise add comments.
private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) { private void resetWillNotDraw(boolean isAccessibilityEnabled, boolean isTouchExplorationEnabled) {
if(flutterEngine==null) return;
if(flutterEngine.getRenderer()==null) return;
if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) { if (!flutterEngine.getRenderer().isSoftwareRenderingEnabled()) {
setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled)); setWillNotDraw(!(isAccessibilityEnabled || isTouchExplorationEnabled));
} else { } else {
...@@ -431,9 +568,11 @@ public class XFlutterView extends FrameLayout { ...@@ -431,9 +568,11 @@ public class XFlutterView extends FrameLayout {
* See {@link #detachFromFlutterEngine()} for information on how to detach from a * See {@link #detachFromFlutterEngine()} for information on how to detach from a
* {@link FlutterEngine}. * {@link FlutterEngine}.
*/ */
public void attachToFlutterEngine(@NonNull FlutterEngine flutterEngine) {
Log.d(TAG, "attachToFlutterEngine()"); public void attachToFlutterEngine(
@NonNull FlutterEngine flutterEngine
) {
Log.d(TAG, "Attaching to a FlutterEngine: " + flutterEngine);
if (isAttachedToFlutterEngine()) { if (isAttachedToFlutterEngine()) {
if (flutterEngine == this.flutterEngine) { if (flutterEngine == this.flutterEngine) {
// We are already attached to this FlutterEngine // We are already attached to this FlutterEngine
...@@ -442,47 +581,49 @@ public class XFlutterView extends FrameLayout { ...@@ -442,47 +581,49 @@ public class XFlutterView extends FrameLayout {
} }
// Detach from a previous FlutterEngine so we can attach to this new one. // Detach from a previous FlutterEngine so we can attach to this new one.
Log.d(TAG, "Currently attached to a different engine. Detaching."); Log.d(TAG, "Currently attached to a different engine. Detaching and then attaching"
+ " to new engine.");
detachFromFlutterEngine(); detachFromFlutterEngine();
} }
this.flutterEngine = flutterEngine; this.flutterEngine = flutterEngine;
// initialize PlatformViewsController
this.flutterEngine.getPluginRegistry().getPlatformViewsController().attach(getContext(),flutterEngine.getRenderer(),flutterEngine.getDartExecutor());
// Instruct our FlutterRenderer that we are now its designated RenderSurface. // Instruct our FlutterRenderer that we are now its designated RenderSurface.
this.flutterEngine.getRenderer().attachToRenderSurface(renderSurface); FlutterRenderer flutterRenderer = this.flutterEngine.getRenderer();
didRenderFirstFrame = flutterRenderer.hasRenderedFirstFrame();
if(!hasAddFirstFrameRenderedListener){
flutterRenderer.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
hasAddFirstFrameRenderedListener=true;
}
flutterRenderer.attachToRenderSurface(renderSurface);
// Initialize various components that know how to process Android View I/O // Initialize various components that know how to process Android View I/O
// in a way that Flutter understands. // in a way that Flutter understands.
if(textInputPlugin==null){ if(textInputPlugin==null){
textInputPlugin = new XTextInputPlugin( textInputPlugin = new XTextInputPlugin(
this, this,
flutterEngine.getTextInputChannel() flutterEngine.getTextInputChannel(),
this.flutterEngine.getPlatformViewsController()
); );
} }
textInputPlugin.setTextInputMethodHandler(); textInputPlugin.setTextInputMethodHandler();
textInputPlugin.getInputMethodManager().restartInput(this); textInputPlugin.getInputMethodManager().restartInput(this);
androidKeyProcessor = new XAndroidKeyProcessor(
this.androidKeyProcessor = new XAndroidKeyProcessor(
this.flutterEngine.getKeyEventChannel(), this.flutterEngine.getKeyEventChannel(),
textInputPlugin textInputPlugin
); );
this.androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
this.accessibilityBridge = new AccessibilityBridge(
androidTouchProcessor = new AndroidTouchProcessor(this.flutterEngine.getRenderer());
if(accessibilityBridge==null){
accessibilityBridge = new AccessibilityBridge(
this, this,
flutterEngine.getAccessibilityChannel(), flutterEngine.getAccessibilityChannel(),
(AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE), (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE),
getContext().getContentResolver(), getContext().getContentResolver(),
// TODO(mattcaroll): plumb the platform views controller to the accessibility bridge. this.flutterEngine.getPlatformViewsController()
// https://github.com/flutter/flutter/issues/29618
null
); );
accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener); accessibilityBridge.setOnAccessibilityChangeListener(onAccessibilityChangeListener);
resetWillNotDraw( resetWillNotDraw(
...@@ -490,28 +631,31 @@ public class XFlutterView extends FrameLayout { ...@@ -490,28 +631,31 @@ public class XFlutterView extends FrameLayout {
accessibilityBridge.isTouchExplorationEnabled() accessibilityBridge.isTouchExplorationEnabled()
); );
} // Connect AccessibilityBridge to the PlatformViewsController within the FlutterEngine.
// This allows platform Views to hook into Flutter's overall accessibility system.
this.flutterEngine.getPlatformViewsController().attachAccessibilityBridge(accessibilityBridge);
// Inform the Android framework that it should retrieve a new InputConnection // Inform the Android framework that it should retrieve a new InputConnection
// now that an engine is attached. // now that an engine is attached.
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
textInputPlugin.getInputMethodManager().restartInput(this);
// Push View and Context related information from Android to Flutter. // Push View and Context related information from Android to Flutter.
sendUserSettingsToFlutter(); sendUserSettingsToFlutter();
sendLocalesToFlutter(getResources().getConfiguration()); sendLocalesToFlutter(getResources().getConfiguration());
sendViewportMetricsToFlutter(); sendViewportMetricsToFlutter();
}
public void release(){ // Notify engine attachment listeners of the attachment.
for (FlutterView.FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
if(accessibilityBridge!=null){ listener.onFlutterEngineAttachedToFlutterView(flutterEngine);
accessibilityBridge.release();
} }
textInputPlugin.release();
// If the first frame has already been rendered, notify all first frame listeners.
// Do this after all other initialization so that listeners don't inadvertently interact
// with a FlutterView that is only partially attached to a FlutterEngine.
// if (didRenderFirstFrame) {
// onFirstFrameRenderedListener.onFirstFrameRendered();
// }
} }
/** /**
...@@ -525,45 +669,83 @@ public class XFlutterView extends FrameLayout { ...@@ -525,45 +669,83 @@ public class XFlutterView extends FrameLayout {
* {@link FlutterEngine}. * {@link FlutterEngine}.
*/ */
public void detachFromFlutterEngine() { public void detachFromFlutterEngine() {
Log.d(TAG, "detachFromFlutterEngine()"); Log.d(TAG, "Detaching from a FlutterEngine: " + flutterEngine);
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
Log.d(TAG, "Not attached to an engine. Doing nothing."); Log.d(TAG, "Not attached to an engine. Doing nothing.");
return; return;
} }
Log.d(TAG, "Detaching from Flutter Engine");
// detach platformviews in page in case memory leak // Notify engine attachment listeners of the detachment.
flutterEngine.getPluginRegistry().getPlatformViewsController().detach(); for (FlutterView.FlutterEngineAttachmentListener listener : flutterEngineAttachmentListeners) {
listener.onFlutterEngineDetachedFromFlutterView();
}
// Disconnect the FlutterEngine's PlatformViewsController from the AccessibilityBridge.
flutterEngine.getPlatformViewsController().detachAccessibiltyBridge();
// Disconnect and clean up the AccessibilityBridge.
accessibilityBridge.release();
accessibilityBridge = null;
// Inform the Android framework that it should retrieve a new InputConnection // Inform the Android framework that it should retrieve a new InputConnection
// now that the engine is detached. The new InputConnection will be null, which // now that the engine is detached. The new InputConnection will be null, which
// signifies that this View does not process input (until a new engine is attached). // signifies that this View does not process input (until a new engine is attached).
// TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin // TODO(mattcarroll): once this is proven to work, move this line ot TextInputPlugin
// textInputPlugin.getInputMethodManager().restartInput(this);
// Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface. // Instruct our FlutterRenderer that we are no longer interested in being its RenderSurface.
// this.textInputPlugin.getInputMethodManager().restartInput(this); FlutterRenderer flutterRenderer = flutterEngine.getRenderer();
flutterEngine.getRenderer().detachFromRenderSurface(); // didRenderFirstFrame = false;
flutterRenderer.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
flutterRenderer.detachFromRenderSurface();
flutterEngine = null; flutterEngine = null;
// TODO(mattcarroll): clear the surface when JNI doesn't blow up
// if (isSurfaceAvailableForRendering) {
// Canvas canvas = surfaceHolder.lockCanvas();
// canvas.drawColor(Color.RED);
// surfaceHolder.unlockCanvasAndPost(canvas);
// }
} }
public void release(){
if(textInputPlugin!=null){
textInputPlugin.release();
}
}
private boolean isAttachedToFlutterEngine() { /**
* Returns true if this {@code FlutterView} is currently attached to a {@link FlutterEngine}.
*/
@VisibleForTesting
public boolean isAttachedToFlutterEngine() {
return flutterEngine != null; return flutterEngine != null;
} }
/**
* Returns the {@link FlutterEngine} to which this {@code FlutterView} is currently attached,
* or null if this {@code FlutterView} is not currently attached to a {@link FlutterEngine}.
*/
@VisibleForTesting
@Nullable
public FlutterEngine getAttachedFlutterEngine() {
return flutterEngine;
}
/**
* attached to/detaches from a {@link FlutterEngine}.
*/
@VisibleForTesting
public void addFlutterEngineAttachmentListener(@NonNull FlutterView.FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.add(listener);
}
/**
*/
@VisibleForTesting
public void removeFlutterEngineAttachmentListener(@NonNull FlutterView.FlutterEngineAttachmentListener listener) {
flutterEngineAttachmentListeners.remove(listener);
}
/** /**
* Send the current {@link Locale} configuration to Flutter. * Send the current {@link Locale} configuration to Flutter.
* *
* FlutterEngine must be non-null when this method is invoked. * FlutterEngine must be non-null when this method is invoked.
*/ */
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
private void sendLocalesToFlutter(Configuration config) { private void sendLocalesToFlutter(@NonNull Configuration config) {
List<Locale> locales = new ArrayList<>(); List<Locale> locales = new ArrayList<>();
if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
LocaleList localeList = config.getLocales(); LocaleList localeList = config.getLocales();
...@@ -595,14 +777,13 @@ public class XFlutterView extends FrameLayout { ...@@ -595,14 +777,13 @@ public class XFlutterView extends FrameLayout {
.setUse24HourFormat(DateFormat.is24HourFormat(getContext())) .setUse24HourFormat(DateFormat.is24HourFormat(getContext()))
.send(); .send();
} }
} }
// TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI // TODO(mattcarroll): consider introducing a system channel for this communication instead of JNI
private void sendViewportMetricsToFlutter() { private void sendViewportMetricsToFlutter() {
Log.d(TAG, "sendViewportMetricsToFlutter()");
if (!isAttachedToFlutterEngine()) { if (!isAttachedToFlutterEngine()) {
Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this FlutterView was not attached to a FlutterEngine."); Log.w(TAG, "Tried to send viewport metrics from Android to Flutter but this "
+ "FlutterView was not attached to a FlutterEngine.");
return; return;
} }
......
...@@ -12,6 +12,23 @@ import android.view.inputmethod.InputMethodManager; ...@@ -12,6 +12,23 @@ import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.plugin.common.ErrorLogResult; import io.flutter.plugin.common.ErrorLogResult;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import android.content.Context;
import android.text.DynamicLayout;
import android.text.Editable;
import android.text.Layout;
import android.text.Layout.Directions;
import android.text.Selection;
import android.text.TextPaint;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.Log;
import io.flutter.plugin.common.ErrorLogResult;
import io.flutter.plugin.common.MethodChannel;
class XInputConnectionAdaptor extends BaseInputConnection { class XInputConnectionAdaptor extends BaseInputConnection {
private final View mFlutterView; private final View mFlutterView;
...@@ -20,10 +37,9 @@ class XInputConnectionAdaptor extends BaseInputConnection { ...@@ -20,10 +37,9 @@ class XInputConnectionAdaptor extends BaseInputConnection {
private final Editable mEditable; private final Editable mEditable;
private int mBatchCount; private int mBatchCount;
private InputMethodManager mImm; private InputMethodManager mImm;
private final Layout mLayout;
private static final MethodChannel.Result logger = @SuppressWarnings("deprecation")
new ErrorLogResult("FlutterTextInput");
public XInputConnectionAdaptor( public XInputConnectionAdaptor(
View view, View view,
int client, int client,
...@@ -36,6 +52,9 @@ class XInputConnectionAdaptor extends BaseInputConnection { ...@@ -36,6 +52,9 @@ class XInputConnectionAdaptor extends BaseInputConnection {
this.textInputChannel = textInputChannel; this.textInputChannel = textInputChannel;
mEditable = editable; mEditable = editable;
mBatchCount = 0; mBatchCount = 0;
// We create a dummy Layout with max width so that the selection
// shifting acts as if all text were in one line.
mLayout = new DynamicLayout(mEditable, new TextPaint(), Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, 1.0f, 0.0f, false);
mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE); mImm = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
} }
...@@ -126,12 +145,25 @@ class XInputConnectionAdaptor extends BaseInputConnection { ...@@ -126,12 +145,25 @@ class XInputConnectionAdaptor extends BaseInputConnection {
return result; return result;
} }
// Sanitizes the index to ensure the index is within the range of the
// contents of editable.
private static int clampIndexToEditable(int index, Editable editable) {
int clamped = Math.max(0, Math.min(editable.length(), index));
if (clamped != index) {
Log.d("flutter", "Text selection index was clamped ("
+ index + "->" + clamped
+ ") to remain in bounds. This may not be your fault, as some keyboards may select outside of bounds."
);
}
return clamped;
}
@Override @Override
public boolean sendKeyEvent(KeyEvent event) { public boolean sendKeyEvent(KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) { if (event.getAction() == KeyEvent.ACTION_DOWN) {
if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) { if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
int selStart = Selection.getSelectionStart(mEditable); int selStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
int selEnd = Selection.getSelectionEnd(mEditable); int selEnd = clampIndexToEditable(Selection.getSelectionEnd(mEditable), mEditable);
if (selEnd > selStart) { if (selEnd > selStart) {
// Delete the selection. // Delete the selection.
Selection.setSelection(mEditable, selStart); Selection.setSelection(mEditable, selStart);
...@@ -139,10 +171,33 @@ class XInputConnectionAdaptor extends BaseInputConnection { ...@@ -139,10 +171,33 @@ class XInputConnectionAdaptor extends BaseInputConnection {
updateEditingState(); updateEditingState();
return true; return true;
} else if (selStart > 0) { } else if (selStart > 0) {
// Delete to the left of the cursor. // Delete to the left/right of the cursor depending on direction of text.
int newSel = Math.max(selStart - 1, 0); // TODO(garyq): Explore how to obtain per-character direction. The
Selection.setSelection(mEditable, newSel); // isRTLCharAt() call below is returning blanket direction assumption
mEditable.delete(newSel, selStart); // based on the first character in the line.
boolean isRtl = mLayout.isRtlCharAt(mLayout.getLineForOffset(selStart));
try {
if (isRtl) {
Selection.extendRight(mEditable, mLayout);
} else {
Selection.extendLeft(mEditable, mLayout);
}
} catch (IndexOutOfBoundsException e) {
// On some Chinese devices (primarily Huawei, some Xiaomi),
// on initial app startup before focus is lost, the
// Selection.extendLeft and extendRight calls always extend
// from the index of the initial contents of mEditable. This
// try-catch will prevent crashing on Huawei devices by falling
// back to a simple way of deletion, although this a hack and
// will not handle emojis.
Selection.setSelection(mEditable, selStart, selStart - 1);
}
int newStart = clampIndexToEditable(Selection.getSelectionStart(mEditable), mEditable);
int newEnd = clampIndexToEditable(Selection.getSelectionEnd(mEditable), mEditable);
Selection.setSelection(mEditable, Math.min(newStart, newEnd));
// Min/Max the values since RTL selections will start at a higher
// index than they end at.
mEditable.delete(Math.min(newStart, newEnd), Math.max(newStart, newEnd));
updateEditingState(); updateEditingState();
return true; return true;
} }
......
package com.idlefish.flutterboost;
import android.app.Activity;
import android.app.ActivityManager.TaskDescription;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.HapticFeedbackConstants;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.Window;
import java.util.List;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import io.flutter.plugin.common.ActivityLifecycleListener;
public class XPlatformPlugin implements ActivityLifecycleListener {
public static final int DEFAULT_SYSTEM_UI = View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
private Activity activity;
private final PlatformChannel platformChannel;
private PlatformChannel.SystemChromeStyle currentTheme;
private int mEnabledOverlays;
private final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler = new PlatformChannel.PlatformMessageHandler() {
@Override
public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) {
XPlatformPlugin.this.playSystemSound(soundType);
}
@Override
public void vibrateHapticFeedback(@NonNull PlatformChannel.HapticFeedbackType feedbackType) {
XPlatformPlugin.this.vibrateHapticFeedback(feedbackType);
}
@Override
public void setPreferredOrientations(int androidOrientation) {
setSystemChromePreferredOrientations(androidOrientation);
}
@Override
public void setApplicationSwitcherDescription(@NonNull PlatformChannel.AppSwitcherDescription description) {
setSystemChromeApplicationSwitcherDescription(description);
}
@Override
public void showSystemOverlays(@NonNull List<PlatformChannel.SystemUiOverlay> overlays) {
setSystemChromeEnabledSystemUIOverlays(overlays);
}
@Override
public void restoreSystemUiOverlays() {
restoreSystemChromeSystemUIOverlays();
}
@Override
public void setSystemUiOverlayStyle(@NonNull PlatformChannel.SystemChromeStyle systemUiOverlayStyle) {
setSystemChromeSystemUIOverlayStyle(systemUiOverlayStyle);
}
@Override
public void popSystemNavigator() {
XPlatformPlugin.this.popSystemNavigator();
}
@Override
public CharSequence getClipboardData(@Nullable PlatformChannel.ClipboardContentFormat format) {
return XPlatformPlugin.this.getClipboardData(format);
}
@Override
public void setClipboardData(@NonNull String text) {
XPlatformPlugin.this.setClipboardData(text);
}
};
public XPlatformPlugin(Activity activity, PlatformChannel platformChannel) {
this.activity = activity;
this.platformChannel = platformChannel;
this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler);
mEnabledOverlays = DEFAULT_SYSTEM_UI;
}
private void playSystemSound(PlatformChannel.SoundType soundType) {
if (soundType == PlatformChannel.SoundType.CLICK) {
View view = activity.getWindow().getDecorView();
view.playSoundEffect(SoundEffectConstants.CLICK);
}
}
private void vibrateHapticFeedback(PlatformChannel.HapticFeedbackType feedbackType) {
View view = activity.getWindow().getDecorView();
switch (feedbackType) {
case STANDARD:
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
break;
case LIGHT_IMPACT:
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
break;
case MEDIUM_IMPACT:
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
break;
case HEAVY_IMPACT:
// HapticFeedbackConstants.CONTEXT_CLICK from API level 23.
view.performHapticFeedback(6);
break;
case SELECTION_CLICK:
view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
break;
}
}
private void setSystemChromePreferredOrientations(int androidOrientation) {
activity.setRequestedOrientation(androidOrientation);
}
private void setSystemChromeApplicationSwitcherDescription(PlatformChannel.AppSwitcherDescription description) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
// Linter refuses to believe we're only executing this code in API 28 unless we use distinct if blocks and
// hardcode the API 28 constant.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P && Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
activity.setTaskDescription(new TaskDescription(description.label));
}
if (Build.VERSION.SDK_INT >= 28) {
TaskDescription taskDescription = new TaskDescription(description.label, 0, description.color);
activity.setTaskDescription(taskDescription);
}
}
private void setSystemChromeEnabledSystemUIOverlays(List<PlatformChannel.SystemUiOverlay> overlaysToShow) {
// Start by assuming we want to hide all system overlays (like an immersive game).
int enabledOverlays = DEFAULT_SYSTEM_UI
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
if (overlaysToShow.size() == 0) {
enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
// Re-add any desired system overlays.
for (int i = 0; i < overlaysToShow.size(); ++i) {
PlatformChannel.SystemUiOverlay overlayToShow = overlaysToShow.get(i);
switch (overlayToShow) {
case TOP_OVERLAYS:
enabledOverlays &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
break;
case BOTTOM_OVERLAYS:
enabledOverlays &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
enabledOverlays &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
break;
}
}
mEnabledOverlays = enabledOverlays;
updateSystemUiOverlays();
}
private void updateSystemUiOverlays(){
activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays);
if (currentTheme != null) {
setSystemChromeSystemUIOverlayStyle(currentTheme);
}
}
private void restoreSystemChromeSystemUIOverlays() {
updateSystemUiOverlays();
}
private void setSystemChromeSystemUIOverlayStyle(PlatformChannel.SystemChromeStyle systemChromeStyle) {
Window window = activity.getWindow();
View view = window.getDecorView();
int flags = view.getSystemUiVisibility();
// You can change the navigation bar color (including translucent colors)
// in Android, but you can't change the color of the navigation buttons until Android O.
// LIGHT vs DARK effectively isn't supported until then.
// Build.VERSION_CODES.O
if (Build.VERSION.SDK_INT >= 26) {
if (systemChromeStyle.systemNavigationBarIconBrightness != null) {
switch (systemChromeStyle.systemNavigationBarIconBrightness) {
case DARK:
//View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
flags |= 0x10;
break;
case LIGHT:
flags &= ~0x10;
break;
}
}
if (systemChromeStyle.systemNavigationBarColor != null) {
window.setNavigationBarColor(systemChromeStyle.systemNavigationBarColor);
}
}
// Build.VERSION_CODES.M
if (Build.VERSION.SDK_INT >= 23) {
if (systemChromeStyle.statusBarIconBrightness != null) {
switch (systemChromeStyle.statusBarIconBrightness) {
case DARK:
// View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
flags |= 0x2000;
break;
case LIGHT:
flags &= ~0x2000;
break;
}
}
if (systemChromeStyle.statusBarColor != null) {
window.setStatusBarColor(systemChromeStyle.statusBarColor);
}
}
if (systemChromeStyle.systemNavigationBarDividerColor != null) {
// Not availible until Android P.
// window.setNavigationBarDividerColor(systemNavigationBarDividerColor);
}
view.setSystemUiVisibility(flags);
currentTheme = systemChromeStyle;
}
private void popSystemNavigator() {
activity.finish();
}
private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = clipboard.getPrimaryClip();
if (clip == null)
return null;
if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) {
return clip.getItemAt(0).coerceToText(activity);
}
return null;
}
private void setClipboardData(String text) {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text label?", text);
clipboard.setPrimaryClip(clip);
}
@Override
public void onPostResume() {
updateSystemUiOverlays();
}
public void release(){
this.activity=null;
}
}
\ No newline at end of file
...@@ -11,9 +11,10 @@ import android.view.inputmethod.BaseInputConnection; ...@@ -11,9 +11,10 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodManager;
import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.engine.dart.DartExecutor;
import io.flutter.embedding.engine.systemchannels.TextInputChannel; import io.flutter.embedding.engine.systemchannels.TextInputChannel;
import io.flutter.view.FlutterView; import io.flutter.plugin.platform.PlatformViewsController;
/** /**
* Android implementation of the text input plugin. * Android implementation of the text input plugin.
...@@ -24,7 +25,9 @@ public class XTextInputPlugin { ...@@ -24,7 +25,9 @@ public class XTextInputPlugin {
@NonNull @NonNull
private final InputMethodManager mImm; private final InputMethodManager mImm;
@NonNull @NonNull
private int mClient = 0; private final TextInputChannel textInputChannel;
@NonNull
private InputTarget inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
@Nullable @Nullable
private TextInputChannel.Configuration configuration; private TextInputChannel.Configuration configuration;
@Nullable @Nullable
...@@ -32,17 +35,29 @@ public class XTextInputPlugin { ...@@ -32,17 +35,29 @@ public class XTextInputPlugin {
private boolean mRestartInputPending; private boolean mRestartInputPending;
@Nullable @Nullable
private InputConnection lastInputConnection; private InputConnection lastInputConnection;
private TextInputChannel textInputChannel; @NonNull
public XTextInputPlugin(View view, TextInputChannel mTextInputChannel) { private PlatformViewsController platformViewsController;
// When true following calls to createInputConnection will return the cached lastInputConnection if the input
// target is a platform view. See the comments on lockPlatformViewInputConnection for more details.
private boolean isInputConnectionLocked;
public XTextInputPlugin(View view, @NonNull TextInputChannel textInputChannel, @NonNull PlatformViewsController platformViewsController) {
mView = view; mView = view;
mImm = (InputMethodManager) view.getContext().getSystemService( mImm = (InputMethodManager) view.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE); Context.INPUT_METHOD_SERVICE);
textInputChannel=mTextInputChannel; this.textInputChannel = textInputChannel;
this.platformViewsController = platformViewsController;
// this.platformViewsController.attachTextInputPlugin(this);
}
public void release() {
mView = null;
} }
public void setTextInputMethodHandler( ){ public void setTextInputMethodHandler() {
textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() { textInputChannel.setTextInputMethodHandler(new TextInputChannel.TextInputMethodHandler() {
@Override @Override
public void show() { public void show() {
...@@ -59,6 +74,11 @@ public class XTextInputPlugin { ...@@ -59,6 +74,11 @@ public class XTextInputPlugin {
setTextInputClient(textInputClientId, configuration); setTextInputClient(textInputClientId, configuration);
} }
@Override
public void setPlatformViewClient(int platformViewId) {
setPlatformViewTextInputClient(platformViewId);
}
@Override @Override
public void setEditingState(TextInputChannel.TextEditState editingState) { public void setEditingState(TextInputChannel.TextEditState editingState) {
setTextInputEditingState(mView, editingState); setTextInputEditingState(mView, editingState);
...@@ -70,15 +90,46 @@ public class XTextInputPlugin { ...@@ -70,15 +90,46 @@ public class XTextInputPlugin {
} }
}); });
} }
public void release(){
mView=null;
}
@NonNull @NonNull
public InputMethodManager getInputMethodManager() { public InputMethodManager getInputMethodManager() {
return mImm; return mImm;
} }
/***
* Use the current platform view input connection until unlockPlatformViewInputConnection is called.
*
* The current input connection instance is cached and any following call to @{link createInputConnection} returns
* the cached connection until unlockPlatformViewInputConnection is called.
*
* This is a no-op if the current input target isn't a platform view.
*
* This is used to preserve an input connection when moving a platform view from one virtual display to another.
*/
public void lockPlatformViewInputConnection() {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
isInputConnectionLocked = true;
}
}
/**
* Unlocks the input connection.
* <p>
* See also: @{link lockPlatformViewInputConnection}.
*/
public void unlockPlatformViewInputConnection() {
isInputConnectionLocked = false;
}
/**
* Detaches the text input plugin from the platform views controller.
* <p>
* The TextInputPlugin instance should not be used after calling this.
*/
public void destroy() {
platformViewsController.detachTextInputPlugin();
}
private static int inputTypeFromTextInputType( private static int inputTypeFromTextInputType(
TextInputChannel.InputType type, TextInputChannel.InputType type,
boolean obscureText, boolean obscureText,
...@@ -107,6 +158,8 @@ public class XTextInputPlugin { ...@@ -107,6 +158,8 @@ public class XTextInputPlugin {
textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS; textType |= InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
} else if (type.type == TextInputChannel.TextInputType.URL) { } else if (type.type == TextInputChannel.TextInputType.URL) {
textType |= InputType.TYPE_TEXT_VARIATION_URI; textType |= InputType.TYPE_TEXT_VARIATION_URI;
} else if (type.type == TextInputChannel.TextInputType.VISIBLE_PASSWORD) {
textType |= InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD;
} }
if (obscureText) { if (obscureText) {
...@@ -129,8 +182,16 @@ public class XTextInputPlugin { ...@@ -129,8 +182,16 @@ public class XTextInputPlugin {
} }
public InputConnection createInputConnection(View view, EditorInfo outAttrs) { public InputConnection createInputConnection(View view, EditorInfo outAttrs) {
if (mClient == 0) { if (inputTarget.type == InputTarget.Type.NO_TARGET) {
lastInputConnection = null; lastInputConnection = null;
return null;
}
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
if (isInputConnectionLocked) {
return lastInputConnection;
}
lastInputConnection = platformViewsController.getPlatformViewById(inputTarget.id).onCreateInputConnection(outAttrs);
return lastInputConnection; return lastInputConnection;
} }
...@@ -159,7 +220,7 @@ public class XTextInputPlugin { ...@@ -159,7 +220,7 @@ public class XTextInputPlugin {
XInputConnectionAdaptor connection = new XInputConnectionAdaptor( XInputConnectionAdaptor connection = new XInputConnectionAdaptor(
view, view,
mClient, inputTarget.id,
textInputChannel, textInputChannel,
mEditable mEditable
); );
...@@ -175,24 +236,53 @@ public class XTextInputPlugin { ...@@ -175,24 +236,53 @@ public class XTextInputPlugin {
return lastInputConnection; return lastInputConnection;
} }
/**
* Clears a platform view text input client if it is the current input target.
* <p>
* This is called when a platform view is disposed to make sure we're not hanging to a stale input
* connection.
*/
public void clearPlatformViewClient(int platformViewId) {
if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW && inputTarget.id == platformViewId) {
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
hideTextInput(mView);
mImm.restartInput(mView);
mRestartInputPending = false;
}
}
private void showTextInput(View view) { private void showTextInput(View view) {
if(view==null) return;
view.requestFocus(); view.requestFocus();
mImm.showSoftInput(view, 0); mImm.showSoftInput(view, 0);
} }
private void hideTextInput(View view) { private void hideTextInput(View view) {
// Note: a race condition may lead to us hiding the keyboard here just after a platform view has shown it.
// This can only potentially happen when switching focus from a Flutter text field to a platform view's text
// field(by text field here I mean anything that keeps the keyboard open).
// See: https://github.com/flutter/flutter/issues/34169
mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0); mImm.hideSoftInputFromWindow(view.getApplicationWindowToken(), 0);
} }
private void setTextInputClient(int client, TextInputChannel.Configuration configuration) { private void setTextInputClient(int client, TextInputChannel.Configuration configuration) {
mClient = client; inputTarget = new InputTarget(InputTarget.Type.FRAMEWORK_CLIENT, client);
this.configuration = configuration; this.configuration = configuration;
mEditable = Editable.Factory.getInstance().newEditable(""); mEditable = Editable.Factory.getInstance().newEditable("");
// setTextInputClient will be followed by a call to setTextInputEditingState. // setTextInputClient will be followed by a call to setTextInputEditingState.
// Do a restartInput at that time. // Do a restartInput at that time.
mRestartInputPending = true; mRestartInputPending = true;
unlockPlatformViewInputConnection();
}
private void setPlatformViewTextInputClient(int platformViewId) {
// We need to make sure that the Flutter view is focused so that no imm operations get short circuited.
// Not asking for focus here specifically manifested in a but on API 28 devices where the platform view's
// request to show a keyboard was ignored.
mView.requestFocus();
inputTarget = new InputTarget(InputTarget.Type.PLATFORM_VIEW, platformViewId);
mImm.restartInput(mView);
mRestartInputPending = false;
} }
private void applyStateToSelection(TextInputChannel.TextEditState state) { private void applyStateToSelection(TextInputChannel.TextEditState state) {
...@@ -222,6 +312,45 @@ public class XTextInputPlugin { ...@@ -222,6 +312,45 @@ public class XTextInputPlugin {
} }
private void clearTextInputClient() { private void clearTextInputClient() {
mClient = 0; if (inputTarget.type == InputTarget.Type.PLATFORM_VIEW) {
// Focus changes in the framework tree have no guarantees on the order focus nodes are notified. A node
// that lost focus may be notified before or after a node that gained focus.
// When moving the focus from a Flutter text field to an AndroidView, it is possible that the Flutter text
// field's focus node will be notified that it lost focus after the AndroidView was notified that it gained
// focus. When this happens the text field will send a clearTextInput command which we ignore.
// By doing this we prevent the framework from clearing a platform view input client(the only way to do so
// is to set a new framework text client). I don't see an obvious use case for "clearing" a platform views
// text input client, and it may be error prone as we don't know how the platform view manages the input
// connection and we probably shouldn't interfere.
// If we ever want to allow the framework to clear a platform view text client we should probably consider
// changing the focus manager such that focus nodes that lost focus are notified before focus nodes that
// gained focus as part of the same focus event.
return;
}
inputTarget = new InputTarget(InputTarget.Type.NO_TARGET, 0);
unlockPlatformViewInputConnection();
}
static private class InputTarget {
enum Type {
NO_TARGET,
// InputConnection is managed by the TextInputPlugin, and events are forwarded to the Flutter framework.
FRAMEWORK_CLIENT,
// InputConnection is managed by an embedded platform view.
PLATFORM_VIEW
}
public InputTarget(@NonNull Type type, int id) {
this.type = type;
this.id = id;
}
@NonNull
Type type;
// The ID of the input target.
//
// For framework clients this is the framework input connection client ID.
// For platform views this is the platform view's ID.
int id;
} }
} }
\ No newline at end of file
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost.containers; package com.idlefish.flutterboost.containers;
import android.app.Activity; import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.arch.lifecycle.LifecycleOwner;
import android.arch.lifecycle.LifecycleRegistry;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler; import android.support.annotation.NonNull;
import android.os.Looper; import android.support.annotation.Nullable;
import android.view.Gravity; import android.view.*;
import android.view.KeyEvent; import android.widget.*;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.idlefish.flutterboost.BoostFlutterEngine;
import com.idlefish.flutterboost.BoostFlutterView;
import com.idlefish.flutterboost.FlutterBoost; import com.idlefish.flutterboost.FlutterBoost;
import com.idlefish.flutterboost.Utils; import com.idlefish.flutterboost.XFlutterView;
import com.idlefish.flutterboost.interfaces.IFlutterViewContainer; import io.flutter.Log;
import com.idlefish.flutterboost.interfaces.IOperateSyncer; import io.flutter.embedding.android.DrawableSplashScreen;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.SplashScreen;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.plugin.platform.PlatformPlugin;
import java.io.Serializable; import java.io.Serializable;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import io.flutter.embedding.android.FlutterView; public class BoostFlutterActivity extends Activity
import io.flutter.plugin.platform.PlatformPlugin; implements FlutterActivityAndFragmentDelegate.Host,
LifecycleOwner {
public abstract class BoostFlutterActivity extends Activity implements IFlutterViewContainer { private static final String TAG = "NewBoostFlutterActivity";
protected BoostFlutterEngine mFlutterEngine; // Meta-data arguments, processed from manifest XML.
protected BoostFlutterView mFlutterView; protected static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable";
protected IOperateSyncer mSyncer; protected static final String NORMAL_THEME_META_DATA_KEY = "io.flutter.embedding.android.NormalTheme";
private Handler mHandler = new Handler(Looper.getMainLooper()); // Intent extra arguments.
protected static final String EXTRA_DART_ENTRYPOINT = "dart_entrypoint";
protected static final String EXTRA_BACKGROUND_MODE = "background_mode";
protected static final String EXTRA_DESTROY_ENGINE_WITH_ACTIVITY = "destroy_engine_with_activity";
@Override protected static final String EXTRA_URL = "url";
protected void onCreate(Bundle savedInstanceState) { protected static final String EXTRA_PARAMS = "params";
super.onCreate(savedInstanceState);
configureWindowForTransparency();
mSyncer = FlutterBoost.singleton().containerManager().generateSyncer(this); // Default configuration.
protected static final String DEFAULT_BACKGROUND_MODE = BackgroundMode.opaque.name();
mFlutterEngine = createFlutterEngine(); public static Intent createDefaultIntent(@NonNull Context launchContext) {
mFlutterView = createFlutterView(mFlutterEngine); return withNewEngine().build(launchContext);
}
setContentView(mFlutterView);
mSyncer.onCreate(); public static NewEngineIntentBuilder withNewEngine() {
return new NewEngineIntentBuilder(BoostFlutterActivity.class);
}
configureStatusBarForFullscreenFlutterExperience();
public static class NewEngineIntentBuilder {
private final Class<? extends BoostFlutterActivity> activityClass;
private String backgroundMode = DEFAULT_BACKGROUND_MODE;
private String url = "";
private Map params = new HashMap();
protected NewEngineIntentBuilder(@NonNull Class<? extends BoostFlutterActivity> activityClass) {
this.activityClass = activityClass;
}
public NewEngineIntentBuilder url(@NonNull String url) {
this.url = url;
return this;
}
public NewEngineIntentBuilder params(@NonNull Map params) {
this.params = params;
return this;
}
public NewEngineIntentBuilder backgroundMode(@NonNull BackgroundMode backgroundMode) {
this.backgroundMode = backgroundMode.name();
return this;
}
public Intent build(@NonNull Context context) {
SerializableMap serializableMap = new SerializableMap();
serializableMap.setMap(params);
return new Intent(context, activityClass)
.putExtra(EXTRA_BACKGROUND_MODE, backgroundMode)
.putExtra(EXTRA_DESTROY_ENGINE_WITH_ACTIVITY, false)
.putExtra(EXTRA_URL, url)
.putExtra(EXTRA_PARAMS, serializableMap);
}
}
public static class SerializableMap implements Serializable {
private Map<String, Object> map;
public Map<String, Object> getMap() {
return map;
}
public void setMap(Map<String, Object> map) {
this.map = map;
}
}
private FlutterActivityAndFragmentDelegate delegate;
@NonNull
private LifecycleRegistry lifecycle;
public BoostFlutterActivity() {
lifecycle = new LifecycleRegistry(this);
} }
@Override @Override
public void onAttachedToWindow() { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onAttachedToWindow(); switchLaunchThemeForNormalTheme();
mHandler.post(new Runnable() {
@Override super.onCreate(savedInstanceState);
public void run() {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(this);
configureWindowForTransparency();
setContentView(createFlutterView());
configureStatusBarForFullscreenFlutterExperience(); configureStatusBarForFullscreenFlutterExperience();
} }
});
private void switchLaunchThemeForNormalTheme() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
if (activityInfo.metaData != null) {
int normalThemeRID = activityInfo.metaData.getInt(NORMAL_THEME_META_DATA_KEY, -1);
if (normalThemeRID != -1) {
setTheme(normalThemeRID);
}
} else {
Log.d(TAG, "Using the launch theme as normal theme.");
}
} catch (PackageManager.NameNotFoundException exception) {
Log.e(TAG, "Could not read meta-data for FlutterActivity. Using the launch theme as normal theme.");
}
}
@Nullable
@Override
public SplashScreen provideSplashScreen() {
Drawable manifestSplashDrawable = getSplashScreenFromManifest();
if (manifestSplashDrawable != null) {
return new DrawableSplashScreen(manifestSplashDrawable, ImageView.ScaleType.CENTER, 500L);
} else {
return null;
}
}
/**
* Returns a {@link Drawable} to be used as a splash screen as requested by meta-data in the
* {@code AndroidManifest.xml} file, or null if no such splash screen is requested.
* <p>
* See {@link #SPLASH_SCREEN_META_DATA_KEY} for the meta-data key to be used in a
* manifest file.
*/
@Nullable
@SuppressWarnings("deprecation")
private Drawable getSplashScreenFromManifest() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(
getComponentName(),
PackageManager.GET_META_DATA | PackageManager.GET_ACTIVITIES
);
Bundle metadata = activityInfo.metaData;
Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
return splashScreenId != null
? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
? getResources().getDrawable(splashScreenId, getTheme())
: getResources().getDrawable(splashScreenId)
: null;
} catch (PackageManager.NameNotFoundException e) {
// This is never expected to happen.
return null;
}
} }
protected void configureWindowForTransparency() { /**
if (isBackgroundTransparent()) { * Sets this {@code Activity}'s {@code Window} background to be transparent, and hides the status
* bar, if this {@code Activity}'s desired {@link BackgroundMode} is {@link BackgroundMode#transparent}.
* <p>
* For {@code Activity} transparency to work as expected, the theme applied to this {@code Activity}
* must include {@code <item name="android:windowIsTranslucent">true</item>}.
*/
private void configureWindowForTransparency() {
BackgroundMode backgroundMode = getBackgroundMode();
if (backgroundMode == BackgroundMode.transparent) {
getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
getWindow().setFlags( getWindow().setFlags(
WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
...@@ -105,7 +221,15 @@ public abstract class BoostFlutterActivity extends Activity implements IFlutterV ...@@ -105,7 +221,15 @@ public abstract class BoostFlutterActivity extends Activity implements IFlutterV
} }
} }
protected void configureStatusBarForFullscreenFlutterExperience() { @NonNull
protected View createFlutterView() {
return delegate.onCreateView(
null /* inflater */,
null /* container */,
null /* savedInstanceState */);
}
private void configureStatusBarForFullscreenFlutterExperience() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
Window window = getWindow(); Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
...@@ -113,158 +237,269 @@ public abstract class BoostFlutterActivity extends Activity implements IFlutterV ...@@ -113,158 +237,269 @@ public abstract class BoostFlutterActivity extends Activity implements IFlutterV
window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI); window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI);
} }
Utils.setStatusBarLightMode(this,true);
}
protected BoostFlutterEngine createFlutterEngine(){
return FlutterBoost.singleton().engineProvider().provideEngine(this);
} }
protected BoostFlutterView createFlutterView(BoostFlutterEngine engine){ protected XFlutterView getFlutterView() {
BoostFlutterView.Builder builder = new BoostFlutterView.Builder(this); return delegate.getFlutterView();
return builder.flutterEngine(engine)
.renderMode(FlutterView.RenderMode.texture)
.transparencyMode(isBackgroundTransparent() ?
FlutterView.TransparencyMode.transparent :
FlutterView.TransparencyMode.opaque)
.renderingProgressCoverCreator(new BoostFlutterView.RenderingProgressCoverCreator() {
@Override
public View createRenderingProgressCover(Context context) {
return BoostFlutterActivity.this.createRenderingProgressCover();
}
})
.build();
} }
protected boolean isBackgroundTransparent(){ @Override
return false; protected void onStart() {
} super.onStart();
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
protected View createRenderingProgressCover(){ delegate.onStart();
FrameLayout frameLayout = new FrameLayout(this);
LinearLayout linearLayout = new LinearLayout(this);
linearLayout.setOrientation(LinearLayout.VERTICAL);
FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.CENTER;
frameLayout.addView(linearLayout,layoutParams);
ProgressBar progressBar = new ProgressBar(this);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
linearLayout.addView(progressBar,params);
TextView textView = new TextView(this);
params = new LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.gravity = Gravity.CENTER_HORIZONTAL;
textView.setText("Frame Rendering...");
linearLayout.addView(textView,params);
return frameLayout;
} }
@Override @Override
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
mSyncer.onAppear(); lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME);
mFlutterEngine.getLifecycleChannel().appIsResumed(); delegate.onResume();
}
@Override
public void onPostResume() {
super.onPostResume();
delegate.onPostResume();
} }
@Override @Override
protected void onPause() { protected void onPause() {
mSyncer.onDisappear();
super.onPause(); super.onPause();
mFlutterEngine.getLifecycleChannel().appIsInactive(); delegate.onPause();
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE);
}
@Override
protected void onStop() {
super.onStop();
delegate.onStop();
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
Utils.fixInputMethodManagerLeak(this);
mSyncer.onDestroy();
super.onDestroy(); super.onDestroy();
delegate.onDestroyView();
delegate.onDetach();
// lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY);
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
delegate.onActivityResult(requestCode, resultCode, data);
}
@Override
protected void onNewIntent(@NonNull Intent intent) {
// TODO(mattcarroll): change G3 lint rule that forces us to call super
super.onNewIntent(intent);
delegate.onNewIntent(intent);
} }
@Override @Override
public void onBackPressed() { public void onBackPressed() {
mSyncer.onBackPressed(); delegate.onBackPressed();
} }
@Override @Override
protected void onNewIntent(Intent intent) { public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onNewIntent(intent); delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
mSyncer.onNewIntent(intent);
} }
@Override @Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) { public void onUserLeaveHint() {
super.onActivityResult(requestCode, resultCode, data); delegate.onUserLeaveHint();
mSyncer.onActivityResult(requestCode,resultCode,data); }
Map<String,Object> result = null; @Override
if(data != null) { public void onTrimMemory(int level) {
Serializable rlt = data.getSerializableExtra(RESULT_KEY); super.onTrimMemory(level);
if(rlt instanceof Map) { delegate.onTrimMemory(level);
result = (Map<String,Object>)rlt;
} }
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain a {@code Context} reference as
* needed.
*/
@Override
@NonNull
public Context getContext() {
return this;
} }
mSyncer.onContainerResult(requestCode,resultCode,result); /**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain an {@code Activity} reference as
* needed. This reference is used by the delegate to instantiate a {@link FlutterView},
* a {@link PlatformPlugin}, and to determine if the {@code Activity} is changing
* configurations.
*/
@Override
@NonNull
public Activity getActivity() {
return this;
} }
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain a {@code Lifecycle} reference as
* needed. This reference is used by the delegate to provide Flutter plugins with access
* to lifecycle events.
*/
@Override @Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) { @NonNull
super.onRequestPermissionsResult(requestCode, permissions, grantResults); public Lifecycle getLifecycle() {
mSyncer.onRequestPermissionsResult(requestCode, permissions, grantResults); return lifecycle;
} }
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when
* initializing Flutter.
*/
@NonNull
@Override @Override
public void onTrimMemory(int level) { public FlutterShellArgs getFlutterShellArgs() {
super.onTrimMemory(level); return FlutterShellArgs.fromIntent(getIntent());
mSyncer.onTrimMemory(level);
} }
/**
* Returns true if Flutter is running in "debug mode", and false otherwise.
* <p>
* Debug mode allows Flutter to operate with hot reload and hot restart. Release mode does not.
*/
private boolean isDebuggable() {
return (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain the desired {@link FlutterView.RenderMode}
* that should be used when instantiating a {@link FlutterView}.
*/
@NonNull
@Override @Override
public void onLowMemory() { public FlutterView.RenderMode getRenderMode() {
super.onLowMemory(); return getBackgroundMode() == BackgroundMode.opaque
mSyncer.onLowMemory(); ? FlutterView.RenderMode.surface
: FlutterView.RenderMode.texture;
} }
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain the desired
* {@link FlutterView.TransparencyMode} that should be used when instantiating a
* {@link FlutterView}.
*/
@NonNull
@Override @Override
protected void onUserLeaveHint() { public FlutterView.TransparencyMode getTransparencyMode() {
super.onUserLeaveHint(); return getBackgroundMode() == BackgroundMode.opaque
mSyncer.onUserLeaveHint(); ? FlutterView.TransparencyMode.opaque
: FlutterView.TransparencyMode.transparent;
}
/**
* The desired window background mode of this {@code Activity}, which defaults to
* {@link BackgroundMode#opaque}.
*/
@NonNull
protected BackgroundMode getBackgroundMode() {
if (getIntent().hasExtra(EXTRA_BACKGROUND_MODE)) {
return BackgroundMode.valueOf(getIntent().getStringExtra(EXTRA_BACKGROUND_MODE));
} else {
return BackgroundMode.opaque;
}
} }
/**
* Hook for subclasses to easily provide a custom {@link FlutterEngine}.
* <p>
* This hook is where a cached {@link FlutterEngine} should be provided, if a cached
* {@link FlutterEngine} is desired.
*/
@Nullable
@Override @Override
public Activity getContextActivity() { public FlutterEngine provideFlutterEngine(@NonNull Context context) {
return this; // No-op. Hook for subclasses.
return FlutterBoost.instance().engineProvider();
}
/**
* Hook for subclasses to obtain a reference to the {@link FlutterEngine} that is owned
* by this {@code FlutterActivity}.
*/
@Nullable
protected FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
} }
@Nullable
@Override @Override
public BoostFlutterView getBoostFlutterView() { public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
return mFlutterView; if (activity != null) {
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel());
} else {
return null;
}
} }
/**
* Hook for subclasses to easily configure a {@code FlutterEngine}, e.g., register
* plugins.
* <p>
* This method is called after {@link #provideFlutterEngine(Context)}.
*/
@Override @Override
public void finishContainer(Map<String,Object> result) { public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
if(result != null) { // No-op. Hook for subclasses.
FlutterBoost.setBoostResult(this,new HashMap<>(result));
finish();
}else{
finish();
} }
@Override
public boolean shouldAttachEngineToActivity() {
return true;
} }
@Override @Override
public void onContainerShown() {} public String getContainerUrl() {
if (getIntent().hasExtra(EXTRA_URL)) {
return getIntent().getStringExtra(EXTRA_URL);
}
return "";
}
@Override @Override
public void onContainerHidden() {} public Map getContainerUrlParams() {
if (getIntent().hasExtra(EXTRA_PARAMS)) {
SerializableMap serializableMap = (SerializableMap) getIntent().getSerializableExtra(EXTRA_PARAMS);
return serializableMap.getMap();
}
Map<String, String> params = new HashMap<>();
return params;
}
/**
* The mode of the background of a {@code FlutterActivity}, either opaque or transparent.
*/
public enum BackgroundMode {
/**
* Indicates a FlutterActivity with an opaque background. This is the default.
*/
opaque,
/**
* Indicates a FlutterActivity with a transparent background.
*/
transparent
}
} }
package com.idlefish.flutterboost.containers;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import java.util.HashMap;
import java.util.Map;
public class BoostFlutterDefaultActivity extends BoostFlutterActivity {
@Override
public String getContainerUrl() {
return getIntent().getStringExtra("url");
}
@Override
public Map getContainerUrlParams() {
return (Map)(getIntent().getSerializableExtra("params"));
}
private static Intent intent(Context context, String url, HashMap<String, Object> params) {
final Intent intent = new Intent(context, BoostFlutterDefaultActivity.class);
intent.putExtra("url", url);
intent.putExtra("params", params);
return intent;
}
public static void open(Context context, String url, HashMap<String, Object> params) {
context.startActivity(intent(context, url, params));
}
public static void open(Activity activity, String url, HashMap<String, Object> params, int requestCode) {
activity.startActivityForResult(intent(activity, url, params), requestCode);
}
}
\ No newline at end of file
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost.containers;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import com.idlefish.flutterboost.BoostFlutterEngine;
import com.idlefish.flutterboost.BoostFlutterView;
import com.idlefish.flutterboost.FlutterBoost;
import com.idlefish.flutterboost.Utils;
import com.idlefish.flutterboost.interfaces.IFlutterViewContainer;
import com.idlefish.flutterboost.interfaces.IOperateSyncer;
import java.util.Map;
import io.flutter.embedding.android.FlutterView;
import io.flutter.plugin.platform.PlatformPlugin;
abstract public class BoostFlutterFragment extends Fragment implements IFlutterViewContainer {
protected BoostFlutterEngine mFlutterEngine;
protected BoostFlutterView mFlutterView;
protected IOperateSyncer mSyncer;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
mSyncer = FlutterBoost.singleton().containerManager().generateSyncer(this);
mFlutterEngine = createFlutterEngine();
mFlutterView = createFlutterView(mFlutterEngine);
mSyncer.onCreate();
return mFlutterView;
}
protected BoostFlutterEngine createFlutterEngine(){
return FlutterBoost.singleton().engineProvider().provideEngine(getContext());
}
protected BoostFlutterView createFlutterView(BoostFlutterEngine engine){
BoostFlutterView.Builder builder = new BoostFlutterView.Builder(getContextActivity());
return builder.flutterEngine(engine)
.renderMode(FlutterView.RenderMode.texture)
.transparencyMode(FlutterView.TransparencyMode.opaque)
.build();
}
@Override
public void onResume() {
super.onResume();
mSyncer.onAppear();
mFlutterEngine.getLifecycleChannel().appIsResumed();
}
@Override
public void onPause() {
mSyncer.onDisappear();
super.onPause();
mFlutterEngine.getLifecycleChannel().appIsInactive();
}
@Override
public void onDestroy() {
mSyncer.onDestroy();
super.onDestroy();
}
public void onBackPressed() {
mSyncer.onBackPressed();
}
public void onNewIntent(Intent intent) {
mSyncer.onNewIntent(intent);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
mSyncer.onActivityResult(requestCode,resultCode,data);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
mSyncer.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
public void onTrimMemory(int level) {
mSyncer.onTrimMemory(level);
}
@Override
public void onLowMemory() {
super.onLowMemory();
mSyncer.onLowMemory();
}
public void onUserLeaveHint() {
mSyncer.onUserLeaveHint();
}
@Override
public Activity getContextActivity() {
return getActivity();
}
@Override
public BoostFlutterView getBoostFlutterView() {
return mFlutterView;
}
@Override
public void finishContainer(Map<String,Object> result) {
getFragmentManager().popBackStack();
}
@Override
public void onContainerShown() {}
@Override
public void onContainerHidden() {}
}
package com.idlefish.flutterboost.containers;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.content.Context;
import android.content.Intent;
import android.graphics.PixelFormat;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.io.Serializable;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import com.idlefish.flutterboost.BoostPluginRegistry;
import com.idlefish.flutterboost.FlutterBoost;
import com.idlefish.flutterboost.Utils;
import com.idlefish.flutterboost.XFlutterView;
import com.idlefish.flutterboost.interfaces.IFlutterViewContainer;
import com.idlefish.flutterboost.interfaces.IOperateSyncer;
import io.flutter.Log;
import io.flutter.app.FlutterActivity;
import io.flutter.embedding.android.*;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.platform.PlatformPlugin;
import static android.content.ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW;
public class FlutterActivityAndFragmentDelegate implements IFlutterViewContainer {
private static final String TAG = "FlutterActivityAndFragmentDelegate";
@NonNull
private Host host;
@Nullable
private FlutterEngine flutterEngine;
@Nullable
private FlutterSplashView flutterSplashView;
@Nullable
private XFlutterView flutterView;
@Nullable
private PlatformPlugin platformPlugin;
private boolean isFlutterEngineFromHost;
protected IOperateSyncer mSyncer;
FlutterActivityAndFragmentDelegate(@NonNull Host host) {
this.host = host;
}
void release() {
this.host = null;
this.flutterEngine = null;
this.flutterView = null;
this.platformPlugin = null;
}
@Nullable
FlutterEngine getFlutterEngine() {
return flutterEngine;
}
XFlutterView getFlutterView() {
return flutterView;
}
void onAttach(@NonNull Context context) {
ensureAlive();
if (FlutterBoost.instance().platform().whenEngineStart() == FlutterBoost.ConfigBuilder.FLUTTER_ACTIVITY_CREATED) {
FlutterBoost.instance().doInitialFlutter();
FlutterBoost.instance().boostPluginRegistry();
}
// When "retain instance" is true, the FlutterEngine will survive configuration
// changes. Therefore, we create a new one only if one does not already exist.
if (flutterEngine == null) {
setupFlutterEngine();
}
// Regardless of whether or not a FlutterEngine already existed, the PlatformPlugin
// is bound to a specific Activity. Therefore, it needs to be created and configured
// every time this Fragment attaches to a new Activity.
// TODO(mattcarroll): the PlatformPlugin needs to be reimagined because it implicitly takes
// control of the entire window. This is unacceptable for non-fullscreen
// use-cases.
platformPlugin = host.providePlatformPlugin(host.getActivity(), flutterEngine);
host.configureFlutterEngine(flutterEngine);
host.getActivity().getWindow().setFormat(PixelFormat.TRANSLUCENT);
}
private void setupFlutterEngine() {
Log.d(TAG, "Setting up FlutterEngine.");
// Second, defer to subclasses for a custom FlutterEngine.
flutterEngine = host.provideFlutterEngine(host.getContext());
if (flutterEngine != null) {
isFlutterEngineFromHost = true;
return;
}
// Our host did not provide a custom FlutterEngine. Create a FlutterEngine to back our
// FlutterView.
Log.d(TAG, "No preferred FlutterEngine was provided. Creating a new FlutterEngine for"
+ " this NewFlutterFragment.");
isFlutterEngineFromHost = false;
}
@SuppressLint("ResourceType")
@NonNull
View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
Log.v(TAG, "Creating FlutterView.");
flutterEngine.getActivityControlSurface().attachToActivity(
host.getActivity(),
host.getLifecycle()
);
mSyncer = FlutterBoost.instance().containerManager().generateSyncer(this);
ensureAlive();
flutterView = new XFlutterView(host.getActivity(), FlutterBoost.instance().platform().renderMode(), host.getTransparencyMode());
flutterSplashView = new FlutterSplashView(host.getContext());
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
flutterSplashView.setId(View.generateViewId());
} else {
// TODO(mattcarroll): Find a better solution to this ID. This is a random, static ID.
// It might conflict with other Views, and it means that only a single FlutterSplashView
// can exist in a View hierarchy at one time.
flutterSplashView.setId(486947586);
}
flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen());
mSyncer.onCreate();
return flutterSplashView;
}
void onStart() {
Log.v(TAG, "onStart()");
ensureAlive();
// We post() the code that attaches the FlutterEngine to our FlutterView because there is
// some kind of blocking logic on the native side when the surface is connected. That lag
// causes launching Activitys to wait a second or two before launching. By post()'ing this
// behavior we are able to move this blocking logic to after the Activity's launch.
// TODO(mattcarroll): figure out how to avoid blocking the MAIN thread when connecting a surface
}
void onResume() {
mSyncer.onAppear();
Log.v(TAG, "onResume()");
ensureAlive();
flutterEngine.getLifecycleChannel().appIsResumed();
BoostPluginRegistry registry = (BoostPluginRegistry) FlutterBoost.instance().getPluginRegistry();
ActivityPluginBinding binding = registry.getRegistrarAggregate().getActivityPluginBinding();
if (binding != null && (binding.getActivity() != this.host.getActivity())) {
flutterEngine.getActivityControlSurface().attachToActivity(
host.getActivity(),
host.getLifecycle()
);
}
}
void onPostResume() {
Log.v(TAG, "onPostResume()");
ensureAlive();
Utils.setStatusBarLightMode(host.getActivity(), true);
}
void onPause() {
Log.v(TAG, "onPause()");
ensureAlive();
mSyncer.onDisappear();
flutterEngine.getLifecycleChannel().appIsInactive();
}
void onStop() {
Log.v(TAG, "onStop()");
ensureAlive();
}
void onDestroyView() {
Log.v(TAG, "onDestroyView()");
mSyncer.onDestroy();
ensureAlive();
BoostPluginRegistry registry = (BoostPluginRegistry) FlutterBoost.instance().getPluginRegistry();
if (registry != null) {
ActivityPluginBinding binding = registry.getRegistrarAggregate().getActivityPluginBinding();
if (binding != null && (binding.getActivity() == this.host.getActivity())) {
registry.getRegistrarAggregate().onDetachedFromActivityForConfigChanges();
flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges();
}
}
flutterView.release();
}
void onDetach() {
Log.v(TAG, "onDetach()");
ensureAlive();
// Null out the platformPlugin to avoid a possible retain cycle between the plugin, this Fragment,
// and this Fragment's Activity.
if (platformPlugin != null) {
platformPlugin.destroy();
platformPlugin = null;
}
Utils.fixInputMethodManagerLeak(host.getActivity());
}
void onBackPressed() {
mSyncer.onBackPressed();
ensureAlive();
}
void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
mSyncer.onRequestPermissionsResult(requestCode, permissions, grantResults);
ensureAlive();
if (flutterEngine != null) {
Log.v(TAG, "Forwarding onRequestPermissionsResult() to FlutterEngine:\n"
+ "requestCode: " + requestCode + "\n"
+ "permissions: " + Arrays.toString(permissions) + "\n"
+ "grantResults: " + Arrays.toString(grantResults));
flutterEngine.getActivityControlSurface().onRequestPermissionsResult(requestCode, permissions, grantResults);
} else {
Log.w(TAG, "onRequestPermissionResult() invoked before NewFlutterFragment was attached to an Activity.");
}
}
void onNewIntent(@NonNull Intent intent) {
mSyncer.onNewIntent(intent);
ensureAlive();
if (flutterEngine != null) {
Log.v(TAG, "Forwarding onNewIntent() to FlutterEngine.");
flutterEngine.getActivityControlSurface().onNewIntent(intent);
} else {
Log.w(TAG, "onNewIntent() invoked before NewFlutterFragment was attached to an Activity.");
}
}
void onActivityResult(int requestCode, int resultCode, Intent data) {
mSyncer.onActivityResult(requestCode, resultCode, data);
Map<String, Object> result = null;
if (data != null) {
Serializable rlt = data.getSerializableExtra(RESULT_KEY);
if (rlt instanceof Map) {
result = (Map<String, Object>) rlt;
}
}
mSyncer.onContainerResult(requestCode, resultCode, result);
ensureAlive();
if (flutterEngine != null) {
Log.v(TAG, "Forwarding onActivityResult() to FlutterEngine:\n"
+ "requestCode: " + requestCode + "\n"
+ "resultCode: " + resultCode + "\n"
+ "data: " + data);
flutterEngine.getActivityControlSurface().onActivityResult(requestCode, resultCode, data);
} else {
Log.w(TAG, "onActivityResult() invoked before NewFlutterFragment was attached to an Activity.");
}
}
void onUserLeaveHint() {
ensureAlive();
if (flutterEngine != null) {
Log.v(TAG, "Forwarding onUserLeaveHint() to FlutterEngine.");
flutterEngine.getActivityControlSurface().onUserLeaveHint();
} else {
Log.w(TAG, "onUserLeaveHint() invoked before NewFlutterFragment was attached to an Activity.");
}
}
void onTrimMemory(int level) {
mSyncer.onTrimMemory(level);
ensureAlive();
if (flutterEngine != null) {
// Use a trim level delivered while the application is running so the
// framework has a chance to react to the notification.
if (level == TRIM_MEMORY_RUNNING_LOW) {
Log.v(TAG, "Forwarding onTrimMemory() to FlutterEngine. Level: " + level);
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
} else {
Log.w(TAG, "onTrimMemory() invoked before NewFlutterFragment was attached to an Activity.");
}
}
void onLowMemory() {
Log.v(TAG, "Forwarding onLowMemory() to FlutterEngine.");
mSyncer.onLowMemory();
ensureAlive();
flutterEngine.getSystemChannel().sendMemoryPressureWarning();
}
/**
* Ensures that this delegate has not been {@link #release()}'ed.
* <p>
* An {@code IllegalStateException} is thrown if this delegate has been {@link #release()}'ed.
*/
private void ensureAlive() {
if (host == null) {
throw new IllegalStateException("Cannot execute method on a destroyed FlutterActivityAndFragmentDelegate.");
}
}
@Override
public Activity getContextActivity() {
return (Activity) this.host.getActivity();
}
@Override
public FlutterSplashView getBoostFlutterView() {
return this.flutterSplashView;
}
@Override
public void finishContainer(Map<String, Object> result) {
if (result != null) {
setBoostResult(this.host.getActivity(), new HashMap<>(result));
this.host.getActivity().finish();
} else {
this.host.getActivity().finish();
}
}
public void setBoostResult(Activity activity, HashMap result) {
Intent intent = new Intent();
if (result != null) {
intent.putExtra(IFlutterViewContainer.RESULT_KEY, result);
}
activity.setResult(Activity.RESULT_OK, intent);
}
@Override
public String getContainerUrl() {
return this.host.getContainerUrl();
}
@Override
public Map getContainerUrlParams() {
return this.host.getContainerUrlParams();
}
@Override
public void onContainerShown() {
}
@Override
public void onContainerHidden() {
}
/**
* The {@link FlutterActivity} or {@link FlutterFragment} that owns this
* {@code FlutterActivityAndFragmentDelegate}.
*/
/* package */ interface Host extends SplashScreenProvider, FlutterEngineProvider, FlutterEngineConfigurator {
/**
* Returns the {@link Context} that backs the host {@link Activity} or {@code Fragment}.
*/
@NonNull
Context getContext();
/**
* Returns the host {@link Activity} or the {@code Activity} that is currently attached
* to the host {@code Fragment}.
*/
@Nullable
Activity getActivity();
/**
* Returns the {@link Lifecycle} that backs the host {@link Activity} or {@code Fragment}.
*/
@NonNull
Lifecycle getLifecycle();
/**
* Returns the {@link FlutterShellArgs} that should be used when initializing Flutter.
*/
@NonNull
FlutterShellArgs getFlutterShellArgs();
/**
* Returns the {@link FlutterView.RenderMode} used by the {@link FlutterView} that
* displays the {@link FlutterEngine}'s content.
*/
@NonNull
FlutterView.RenderMode getRenderMode();
/**
* Returns the {@link FlutterView.TransparencyMode} used by the {@link FlutterView} that
* displays the {@link FlutterEngine}'s content.
*/
@NonNull
FlutterView.TransparencyMode getTransparencyMode();
@Nullable
SplashScreen provideSplashScreen();
/**
* Returns the {@link FlutterEngine} that should be rendered to a {@link FlutterView}.
* <p>
* If {@code null} is returned, a new {@link FlutterEngine} will be created automatically.
*/
@Nullable
FlutterEngine provideFlutterEngine(@NonNull Context context);
/**
* Hook for the host to create/provide a {@link PlatformPlugin} if the associated
* Flutter experience should control system chrome.
*/
@Nullable
PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine);
/**
* Hook for the host to configure the {@link FlutterEngine} as desired.
*/
void configureFlutterEngine(@NonNull FlutterEngine flutterEngine);
/**
* Returns true if the {@link FlutterEngine}'s plugin system should be connected to the
* host {@link Activity}, allowing plugins to interact with it.
*/
boolean shouldAttachEngineToActivity();
String getContainerUrl();
Map getContainerUrlParams();
}
}
package com.idlefish.flutterboost.containers;
import android.app.Activity;
import android.arch.lifecycle.Lifecycle;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.idlefish.flutterboost.FlutterBoost;
import com.idlefish.flutterboost.XFlutterView;
import io.flutter.embedding.android.*;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.FlutterShellArgs;
import io.flutter.plugin.platform.PlatformPlugin;
import java.util.HashMap;
import java.util.Map;
public class FlutterFragment extends Fragment implements FlutterActivityAndFragmentDelegate.Host {
private static final String TAG = "NewFlutterFragment";
/**
* The Dart entrypoint method name that is executed upon initialization.
*/
protected static final String ARG_DART_ENTRYPOINT = "dart_entrypoint";
/**
* Initial Flutter route that is rendered in a Navigator widget.
*/
protected static final String ARG_INITIAL_ROUTE = "initial_route";
/**
* Path to Flutter's Dart code.
*/
protected static final String ARG_APP_BUNDLE_PATH = "app_bundle_path";
/**
* Flutter shell arguments.
*/
protected static final String ARG_FLUTTER_INITIALIZATION_ARGS = "initialization_args";
/**
* {@link FlutterView.RenderMode} to be used for the {@link FlutterView} in this
* {@code NewFlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode";
/**
* {@link FlutterView.TransparencyMode} to be used for the {@link FlutterView} in this
* {@code NewFlutterFragment}
*/
protected static final String ARG_FLUTTERVIEW_TRANSPARENCY_MODE = "flutterview_transparency_mode";
/**
* See {@link #shouldAttachEngineToActivity()}.
*/
protected static final String ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY = "should_attach_engine_to_activity";
/**
* the created {@code NewFlutterFragment}.
*/
protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id";
/**
* True if the {@link FlutterEngine} in the created {@code NewFlutterFragment} should be destroyed
* when the {@code NewFlutterFragment} is destroyed, false if the {@link FlutterEngine} should
* outlive the {@code NewFlutterFragment}.
*/
protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment";
protected static final String EXTRA_URL = "url";
protected static final String EXTRA_PARAMS = "params";
/**
* Creates a {@code NewFlutterFragment} with a default configuration.
* <p>
* {@code NewFlutterFragment}'s default configuration creates a new {@link FlutterEngine} within
* the {@code NewFlutterFragment} and uses the following settings:
* <ul>
* <li>Dart entrypoint: "main"</li>
* <li>Initial route: "/"</li>
* <li>Render mode: surface</li>
* <li>Transparency mode: transparent</li>
* </ul>
* <p>
* To use a new {@link FlutterEngine} with different settings, use {@link #withNewEngine()}.
* <p>
* To use a cached {@link FlutterEngine} instead of creating a new one, use
*/
@NonNull
public static FlutterFragment createDefault() {
return new NewEngineFragmentBuilder().build();
}
/**
* Returns a {@link NewEngineFragmentBuilder} to create a {@code NewFlutterFragment} with a new
* {@link FlutterEngine} and a desired engine configuration.
*/
@NonNull
public static NewEngineFragmentBuilder withNewEngine() {
return new NewEngineFragmentBuilder();
}
public static class NewEngineFragmentBuilder {
private final Class<? extends FlutterFragment> fragmentClass;
private FlutterShellArgs shellArgs = null;
private FlutterView.RenderMode renderMode = FlutterView.RenderMode.surface;
private FlutterView.TransparencyMode transparencyMode = FlutterView.TransparencyMode.transparent;
private boolean shouldAttachEngineToActivity = true;
private String url = "";
private Map params = new HashMap();
/**
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
* {@code NewFlutterFragment}.
*/
public NewEngineFragmentBuilder() {
fragmentClass = FlutterFragment.class;
}
/**
* Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of
* {@code subclass}, which extends {@code NewFlutterFragment}.
*/
public NewEngineFragmentBuilder(@NonNull Class<? extends FlutterFragment> subclass) {
fragmentClass = subclass;
}
/**
* Any special configuration arguments for the Flutter engine
*/
@NonNull
public NewEngineFragmentBuilder flutterShellArgs(@NonNull FlutterShellArgs shellArgs) {
this.shellArgs = shellArgs;
return this;
}
/**
* Render Flutter either as a {@link FlutterView.RenderMode#surface} or a
* {@link FlutterView.RenderMode#texture}. You should use {@code surface} unless
* you have a specific reason to use {@code texture}. {@code texture} comes with
* a significant performance impact, but {@code texture} can be displayed
* beneath other Android {@code View}s and animated, whereas {@code surface}
* cannot.
*/
@NonNull
public NewEngineFragmentBuilder renderMode(@NonNull FlutterView.RenderMode renderMode) {
this.renderMode = renderMode;
return this;
}
public NewEngineFragmentBuilder url(@NonNull String url) {
this.url = url;
return this;
}
public NewEngineFragmentBuilder params(@NonNull Map params) {
this.params = params;
return this;
}
/**
* Support a {@link FlutterView.TransparencyMode#transparent} background within {@link FlutterView},
* or force an {@link FlutterView.TransparencyMode#opaque} background.
* <p>
* See {@link FlutterView.TransparencyMode} for implications of this selection.
*/
@NonNull
public NewEngineFragmentBuilder transparencyMode(@NonNull FlutterView.TransparencyMode transparencyMode) {
this.transparencyMode = transparencyMode;
return this;
}
@NonNull
protected Bundle createArgs() {
Bundle args = new Bundle();
// TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of conflating.
if (null != shellArgs) {
args.putStringArray(ARG_FLUTTER_INITIALIZATION_ARGS, shellArgs.toArray());
}
BoostFlutterActivity.SerializableMap serializableMap = new BoostFlutterActivity.SerializableMap();
serializableMap.setMap(params);
args.putString(EXTRA_URL, url);
args.putSerializable(EXTRA_PARAMS, serializableMap);
args.putString(ARG_FLUTTERVIEW_RENDER_MODE, renderMode != null ? renderMode.name() : FlutterView.RenderMode.surface.name());
args.putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, transparencyMode != null ? transparencyMode.name() : FlutterView.TransparencyMode.transparent.name());
args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true);
return args;
}
/**
* Constructs a new {@code NewFlutterFragment} (or a subclass) that is configured based on
* properties set on this {@code Builder}.
*/
@NonNull
public <T extends FlutterFragment> T build() {
try {
@SuppressWarnings("unchecked")
T frag = (T) fragmentClass.getDeclaredConstructor().newInstance();
if (frag == null) {
throw new RuntimeException("The NewFlutterFragment subclass sent in the constructor ("
+ fragmentClass.getCanonicalName() + ") does not match the expected return type.");
}
Bundle args = createArgs();
frag.setArguments(args);
return frag;
} catch (Exception e) {
throw new RuntimeException("Could not instantiate NewFlutterFragment subclass (" + fragmentClass.getName() + ")", e);
}
}
}
// Delegate that runs all lifecycle and OS hook logic that is common between
// FlutterActivity and NewFlutterFragment. See the FlutterActivityAndFragmentDelegate
// implementation for details about why it exists.
private FlutterActivityAndFragmentDelegate delegate;
protected XFlutterView getFlutterView() {
return delegate.getFlutterView();
}
public FlutterFragment() {
// Ensure that we at least have an empty Bundle of arguments so that we don't
// need to continually check for null arguments before grabbing one.
setArguments(new Bundle());
}
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
delegate = new FlutterActivityAndFragmentDelegate(this);
delegate.onAttach(context);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
return delegate.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onStart() {
super.onStart();
delegate.onStart();
}
@Override
public void onResume() {
super.onResume();
delegate.onResume();
}
// TODO(mattcarroll): determine why this can't be in onResume(). Comment reason, or move if possible.
@ActivityCallThrough
public void onPostResume() {
delegate.onPostResume();
}
@Override
public void onPause() {
super.onPause();
delegate.onPause();
}
@Override
public void onStop() {
super.onStop();
delegate.onStop();
}
@Override
public void onDestroyView() {
super.onDestroyView();
delegate.onDestroyView();
}
@Override
public void onDetach() {
super.onDetach();
delegate.onDetach();
delegate.release();
delegate = null;
}
@ActivityCallThrough
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
delegate.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
@ActivityCallThrough
public void onNewIntent(@NonNull Intent intent) {
delegate.onNewIntent(intent);
}
@ActivityCallThrough
public void onBackPressed() {
delegate.onBackPressed();
}
/**
* A result has been returned after an invocation of {@link Fragment#startActivityForResult(Intent, int)}.
* <p>
*
* @param requestCode request code sent with {@link Fragment#startActivityForResult(Intent, int)}
* @param resultCode code representing the result of the {@code Activity} that was launched
* @param data any corresponding return data, held within an {@code Intent}
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
delegate.onActivityResult(requestCode, resultCode, data);
}
@ActivityCallThrough
public void onUserLeaveHint() {
delegate.onUserLeaveHint();
}
/**
* Callback invoked when memory is low.
* <p>
* This implementation forwards a memory pressure warning to the running Flutter app.
* <p>
*
* @param level level
*/
@ActivityCallThrough
public void onTrimMemory(int level) {
delegate.onTrimMemory(level);
}
/**
* Callback invoked when memory is low.
* <p>
* This implementation forwards a memory pressure warning to the running Flutter app.
*/
@Override
public void onLowMemory() {
super.onLowMemory();
delegate.onLowMemory();
}
@NonNull
private Context getContextCompat() {
return Build.VERSION.SDK_INT >= 23
? getContext()
: getActivity();
}
/**
* {@link FlutterActivityAndFragmentDelegate.Host} method that is used by
* {@link FlutterActivityAndFragmentDelegate} to obtain Flutter shell arguments when
* initializing Flutter.
*/
@Override
@NonNull
public FlutterShellArgs getFlutterShellArgs() {
String[] flutterShellArgsArray = getArguments().getStringArray(ARG_FLUTTER_INITIALIZATION_ARGS);
return new FlutterShellArgs(
flutterShellArgsArray != null ? flutterShellArgsArray : new String[]{}
);
}
/**
* Returns the desired {@link FlutterView.RenderMode} for the {@link FlutterView} displayed in
* this {@code NewFlutterFragment}.
* <p>
* Defaults to {@link FlutterView.RenderMode#surface}.
* <p>
* Used by this {@code NewFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public FlutterView.RenderMode getRenderMode() {
String renderModeName = getArguments().getString(
ARG_FLUTTERVIEW_RENDER_MODE,
FlutterView.RenderMode.surface.name()
);
return FlutterView.RenderMode.valueOf(renderModeName);
}
/**
* Returns the desired {@link FlutterView.TransparencyMode} for the {@link FlutterView} displayed in
* this {@code NewFlutterFragment}.
* <p>
* Defaults to {@link FlutterView.TransparencyMode#transparent}.
* <p>
* Used by this {@code NewFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
@NonNull
public FlutterView.TransparencyMode getTransparencyMode() {
String transparencyModeName = getArguments().getString(
ARG_FLUTTERVIEW_TRANSPARENCY_MODE,
FlutterView.TransparencyMode.transparent.name()
);
return FlutterView.TransparencyMode.valueOf(transparencyModeName);
}
@Override
@Nullable
public SplashScreen provideSplashScreen() {
FragmentActivity parentActivity = getActivity();
if (parentActivity instanceof SplashScreenProvider) {
SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity;
return splashScreenProvider.provideSplashScreen();
}
return null;
}
@Override
@Nullable
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
return FlutterBoost.instance().engineProvider();
}
/**
* Hook for subclasses to obtain a reference to the {@link FlutterEngine} that is owned
* by this {@code FlutterActivity}.
*/
@Nullable
public FlutterEngine getFlutterEngine() {
return delegate.getFlutterEngine();
}
@Nullable
@Override
public PlatformPlugin providePlatformPlugin(@Nullable Activity activity, @NonNull FlutterEngine flutterEngine) {
if (activity != null) {
return new PlatformPlugin(getActivity(), flutterEngine.getPlatformChannel());
} else {
return null;
}
}
/**
* Configures a {@link FlutterEngine} after its creation.
* <p>
* This method is called after {@link #provideFlutterEngine(Context)}, and after the given
* {@link FlutterEngine} has been attached to the owning {@code FragmentActivity}. See
* {@link io.flutter.embedding.engine.plugins.activity.ActivityControlSurface#attachToActivity(Activity, Lifecycle)}.
* <p>
* It is possible that the owning {@code FragmentActivity} opted not to connect itself as
* an {@link io.flutter.embedding.engine.plugins.activity.ActivityControlSurface}. In that
* case, any configuration, e.g., plugins, must not expect or depend upon an available
* {@code Activity} at the time that this method is invoked.
* <p>
* The default behavior of this method is to defer to the owning {@code FragmentActivity}
* as a {@link FlutterEngineConfigurator}. Subclasses can override this method if the
* subclass needs to override the {@code FragmentActivity}'s behavior, or add to it.
* <p>
* Used by this {@code NewFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host}
*/
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
FragmentActivity attachedActivity = getActivity();
if (attachedActivity instanceof FlutterEngineConfigurator) {
((FlutterEngineConfigurator) attachedActivity).configureFlutterEngine(flutterEngine);
}
}
/**
* See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and
* <p>
* Used by this {@code NewFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate}
*/
@Override
public boolean shouldAttachEngineToActivity() {
return true;
}
@Override
public String getContainerUrl() {
return getArguments().getString(EXTRA_URL);
}
@Override
public Map getContainerUrlParams() {
BoostFlutterActivity.SerializableMap serializableMap = (BoostFlutterActivity.SerializableMap) getArguments().getSerializable(EXTRA_PARAMS);
return serializableMap.getMap();
}
/**
* Annotates methods in {@code NewFlutterFragment} that must be called by the containing
* {@code Activity}.
*/
@interface ActivityCallThrough {
}
}
\ No newline at end of file
package com.idlefish.flutterboost.containers;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.os.Handler;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;
import com.idlefish.flutterboost.*;
import io.flutter.Log;
import io.flutter.embedding.android.FlutterView;
import io.flutter.embedding.android.SplashScreen;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.renderer.OnFirstFrameRenderedListener;
import java.util.Date;
/**
* {@code View} that displays a {@link SplashScreen} until a given {@link FlutterView}
* renders its first frame.
*/
public class FlutterSplashView extends FrameLayout {
private static String TAG = "FlutterSplashView";
private FlutterEngine mFlutterEngine;
@Nullable
private SplashScreen splashScreen;
@Nullable
private XFlutterView flutterView;
@Nullable
private View splashScreenView;
@Nullable
private Bundle splashScreenState;
@Nullable
private String transitioningIsolateId;
@Nullable
private String previousCompletedSplashIsolate;
private Handler handler = new Handler();
@NonNull
private final FlutterView.FlutterEngineAttachmentListener flutterEngineAttachmentListener = new FlutterView.FlutterEngineAttachmentListener() {
@Override
public void onFlutterEngineAttachedToFlutterView(@NonNull FlutterEngine engine) {
flutterView.removeFlutterEngineAttachmentListener(this);
}
@Override
public void onFlutterEngineDetachedFromFlutterView() {
}
};
@NonNull
private final OnFirstFrameRenderedListener onFirstFrameRenderedListener = new OnFirstFrameRenderedListener() {
int i = 0;
@Override
public void onFirstFrameRendered() {
if (FlutterBoost.instance().platform().whenEngineStart() == FlutterBoost.ConfigBuilder.FLUTTER_ACTIVITY_CREATED) {
long now = new Date().getTime();
long flutterPostFrameCallTime = FlutterBoost.instance().getFlutterPostFrameCallTime();
if (flutterPostFrameCallTime != 0 && (now - flutterPostFrameCallTime) > 800) {
if (splashScreen != null) {
transitionToFlutter();
}
return;
}
handler.postDelayed(new Runnable() {
@Override
public void run() {
onFirstFrameRenderedListener.onFirstFrameRendered();
}
}, 200);
} else {
if (splashScreen != null) {
transitionToFlutter();
}
}
}
};
@NonNull
private final Runnable onTransitionComplete = new Runnable() {
@Override
public void run() {
removeView(splashScreenView);
previousCompletedSplashIsolate = transitioningIsolateId;
}
};
public FlutterSplashView(@NonNull Context context) {
this(context, null, 0);
}
public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public FlutterSplashView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setSaveEnabled(true);
if (mFlutterEngine == null) {
mFlutterEngine = FlutterBoost.instance().engineProvider();
}
}
/**
* Displays the given {@code splashScreen} on top of the given {@code flutterView} until
* Flutter has rendered its first frame, then the {@code splashScreen} is transitioned away.
* <p>
* If no {@code splashScreen} is provided, this {@code FlutterSplashView} displays the
* given {@code flutterView} on its own.
*/
public void displayFlutterViewWithSplash(@NonNull XFlutterView flutterView, @Nullable SplashScreen splashScreen) {
// If we were displaying a previous FlutterView, remove it.
if (this.flutterView != null) {
this.flutterView.removeOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
removeView(this.flutterView);
}
// If we were displaying a previous splash screen View, remove it.
if (splashScreenView != null) {
removeView(splashScreenView);
}
// Display the new FlutterView.
this.flutterView = flutterView;
addView(flutterView);
this.splashScreen = splashScreen;
// Display the new splash screen, if needed.
if (splashScreen != null) {
splashScreenView = splashScreen.createSplashView(getContext(), splashScreenState);
splashScreenView.setBackgroundColor(Color.WHITE);
addView(this.splashScreenView);
flutterView.addOnFirstFrameRenderedListener(onFirstFrameRenderedListener);
}
}
/**
* Returns true if a splash UI for a specific Flutter experience has already completed.
* <p>
* A "specific Flutter experience" is defined as any experience with the same Dart isolate
* ID. The purpose of this distinction is to prevent a situation where a user gets past a
* splash UI, rotates the device (or otherwise triggers a recreation) and the splash screen
* reappears.
* <p>
* An isolate ID is deemed reasonable as a key for a completion event because a Dart isolate
* cannot be entered twice. Therefore, a single Dart isolate cannot return to an "un-rendered"
* state after having previously rendered content.
*/
private boolean hasSplashCompleted() {
if (flutterView == null) {
throw new IllegalStateException("Cannot determine if splash has completed when no FlutterView "
+ "is set.");
}
if (!flutterView.isAttachedToFlutterEngine()) {
throw new IllegalStateException("Cannot determine if splash has completed when no "
+ "FlutterEngine is attached to our FlutterView. This question depends on an isolate ID "
+ "to differentiate Flutter experiences.");
}
// A null isolate ID on a non-null FlutterEngine indicates that the Dart isolate has not
// been initialized. Therefore, no frame has been rendered for this engine, which means
// no splash screen could have completed yet.
return flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId() != null
&& flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId().equals(previousCompletedSplashIsolate);
}
/**
* Transitions a splash screen to the Flutter UI.
* <p>
* This method requires that our FlutterView be attached to an engine, and that engine have
* a Dart isolate ID. It also requires that a {@code splashScreen} exist.
*/
private void transitionToFlutter() {
transitioningIsolateId = flutterView.getAttachedFlutterEngine().getDartExecutor().getIsolateServiceId();
Log.v(TAG, "Transitioning splash screen to a Flutter UI. Isolate: " + transitioningIsolateId);
splashScreen.transitionToFlutter(onTransitionComplete);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
handler.removeCallbacksAndMessages(null);
}
public void onAttach() {
Debuger.log("BoostFlutterView onAttach");
flutterView.attachToFlutterEngine(mFlutterEngine);
}
public void onDetach() {
Debuger.log("BoostFlutterView onDetach");
flutterView.detachFromFlutterEngine();
}
}
\ No newline at end of file
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost.interfaces;
import android.content.Context;
import com.idlefish.flutterboost.BoostFlutterEngine;
/**
* a flutter engine provider
*/
public interface IFlutterEngineProvider {
/**
* create flutter engine, we just hold a single instance now
* @param context
* @return
*/
BoostFlutterEngine createEngine(Context context);
/**
* provide a flutter engine
* @param context
* @return
*/
BoostFlutterEngine provideEngine(Context context);
/**
* may return null
* @return
*/
BoostFlutterEngine tryGetEngine();
}
...@@ -25,7 +25,7 @@ package com.idlefish.flutterboost.interfaces; ...@@ -25,7 +25,7 @@ package com.idlefish.flutterboost.interfaces;
import android.app.Activity; import android.app.Activity;
import com.idlefish.flutterboost.BoostFlutterView; import com.idlefish.flutterboost.containers.FlutterSplashView;
import java.util.Map; import java.util.Map;
...@@ -41,7 +41,7 @@ public interface IFlutterViewContainer { ...@@ -41,7 +41,7 @@ public interface IFlutterViewContainer {
* provide a flutter view * provide a flutter view
* @return * @return
*/ */
BoostFlutterView getBoostFlutterView(); FlutterSplashView getBoostFlutterView();
/** /**
* call to destroy the container * call to destroy the container
......
package com.idlefish.flutterboost.interfaces;
import android.content.Context;
import java.util.Map;
public interface INativeRouter {
void openContainer(Context context, String url, Map<String,Object> urlParams, int requestCode, Map<String,Object> exts);
}
/*
* The MIT License (MIT)
*
* Copyright (c) 2019 Alibaba Group
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.idlefish.flutterboost.interfaces;
import android.app.Application;
import android.content.Context;
import java.util.Map;
import io.flutter.plugin.common.PluginRegistry;
/**
* APIs that platform(Android App) must provide
*/
public interface IPlatform {
int IMMEDIATELY = 0; //立即启动引擎
int ANY_ACTIVITY_CREATED = 1; //当有任何Activity创建时,启动引擎
/**
* get current application
* @return
*/
Application getApplication();
/**
* debug or not
* @return
*/
boolean isDebug();
/**
* register plugins
* @return
*/
void registerPlugins(PluginRegistry registry);
void openContainer(Context context,String url,Map<String,Object> urlParams,int requestCode,Map<String,Object> exts);
void closeContainer(IContainerRecord record, Map<String,Object> result, Map<String,Object> exts);
IFlutterEngineProvider engineProvider();
/**
* @return
*
* IMMEDIATELY //立即
* ANY_ACTIVITY_CREATED //当有任何Activity创建的时候
* LAZY //懒加载,尽可能延后
*/
int whenEngineStart();
}
package com.idlefish.flutterboost.interfaces;
import com.idlefish.flutterboost.BoostChannel;
import com.idlefish.flutterboost.BoostFlutterEngine;
import com.idlefish.flutterboost.BoostFlutterView;
import io.flutter.plugin.common.PluginRegistry;
public interface IStateListener {
void onEngineCreated(BoostFlutterEngine engine);
void onEngineStarted(BoostFlutterEngine engine);
void onChannelRegistered(PluginRegistry.Registrar registrar, BoostChannel channel);
void onFlutterViewInited(BoostFlutterEngine engine, BoostFlutterView flutterView);
void beforeEngineAttach(BoostFlutterEngine engine, BoostFlutterView flutterView);
void afterEngineAttached(BoostFlutterEngine engine, BoostFlutterView flutterView);
void beforeEngineDetach(BoostFlutterEngine engine, BoostFlutterView flutterView);
void afterEngineDetached(BoostFlutterEngine engine, BoostFlutterView flutterView);
}
...@@ -25,7 +25,7 @@ apply plugin: 'com.android.application' ...@@ -25,7 +25,7 @@ apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android { android {
compileSdkVersion 26 compileSdkVersion 28
lintOptions { lintOptions {
disable 'InvalidPackage' disable 'InvalidPackage'
...@@ -35,7 +35,7 @@ android { ...@@ -35,7 +35,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.taobao.idlefish.flutterboostexample" applicationId "com.taobao.idlefish.flutterboostexample"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 26 targetSdkVersion 28
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
...@@ -58,6 +58,6 @@ dependencies { ...@@ -58,6 +58,6 @@ dependencies {
testImplementation 'junit:junit:4.12' testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
implementation 'com.android.support:support-v4:26.1.0' implementation 'com.android.support:appcompat-v7:28.0.0'
implementation 'com.android.support:appcompat-v7:26.1.0'
} }
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.taobao.idlefish.flutterboostexample"> package="com.taobao.idlefish.flutterboostexample">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
...@@ -33,24 +34,32 @@ ...@@ -33,24 +34,32 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".FlutterPageActivity" android:name="com.idlefish.flutterboost.containers.BoostFlutterActivity"
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"/> android:windowSoftInputMode="adjustResize" >
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/page_loading"/>
</activity>
<activity <activity
android:name=".FlutterFragmentPageActivity" android:name=".FlutterFragmentPageActivity"
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"/> android:windowSoftInputMode="adjustResize">
<meta-data android:name="io.flutter.embedding.android.SplashScreenDrawable" android:resource="@drawable/page_loading"/>
</activity>
<activity <activity
android:name=".NativePageActivity" android:name=".NativePageActivity"
android:theme="@style/Theme.AppCompat" android:theme="@style/Theme.AppCompat"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density"
android:windowSoftInputMode="adjustResize"/> android:windowSoftInputMode="adjustResize"/>
</application> </application>
</manifest> </manifest>
package com.taobao.idlefish.flutterboostexample;
import android.os.Bundle;
import android.support.annotation.Nullable;
import com.idlefish.flutterboost.containers.BoostFlutterFragment;
import java.util.HashMap;
import java.util.Map;
public class FlutterFragment extends BoostFlutterFragment {
@Override
public void setArguments(@Nullable Bundle args) {
if(args == null) {
args = new Bundle();
args.putString("tag","");
}
super.setArguments(args);
}
public void setTabTag(String tag) {
Bundle args = new Bundle();
args.putString("tag",tag);
super.setArguments(args);
}
@Override
public String getContainerUrl() {
return "flutterFragment";
}
@Override
public Map getContainerUrlParams() {
Map<String,String> params = new HashMap<>();
params.put("tag",getArguments().getString("tag"));
return params;
}
public static FlutterFragment instance(String tag){
FlutterFragment fragment = new FlutterFragment();
fragment.setTabTag(tag);
return fragment;
}
}
package com.taobao.idlefish.flutterboostexample; package com.taobao.idlefish.flutterboostexample;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
...@@ -9,10 +12,15 @@ import android.support.v7.app.AppCompatActivity; ...@@ -9,10 +12,15 @@ import android.support.v7.app.AppCompatActivity;
import android.view.View; import android.view.View;
import android.view.Window; import android.view.Window;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.ImageView;
import com.idlefish.flutterboost.containers.FlutterFragment;
import io.flutter.embedding.android.DrawableSplashScreen;
import io.flutter.embedding.android.SplashScreen;
import io.flutter.embedding.android.SplashScreenProvider;
import io.flutter.plugin.platform.PlatformPlugin; import io.flutter.plugin.platform.PlatformPlugin;
public class FlutterFragmentPageActivity extends AppCompatActivity implements View.OnClickListener { public class FlutterFragmentPageActivity extends AppCompatActivity implements View.OnClickListener, SplashScreenProvider {
protected static final String SPLASH_SCREEN_META_DATA_KEY = "io.flutter.embedding.android.SplashScreenDrawable";
private FlutterFragment mFragment; private FlutterFragment mFragment;
...@@ -61,16 +69,18 @@ public class FlutterFragmentPageActivity extends AppCompatActivity implements Vi ...@@ -61,16 +69,18 @@ public class FlutterFragmentPageActivity extends AppCompatActivity implements Vi
if(mTab1 == v) { if(mTab1 == v) {
mTab1.setBackgroundColor(Color.YELLOW); mTab1.setBackgroundColor(Color.YELLOW);
mFragment = FlutterFragment.instance("tab1");
mFragment= new FlutterFragment.NewEngineFragmentBuilder().url("flutterFragment").build();
}else if(mTab2 == v) { }else if(mTab2 == v) {
mTab2.setBackgroundColor(Color.YELLOW); mTab2.setBackgroundColor(Color.YELLOW);
mFragment = FlutterFragment.instance("tab2"); mFragment= new FlutterFragment.NewEngineFragmentBuilder().url("flutterFragment").build();
}else if(mTab3 == v) { }else if(mTab3 == v) {
mTab3.setBackgroundColor(Color.YELLOW); mTab3.setBackgroundColor(Color.YELLOW);
mFragment = FlutterFragment.instance("tab3"); mFragment= new FlutterFragment.NewEngineFragmentBuilder().url("flutterFragment").build();
}else{ }else{
mTab4.setBackgroundColor(Color.YELLOW); mTab4.setBackgroundColor(Color.YELLOW);
mFragment = FlutterFragment.instance("tab4"); mFragment= new FlutterFragment.NewEngineFragmentBuilder().url("flutterFragment").build();
} }
getSupportFragmentManager() getSupportFragmentManager()
...@@ -84,4 +94,34 @@ public class FlutterFragmentPageActivity extends AppCompatActivity implements Vi ...@@ -84,4 +94,34 @@ public class FlutterFragmentPageActivity extends AppCompatActivity implements Vi
super.onResume(); super.onResume();
mTab1.performClick(); mTab1.performClick();
} }
@Nullable
@Override
public SplashScreen provideSplashScreen() {
Drawable manifestSplashDrawable = getSplashScreenFromManifest();
if (manifestSplashDrawable != null) {
return new DrawableSplashScreen(manifestSplashDrawable, ImageView.ScaleType.CENTER,500L);
} else {
return null;
}
}
private Drawable getSplashScreenFromManifest() {
try {
ActivityInfo activityInfo = getPackageManager().getActivityInfo(
getComponentName(),
PackageManager.GET_META_DATA | PackageManager.GET_ACTIVITIES
);
Bundle metadata = activityInfo.metaData;
Integer splashScreenId = metadata != null ? metadata.getInt(SPLASH_SCREEN_META_DATA_KEY) : null;
return splashScreenId != null
? Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP
? getResources().getDrawable(splashScreenId, getTheme())
: getResources().getDrawable(splashScreenId)
: null;
} catch (PackageManager.NameNotFoundException e) {
// This is never expected to happen.
return null;
}
}
} }
package com.taobao.idlefish.flutterboostexample;
import com.idlefish.flutterboost.containers.BoostFlutterActivity;
import java.util.HashMap;
import java.util.Map;
public class FlutterPageActivity extends BoostFlutterActivity {
/**
* 该方法返回当前Activity在Flutter层对应的name,
* 混合栈将会在flutter层根据这个名字,在注册的Route表中查找对应的Widget
*
* 在flutter层有注册函数:
* FlutterBoost.singleton.registerPageBuilders({
* 'first': (pageName, params, _) => FirstRouteWidget(),
* 'second': (pageName, params, _) => SecondRouteWidget(),
* ...
* });
*
* 该方法中返回的就是注册的key:first , second
*
* @return
*/
@Override
public String getContainerUrl() {
return "flutterPage";
}
/**
* 该方法返回的参数将会传递给上层的flutter对应的Widget
*
* 在flutter层有注册函数:
* FlutterBoost.singleton.registerPageBuilders({
* 'first': (pageName, params, _) => FirstRouteWidget(),
* 'second': (pageName, params, _) => SecondRouteWidget(),
* ...
* });
*
* 该方法返回的参数就会封装成上面的params
*
* @return
*/
@Override
public Map getContainerUrlParams() {
Map<String,String> params = new HashMap<>();
params.put("aaa","bbb");
return params;
}
}
...@@ -45,6 +45,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe ...@@ -45,6 +45,8 @@ public class MainActivity extends AppCompatActivity implements View.OnClickListe
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Map params = new HashMap(); Map params = new HashMap();
params.put("test1","v_test1");
params.put("test2","v_test2");
//Add some params if needed. //Add some params if needed.
if (v == mOpenNative) { if (v == mOpenNative) {
PageRouter.openPageByUrl(this, PageRouter.NATIVE_PAGE_URL , params); PageRouter.openPageByUrl(this, PageRouter.NATIVE_PAGE_URL , params);
......
...@@ -3,66 +3,53 @@ package com.taobao.idlefish.flutterboostexample; ...@@ -3,66 +3,53 @@ package com.taobao.idlefish.flutterboostexample;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import com.idlefish.flutterboost.BoostChannel; import android.util.Log;
import com.idlefish.flutterboost.BoostEngineProvider; import com.idlefish.flutterboost.*;
import com.idlefish.flutterboost.BoostFlutterEngine;
import com.idlefish.flutterboost.FlutterBoost;
import com.idlefish.flutterboost.Platform;
import com.idlefish.flutterboost.interfaces.IFlutterEngineProvider;
import java.util.Map; import java.util.Map;
import io.flutter.app.FlutterApplication; import com.idlefish.flutterboost.interfaces.INativeRouter;
import io.flutter.embedding.engine.dart.DartExecutor; import io.flutter.embedding.android.FlutterView;
import io.flutter.view.FlutterMain; import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugins.GeneratedPluginRegistrant;
public class MyApplication extends Application {
public class MyApplication extends FlutterApplication {
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
FlutterBoost.init(new Platform() { INativeRouter router =new INativeRouter() {
@Override @Override
public Application getApplication() { public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
return MyApplication.this; String assembleUrl=Utils.assembleUrl(url,urlParams);
PageRouter.openPageByUrl(context,assembleUrl, urlParams);
} }
@Override };
public boolean isDebug() {
return true;
}
@Override FlutterBoost.BoostPluginsRegister pluginsRegister= new FlutterBoost.BoostPluginsRegister(){
public void openContainer(Context context, String url, Map<String, Object> urlParams, int requestCode, Map<String, Object> exts) {
PageRouter.openPageByUrl(context, url, urlParams, requestCode);
}
@Override @Override
public IFlutterEngineProvider engineProvider() { public void registerPlugins(PluginRegistry mRegistry) {
return new BoostEngineProvider() { GeneratedPluginRegistrant.registerWith(mRegistry);
@Override TextPlatformViewPlugin.register(mRegistry.registrarFor("TextPlatformViewPlugin"));
public BoostFlutterEngine createEngine(Context context) {
return new BoostFlutterEngine(context, new DartExecutor.DartEntrypoint(
context.getResources().getAssets(),
FlutterMain.findAppBundlePath(context),
"main"), "/");
} }
}; };
}
@Override Platform platform= new FlutterBoost
public int whenEngineStart() { .ConfigBuilder(this,router)
return ANY_ACTIVITY_CREATED; .isDebug(true)
} .whenEngineStart(FlutterBoost.ConfigBuilder.ANY_ACTIVITY_CREATED)
}); .renderMode(FlutterView.RenderMode.texture)
.pluginsRegister(pluginsRegister)
.build();
FlutterBoost.instance().init(platform);
BoostChannel.addActionAfterRegistered(new BoostChannel.ActionAfterRegistered() {
@Override
public void onChannelRegistered(BoostChannel channel) {
//platform view register should use FlutterPluginRegistry instread of BoostPluginRegistry
TextPlatformViewPlugin.register(FlutterBoost.singleton().engineProvider().tryGetEngine().getPluginRegistry());
}
});
} }
} }
...@@ -33,6 +33,9 @@ public class NativePageActivity extends AppCompatActivity implements View.OnClic ...@@ -33,6 +33,9 @@ public class NativePageActivity extends AppCompatActivity implements View.OnClic
@Override @Override
public void onClick(View v) { public void onClick(View v) {
Map params = new HashMap(); Map params = new HashMap();
params.put("test1","v_test1");
params.put("test2","v_test2");
if (v == mOpenNative) { if (v == mOpenNative) {
PageRouter.openPageByUrl(this, PageRouter.NATIVE_PAGE_URL,params); PageRouter.openPageByUrl(this, PageRouter.NATIVE_PAGE_URL,params);
} else if (v == mOpenFlutter) { } else if (v == mOpenFlutter) {
......
package com.taobao.idlefish.flutterboostexample; package com.taobao.idlefish.flutterboostexample;
import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.text.TextUtils; import android.util.Log;
import com.idlefish.flutterboost.containers.BoostFlutterActivity;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
public class PageRouter { public class PageRouter {
public final static Map<String, String> pageName = new HashMap<String, String>() {{
put("first", "first");
put("second", "second");
put("tab", "tab");
put("sample://flutterPage", "flutterPage");
}};
public static final String NATIVE_PAGE_URL = "sample://nativePage"; public static final String NATIVE_PAGE_URL = "sample://nativePage";
public static final String FLUTTER_PAGE_URL = "sample://flutterPage"; public static final String FLUTTER_PAGE_URL = "sample://flutterPage";
public static final String FLUTTER_FRAGMENT_PAGE_URL = "sample://flutterFragmentPage"; public static final String FLUTTER_FRAGMENT_PAGE_URL = "sample://flutterFragmentPage";
public static boolean openPageByUrl(Context context, String url,Map params) { public static boolean openPageByUrl(Context context, String url, Map params) {
return openPageByUrl(context, url,params, 0); return openPageByUrl(context, url, params, 0);
} }
public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) { public static boolean openPageByUrl(Context context, String url, Map params, int requestCode) {
String path = url.split("\\?")[0];
Log.i("openPageByUrl",path);
try { try {
if (url.startsWith(FLUTTER_PAGE_URL)) { if (pageName.containsKey(path)) {
context.startActivity(new Intent(context, FlutterPageActivity.class)); Intent intent = BoostFlutterActivity.withNewEngine().url(pageName.get(path)).params(params)
.backgroundMode(BoostFlutterActivity.BackgroundMode.opaque).build(context);
if(context instanceof Activity){
Activity activity=(Activity)context;
activity.startActivityForResult(intent,requestCode);
}else{
context.startActivity(intent);
}
return true; return true;
} else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) { } else if (url.startsWith(FLUTTER_FRAGMENT_PAGE_URL)) {
context.startActivity(new Intent(context, FlutterFragmentPageActivity.class)); context.startActivity(new Intent(context, FlutterFragmentPageActivity.class));
...@@ -27,9 +50,10 @@ public class PageRouter { ...@@ -27,9 +50,10 @@ public class PageRouter {
} else if (url.startsWith(NATIVE_PAGE_URL)) { } else if (url.startsWith(NATIVE_PAGE_URL)) {
context.startActivity(new Intent(context, NativePageActivity.class)); context.startActivity(new Intent(context, NativePageActivity.class));
return true; return true;
} else {
return false;
} }
return false;
} catch (Throwable t) { } catch (Throwable t) {
return false; return false;
} }
......
package com.taobao.idlefish.flutterboostexample; package com.taobao.idlefish.flutterboostexample;
import io.flutter.app.FlutterPluginRegistry; import io.flutter.app.FlutterPluginRegistry;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.StandardMessageCodec; import io.flutter.plugin.common.StandardMessageCodec;
public class TextPlatformViewPlugin { public class TextPlatformViewPlugin {
public static void register(FlutterPluginRegistry registry) { public static void register(PluginRegistry.Registrar registrar) {
registry.getPlatformViewsController().getRegistry().registerViewFactory("plugins.test/view", registrar.platformViewRegistry().registerViewFactory("plugins.test/view",
new TextPlatformViewFactory(StandardMessageCodec.INSTANCE)); new TextPlatformViewFactory(StandardMessageCodec.INSTANCE));
} }
} }
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#import <flutter_boost/FlutterBoost.h> #import <flutter_boost/FlutterBoost.h>
@interface AppDelegate : FLBFlutterAppDelegate <UIApplicationDelegate> @interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (nullable, nonatomic, strong) UIWindow *window;
@end @end
...@@ -3,12 +3,12 @@ ...@@ -3,12 +3,12 @@
// //
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import <flutter_boost/BoostChannel.h> #import <flutter_boost/FlutterBoostPlugin.h>
@implementation GeneratedPluginRegistrant @implementation GeneratedPluginRegistrant
+ (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry { + (void)registerWithRegistry:(NSObject<FlutterPluginRegistry>*)registry {
[BoostChannel registerWithRegistrar:[registry registrarForPlugin:@"BoostChannel"]]; [FlutterBoostPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterBoostPlugin"]];
} }
@end @end
...@@ -20,13 +20,13 @@ class _MyAppState extends State<MyApp> { ...@@ -20,13 +20,13 @@ class _MyAppState extends State<MyApp> {
'first': (pageName, params, _) => FirstRouteWidget(), 'first': (pageName, params, _) => FirstRouteWidget(),
'second': (pageName, params, _) => SecondRouteWidget(), 'second': (pageName, params, _) => SecondRouteWidget(),
'tab': (pageName, params, _) => TabRouteWidget(), 'tab': (pageName, params, _) => TabRouteWidget(),
'platformView': (pageName, params, _) => PlatformRouteWidget(),
'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params), 'flutterFragment': (pageName, params, _) => FragmentRouteWidget(params),
///可以在native层通过 getContainerParams 来传递参数 ///可以在native层通过 getContainerParams 来传递参数
'flutterPage': (pageName, params, _) { 'flutterPage': (pageName, params, _) {
print("flutterPage params:$params"); print("flutterPage params:$params");
return FlutterRouteWidget(); return FlutterRouteWidget(params:params);
}, },
}); });
} }
......
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
typedef void TextViewCreatedCallback(TextViewController controller);
class TextView extends StatefulWidget {
const TextView({
Key key,
this.onTextViewCreated,
}) : super(key: key);
final TextViewCreatedCallback onTextViewCreated;
@override
State<StatefulWidget> createState() => _TextViewState();
}
class _TextViewState extends State<TextView> {
@override
Widget build(BuildContext context) {
if (defaultTargetPlatform == TargetPlatform.android) {
return AndroidView(
viewType: 'plugins.test/view',
onPlatformViewCreated: _onPlatformViewCreated,
);
}
return Text(
'$defaultTargetPlatform is not yet supported by the text_view plugin');
}
void _onPlatformViewCreated(int id) {
if (widget.onTextViewCreated == null) {
return;
}
widget.onTextViewCreated(new TextViewController._(id));
}
}
class TextViewController {
TextViewController._(int id)
: _channel = new MethodChannel('plugins.felix.angelov/textview_$id');
final MethodChannel _channel;
Future<void> setText(String text) async {
assert(text != null);
return _channel.invokeMethod('setText', text);
}
}
\ No newline at end of file
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_boost/flutter_boost.dart'; import 'package:flutter_boost/flutter_boost.dart';
import 'package:flutter_boost_example/platform_view.dart';
class FirstRouteWidget extends StatelessWidget { class FirstRouteWidget extends StatelessWidget {
@override @override
...@@ -14,7 +16,8 @@ class FirstRouteWidget extends StatelessWidget { ...@@ -14,7 +16,8 @@ class FirstRouteWidget extends StatelessWidget {
onPressed: () { onPressed: () {
print("open second page!"); print("open second page!");
FlutterBoost.singleton.open("second").then((Map value) { FlutterBoost.singleton.open("second").then((Map value) {
print("call me when page is finished. did recieve second route result $value"); print(
"call me when page is finished. did recieve second route result $value");
}); });
}, },
), ),
...@@ -66,29 +69,81 @@ class TabRouteWidget extends StatelessWidget { ...@@ -66,29 +69,81 @@ class TabRouteWidget extends StatelessWidget {
} }
} }
class FlutterRouteWidget extends StatelessWidget { class PlatformRouteWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title:Text("Platform Route"),
),
body: Center(
child: RaisedButton(
child: TextView(),
onPressed: () {
print("open second page!");
FlutterBoost.singleton.open("second").then((Map value) {
print(
"call me when page is finished. did recieve second route result $value");
});
},
),
),
);
}
}
class FlutterRouteWidget extends StatefulWidget {
FlutterRouteWidget({this.params,this.message});
final Map params;
final String message; final String message;
FlutterRouteWidget({this.message}); @override
_FlutterRouteWidgetState createState() => _FlutterRouteWidgetState();
}
class _FlutterRouteWidgetState extends State<FlutterRouteWidget> {
final TextEditingController _usernameController = TextEditingController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String message=widget.message;
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
brightness:Brightness.light,
backgroundColor: Colors.white,
textTheme:new TextTheme(title: TextStyle(color: Colors.black)) ,
title: Text('flutter_boost_example'), title: Text('flutter_boost_example'),
), ),
body: Column( body: SingleChildScrollView(
child:Container(
margin: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Container( Container(
margin: const EdgeInsets.only(top: 80.0), margin: const EdgeInsets.only(top: 10.0,bottom: 20.0),
child: Text( child: Text(
message ?? "This is a flutter activity", message ?? "This is a flutter activity \n params:${widget.params}",
style: TextStyle(fontSize: 28.0, color: Colors.blue), style: TextStyle(fontSize: 28.0, color: Colors.blue),
), ),
alignment: AlignmentDirectional.center, alignment: AlignmentDirectional.center,
), ),
Expanded(child: Container()), // Expanded(child: Container()),
const CupertinoTextField(
prefix: Icon(
CupertinoIcons.person_solid,
color: CupertinoColors.lightBackgroundGray,
size: 28.0,
),
padding: EdgeInsets.symmetric(horizontal: 6.0, vertical: 12.0),
clearButtonMode: OverlayVisibilityMode.editing,
textCapitalization: TextCapitalization.words,
autocorrect: false,
decoration: BoxDecoration(
border: Border(bottom: BorderSide(width: 0.0, color: CupertinoColors.inactiveGray)),
),
placeholder: 'Name',
),
InkWell( InkWell(
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
...@@ -101,8 +156,59 @@ class FlutterRouteWidget extends StatelessWidget { ...@@ -101,8 +156,59 @@ class FlutterRouteWidget extends StatelessWidget {
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb ///例如:sample://nativePage?aaa=bbb
onTap: () => onTap: () => FlutterBoost.singleton
FlutterBoost.singleton.open("sample://nativePage", urlParams: { .open("sample://nativePage", urlParams: {
"query": {"aaa": "bbb"}
}),
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text(
'open first',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb
onTap: () => FlutterBoost.singleton
.open("first", urlParams: {
"query": {"aaa": "bbb"}
}),
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text(
'open second',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb
onTap: () => FlutterBoost.singleton
.open("second", urlParams: {
"query": {"aaa": "bbb"}
}),
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text(
'open tab',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb
onTap: () => FlutterBoost.singleton
.open("tab", urlParams: {
"query": {"aaa": "bbb"} "query": {"aaa": "bbb"}
}), }),
), ),
...@@ -118,8 +224,8 @@ class FlutterRouteWidget extends StatelessWidget { ...@@ -118,8 +224,8 @@ class FlutterRouteWidget extends StatelessWidget {
///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。 ///后面的参数会在native的IPlatform.startActivity方法回调中拼接到url的query部分。
///例如:sample://nativePage?aaa=bbb ///例如:sample://nativePage?aaa=bbb
onTap: () => onTap: () => FlutterBoost.singleton
FlutterBoost.singleton.open("sample://flutterPage", urlParams: { .open("sample://flutterPage", urlParams: {
"query": {"aaa": "bbb"} "query": {"aaa": "bbb"}
}), }),
), ),
...@@ -133,8 +239,23 @@ class FlutterRouteWidget extends StatelessWidget { ...@@ -133,8 +239,23 @@ class FlutterRouteWidget extends StatelessWidget {
style: TextStyle(fontSize: 22.0, color: Colors.black), style: TextStyle(fontSize: 22.0, color: Colors.black),
)), )),
onTap: () { onTap: () {
Navigator.push( Navigator.push(context,
context, MaterialPageRoute(builder: (_) => PushWidget())); MaterialPageRoute(builder: (_) => PushWidget()));
},
),
InkWell(
child: Container(
padding: const EdgeInsets.all(8.0),
margin: const EdgeInsets.all(8.0),
color: Colors.yellow,
child: Text(
'push Platform demo',
style: TextStyle(fontSize: 22.0, color: Colors.black),
)),
onTap: () {
Navigator.push(context,
MaterialPageRoute(builder: (_) => PlatformRouteWidget()));
}, },
), ),
InkWell( InkWell(
...@@ -146,11 +267,14 @@ class FlutterRouteWidget extends StatelessWidget { ...@@ -146,11 +267,14 @@ class FlutterRouteWidget extends StatelessWidget {
'open flutter fragment page', 'open flutter fragment page',
style: TextStyle(fontSize: 22.0, color: Colors.black), style: TextStyle(fontSize: 22.0, color: Colors.black),
)), )),
onTap: () => onTap: () => FlutterBoost.singleton
FlutterBoost.singleton.open("sample://flutterFragmentPage"), .open("sample://flutterFragmentPage"),
), ),
], ],
), ),
),
),
); );
} }
} }
......
...@@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN ...@@ -35,6 +35,7 @@ NS_ASSUME_NONNULL_BEGIN
- (id<FLBFlutterProvider>)flutterProvider; - (id<FLBFlutterProvider>)flutterProvider;
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform - (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
withEngine:(FlutterEngine* _Nullable)engine
onStart:(void (^)(FlutterEngine *engine))callback; onStart:(void (^)(FlutterEngine *engine))callback;
- (FlutterViewController *)flutterViewController; - (FlutterViewController *)flutterViewController;
......
...@@ -26,6 +26,7 @@ ...@@ -26,6 +26,7 @@
#import "FLBPlatform.h" #import "FLBPlatform.h"
#import "FLBTypes.h" #import "FLBTypes.h"
NS_ASSUME_NONNULL_BEGIN
@interface FlutterBoostPlugin : NSObject<FlutterPlugin> @interface FlutterBoostPlugin : NSObject<FlutterPlugin>
#pragma mark - Initializer #pragma mark - Initializer
+ (instancetype)sharedInstance; + (instancetype)sharedInstance;
...@@ -38,6 +39,16 @@ ...@@ -38,6 +39,16 @@
*/ */
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform - (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
onStart:(void (^)(FlutterEngine *engine))callback; onStart:(void (^)(FlutterEngine *engine))callback;
/**
* 初始化FlutterBoost混合栈环境。应在程序使用混合栈之前调用。如在AppDelegate中
*
* @param platform 平台层实现FLBPlatform的对象
* @param engine 外部实例化engine后传入
* @param callback 启动之后回调
*/
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
engine:(FlutterEngine* _Nullable)engine
onStart:(void (^)(FlutterEngine *engine))callback;
#pragma mark - Some properties. #pragma mark - Some properties.
- (BOOL)isRunning; - (BOOL)isRunning;
...@@ -109,3 +120,4 @@ ...@@ -109,3 +120,4 @@
onPageFinished:(void (^)(NSDictionary *))resultCallback onPageFinished:(void (^)(NSDictionary *))resultCallback
completion:(void (^)(BOOL))completion; completion:(void (^)(BOOL))completion;
@end @end
NS_ASSUME_NONNULL_END
...@@ -113,6 +113,13 @@ ...@@ -113,6 +113,13 @@
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform - (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
onStart:(void (^)(FlutterEngine *engine))callback; onStart:(void (^)(FlutterEngine *engine))callback;
{
[self startFlutterWithPlatform:platform engine:nil onStart:callback];
}
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
engine:(FlutterEngine* _Nullable)engine
onStart:(void (^)(FlutterEngine *engine))callback;
{ {
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
__weak __typeof__(self) weakSelf = self; __weak __typeof__(self) weakSelf = self;
...@@ -121,6 +128,7 @@ ...@@ -121,6 +128,7 @@
self.factory = FLBFactory.new; self.factory = FLBFactory.new;
self.application = [self->_factory createApplication:platform]; self.application = [self->_factory createApplication:platform];
[self.application startFlutterWithPlatform:platform [self.application startFlutterWithPlatform:platform
withEngine:engine
onStart:callback]; onStart:callback];
}); });
} }
......
...@@ -50,12 +50,13 @@ ...@@ -50,12 +50,13 @@
} }
- (void)startFlutterWithPlatform:(id<FLBPlatform>)platform - (void)startFlutterWithPlatform:(id<FLBPlatform>)platform
withEngine:(FlutterEngine* _Nullable)engine
onStart:(void (^)(FlutterEngine *engine))callback onStart:(void (^)(FlutterEngine *engine))callback
{ {
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
self.platform = platform; self.platform = platform;
self.viewProvider = [[FLBFlutterEngine alloc] initWithPlatform:platform]; self.viewProvider = [[FLBFlutterEngine alloc] initWithPlatform:platform engine:engine];
self.isRunning = YES; self.isRunning = YES;
if(callback) callback(self.viewProvider.engine); if(callback) callback(self.viewProvider.engine);
}); });
......
...@@ -28,6 +28,6 @@ ...@@ -28,6 +28,6 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface FLBFlutterEngine : NSObject<FLBFlutterProvider> @interface FLBFlutterEngine : NSObject<FLBFlutterProvider>
- (instancetype)initWithPlatform:(id<FLBPlatform>_Nullable)platform; - (instancetype)initWithPlatform:(id<FLBPlatform> _Nullable)platform engine:(FlutterEngine* _Nullable)engine;
@end @end
NS_ASSUME_NONNULL_END NS_ASSUME_NONNULL_END
...@@ -35,13 +35,17 @@ ...@@ -35,13 +35,17 @@
@implementation FLBFlutterEngine @implementation FLBFlutterEngine
- (instancetype)initWithPlatform:(id<FLBPlatform> _Nullable)platform - (instancetype)initWithPlatform:(id<FLBPlatform> _Nullable)platform engine:(FlutterEngine * _Nullable)engine
{ {
#pragma clang diagnostic push #pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks" #pragma clang diagnostic ignored "-Warc-performSelector-leaks"
if (self = [super init]) { if (self = [super init]) {
if(!engine){
_engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil]; _engine = [[FlutterEngine alloc] initWithName:@"io.flutter" project:nil];
}else{
_engine = engine;
}
if(platform && if(platform &&
[platform respondsToSelector: @selector(entryForDart)] && [platform respondsToSelector: @selector(entryForDart)] &&
platform.entryForDart){ platform.entryForDart){
...@@ -69,7 +73,7 @@ ...@@ -69,7 +73,7 @@
- (instancetype)init - (instancetype)init
{ {
return [self initWithPlatform:nil]; return [self initWithPlatform:nil engine:nil];
} }
- (void)pause - (void)pause
...@@ -91,14 +95,14 @@ ...@@ -91,14 +95,14 @@
- (void)didEnterBackground - (void)didEnterBackground
{ {
[BoostMessageChannel sendEvent:@"background" [BoostMessageChannel sendEvent:@"lifecycle"
arguments:nil]; arguments:@{@"type":@"background"}];
} }
- (void)willEnterForeground - (void)willEnterForeground
{ {
[BoostMessageChannel sendEvent:@"foreground" [BoostMessageChannel sendEvent:@"lifecycle"
arguments:nil]; arguments:@{@"type":@"foreground"}];
} }
- (FlutterEngine *)engine - (FlutterEngine *)engine
......
...@@ -29,6 +29,7 @@ ...@@ -29,6 +29,7 @@
NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_BEGIN
@interface FLBFlutterViewContainer : FlutterViewController<FLBFlutterContainer> @interface FLBFlutterViewContainer : FlutterViewController<FLBFlutterContainer>
@property (nonatomic,copy,readwrite) NSString *name; @property (nonatomic,copy,readwrite) NSString *name;
- (instancetype)init NS_DESIGNATED_INITIALIZER;
- (void)surfaceUpdated:(BOOL)appeared; - (void)surfaceUpdated:(BOOL)appeared;
- (void)setEnableForRunnersBatch:(BOOL)enable; - (void)setEnableForRunnersBatch:(BOOL)enable;
@end @end
......
...@@ -37,6 +37,8 @@ ...@@ -37,6 +37,8 @@
@interface FLBFlutterViewContainer () @interface FLBFlutterViewContainer ()
@property (nonatomic,strong,readwrite) NSDictionary *params; @property (nonatomic,strong,readwrite) NSDictionary *params;
@property (nonatomic,assign) long long identifier; @property (nonatomic,assign) long long identifier;
@property (nonatomic, copy) NSString *flbNibName;
@property (nonatomic, strong) NSBundle *flbNibBundle;
@end @end
#pragma clang diagnostic push #pragma clang diagnostic push
...@@ -47,13 +49,15 @@ ...@@ -47,13 +49,15 @@
{ {
[FLUTTER_APP.flutterProvider prepareEngineIfNeeded]; [FLUTTER_APP.flutterProvider prepareEngineIfNeeded];
if(self = [super initWithEngine:FLUTTER_APP.flutterProvider.engine if(self = [super initWithEngine:FLUTTER_APP.flutterProvider.engine
nibName:nil nibName:_flbNibName
bundle:nil]){ bundle:_flbNibBundle]){
[self _setup]; [self _setup];
} }
return self; return self;
} }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)initWithCoder:(NSCoder *)aDecoder{ - (instancetype)initWithCoder:(NSCoder *)aDecoder{
if (self = [super initWithCoder: aDecoder]) { if (self = [super initWithCoder: aDecoder]) {
NSAssert(NO, @"unsupported init method!"); NSAssert(NO, @"unsupported init method!");
...@@ -61,6 +65,13 @@ ...@@ -61,6 +65,13 @@
} }
return self; return self;
} }
#pragma pop
- (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
_flbNibName = nibNameOrNil;
_flbNibBundle = nibBundleOrNil;
return [self init];
}
- (void)setName:(NSString *)name params:(NSDictionary *)params - (void)setName:(NSString *)name params:(NSDictionary *)params
{ {
...@@ -149,7 +160,7 @@ static NSUInteger kInstanceCounter = 0; ...@@ -149,7 +160,7 @@ static NSUInteger kInstanceCounter = 0;
- (void)setEnableForRunnersBatch:(BOOL)enable{ - (void)setEnableForRunnersBatch:(BOOL)enable{
//dummy function //dummy function
// NSLog(@"[DEBUG]- I did nothing, I am innocent"); NSLog(@"[DEBUG]- I did nothing, I am innocent");
} }
#pragma mark - Life circle methods #pragma mark - Life circle methods
...@@ -177,7 +188,7 @@ static NSUInteger kInstanceCounter = 0; ...@@ -177,7 +188,7 @@ static NSUInteger kInstanceCounter = 0;
} }
[super viewWillAppear:animated]; [super viewWillAppear:animated];
// //instead of calling [super viewWillAppear:animated];, call super's super //instead of calling [super viewWillAppear:animated];, call super's super
// struct objc_super target = { // struct objc_super target = {
// .super_class = class_getSuperclass([FlutterViewController class]), // .super_class = class_getSuperclass([FlutterViewController class]),
// .receiver = self, // .receiver = self,
...@@ -222,7 +233,7 @@ static NSUInteger kInstanceCounter = 0; ...@@ -222,7 +233,7 @@ static NSUInteger kInstanceCounter = 0;
params:_params params:_params
uniqueId:self.uniqueIDString]; uniqueId:self.uniqueIDString];
[super viewDidDisappear:animated]; [super viewDidDisappear:animated];
//// instead of calling [super viewDidDisappear:animated];, call super's super // instead of calling [super viewDidDisappear:animated];, call super's super
// struct objc_super target = { // struct objc_super target = {
// .super_class = class_getSuperclass([FlutterViewController class]), // .super_class = class_getSuperclass([FlutterViewController class]),
// .receiver = self, // .receiver = self,
......
...@@ -39,7 +39,7 @@ class BoostPageRoute<T> extends MaterialPageRoute<T> { ...@@ -39,7 +39,7 @@ class BoostPageRoute<T> extends MaterialPageRoute<T> {
@override @override
Widget buildTransitions(BuildContext context, Animation<double> animation, Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) { Animation<double> secondaryAnimation, Widget child) {
return child; return super.buildTransitions(context, animation, secondaryAnimation, child);
} }
BoostPageRoute( BoostPageRoute(
......
...@@ -206,6 +206,18 @@ class ContainerCoordinator { ...@@ -206,6 +206,18 @@ class ContainerCoordinator {
?.pushContainer(_createContainerSettings(name, params, pageId)); ?.pushContainer(_createContainerSettings(name, params, pageId));
} }
//TODO, 需要验证android代码是否也可以移到这里
if (Platform.isIOS) {
try {
final SemanticsOwner owner =
WidgetsBinding.instance.pipelineOwner?.semanticsOwner;
final SemanticsNode root = owner?.rootSemanticsNode;
root?.detach();
root?.attach(owner);
} catch (e) {
assert(false, e.toString());
}
}
return true; return true;
} }
...@@ -254,9 +266,14 @@ class ContainerCoordinator { ...@@ -254,9 +266,14 @@ class ContainerCoordinator {
} }
bool _nativeContainerWillDealloc(String name, Map params, String pageId) { bool _nativeContainerWillDealloc(String name, Map params, String pageId) {
try{
performContainerLifeCycle(_createContainerSettings(name, params, pageId), performContainerLifeCycle(_createContainerSettings(name, params, pageId),
ContainerLifeCycle.Destroy); ContainerLifeCycle.Destroy);
} catch (e){
Logger.log(
'nativeContainerWillDealloc error: ${e}' );
}
FlutterBoost.containerManager?.remove(pageId); FlutterBoost.containerManager?.remove(pageId);
Logger.log( Logger.log(
......
...@@ -22,6 +22,7 @@ ...@@ -22,6 +22,7 @@
* THE SOFTWARE. * THE SOFTWARE.
*/ */
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'container/boost_container.dart'; import 'container/boost_container.dart';
...@@ -60,6 +61,26 @@ class FlutterBoost { ...@@ -60,6 +61,26 @@ class FlutterBoost {
PrePushRoute prePush, PrePushRoute prePush,
PostPushRoute postPush}) { PostPushRoute postPush}) {
if(Platform.isAndroid){
WidgetsBinding.instance.addPostFrameCallback((_){
singleton.channel.invokeMethod<Map>('pageOnStart').then((Map pageInfo){
if (pageInfo == null || pageInfo.isEmpty) return;
if (pageInfo.containsKey("name") &&
pageInfo.containsKey("params") &&
pageInfo.containsKey("uniqueId")) {
ContainerCoordinator.singleton.nativeContainerDidShow(
pageInfo["name"], pageInfo["params"], pageInfo["uniqueId"]);
}
});
});
}
return (BuildContext context, Widget child) { return (BuildContext context, Widget child) {
assert(child is Navigator, 'child must be Navigator, what is wrong?'); assert(child is Navigator, 'child must be Navigator, what is wrong?');
......
name: flutter_boost name: flutter_boost
description: A next-generation Flutter-Native hybrid solution. FlutterBoost is a Flutter plugin which enables hybrid integration of Flutter for your existing native apps with minimum efforts. description: A next-generation Flutter-Native hybrid solution. FlutterBoost is a Flutter plugin which enables hybrid integration of Flutter for your existing native apps with minimum efforts.
version: 0.1.54 version: 0.1.63
author: Alibaba Xianyu author: Alibaba Xianyu
homepage: https://github.com/alibaba/flutter_boost homepage: https://github.com/alibaba/flutter_boost
...@@ -19,7 +19,7 @@ dependencies: ...@@ -19,7 +19,7 @@ dependencies:
flutter: flutter:
plugin: plugin:
androidPackage: com.idlefish.flutterboost androidPackage: com.idlefish.flutterboost
pluginClass: BoostChannel pluginClass: FlutterBoostPlugin
# To add assets to your plugin package, add an assets section, like this: # To add assets to your plugin package, add an assets section, like this:
# assets: # assets:
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment