From 78c25deed5aed5f4878722fa16d16399943addcc Mon Sep 17 00:00:00 2001 From: siguangli Date: Tue, 11 Jun 2024 14:37:38 +0800 Subject: [PATCH] feat(android): support get screenshot bitmap from hippy engine --- .../com/tencent/mtt/hippy/HippyEngine.java | 716 +++++++++--------- .../mtt/hippy/HippyEngineManagerImpl.java | 116 ++- .../openhippy/example/HippyEngineWrapper.kt | 29 +- .../openhippy/example/PageConfiguration.kt | 78 +- .../com/openhippy/example/PageManagement.kt | 6 +- .../com/tencent/renderer/NativeRenderer.java | 10 +- 6 files changed, 538 insertions(+), 417 deletions(-) diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngine.java b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngine.java index c020af46168..2845803f665 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngine.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngine.java @@ -13,9 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.tencent.mtt.hippy; import android.content.Context; +import android.graphics.Bitmap; import android.text.TextUtils; import android.view.View; @@ -57,8 +59,11 @@ import com.tencent.mtt.hippy.utils.LogUtils; import com.tencent.mtt.hippy.utils.UIThreadUtils; import com.tencent.mtt.hippy.adapter.thirdparty.HippyThirdPartyAdapter; +import com.tencent.renderer.NativeRenderException; import com.tencent.renderer.component.image.ImageDecoderAdapter; +import com.tencent.renderer.serialization.Serializer; import com.tencent.vfs.Processor; +import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -68,378 +73,411 @@ @SuppressWarnings({"deprecation", "unused", "rawtypes"}) public abstract class HippyEngine { - private static final AtomicInteger ID_COUNTER = new AtomicInteger(); - private final int engineId = ID_COUNTER.getAndIncrement(); - - @SuppressWarnings("unchecked") - final CopyOnWriteArrayList mEventListeners = new CopyOnWriteArrayList(); - volatile EngineState mCurrentState = EngineState.UNINIT; - // Engine所属的分组ID,同一个组共享线程和isolate,不同context - protected int mGroupId; - ModuleListener mModuleListener; - - private static HippyLogAdapter sLogAdapter = null; - @SuppressWarnings("JavaJniMissingFunction") - private static native void setNativeLogHandler(HippyLogAdapter handler); - - /** - * @param params 创建实例需要的参数 创建一个HippyEngine实例 - */ - public static HippyEngine create(EngineInitParams params) { - if (params == null) { - throw new RuntimeException("Hippy: initParams must no be null"); - } - LogUtils.enableDebugLog(BuildConfig.DEBUG); - LibraryLoader.loadLibraryIfNeeded(params.soLoader); - if (sLogAdapter == null && params.logAdapter != null) { - setNativeLogHandler(params.logAdapter); - } - ContextHolder.initAppContext(params.context); - BuglyUtils.registerSdkAppIdIfNeeded(params.context); - params.check(); - HippyEngine hippyEngine; - if (params.groupId == -1) { - hippyEngine = new HippyNormalEngineManager(params, null); - } else { - hippyEngine = new HippySingleThreadEngineManager(params, null); - } - return hippyEngine; - } - - /** - * listen engine state. no need to make this method public - */ - protected void listen(EngineListener listener) { - // 通知listener都要在UI线程 - if (UIThreadUtils.isOnUiThread()) { - listenInUIThread(listener); - } else { - final EngineListener listenerFinal = listener; - UIThreadUtils.runOnUiThread(new Runnable() { - @Override - public void run() { - listenInUIThread(listenerFinal); + private static final AtomicInteger ID_COUNTER = new AtomicInteger(); + private final int engineId = ID_COUNTER.getAndIncrement(); + + @SuppressWarnings("unchecked") + final CopyOnWriteArrayList mEventListeners = new CopyOnWriteArrayList(); + volatile EngineState mCurrentState = EngineState.UNINIT; + // Engine所属的分组ID,同一个组共享线程和isolate,不同context + protected int mGroupId; + ModuleListener mModuleListener; + + private static HippyLogAdapter sLogAdapter = null; + + @SuppressWarnings("JavaJniMissingFunction") + private static native void setNativeLogHandler(HippyLogAdapter handler); + + /** + * @param params 创建实例需要的参数 创建一个HippyEngine实例 + */ + public static HippyEngine create(EngineInitParams params) { + if (params == null) { + throw new RuntimeException("Hippy: initParams must no be null"); } - }); - } - } - - /** - * listen engine state in UI thread - */ - private void listenInUIThread(EngineListener listener) { - // 1. 若mCurrentState是结束态,无论成功还是失败,要直接通知结果并返回。 - // 2. 若mCurrentState是初始化过程中的状态,则把listener添加到mEventListeners后返回 - if (mCurrentState == EngineState.INITED) { - listener.onInitialized(EngineInitStatus.STATUS_OK, null); - } else if (mCurrentState == EngineState.INITERRORED || mCurrentState == EngineState.DESTROYED) { - listener.onInitialized(EngineInitStatus.STATUS_WRONG_STATE, "engine state=" + mCurrentState); - } else // 说明mCurrentState是初始化过程中的状态 - { - mEventListeners.add(listener); + LogUtils.enableDebugLog(BuildConfig.DEBUG); + LibraryLoader.loadLibraryIfNeeded(params.soLoader); + if (sLogAdapter == null && params.logAdapter != null) { + setNativeLogHandler(params.logAdapter); + } + ContextHolder.initAppContext(params.context); + BuglyUtils.registerSdkAppIdIfNeeded(params.context); + params.check(); + HippyEngine hippyEngine; + if (params.groupId == -1) { + hippyEngine = new HippyNormalEngineManager(params, null); + } else { + hippyEngine = new HippySingleThreadEngineManager(params, null); + } + + return hippyEngine; } - } - @SuppressWarnings("unused") - public EngineState getEngineState() { - return mCurrentState; - } + /** + * listen engine state. no need to make this method public + */ + protected void listen(EngineListener listener) { + // 通知listener都要在UI线程 + if (UIThreadUtils.isOnUiThread()) { + listenInUIThread(listener); + } else { + final EngineListener listenerFinal = listener; + UIThreadUtils.runOnUiThread(new Runnable() { + @Override + public void run() { + listenInUIThread(listenerFinal); + } + }); + } + } - /** - * get group id - */ - public int getGroupId() { - return mGroupId; - } - - /** - * get engine id - */ - public int getEngineId() { - return engineId; - } - - public abstract void initEngine(EngineListener listener); - - // 是否调试模式 - @SuppressWarnings("BooleanMethodIsAlwaysInverted") - public abstract boolean isDebugMode(); - - /** - * destroy the hippy engine All hippy instance will be destroyed - */ - @SuppressWarnings("EmptyMethod") - public abstract void destroyEngine(); - - /** - * 加载hippy业务模块 ,函数组 - * - * @param loadParams 加载hippy业务模块时需要的参数 - */ - public abstract ViewGroup loadModule(ModuleLoadParams loadParams); - - public abstract ViewGroup loadModule(ModuleLoadParams loadParams, ModuleListener listener); - - public abstract void destroyModule(@Nullable ViewGroup rootView, @NonNull Callback callback); - - public abstract void onEngineResume(); - - public abstract void onEnginePause(); - - public abstract void sendEvent(String event, Object params); - - public abstract void sendEvent(String event, Object params, BridgeTransferType transferType); - - public abstract void preloadModule(HippyBundleLoader loader); - - public abstract boolean onBackPressed(BackPressHandler handler); - - public abstract HippyEngineContext getEngineContext(); - - public abstract void addControllers(@NonNull List providers); - - public abstract void addModules(@NonNull List providers); - - public abstract void recordSnapshot(@NonNull View rootView, @NonNull final Callback callback); - - public abstract View replaySnapshot(@NonNull Context context, byte[] buffer); - - public abstract View replaySnapshot(@NonNull Context context, @NonNull Map snapshotMap); - - public abstract void removeSnapshotView(); - - public interface BackPressHandler { - - void handleBackPress(); - } - - public enum EngineState { - UNINIT, - INITING, - ONRESTART, - INITERRORED, - INITED, - DESTROYED - } - - // Hippy 引擎初始化时的参数设置 - @SuppressWarnings("deprecation") - public static class EngineInitParams { - - // 必须 宿主(Hippy的使用者)的Context - public Context context; - // 可选参数 核心的jsbundle的assets路径(assets路径和文件路径二选一,优先使用assets路径),debugMode = false时有效 - public String coreJSAssetsPath; - // 可选参数 核心的jsbundle的文件路径(assets路径和文件路径二选一,优先使用assets路径),debugMode = false时有效 - public String coreJSFilePath; - // 可选参数 指定需要预加载的业务模块bundle assets路径 - public HippyBundleLoader jsPreloadAssetsPath; - // 可选参数 指定需要预加载的业务模块bundle 文件路径 - public HippyBundleLoader jsPreloadFilePath; - public boolean debugMode = false; - // 可选参数 是否开启调试模式,默认为false,不开启 - // 可选参数 Hippy Server的jsbundle名字,默认为"index.bundle"。debugMode = true时有效 - public String debugBundleName = "index.bundle"; - // 可选参数 Hippy Server的Host。默认为"localhost:38989"。debugMode = true时有效 - public String debugServerHost = "localhost:38989"; - // optional args, Hippy Server url using remote debug in no usb (if not empty will replace debugServerHost and debugBundleName). debugMode = true take effect - public String remoteServerUrl = ""; - // 可选参数 自定义的,用来提供Native modules、JavaScript modules、View controllers的管理器。1个或多个 - public List providers; - public List processors; - //Optional is use V8 serialization or json - public boolean enableV8Serialization = true; - // 可选参数 是否打印引擎的完整的log。默认为false - public boolean enableLog = false; - // 可选参数 code cache的名字,如果设置为空,则不启用code cache,默认为 "" - public String codeCacheTag = ""; - - public ImageDecoderAdapter imageDecoderAdapter; - //可选参数 接收RuntimeId - public HippyThirdPartyAdapter thirdPartyAdapter; - - // 可选参数 接收异常 - public HippyExceptionHandlerAdapter exceptionHandler; - // 可选参数 设置相关 - public HippySharedPreferencesAdapter sharedPreferencesAdapter; - // 可选参数 Http request adapter - public HippyHttpAdapter httpAdapter; - // 可选参数 Storage adapter 设置相关 - public HippyStorageAdapter storageAdapter; - // 可选参数 Executor Supplier adapter - public HippyExecutorSupplierAdapter executorSupplier; - // 可选参数 Engine Monitor adapter - public HippyEngineMonitorAdapter engineMonitor; - // 可选参数 font scale adapter - public HippyFontScaleAdapter fontScaleAdapter; - // 可选参数 so加载位置 - public HippySoLoaderAdapter soLoader; - // 可选参数 device adapter - public HippyDeviceAdapter deviceAdapter; - // 设置Hippy引擎的组,同一组的HippyEngine,会共享C层的v8 引擎实例。 默认值为-1(无效组,即不属于任何group组) - public int groupId = -1; - // 可选参数 日志输出 - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated - public HippyLogAdapter logAdapter; - public V8InitParams v8InitParams; - public boolean enableTurbo; - - protected void check() { - if (context == null) { - throw new IllegalArgumentException( - EngineInitParams.class.getName() + " context must not be null!"); - } - if (sharedPreferencesAdapter == null) { - sharedPreferencesAdapter = new DefaultSharedPreferencesAdapter(context); - } - if (exceptionHandler == null) { - exceptionHandler = new DefaultExceptionHandler(); - } - if (httpAdapter == null) { - httpAdapter = new DefaultHttpAdapter(); - } - if (executorSupplier == null) { - executorSupplier = new DefaultExecutorSupplierAdapter(); - } - if (storageAdapter == null) { - storageAdapter = new DefaultStorageAdapter(context, executorSupplier.getDBExecutor()); - } - if (engineMonitor == null) { - engineMonitor = new DefaultEngineMonitorAdapter(); - } - if (fontScaleAdapter == null) { - fontScaleAdapter = new DefaultFontScaleAdapter(); - } - if (soLoader == null) { - soLoader = new DefaultSoLoaderAdapter(); - } - if (deviceAdapter == null) { - deviceAdapter = new DefaultDeviceAdapter(); - } - if (logAdapter == null) { - logAdapter = new DefaultLogAdapter(); - } - if (providers == null) { - providers = new ArrayList<>(); - } - providers.add(0, new HippyCoreAPI()); - if (!debugMode) { - if (TextUtils.isEmpty(coreJSAssetsPath) && TextUtils.isEmpty(coreJSFilePath)) { - throw new RuntimeException( - "Hippy: debugMode=true, initParams.coreJSAssetsPath and coreJSFilePath both null!"); + /** + * listen engine state in UI thread + */ + private void listenInUIThread(EngineListener listener) { + // 1. 若mCurrentState是结束态,无论成功还是失败,要直接通知结果并返回。 + // 2. 若mCurrentState是初始化过程中的状态,则把listener添加到mEventListeners后返回 + if (mCurrentState == EngineState.INITED) { + listener.onInitialized(EngineInitStatus.STATUS_OK, null); + } else if (mCurrentState == EngineState.INITERRORED || mCurrentState == EngineState.DESTROYED) { + listener.onInitialized(EngineInitStatus.STATUS_WRONG_STATE, "engine state=" + mCurrentState); + } else // 说明mCurrentState是初始化过程中的状态 + { + mEventListeners.add(listener); } - } } - } - // Hippy 业务模块jsbundle加载时的参数设置 - @SuppressWarnings("deprecation") - public static class ModuleLoadParams { + @SuppressWarnings("unused") + public EngineState getEngineState() { + return mCurrentState; + } - // 必须参数 挂载HippyRootView的Activity or Dialog的Context。注意,只有Context为当前Activity时,调试模式才能使用 - public Context context; /** - * 必须参数 业务模块jsbundle中定义的组件名称。componentName对应的是js文件中的"appName",比如: var hippy = new Hippy({ - * appName: "Demo", entryPage: App }); + * get group id */ - public String componentName; - - // 可选参数 二选一设置 自己开发的业务模块的jsbundle的assets路径(assets路径和文件路径二选一,优先使用assets路径) - public String jsAssetsPath; - // 可选参数 二选一设置 自己开发的业务模块的文件路径(assets路径和文件路径二选一,优先使用assets路径) - public String jsFilePath; - // 可选参数 传递给前端的rootview:比如:Hippy.entryPage: class App extends Component - public HippyMap jsParams; - // 可选参数 目前只有一个用处:映射:"CustomViewCreator" <==> 宿主自定义的一个HippyCustomViewCreator(这个creator还得通过ModuleParams.Builder.setCustomViewCreator来指定才行) - public HashMap nativeParams; - // 可选参数 Bundle加载器,老式用法,不建议使用(若一定要使用,则会覆盖jsAssetsPath,jsFilePath的值)。参见jsAssetsPath,jsFilePath - // 可选参数 code cache的名字,如果设置为空,则不启用code cache,默认为 "" - public String codeCacheTag = ""; - @SuppressWarnings("DeprecatedIsStillUsed") - @Deprecated - public HippyBundleLoader bundleLoader; - - public ModuleLoadParams() { + public int getGroupId() { + return mGroupId; } - public ModuleLoadParams(ModuleLoadParams params) { - context = params.context; - jsAssetsPath = params.jsAssetsPath; - jsFilePath = params.jsFilePath; - componentName = params.componentName; - jsParams = params.jsParams; - nativeParams = params.nativeParams; - codeCacheTag = params.codeCacheTag; - bundleLoader = params.bundleLoader; + /** + * get engine id + */ + public int getEngineId() { + return engineId; } - } - /** - * 引擎初始化过程中的错误码,对于hippy sdk开发者调查hippy sdk的使用者在使用过程中遇到的问题,很必须。 - */ + public abstract void initEngine(EngineListener listener); + + // 是否调试模式 + @SuppressWarnings("BooleanMethodIsAlwaysInverted") + public abstract boolean isDebugMode(); + + /** + * destroy the hippy engine All hippy instance will be destroyed + */ + @SuppressWarnings("EmptyMethod") + public abstract void destroyEngine(); + + /** + * 加载hippy业务模块 ,函数组 + * + * @param loadParams 加载hippy业务模块时需要的参数 + */ + public abstract ViewGroup loadModule(ModuleLoadParams loadParams); + + public abstract ViewGroup loadModule(ModuleLoadParams loadParams, ModuleListener listener); + + public abstract void destroyModule(@Nullable ViewGroup rootView, @NonNull Callback callback); + + public abstract void onEngineResume(); + + public abstract void onEnginePause(); + + public abstract void sendEvent(String event, Object params); + + public abstract void sendEvent(String event, Object params, BridgeTransferType transferType); + + public abstract void preloadModule(HippyBundleLoader loader); + + public abstract boolean onBackPressed(BackPressHandler handler); + + public abstract HippyEngineContext getEngineContext(); - public enum EngineInitStatus { - STATUS_OK(0), // 初始化成功 - STATUS_ERR_BRIDGE(-101), // 初始化过程,initBridge错误 - STATUS_ERR_DEVSERVER(-102), // 初始化过程,devServer错误 - STATUS_WRONG_STATE(-103), // 状态错误。调用init函数时,引擎不在未初始化的状态 - STATUS_INIT_EXCEPTION(-104); // 初始化过程,抛出了未知的异常,详情需要查看传回的Throwable + public abstract void addControllers(@NonNull List providers); - private final int iValue; + public abstract void addModules(@NonNull List providers); - EngineInitStatus(int value) { - iValue = value; + public abstract void recordSnapshot(@NonNull View rootView, @NonNull final Callback callback); + + public abstract View replaySnapshot(@NonNull Context context, byte[] buffer); + + public abstract View replaySnapshot(@NonNull Context context, @NonNull Map snapshotMap); + + public abstract void removeSnapshotView(); + + /** + * Generate screenshots of specified views + * + * @param context host container activity, if this parameter is null, the context of the target view will be + * used, if all context is not of activity type, an exception will be thrown + * @param id the target view id + * @param callback the result callback {@link ScreenshotBuildCallback} + * @throws IllegalArgumentException + */ + public abstract void getScreenshotBitmapForView(@Nullable Context context, int id, + @NonNull ScreenshotBuildCallback callback); + + /** + * Generate screenshots of specified views + * + * @param context host container activity, if this parameter is null, the context of the target view will be + * used, if all context is not of activity type, an exception will be thrown + * @param view the target view + * @param callback the result callback {@link ScreenshotBuildCallback} + * @throws IllegalArgumentException + */ + public abstract void getScreenshotBitmapForView(@Nullable Context context, @NonNull View view, + @NonNull ScreenshotBuildCallback callback); + + public interface ScreenshotBuildCallback { + + void onScreenshotBuildCompleted(Bitmap bitmap, int result); } - @SuppressWarnings("unused") - public int value() { - return iValue; + public interface BackPressHandler { + + void handleBackPress(); } - } - public enum ModuleLoadStatus { - STATUS_OK(0), // 加载正常 - STATUS_ENGINE_UNINIT(-201), // 引擎未完成初始化就加载JSBundle - STATUS_VARIABLE_NULL(-202), // check变量(bundleUniKey, loader, rootView)引用为空 - STATUS_ERR_RUN_BUNDLE(-203), // 业务JSBundle执行错误 - STATUS_REPEAT_LOAD(-204); // 重复加载同一JSBundle + public enum EngineState { + UNINIT, + INITING, + ONRESTART, + INITERRORED, + INITED, + DESTROYED + } - private final int iValue; + // Hippy 引擎初始化时的参数设置 + @SuppressWarnings("deprecation") + public static class EngineInitParams { + + // 必须 宿主(Hippy的使用者)的Context + public Context context; + // 可选参数 核心的jsbundle的assets路径(assets路径和文件路径二选一,优先使用assets路径),debugMode = false时有效 + public String coreJSAssetsPath; + // 可选参数 核心的jsbundle的文件路径(assets路径和文件路径二选一,优先使用assets路径),debugMode = false时有效 + public String coreJSFilePath; + // 可选参数 指定需要预加载的业务模块bundle assets路径 + public HippyBundleLoader jsPreloadAssetsPath; + // 可选参数 指定需要预加载的业务模块bundle 文件路径 + public HippyBundleLoader jsPreloadFilePath; + public boolean debugMode = false; + // 可选参数 是否开启调试模式,默认为false,不开启 + // 可选参数 Hippy Server的jsbundle名字,默认为"index.bundle"。debugMode = true时有效 + public String debugBundleName = "index.bundle"; + // 可选参数 Hippy Server的Host。默认为"localhost:38989"。debugMode = true时有效 + public String debugServerHost = "localhost:38989"; + // optional args, Hippy Server url using remote debug in no usb (if not empty will replace debugServerHost + // and debugBundleName). debugMode = true take effect + public String remoteServerUrl = ""; + // 可选参数 自定义的,用来提供Native modules、JavaScript modules、View controllers的管理器。1个或多个 + public List providers; + public List processors; + //Optional is use V8 serialization or json + public boolean enableV8Serialization = true; + // 可选参数 是否打印引擎的完整的log。默认为false + public boolean enableLog = false; + // 可选参数 code cache的名字,如果设置为空,则不启用code cache,默认为 "" + public String codeCacheTag = ""; + + public ImageDecoderAdapter imageDecoderAdapter; + //可选参数 接收RuntimeId + public HippyThirdPartyAdapter thirdPartyAdapter; + + // 可选参数 接收异常 + public HippyExceptionHandlerAdapter exceptionHandler; + // 可选参数 设置相关 + public HippySharedPreferencesAdapter sharedPreferencesAdapter; + // 可选参数 Http request adapter + public HippyHttpAdapter httpAdapter; + // 可选参数 Storage adapter 设置相关 + public HippyStorageAdapter storageAdapter; + // 可选参数 Executor Supplier adapter + public HippyExecutorSupplierAdapter executorSupplier; + // 可选参数 Engine Monitor adapter + public HippyEngineMonitorAdapter engineMonitor; + // 可选参数 font scale adapter + public HippyFontScaleAdapter fontScaleAdapter; + // 可选参数 so加载位置 + public HippySoLoaderAdapter soLoader; + // 可选参数 device adapter + public HippyDeviceAdapter deviceAdapter; + // 设置Hippy引擎的组,同一组的HippyEngine,会共享C层的v8 引擎实例。 默认值为-1(无效组,即不属于任何group组) + public int groupId = -1; + // 可选参数 日志输出 + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + public HippyLogAdapter logAdapter; + public V8InitParams v8InitParams; + public boolean enableTurbo; + + protected void check() { + if (context == null) { + throw new IllegalArgumentException( + EngineInitParams.class.getName() + " context must not be null!"); + } + if (sharedPreferencesAdapter == null) { + sharedPreferencesAdapter = new DefaultSharedPreferencesAdapter(context); + } + if (exceptionHandler == null) { + exceptionHandler = new DefaultExceptionHandler(); + } + if (httpAdapter == null) { + httpAdapter = new DefaultHttpAdapter(); + } + if (executorSupplier == null) { + executorSupplier = new DefaultExecutorSupplierAdapter(); + } + if (storageAdapter == null) { + storageAdapter = new DefaultStorageAdapter(context, executorSupplier.getDBExecutor()); + } + if (engineMonitor == null) { + engineMonitor = new DefaultEngineMonitorAdapter(); + } + if (fontScaleAdapter == null) { + fontScaleAdapter = new DefaultFontScaleAdapter(); + } + if (soLoader == null) { + soLoader = new DefaultSoLoaderAdapter(); + } + if (deviceAdapter == null) { + deviceAdapter = new DefaultDeviceAdapter(); + } + if (logAdapter == null) { + logAdapter = new DefaultLogAdapter(); + } + if (providers == null) { + providers = new ArrayList<>(); + } + providers.add(0, new HippyCoreAPI()); + if (!debugMode) { + if (TextUtils.isEmpty(coreJSAssetsPath) && TextUtils.isEmpty(coreJSFilePath)) { + throw new RuntimeException( + "Hippy: debugMode=true, initParams.coreJSAssetsPath and coreJSFilePath both null!"); + } + } + } + } + + // Hippy 业务模块jsbundle加载时的参数设置 + @SuppressWarnings("deprecation") + public static class ModuleLoadParams { + + // 必须参数 挂载HippyRootView的Activity or Dialog的Context。注意,只有Context为当前Activity时,调试模式才能使用 + public Context context; + /** + * 必须参数 业务模块jsbundle中定义的组件名称。componentName对应的是js文件中的"appName",比如: var hippy = new Hippy({ appName: "Demo", + * entryPage: App }); + */ + public String componentName; + + // 可选参数 二选一设置 自己开发的业务模块的jsbundle的assets路径(assets路径和文件路径二选一,优先使用assets路径) + public String jsAssetsPath; + // 可选参数 二选一设置 自己开发的业务模块的文件路径(assets路径和文件路径二选一,优先使用assets路径) + public String jsFilePath; + // 可选参数 传递给前端的rootview:比如:Hippy.entryPage: class App extends Component + public HippyMap jsParams; + // 可选参数 目前只有一个用处:映射:"CustomViewCreator" <==> 宿主自定义的一个HippyCustomViewCreator(这个creator还得通过ModuleParams.Builder + // .setCustomViewCreator来指定才行) + public HashMap nativeParams; + // 可选参数 Bundle加载器,老式用法,不建议使用(若一定要使用,则会覆盖jsAssetsPath,jsFilePath的值)。参见jsAssetsPath,jsFilePath + // 可选参数 code cache的名字,如果设置为空,则不启用code cache,默认为 "" + public String codeCacheTag = ""; + @SuppressWarnings("DeprecatedIsStillUsed") + @Deprecated + public HippyBundleLoader bundleLoader; + + public ModuleLoadParams() { + } - ModuleLoadStatus(int value) { - iValue = value; + public ModuleLoadParams(ModuleLoadParams params) { + context = params.context; + jsAssetsPath = params.jsAssetsPath; + jsFilePath = params.jsFilePath; + componentName = params.componentName; + jsParams = params.jsParams; + nativeParams = params.nativeParams; + codeCacheTag = params.codeCacheTag; + bundleLoader = params.bundleLoader; + } } - @SuppressWarnings("unused") - public int value() { - return iValue; + /** + * 引擎初始化过程中的错误码,对于hippy sdk开发者调查hippy sdk的使用者在使用过程中遇到的问题,很必须。 + */ + + public enum EngineInitStatus { + STATUS_OK(0), // 初始化成功 + STATUS_ERR_BRIDGE(-101), // 初始化过程,initBridge错误 + STATUS_ERR_DEVSERVER(-102), // 初始化过程,devServer错误 + STATUS_WRONG_STATE(-103), // 状态错误。调用init函数时,引擎不在未初始化的状态 + STATUS_INIT_EXCEPTION(-104); // 初始化过程,抛出了未知的异常,详情需要查看传回的Throwable + + private final int iValue; + + EngineInitStatus(int value) { + iValue = value; + } + + @SuppressWarnings("unused") + public int value() { + return iValue; + } } - } - /** - * Hippy引擎初始化结果listener - */ - public interface EngineListener { + public enum ModuleLoadStatus { + STATUS_OK(0), // 加载正常 + STATUS_ENGINE_UNINIT(-201), // 引擎未完成初始化就加载JSBundle + STATUS_VARIABLE_NULL(-202), // check变量(bundleUniKey, loader, rootView)引用为空 + STATUS_ERR_RUN_BUNDLE(-203), // 业务JSBundle执行错误 + STATUS_REPEAT_LOAD(-204); // 重复加载同一JSBundle + + private final int iValue; + + ModuleLoadStatus(int value) { + iValue = value; + } + + @SuppressWarnings("unused") + public int value() { + return iValue; + } + } /** - * callback after initialization - * - * @param statusCode status code from initializing procedure - * @param msg Message from initializing procedure + * Hippy引擎初始化结果listener */ - void onInitialized(EngineInitStatus statusCode, String msg); - } + public interface EngineListener { + + /** + * callback after initialization + * + * @param statusCode status code from initializing procedure + * @param msg Message from initializing procedure + */ + void onInitialized(EngineInitStatus statusCode, String msg); + } - @SuppressWarnings("unused") - public interface ModuleListener { + @SuppressWarnings("unused") + public interface ModuleListener { - void onLoadCompleted(ModuleLoadStatus statusCode, String msg); + void onLoadCompleted(ModuleLoadStatus statusCode, String msg); - @SuppressWarnings("SameReturnValue") - boolean onJsException(HippyJsException exception); + @SuppressWarnings("SameReturnValue") + boolean onJsException(HippyJsException exception); - void onFirstViewAdded(); - } + void onFirstViewAdded(); + } } diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineManagerImpl.java b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineManagerImpl.java index f78739eaaf0..1e525025c06 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineManagerImpl.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineManagerImpl.java @@ -15,10 +15,23 @@ package com.tencent.mtt.hippy; +import android.app.Activity; +import android.app.Dialog; import android.content.Context; +import android.content.ContextWrapper; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.os.Looper; import android.text.TextUtils; +import android.view.PixelCopy; +import android.view.PixelCopy.OnPixelCopyFinishedListener; import android.view.View; import android.view.ViewGroup; +import android.view.Window; import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -51,14 +64,19 @@ import com.tencent.mtt.hippy.modules.javascriptmodules.EventDispatcher; import com.tencent.mtt.hippy.modules.nativemodules.deviceevent.DeviceEventModule; import com.tencent.mtt.hippy.uimanager.HippyCustomViewCreator; +import com.tencent.mtt.hippy.uimanager.RenderManager; import com.tencent.mtt.hippy.utils.DimensionsUtil; import com.tencent.mtt.hippy.utils.LogUtils; import com.tencent.mtt.hippy.utils.PixelUtil; import com.tencent.mtt.hippy.utils.TimeMonitor; import com.tencent.mtt.hippy.utils.UIThreadUtils; +import com.tencent.mtt.hippy.views.modal.HippyModalHostManager; +import com.tencent.mtt.hippy.views.modal.HippyModalHostView; import com.tencent.renderer.FrameworkProxy; +import com.tencent.renderer.NativeRenderContext; import com.tencent.renderer.component.image.ImageDecoderAdapter; import com.tencent.renderer.component.text.FontAdapter; +import com.tencent.renderer.node.RenderNode; import com.tencent.vfs.DefaultProcessor; import com.tencent.vfs.Processor; import com.tencent.vfs.VfsManager; @@ -363,6 +381,96 @@ public void removeSnapshotView() { } } + public void getScreenshotBitmapForView(@Nullable Context context, int id, + @NonNull final ScreenshotBuildCallback callback) { + if (mEngineContext == null) { + throw new IllegalArgumentException("engine context is null"); + } + View view = mEngineContext.findViewById(id); + if (view == null) { + throw new IllegalArgumentException("can not find view by id"); + } + getScreenshotBitmapForView(context, view, callback); + } + + private Window getDialogWindow(@NonNull RenderNode node) { + View hostView = mEngineContext.findViewById(node.getId()); + if (hostView instanceof HippyModalHostView) { + Dialog dialog = ((HippyModalHostView) hostView).getDialog(); + if (dialog != null) { + return dialog.getWindow(); + } + } + return null; + } + + private Window getViewWindow(@NonNull Activity activity, @NonNull View view) { + Window window = null; + RenderNode node = RenderManager.getRenderNode(view); + if (node == null) { + return activity.getWindow(); + } + if (node.getClassName().equals(HippyModalHostManager.CLASS_NAME)) { + window = getDialogWindow(node); + } + if (window == null) { + RenderNode parent = node.getParent(); + while (parent != null) { + if (parent.getClassName().equals(HippyModalHostManager.CLASS_NAME)) { + window = getDialogWindow(parent); + break; + } + parent = parent.getParent(); + } + } + return window == null ? activity.getWindow() : window; + } + + public void getScreenshotBitmapForView(@Nullable Context context, @NonNull View view, + @NonNull final ScreenshotBuildCallback callback) { + final int width = view.getWidth(); + final int height = view.getHeight(); + if (width <= 0 || height <= 0) { + String error = "error view size: width " + width + ", height " + height; + throw new IllegalArgumentException(error); + } + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (!(context instanceof Activity)) { + context = view.getContext(); + if (context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + if (!(context instanceof Activity)) { + throw new IllegalArgumentException("context is not activity"); + } + } + // Above Android O, use PixelCopy, because another way view.draw will cause Software rendering doesn't support hardware bitmaps + int[] location = new int[2]; + view.getLocationInWindow(location); + Window window = getViewWindow((Activity) context, view); + final Bitmap finalBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + PixelCopy.request(window, + new Rect(location[0], location[1], location[0] + view.getWidth(), + location[1] + view.getHeight()), + finalBitmap, new OnPixelCopyFinishedListener() { + @Override + public void onPixelCopyFinished(int copyResult) { + callback.onScreenshotBuildCompleted(finalBitmap, copyResult); + } + }, new Handler(Looper.getMainLooper())); + } else { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + canvas.drawColor(Color.WHITE); + view.draw(canvas); + callback.onScreenshotBuildCompleted(bitmap, PixelCopy.SUCCESS); + } + } catch (OutOfMemoryError e) { + callback.onScreenshotBuildCompleted(null, PixelCopy.ERROR_DESTINATION_INVALID); + } + } + public void addControllers(@NonNull List providers) { if (mEngineContext != null) { List> controllers = null; @@ -962,14 +1070,12 @@ public void handleException(Throwable throwable) { mDevSupportManager.handleException(throwable); } else { if (throwable instanceof HippyJsException) { - if (mGlobalConfigs != null) { - mGlobalConfigs.getExceptionHandler() - .handleJsException((HippyJsException) throwable); - } + mGlobalConfigs.getExceptionHandler() + .handleJsException((HippyJsException) throwable); if (mModuleListener != null) { mModuleListener.onJsException((HippyJsException) throwable); } - } else if (mGlobalConfigs != null) { + } else { mGlobalConfigs.getExceptionHandler() .handleNativeException(new RuntimeException(throwable), true); } diff --git a/framework/examples/android-demo/src/main/java/com/openhippy/example/HippyEngineWrapper.kt b/framework/examples/android-demo/src/main/java/com/openhippy/example/HippyEngineWrapper.kt index 5c95d1b81cf..ea38d5ce1d1 100644 --- a/framework/examples/android-demo/src/main/java/com/openhippy/example/HippyEngineWrapper.kt +++ b/framework/examples/android-demo/src/main/java/com/openhippy/example/HippyEngineWrapper.kt @@ -47,7 +47,7 @@ class HippyEngineWrapper//TODO: Coming soon var hippyRootView: ViewGroup? = null var hippySnapshotView: ViewGroup? = null var devButton: View? = null - var snapshot: Bitmap? = null + var screenshot: Bitmap? = null var pageItem: View? = null var isDebugMode: Boolean = isDebug var isSnapshotMode: Boolean = useNodeSnapshot @@ -177,7 +177,7 @@ class HippyEngineWrapper//TODO: Coming soon ) var snapshotView: View? = null if (!isDebugMode && isSnapshotMode) { - var buffer = renderNodeSnapshot[driverMode] + val buffer = renderNodeSnapshot[driverMode] buffer?.let { snapshotView = hippyEngine.replaySnapshot(context, it) } @@ -204,7 +204,7 @@ class HippyEngineWrapper//TODO: Coming soon } }) - var loadCallbackTask = Runnable { + val loadCallbackTask = Runnable { callback.onCreateRootView(hippyRootView) snapshotView?.let { callback.onReplaySnapshotViewCompleted(snapshotView as ViewGroup) @@ -222,6 +222,29 @@ class HippyEngineWrapper//TODO: Coming soon }) } + fun buildRootViewScreenshot(context: Context, callback: PageConfiguration.GenerateScreenshotCallback) { + hippyRootView?:let { + callback.onScreenshotBuildFinished() + return + } + try { + hippyEngine.getScreenshotBitmapForView(context, hippyRootView as View + ) { bitmap, result -> + run { + if (result == 0) { + screenshot = bitmap + } else { + LogUtils.e("Demo", "buildRootViewScreenshot error code: $result") + } + callback.onScreenshotBuildFinished() + } + } + } catch (e: IllegalArgumentException) { + LogUtils.e("Demo", "buildRootViewScreenshot exception message: ${e.message}") + callback.onScreenshotBuildFinished() + } + } + interface HippyEngineLoadCallback { fun onInitEngineCompleted(statusCode: EngineInitStatus, msg: String?) fun onCreateRootView(hippyRootView: ViewGroup?) diff --git a/framework/examples/android-demo/src/main/java/com/openhippy/example/PageConfiguration.kt b/framework/examples/android-demo/src/main/java/com/openhippy/example/PageConfiguration.kt index 9d3cf3aaccb..63ef203f3fb 100644 --- a/framework/examples/android-demo/src/main/java/com/openhippy/example/PageConfiguration.kt +++ b/framework/examples/android-demo/src/main/java/com/openhippy/example/PageConfiguration.kt @@ -16,18 +16,14 @@ package com.openhippy.example import android.app.Dialog -import android.graphics.Bitmap -import android.graphics.Canvas import android.graphics.Color -import android.graphics.Rect -import android.os.Build import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.text.SpannableString import android.text.Spanned import android.text.style.ForegroundColorSpan -import android.view.* +import android.view.Gravity +import android.view.View +import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import android.widget.Toast @@ -91,9 +87,7 @@ class PageConfiguration : AppCompatActivity(), View.OnClickListener { } override fun onStop() { - hippyEngineWrapper?.let { - it.onStop() - } + hippyEngineWrapper?.onStop() super.onStop() } @@ -146,15 +140,14 @@ class PageConfiguration : AppCompatActivity(), View.OnClickListener { if (!it.isDebugMode) { hippyEngineWrapper?.recordRenderNodeSnapshot() } + it.buildRootViewScreenshot(this, object : GenerateScreenshotCallback { + override fun onScreenshotBuildFinished() { + (pageConfigurationContainer as ViewGroup).removeAllViews() + runnable.run() + hippyEngineWrapper = null + } + }) } - generateBitmapFromView(rootView, object : SnapshotBuildCallback { - override fun onSnapshotReady(bitmap: Bitmap?) { - hippyEngineWrapper?.snapshot = bitmap - (pageConfigurationContainer as ViewGroup).removeAllViews() - runnable.run() - hippyEngineWrapper = null - } - }) } } @@ -205,56 +198,13 @@ class PageConfiguration : AppCompatActivity(), View.OnClickListener { createButton.setOnClickListener { v -> val debugServerHostInput = pageConfigurationRoot.findViewById(R.id.page_configuration_debug_server_host_input) - (debugServerHostInput as AppCompatEditText)?.let { + (debugServerHostInput as AppCompatEditText).let { debugServerHost = it.text.toString() } onCreateClick() } } - private fun generateBitmapFromView(view: View, callback: SnapshotBuildCallback) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val temporalBitmap = - Bitmap.createBitmap(view.width, view.height, Bitmap.Config.ARGB_8888) - val location = IntArray(2) - view.getLocationInWindow(location) - val viewRectangle = Rect( - location[0], - location[1], - location[0] + view.width, - location[1] + view.height - ) - val onPixelCopyListener: PixelCopy.OnPixelCopyFinishedListener = - PixelCopy.OnPixelCopyFinishedListener { copyResult -> - if (copyResult == PixelCopy.SUCCESS) { - callback.onSnapshotReady(temporalBitmap) - } else { - callback.onSnapshotReady(null) - } - } - PixelCopy.request( - window, - viewRectangle, - temporalBitmap, - onPixelCopyListener, - Handler(Looper.getMainLooper()) - ) - } else { - val bitmap = Bitmap.createBitmap( - view.width, view.height, Bitmap.Config.ARGB_8888 - ) - val canvas = Canvas(bitmap) - view.draw(canvas) - canvas.setBitmap(null) - callback.onSnapshotReady(bitmap) - } - } catch (e: Exception) { - e.printStackTrace() - callback.onSnapshotReady(null) - } - } - private fun onCreateClick() { (pageConfigurationTitle as TextView).text = resources.getText(R.string.page_configuration_navigation_title_demo) @@ -384,7 +334,7 @@ class PageConfiguration : AppCompatActivity(), View.OnClickListener { } } - interface SnapshotBuildCallback { - fun onSnapshotReady(bitmap: Bitmap?) + interface GenerateScreenshotCallback { + fun onScreenshotBuildFinished() } } \ No newline at end of file diff --git a/framework/examples/android-demo/src/main/java/com/openhippy/example/PageManagement.kt b/framework/examples/android-demo/src/main/java/com/openhippy/example/PageManagement.kt index b1e4406c244..ebd9d6d32a0 100644 --- a/framework/examples/android-demo/src/main/java/com/openhippy/example/PageManagement.kt +++ b/framework/examples/android-demo/src/main/java/com/openhippy/example/PageManagement.kt @@ -91,8 +91,8 @@ class PageManagement : AppCompatActivity() { for (hippyEngineWrapper in hippyEngineList) { hippyEngineWrapper.pageItem?.let { val pageItemImage = it.findViewById(R.id.page_item_image) - hippyEngineWrapper.snapshot?.let { - pageItemImage.setImageBitmap(hippyEngineWrapper.snapshot) + hippyEngineWrapper.screenshot?.let { + pageItemImage.setImageBitmap(hippyEngineWrapper.screenshot) } } } @@ -194,7 +194,7 @@ class PageManagement : AppCompatActivity() { pageItemTips.text = resources.getText(R.string.page_add_item_tips_text) pageItemTipsImage.setImageResource(R.drawable.page_item_add_4x) } else { - hippyEngineWrapper.snapshot?.let { + hippyEngineWrapper.screenshot?.let { pageItemImage.setImageBitmap(it) } deletePageButton.setOnClickListener { v -> diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderer.java b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderer.java index bf2e3586877..f241db49e0e 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderer.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderer.java @@ -720,6 +720,10 @@ public void updateLayout(final int rootId, @NonNull List nodeList) final int height = Math.round(MapUtils.getFloatValue(layoutInfo, LAYOUT_HEIGHT)); final TextRenderSupplier supplier = mVirtualNodeManager .updateLayout(rootId, nodeId, width, layoutInfo); + if (LogUtils.isDebugMode()) { +// LogUtils.d(TAG, "updateLayout: id " + nodeId + ", left " + left +// + ", top " + top + ", width " + width + ", height " + height + "\n "); + } // If restoring snapshots, update layout is called directly on the UI thread, // and do not need to use the UI task if (rootId == SCREEN_SNAPSHOT_ROOT_ID) { @@ -795,9 +799,9 @@ public void callUIFunction(final int rootId, final int nodeId, final long callba TAG + ": callUIFunction: invalid negative id=" + nodeId); } if (LogUtils.isDebugMode()) { - LogUtils.d(TAG, - "callUIFunction: id " + nodeId + ", functionName " + functionName + ", params" - + params + "\n "); +// LogUtils.d(TAG, +// "callUIFunction: id " + nodeId + ", functionName " + functionName + ", params" +// + params + "\n "); } // If callbackId equal to 0 mean this call does not need to callback. final UIPromise promise =