diff --git a/docs/api/hippy-react/modules.md b/docs/api/hippy-react/modules.md index 5e2ae4e5eb5..d7a5e8de5a6 100644 --- a/docs/api/hippy-react/modules.md +++ b/docs/api/hippy-react/modules.md @@ -281,6 +281,22 @@ AsyncStorage 是一个简单的、异步的、持久化的 Key-Value 存储系 --- +# FontLoaderModule + +提供通过url动态下载远程字体或者动态加载JSBundle包中字体的能力,下载的远程字体将保存在应用Cache目录下,由Hippy统一管理,可能被终端系统删除。常用字体不推荐使用该模块动态远程下载。 + + +## 方法 + +### FontLoaderModule.load + +`(fontFamily: string, fontUrl: string) => Promise` 通过fontUrl异步下载字体,下载完成后会刷新终端文本。 + +> - fontFamily - 下载字体的字体家族,用于保存文件和检索字体文件 +> - fontUrl - 下载字体的地址,可以是http网络地址,也可以本地文件地址或者以“hpfile://./”为前缀的JSbundle包中的相对地址 + +--- + # NetInfo [[NetInfo 范例]](//github.com/Tencent/Hippy/tree/master/examples/hippy-react-demo/src/modules/NetInfo) diff --git a/docs/api/hippy-vue/vue-native.md b/docs/api/hippy-vue/vue-native.md index d5bca3599d3..cffa04ab28d 100644 --- a/docs/api/hippy-vue/vue-native.md +++ b/docs/api/hippy-vue/vue-native.md @@ -294,6 +294,21 @@ console.log(Vue.Native.getElemCss(this.demon1Point)) // => { height: 80, left: 0 --- +# FontLoaderModule + +提供通过url动态下载远程字体或者动态加载JSBundle包中字体的能力,下载的远程字体将保存在应用Cache目录下,由Hippy统一管理,可能被终端系统删除。常用字体不推荐使用该模块动态远程下载。 + +## 方法 + +### FontLoader.load + +`(fontFamily: string, fontUrl: string) => Promise` 通过fontUrl异步下载字体,下载完成后会刷新终端文本。 + +> * fontFamily - 下载字体的字体家族,用于保存文件和检索字体文件 +> * fontUrl - 下载字体的地址,可以是http网络地址,也可以本地文件地址或者以“hpfile://./”为前缀的JSbundle包中的相对地址 + +--- + # ImageLoader 通过该模块可以对远程图片进行相应操作 diff --git a/docs/api/style/appearance.md b/docs/api/style/appearance.md index e96d56b7f03..1e180cbb9b4 100644 --- a/docs/api/style/appearance.md +++ b/docs/api/style/appearance.md @@ -164,6 +164,14 @@ | ------------------------ | ---- | ------------ | | enum('normal', 'italic') | 否 | Android、iOS | +# fontUrl + +动态加载字体的url,会在渲染时异步动态加载,可以是远程字体地址或者是使用JSBundle包中字体路径的相对地址。 + +| 类型 | 必需 | 支持平台 | +| ------------------------ | ---- | ------------ | +| string | 否 | Android、iOS | + # fontWeight 字体粗细 diff --git a/driver/js/examples/hippy-react-demo/src/modules/FontLoader/index.jsx b/driver/js/examples/hippy-react-demo/src/modules/FontLoader/index.jsx new file mode 100644 index 00000000000..a5b00fff2b0 --- /dev/null +++ b/driver/js/examples/hippy-react-demo/src/modules/FontLoader/index.jsx @@ -0,0 +1,141 @@ +import React from 'react'; +import { + View, + ScrollView, + StyleSheet, + Text, + TextInput, + FontLoaderModule, +} from '@hippy/react'; + +const styles = StyleSheet.create({ + itemTitle: { + alignItems: 'flex-start', + justifyContent: 'center', + height: 40, + borderWidth: 1, + borderStyle: 'solid', + borderColor: '#e0e0e0', + borderRadius: 2, + backgroundColor: '#fafafa', + padding: 10, + marginTop: 10, + }, + wrapper: { + borderColor: '#eee', + borderWidth: 1, + borderStyle: 'solid', + paddingHorizontal: 10, + paddingVertical: 5, + marginVertical: 10, + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'flex-start', + }, + infoContainer: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'flex-start', + marginTop: 5, + marginBottom: 5, + flexWrap: 'wrap', + }, + text_style: { + fontSize: 16, + }, + input_url_style: { + height: 60, + marginVertical: 10, + fontSize: 16, + color: '#242424', + }, + input_font_style: { + height: 30, + marginVertical: 10, + fontSize: 16, + color: '#242424', + }, +}); + +export default class LoadFontExample extends React.Component { + constructor(props) { + super(props); + this.state = { + fontFamily: '', + inputFontFamily: '', + fontUrl: '', + loadState: '', + }; + } + + fillExample() { + this.setState({ inputFontFamily: 'TencentSans W7' }); + this.setState({ fontUrl: 'https://infra-packages.openhippy.com/test/resources/TencentSans_W7.ttf' }); + // this.setState({ fontUrl: 'hpfile://./assets/hanyihuaxianzijianti.ttf' } + } + + async loadFont() { + this.setState({ fontFamily: this.state.inputFontFamily }); + await FontLoaderModule.load(this.state.fontFamily, this.state.fontUrl) + .then((result) => { + this.setState({ loadState: result }); + }) + .catch((error) => { + this.setState({ loadState: error }); + }); + } + + render() { + return ( + + + 通过组件fontUrl属性动态下载并使用字体 + + + This sentence will use font 'TencentSans W3' downloaded dynamically according to 'fontUrl' property. + + + 这句话将使用通过fontUrl属性下载的'TencentSans W3'字体. + + + 下载并使用字体 + + + This sentence will be set the specific font after download. + + + 这句话将用指定的下载字体显示。 + + this.setState({inputFontFamily: text})} + /> + this.setState({fontUrl: text})} + /> + + + this.fillExample()}> + 填充示例 + + this.loadFont()}> + 下载字体 + + + + load state: {this.state.loadState} + + ); + } +} diff --git a/driver/js/examples/hippy-react-demo/src/modules/index.js b/driver/js/examples/hippy-react-demo/src/modules/index.js index 6ff316a0c40..37ae85f39da 100644 --- a/driver/js/examples/hippy-react-demo/src/modules/index.js +++ b/driver/js/examples/hippy-react-demo/src/modules/index.js @@ -5,3 +5,4 @@ export { default as AsyncStorage } from './AsyncStorage'; export { default as NetInfo } from './NetInfo'; export { default as WebSocket } from './WebSocket'; export { default as UIManagerModule } from './UIManagerModule'; +export { default as FontLoader } from './FontLoader'; diff --git a/driver/js/examples/hippy-react-demo/src/routes.js b/driver/js/examples/hippy-react-demo/src/routes.js index f829521fd03..e7b0cfa2c4b 100644 --- a/driver/js/examples/hippy-react-demo/src/routes.js +++ b/driver/js/examples/hippy-react-demo/src/routes.js @@ -184,6 +184,14 @@ export default [ type: Type.MODULE, }, }, + { + path: '/FontLoader', + name: 'FontLoader 模块', + component: PAGE_LIST.FontLoader, + meta: { + type: Type.MODULE, + }, + }, { path: '/Others', name: 'Others', diff --git a/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-vue-native.vue b/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-vue-native.vue index 74b9e686359..622a555e6f2 100644 --- a/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-vue-native.vue +++ b/driver/js/examples/hippy-vue-demo/src/components/native-demos/demo-vue-native.vue @@ -248,6 +248,48 @@ + +
+ + + This sentence will be set the specific font after download. + + + 这句话将用指定的下载字体显示。 + + + +
+ + +
+ load state: {{ loadState }} +
+
{ this.netInfoText = `收到通知: ${info.network_info}`; }); - fetch('https://hippyjs.org', { + fetch('https://openhippy.com/', { mode: 'no-cors', // 2.14.0 or above supports other options(not only method/headers/url/body) }).then((responseJson) => { this.fetchText = `成功状态: ${responseJson.status}`; @@ -402,12 +448,29 @@ export default { console.log('ImageLoader getSize', result); this.imageSize = `${result.width}x${result.height}`; }, + async load() { + this.fontFamily = this.inputFontFaimly; + console.log('fontFamily:', this.fontFamily) + console.log('fontUrl:', this.fontUrl) + let result; + try { + result = await Vue.Native.FontLoader.load(this.fontFamily, this.fontUrl); + } catch (error) { + result = error.message; + } + this.loadState = result; + }, + fillExample() { + this.inputFontFaimly = 'HYHuaXianZi J'; + this.fontUrl = 'https://zf.sc.chinaz.com/Files/DownLoad/upload/2024/1009/hanyihuaxianzijianti.ttf'; + }, + setCookie() { - Vue.Native.Cookie.set('https://hippyjs.org', 'name=hippy;network=mobile'); + Vue.Native.Cookie.set('https://openhippy.com/', 'name=hippy;network=mobile'); this.cookieString = '\'name=hippy;network=mobile\' is set'; }, getCookie() { - Vue.Native.Cookie.getAll('https://hippyjs.org').then((cookies) => { + Vue.Native.Cookie.getAll('https://openhippy.com/').then((cookies) => { this.cookiesValue = cookies; }); }, diff --git a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue index 6405d34d68e..f62d7ec52f9 100644 --- a/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue +++ b/driver/js/examples/hippy-vue-next-demo/src/components/native-demo/demo-vue-native.vue @@ -249,6 +249,50 @@
+ +
+ + + This sentence will be set the specific font after download. + + + 这句话将用指定的下载字体显示。 + + + +
+ + +
+ load state: {{ loadState }} +
+
@@ -337,6 +381,10 @@ export default defineComponent({ const cookieString = ref('ready to set'); const cookiesValue = ref(''); const eventTriggeredTimes = ref(0); + const inputFontFaimly = ref(''); + const fontUrl = ref(''); + const fontFamily = ref(''); + const loadState = ref(''); /** * set local storage @@ -375,12 +423,29 @@ export default defineComponent({ imageSize.value = `${result.width}x${result.height}`; }; + const load = async () => { + fontFamily.value = inputFontFaimly.value; + console.log('load fontFamily:', fontFamily.value) + console.log('load fontUrl:', fontUrl.value) + let result; + try { + result = await Native.FontLoader.load(fontFamily.value, fontUrl.value); + } catch (error) { + result = error.message; + } + loadState.value = result; + }; + const fillExample = () => { + inputFontFaimly.value = 'HYHuaXianZi J'; + fontUrl.value = 'https://zf.sc.chinaz.com/Files/DownLoad/upload/2024/1009/hanyihuaxianzijianti.ttf'; + }; + const setCookie = () => { - Native.Cookie.set('https://hippyjs.org', 'name=hippy;network=mobile'); + Native.Cookie.set('https://openhippy.com/', 'name=hippy;network=mobile'); cookieString.value = '\'name=hippy;network=mobile\' is set'; }; const getCookie = () => { - Native.Cookie.getAll('https://hippyjs.org').then((cookies) => { + Native.Cookie.getAll('https://openhippy.com/').then((cookies) => { cookiesValue.value = cookies; }); }; @@ -412,7 +477,7 @@ export default defineComponent({ netInfoText.value = `收到通知: ${info.network_info}`; }); - fetch('https://hippyjs.org', { + fetch('https://openhippy.com/', { mode: 'no-cors', // 2.14.0 or above supports other options(not only method/headers/url/body) }) .then((responseJson) => { @@ -441,6 +506,8 @@ export default defineComponent({ cookieString, cookiesValue, getSize, + load, + fillExample, setItem, getItem, removeItem, @@ -449,6 +516,10 @@ export default defineComponent({ getBoundingClientRect, triggerAppEvent, eventTriggeredTimes, + inputFontFaimly, + fontUrl, + fontFamily, + loadState, }; }, beforeDestroy() { diff --git a/driver/js/packages/hippy-react/src/index.ts b/driver/js/packages/hippy-react/src/index.ts index 46333be954d..1d1823189e7 100644 --- a/driver/js/packages/hippy-react/src/index.ts +++ b/driver/js/packages/hippy-react/src/index.ts @@ -62,6 +62,7 @@ const { Device, HippyRegister, ImageLoader: ImageLoaderModule, + FontLoader: FontLoaderModule, NetworkInfo: NetInfo, UIManager: UIManagerModule, flushSync, @@ -118,6 +119,7 @@ export { Clipboard, ConsoleModule, ImageLoaderModule, + FontLoaderModule, Platform, BackAndroid, Animation, diff --git a/driver/js/packages/hippy-react/src/modules/font-loader-module.ts b/driver/js/packages/hippy-react/src/modules/font-loader-module.ts new file mode 100644 index 00000000000..1afc64133fe --- /dev/null +++ b/driver/js/packages/hippy-react/src/modules/font-loader-module.ts @@ -0,0 +1,35 @@ +/* + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2017-2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Bridge } from '../global'; + +/** + * Load font from remote url. + * + * @param {string} url - Remote font url. + * @param {string} fontFamily - Remote fontFamily name. + */ +function load(fontFamily: string, url: string) { + return Bridge.callNativeWithPromise('FontLoaderModule', 'load', fontFamily, url); +} + +export { + load, +}; diff --git a/driver/js/packages/hippy-react/src/native.ts b/driver/js/packages/hippy-react/src/native.ts index b92204a81a3..bff83706612 100644 --- a/driver/js/packages/hippy-react/src/native.ts +++ b/driver/js/packages/hippy-react/src/native.ts @@ -22,6 +22,7 @@ import * as HippyGlobal from './global'; import * as Clipboard from './modules/clipboard'; import * as Cookie from './modules/cookie-module'; import * as ImageLoader from './modules/image-loader-module'; +import * as FontLoader from './modules/font-loader-module'; import * as NetworkInfo from './modules/network-info'; import * as UIManager from './modules/ui-manager-module'; import BackAndroid from './modules/back-android'; @@ -52,6 +53,7 @@ export { Device, HippyRegister, ImageLoader, + FontLoader, NetworkInfo, UIManager, flushSync, diff --git a/driver/js/packages/hippy-vue-next/src/runtime/native/index.ts b/driver/js/packages/hippy-vue-next/src/runtime/native/index.ts index 2865236fe0c..14a910bf969 100644 --- a/driver/js/packages/hippy-vue-next/src/runtime/native/index.ts +++ b/driver/js/packages/hippy-vue-next/src/runtime/native/index.ts @@ -241,6 +241,10 @@ export interface NativeApiType { prefetch: (url: string) => void; }; + FontLoader: { + load: (fontFamily: string, url: string) => Promise; + }; + // include window and screen info Dimensions: Dimensions; @@ -517,6 +521,24 @@ export const Native: NativeApiType = { }, }, + FontLoader: { + /** + * Download the font from the url. + * + * @param fontFamily + * @param url - font url + */ + load(fontFamily: string, url: string): Promise { + return Native.callNativeWithPromise.call( + this, + 'FontLoaderModule', + 'load', + fontFamily, + url, + ); + }, + }, + /** * Get the screen or view size. */ diff --git a/driver/js/packages/hippy-vue-next/src/types/native-modules.ts b/driver/js/packages/hippy-vue-next/src/types/native-modules.ts index b57d0a98daa..f095d7fc2e0 100644 --- a/driver/js/packages/hippy-vue-next/src/types/native-modules.ts +++ b/driver/js/packages/hippy-vue-next/src/types/native-modules.ts @@ -22,6 +22,7 @@ import type { ClipboardModule } from './native-modules/clip-board-module'; import type { DeviceEventModule } from './native-modules/device-event-module'; import type { Http } from './native-modules/http'; import type { ImageLoaderModule } from './native-modules/image-loader-module'; +import type { FontLoaderModule } from './native-modules/font-loader-module'; import type { NetInfo } from './native-modules/net-info'; import type { Network } from './native-modules/network'; import type { TestModule } from './native-modules/test-module'; @@ -32,6 +33,7 @@ export interface NativeInterfaceMap { // The key here is the module name set by the native and cannot be changed at will. UIManagerModule: UiManagerModule; ImageLoaderModule: ImageLoaderModule; + FontLoaderModule: FontLoaderModule; websocket: Websocket; NetInfo: NetInfo; ClipboardModule: ClipboardModule; diff --git a/driver/js/packages/hippy-vue-next/src/types/native-modules/font-loader-module.ts b/driver/js/packages/hippy-vue-next/src/types/native-modules/font-loader-module.ts new file mode 100644 index 00000000000..73cbbd664e3 --- /dev/null +++ b/driver/js/packages/hippy-vue-next/src/types/native-modules/font-loader-module.ts @@ -0,0 +1,23 @@ +/* + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2022 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface FontLoaderModule { + load: (fontFamily: string, url: string) => string; +} diff --git a/driver/js/packages/hippy-vue/src/runtime/native.ts b/driver/js/packages/hippy-vue/src/runtime/native.ts index 5ad74bbfdd7..8d1966a8622 100644 --- a/driver/js/packages/hippy-vue/src/runtime/native.ts +++ b/driver/js/packages/hippy-vue/src/runtime/native.ts @@ -443,6 +443,20 @@ const Native: NeedToTyped = { callNative.call(this, 'ImageLoaderModule', 'prefetch', url); }, }, + /** + * operations for font + */ + FontLoader: { + /** + * Download the font from the url. + * + * @param {string} fontFamily - The font family to download, + * @param {string} url - The url where to download the font. + */ + load(fontFamily: string, url: string) { + return callNativeWithPromise.call(this, 'FontLoaderModule', 'load', fontFamily, url); + }, + }, /** * Network operations */ diff --git a/framework/android/connector/renderer/native/src/main/java/com/openhippy/connector/NativeRenderer.java b/framework/android/connector/renderer/native/src/main/java/com/openhippy/connector/NativeRenderer.java index 4ed01ea69f5..d87cac6e6a9 100644 --- a/framework/android/connector/renderer/native/src/main/java/com/openhippy/connector/NativeRenderer.java +++ b/framework/android/connector/renderer/native/src/main/java/com/openhippy/connector/NativeRenderer.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.tencent.mtt.hippy.common.Callback; +import com.tencent.mtt.hippy.modules.Promise; import com.tencent.renderer.FrameworkProxy; import com.tencent.renderer.RenderProxy; import java.util.List; @@ -143,6 +144,15 @@ public void destroy() { mRenderer = null; } + @Override + public void loadFontAndRefreshWindow(@NonNull String fontFamily, @NonNull String fontUrl, + int rootId, Object promise) { + Promise pm = (promise instanceof Promise) ? ((Promise) promise) : null; + if (mRenderer != null) { + mRenderer.loadFontAndRefreshWindow(fontFamily, fontUrl, rootId, pm); + } + } + @Override public int getInstanceId() { return mInstanceId; diff --git a/framework/android/connector/support/src/main/java/com/openhippy/connector/RenderConnector.java b/framework/android/connector/support/src/main/java/com/openhippy/connector/RenderConnector.java index d3ffc92db65..8933044dbff 100644 --- a/framework/android/connector/support/src/main/java/com/openhippy/connector/RenderConnector.java +++ b/framework/android/connector/support/src/main/java/com/openhippy/connector/RenderConnector.java @@ -19,8 +19,10 @@ import android.content.Context; import android.view.View; import android.view.ViewGroup; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import java.util.List; import java.util.Map; @@ -33,6 +35,8 @@ public interface RenderConnector extends Connector { void onRuntimeInitialized(int rootId); + void loadFontAndRefreshWindow(@NonNull String fontFamily, @NonNull String fontUrl, int rootId, Object promise); + void recordSnapshot(int rootId, @NonNull Object callback); View replaySnapshot(@NonNull Context context, @NonNull byte[] buffer); diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineContext.java b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineContext.java index 17b7f195c39..44e00dbd68e 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineContext.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/HippyEngineContext.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.openhippy.connector.JsDriver; +import com.openhippy.connector.RenderConnector; import com.tencent.devtools.DevtoolsManager; import com.tencent.mtt.hippy.HippyEngine.ModuleLoadStatus; import com.tencent.mtt.hippy.bridge.HippyBridgeManager; @@ -65,6 +66,8 @@ public interface HippyEngineContext extends BaseEngineContext { ThreadExecutor getThreadExecutor(); + RenderConnector getRenderer(); + ViewGroup getRootView(); View getRootView(int rootId); 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 769a3cff1b1..7ad440c80e9 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 @@ -940,7 +940,7 @@ DomManager getDomManager() { } @NonNull - RenderConnector getRenderer() { + public RenderConnector getRenderer() { return mRenderer; } diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/bridge/HippyCoreAPI.java b/framework/android/src/main/java/com/tencent/mtt/hippy/bridge/HippyCoreAPI.java index e399f4e94f1..165fefc89ed 100644 --- a/framework/android/src/main/java/com/tencent/mtt/hippy/bridge/HippyCoreAPI.java +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/bridge/HippyCoreAPI.java @@ -27,6 +27,7 @@ import com.tencent.mtt.hippy.modules.nativemodules.console.ConsoleModule; import com.tencent.mtt.hippy.modules.nativemodules.deviceevent.DeviceEventModule; import com.tencent.mtt.hippy.modules.nativemodules.exception.ExceptionModule; +import com.tencent.mtt.hippy.modules.nativemodules.font.FontLoaderModule; import com.tencent.mtt.hippy.modules.nativemodules.image.ImageLoaderModule; import com.tencent.mtt.hippy.modules.nativemodules.netinfo.NetInfoModule; import com.tencent.mtt.hippy.modules.nativemodules.network.NetworkModule; @@ -84,6 +85,12 @@ public HippyNativeModuleBase get() { return new ImageLoaderModule(context); } }); + modules.put(FontLoaderModule.class, new Provider() { + @Override + public HippyNativeModuleBase get() { + return new FontLoaderModule(context); + } + }); modules.put(NetworkModule.class, new Provider() { @Override public HippyNativeModuleBase get() { diff --git a/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/font/FontLoaderModule.java b/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/font/FontLoaderModule.java new file mode 100644 index 00000000000..e0d1aee07af --- /dev/null +++ b/framework/android/src/main/java/com/tencent/mtt/hippy/modules/nativemodules/font/FontLoaderModule.java @@ -0,0 +1,41 @@ +/* Tencent is pleased to support the open source community by making Hippy available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.mtt.hippy.modules.nativemodules.font; + +import com.openhippy.connector.RenderConnector; +import com.tencent.mtt.hippy.HippyEngineContext; +import com.tencent.mtt.hippy.annotation.HippyMethod; +import com.tencent.mtt.hippy.annotation.HippyNativeModule; +import com.tencent.mtt.hippy.modules.Promise; +import com.tencent.mtt.hippy.modules.nativemodules.HippyNativeModuleBase; + +@HippyNativeModule(name = "FontLoaderModule") +public class FontLoaderModule extends HippyNativeModuleBase { + + private final int rootId; + + public FontLoaderModule(HippyEngineContext context) { + super(context); + rootId = context.getRootView().getId(); + } + + @HippyMethod(name = "load") + public void load(final String fontFamily, final String fontUrl, final Promise promise) { + RenderConnector renderer = mContext.getRenderer(); + renderer.loadFontAndRefreshWindow(fontFamily, fontUrl, rootId, promise); + } +} diff --git a/framework/ios/module/fontLoader/HippyFontLoaderModule.h b/framework/ios/module/fontLoader/HippyFontLoaderModule.h new file mode 100644 index 00000000000..9750caea4c4 --- /dev/null +++ b/framework/ios/module/fontLoader/HippyFontLoaderModule.h @@ -0,0 +1,95 @@ +/*! +* iOS SDK +* +* Tencent is pleased to support the open source community by making +* Hippy available. +* +* Copyright (C) 2019 THL A29 Limited, a Tencent company. +* All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import +#import "HippyBridgeModule.h" + +NS_ASSUME_NONNULL_BEGIN + +typedef NS_ENUM(NSInteger, HippyFontUrlState) { + HippyFontUrlPending = 0, + HippyFontUrlLoading = 1, + HippyFontUrlLoaded = 2, + HippyFontUrlFailed = 3, +}; + +/** + * @class HippyFontLoaderModule + * @brief This class is responsible for loading and registering fonts. + * + * This class is a hippy module, providing a load method for the front end to download and register fonts. + * It also provides method for native side to register font. + */ +@interface HippyFontLoaderModule : NSObject + +/** + * Get the font file path according to url. + * + * @param url - The url where font file is downloaded + * @return The font file path. Null means the font file has't been downloaded from url. + */ ++ (nullable NSString *)getFontPath:(NSString *)url; + +/** + * Register font files belong to the specific font family if needed. + * + * @param fontFamily - The font family needs to be registered + */ ++ (BOOL)registerFontIfNeeded:(NSString *)fontFamily; + +/** + * If the font in the url has not been downloaded, download the font. + * Function will be called when downloading fonts through url property of text component. + * + * @param fontFamily - The font family needs to be downloaded + * @param url - The font url needs to download from. + */ ++ (void)loadFontIfNeeded:(NSString *)fontFamily fromUrl:(NSString *)url; + +/** + * Whether the font is downloading from the url. + * + * @param url - The font url needs to download from. + * @return Yes if the font is downloading from the url. + */ ++ (BOOL)isUrlLoading:(NSString *)url; + +/** + * Get the serial queue in HippyFontLoaderModule for asyn serial operations. + * + * @return The serial dispatch_queue_t. + */ ++ (dispatch_queue_t)getFontSerialQueue; + +/** + * Load font from the url. + * + * @param urlString - The url where font file is downloaded. + * @param resolve - The callback block for downloading successful. + * @param reject - The callback block for downloading failed. + */ +- (void)load:(NSString *)fontFamily from:(NSString *)urlString resolver:(nullable HippyPromiseResolveBlock)resolve + rejecter:(nullable HippyPromiseRejectBlock)reject; + +@end + +NS_ASSUME_NONNULL_END diff --git a/framework/ios/module/fontLoader/HippyFontLoaderModule.mm b/framework/ios/module/fontLoader/HippyFontLoaderModule.mm new file mode 100644 index 00000000000..0cead5585b0 --- /dev/null +++ b/framework/ios/module/fontLoader/HippyFontLoaderModule.mm @@ -0,0 +1,297 @@ +/*! +* iOS SDK +* +* Tencent is pleased to support the open source community by making +* Hippy available. +* +* Copyright (C) 2019 THL A29 Limited, a Tencent company. +* All rights reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ + +#import +#import "HippyFontLoaderModule.h" +#import +#import "HippyBridge+Private.h" +#import "HippyBridge+VFSLoader.h" +#import "HippyLog.h" +#import "VFSUriLoader.h" +#import "HippyVFSDefines.h" +#import "HippyUIManager.h" + + +static NSString *const HippyLoadFontNotification = @"HippyLoadFontNotification"; +static NSString *const HippyLoadFontUrlKey = @"fontUrl"; +static NSString *const HippyLoadFontFamilyKey = @"fontFamily"; +static NSString *const kFontLoaderModuleErrorDomain = @"kFontLoaderModuleErrorDomain"; +static NSUInteger const FontLoaderErrorUrlError = 1; +static NSUInteger const FontLoaderErrorDirectoryError = 2; +static NSUInteger const FontLoaderErrorRequestError = 3; +static NSUInteger const FontLoaderErrorRegisterError = 4; +static NSUInteger const FontLoaderErrorWriteFileError = 4; +static NSString *const HippyFontDirName = @"HippyFonts"; +static NSString *const HippyFontUrlCacheName = @"urlToFilePath.plist"; +static NSString *const HippyFontFamilyCacheName = @"fontFaimilyToFiles.plist"; + +static dispatch_queue_t gSerialQueue; +static NSMutableDictionary *gUrlToFilePath; +static NSMutableDictionary *gFontFamilyToFiles; +static NSMutableDictionary *gUrlLoadState; +static NSMutableArray *gFontRegistered; +static NSString *gFontDirPath; +static NSString *gFontUrlSavePath; +static NSString *gFontFamilySavePath; + + +@implementation HippyFontLoaderModule + +HIPPY_EXPORT_MODULE(FontLoaderModule) + +@synthesize bridge = _bridge; + +- (instancetype)init { + if ((self = [super init])) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; + [notificationCenter addObserver:self selector:@selector(loadFont:) name:HippyLoadFontNotification object:nil]; + + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); + NSString *cachesDirectory = [paths objectAtIndex:0]; + gFontDirPath = [cachesDirectory stringByAppendingPathComponent:HippyFontDirName]; + gFontUrlSavePath = [gFontDirPath stringByAppendingPathComponent:HippyFontUrlCacheName]; + gFontFamilySavePath = [gFontDirPath stringByAppendingPathComponent:HippyFontFamilyCacheName]; + gSerialQueue = dispatch_queue_create("com.tencent.hippy.FontLoaderQueue", DISPATCH_QUEUE_SERIAL); + }); + } + return self; +} + ++ (dispatch_queue_t)getFontSerialQueue { + return gSerialQueue; +} + ++ (void)setUrl:(NSString *)url state:(HippyFontUrlState)state { + if (!gUrlLoadState) { + gUrlLoadState = [NSMutableDictionary dictionary]; + } + [gUrlLoadState setObject:@(state) forKey:url]; +} + ++ (BOOL)isUrlLoading:(NSString *)url { + if (!gUrlLoadState) { + gUrlLoadState = [NSMutableDictionary dictionary]; + } + return [[gUrlLoadState objectForKey:url] integerValue] == HippyFontUrlLoading; +} + +// Read file to init dict if needed. This function will be called asynchronously. ++ (void)initDictIfNeeded { + if (gFontFamilyToFiles == nil) { + gFontFamilyToFiles = [NSMutableDictionary dictionaryWithContentsOfFile:gFontFamilySavePath]; + if (gFontFamilyToFiles == nil) { + gFontFamilyToFiles = [NSMutableDictionary dictionary]; + } + } + if (gUrlToFilePath == nil) { + gUrlToFilePath = [NSMutableDictionary dictionaryWithContentsOfFile:gFontUrlSavePath]; + if (gUrlToFilePath == nil) { + gUrlToFilePath = [NSMutableDictionary dictionary]; + } + } +} + ++ (void)loadFontIfNeeded:(NSString *)fontFamily fromUrl:(NSString *)url { + if (url && ![HippyFontLoaderModule isUrlLoading:url]) { + dispatch_async([HippyFontLoaderModule getFontSerialQueue], ^{ + NSString *fontPath = [HippyFontLoaderModule getFontPath:url]; + if (!fontPath && fontFamily) { + NSDictionary *userInfo = @{HippyLoadFontUrlKey: url, HippyLoadFontFamilyKey: fontFamily}; + [[NSNotificationCenter defaultCenter] postNotificationName:HippyLoadFontNotification object:nil userInfo:userInfo]; + } + }); + } +} + +- (void)loadFont:(NSNotification *)notification { + NSString *urlString = [notification.userInfo objectForKey:HippyLoadFontUrlKey]; + NSString *fontFamily = [notification.userInfo objectForKey:HippyLoadFontFamilyKey]; + [self load:fontFamily from:urlString resolver:nil rejecter:nil]; +} + ++ (NSString *)getFontPath:(NSString *)url { + [self initDictIfNeeded]; + NSString *fontFilePath = gUrlToFilePath[url]; + if (!fontFilePath) { + return nil; + } + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:fontFilePath]) { + return nil; + } + return fontFilePath; +} + ++ (BOOL)registerFontIfNeeded:(NSString *)fontFamily { + [self initDictIfNeeded]; + NSMutableArray *fontFiles = [gFontFamilyToFiles objectForKey:fontFamily]; + if (!gFontRegistered) { + gFontRegistered = [NSMutableArray array]; + } + BOOL isFontRegistered = NO; + if (fontFiles) { + NSMutableArray *fileNotExist = [NSMutableArray array]; + for (NSString *fontFile in fontFiles) { + if (![gFontRegistered containsObject:fontFile]) { + NSError *error = nil; + if ([self registerFontFromURL:fontFile error:&error]) { + [gFontRegistered addObject:fontFile]; + isFontRegistered = YES; + HippyLogInfo(@"register font \"%@\" success!", fontFile); + } else { + if (error.domain == kFontLoaderModuleErrorDomain && error.code == FontLoaderErrorRegisterError) { + [fileNotExist addObject:fontFile]; + } + HippyLogWarn(@"register font \"%@\" fail!", fontFile); + } + } + } + [fontFiles removeObjectsInArray:fileNotExist]; + if (isFontRegistered) { + [[NSNotificationCenter defaultCenter] postNotificationName:HippyFontChangeTriggerNotification object:nil]; + } + } + return isFontRegistered; +} + + ++ (BOOL)registerFontFromURL:(NSString *)urlString error:(NSError **)error { + NSURL *url = [NSURL fileURLWithPath:urlString]; + CGDataProviderRef fontDataProvider = CGDataProviderCreateWithURL((CFURLRef)url); + CGFontRef font = CGFontCreateWithDataProvider(fontDataProvider); + CGDataProviderRelease(fontDataProvider); + if (!font) { + *error = [NSError errorWithDomain:kFontLoaderModuleErrorDomain + code:FontLoaderErrorRegisterError userInfo:@{@"reason": @"font dosen't exist"}]; + return NO; + } + CFErrorRef cfError; + BOOL success = CTFontManagerRegisterGraphicsFont(font, &cfError); + CFRelease(font); + if (!success) { + *error = CFBridgingRelease(cfError); + return NO; + } + return YES; +} + +- (void)saveFontfamily:(NSString *)fontFamily url:(NSString *)url filePath:(NSString *)filePath { + [HippyFontLoaderModule initDictIfNeeded]; + [gUrlToFilePath setObject:filePath forKey:url]; + NSMutableArray *fontFiles = [gFontFamilyToFiles objectForKey:fontFamily]; + if (!fontFiles) { + fontFiles = [NSMutableArray arrayWithObject:filePath]; + [gFontFamilyToFiles setObject:fontFiles forKey:fontFamily]; + } else { + [fontFiles addObject:filePath]; + } + [gUrlToFilePath writeToFile:gFontUrlSavePath atomically:YES]; + [gFontFamilyToFiles writeToFile:gFontFamilySavePath atomically:YES]; +} + + +HIPPY_EXPORT_METHOD(load:(NSString *)fontFamily from:(NSString *)urlString resolver:(HippyPromiseResolveBlock)resolve rejecter:(HippyPromiseRejectBlock)reject) { + if (!urlString) { + NSError *error = [NSError errorWithDomain:kFontLoaderModuleErrorDomain + code:FontLoaderErrorUrlError userInfo:@{@"reason": @"url is empty"}]; + NSString *errorKey = [NSString stringWithFormat:@"%lu", FontLoaderErrorUrlError]; + if (reject) { + reject(errorKey, @"url is empty", error); + } + return; + } + + @synchronized (self) { + if ([HippyFontLoaderModule isUrlLoading:urlString]) { + resolve([NSString stringWithFormat:@"url \"%@\" is downloading!", urlString]); + return; + } + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlLoading]; + } + + __weak __typeof(self) weakSelf = self; + [self.bridge loadContentsAsyncFromUrl:urlString + params:nil + queue:nil + progress:nil + completionHandler:^(NSData *data, NSDictionary *userInfo, NSURLResponse *response, NSError *error) { + __strong __typeof(weakSelf) strongSelf = weakSelf; + if (error) { + if (reject) { + NSString *errorKey = [NSString stringWithFormat:@"%lu", FontLoaderErrorRequestError]; + reject(errorKey, @"font request error", error); + } + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlFailed]; + return; + } + // is local file url + if ([userInfo[HippyVFSResponseURLTypeKey] integerValue] == HippyVFSURLTypeFile) { + NSString *fontFilePath = userInfo[HippyVFSResponseAbsoluteURLStringKey] ?: urlString; + dispatch_async([HippyFontLoaderModule getFontSerialQueue], ^{ + [strongSelf saveFontfamily:fontFamily url:urlString filePath:fontFilePath]; + [HippyFontLoaderModule registerFontIfNeeded:fontFamily]; + }); + if (resolve) { + resolve([NSString stringWithFormat:@"load local font file \"%@\" success!", fontFilePath]); + } + @synchronized (strongSelf) { + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlLoaded]; + } + } else { // is http url + NSFileManager *fileManager = [NSFileManager defaultManager]; + if (![fileManager fileExistsAtPath:gFontDirPath]) { + NSError *error; + [fileManager createDirectoryAtPath:gFontDirPath withIntermediateDirectories:YES attributes:nil error:&error]; + if (error) { + NSString *errorKey = [NSString stringWithFormat:@"%lu", FontLoaderErrorDirectoryError]; + if (reject) { + reject(errorKey, @"directory create error", error); + } + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlFailed]; + return; + } + } + NSString *fileName = [fontFamily stringByAppendingFormat:@".%@", [response.suggestedFilename pathExtension]]; + NSString *fontFilePath = [gFontDirPath stringByAppendingPathComponent:fileName]; + if ([data writeToFile:fontFilePath atomically:YES]) { + dispatch_async([HippyFontLoaderModule getFontSerialQueue], ^{ + [strongSelf saveFontfamily:fontFamily url:urlString filePath:fontFilePath]; + [HippyFontLoaderModule registerFontIfNeeded:fontFamily]; + }); + if (resolve) { + resolve([NSString stringWithFormat:@"download font file \"%@\" success!", fileName]); + } + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlLoaded]; + } else { + if (reject) { + NSString *errorKey = [NSString stringWithFormat:@"%lu", FontLoaderErrorWriteFileError]; + reject(errorKey, @"font request error", error); + } + [HippyFontLoaderModule setUrl:urlString state:HippyFontUrlFailed]; + } + } + }]; +} + +@end diff --git a/hippy.podspec b/hippy.podspec index ef43c7e17ff..c4bcdd27ba3 100644 --- a/hippy.podspec +++ b/hippy.podspec @@ -365,6 +365,7 @@ Pod::Spec.new do |s| s.test_spec 'UnitTests' do |test_spec| test_spec.source_files = 'tests/ios/**/*.{h,m,mm}' + test_spec.resource_bundles = { 'TestFonts' => ['framework/examples/ios-demo/fonts/TTTGB-Medium.otf'], } test_spec.dependency 'OCMock' end diff --git a/modules/vfs/ios/HippyFileHandler.mm b/modules/vfs/ios/HippyFileHandler.mm index cd308b70c49..cbc20884082 100644 --- a/modules/vfs/ios/HippyFileHandler.mm +++ b/modules/vfs/ios/HippyFileHandler.mm @@ -25,6 +25,9 @@ #include "HippyFileHandler.h" #include "footstone/logging.h" +NSString *const HippyVFSResponseAbsoluteURLStringKey = @"HippyVFSResponseAbsoluteURLStringKey"; +NSString *const HippyVFSResponseURLTypeKey = @"HippyVFSResponseURLTypeKey"; + HippyFileHandler::HippyFileHandler(HippyBridge *bridge) { bridge_ = bridge; } @@ -82,12 +85,12 @@ } HippyBridge *bridge = bridge_; if (!bridge || !request) { - completion(nil, nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); + completion(nil, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeFile)}, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); return; } NSURL *url = [request URL]; if (!url) { - completion(nil, nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); + completion(nil, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeFile)}, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); return; } @@ -100,7 +103,7 @@ MIMEType:nil expectedContentLength:fileData.length textEncodingName:nil]; - completion(fileData, nil, rsp, error); + completion(fileData, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeFile), HippyVFSResponseAbsoluteURLStringKey: [absoluteURL path]}, rsp, error); }; if (queue) { [queue addOperationWithBlock:opBlock]; @@ -109,6 +112,6 @@ } } else { FOOTSTONE_DLOG(ERROR) << "HippyFileHandler cannot load url " << [[absoluteURL absoluteString] UTF8String]; - completion(nil, nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); + completion(nil, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeFile)}, nil, [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:nil]); } } diff --git a/modules/vfs/ios/HippyVFSDefines.h b/modules/vfs/ios/HippyVFSDefines.h index ecadb88f2d2..04f6092c581 100644 --- a/modules/vfs/ios/HippyVFSDefines.h +++ b/modules/vfs/ios/HippyVFSDefines.h @@ -39,6 +39,12 @@ enum HippyVFSRscType { HippyVFSRscTypeImage, }; +enum HippyVFSURLType { + HippyVFSURLTypeUnknown = 0, + HippyVFSURLTypeHTTP, + HippyVFSURLTypeFile, +}; + // Resource Type Key for VFS Request in `extraInfo` parameter, // Value is defined in HippyVFSRscType @@ -53,6 +59,13 @@ FOUNDATION_EXPORT NSString *_Nonnull const kHippyVFSRequestExtraInfoForCustomIma // The image returned in `userInfo` parameter of VFSHandlerCompletionBlock FOUNDATION_EXPORT NSString *_Nonnull const HippyVFSResponseDecodedImageKey; +// The absolute file url returned in `userInfo` parameter of VFSHandlerCompletionBlock +FOUNDATION_EXPORT NSString *_Nonnull const HippyVFSResponseAbsoluteURLStringKey; + +// The type of url returned in `userInfo` parameter of VFSHandlerCompletionBlock +// Value is defined in HippyVFSURLType +FOUNDATION_EXPORT NSString *_Nonnull const HippyVFSResponseURLTypeKey; + typedef void(^VFSHandlerProgressBlock)(NSUInteger current, NSUInteger total); typedef void(^VFSHandlerCompletionBlock)(NSData *_Nullable data, diff --git a/modules/vfs/ios/VFSUriHandler.mm b/modules/vfs/ios/VFSUriHandler.mm index 89f5e465473..59597018f25 100644 --- a/modules/vfs/ios/VFSUriHandler.mm +++ b/modules/vfs/ios/VFSUriHandler.mm @@ -209,7 +209,7 @@ static bool CheckRequestFromCPP(const std::unordered_map(hippy::JobResponse::RetCode::SchemeNotRegister); NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:code userInfo:userInfo]; - completion(nil, nil, nil, error); + completion(nil, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeHTTP)}, nil, error); return; } NSURLSessionDataProgress *dataProgress = [[NSURLSessionDataProgress alloc] initWithProgress:progress result:completion]; @@ -247,7 +247,7 @@ static bool CheckRequestFromCPP(const std::unordered_map(hippy::JobResponse::RetCode::ResourceNotFound); NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:code userInfo:userInfo]; - completion(nil, nil, nil, error); + completion(nil, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeHTTP)}, nil, error); return; } auto progressCallback = [progress](int64_t current, int64_t total){ @@ -274,7 +274,7 @@ static bool CheckRequestFromCPP(const std::unordered_map(cb->GetRetCode()); error = [NSError errorWithDomain:NSURLErrorDomain code:code userInfo:userInfo]; } - completion(data, nil, response, error); + completion(data, @{HippyVFSResponseURLTypeKey: @(HippyVFSURLTypeHTTP)}, response, error); } }; loader->hippy::UriLoader::RequestUntrustedContent(requestJob, responseCallback); diff --git a/renderer/native/android/src/main/cpp/include/renderer/native_render_jni.h b/renderer/native/android/src/main/cpp/include/renderer/native_render_jni.h index 2abeb3bf21e..a9d5146c098 100644 --- a/renderer/native/android/src/main/cpp/include/renderer/native_render_jni.h +++ b/renderer/native/android/src/main/cpp/include/renderer/native_render_jni.h @@ -41,6 +41,9 @@ jobject GetNativeRendererInstance(JNIEnv* j_env, jobject j_object, jint j_render_manager_id); + +void OnFontLoaded(JNIEnv *j_env, jobject j_object, jint j_render_manager_id, jint j_root_id); + void UpdateRootSize(JNIEnv* j_env, jobject j_obj, jint j_render_manager_id, jint j_root_id, jfloat width, jfloat height); diff --git a/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc b/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc index 59c8113db93..97fa4f18cc6 100644 --- a/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc +++ b/renderer/native/android/src/main/cpp/src/renderer/native_render_jni.cc @@ -77,6 +77,11 @@ REGISTER_JNI("com/tencent/renderer/NativeRenderProvider", "(IIFF)V", UpdateRootSize) +REGISTER_JNI("com/tencent/renderer/NativeRenderProvider", + "onFontLoaded", + "(II)V", + OnFontLoaded) + static jint JNI_OnLoad(__unused JavaVM* j_vm, __unused void* reserved) { auto j_env = JNIEnvironment::GetInstance()->AttachCurrentThread(); @@ -149,6 +154,51 @@ jobject GetNativeRendererInstance(JNIEnv* j_env, jobject j_object, jint j_render return nullptr; } +void MarkTextNodeDirtyRecursive(const std::shared_ptr& node) { + if (!node) { + return; + } + uint32_t child_count = node->GetChildCount(); + for (uint32_t i = 0; i < child_count; i++) { + MarkTextNodeDirtyRecursive(node->GetChildAt(i)); + } + if (node->GetViewName() == "TextInput" || node->GetViewName() == "Text") { + auto layout_node = node->GetLayoutNode(); + layout_node->MarkDirty(); + } +} + +void OnFontLoaded(JNIEnv *j_env, jobject j_object, jint j_render_manager_id, jint j_root_id) { + auto& map = NativeRenderManager::PersistentMap(); + std::shared_ptr render_manager; + bool ret = map.Find(static_cast(j_render_manager_id), render_manager); + if (!ret) { + FOOTSTONE_DLOG(WARNING) << "OnFontLoaded j_render_manager_id invalid"; + return; + } + std::shared_ptr dom_manager = render_manager->GetDomManager(); + if (dom_manager == nullptr) { + FOOTSTONE_DLOG(WARNING) << "OnFontLoaded dom_manager is nullptr"; + return; + } + auto& root_map = RootNode::PersistentMap(); + std::shared_ptr root_node; + uint32_t root_id = footstone::check::checked_numeric_cast(j_root_id); + ret = root_map.Find(root_id, root_node); + if (!ret) { + FOOTSTONE_DLOG(WARNING) << "OnFontLoaded root_node is nullptr"; + return; + } + + std::vector> ops; + ops.emplace_back([dom_manager, root_node]{ + MarkTextNodeDirtyRecursive(root_node); + dom_manager->DoLayout(root_node); + dom_manager->EndBatch(root_node); + }); + dom_manager->PostTask(Scene(std::move(ops))); +} + void UpdateRootSize(JNIEnv *j_env, jobject j_object, jint j_render_manager_id, jint j_root_id, jfloat j_width, jfloat j_height) { auto& map = NativeRenderManager::PersistentMap(); diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java index c1c9b1346d6..4d3048e8e0c 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/dom/node/NodeProps.java @@ -94,6 +94,7 @@ public class NodeProps { public static final String FONT_WEIGHT = "fontWeight"; public static final String FONT_STYLE = "fontStyle"; public static final String FONT_FAMILY = "fontFamily"; + public static final String FONT_URL = "fontUrl"; public static final String LINE_HEIGHT = "lineHeight"; public static final String LINE_SPACING_MULTIPLIER = "lineSpacingMultiplier"; public static final String LINE_SPACING_EXTRA = "lineSpacingExtra"; diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java index 231aefda270..b4063cb4076 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInput.java @@ -16,8 +16,6 @@ package com.tencent.mtt.hippy.views.textinput; -import static com.tencent.mtt.hippy.views.textinput.HippyTextInputController.UNSET; - import android.annotation.SuppressLint; import android.content.Context; import android.graphics.BlendMode; @@ -43,8 +41,10 @@ import android.view.inputmethod.InputMethodManager; import android.widget.EditText; import android.widget.TextView; + import androidx.annotation.Nullable; import androidx.appcompat.widget.AppCompatEditText; + import com.tencent.mtt.hippy.common.HippyMap; import com.tencent.mtt.hippy.uimanager.HippyViewBase; import com.tencent.mtt.hippy.uimanager.NativeGestureDispatcher; @@ -55,14 +55,17 @@ import com.tencent.renderer.NativeRendererManager; import com.tencent.renderer.component.Component; import com.tencent.renderer.component.text.FontAdapter; +import com.tencent.renderer.component.text.FontLoader; import com.tencent.renderer.component.text.TypeFaceUtil; import com.tencent.renderer.node.RenderNode; import com.tencent.renderer.utils.EventUtils; + import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; import java.util.Objects; +import java.util.concurrent.Executor; @SuppressWarnings({"deprecation", "unused"}) public class HippyTextInput extends AppCompatEditText implements HippyViewBase, @@ -96,14 +99,15 @@ public class HippyTextInput extends AppCompatEditText implements HippyViewBase, private int mLineHeight = 0; @Nullable private String mFontFamily; + private String mFontUrl; private Paint mTextPaint; + private FontLoader.FontLoadState mFontLoadState; public HippyTextInput(Context context) { super(context); setFocusable(true); setFocusableInTouchMode(true); setOverScrollMode(View.OVER_SCROLL_IF_CONTENT_SCROLLS); - mDefaultGravityHorizontal = getGravity() & (Gravity.HORIZONTAL_GRAVITY_MASK | Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK); @@ -206,6 +210,16 @@ public void setTextLineHeight(int lineHeight) { } public void onBatchComplete() { + NativeRender nativeRenderer = NativeRendererManager.getNativeRenderer(this.getContext()); + FontLoader fontLoader = null; + if (nativeRenderer != null) { + fontLoader = nativeRenderer.getFontLoader(); + } + if (mFontLoadState == FontLoader.FontLoadState.FONT_UNLOAD && fontLoader != null && + fontLoader.isFontLoaded(mFontFamily)) { + mShouldUpdateTypeface = true; + mFontLoadState = FontLoader.FontLoadState.FONT_LOADED; + } if (mShouldUpdateTypeface) { updateTypeface(); mShouldUpdateTypeface = false; @@ -759,6 +773,21 @@ public void setFontFamily(String family) { if (!Objects.equals(mFontFamily, family)) { mFontFamily = family; mShouldUpdateTypeface = true; + NativeRender nativeRenderer = NativeRendererManager.getNativeRenderer(this.getContext()); + FontLoader fontLoader = null; + if (nativeRenderer != null) { + fontLoader = nativeRenderer.getFontLoader(); + } + if (fontLoader != null && !fontLoader.isFontLoaded(mFontFamily)) { + mFontLoadState = FontLoader.FontLoadState.FONT_UNLOAD; + } + } + } + + public void setFontUrl(String fontUrl) { + if (!Objects.equals(mFontUrl, fontUrl)) { + mFontUrl = fontUrl; + mShouldUpdateTypeface = true; } } @@ -776,6 +805,18 @@ private void updateTypeface() { mTextPaint.reset(); } NativeRender nativeRenderer = NativeRendererManager.getNativeRenderer(getContext()); + if (!TextUtils.isEmpty(mFontUrl)) { + FontLoader loader = nativeRenderer == null ? null : nativeRenderer.getFontLoader(); + if (loader != null) { + int rootId = nativeRenderer.getRootView(this).getId(); + Executor executor = nativeRenderer.getBackgroundExecutor(); + if (executor != null) { + executor.execute(() -> { + loader.loadIfNeeded(mFontFamily, mFontUrl, rootId); + }); + } + } + } FontAdapter fontAdapter = nativeRenderer == null ? null : nativeRenderer.getFontAdapter(); TypeFaceUtil.apply(mTextPaint, mItalic, mFontWeight, mFontFamily, fontAdapter); setTypeface(mTextPaint.getTypeface(), mTextPaint.isFakeBoldText() ? Typeface.BOLD : Typeface.NORMAL); diff --git a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java index c5e87494e82..e15244195d2 100644 --- a/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java +++ b/renderer/native/android/src/main/java/com/tencent/mtt/hippy/views/textinput/HippyTextInputController.java @@ -241,6 +241,11 @@ public void setFontFamily(HippyTextInput view, String fontFamily) { view.setFontFamily(fontFamily); } + @HippyControllerProps(name = NodeProps.FONT_URL, defaultType = HippyControllerProps.STRING) + public void setFontUrl(HippyTextInput view, String fontUrl) { + view.setFontUrl(fontUrl); + } + private static final InputFilter[] EMPTY_FILTERS = new InputFilter[0]; @HippyControllerProps(name = "maxLength", defaultType = HippyControllerProps.NUMBER, defaultNumber = Integer.MAX_VALUE) diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRender.java b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRender.java index f4e5256f91a..1bd76c87640 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRender.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRender.java @@ -29,6 +29,7 @@ import com.tencent.renderer.component.image.ImageDecoderAdapter; import com.tencent.renderer.component.image.ImageLoaderAdapter; import com.tencent.renderer.component.text.FontAdapter; +import com.tencent.renderer.component.text.FontLoader; import com.tencent.renderer.node.VirtualNode; import com.tencent.renderer.utils.EventUtils.EventType; @@ -52,6 +53,8 @@ public interface NativeRender extends RenderExceptionHandler, RenderLogHandler { @Nullable ImageLoaderAdapter getImageLoader(); + FontLoader getFontLoader(); + @Nullable VfsManager getVfsManager(); @@ -105,6 +108,8 @@ VirtualNode createVirtualNode(int rootId, int id, int pid, int index, @NonNull S void onSizeChanged(int rootId, int nodeId, int width, int height, boolean isSync); + void onFontLoaded(int rootId); + void updateDimension(int width, int height); void dispatchEvent(int rootId, int nodeId, @NonNull String eventName, diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderProvider.java b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderProvider.java index e2a6a55e64e..a1da73cd184 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderProvider.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/NativeRenderProvider.java @@ -337,6 +337,10 @@ public void onSizeChanged(int rootId, int width, int height) { updateRootSize(mInstanceId, rootId, PixelUtil.px2dp(width), PixelUtil.px2dp(height)); } + public void onFontLoaded(int rootId) { + onFontLoaded(mInstanceId, rootId); + } + public void onSizeChanged(int rootId, int nodeId, int width, int height, boolean isSync) { updateNodeSize(mInstanceId, rootId, nodeId, PixelUtil.px2dp(width), PixelUtil.px2dp(height), isSync); @@ -433,6 +437,16 @@ private void dispatchEventImpl(int rootId, int nodeId, @NonNull String eventName @SuppressWarnings("JavaJniMissingFunction") private native void updateRootSize(int instanceId, int rootId, float width, float height); + /** + * Call back from Android system when size changed, just like horizontal and vertical screen + * switching, call this jni interface to invoke dom tree relayout. + * + * @param rootId the root node id + * @param instanceId the unique id of native (C++) render manager + */ + @SuppressWarnings("JavaJniMissingFunction") + private native void onFontLoaded(int instanceId, int rootId); + /** * Updates the size to the specified node, such as modal node, should set new window size before * layout. 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 f7c5a81517a..58c1df38c83 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 @@ -20,8 +20,8 @@ import static com.tencent.mtt.hippy.dom.node.NodeProps.PADDING_LEFT; import static com.tencent.mtt.hippy.dom.node.NodeProps.PADDING_RIGHT; import static com.tencent.mtt.hippy.dom.node.NodeProps.PADDING_TOP; -import static com.tencent.renderer.NativeRenderException.ExceptionCode.UI_TASK_QUEUE_ADD_ERR; import static com.tencent.renderer.NativeRenderException.ExceptionCode.INVALID_NODE_DATA_ERR; +import static com.tencent.renderer.NativeRenderException.ExceptionCode.UI_TASK_QUEUE_ADD_ERR; import static com.tencent.renderer.NativeRenderException.ExceptionCode.UI_TASK_QUEUE_UNAVAILABLE_ERR; import android.content.Context; @@ -29,19 +29,24 @@ import android.text.Layout; import android.view.View; import android.view.ViewGroup; - import android.view.ViewParent; + import androidx.annotation.MainThread; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.tencent.mtt.hippy.HippyInstanceLifecycleEventListener; +import com.tencent.mtt.hippy.HippyRootView; import com.tencent.mtt.hippy.common.BaseEngineContext; import com.tencent.mtt.hippy.common.Callback; import com.tencent.mtt.hippy.common.LogAdapter; +import com.tencent.mtt.hippy.modules.Promise; import com.tencent.mtt.hippy.serialization.nio.reader.BinaryReader; import com.tencent.mtt.hippy.serialization.nio.reader.SafeHeapReader; import com.tencent.mtt.hippy.serialization.nio.writer.SafeHeapWriter; import com.tencent.mtt.hippy.serialization.string.InternalizedStringTable; +import com.tencent.mtt.hippy.uimanager.RenderManager; +import com.tencent.mtt.hippy.utils.LogUtils; import com.tencent.mtt.hippy.utils.PixelUtil; import com.tencent.mtt.hippy.utils.UIThreadUtils; import com.tencent.mtt.hippy.views.image.HippyImageViewController; @@ -50,6 +55,7 @@ import com.tencent.renderer.component.image.ImageLoader; import com.tencent.renderer.component.image.ImageLoaderAdapter; import com.tencent.renderer.component.text.FontAdapter; +import com.tencent.renderer.component.text.FontLoader; import com.tencent.renderer.component.text.TextRenderSupplier; import com.tencent.renderer.node.ListItemRenderNode; import com.tencent.renderer.node.RenderNode; @@ -57,23 +63,24 @@ import com.tencent.renderer.node.TextRenderNode; import com.tencent.renderer.node.VirtualNode; import com.tencent.renderer.node.VirtualNodeManager; - import com.tencent.renderer.serialization.Deserializer; import com.tencent.renderer.serialization.Serializer; import com.tencent.renderer.utils.ArrayUtils; import com.tencent.renderer.utils.ChoreographerUtils; import com.tencent.renderer.utils.DisplayUtils; import com.tencent.renderer.utils.EventUtils.EventType; - +import com.tencent.renderer.utils.FlexUtils; +import com.tencent.renderer.utils.FlexUtils.FlexMeasureMode; import com.tencent.renderer.utils.MapUtils; import com.tencent.vfs.VfsManager; + import java.lang.ref.WeakReference; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.concurrent.BlockingQueue; @@ -81,13 +88,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.LinkedBlockingQueue; - -import com.tencent.mtt.hippy.utils.LogUtils; -import com.tencent.mtt.hippy.HippyInstanceLifecycleEventListener; -import com.tencent.mtt.hippy.HippyRootView; -import com.tencent.mtt.hippy.uimanager.RenderManager; -import com.tencent.renderer.utils.FlexUtils; -import com.tencent.renderer.utils.FlexUtils.FlexMeasureMode; import java.util.concurrent.atomic.AtomicInteger; public class NativeRenderer extends Renderer implements NativeRender, NativeRenderDelegate { @@ -136,6 +136,8 @@ public class NativeRenderer extends Renderer implements NativeRender, NativeRend private ExecutorService mBackgroundExecutor; @Nullable private ImageLoaderAdapter mImageLoader; + @Nullable + private FontLoader mFontLoader; public enum FCPBatchState { WATCHING, @@ -223,6 +225,13 @@ public ImageLoaderAdapter getImageLoader() { return mImageLoader; } + public FontLoader getFontLoader() { + if (mFontLoader == null) { + mFontLoader = new FontLoader(getVfsManager(), this); + } + return mFontLoader; + } + @Override @Nullable public VfsManager getVfsManager() { @@ -406,6 +415,10 @@ private void onSizeChanged(int rootId, int w, int h) { mRenderProvider.onSizeChanged(rootId, w, h); } + public void onFontLoaded(int rootId) { + mRenderProvider.onFontLoaded(rootId); + } + @Override public void onSizeChanged(int rootId, int w, int h, int ow, int oh) { FrameworkProxy frameworkProxy = getFrameworkProxy(); @@ -1127,6 +1140,15 @@ private boolean collectNodeInfo(@NonNull RenderNode child, int pid, int outerLef return true; } + @Override + public void loadFontAndRefreshWindow(@NonNull String fontFamily, @NonNull String fontUrl, + int rootId, final Promise promise) { + if (mFontLoader == null) { + mFontLoader = new FontLoader(getVfsManager(), this); + } + mFontLoader.loadAndRefresh(fontFamily, fontUrl, rootId, promise); + } + private interface UITaskExecutor { void exec(); diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/RenderProxy.java b/renderer/native/android/src/main/java/com/tencent/renderer/RenderProxy.java index 1aa50a9f303..d89551bbb68 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/RenderProxy.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/RenderProxy.java @@ -23,6 +23,8 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.tencent.mtt.hippy.common.Callback; +import com.tencent.mtt.hippy.modules.Promise; + import java.util.List; import java.util.Map; @@ -131,4 +133,9 @@ public interface RenderProxy { */ void removeSnapshotView(); + /** + * Notify renderer to load font from url and refresh text window. + */ + void loadFontAndRefreshWindow(@NonNull String fontFamily, @NonNull String fontUrl, int rootId, final Promise promise); + } diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/text/FontLoader.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/FontLoader.java new file mode 100644 index 00000000000..2b78dd86f74 --- /dev/null +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/FontLoader.java @@ -0,0 +1,301 @@ +/* Tencent is pleased to support the open source community by making Hippy available. + * Copyright (C) 2018 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.tencent.renderer.component.text; + +import static com.tencent.vfs.UrlUtils.PREFIX_ASSETS; +import static com.tencent.vfs.UrlUtils.PREFIX_FILE; + +import android.content.res.AssetManager; +import android.text.TextUtils; + +import androidx.annotation.NonNull; + +import com.tencent.mtt.hippy.modules.Promise; +import com.tencent.mtt.hippy.utils.ContextHolder; +import com.tencent.mtt.hippy.utils.LogUtils; +import com.tencent.renderer.NativeRender; +import com.tencent.vfs.ResourceDataHolder; +import com.tencent.vfs.UrlUtils; +import com.tencent.vfs.VfsManager; +import com.tencent.vfs.VfsManager.FetchResourceCallback; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class FontLoader { + + private final WeakReference mNativeRenderRef; + private final WeakReference mVfsManager; + private final File mFontDir; + private final File mUrlFontMapFile; + private final File mLocalFontPathMapFile; + private static Map mConcurrentUrlFontMap; + private static Map mConcurrentLocalFontPathMap; + private final Map mConcurrentFontLoadStateMap; + private static final String[] ALLOWED_EXTENSIONS = {"otf", "ttf"}; + private static final String FONT_DIR_NAME = "fonts"; + private static final String URL_FONT_MAP_NAME = "urlFontMap.ser"; + private static final String LOCAL_FONT_PATH_MAP_NAME = "localFontPathMap.ser"; + + public enum FontLoadState { + FONT_UNLOAD, + FONT_LOADING, + FONT_LOADED, + } + + + public FontLoader(VfsManager vfsManager, NativeRender nativeRender) { + mNativeRenderRef = new WeakReference<>(nativeRender); + mVfsManager = new WeakReference<>(vfsManager); + mFontDir = new File(ContextHolder.getAppContext().getCacheDir(), FONT_DIR_NAME); + mUrlFontMapFile = new File(mFontDir, URL_FONT_MAP_NAME); + mLocalFontPathMapFile = new File(mFontDir, LOCAL_FONT_PATH_MAP_NAME); + mConcurrentFontLoadStateMap = new ConcurrentHashMap<>(); + } + + private boolean saveFontFile(byte[] byteArray, String fileName, Promise promise) { + if (!mFontDir.exists()) { + if (!mFontDir.mkdirs()) { + if (promise != null) { + promise.reject("Create font directory failed"); + } + return false; + } + } + File fontFile = new File(mFontDir, fileName); + try (FileOutputStream fos = new FileOutputStream(fontFile)) { + fos.write(byteArray); + if (promise != null) { + promise.resolve(fileName + " download success!"); + } + return true; + } catch (IOException e) { + if (promise != null) { + promise.reject("Write font file failed:" + e.getMessage()); + } + return false; + } + } + + public boolean isFontLoaded(String fontFamily) { + return fontFamily != null && + mConcurrentFontLoadStateMap.get(fontFamily) == FontLoadState.FONT_LOADED; + } + + private void saveMapFile(File outputFile, Map map) { + try (FileOutputStream fos = new FileOutputStream(outputFile); + ObjectOutputStream oos = new ObjectOutputStream(fos)) { + oos.writeObject(map); + LogUtils.d("FontLoader", String.format("Save %s success!", outputFile.getName())); + } catch (IOException e) { + LogUtils.d("FontLoader", String.format("Save %s failed!", outputFile.getName())); + } + } + + public static String getFileExtension(String url) { + int dotIndex = url.lastIndexOf('.'); + if (dotIndex > 0 && dotIndex < url.length() - 1) { + String ext = url.substring(dotIndex + 1).toLowerCase(); + if (Arrays.asList(ALLOWED_EXTENSIONS).contains(ext)) { + return "." + ext; + } + } + return ""; + } + + public static String getFontPath(String fontFileName) { + if (mConcurrentLocalFontPathMap != null && fontFileName != null) { + return mConcurrentLocalFontPathMap.get(fontFileName); + } + return null; + } + + // Convert "hpfile://" to "file://" or "assets://" + private String convertToLocalPathIfNeeded(String fontUrl) { + if (fontUrl != null && fontUrl.startsWith("hpfile://")) { + final NativeRender nativeRender = mNativeRenderRef.get(); + String bundlePath = null; + if (nativeRender != null) { + bundlePath = mNativeRenderRef.get().getBundlePath(); + } + String relativePath = fontUrl.replace("hpfile://./", ""); + fontUrl = bundlePath == null ? null + : bundlePath.subSequence(0, bundlePath.lastIndexOf(File.separator) + 1) + + relativePath; + } + return fontUrl; + } + + private Map readMapFromFile(File file) { + Map map; + try (FileInputStream fis = new FileInputStream(file); + ObjectInputStream ois = new ObjectInputStream(fis)) { + map = (Map) ois.readObject(); + } catch (IOException | ClassNotFoundException e) { + map = new HashMap<>(); + } + return map; + } + + private boolean isAssetFileExists(String assetFilePath) { + AssetManager assetManager = ContextHolder.getAppContext().getAssets(); + String directory = ""; + String fileName = assetFilePath; + if (fileName.startsWith(PREFIX_ASSETS)) { + fileName = fileName.substring(PREFIX_ASSETS.length()); + } + int lastSlashIndex = assetFilePath.lastIndexOf(File.separator); + if (lastSlashIndex != -1) { + directory = assetFilePath.substring(0, lastSlashIndex); + fileName = assetFilePath.substring(lastSlashIndex + 1); + } + try { + String[] files = assetManager.list(directory); + if (files != null) { + for (String file : files) { + if (file.equals(fileName)) { + return true; + } + } + } + } catch (IOException e) { + LogUtils.d("FontLoader", String.format("Find directory %s failed", directory)); + } + return false; + } + + private void initMapIfNeeded() { + if (mConcurrentUrlFontMap == null) { + mConcurrentUrlFontMap = new ConcurrentHashMap<>(readMapFromFile(mUrlFontMapFile)); + } + if (mConcurrentLocalFontPathMap == null) { + mConcurrentLocalFontPathMap = new ConcurrentHashMap<>(readMapFromFile(mLocalFontPathMapFile)); + } + } + + public boolean loadIfNeeded(final String fontFamily, final String fontUrl, int rootId) { + initMapIfNeeded(); + if (TextUtils.isEmpty(fontFamily) || TextUtils.isEmpty(fontUrl)) { + return false; + } + String fontFileName = mConcurrentUrlFontMap.get(fontUrl); + if (fontFileName != null) { + if (fontFileName.startsWith(PREFIX_ASSETS) && isAssetFileExists(fontFileName)) { + return false; + } + if (fontFileName.startsWith(PREFIX_FILE)) { + fontFileName = fontFileName.substring(PREFIX_FILE.length()); + } + File fontFile = new File(fontFileName); + if (fontFile.exists()) { + return false; + } + } + FontLoadState state = mConcurrentFontLoadStateMap.get(fontFamily); + if (state == FontLoadState.FONT_LOADING || state == FontLoadState.FONT_LOADED) { + return false; + } + loadAndRefresh(fontFamily, fontUrl, rootId, null); + return true; + } + + + public void loadAndRefresh(final String fontFamily, final String fontUrl, int rootId, + Promise promise) { + LogUtils.d("FontLoader", "Start load " + fontUrl); + if (TextUtils.isEmpty(fontUrl)) { + if (promise != null) { + promise.reject("Url parameter is empty!"); + } + return; + } + mConcurrentFontLoadStateMap.put(fontFamily, FontLoadState.FONT_LOADING); + String convertFontUrl = convertToLocalPathIfNeeded(fontUrl); + final VfsManager vfsManager = mVfsManager.get(); + if (vfsManager == null) { + if (promise != null) { + promise.reject("Get vfsManager failed!"); + } + return; + } + vfsManager.fetchResourceAsync(convertFontUrl, null, null, + new FetchResourceCallback() { + @Override + public void onFetchCompleted(@NonNull final ResourceDataHolder dataHolder) { + byte[] bytes = dataHolder.getBytes(); + if (dataHolder.resultCode + != ResourceDataHolder.RESOURCE_LOAD_SUCCESS_CODE || bytes == null + || bytes.length <= 0) { + mConcurrentFontLoadStateMap.put(fontFamily, FontLoadState.FONT_UNLOAD); + if (promise != null) { + promise.reject("Fetch font file failed, url=" + fontUrl); + } + } else { + initMapIfNeeded(); + boolean needRefresh = true; + if (UrlUtils.isFileUrl(convertFontUrl) || UrlUtils.isAssetsUrl(convertFontUrl)) { + mConcurrentUrlFontMap.put(fontUrl, convertFontUrl); + String fileName = fontFamily + getFileExtension(fontUrl); + if (convertFontUrl.equals(mConcurrentLocalFontPathMap.get(fileName))) { + needRefresh = false; + } else { + mConcurrentLocalFontPathMap.put(fileName, convertFontUrl); + } + if (promise != null) { + promise.resolve(String.format("Load local font %s success", convertFontUrl)); + } + } else { + String fileName = fontFamily + getFileExtension(fontUrl); + if (!saveFontFile(bytes, fileName, promise)) { + mConcurrentFontLoadStateMap.remove(fontFamily); + return; + } else { + File fontFile = new File(mFontDir, fileName); + mConcurrentUrlFontMap.put(fontUrl, fontFile.getAbsolutePath()); + if (promise != null) { + promise.resolve(String.format("Download font %s success", fileName)); + } + } + } + mConcurrentFontLoadStateMap.put(fontFamily, FontLoadState.FONT_LOADED); + saveMapFile(mUrlFontMapFile, mConcurrentUrlFontMap); + saveMapFile(mLocalFontPathMapFile, mConcurrentLocalFontPathMap); + TypeFaceUtil.clearFontCache(fontFamily); + final NativeRender nativeRender = mNativeRenderRef.get(); + if (nativeRender != null && needRefresh) { + nativeRender.onFontLoaded(rootId); + } + } + dataHolder.recycle(); + } + + @Override + public void onFetchProgress(long total, long loaded) { + // Nothing need to do here. + } + }); + } +} diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/component/text/TypeFaceUtil.java b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/TypeFaceUtil.java index 3dd6043f22b..b3cb3df5d2a 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/component/text/TypeFaceUtil.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/component/text/TypeFaceUtil.java @@ -21,13 +21,15 @@ import android.os.Build.VERSION; import android.os.Build.VERSION_CODES; import android.text.TextUtils; - import android.util.SparseArray; + import androidx.annotation.Nullable; import com.tencent.mtt.hippy.utils.ContextHolder; import com.tencent.mtt.hippy.utils.LogUtils; +import com.tencent.vfs.UrlUtils; +import java.io.File; import java.util.HashMap; import java.util.Map; @@ -40,7 +42,7 @@ public class TypeFaceUtil { public static final String TEXT_FONT_STYLE_NORMAL = "normal"; private static final String TAG = "TypeFaceUtil"; private static final String[] EXTENSIONS = {"", "_bold", "_italic", "_bold_italic"}; - private static final String[] FONT_EXTENSIONS = {".ttf", ".otf"}; + private static final String[] FONT_EXTENSIONS = {".ttf", ".otf", ""}; private static final String FONTS_PATH = "fonts/"; private static final Map> sFontCache = new HashMap<>(); @@ -79,6 +81,53 @@ public static Typeface getTypeface(String fontFamilyName, String weight, boolean return typeface; } + public static void clearFontCache(String fontFamilyName) { + sFontCache.remove(fontFamilyName); + } + + private static Typeface createExactTypeFace(String fileName) { + // create from assets + Typeface typeface = null; + try { + typeface = Typeface.createFromAsset(ContextHolder.getAppContext().getAssets(), FONTS_PATH+fileName); + } catch (Exception e) { + LogUtils.w(TAG, e.getMessage()); + } + // create from cache dir + if (typeface == null || typeface.equals(Typeface.DEFAULT)) { + try { + File cacheDir = ContextHolder.getAppContext().getCacheDir(); + typeface = Typeface.createFromFile(new File(cacheDir, FONTS_PATH+fileName)); + } catch (Exception e) { + LogUtils.w(TAG, e.getMessage()); + } + } + // create from local bundle file + if (typeface == null || typeface.equals(Typeface.DEFAULT)) { + String bundleFontPath = FontLoader.getFontPath(fileName); + if (bundleFontPath != null) { + if (bundleFontPath.startsWith(UrlUtils.PREFIX_ASSETS)) { + try { + typeface = Typeface.createFromAsset(ContextHolder.getAppContext().getAssets(), + bundleFontPath.substring(UrlUtils.PREFIX_ASSETS.length())); + } catch (Exception e) { + LogUtils.w(TAG, e.getMessage()); + } + } else { + if (bundleFontPath.startsWith(UrlUtils.PREFIX_FILE)) { + bundleFontPath = bundleFontPath.substring(UrlUtils.PREFIX_FILE.length()); + } + try { + typeface = Typeface.createFromFile(new File(bundleFontPath)); + } catch (Exception e) { + LogUtils.w(TAG, e.getMessage()); + } + } + } + } + return typeface; + } + private static Typeface createTypeface(String fontFamilyName, int weightNumber, int style, boolean italic, @Nullable FontAdapter fontAdapter) { final String extension = EXTENSIONS[style]; @@ -93,23 +142,17 @@ private static Typeface createTypeface(String fontFamilyName, int weightNumber, continue; } for (String fileExtension : FONT_EXTENSIONS) { - String fileName = FONTS_PATH + splitName + extension + fileExtension; - try { - Typeface typeface = Typeface.createFromAsset(ContextHolder.getAppContext().getAssets(), fileName); - if (typeface != null && !typeface.equals(Typeface.DEFAULT)) { - return typeface; - } - } catch (Exception e) { - // If create type face from asset failed, other builder can also be used - LogUtils.w(TAG, e.getMessage()); - } - if (style == Typeface.NORMAL) { - continue; + String fileName = splitName + extension + fileExtension; + Typeface typeface = createExactTypeFace(fileName); + if (typeface != null && !typeface.equals(Typeface.DEFAULT)) { + return typeface; } - // try to load font file without extension - fileName = FONTS_PATH + splitName + fileExtension; - try { - Typeface typeface = Typeface.createFromAsset(ContextHolder.getAppContext().getAssets(), fileName); + } + // try to load font file without extension + if (style != Typeface.NORMAL) { + for (String fileExtension : FONT_EXTENSIONS) { + String fileName = FONTS_PATH + splitName + fileExtension; + Typeface typeface = createExactTypeFace(fileName); if (typeface != null && !typeface.equals(Typeface.DEFAULT)) { if (VERSION.SDK_INT >= VERSION_CODES.P && weightNumber > 0) { return Typeface.create(typeface, weightNumber, italic); @@ -117,8 +160,6 @@ private static Typeface createTypeface(String fontFamilyName, int weightNumber, // "bold" has no effect on api level < P, prefer to use `Paint.setFakeBoldText(boolean)` return italic ? Typeface.create(typeface, Typeface.ITALIC) : typeface; } - } catch (Exception e) { - LogUtils.w(TAG, e.getMessage()); } } if (fontAdapter != null) { diff --git a/renderer/native/android/src/main/java/com/tencent/renderer/node/TextVirtualNode.java b/renderer/native/android/src/main/java/com/tencent/renderer/node/TextVirtualNode.java index fb4d89e38d6..afe4298b139 100644 --- a/renderer/native/android/src/main/java/com/tencent/renderer/node/TextVirtualNode.java +++ b/renderer/native/android/src/main/java/com/tencent/renderer/node/TextVirtualNode.java @@ -35,15 +35,19 @@ import android.text.style.AbsoluteSizeSpan; import android.text.style.BackgroundColorSpan; import android.text.style.ImageSpan; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; + import com.tencent.mtt.hippy.annotation.HippyControllerProps; import com.tencent.mtt.hippy.dom.node.NodeProps; import com.tencent.mtt.hippy.utils.I18nUtil; +import com.tencent.mtt.hippy.utils.LogUtils; import com.tencent.mtt.hippy.utils.PixelUtil; import com.tencent.renderer.NativeRender; import com.tencent.renderer.component.text.FontAdapter; +import com.tencent.renderer.component.text.FontLoader; import com.tencent.renderer.component.text.TextDecorationSpan; import com.tencent.renderer.component.text.TextForegroundColorSpan; import com.tencent.renderer.component.text.TextGestureSpan; @@ -55,10 +59,13 @@ import com.tencent.renderer.component.text.TextVerticalAlignSpan; import com.tencent.renderer.component.text.TypeFaceUtil; import com.tencent.renderer.utils.FlexUtils.FlexMeasureMode; + +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Objects; +import java.util.concurrent.Executor; public class TextVirtualNode extends VirtualNode { @@ -110,6 +117,12 @@ public class TextVirtualNode extends VirtualNode { @Nullable protected String mFontFamily; @Nullable + protected String mFontUrl; + @Nullable + protected WeakReference mFontLoaderRef; + protected WeakReference mNativeRenderRef; + protected FontLoader.FontLoadState mFontLoadState; + @Nullable protected SpannableStringBuilder mSpanned; @Nullable protected CharSequence mText; @@ -129,6 +142,8 @@ public TextVirtualNode(int rootId, int id, int pid, int index, @NonNull NativeRender nativeRender) { super(rootId, id, pid, index); mFontAdapter = nativeRender.getFontAdapter(); + mFontLoaderRef = new WeakReference<>(nativeRender.getFontLoader()); + mNativeRenderRef = new WeakReference<>(nativeRender); if (I18nUtil.isRTL()) { mAlignment = Layout.Alignment.ALIGN_OPPOSITE; } @@ -178,6 +193,19 @@ public void setFontFamily(String family) { if (!Objects.equals(mFontFamily, family)) { mFontFamily = family; markDirty(); + final FontLoader fontLoader = mFontLoaderRef.get(); + if (fontLoader != null && !fontLoader.isFontLoaded(mFontFamily)) { + mFontLoadState = FontLoader.FontLoadState.FONT_UNLOAD; + } + } + } + + @HippyControllerProps(name = NodeProps.FONT_URL, defaultType = HippyControllerProps.STRING) + public void setFontUrl(String fontUrl) { + if (!Objects.equals(mFontUrl, fontUrl)) { + mFontUrl = fontUrl; + LogUtils.d("TextVirtualNode", "fontUrl:"+fontUrl); + markDirty(); } } @@ -469,6 +497,18 @@ protected void createSpanOperationImpl(@NonNull List ops, if (mFontAdapter != null && mEnableScale) { size = (int) (size * mFontAdapter.getFontScale()); } + final FontLoader fontLoader = mFontLoaderRef.get(); + if (!TextUtils.isEmpty(mFontUrl) && fontLoader != null) { + final NativeRender nativeRender = mNativeRenderRef.get(); + if (nativeRender != null) { + Executor executor = nativeRender.getBackgroundExecutor(); + if (executor != null) { + executor.execute(() -> { + fontLoader.loadIfNeeded(mFontFamily, mFontUrl, getRootId()); + }); + } + } + } ops.add(new SpanOperation(start, end, new AbsoluteSizeSpan(size))); ops.add(new SpanOperation(start, end, new TextStyleSpan(mItalic, mFontWeight, mFontFamily, mFontAdapter))); if (mShadowOffsetDx != 0 || mShadowOffsetDy != 0) { @@ -535,6 +575,12 @@ protected Layout createLayout() { @NonNull protected Layout createLayout(final float width, final FlexMeasureMode widthMode) { + final FontLoader fontLoader = mFontLoaderRef.get(); + if (mFontLoadState == FontLoader.FontLoadState.FONT_UNLOAD && fontLoader != null && + fontLoader.isFontLoaded(mFontFamily)) { + mDirty = true; + mFontLoadState = FontLoader.FontLoadState.FONT_LOADED; + } if (mSpanned == null || mDirty) { mSpanned = createSpan(true); mDirty = false; diff --git a/renderer/native/ios/renderer/HippyFont.h b/renderer/native/ios/renderer/HippyFont.h index f120b8f779b..9e0dcfa3784 100644 --- a/renderer/native/ios/renderer/HippyFont.h +++ b/renderer/native/ios/renderer/HippyFont.h @@ -33,6 +33,7 @@ */ + (UIFont *)updateFont:(UIFont *)font withFamily:(NSString *)family + url:(NSString *)url size:(NSNumber *)size weight:(NSString *)weight style:(NSString *)style diff --git a/renderer/native/ios/renderer/HippyFont.mm b/renderer/native/ios/renderer/HippyFont.mm index ef945ca6f13..829b6d9c7a4 100644 --- a/renderer/native/ios/renderer/HippyFont.mm +++ b/renderer/native/ios/renderer/HippyFont.mm @@ -24,6 +24,7 @@ #import "HippyFont.h" #import "HippyLog.h" +#import "HippyFontLoaderModule.h" static NSCache *fontCache; @@ -102,7 +103,7 @@ struct __attribute__((__packed__)) CacheKey { [cache removeAllObjects]; }]; }); - + NSArray *names = [cache objectForKey:familyName]; if (!names) { names = [UIFont fontNamesForFamilyName:familyName] ?: [NSArray new]; @@ -115,8 +116,9 @@ @implementation HippyConvert (NativeRenderFont) + (UIFont *)UIFont:(id)json { json = [self NSDictionary:json]; - return [HippyFont updateFont:nil + return [HippyFont updateFont:nil withFamily:[HippyConvert NSString:json[@"fontFamily"]] + url:[HippyConvert NSString:json[@"fontUrl"]] size:[HippyConvert NSNumber:json[@"fontSize"]] weight:[HippyConvert NSString:json[@"fontWeight"]] style:[HippyConvert NSString:json[@"fontStyle"]] @@ -197,12 +199,18 @@ + (void)initialize { + (UIFont *)updateFont:(UIFont *)font withFamily:(NSString *)family + url:(NSString *)url size:(NSNumber *)size weight:(NSString *)weight style:(NSString *)style variant:(NSArray *)variant scaleMultiplier:(CGFloat)scaleMultiplier { // Defaults + if (url) { + dispatch_async([HippyFontLoaderModule getFontSerialQueue], ^{ + [HippyFontLoaderModule loadFontIfNeeded:family fromUrl:url]; + }); + } static NSString *defaultFontFamily; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -231,7 +239,9 @@ + (UIFont *)updateFont:(UIFont *)font if (scaleMultiplier > 0.0 && scaleMultiplier != 1.0) { fontSize = round(fontSize * scaleMultiplier); } - familyName = [HippyConvert NSString:family] ?: familyName; + if ([HippyConvert NSString:family] && family.length != 0) { + familyName = family; + } isItalic = style ? [HippyConvert NativeRenderFontStyle:style] : isItalic; fontWeight = weight ? [HippyConvert NativeRenderFontWeight:weight] : fontWeight; @@ -258,15 +268,25 @@ + (UIFont *)updateFont:(UIFont *)font } } } + + if (!didFindFont && fontNamesForFamilyName(familyName).count == 0) { + dispatch_async([HippyFontLoaderModule getFontSerialQueue], ^{ + [HippyFontLoaderModule registerFontIfNeeded:familyName]; + }); + } // Gracefully handle being given a font name rather than font family, for // example: "Helvetica Light Oblique" rather than just "Helvetica". if (!didFindFont && familyName.length > 0 && fontNamesForFamilyName(familyName).count == 0) { - familyName = font.familyName; - fontWeight = weight ? fontWeight : weightOfFont(font); - isItalic = style ? isItalic : isItalicFont(font); - isCondensed = isCondensedFont(font); - font = cachedSystemFont(fontSize, fontWeight); + font = [UIFont fontWithName:familyName size:fontSize]; + if (font) { + didFindFont = YES; + } else { + fontWeight = weight ? fontWeight : weightOfFont(font); + isItalic = style ? isItalic : isItalicFont(font); + isCondensed = isCondensedFont(font); + font = cachedSystemFont(fontSize, fontWeight); + } if (font) { // It's actually a font name, not a font family name, @@ -311,7 +331,7 @@ + (UIFont *)updateFont:(UIFont *)font if (!font && names.count > 0) { font = [UIFont fontWithName:names[0] size:fontSize]; } - + // Apply font variants to font object if (variant) { NSArray *fontFeatures = [HippyConvert NativeRenderFontVariantDescriptorArray:variant]; diff --git a/renderer/native/ios/renderer/component/text/HippyShadowText.h b/renderer/native/ios/renderer/component/text/HippyShadowText.h index a9be9a1525b..94e6cb4bbd4 100644 --- a/renderer/native/ios/renderer/component/text/HippyShadowText.h +++ b/renderer/native/ios/renderer/component/text/HippyShadowText.h @@ -49,6 +49,7 @@ extern NSAttributedStringKey const HippyShadowViewAttributeName; @property (nonatomic, strong) UIColor *color; @property (nonatomic, copy) NSString *fontFamily; +@property (nonatomic, copy) NSString *fontUrl; @property (nonatomic, assign) CGFloat fontSize; @property (nonatomic, copy) NSString *fontWeight; @property (nonatomic, copy) NSString *fontStyle; @@ -83,6 +84,7 @@ extern NSAttributedStringKey const HippyShadowViewAttributeName; @interface HippyAttributedStringStyleInfo : NSObject @property (nonatomic, strong) NSString *fontFamily; +@property (nonatomic, strong) NSString *fontUrl; @property (nonatomic, strong) NSNumber *fontSize; @property (nonatomic, strong) NSString *fontWeight; @property (nonatomic, strong) NSString *fontStyle; diff --git a/renderer/native/ios/renderer/component/text/HippyShadowText.mm b/renderer/native/ios/renderer/component/text/HippyShadowText.mm index a9e404624b8..4a09beb90a5 100644 --- a/renderer/native/ios/renderer/component/text/HippyShadowText.mm +++ b/renderer/native/ios/renderer/component/text/HippyShadowText.mm @@ -472,6 +472,9 @@ - (NSAttributedString *)_attributedStringWithStyleInfo:(HippyAttributedStringSty if (_fontFamily) { styleInfo.fontFamily = _fontFamily; } + if (_fontUrl) { + styleInfo.fontUrl = _fontUrl; + } if (!isnan(_letterSpacing)) { styleInfo.letterSpacing = @(_letterSpacing); } @@ -485,6 +488,7 @@ - (NSAttributedString *)_attributedStringWithStyleInfo:(HippyAttributedStringSty UIFont *font = [HippyFont updateFont:f withFamily:styleInfo.fontFamily + url:styleInfo.fontUrl size:styleInfo.fontSize weight:styleInfo.fontWeight style:styleInfo.fontStyle @@ -877,6 +881,7 @@ -(void)set##setProp : (type)value \ NATIVE_RENDER_TEXT_PROPERTY(AdjustsFontSizeToFit, _adjustsFontSizeToFit, BOOL) NATIVE_RENDER_TEXT_PROPERTY(Color, _color, UIColor *) NATIVE_RENDER_TEXT_PROPERTY(FontFamily, _fontFamily, NSString *) +NATIVE_RENDER_TEXT_PROPERTY(FontUrl, _fontUrl, NSString *) NATIVE_RENDER_TEXT_PROPERTY(FontSize, _fontSize, CGFloat) NATIVE_RENDER_TEXT_PROPERTY(FontWeight, _fontWeight, NSString *) NATIVE_RENDER_TEXT_PROPERTY(FontStyle, _fontStyle, NSString *) diff --git a/renderer/native/ios/renderer/component/text/HippyTextManager.mm b/renderer/native/ios/renderer/component/text/HippyTextManager.mm index edd534a17ea..931dd152ca6 100644 --- a/renderer/native/ios/renderer/component/text/HippyTextManager.mm +++ b/renderer/native/ios/renderer/component/text/HippyTextManager.mm @@ -53,6 +53,7 @@ - (HippyShadowView *)shadowView { HIPPY_EXPORT_SHADOW_PROPERTY(color, UIColor) HIPPY_EXPORT_SHADOW_PROPERTY(fontFamily, NSString) +HIPPY_EXPORT_SHADOW_PROPERTY(fontUrl, NSString) HIPPY_EXPORT_SHADOW_PROPERTY(fontSize, CGFloat) HIPPY_EXPORT_SHADOW_PROPERTY(fontWeight, NSString) HIPPY_EXPORT_SHADOW_PROPERTY(fontStyle, NSString) diff --git a/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.h b/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.h index 5b7ac631163..c2c99d18443 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.h +++ b/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.h @@ -32,6 +32,8 @@ @property (nonatomic, strong) NSString *fontStyle; /// Font property - FontFamily @property (nonatomic, strong) NSString *fontFamily; +/// Font property - FontUrl +@property (nonatomic, strong) NSString *fontUrl; @property (nonatomic, strong) UIFont *font; @property (nonatomic, assign) UIEdgeInsets contentInset; diff --git a/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.m b/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.m index ff622e02055..a1bdc9f20d1 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.m +++ b/renderer/native/ios/renderer/component/textinput/HippyBaseTextInput.m @@ -132,12 +132,17 @@ - (void)setFontFamily:(NSString *)fontFamily { [self setNeedsLayout]; } +- (void)setFontUrl:(NSString *)fontUrl { + _fontUrl = fontUrl; + [self setNeedsLayout]; +} + - (void)rebuildAndUpdateFont { // Convert fontName to fontFamily if needed CGFloat scaleMultiplier = 1.0; // scale not supported - NSString *familyName = [HippyFont familyNameWithCSSNameMatching:self.fontFamily]; UIFont *font = [HippyFont updateFont:self.font - withFamily:familyName + withFamily:self.fontFamily + url:self.fontUrl size:self.fontSize weight:self.fontWeight style:self.fontStyle diff --git a/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.h b/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.h index 7648e2a6bab..cfeb2769444 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.h +++ b/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.h @@ -42,5 +42,7 @@ @property (nonatomic, strong) NSString *fontStyle; /// Font property - FontFamily @property (nonatomic, strong) NSString *fontFamily; +/// Font property - FontUrl +@property (nonatomic, strong) NSString *fontUrl; @end diff --git a/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.mm b/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.mm index 0d38fba2fc0..fa432c48a62 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.mm +++ b/renderer/native/ios/renderer/component/textinput/HippyShadowTextView.mm @@ -226,12 +226,17 @@ - (void)setFontFamily:(NSString *)fontFamily { self.isFontDirty = YES; } +- (void)setFontUrl:(NSString *)fontUrl { + _fontUrl = fontUrl; + self.isFontDirty = YES; +} + - (void)rebuildAndUpdateFont { // Convert fontName to fontFamily if needed CGFloat scaleMultiplier = 1.0; // scale not supported - NSString *familyName = [HippyFont familyNameWithCSSNameMatching:self.fontFamily]; UIFont *font = [HippyFont updateFont:self.font - withFamily:familyName + withFamily:self.fontFamily + url:self.fontUrl size:self.fontSize weight:self.fontWeight style:self.fontStyle diff --git a/renderer/native/ios/renderer/component/textinput/HippyTextViewManager.mm b/renderer/native/ios/renderer/component/textinput/HippyTextViewManager.mm index 7b9bfbf3f1c..e1bf3ebb4fb 100644 --- a/renderer/native/ios/renderer/component/textinput/HippyTextViewManager.mm +++ b/renderer/native/ios/renderer/component/textinput/HippyTextViewManager.mm @@ -144,6 +144,7 @@ - (HippyShadowView *)shadowView { HIPPY_EXPORT_SHADOW_PROPERTY(fontWeight, NSString) HIPPY_EXPORT_SHADOW_PROPERTY(fontStyle, NSString) HIPPY_EXPORT_SHADOW_PROPERTY(fontFamily, NSString) +HIPPY_EXPORT_SHADOW_PROPERTY(fontUrl, NSString) HIPPY_EXPORT_VIEW_PROPERTY(lineHeight, NSNumber) HIPPY_EXPORT_VIEW_PROPERTY(lineSpacing, NSNumber) @@ -177,6 +178,7 @@ - (HippyShadowView *)shadowView { HIPPY_EXPORT_VIEW_PROPERTY(fontWeight, NSString) HIPPY_EXPORT_VIEW_PROPERTY(fontStyle, NSString) HIPPY_EXPORT_VIEW_PROPERTY(fontFamily, NSString) +HIPPY_EXPORT_VIEW_PROPERTY(fontUrl, NSString) - (HippyViewManagerUIBlock)uiBlockToAmendWithShadowView:(HippyShadowView *)hippyShadowView { NSNumber *componentTag = hippyShadowView.hippyTag; diff --git a/tests/ios/HippyFontLoaderTest.m b/tests/ios/HippyFontLoaderTest.m new file mode 100644 index 00000000000..6cd19337441 --- /dev/null +++ b/tests/ios/HippyFontLoaderTest.m @@ -0,0 +1,114 @@ +/*! + * iOS SDK + * + * Tencent is pleased to support the open source community by making + * Hippy available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#import +#import +#import + +@interface HippyFontLoaderTest : XCTestCase + +@property (nonatomic, strong) HippyFontLoaderModule *fontLoader; + +@end + +@implementation HippyFontLoaderTest + +- (void)setUp { + // Put setup code here. This method is called before the invocation of each test method in the class. +} + +- (void)tearDown { + // Put teardown code here. This method is called after the invocation of each test method in the class. +} + +- (void)testHippyFontLoaderModule { + NSString* invalidURL = @"https://example.url"; + // set arbitrary valid font file url + NSBundle *testBundle = [NSBundle bundleForClass:[self class]]; + NSString* filePath = [testBundle pathForResource:@"TestFonts.bundle/TTTGB-Medium" ofType:@"otf"]; + NSString* validURL = [@"file://" stringByAppendingString:filePath]; + NSString* fontFamily = @"TTTGB Medium"; + HippyBridge *bridge = [[HippyBridge alloc] initWithDelegate:nil moduleProvider:nil launchOptions:nil executorKey:nil]; + HippyFontLoaderModule *fontLoader = [[HippyFontLoaderModule alloc] init]; + [fontLoader setValue:bridge forKey:@"bridge"]; + + // test fetch from invalidURL + XCTestExpectation *invalidURLExpectation = [self expectationWithDescription:@"Fetch data from invalid url expectation"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + [HippyFontLoaderModule getFontSerialQueue], ^{ + [fontLoader load:fontFamily from:invalidURL resolver:^(id result) { + [invalidURLExpectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + // test whether the url is loading + XCTAssertTrue([HippyFontLoaderModule isUrlLoading:invalidURL]); + XCTAssertEqual(message, @"font request error"); + [invalidURLExpectation fulfill]; + }]; + }); + [self waitForExpectationsWithTimeout:5 handler:nil]; + + // test fetch from validURL + XCTestExpectation *validURLExpectation = [self expectationWithDescription:@"Fetch data from valid url expectation"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + [HippyFontLoaderModule getFontSerialQueue], ^{ + [fontLoader load:fontFamily from:validURL resolver:^(id result) { + // test whether the url is loading + XCTAssertTrue([HippyFontLoaderModule isUrlLoading:validURL]); + [validURLExpectation fulfill]; + } rejecter:^(NSString *code, NSString *message, NSError *error) { + XCTAssert(true, @"fetch valid url failed"); + [validURLExpectation fulfill]; + }]; + }); + [self waitForExpectationsWithTimeout:5 handler:nil]; + + __block NSString *fontPath; + // test get font path using undownloaded font + XCTestExpectation *undownloadedExpectation = [self expectationWithDescription:@"get undownloaded font path expectation"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + [HippyFontLoaderModule getFontSerialQueue], ^{ + fontPath = [HippyFontLoaderModule getFontPath:invalidURL]; + XCTAssertNil(fontPath); + [undownloadedExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:5 handler:nil]; + + // test get font path using downloaded font + XCTestExpectation *downloadedExpectation = [self expectationWithDescription:@"get downloaded font path expectation"]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + [HippyFontLoaderModule getFontSerialQueue], ^{ + fontPath = [HippyFontLoaderModule getFontPath:validURL]; + XCTAssertNotNil(fontPath); + [downloadedExpectation fulfill]; + }); + [self waitForExpectationsWithTimeout:5 handler:nil]; + + // test whether font registered successfully in load method + BOOL needRegister = [HippyFontLoaderModule registerFontIfNeeded:fontFamily]; + XCTAssertFalse(needRegister); + + // delete font directory + [[NSFileManager defaultManager] removeItemAtPath:[fontPath stringByDeletingLastPathComponent] error:nil]; +} + +@end