diff --git a/CHANGELOG.md b/CHANGELOG.md index 096de62..8daf06e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -# 0.4.1-pre.0 +# 0.4.1-pre.1 + +* Support Activity animation +* Support Fragment + +## 0.4.1-pre.0 * Remove Page.Hidden event * Custom FlutterActivity diff --git a/README.md b/README.md index 9ba2924..6329342 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,8 @@ |___/ ``` -![Platform](https://img.shields.io/badge/platform-ios%7Candroid-green) +![Pub_Version](https://img.shields.io/pub/v/g_faraday?style=for-the-badge) +![Platform](https://img.shields.io/badge/platform-ios%7Candroid-green?style=for-the-badge) 一个`Flutter`混合开发解决方案 @@ -25,18 +26,21 @@ _Flutter **stable channel** 发布后 **一周内**适配发布对应的`g_farad ## Features -- [x] `iOS/Android` 原生页面堆栈与`Flutter Navigator`无缝桥接 +- [x] iOS/Android原生页面堆栈与`Flutter Navigator`无缝桥接 - [x] 支持所有`Navigator`特性 -- [x] [页面间回调](doc/callback.md) -- [x] [`iOS`导航条自动隐藏/显示](doc/ios_navigation_bar.md) +- [x] [页面间回调](docs/callback.md) +- [x] [iOS导航条自动隐藏/显示](docs/ios_navigation_bar.md) +- [x] iOS完美支持`push`与`present` +- [x] Android完美支持`Activity`与`Fragment` +- [ ] Android支持`FlutterView` - [x] `WillPopScope`拦截滑动返回(ios)或者返回按键键(android) - [x] [发送/接收全局通知](doc/notification.md) - [ ] 监听页面生命周期 -- [x] 完整的文档(7/9) +- [x] 完整的文档(7/10) ## Requirements -- Flutter 1.23.0-18.1.pre *flutter channel beta* +- Flutter 1.23.0-18.1.pre `beta channel` - iOS 10.0+ Xcode 12.0+ Swift 5.1+ - Android minSdkVersion 16 Kotlin 1.4.10+ @@ -49,7 +53,7 @@ _Flutter **stable channel** 发布后 **一周内**适配发布对应的`g_farad ``` yaml dependencies: # 请确认与本地Flutter兼容的版本 - g_faraday: ^0.4.0 + g_faraday: ^0.4.1.pre.1 ``` ### Flutter 端集成 diff --git a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/Faraday.kt b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/Faraday.kt index 4aab86a..6369f84 100644 --- a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/Faraday.kt +++ b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/Faraday.kt @@ -76,6 +76,10 @@ object Faraday { CommonChannel(engine.dartExecutor, handler) } + internal fun genPageId(): Int { + return nextCode.getAndIncrement() + } + /** * The current flutter container Activity */ diff --git a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayActivity.kt b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayActivity.kt index ccf9101..ab294c8 100644 --- a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayActivity.kt +++ b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayActivity.kt @@ -2,9 +2,6 @@ package com.yuxiaor.flutter.g_faraday import android.content.Context import android.content.Intent -import android.os.Bundle -import android.os.PersistableBundle -//import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.android.XFlutterActivity import io.flutter.embedding.engine.FlutterEngine import java.io.Serializable @@ -16,35 +13,36 @@ import java.io.Serializable */ class FaradayActivity : XFlutterActivity(), ResultProvider { - private var seqId: Int? = null + private val pageId by lazy { intent.getIntExtra(ID, 0) } private var resultListener: ((requestCode: Int, resultCode: Int, data: Intent?) -> Unit)? = null companion object { - private const val ARGS_KEY = "_flutter_args" - private const val ROUTE_KEY = "_flutter_route" + private const val ID = "_flutter_id" + private const val ARGS = "_flutter_args" + private const val ROUTE = "_flutter_route" fun build(context: Context, routeName: String, params: Serializable? = null): Intent { - return Intent(context, FaradayActivity::class.java).apply { - putExtra(ROUTE_KEY, routeName) - putExtra(ARGS_KEY, params) - } + val pageId = Faraday.genPageId() + Faraday.plugin?.onPageCreate(routeName, params, pageId) + val intent = Intent(context, FaradayActivity::class.java) + intent.putExtra(ID, pageId) + intent.putExtra(ARGS, params) + intent.putExtra(ROUTE, routeName) + return intent } } override fun onAttachedToWindow() { super.onAttachedToWindow() - createFlutterPage() + Faraday.plugin?.onPageShow(pageId) } - internal fun createFlutterPage() { - val route = intent.getStringExtra(ROUTE_KEY) + internal fun buildFlutterPage() { + val route = intent.getStringExtra(ROUTE) require(route != null) { "route must not be null!" } - val args = intent.getSerializableExtra(ARGS_KEY) - Faraday.plugin?.onPageCreate(route, args, seqId) { - seqId = it - Faraday.plugin?.onPageShow(it) - } + val args = intent.getSerializableExtra(ARGS) + Faraday.plugin?.onPageCreate(route, args, pageId) } override fun provideFlutterEngine(context: Context): FlutterEngine? { @@ -60,11 +58,11 @@ class FaradayActivity : XFlutterActivity(), ResultProvider { override fun onResume() { super.onResume() - seqId?.let { Faraday.plugin?.onPageShow(it) } + Faraday.plugin?.onPageShow(pageId) } override fun onDestroy() { - seqId?.let { Faraday.plugin?.onPageDealloc(it) } + Faraday.plugin?.onPageDealloc(pageId) super.onDestroy() } @@ -77,5 +75,4 @@ class FaradayActivity : XFlutterActivity(), ResultProvider { resultListener?.invoke(requestCode, resultCode, data) resultListener = null } - } diff --git a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayFragment.kt b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayFragment.kt index 69f9c9d..787cd6f 100644 --- a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayFragment.kt +++ b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/FaradayFragment.kt @@ -1,10 +1,12 @@ package com.yuxiaor.flutter.g_faraday +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import io.flutter.embedding.android.FlutterFragment import io.flutter.embedding.android.TransparencyMode +import io.flutter.embedding.android.XFlutterFragment import io.flutter.embedding.engine.FlutterEngine /** @@ -12,66 +14,71 @@ import io.flutter.embedding.engine.FlutterEngine * Date: 2020-09-07 * Description: */ -class FaradayFragment : FlutterFragment(), ResultProvider { +class FaradayFragment : XFlutterFragment(), ResultProvider { - private var seqId: Int? = null + private val pageId by lazy { arguments?.getInt(ID) ?: 0 } private var resultListener: ((requestCode: Int, resultCode: Int, data: Intent?) -> Unit)? = null companion object { - private const val ARGS_KEY = "_flutter_args" - private const val ROUTE_KEY = "_flutter_route" + private const val ID = "_flutter_id" + private const val ARGS = "_flutter_args" + private const val ROUTE = "_flutter_route" @JvmStatic fun newInstance(routeName: String, params: HashMap? = null): FaradayFragment { + val pageId = Faraday.genPageId() + Faraday.plugin?.onPageCreate(routeName, params, pageId) val bundle = Bundle().apply { - putString(ROUTE_KEY, routeName) - putSerializable(ARGS_KEY, params) + putInt(ID, pageId) + putString(ROUTE, routeName) + putSerializable(ARGS, params) putString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.opaque.name) } return FaradayFragment().apply { arguments = bundle } } } - override fun onAttach(context: Context) { - createFlutterPage() - super.onAttach(context) + internal fun rebuild() { + val route = arguments?.getString(ROUTE) + require(route != null) { "route must not be null!" } + val args = arguments?.getSerializable(ARGS) + Faraday.plugin?.onPageCreate(route, args, pageId) } override fun provideFlutterEngine(context: Context): FlutterEngine? { return Faraday.engine } - internal fun createFlutterPage() { - val route = arguments?.getString(ROUTE_KEY) - require(route != null) { "route must not be null!" } - val args = arguments?.getSerializable(ARGS_KEY) - Faraday.plugin?.onPageCreate(route, args, seqId) { - seqId = it - Faraday.plugin?.onPageShow(it) + override fun onStart() { + super.onStart() + if (!isHidden) { + Faraday.plugin?.onPageShow(pageId) } } override fun onHiddenChanged(hidden: Boolean) { + super.onHiddenChanged(hidden) if (!hidden) { - seqId?.let { Faraday.plugin?.onPageShow(it) } + Faraday.plugin?.onPageShow(pageId) } - super.onHiddenChanged(hidden) } override fun onResume() { super.onResume() - seqId?.let { Faraday.plugin?.onPageShow(it) } + if (!isHidden) { + Faraday.plugin?.onPageShow(pageId) + } } override fun onDetach() { super.onDetach() - seqId?.let { Faraday.plugin?.onPageDealloc(it) } + Faraday.plugin?.onPageDealloc(pageId) } -// override fun shouldAttachEngineToActivity(): Boolean { -// return true -// } + override fun shouldAttachEngineToActivity(): Boolean { + return true + } override fun addResultListener(resultListener: (requestCode: Int, resultCode: Int, data: Intent?) -> Unit) { this.resultListener = resultListener diff --git a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/GFaradayPlugin.kt b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/GFaradayPlugin.kt index 3fe95eb..b4ecb25 100644 --- a/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/GFaradayPlugin.kt +++ b/android/src/main/kotlin/com/yuxiaor/flutter/g_faraday/GFaradayPlugin.kt @@ -2,7 +2,6 @@ package com.yuxiaor.flutter.g_faraday import androidx.annotation.NonNull import androidx.fragment.app.FragmentActivity -import io.flutter.embedding.engine.dart.DartExecutor import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.embedding.engine.plugins.activity.ActivityAware import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding @@ -21,7 +20,7 @@ class GFaradayPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { private var navigator: FaradayNavigator? = null internal var binding: ActivityPluginBinding? = null - private var pageCount = 0; + private var pageCount = 0; override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { when (call.method) { @@ -49,12 +48,12 @@ class GFaradayPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { "reCreateLastPage" -> { when (val activity = Faraday.getCurrentActivity()) { is FaradayActivity -> { - activity.createFlutterPage() + activity.buildFlutterPage() } is FragmentActivity -> { val fragment = activity.supportFragmentManager.fragments.first { it.isVisible } if (fragment is FaradayFragment) { - fragment.createFlutterPage() + fragment.rebuild() } } } @@ -64,17 +63,15 @@ class GFaradayPlugin : FlutterPlugin, MethodCallHandler, ActivityAware { } } - internal fun onPageCreate(route: String, args: Any?, seq: Int?, callback: (seqId: Int) -> Unit) { + internal fun onPageCreate(route: String, args: Any?, id: Int) { val data = hashMapOf() data["name"] = route if (args != null) { data["args"] = args } - data["seq"] = seq ?: -1 + data["id"] = id pageCount++ - channel.invoke("pageCreate", data) { - callback.invoke(it as Int) - } + channel.invoke("pageCreate", data) } internal fun onPageShow(seqId: Int) { diff --git a/android/src/main/kotlin/io/flutter/embedding/android/FlutterViewSnapshotSplashScreen.java b/android/src/main/kotlin/io/flutter/embedding/android/FlutterViewSnapshotSplashScreen.java new file mode 100644 index 0000000..84425bd --- /dev/null +++ b/android/src/main/kotlin/io/flutter/embedding/android/FlutterViewSnapshotSplashScreen.java @@ -0,0 +1,63 @@ +package io.flutter.embedding.android; + +import android.animation.Animator; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Bundle; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import io.flutter.embedding.engine.FlutterEngine; + +public class FlutterViewSnapshotSplashScreen implements SplashScreen { + + @NonNull private final Bitmap flutterViewSnapshot; + @Nullable private View splashView; + + public FlutterViewSnapshotSplashScreen(@NonNull FlutterEngine flutterEngine) { + flutterViewSnapshot = flutterEngine.getRenderer().getBitmap(); + } + + @Nullable + @Override + public View createSplashView(@NonNull Context context, @Nullable Bundle savedInstanceState) { + ImageView splash = new ImageView(context); + splash.setImageBitmap(flutterViewSnapshot); + splashView = splash; + return splash; + } + + @Override + public void transitionToFlutter(@NonNull final Runnable onTransitionComplete) { + if (splashView == null) { + onTransitionComplete.run(); + return; + } + + splashView + .animate() + .alpha(0.0f) + .setDuration(500) + .setListener( + new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) {} + + @Override + public void onAnimationEnd(Animator animation) { + onTransitionComplete.run(); + } + + @Override + public void onAnimationCancel(Animator animation) { + onTransitionComplete.run(); + } + + @Override + public void onAnimationRepeat(Animator animation) {} + }); + } +} diff --git a/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivity.java b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivity.java index 710e15e..944a3f3 100644 --- a/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivity.java +++ b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivity.java @@ -534,7 +534,7 @@ protected void onStart() { super.onStart(); lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START); if (delegate.isDetached()) { - delegate.reAttach(); + delegate.reattach(); } delegate.onStart(); } @@ -610,7 +610,6 @@ protected void onDestroy() { super.onDestroy(); if (stillAttachedForEvent("onDestroy")) { release(); - delegate = null; } lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY); } diff --git a/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivityAndFragmentDelegate.java b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivityAndFragmentDelegate.java index 528309d..943a307 100644 --- a/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivityAndFragmentDelegate.java +++ b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterActivityAndFragmentDelegate.java @@ -17,6 +17,7 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -31,8 +32,21 @@ import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; import io.flutter.plugin.platform.PlatformPlugin; import java.util.Arrays; +import java.util.Objects; /** + * + * Copied from FlutterActivityAndFragmentDelegate + * + * 添加了 detach 和 reattach 两个方法 + * + * 这两个方法的内部实现的方法调用顺序非常重要,不能随便更改 + * + * + * 调用顺序 会影响 Activity 和 fragment 动画 + * + * 修改不当会出现黑屏 白屏 闪屏等等 + * * Delegate that implements all Flutter logic that is the same between a {@link FlutterActivity} and * a {@link FlutterFragment}. * @@ -79,6 +93,9 @@ @Nullable private PlatformPlugin platformPlugin; private boolean isFlutterEngineFromHost; + @Nullable private FlutterViewSnapshotSplashScreen reAttachSplashScreen; + @Nullable private View reattachView; + @NonNull private final FlutterUiDisplayListener flutterUiDisplayListener = new FlutterUiDisplayListener() { @@ -108,30 +125,76 @@ public void onFlutterUiNoLongerDisplayed() { * behavior that destroys a {@link FlutterEngine} can be found in {@link #onDetach()}. */ void release() { + assert flutterEngine != null; Log.w(TAG, "flutter Engine" + flutterEngine.toString()); - this.host = null; +// this.host = null; this.flutterEngine = null; this.flutterView = null; this.platformPlugin = null; } + /** + * + * + */ void detach() { - this.onDestroyView(); - this.onDetach(); - this.platformPlugin = null; + + assert flutterView != null; + assert flutterSplashView != null; + assert flutterEngine != null; + + reAttachSplashScreen = new FlutterViewSnapshotSplashScreen(flutterEngine); + reattachView = reAttachSplashScreen.createSplashView(getAppComponent(), null); + + Log.w(TAG, "detach " + flutterView.toString()); + + if (host.shouldAttachEngineToActivity()) { + // Notify plugins that they are no longer attached to an Activity. + Log.v(TAG, "Detaching FlutterEngine from the Activity that owns this Fragment."); + if (Objects.requireNonNull(host.getActivity()).isChangingConfigurations()) { + flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges(); + } else { + flutterEngine.getActivityControlSurface().detachFromActivity(); + } + } + + // 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; + } + + flutterView.detachFromFlutterEngine(); + flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener); + + flutterEngine.getLifecycleChannel().appIsInactive(); + + flutterSplashView.addView(reattachView); } boolean isDetached() { return this.platformPlugin == null; } - void reAttach() { + void reattach() { - onAttach(host.getContext()); + assert flutterView != null; + assert flutterSplashView != null; + assert flutterEngine != null; - flutterView.attachToFlutterEngine(this.flutterEngine); + Log.i(TAG, "reattach " + flutterView.toString()); - flutterSplashView.displayFlutterViewWithSplash(flutterView, host.provideSplashScreen()); + flutterSplashView.displayFlutterViewWithSplash(flutterView, reAttachSplashScreen); + flutterSplashView.removeView(reattachView); + + onAttach(host.getContext()); + flutterView.addOnFirstFrameRenderedListener(flutterUiDisplayListener); + flutterView.attachToFlutterEngine(flutterEngine); + + flutterEngine.getLifecycleChannel().appIsResumed(); + flutterSplashView.displayFlutterViewWithSplash(flutterView, null); } /** @@ -508,6 +571,8 @@ void onDestroyView() { Log.v(TAG, "onDestroyView()"); ensureAlive(); + assert flutterView != null; + flutterView.detachFromFlutterEngine(); flutterView.removeOnFirstFrameRenderedListener(flutterUiDisplayListener); } @@ -568,12 +633,13 @@ void onDetach() { // Give the host an opportunity to cleanup any references that were created in // configureFlutterEngine(). + assert flutterEngine != null; host.cleanUpFlutterEngine(flutterEngine); if (host.shouldAttachEngineToActivity()) { // Notify plugins that they are no longer attached to an Activity. Log.v(TAG, "Detaching FlutterEngine from the Activity that owns this Fragment."); - if (host.getActivity().isChangingConfigurations()) { + if (Objects.requireNonNull(host.getActivity()).isChangingConfigurations()) { flutterEngine.getActivityControlSurface().detachFromActivityForConfigChanges(); } else { flutterEngine.getActivityControlSurface().detachFromActivity(); diff --git a/android/src/main/kotlin/io/flutter/embedding/android/XFlutterFragment.java b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterFragment.java new file mode 100644 index 0000000..04d196e --- /dev/null +++ b/android/src/main/kotlin/io/flutter/embedding/android/XFlutterFragment.java @@ -0,0 +1,1153 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.embedding.android; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.lifecycle.Lifecycle; + +import com.yuxiaor.flutter.g_faraday.Faraday; + +import io.flutter.Log; +import io.flutter.embedding.engine.FlutterEngine; +import io.flutter.embedding.engine.FlutterEngineCache; +import io.flutter.embedding.engine.FlutterShellArgs; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.embedding.engine.renderer.FlutterUiDisplayListener; +import io.flutter.plugin.platform.PlatformPlugin; + +/** + * {@code Fragment} which displays a Flutter UI that takes up all available {@code Fragment} space. + * + *

Using a {@code XFlutterFragment} requires forwarding a number of calls from an {@code Activity} + * to ensure that the internal Flutter app behaves as expected: + * + *

    + *
  1. {@link #onPostResume()} + *
  2. {@link #onBackPressed()} + *
  3. {@link #onRequestPermissionsResult(int, String[], int[])} ()} + *
  4. {@link #onNewIntent(Intent)} ()} + *
  5. {@link #onUserLeaveHint()} + *
  6. {@link #onTrimMemory(int)} + *
+ * + * Additionally, when starting an {@code Activity} for a result from this {@code Fragment}, be sure + * to invoke {@link Fragment#startActivityForResult(Intent, int)} rather than {@link + * android.app.Activity#startActivityForResult(Intent, int)}. If the {@code Activity} version of the + * method is invoked then this {@code Fragment} will never receive its {@link + * Fragment#onActivityResult(int, int, Intent)} callback. + * + *

If convenient, consider using a {@link FlutterActivity} instead of a {@code XFlutterFragment} + * to avoid the work of forwarding calls. + * + *

{@code XFlutterFragment} supports the use of an existing, cached {@link FlutterEngine}. To use + * a cached {@link FlutterEngine}, ensure that the {@link FlutterEngine} is stored in {@link + * FlutterEngineCache} and then use {@link #withCachedEngine(String)} to build a {@code + * XFlutterFragment} with the cached {@link FlutterEngine}'s ID. + * + *

It is generally recommended to use a cached {@link FlutterEngine} to avoid a momentary delay + * when initializing a new {@link FlutterEngine}. The two exceptions to using a cached {@link + * FlutterEngine} are: + * + *

+ * + *

+ * + *

The following illustrates how to pre-warm and cache a {@link FlutterEngine}: + * + *

{@code
+ * // Create and pre-warm a FlutterEngine.
+ * FlutterEngine flutterEngine = new FlutterEngine(context);
+ * flutterEngine
+ *   .getDartExecutor()
+ *   .executeDartEntrypoint(DartEntrypoint.createDefault());
+ *
+ * // Cache the pre-warmed FlutterEngine in the FlutterEngineCache.
+ * FlutterEngineCache.getInstance().put("my_engine", flutterEngine);
+ * }
+ * + *

If Flutter is needed in a location that can only use a {@code View}, consider using a {@link + * FlutterView}. Using a {@link FlutterView} requires forwarding some calls from an {@code + * Activity}, as well as forwarding lifecycle calls from an {@code Activity} or a {@code Fragment}. + */ +public class XFlutterFragment extends Fragment implements XFlutterActivityAndFragmentDelegate.Host { + private static final String TAG = "XFlutterFragment"; + + /** 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 RenderMode} to be used for the {@link FlutterView} in this {@code XFlutterFragment} */ + protected static final String ARG_FLUTTERVIEW_RENDER_MODE = "flutterview_render_mode"; + /** + * {@link TransparencyMode} to be used for the {@link FlutterView} in this {@code XFlutterFragment} + */ + 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 ID of a {@link FlutterEngine} cached in {@link FlutterEngineCache} that will be used within + * the created {@code XFlutterFragment}. + */ + protected static final String ARG_CACHED_ENGINE_ID = "cached_engine_id"; + /** + * True if the {@link FlutterEngine} in the created {@code XFlutterFragment} should be destroyed + * when the {@code XFlutterFragment} is destroyed, false if the {@link FlutterEngine} should + * outlive the {@code XFlutterFragment}. + */ + protected static final String ARG_DESTROY_ENGINE_WITH_FRAGMENT = "destroy_engine_with_fragment"; + /** + * True if the framework state in the engine attached to this engine should be stored and restored + * when this fragment is created and destroyed. + */ + protected static final String ARG_ENABLE_STATE_RESTORATION = "enable_state_restoration"; + + /** + * Creates a {@code XFlutterFragment} with a default configuration. + * + *

{@code XFlutterFragment}'s default configuration creates a new {@link FlutterEngine} within + * the {@code XFlutterFragment} and uses the following settings: + * + *

+ * + *

To use a new {@link FlutterEngine} with different settings, use {@link #withNewEngine()}. + * + *

To use a cached {@link FlutterEngine} instead of creating a new one, use {@link + * #withCachedEngine(String)}. + */ + @NonNull + public static XFlutterFragment createDefault() { + return new NewEngineFragmentBuilder().build(); + } + + /** + * Returns a {@link NewEngineFragmentBuilder} to create a {@code XFlutterFragment} with a new + * {@link FlutterEngine} and a desired engine configuration. + */ + @NonNull + public static NewEngineFragmentBuilder withNewEngine() { + return new NewEngineFragmentBuilder(); + } + + /** + * Builder that creates a new {@code XFlutterFragment} with {@code arguments} that correspond to + * the values set on this {@code NewEngineFragmentBuilder}. + * + *

To create a {@code XFlutterFragment} with default {@code arguments}, invoke {@link + * #createDefault()}. + * + *

Subclasses of {@code XFlutterFragment} that do not introduce any new arguments can use this + * {@code NewEngineFragmentBuilder} to construct instances of the subclass without subclassing + * this {@code NewEngineFragmentBuilder}. {@code MyXFlutterFragment f = new + * XFlutterFragment.NewEngineFragmentBuilder(MyXFlutterFragment.class) .someProperty(...) + * .someOtherProperty(...) .build(); } + * + *

Subclasses of {@code XFlutterFragment} that introduce new arguments should subclass this + * {@code NewEngineFragmentBuilder} to add the new properties: + * + *

    + *
  1. Ensure the {@code XFlutterFragment} subclass has a no-arg constructor. + *
  2. Subclass this {@code NewEngineFragmentBuilder}. + *
  3. Override the new {@code NewEngineFragmentBuilder}'s no-arg constructor and invoke the + * super constructor to set the {@code XFlutterFragment} subclass: {@code public MyBuilder() + * { super(MyXFlutterFragment.class); } } + *
  4. Add appropriate property methods for the new properties. + *
  5. Override {@link NewEngineFragmentBuilder#createArgs()}, call through to the super method, + * then add the new properties as arguments in the {@link Bundle}. + *
+ * + * Once a {@code NewEngineFragmentBuilder} subclass is defined, the {@code XFlutterFragment} + * subclass can be instantiated as follows. {@code MyXFlutterFragment f = new MyBuilder() + * .someExistingProperty(...) .someNewProperty(...) .build(); } + */ + public static class NewEngineFragmentBuilder { + private final Class fragmentClass; + private String dartEntrypoint = "main"; + private String initialRoute = "/"; + private String appBundlePath = null; + private FlutterShellArgs shellArgs = null; + private RenderMode renderMode = RenderMode.surface; + private TransparencyMode transparencyMode = TransparencyMode.transparent; + private boolean shouldAttachEngineToActivity = true; + + /** + * Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of + * {@code XFlutterFragment}. + */ + public NewEngineFragmentBuilder() { + fragmentClass = XFlutterFragment.class; + } + + /** + * Constructs a {@code NewEngineFragmentBuilder} that is configured to construct an instance of + * {@code subclass}, which extends {@code XFlutterFragment}. + */ + public NewEngineFragmentBuilder(@NonNull Class subclass) { + fragmentClass = subclass; + } + + /** The name of the initial Dart method to invoke, defaults to "main". */ + @NonNull + public NewEngineFragmentBuilder dartEntrypoint(@NonNull String dartEntrypoint) { + this.dartEntrypoint = dartEntrypoint; + return this; + } + + /** + * The initial route that a Flutter app will render in this {@link XFlutterFragment}, defaults to + * "/". + */ + @NonNull + public NewEngineFragmentBuilder initialRoute(@NonNull String initialRoute) { + this.initialRoute = initialRoute; + return this; + } + + /** + * The path to the app bundle which contains the Dart app to execute. Null when unspecified, + * which defaults to {@link FlutterLoader#findAppBundlePath()} + */ + @NonNull + public NewEngineFragmentBuilder appBundlePath(@NonNull String appBundlePath) { + this.appBundlePath = appBundlePath; + return this; + } + + /** 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 RenderMode#surface} or a {@link 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 RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + /** + * Support a {@link TransparencyMode#transparent} background within {@link FlutterView}, or + * force an {@link TransparencyMode#opaque} background. + * + *

See {@link TransparencyMode} for implications of this selection. + */ + @NonNull + public NewEngineFragmentBuilder transparencyMode(@NonNull TransparencyMode transparencyMode) { + this.transparencyMode = transparencyMode; + return this; + } + + /** + * Whether or not this {@code XFlutterFragment} should automatically attach its {@code Activity} + * as a control surface for its {@link FlutterEngine}. + * + *

Control surfaces are used to provide Android resources and lifecycle events to plugins + * that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity} is + * true then this {@code XFlutterFragment} will connect its {@link FlutterEngine} to the + * surrounding {@code Activity}, along with any plugins that are registered with that {@link + * FlutterEngine}. This allows plugins to access the {@code Activity}, as well as receive {@code + * Activity}-specific calls, e.g., {@link android.app.Activity#onNewIntent(Intent)}. If {@code + * shouldAttachEngineToActivity} is false, then this {@code XFlutterFragment} will not + * automatically manage the connection between its {@link FlutterEngine} and the surrounding + * {@code Activity}. The {@code Activity} will need to be manually connected to this {@code + * XFlutterFragment}'s {@link FlutterEngine} by the app developer. See {@link + * FlutterEngine#getActivityControlSurface()}. + * + *

One reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to move the {@link + * FlutterEngine} somewhere else. For example, a developer might want the {@link FlutterEngine} + * to outlive the surrounding {@code Activity} so that it can be used later in a different + * {@code Activity}. To accomplish this, the {@link FlutterEngine} will need to be disconnected + * from the surrounding {@code Activity} at an unusual time, preventing this {@code + * XFlutterFragment} from correctly managing the relationship between the {@link FlutterEngine} + * and the surrounding {@code Activity}. + * + *

Another reason that a developer might choose to manually manage the relationship between + * the {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or + * explicitly control when the {@link FlutterEngine}'s plugins have access to the surrounding + * {@code Activity}. For example, imagine that this {@code XFlutterFragment} only takes up part + * of the screen and the app developer wants to ensure that none of the Flutter plugins are able + * to manipulate the surrounding {@code Activity}. In this case, the developer would not want + * the {@link FlutterEngine} to have access to the {@code Activity}, which can be accomplished + * by setting {@code shouldAttachEngineToActivity} to {@code false}. + */ + @NonNull + public NewEngineFragmentBuilder shouldAttachEngineToActivity( + boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Creates a {@link Bundle} of arguments that are assigned to the new {@code XFlutterFragment}. + * + *

Subclasses should override this method to add new properties to the {@link Bundle}. + * Subclasses must call through to the super method to collect all existing property values. + */ + @NonNull + protected Bundle createArgs() { + Bundle args = new Bundle(); + args.putString(ARG_INITIAL_ROUTE, initialRoute); + args.putString(ARG_APP_BUNDLE_PATH, appBundlePath); + args.putString(ARG_DART_ENTRYPOINT, dartEntrypoint); + // TODO(mattcarroll): determine if we should have an explicit FlutterTestFragment instead of + // conflating. + if (null != shellArgs) { + args.putStringArray(ARG_FLUTTER_INITIALIZATION_ARGS, shellArgs.toArray()); + } + args.putString( + ARG_FLUTTERVIEW_RENDER_MODE, + renderMode != null ? renderMode.name() : RenderMode.surface.name()); + args.putString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); + return args; + } + + /** + * Constructs a new {@code XFlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code Builder}. + */ + @NonNull + public T build() { + try { + @SuppressWarnings("unchecked") + T frag = (T) fragmentClass.getDeclaredConstructor().newInstance(); + if (frag == null) { + throw new RuntimeException( + "The XFlutterFragment 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 XFlutterFragment subclass (" + fragmentClass.getName() + ")", e); + } + } + } + + /** + * Returns a {@link CachedEngineFragmentBuilder} to create a {@code XFlutterFragment} with a cached + * {@link FlutterEngine} in {@link FlutterEngineCache}. + * + *

An {@code IllegalStateException} will be thrown during the lifecycle of the {@code + * XFlutterFragment} if a cached {@link FlutterEngine} is requested but does not exist in the + * cache. + * + *

To create a {@code XFlutterFragment} that uses a new {@link FlutterEngine}, use {@link + * #createDefault()} or {@link #withNewEngine()}. + */ + @NonNull + public static CachedEngineFragmentBuilder withCachedEngine(@NonNull String engineId) { + return new CachedEngineFragmentBuilder(engineId); + } + + /** + * Builder that creates a new {@code XFlutterFragment} that uses a cached {@link FlutterEngine} + * with {@code arguments} that correspond to the values set on this {@code Builder}. + * + *

Subclasses of {@code XFlutterFragment} that do not introduce any new arguments can use this + * {@code Builder} to construct instances of the subclass without subclassing this {@code + * Builder}. {@code MyXFlutterFragment f = new + * XFlutterFragment.CachedEngineFragmentBuilder(MyXFlutterFragment.class) .someProperty(...) + * .someOtherProperty(...) .build(); } + * + *

Subclasses of {@code XFlutterFragment} that introduce new arguments should subclass this + * {@code CachedEngineFragmentBuilder} to add the new properties: + * + *

    + *
  1. Ensure the {@code XFlutterFragment} subclass has a no-arg constructor. + *
  2. Subclass this {@code CachedEngineFragmentBuilder}. + *
  3. Override the new {@code CachedEngineFragmentBuilder}'s no-arg constructor and invoke the + * super constructor to set the {@code XFlutterFragment} subclass: {@code public MyBuilder() + * { super(MyXFlutterFragment.class); } } + *
  4. Add appropriate property methods for the new properties. + *
  5. Override {@link CachedEngineFragmentBuilder#createArgs()}, call through to the super + * method, then add the new properties as arguments in the {@link Bundle}. + *
+ * + * Once a {@code CachedEngineFragmentBuilder} subclass is defined, the {@code XFlutterFragment} + * subclass can be instantiated as follows. {@code MyXFlutterFragment f = new MyBuilder() + * .someExistingProperty(...) .someNewProperty(...) .build(); } + */ + public static class CachedEngineFragmentBuilder { + private final Class fragmentClass; + private final String engineId; + private boolean destroyEngineWithFragment = false; + private RenderMode renderMode = RenderMode.surface; + private TransparencyMode transparencyMode = TransparencyMode.transparent; + private boolean shouldAttachEngineToActivity = true; + + private CachedEngineFragmentBuilder(@NonNull String engineId) { + this(XFlutterFragment.class, engineId); + } + + protected CachedEngineFragmentBuilder( + @NonNull Class subclass, @NonNull String engineId) { + this.fragmentClass = subclass; + this.engineId = engineId; + } + + /** + * Pass {@code true} to destroy the cached {@link FlutterEngine} when this {@code + * XFlutterFragment} is destroyed, or {@code false} for the cached {@link FlutterEngine} to + * outlive this {@code XFlutterFragment}. + */ + @NonNull + public CachedEngineFragmentBuilder destroyEngineWithFragment( + boolean destroyEngineWithFragment) { + this.destroyEngineWithFragment = destroyEngineWithFragment; + return this; + } + + /** + * Render Flutter either as a {@link RenderMode#surface} or a {@link 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 CachedEngineFragmentBuilder renderMode(@NonNull RenderMode renderMode) { + this.renderMode = renderMode; + return this; + } + + /** + * Support a {@link TransparencyMode#transparent} background within {@link FlutterView}, or + * force an {@link TransparencyMode#opaque} background. + * + *

See {@link TransparencyMode} for implications of this selection. + */ + @NonNull + public CachedEngineFragmentBuilder transparencyMode( + @NonNull TransparencyMode transparencyMode) { + this.transparencyMode = transparencyMode; + return this; + } + + /** + * Whether or not this {@code XFlutterFragment} should automatically attach its {@code Activity} + * as a control surface for its {@link FlutterEngine}. + * + *

Control surfaces are used to provide Android resources and lifecycle events to plugins + * that are attached to the {@link FlutterEngine}. If {@code shouldAttachEngineToActivity} is + * true then this {@code XFlutterFragment} will connect its {@link FlutterEngine} to the + * surrounding {@code Activity}, along with any plugins that are registered with that {@link + * FlutterEngine}. This allows plugins to access the {@code Activity}, as well as receive {@code + * Activity}-specific calls, e.g., {@link android.app.Activity#onNewIntent(Intent)}. If {@code + * shouldAttachEngineToActivity} is false, then this {@code XFlutterFragment} will not + * automatically manage the connection between its {@link FlutterEngine} and the surrounding + * {@code Activity}. The {@code Activity} will need to be manually connected to this {@code + * XFlutterFragment}'s {@link FlutterEngine} by the app developer. See {@link + * FlutterEngine#getActivityControlSurface()}. + * + *

One reason that a developer might choose to manually manage the relationship between the + * {@code Activity} and {@link FlutterEngine} is if the developer wants to move the {@link + * FlutterEngine} somewhere else. For example, a developer might want the {@link FlutterEngine} + * to outlive the surrounding {@code Activity} so that it can be used later in a different + * {@code Activity}. To accomplish this, the {@link FlutterEngine} will need to be disconnected + * from the surrounding {@code Activity} at an unusual time, preventing this {@code + * XFlutterFragment} from correctly managing the relationship between the {@link FlutterEngine} + * and the surrounding {@code Activity}. + * + *

Another reason that a developer might choose to manually manage the relationship between + * the {@code Activity} and {@link FlutterEngine} is if the developer wants to prevent, or + * explicitly control when the {@link FlutterEngine}'s plugins have access to the surrounding + * {@code Activity}. For example, imagine that this {@code XFlutterFragment} only takes up part + * of the screen and the app developer wants to ensure that none of the Flutter plugins are able + * to manipulate the surrounding {@code Activity}. In this case, the developer would not want + * the {@link FlutterEngine} to have access to the {@code Activity}, which can be accomplished + * by setting {@code shouldAttachEngineToActivity} to {@code false}. + */ + @NonNull + public CachedEngineFragmentBuilder shouldAttachEngineToActivity( + boolean shouldAttachEngineToActivity) { + this.shouldAttachEngineToActivity = shouldAttachEngineToActivity; + return this; + } + + /** + * Creates a {@link Bundle} of arguments that are assigned to the new {@code XFlutterFragment}. + * + *

Subclasses should override this method to add new properties to the {@link Bundle}. + * Subclasses must call through to the super method to collect all existing property values. + */ + @NonNull + protected Bundle createArgs() { + Bundle args = new Bundle(); + args.putString(ARG_CACHED_ENGINE_ID, engineId); + args.putBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, destroyEngineWithFragment); + args.putString( + ARG_FLUTTERVIEW_RENDER_MODE, + renderMode != null ? renderMode.name() : RenderMode.surface.name()); + args.putString( + ARG_FLUTTERVIEW_TRANSPARENCY_MODE, + transparencyMode != null ? transparencyMode.name() : TransparencyMode.transparent.name()); + args.putBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY, shouldAttachEngineToActivity); + return args; + } + + /** + * Constructs a new {@code XFlutterFragment} (or a subclass) that is configured based on + * properties set on this {@code CachedEngineFragmentBuilder}. + */ + @NonNull + public T build() { + try { + @SuppressWarnings("unchecked") + T frag = (T) fragmentClass.getDeclaredConstructor().newInstance(); + if (frag == null) { + throw new RuntimeException( + "The XFlutterFragment 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 XFlutterFragment subclass (" + fragmentClass.getName() + ")", e); + } + } + } + + // Delegate that runs all lifecycle and OS hook logic that is common between + // FlutterActivity and XFlutterFragment. See the FlutterActivityAndFragmentDelegate + // implementation for details about why it exists. + @VisibleForTesting /* package */ XFlutterActivityAndFragmentDelegate delegate; + + public XFlutterFragment() { + // 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()); + } + + /** + * This method exists so that JVM tests can ensure that a delegate exists without putting this + * Fragment through any lifecycle events, because JVM tests cannot handle executing any lifecycle + * methods, at the time of writing this. + * + *

The testing infrastructure should be upgraded to make XFlutterFragment tests easy to write + * while exercising real lifecycle methods. At such a time, this method should be removed. + */ + // TODO(mattcarroll): remove this when tests allow for it + // (https://github.com/flutter/flutter/issues/43798) + @VisibleForTesting + /* package */ void setDelegate(@NonNull XFlutterActivityAndFragmentDelegate delegate) { + this.delegate = delegate; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + delegate = new XFlutterActivityAndFragmentDelegate(this); + delegate.onAttach(context); + } + + @Nullable + @Override + public View onCreateView( + LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + +// FrameLayout parentLayout = new FrameLayout(getContext()); +// parentLayout.setLayoutParams(new ViewGroup.LayoutParams(-1, -1)); +// parentLayout.setBackgroundColor(Color.YELLOW); + + return delegate.onCreateView(inflater, container, savedInstanceState); + +// ViewGroup.LayoutParams lp = new ViewGroup.LayoutParams(1000, 1500); +// layout.setLayoutParams(lp); +// +// parentLayout.addView(layout); +// +// TextView textView = new TextView(getContext()); +// textView.setText(layout.toString()); +// textView.setTextColor(Color.RED); +// +// FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(-2, -2); +// flp.gravity = Gravity.CENTER_VERTICAL; +// textView.setLayoutParams(flp); +// +// parentLayout.addView(textView); +// +// return parentLayout; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); +// delegate.onActivityCreated(savedInstanceState); + } + + @Override + public void onStart() { + super.onStart(); + if (!isHidden()) { + delegate.onStart(); + } + } + + private void reattachIfNeeded() { + if (!isHidden()) { + if (delegate.isDetached()) { + delegate.reattach(); + } else { + delegate.onResume(); + } + } + } + + @Override + public void onResume() { + super.onResume(); + reattachIfNeeded(); + } + + @Override + public void onHiddenChanged(boolean hidden) { + super.onHiddenChanged(hidden); + reattachIfNeeded(); + } + + // TODO(mattcarroll): determine why this can't be in onResume(). Comment reason, or move if + // possible. + @ActivityCallThrough + public void onPostResume() { + if(!isHidden()) { + delegate.onPostResume(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (!isHidden()) { + delegate.onPause(); + } + } + + @Override + public void onStop() { + super.onStop(); + if (!isHidden() && stillAttachedForEvent("onStop")) { + delegate.onStop(); + } + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + if (stillAttachedForEvent("onDestroyView")) { + delegate.onDestroyView(); + } + } + + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (!isHidden() && stillAttachedForEvent("onSaveInstanceState")) { + delegate.onSaveInstanceState(outState); + } + } + + @Override + public void detachFromFlutterEngine() { + Log.v( + TAG, + "XFlutterFragment " + + this + + " connection to the engine " + + getFlutterEngine() + + " evicted by another attaching activity"); + // Redundant calls are ok. + delegate.detach(); + } + + private void release() { + delegate.onDestroyView(); + delegate.onDetach(); + delegate.release(); + delegate = null; + } + + @Override + public void onDetach() { + super.onDetach(); + if (delegate != null) { + release(); + } else { + Log.v(TAG, "XFlutterFragment " + this + " onDetach called after release."); + } + } + + /** + * The result of a permission request has been received. + * + *

See {@link android.app.Activity#onRequestPermissionsResult(int, String[], int[])} + * + *

+ * + * @param requestCode identifier passed with the initial permission request + * @param permissions permissions that were requested + * @param grantResults permission grants or denials + */ + @ActivityCallThrough + public void onRequestPermissionsResult( + int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + if (stillAttachedForEvent("onRequestPermissionsResult")) { + delegate.onRequestPermissionsResult(requestCode, permissions, grantResults); + } + } + + /** + * A new Intent was received by the {@link android.app.Activity} that currently owns this {@link + * Fragment}. + * + *

See {@link android.app.Activity#onNewIntent(Intent)} + * + *

+ * + * @param intent new Intent + */ + @ActivityCallThrough + public void onNewIntent(@NonNull Intent intent) { + if (stillAttachedForEvent("onNewIntent")) { + delegate.onNewIntent(intent); + } + } + + /** + * The hardware back button was pressed. + * + *

See {@link android.app.Activity#onBackPressed()} + */ + @ActivityCallThrough + public void onBackPressed() { + if (!isHidden() && stillAttachedForEvent("onBackPressed")) { + delegate.onBackPressed(); + } + } + + /** + * A result has been returned after an invocation of {@link + * Fragment#startActivityForResult(Intent, int)}. + * + *

+ * + * @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) { + if (stillAttachedForEvent("onActivityResult")) { + delegate.onActivityResult(requestCode, resultCode, data); + } + } + + /** + * The {@link android.app.Activity} that owns this {@link Fragment} is about to go to the + * background as the result of a user's choice/action, i.e., not as the result of an OS decision. + * + *

See {@link android.app.Activity#onUserLeaveHint()} + */ + @ActivityCallThrough + public void onUserLeaveHint() { + if (stillAttachedForEvent("onUserLeaveHint")) { + delegate.onUserLeaveHint(); + } + } + + /** + * Callback invoked when memory is low. + * + *

This implementation forwards a memory pressure warning to the running Flutter app. + * + *

+ * + * @param level level + */ + @ActivityCallThrough + public void onTrimMemory(int level) { + if (stillAttachedForEvent("onTrimMemory")) { + delegate.onTrimMemory(level); + } + } + + /** + * Callback invoked when memory is low. + * + *

This implementation forwards a memory pressure warning to the running Flutter app. + */ + @Override + public void onLowMemory() { + super.onLowMemory(); + if (stillAttachedForEvent("onLowMemory")) { + delegate.onLowMemory(); + } + } + + @Nullable + @Override + public boolean shouldHandleDeeplinking() { + return false; + } + + /** + * {@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 ID of a statically cached {@link FlutterEngine} to use within this {@code + * XFlutterFragment}, or {@code null} if this {@code XFlutterFragment} does not want to use a cached + * {@link FlutterEngine}. + */ + @Nullable + @Override + public String getCachedEngineId() { + return getArguments().getString(ARG_CACHED_ENGINE_ID, null); + } + + /** + * Returns false if the {@link FlutterEngine} within this {@code XFlutterFragment} should outlive + * the {@code XFlutterFragment}, itself. + * + *

Defaults to true if no custom {@link FlutterEngine is provided}, false if a custom {@link + * FlutterEngine} is provided. + */ + @Override + public boolean shouldDestroyEngineWithHost() { + boolean explicitDestructionRequested = + getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, false); + if (getCachedEngineId() != null || delegate.isFlutterEngineFromHost()) { + // Only destroy a cached engine if explicitly requested by app developer. + return explicitDestructionRequested; + } else { + // If this Fragment created the FlutterEngine, destroy it by default unless + // explicitly requested not to. + return getArguments().getBoolean(ARG_DESTROY_ENGINE_WITH_FRAGMENT, true); + } + } + + /** + * Returns the name of the Dart method that this {@code XFlutterFragment} should execute to start a + * Flutter app. + * + *

Defaults to "main". + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public String getDartEntrypointFunctionName() { + return getArguments().getString(ARG_DART_ENTRYPOINT, "main"); + } + + /** + * A custom path to the bundle that contains this Flutter app's resources, e.g., Dart code + * snapshots. + * + *

When unspecified, the value is null, which defaults to the app bundle path defined in {@link + * FlutterLoader#findAppBundlePath()}. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public String getAppBundlePath() { + return getArguments().getString(ARG_APP_BUNDLE_PATH); + } + + /** + * Returns the initial route that should be rendered within Flutter, once the Flutter app starts. + * + *

Defaults to {@code null}, which signifies a route of "/" in Flutter. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @Nullable + public String getInitialRoute() { + return getArguments().getString(ARG_INITIAL_ROUTE); + } + + /** + * Returns the desired {@link RenderMode} for the {@link FlutterView} displayed in this {@code + * XFlutterFragment}. + * + *

Defaults to {@link RenderMode#surface}. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public RenderMode getRenderMode() { + String renderModeName = + getArguments().getString(ARG_FLUTTERVIEW_RENDER_MODE, RenderMode.surface.name()); + return RenderMode.valueOf(renderModeName); + } + + /** + * Returns the desired {@link TransparencyMode} for the {@link FlutterView} displayed in this + * {@code XFlutterFragment}. + * + *

Defaults to {@link TransparencyMode#transparent}. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @NonNull + public TransparencyMode getTransparencyMode() { + String transparencyModeName = + getArguments() + .getString(ARG_FLUTTERVIEW_TRANSPARENCY_MODE, TransparencyMode.transparent.name()); + return TransparencyMode.valueOf(transparencyModeName); + } + + @Override + @Nullable + public SplashScreen provideSplashScreen() { + FragmentActivity parentActivity = getActivity(); + if (parentActivity instanceof SplashScreenProvider) { + SplashScreenProvider splashScreenProvider = (SplashScreenProvider) parentActivity; + return splashScreenProvider.provideSplashScreen(); + } + + return null; + } + + /** + * Hook for subclasses to return a {@link FlutterEngine} with whatever configuration is desired. + * + *

By default this method defers to this {@code XFlutterFragment}'s surrounding {@code + * Activity}, if that {@code Activity} implements {@link FlutterEngineProvider}. If this method is + * overridden, the surrounding {@code Activity} will no longer be given an opportunity to provide + * a {@link FlutterEngine}, unless the subclass explicitly implements that behavior. + * + *

Consider returning a cached {@link FlutterEngine} instance from this method to avoid the + * typical warm-up time that a new {@link FlutterEngine} instance requires. + * + *

If null is returned then a new default {@link FlutterEngine} will be created to back this + * {@code XFlutterFragment}. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + @Nullable + public FlutterEngine provideFlutterEngine(@NonNull Context context) { + // Defer to the FragmentActivity that owns us to see if it wants to provide a + // FlutterEngine. + FlutterEngine flutterEngine = null; + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterEngineProvider) { + // Defer to the Activity that owns us to provide a FlutterEngine. + Log.v(TAG, "Deferring to attached Activity to provide a FlutterEngine."); + FlutterEngineProvider flutterEngineProvider = (FlutterEngineProvider) attachedActivity; + flutterEngine = flutterEngineProvider.provideFlutterEngine(getContext()); + } + + return flutterEngine; + } + + /** + * 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. + * + *

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( + * ExclusiveAppComponent, Lifecycle)}. + * + *

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. + * + *

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. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterEngineConfigurator) { + ((FlutterEngineConfigurator) attachedActivity).configureFlutterEngine(flutterEngine); + } + } + + /** + * Hook for the host to cleanup references that were established in {@link + * #configureFlutterEngine(FlutterEngine)} before the host is destroyed or detached. + * + *

This method is called in {@link #onDetach()}. + */ + @Override + public void cleanUpFlutterEngine(@NonNull FlutterEngine flutterEngine) { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterEngineConfigurator) { + ((FlutterEngineConfigurator) attachedActivity).cleanUpFlutterEngine(flutterEngine); + } + } + + /** + * See {@link NewEngineFragmentBuilder#shouldAttachEngineToActivity()} and {@link + * CachedEngineFragmentBuilder#shouldAttachEngineToActivity()}. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate} + */ + @Override + public boolean shouldAttachEngineToActivity() { + return getArguments().getBoolean(ARG_SHOULD_ATTACH_ENGINE_TO_ACTIVITY); + } + + @Override + public void onFlutterSurfaceViewCreated(@NonNull FlutterSurfaceView flutterSurfaceView) { + // Hook for subclasses. + } + + @Override + public void onFlutterTextureViewCreated(@NonNull FlutterTextureView flutterTextureView) { + // Hook for subclasses. + } + + /** + * Invoked after the {@link FlutterView} within this {@code XFlutterFragment} starts rendering + * pixels to the screen. + * + *

This method forwards {@code onFlutterUiDisplayed()} to its attached {@code Activity}, if the + * attached {@code Activity} implements {@link FlutterUiDisplayListener}. + * + *

Subclasses that override this method must call through to the {@code super} method. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + public void onFlutterUiDisplayed() { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterUiDisplayListener) { + ((FlutterUiDisplayListener) attachedActivity).onFlutterUiDisplayed(); + } + } + + /** + * Invoked after the {@link FlutterView} within this {@code XFlutterFragment} stops rendering + * pixels to the screen. + * + *

This method forwards {@code onFlutterUiNoLongerDisplayed()} to its attached {@code + * Activity}, if the attached {@code Activity} implements {@link FlutterUiDisplayListener}. + * + *

Subclasses that override this method must call through to the {@code super} method. + * + *

Used by this {@code XFlutterFragment}'s {@link FlutterActivityAndFragmentDelegate.Host} + */ + @Override + public void onFlutterUiNoLongerDisplayed() { + FragmentActivity attachedActivity = getActivity(); + if (attachedActivity instanceof FlutterUiDisplayListener) { + ((FlutterUiDisplayListener) attachedActivity).onFlutterUiNoLongerDisplayed(); + } + } + + @Override + public boolean shouldRestoreAndSaveState() { + if (getArguments().containsKey(ARG_ENABLE_STATE_RESTORATION)) { + return getArguments().getBoolean(ARG_ENABLE_STATE_RESTORATION); + } + if (getCachedEngineId() != null) { + return false; + } + return true; + } + + private boolean stillAttachedForEvent(String event) { + if (delegate.isDetached()) { + Log.v(TAG, "XFlutterFragment " + hashCode() + " " + event + " called after release."); + return false; + } + return true; + } + + /** + * Annotates methods in {@code XFlutterFragment} that must be called by the containing {@code + * Activity}. + */ + @interface ActivityCallThrough {} +} diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index a4d2286..c19ae69 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -28,7 +28,7 @@ android:scheme="native" /> - + +//*

  • {@link #onPostResume()} +//*
  • {@link #onBackPressed()} +//*
  • {@link #onRequestPermissionsResult(int, String[], int[])} ()} +//*
  • {@link #onNewIntent(Intent)} ()} +//*
  • {@link #onUserLeaveHint()} +//*
  • {@link #onTrimMemory(int)} +//* + + class FragActivity : AppCompatActivity() { private var tempFragment: Fragment? = null @@ -41,15 +55,57 @@ class FragActivity : AppCompatActivity() { private fun switchFragment(fragment: Fragment, tag: String) { if (tempFragment == fragment) return - val transaction = supportFragmentManager.beginTransaction() if (!fragment.isAdded) { transaction.add(R.id.frag, fragment, tag) } - transaction.show(fragment) tempFragment?.let { transaction.hide(it) } tempFragment = fragment transaction.commitNow() } + + override fun onTrimMemory(level: Int) { + super.onTrimMemory(level) + if (tempFragment is FaradayFragment) { + (tempFragment as FaradayFragment).onTrimMemory(level) + } + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (tempFragment is FaradayFragment) { + (tempFragment as FaradayFragment).onUserLeaveHint() + } + } + + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + if (tempFragment is FaradayFragment && intent != null) { + (tempFragment as FaradayFragment).onNewIntent(intent) + } + } + + override fun onPostResume() { + super.onPostResume() + if (tempFragment is FaradayFragment) { + (tempFragment as FaradayFragment).onPostResume() + } + } + + override fun onBackPressed() { + if (tempFragment is FaradayFragment) { + (tempFragment as FaradayFragment).onBackPressed() + } else { + super.onBackPressed() + } + } + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (tempFragment is FaradayFragment) { + (tempFragment as FaradayFragment).onRequestPermissionsResult(requestCode, permissions, grantResults) + } + } + } \ No newline at end of file diff --git a/example/android/app/src/main/res/anim/in_from_left.xml b/example/android/app/src/main/res/anim/in_from_left.xml new file mode 100644 index 0000000..02cca10 --- /dev/null +++ b/example/android/app/src/main/res/anim/in_from_left.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/anim/in_from_right.xml b/example/android/app/src/main/res/anim/in_from_right.xml new file mode 100644 index 0000000..2760f8e --- /dev/null +++ b/example/android/app/src/main/res/anim/in_from_right.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/anim/out_from_left.xml b/example/android/app/src/main/res/anim/out_from_left.xml new file mode 100644 index 0000000..025c02a --- /dev/null +++ b/example/android/app/src/main/res/anim/out_from_left.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/anim/out_from_right.xml b/example/android/app/src/main/res/anim/out_from_right.xml new file mode 100644 index 0000000..dc5a1c2 --- /dev/null +++ b/example/android/app/src/main/res/anim/out_from_right.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index ebc2036..94151fa 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -5,5 +5,22 @@ @color/colorPrimary @color/colorPrimaryDark @color/colorAccent + @style/WindowAnimTheme + + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle index 2835796..cfddc2a 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,8 +1,8 @@ buildscript { ext.kotlin_version = '1.4.10' repositories { - google() - jcenter() + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} } dependencies { @@ -13,8 +13,8 @@ buildscript { allprojects { repositories { - google() - jcenter() + maven { url 'https://maven.aliyun.com/repository/google/'} + maven { url 'https://maven.aliyun.com/repository/jcenter/'} } } diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 3756188..fcc4531 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -26,4 +26,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 6aa33108b2b7376098e10856d131f1ce07024e28 -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.0 diff --git a/example/lib/src/pages/fragment_page.dart b/example/lib/src/pages/fragment_page.dart index eba6277..929403d 100644 --- a/example/lib/src/pages/fragment_page.dart +++ b/example/lib/src/pages/fragment_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:g_faraday/g_faraday.dart'; /// @@ -18,17 +19,22 @@ class _State extends State { @override Widget build(BuildContext context) { return CupertinoPageScaffold( - child: Container( - padding: EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Center(child: Text("Flutter Fragment")), - button( - "Push New Flutter Page", - pushNewFlutterPage, - ), - ], + navigationBar: CupertinoNavigationBar( + middle: Text('Flutter Fragment'), + ), + backgroundColor: Colors.green, + child: SafeArea( + child: Container( + padding: EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + button( + "Push New Flutter Page", + pushNewFlutterPage, + ), + ], + ), ), ), ); diff --git a/example/pubspec.lock b/example/pubspec.lock index b8c28c1..2e52196 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -73,7 +73,7 @@ packages: path: ".." relative: true source: path - version: "0.4.1-pre.0" + version: "0.4.1-pre.1" g_json: dependency: transitive description: diff --git a/ios/Classes/Faraday.swift b/ios/Classes/Faraday.swift index 4b6481c..914c071 100644 --- a/ios/Classes/Faraday.swift +++ b/ios/Classes/Faraday.swift @@ -100,7 +100,7 @@ public class Faraday { self.disableHorizontalSwipePopGesture(arguments: call.arguments, callback: result) } else if (call.method == "reCreateLastPage") { let vc = self.currentFlutterViewController - result(vc?.seq) + result(vc?.id) vc?.createFlutterPage() } }) @@ -227,33 +227,33 @@ extension Faraday { enum PageState { - case create(String, Any?, Int?) // name, arguments, seq - case show(Int) // seq -// case hiden(Int) // seq - case dealloc(Int) //seq + case create(String, Any?, Int) // name, arguments, seq + case show(Int) // id +// case hiden(Int) // id + case dealloc(Int) //id var info: (String, Any?) { switch self { - case .create(let name, let arguments, let seq): - return ("pageCreate", ["name": name, "args": arguments, "seq": seq ?? -1]) - case .show(let seq): - return ("pageShow", seq) + case .create(let name, let arguments, let id): + return ("pageCreate", ["name": name, "args": arguments, "id": id]) + case .show(let id): + return ("pageShow", id) // case .hiden(let seq): // return ("pageHidden", seq) - case .dealloc(let seq): - return ("pageDealloc", seq) + case .dealloc(let id): + return ("pageDealloc", id) } } } - static func sendPageState(_ state: Faraday.PageState, result: @escaping (Any?) -> Void) { + static func sendPageState(_ state: Faraday.PageState, result: @escaping (Bool) -> Void) { let faraday = Faraday.default let info = state.info; faraday.channel?.invokeMethod(info.0, arguments: info.1, result: { r in if (r is FlutterError) { fatalError((r as! FlutterError).message ?? "unkonwn error") } else { - result(r) + result(r as? Bool ?? false) } }) } diff --git a/ios/Classes/FaradayFlutterViewController.swift b/ios/Classes/FaradayFlutterViewController.swift index d39a8c3..28e5593 100644 --- a/ios/Classes/FaradayFlutterViewController.swift +++ b/ios/Classes/FaradayFlutterViewController.swift @@ -13,13 +13,13 @@ open class FaradayFlutterViewController: FlutterViewController { public let name: String public let arguments: Any? + let id: Int + private var callback: ((Any?) -> ())? private var isShowing = false private weak var previousFlutterViewController: FaradayFlutterViewController? - - var seq: Int? - + public init(_ name: String, arguments: Any? = nil, engine: FlutterEngine? = nil, callback: ((Any?) -> ())? = nil) { self.name = name self.arguments = arguments @@ -31,7 +31,10 @@ open class FaradayFlutterViewController: FlutterViewController { previousFlutterViewController = rawEngine.viewController as? FaradayFlutterViewController rawEngine.viewController = nil + + self.id = rawEngine.fa.generateNewId() super.init(engine: rawEngine, nibName: nil, bundle: nil) + modalPresentationStyle = .overFullScreen isShowing = true createFlutterPage() } @@ -41,10 +44,7 @@ open class FaradayFlutterViewController: FlutterViewController { } func createFlutterPage() { - Faraday.sendPageState(.create(name, arguments, seq)) { [weak self] r in - self?.seq = r as? Int - debugPrint("seq: \(r!) create page succeed") - } + Faraday.sendPageState(.create(name, arguments, id)) { _ in } } weak var interactivePopGestureRecognizerDelegate: UIGestureRecognizerDelegate? @@ -69,19 +69,19 @@ open class FaradayFlutterViewController: FlutterViewController { open override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .white + view.backgroundColor = .clear } open override func viewWillAppear(_ animated: Bool) { - if let s = seq { - engine?.viewController = self - isShowing = true - Faraday.sendPageState(.show(s)) { r in - let succeed = r as? Bool ?? false - debugPrint("seq: \(s) send pageState `show` \(succeed ? "succeed" : "failed")") - } - } + engine?.viewController = self + isShowing = true + Faraday.sendPageState(.show(id)) { _ in } super.viewWillAppear(animated) + if (isBeingPresented) { + view.backgroundColor = .clear + } else { + view.backgroundColor = .white + } } open override func viewDidAppear(_ animated: Bool) { @@ -109,24 +109,16 @@ open class FaradayFlutterViewController: FlutterViewController { navigationController?.interactivePopGestureRecognizer?.isEnabled = true } - if seq != nil { - isShowing = false -// Faraday.sendPageState(.hiden(s)) { r in -// let succeed = r as? Bool ?? false -// debugPrint("seq: \(s) send pageState `hiden` \(succeed ? "succeed" : "failed")") -// } - } + isShowing = false +// Faraday.sendPageState(.hiden(id)) { r in +// let succeed = r as? Bool ?? false +// debugPrint("id: \(id) send pageState `hiden` \(succeed ? "succeed" : "failed")") +// } super.viewDidAppear(animated) } deinit { - if let s = seq { - Faraday.sendPageState(.dealloc(s)) { r in - let succeed = r as? Bool ?? false - debugPrint("seq: \(s) send pageState `dealloc` \(succeed ? "succeed" : "failed")") - } - } - debugPrint("faraday flutter deinit") + Faraday.sendPageState(.dealloc(id)) { _ in } + debugPrint("faraday flutter deinit \(name) \(id)") } } - diff --git a/ios/Classes/FlutterEngine+Identifier.swift b/ios/Classes/FlutterEngine+Identifier.swift new file mode 100644 index 0000000..b05d3b1 --- /dev/null +++ b/ios/Classes/FlutterEngine+Identifier.swift @@ -0,0 +1,33 @@ +// +// FlutterEngine+Identifier.swift +// g_faraday +// +// Created by gix on 2020/11/19. +// + +import Flutter + +private struct AssociatedKeys { + static var IdentifierKeyName = "faraday_IdentifierKeyName" +} + +extension FaradayExtension where ExtendedType: FlutterEngine { + + internal var id: Int? { + get { + return objc_getAssociatedObject(UIViewController.self, &AssociatedKeys.IdentifierKeyName) as? Int + } + nonmutating set { + if let newValue = newValue { + objc_setAssociatedObject(UIViewController.self, &AssociatedKeys.IdentifierKeyName, newValue as Int?, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + } + } + } + + func generateNewId() -> Int { + id = (id ?? 0) + 1 + return id! + } +} + +extension FlutterEngine: FaradayExtended { } diff --git a/lib/src/faraday.dart b/lib/src/faraday.dart index 28baa06..8ffe906 100644 --- a/lib/src/faraday.dart +++ b/lib/src/faraday.dart @@ -36,12 +36,19 @@ class Faraday { /// ``` /// Route wrapper(RouteFactory rawFactory, - {TransitionBuilder decorator, RouteFactory onUnknownRoute}) { + {TransitionBuilder decorator, + RouteFactory onUnknownRoute = _default404Page, + Color nativeContainerBackgroundColor}) { return FaradayPageRouteBuilder( pageBuilder: (context) { final page = FaradayNativeBridge( - onGenerateRoute: rawFactory, - onUnknownRoute: onUnknownRoute ?? _default404Page); + rawFactory, + onUnknownRoute: onUnknownRoute, + backgroundColor: nativeContainerBackgroundColor ?? + (MediaQuery.of(context).platformBrightness == Brightness.light + ? CupertinoColors.white + : CupertinoColors.black), + ); return decorator != null ? decorator(context, page) : page; }, ); diff --git a/lib/src/route/arg.dart b/lib/src/route/arg.dart index 457146f..4fb82dc 100644 --- a/lib/src/route/arg.dart +++ b/lib/src/route/arg.dart @@ -8,9 +8,9 @@ class FaradayArguments { final GlobalKey key; final Object arguments; final String name; - final int seq; + final int id; final observer = FaradayNavigatorObserver(); - FaradayArguments(this.arguments, this.name, this.seq) - : key = GlobalKey(debugLabel: 'seq: $seq'); + FaradayArguments(this.arguments, this.name, this.id) + : key = GlobalKey(debugLabel: 'id: $id'); } diff --git a/lib/src/route/native_bridge.dart b/lib/src/route/native_bridge.dart index f880ece..3aa7e3d 100644 --- a/lib/src/route/native_bridge.dart +++ b/lib/src/route/native_bridge.dart @@ -17,8 +17,12 @@ class FaradayNativeBridge extends StatefulWidget { final RouteFactory onGenerateRoute; final RouteFactory onUnknownRoute; - FaradayNativeBridge( - {Key key, @required this.onGenerateRoute, this.onUnknownRoute}) + // 页面有切换动画时,可能会出现大概10ms + + final Color backgroundColor; + + FaradayNativeBridge(this.onGenerateRoute, + {Key key, this.onUnknownRoute, this.backgroundColor}) : super(key: key); static FaradayNativeBridgeState of(BuildContext context) { @@ -38,8 +42,6 @@ class FaradayNativeBridge extends StatefulWidget { class FaradayNativeBridgeState extends State { final List _navigatorStack = []; int _index; - int _preIndex = 0; - int _seq = 0; Timer _reassembleTimer; @@ -118,43 +120,55 @@ class FaradayNativeBridgeState extends State { if (kDebugMode) { _reassembleTimer?.cancel(); } - return IndexedStack( - children: _navigatorStack, - index: _index, + return Container( + color: widget.backgroundColor, + child: IndexedStack( + children: _navigatorStack + .map((navigator) => TweenAnimationBuilder( + builder: (context, value, child) => AnimatedOpacity( + duration: Duration(milliseconds: 50), + opacity: value, + child: child, + ), + child: navigator, + duration: Duration(milliseconds: 250), + tween: Tween(begin: 0, end: 1), + )) + .toList(growable: false), + index: _index, + ), ); } - Future _handler(MethodCall call) { + Future _handler(MethodCall call) async { switch (call.method) { case 'pageCreate': String name = call.arguments['name']; - final seq = call.arguments['seq'] as int; - // seq 不等于null 证明整个app 部分状态丢失,此时需要重建页面 - if (seq != null) { - if (seq == -1) { - debugPrint('recreate page: $name seq: $seq'); - } else { - // seq 不为空 native可能重复调用了onCreate 方法 - final index = _findIndexBy(seq: seq); - if (index != null) { - _updateIndex(index); - return Future.value(index); - } - _seq = seq; - } + int id = call.arguments['id']; + + assert(name != null); + assert(id != null); + + // 通过id查找,当前堆栈中是否存在对应的页面,如果存在 直接显示出来 + final index = _findIndexBy(id: id); + if (index != null) { + _updateIndex(index); + return true; } - final arg = FaradayArguments(call.arguments['args'], name, _seq++); + // seq 不为空 native可能重复调用了onCreate 方法 + + final arg = FaradayArguments(call.arguments['args'], name, id); _navigatorStack.add(_appRoot(arg)); - _updateIndex(_navigatorStack.length - 1); - return Future.value(arg.seq); + // _updateIndex(_navigatorStack.length - 1); + return true; case 'pageShow': - final index = _findIndexBy(seq: call.arguments); + final index = _findIndexBy(id: call.arguments); _updateIndex(index); return Future.value(index != null); case 'pageDealloc': assert(_index != null, _index < _navigatorStack.length); final current = _navigatorStack[_index]; - final index = _findIndexBy(seq: call.arguments); + final index = _findIndexBy(id: call.arguments); assert(index != null, 'page not found seq: ${call.arguments}'); _navigatorStack.removeAt(index); _updateIndex(_navigatorStack.indexOf(current)); @@ -165,9 +179,8 @@ class FaradayNativeBridgeState extends State { } // 如果找不到返回null,不会返回-1 - int _findIndexBy({@required int seq}) { - assert(seq != null); - final index = _navigatorStack.indexWhere((n) => n.arg.seq == seq); + int _findIndexBy({@required int id}) { + final index = _navigatorStack.indexWhere((n) => n.arg.id == id); return index != -1 ? index : null; } @@ -175,9 +188,8 @@ class FaradayNativeBridgeState extends State { if (index == null) return; if (index == _index) return; setState(() { - _preIndex = _index; _index = index; - debugPrint('index: $_index, preIndex: $_preIndex, max_seq: $_seq'); + debugPrint('index: $_index'); }); } diff --git a/lib/src/route/navigator.dart b/lib/src/route/navigator.dart index 487458d..498522f 100644 --- a/lib/src/route/navigator.dart +++ b/lib/src/route/navigator.dart @@ -104,6 +104,16 @@ class FaradayNavigatorState extends NavigatorState { super.pop(result); } } + + @override + Future maybePop([T result]) async { + final r = await super.maybePop(result); + if (!r && observer.onlyOnePage) { + pop(result); + return true; + } + return r; + } } class _FaradayWidgetsBindingObserver extends WidgetsBindingObserver { diff --git a/pubspec.yaml b/pubspec.yaml index 885f2d4..8a016fc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: g_faraday description: A new hybrid stack plugin -version: 0.4.1-pre.0 +version: 0.4.1-pre.1 homepage: http://git.yuxiaor.com/yuxiaor-mobile/g_faraday environment: