From a2d25c4cb5bfb7b90cdf495944df427ea93f14b2 Mon Sep 17 00:00:00 2001 From: "pengfei.zhou" Date: Wed, 4 Dec 2019 14:11:47 +0800 Subject: [PATCH] move files from main project --- .gitignore | 8 + app/.gitignore | 1 + app/build.gradle | 33 + app/proguard-rules.pro | 21 + app/src/main/AndroidManifest.xml | 35 + app/src/main/assets/demo | 1 + .../java/pub/doric/demo/DemoActivity.java | 46 + .../main/java/pub/doric/demo/DemoLibrary.java | 28 + .../main/java/pub/doric/demo/DemoPlugin.java | 49 + .../java/pub/doric/demo/MainActivity.java | 134 + .../java/pub/doric/demo/MyApplication.java | 30 + .../java/pub/doric/demo/MyGlideModule.java | 23 + .../java/pub/doric/demo/PullableActivity.java | 28 + .../drawable-v24/ic_launcher_foreground.xml | 34 + .../res/drawable/ic_launcher_background.xml | 170 ++ app/src/main/res/layout/activity_main.xml | 13 + app/src/main/res/layout/activity_pullable.xml | 13 + .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 + app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2963 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 4905 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2060 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 2783 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4490 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 6895 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6387 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 10413 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9128 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 15132 bytes app/src/main/res/values/colors.xml | 6 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/styles.xml | 11 + .../main/res/xml/network_security_config.xml | 4 + build.gradle | 30 + devkit/.gitignore | 1 + devkit/build.gradle | 52 + devkit/consumer-rules.pro | 0 devkit/proguard-rules.pro | 21 + .../doric/devkit/ExampleInstrumentedTest.java | 27 + devkit/src/main/AndroidManifest.xml | 10 + .../main/java/pub/doric/devkit/DevKit.java | 44 + .../doric/devkit/DoricContextDebuggable.java | 29 + .../pub/doric/devkit/DoricDebugDriver.java | 135 + .../pub/doric/devkit/DoricDebugJSEngine.java | 19 + .../main/java/pub/doric/devkit/DoricDev.java | 17 + .../main/java/pub/doric/devkit/IDevKit.java | 16 + .../pub/doric/devkit/IStatusCallback.java | 5 + .../java/pub/doric/devkit/LocalServer.java | 181 ++ .../main/java/pub/doric/devkit/WSClient.java | 119 + .../devkit/event/ConnectExceptionEvent.java | 4 + .../doric/devkit/event/EOFExceptionEvent.java | 4 + .../doric/devkit/event/EnterDebugEvent.java | 4 + .../pub/doric/devkit/event/OpenEvent.java | 4 + .../doric/devkit/event/QuitDebugEvent.java | 4 + .../pub/doric/devkit/event/ReloadEvent.java | 11 + .../devkit/remote/DoricRemoteJSExecutor.java | 63 + .../doric/devkit/remote/RemoteJSExecutor.java | 148 ++ .../pub/doric/devkit/remote/ValueBuilder.java | 91 + .../doric/devkit/ui/DebugContextPanel.java | 84 + .../doric/devkit/ui/DemoDebugActivity.java | 113 + .../java/pub/doric/devkit/ui/DevPanel.java | 136 + .../doric/devkit/ui/ScanQRCodeActivity.java | 79 + .../devkit/util/SensorManagerHelper.java | 113 + .../main/res/layout/layout_debug_context.xml | 13 + .../res/layout/layout_debug_context_cell.xml | 38 + devkit/src/main/res/layout/layout_dev.xml | 34 + .../main/res/layout/layout_scan_qrcode.xml | 22 + devkit/src/main/res/values/strings.xml | 3 + doric/.gitignore | 1 + doric/build.gradle | 59 + doric/proguard-rules.pro | 31 + doric/src/main/AndroidManifest.xml | 18 + doric/src/main/assets/bundle | 1 + doric/src/main/assets/debugger | 1 + doric/src/main/java/pub/doric/Doric.java | 35 + .../main/java/pub/doric/DoricActivity.java | 53 + .../main/java/pub/doric/DoricComponent.java | 34 + .../src/main/java/pub/doric/DoricContext.java | 209 ++ .../java/pub/doric/DoricContextManager.java | 91 + .../main/java/pub/doric/DoricFragment.java | 84 + .../src/main/java/pub/doric/DoricLibrary.java | 25 + .../java/pub/doric/DoricNativeDriver.java | 135 + doric/src/main/java/pub/doric/DoricPanel.java | 113 + .../java/pub/doric/DoricPanelFragment.java | 92 + .../main/java/pub/doric/DoricRegistry.java | 149 ++ .../src/main/java/pub/doric/IDoricDriver.java | 44 + .../main/java/pub/doric/async/AsyncCall.java | 70 + .../java/pub/doric/async/AsyncResult.java | 100 + .../java/pub/doric/async/SettableFuture.java | 82 + .../java/pub/doric/engine/DoricJSEngine.java | 228 ++ .../doric/engine/DoricNativeJSExecutor.java | 66 + .../main/java/pub/doric/engine/IDoricJSE.java | 79 + .../bridge/DoricBridgeExtension.java | 101 + .../doric/extension/bridge/DoricMethod.java | 38 + .../doric/extension/bridge/DoricPlugin.java | 34 + .../doric/extension/bridge/DoricPromise.java | 56 + .../extension/timer/DoricTimerExtension.java | 85 + .../pub/doric/loader/DoricAssetJSLoader.java | 36 + .../pub/doric/loader/DoricHttpJSLoader.java | 64 + .../doric/loader/DoricJSLoaderManager.java | 51 + .../java/pub/doric/loader/IDoricJSLoader.java | 29 + .../pub/doric/navbar/BaseDoricNavBar.java | 105 + .../java/pub/doric/navbar/IDoricNavBar.java | 31 + .../pub/doric/navigator/IDoricNavigator.java | 27 + .../java/pub/doric/plugin/AnimatePlugin.java | 86 + .../pub/doric/plugin/DoricJavaPlugin.java | 30 + .../java/pub/doric/plugin/ModalPlugin.java | 211 ++ .../java/pub/doric/plugin/NavBarPlugin.java | 114 + .../pub/doric/plugin/NavigatorPlugin.java | 61 + .../java/pub/doric/plugin/NetworkPlugin.java | 115 + .../java/pub/doric/plugin/PopoverPlugin.java | 139 + .../java/pub/doric/plugin/ShaderPlugin.java | 186 ++ .../java/pub/doric/plugin/StoragePlugin.java | 110 + .../pub/doric/refresh/DoricRefreshView.java | 103 + .../pub/doric/refresh/DoricSwipeLayout.java | 920 +++++++ .../pub/doric/refresh/PullingListener.java | 20 + .../pub/doric/refresh/RefreshableNode.java | 196 ++ .../java/pub/doric/shader/DoricLayer.java | 161 ++ .../main/java/pub/doric/shader/GroupNode.java | 157 ++ .../java/pub/doric/shader/HLayoutNode.java | 42 + .../main/java/pub/doric/shader/ImageNode.java | 129 + .../java/pub/doric/shader/LinearNode.java | 89 + .../main/java/pub/doric/shader/RootNode.java | 56 + .../java/pub/doric/shader/ScrollerNode.java | 101 + .../main/java/pub/doric/shader/StackNode.java | 61 + .../main/java/pub/doric/shader/SuperNode.java | 180 ++ .../main/java/pub/doric/shader/TextNode.java | 67 + .../java/pub/doric/shader/VLayoutNode.java | 41 + .../main/java/pub/doric/shader/ViewNode.java | 829 ++++++ .../doric/shader/flowlayout/FlowAdapter.java | 137 + .../shader/flowlayout/FlowLayoutItemNode.java | 56 + .../shader/flowlayout/FlowLayoutNode.java | 146 ++ .../pub/doric/shader/list/ListAdapter.java | 137 + .../pub/doric/shader/list/ListItemNode.java | 56 + .../java/pub/doric/shader/list/ListNode.java | 125 + .../pub/doric/shader/slider/SlideAdapter.java | 136 + .../doric/shader/slider/SlideItemNode.java | 56 + .../pub/doric/shader/slider/SliderNode.java | 125 + .../java/pub/doric/utils/DoricConstant.java | 71 + .../pub/doric/utils/DoricContextHolder.java | 35 + .../main/java/pub/doric/utils/DoricLog.java | 64 + .../java/pub/doric/utils/DoricMetaInfo.java | 78 + .../main/java/pub/doric/utils/DoricUtils.java | 235 ++ .../main/java/pub/doric/utils/ThreadMode.java | 27 + .../java/pub/doric/widget/HVScrollView.java | 2250 +++++++++++++++++ .../res/drawable-xhdpi/doric_icon_back.png | Bin 0 -> 1098 bytes doric/src/main/res/drawable/divider.xml | 5 + doric/src/main/res/layout/doric_activity.xml | 5 + doric/src/main/res/layout/doric_fragment.xml | 7 + .../main/res/layout/doric_framgent_panel.xml | 18 + .../main/res/layout/doric_modal_prompt.xml | 21 + doric/src/main/res/layout/doric_navigator.xml | 63 + doric/src/main/res/values-v21/styles.xml | 5 + doric/src/main/res/values/strings.xml | 3 + doric/src/main/res/values/styles.xml | 15 + gradle.properties | 22 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++ gradlew.bat | 84 + settings.gradle | 1 + 161 files changed, 13197 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/main/AndroidManifest.xml create mode 120000 app/src/main/assets/demo create mode 100644 app/src/main/java/pub/doric/demo/DemoActivity.java create mode 100644 app/src/main/java/pub/doric/demo/DemoLibrary.java create mode 100644 app/src/main/java/pub/doric/demo/DemoPlugin.java create mode 100644 app/src/main/java/pub/doric/demo/MainActivity.java create mode 100644 app/src/main/java/pub/doric/demo/MyApplication.java create mode 100644 app/src/main/java/pub/doric/demo/MyGlideModule.java create mode 100644 app/src/main/java/pub/doric/demo/PullableActivity.java create mode 100644 app/src/main/res/drawable-v24/ic_launcher_foreground.xml create mode 100644 app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 app/src/main/res/layout/activity_main.xml create mode 100644 app/src/main/res/layout/activity_pullable.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 app/src/main/res/values/colors.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/network_security_config.xml create mode 100644 build.gradle create mode 100644 devkit/.gitignore create mode 100644 devkit/build.gradle create mode 100644 devkit/consumer-rules.pro create mode 100644 devkit/proguard-rules.pro create mode 100644 devkit/src/androidTest/java/pub/doric/devkit/ExampleInstrumentedTest.java create mode 100644 devkit/src/main/AndroidManifest.xml create mode 100644 devkit/src/main/java/pub/doric/devkit/DevKit.java create mode 100644 devkit/src/main/java/pub/doric/devkit/DoricContextDebuggable.java create mode 100644 devkit/src/main/java/pub/doric/devkit/DoricDebugDriver.java create mode 100644 devkit/src/main/java/pub/doric/devkit/DoricDebugJSEngine.java create mode 100644 devkit/src/main/java/pub/doric/devkit/DoricDev.java create mode 100644 devkit/src/main/java/pub/doric/devkit/IDevKit.java create mode 100644 devkit/src/main/java/pub/doric/devkit/IStatusCallback.java create mode 100644 devkit/src/main/java/pub/doric/devkit/LocalServer.java create mode 100644 devkit/src/main/java/pub/doric/devkit/WSClient.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/ConnectExceptionEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/EOFExceptionEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/EnterDebugEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/OpenEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/QuitDebugEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/event/ReloadEvent.java create mode 100644 devkit/src/main/java/pub/doric/devkit/remote/DoricRemoteJSExecutor.java create mode 100644 devkit/src/main/java/pub/doric/devkit/remote/RemoteJSExecutor.java create mode 100644 devkit/src/main/java/pub/doric/devkit/remote/ValueBuilder.java create mode 100644 devkit/src/main/java/pub/doric/devkit/ui/DebugContextPanel.java create mode 100644 devkit/src/main/java/pub/doric/devkit/ui/DemoDebugActivity.java create mode 100644 devkit/src/main/java/pub/doric/devkit/ui/DevPanel.java create mode 100644 devkit/src/main/java/pub/doric/devkit/ui/ScanQRCodeActivity.java create mode 100644 devkit/src/main/java/pub/doric/devkit/util/SensorManagerHelper.java create mode 100644 devkit/src/main/res/layout/layout_debug_context.xml create mode 100644 devkit/src/main/res/layout/layout_debug_context_cell.xml create mode 100644 devkit/src/main/res/layout/layout_dev.xml create mode 100644 devkit/src/main/res/layout/layout_scan_qrcode.xml create mode 100644 devkit/src/main/res/values/strings.xml create mode 100644 doric/.gitignore create mode 100644 doric/build.gradle create mode 100644 doric/proguard-rules.pro create mode 100644 doric/src/main/AndroidManifest.xml create mode 120000 doric/src/main/assets/bundle create mode 120000 doric/src/main/assets/debugger create mode 100644 doric/src/main/java/pub/doric/Doric.java create mode 100644 doric/src/main/java/pub/doric/DoricActivity.java create mode 100644 doric/src/main/java/pub/doric/DoricComponent.java create mode 100644 doric/src/main/java/pub/doric/DoricContext.java create mode 100644 doric/src/main/java/pub/doric/DoricContextManager.java create mode 100644 doric/src/main/java/pub/doric/DoricFragment.java create mode 100644 doric/src/main/java/pub/doric/DoricLibrary.java create mode 100644 doric/src/main/java/pub/doric/DoricNativeDriver.java create mode 100644 doric/src/main/java/pub/doric/DoricPanel.java create mode 100644 doric/src/main/java/pub/doric/DoricPanelFragment.java create mode 100644 doric/src/main/java/pub/doric/DoricRegistry.java create mode 100644 doric/src/main/java/pub/doric/IDoricDriver.java create mode 100644 doric/src/main/java/pub/doric/async/AsyncCall.java create mode 100644 doric/src/main/java/pub/doric/async/AsyncResult.java create mode 100644 doric/src/main/java/pub/doric/async/SettableFuture.java create mode 100644 doric/src/main/java/pub/doric/engine/DoricJSEngine.java create mode 100644 doric/src/main/java/pub/doric/engine/DoricNativeJSExecutor.java create mode 100644 doric/src/main/java/pub/doric/engine/IDoricJSE.java create mode 100644 doric/src/main/java/pub/doric/extension/bridge/DoricBridgeExtension.java create mode 100644 doric/src/main/java/pub/doric/extension/bridge/DoricMethod.java create mode 100644 doric/src/main/java/pub/doric/extension/bridge/DoricPlugin.java create mode 100644 doric/src/main/java/pub/doric/extension/bridge/DoricPromise.java create mode 100644 doric/src/main/java/pub/doric/extension/timer/DoricTimerExtension.java create mode 100644 doric/src/main/java/pub/doric/loader/DoricAssetJSLoader.java create mode 100644 doric/src/main/java/pub/doric/loader/DoricHttpJSLoader.java create mode 100644 doric/src/main/java/pub/doric/loader/DoricJSLoaderManager.java create mode 100644 doric/src/main/java/pub/doric/loader/IDoricJSLoader.java create mode 100644 doric/src/main/java/pub/doric/navbar/BaseDoricNavBar.java create mode 100644 doric/src/main/java/pub/doric/navbar/IDoricNavBar.java create mode 100644 doric/src/main/java/pub/doric/navigator/IDoricNavigator.java create mode 100644 doric/src/main/java/pub/doric/plugin/AnimatePlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/DoricJavaPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/ModalPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/NavBarPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/NavigatorPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/NetworkPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/PopoverPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/ShaderPlugin.java create mode 100644 doric/src/main/java/pub/doric/plugin/StoragePlugin.java create mode 100644 doric/src/main/java/pub/doric/refresh/DoricRefreshView.java create mode 100644 doric/src/main/java/pub/doric/refresh/DoricSwipeLayout.java create mode 100644 doric/src/main/java/pub/doric/refresh/PullingListener.java create mode 100644 doric/src/main/java/pub/doric/refresh/RefreshableNode.java create mode 100644 doric/src/main/java/pub/doric/shader/DoricLayer.java create mode 100644 doric/src/main/java/pub/doric/shader/GroupNode.java create mode 100644 doric/src/main/java/pub/doric/shader/HLayoutNode.java create mode 100644 doric/src/main/java/pub/doric/shader/ImageNode.java create mode 100644 doric/src/main/java/pub/doric/shader/LinearNode.java create mode 100644 doric/src/main/java/pub/doric/shader/RootNode.java create mode 100644 doric/src/main/java/pub/doric/shader/ScrollerNode.java create mode 100644 doric/src/main/java/pub/doric/shader/StackNode.java create mode 100644 doric/src/main/java/pub/doric/shader/SuperNode.java create mode 100644 doric/src/main/java/pub/doric/shader/TextNode.java create mode 100644 doric/src/main/java/pub/doric/shader/VLayoutNode.java create mode 100644 doric/src/main/java/pub/doric/shader/ViewNode.java create mode 100644 doric/src/main/java/pub/doric/shader/flowlayout/FlowAdapter.java create mode 100644 doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutItemNode.java create mode 100644 doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutNode.java create mode 100644 doric/src/main/java/pub/doric/shader/list/ListAdapter.java create mode 100644 doric/src/main/java/pub/doric/shader/list/ListItemNode.java create mode 100644 doric/src/main/java/pub/doric/shader/list/ListNode.java create mode 100644 doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java create mode 100644 doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java create mode 100644 doric/src/main/java/pub/doric/shader/slider/SliderNode.java create mode 100644 doric/src/main/java/pub/doric/utils/DoricConstant.java create mode 100644 doric/src/main/java/pub/doric/utils/DoricContextHolder.java create mode 100644 doric/src/main/java/pub/doric/utils/DoricLog.java create mode 100644 doric/src/main/java/pub/doric/utils/DoricMetaInfo.java create mode 100644 doric/src/main/java/pub/doric/utils/DoricUtils.java create mode 100644 doric/src/main/java/pub/doric/utils/ThreadMode.java create mode 100644 doric/src/main/java/pub/doric/widget/HVScrollView.java create mode 100644 doric/src/main/res/drawable-xhdpi/doric_icon_back.png create mode 100644 doric/src/main/res/drawable/divider.xml create mode 100644 doric/src/main/res/layout/doric_activity.xml create mode 100644 doric/src/main/res/layout/doric_fragment.xml create mode 100644 doric/src/main/res/layout/doric_framgent_panel.xml create mode 100644 doric/src/main/res/layout/doric_modal_prompt.xml create mode 100644 doric/src/main/res/layout/doric_navigator.xml create mode 100644 doric/src/main/res/values-v21/styles.xml create mode 100644 doric/src/main/res/values/strings.xml create mode 100644 doric/src/main/res/values/styles.xml create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..0df7064d --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 00000000..3f343c91 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,33 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 29 + buildToolsVersion '29.0.2' + defaultConfig { + applicationId "pub.doric.demo" + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "com.google.android.material:material:1.0.0" + implementation project(':devkit') + implementation 'com.github.bumptech.glide:glide:4.10.0' + implementation 'com.github.bumptech.glide:annotations:4.10.0' + implementation 'com.github.penfeizhou.android.animation:glide-plugin:1.3.1' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0' + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..3988de39 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/demo b/app/src/main/assets/demo new file mode 120000 index 00000000..6bd58cbd --- /dev/null +++ b/app/src/main/assets/demo @@ -0,0 +1 @@ +../../../../../demo/bundle/src \ No newline at end of file diff --git a/app/src/main/java/pub/doric/demo/DemoActivity.java b/app/src/main/java/pub/doric/demo/DemoActivity.java new file mode 100644 index 00000000..1ec7b59a --- /dev/null +++ b/app/src/main/java/pub/doric/demo/DemoActivity.java @@ -0,0 +1,46 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import android.os.Bundle; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import pub.doric.DoricContext; +import pub.doric.DoricPanel; +import pub.doric.utils.DoricUtils; + +/** + * @Description: pub.doric.demo + * @Author: pengfei.zhou + * @CreateDate: 2019-11-19 + */ +public class DemoActivity extends AppCompatActivity { + private DoricContext doricContext; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String source = getIntent().getStringExtra("source"); + DoricPanel doricPanel = new DoricPanel(this); + addContentView(doricPanel, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + doricPanel.config(DoricUtils.readAssetFile("demo/" + source), source); + doricContext = doricPanel.getDoricContext(); + } +} diff --git a/app/src/main/java/pub/doric/demo/DemoLibrary.java b/app/src/main/java/pub/doric/demo/DemoLibrary.java new file mode 100644 index 00000000..4a76f7b4 --- /dev/null +++ b/app/src/main/java/pub/doric/demo/DemoLibrary.java @@ -0,0 +1,28 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import pub.doric.DoricComponent; +import pub.doric.DoricLibrary; +import pub.doric.DoricRegistry; + +@DoricComponent +public class DemoLibrary extends DoricLibrary { + @Override + public void load(DoricRegistry registry) { + registry.registerNativePlugin(DemoPlugin.class); + } +} diff --git a/app/src/main/java/pub/doric/demo/DemoPlugin.java b/app/src/main/java/pub/doric/demo/DemoPlugin.java new file mode 100644 index 00000000..3fbe6658 --- /dev/null +++ b/app/src/main/java/pub/doric/demo/DemoPlugin.java @@ -0,0 +1,49 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import android.widget.Toast; + +import com.github.pengfeizhou.jscore.JavaValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.plugin.DoricJavaPlugin; +import pub.doric.utils.ThreadMode; + +@DoricPlugin(name = "demo") +public class DemoPlugin extends DoricJavaPlugin { + public DemoPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod(thread = ThreadMode.UI) + public void test() { + Toast.makeText(getDoricContext().getContext(), "test", Toast.LENGTH_SHORT).show(); + } + + @DoricMethod(thread = ThreadMode.UI) + public void testPromise(boolean b, DoricPromise doricPromise) { + if (b) { + doricPromise.resolve(new JavaValue("resolved by me")); + } else { + doricPromise.reject(new JavaValue("rejected by me")); + } + Toast.makeText(getDoricContext().getContext(), "test", Toast.LENGTH_SHORT).show(); + } +} diff --git a/app/src/main/java/pub/doric/demo/MainActivity.java b/app/src/main/java/pub/doric/demo/MainActivity.java new file mode 100644 index 00000000..b9153c44 --- /dev/null +++ b/app/src/main/java/pub/doric/demo/MainActivity.java @@ -0,0 +1,134 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import pub.doric.DoricActivity; +import pub.doric.devkit.ui.DemoDebugActivity; +import pub.doric.refresh.DoricSwipeLayout; +import pub.doric.utils.DoricUtils; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.activity_main); + final DoricSwipeLayout swipeLayout = findViewById(R.id.swipe_layout); + swipeLayout.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() { + @Override + public void onRefresh() { + swipeLayout.setRefreshing(false); + } + }); + swipeLayout.setBackgroundColor(Color.YELLOW); + swipeLayout.getRefreshView().setBackgroundColor(Color.RED); + TextView textView = new TextView(this); + textView.setText("This is header"); + swipeLayout.getRefreshView().setContent(textView); + RecyclerView recyclerView = findViewById(R.id.root); + recyclerView.setBackgroundColor(Color.WHITE); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + try { + String[] demos = getAssets().list("demo"); + List ret = new ArrayList<>(); + ret.add("Test"); + for (String str : demos) { + if (str.endsWith("js")) { + ret.add(str); + } + } + recyclerView.setAdapter(new MyAdapter(ret.toArray(new String[0]))); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public class MyAdapter extends RecyclerView.Adapter { + + private final String[] data; + + public MyAdapter(String[] demos) { + this.data = demos; + } + + @NonNull + @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + TextView textView = new TextView(parent.getContext()); + textView.setGravity(Gravity.CENTER); + textView.setLayoutParams(new ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + DoricUtils.dp2px(50))); + textView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20); + return new RecyclerView.ViewHolder(textView) { + @Override + public String toString() { + return super.toString(); + } + }; + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, final int position) { + final TextView tv = (TextView) holder.itemView; + tv.setText(data[position]); + tv.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (data[position].contains("Test")) { + Intent intent = new Intent(tv.getContext(), PullableActivity.class); + tv.getContext().startActivity(intent); + return; + } + if (data[position].contains("NavigatorDemo")) { + Intent intent = new Intent(tv.getContext(), DoricActivity.class); + intent.putExtra("scheme", "assets://demo/" + data[position]); + intent.putExtra("alias", data[position]); + tv.getContext().startActivity(intent); + return; + } + Intent intent = new Intent(tv.getContext(), DemoDebugActivity.class); + intent.putExtra("source", data[position]); + tv.getContext().startActivity(intent); + } + }); + } + + @Override + public int getItemCount() { + return data.length; + } + } +} diff --git a/app/src/main/java/pub/doric/demo/MyApplication.java b/app/src/main/java/pub/doric/demo/MyApplication.java new file mode 100644 index 00000000..225a1cda --- /dev/null +++ b/app/src/main/java/pub/doric/demo/MyApplication.java @@ -0,0 +1,30 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import android.app.Application; + +import pub.doric.Doric; +import pub.doric.DoricRegistry; + +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + Doric.init(this); + DoricRegistry.register(new DemoLibrary()); + } +} diff --git a/app/src/main/java/pub/doric/demo/MyGlideModule.java b/app/src/main/java/pub/doric/demo/MyGlideModule.java new file mode 100644 index 00000000..6c61e875 --- /dev/null +++ b/app/src/main/java/pub/doric/demo/MyGlideModule.java @@ -0,0 +1,23 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.demo; + +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; + +@GlideModule +public class MyGlideModule extends AppGlideModule { +} diff --git a/app/src/main/java/pub/doric/demo/PullableActivity.java b/app/src/main/java/pub/doric/demo/PullableActivity.java new file mode 100644 index 00000000..2ce73a2a --- /dev/null +++ b/app/src/main/java/pub/doric/demo/PullableActivity.java @@ -0,0 +1,28 @@ +package pub.doric.demo; + +import androidx.appcompat.app.AppCompatActivity; + +import android.graphics.Color; +import android.os.Bundle; +import android.widget.FrameLayout; + +import pub.doric.refresh.DoricSwipeLayout; + +public class PullableActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_pullable); + final DoricSwipeLayout swipeRefreshLayout = findViewById(R.id.swipe_layout); + FrameLayout frameLayout = new FrameLayout(this); + frameLayout.setBackgroundColor(Color.YELLOW); + swipeRefreshLayout.addView(frameLayout); + swipeRefreshLayout.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() { + @Override + public void onRefresh() { + swipeRefreshLayout.setRefreshing(false); + } + }); + } +} diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 00000000..1f6bb290 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..0d025f9b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..8a4718b2 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_pullable.xml b/app/src/main/res/layout/activity_pullable.xml new file mode 100644 index 00000000..c6923232 --- /dev/null +++ b/app/src/main/res/layout/activity_pullable.xml @@ -0,0 +1,13 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..eca70cfe --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..898f3ed59ac9f3248734a00e5902736c9367d455 GIT binary patch literal 2963 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 JPDHLkV1g@XpTGbB literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..dffca3601eba7bf5f409bdd520820e2eb5122c75 GIT binary patch literal 4905 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 lV%Z2*hu);6rydA(}wUDXPCF_W1vnaRBK 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..14ed0af35023e4f1901cf03487b6c524257b8483 GIT binary patch literal 6895 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b0907cac3bfd8fbfdc46e1108247f0a1055387ec GIT binary patch literal 6387 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..d8ae03154975f397f8ed1b84f2d4bf9783ecfa26 GIT binary patch literal 10413 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 XdJ+W}DiIkV00000NkvXXu0mjfKBlg6 literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2c18de9e66108411737e910f5c1972476f03ddbf GIT binary patch literal 9128 zcmb`NcT^K!5btji2)!5SAPPuNq)Ls56s4*38hVo^(nUfO6%ZAH(6N9hNR=iCp@USV zNUs_|I-wKc#ou}5-}laWIcKxU$(_yIot@8o_s%{sGSH@@=As4w(CO-E-X`sF|29fE z>HYT9T?zm$_~>e0H4dIw&!!4C9vSZxNlr9*d^_s#H!1R~WS_6MVYz@X@%G!e zXHz-tb|VivQj`iFZDUWNj>i`*9rwT8VC9f`)ww2)D0tG&WBFX^J|oMigqUy#_eV)Q z<3?;pz6pkr(;Z)thNWZ3Tu^XIU(m2~K2{iFEAS`~Gy5VW_tC>i*Cl0kv`b9xtW+!e zPD_a1*)E4YGCWy+8(ZVrP7}Y9URLg*>8E8fyY^0u;VQCkoBQJ<_5zdXl(d!zb~b;b z)6|dkG)>oK`*erN6Q98nTc z*T4b)onLqyA@?UYxy_MYQjd+D&|e(Pm(0oT&BjWQ4@?kFIoB**?M#(;rSUW9SnG<- zSt-|WaL6iG_P3uZd9eIpr{TtNWC*$Hh2Qz?uBS}bIbRfO#e{zRE!IEy&YexD%F}@N zL-y@k#YdI*GK@^S9Mw$gu9^2z1mSnEkrdxz+MPN|ZNhhS)_oYvhM)cLTYGn3J-&{3 z*gO%dE$+F=!pgEJp;TQOxUvmXY0MZXd)l&aIQ@q%&TOO4FwrA~ak$>;=zXV4zzr%` z=0~OcyNxrVAu`L~2ctf1)jOUXrl5QhI{u_3cR4;2>t?n_c`o(TMz?xA14+Wh$Va%BY0&2$WKO9mM2sYf3h-OCY*=ZOJ$Ngw)1D_iorRZXHQZi4&2K7qT927nQC0Lrg3 z(#lL522bDvLQQ|!4#s}u&v;Yf6v=QytSm1*VR`JzNHPFHGlJ!`WMgHC3lNnE^`=*0 zy?^9tJWsJlLSn+d=%5(DNQYCcv%)omexK}hyZmUHWQF=7JRFKXB_b-*?UD4{x!=dVwazRjll3YN!e1GQ6{ViI{ zhkd)N+MWKT`q_V0)j;tA_oAca{;nI(Y$Pb7t7Zgb7)DUREOEf@igE4Q;TqcgkX-wd zJ;8G+7!?>DALr#bk)GNchOvQs{BBN~iU1F0&RMR&ou$CHl>C|ZrZ@PkAenI@K>Al% zQ7|N8uxRTq4vM*lnm?oa%}HLn-3G$yJC_b75?=65k%LM)%(H@{N`65=i4pdO>Mz+= zLeav25B?f086=X6O6;%!2@%ZP1|;Nvbnj_2aSc+8ZOx$k{x3Drh^ zc*UWh!@lFm$>1}Uo>u2rUqXSar;=W-2Mqo41Pl(rQD;>HWC;@e#W@Z29HUt(caNqC zC&6BqG(7E8;B^rX*m6|Ejm>-6L>RWQs{?%J*!{N&Cn3FMX$DmBS8~(Emio*Dj(^J_ zk~mE@d*561epZk|Er>78iC#q_4Sp0Y3GD6B@JKKrmyoJG4WGBh)HqTZZw>kH>(OJH zlp#iE)N?g*Z@4^*MV+s+H!!1LJlIN*`JxC#o-v0{2|BS}}kDUMqX8%d%;Zo1pF*{G_rVrzNd`M2ya!T0DJTesuRVwL9u7n&PS ze_~l@1G?`(riUCq#<3T)^gi`sw~pk^JSP})C#_iBKTD*{^N7d0$A0wJ3#IRYe;0q4 zA*$YJb_LE1lo-`!M^fB~U00SLiLywh>%-_CXgSb{ju=7v+FzB+78O;y>TeZvRv&RoWxTLP?d+9Zi&Ypua2+{3 z?&P=TOQKt{%~L~p0$j8^;iia9j_>fKovkcwq%sUQ@nh>Z!)%cfJ0$;z4CPrz6I0OU z@+^ZT$qbq`@V*LyaM7l>CZ1ZQo!IplAN5a81(Tt~ztAbYc(d{@u2@?f2YdnGcoX!#60Ixw-Nvix#$k1X*NJg)beTLqL8^6*<{2f@@ns|Q}RjZ!$JIHK8NbS8xrmu#@ z6ulfiVr7xxNb~dV#acSrSX_pQm;bUeyjdV!{OZy#M4(A` zwu81?V`O!?oZ`D{REMi+x!1hB*6Cy(I?k8T%kET=uKQWo39E}=ca$my=uHTEyP8y z54Nz1YH*)(w%#ztIo^C*PQOjte`Hel~gpFN_jZaXoFZnUzuu<)94E6T<5ZU?s4>c zpU3Uo@d?+!hgYmVil!6X(ly;KNm*OwbI8{z3v|%I_4HT>Nt&7^q0@@SPXaA`iAvAR zSr*v1muELwpeL3wqu$P7L5q4m)-N%|J6fE`4!V+xyrOkr+X2!LT$k#tFYksHJH=n z3F!I2Qe4B5pnFmAer;+($yQcgD*uHlDurPx@2dd)1-RjhQe(5`*~SLS`q|S9v+`3~ zQ>IMi+hcTX^%}_YWT=}koWlGSwSH~mOvRNJ&Sfrc>H__ux(6*kTUubhdoQN>V2}J< zR)ymBx4g=I%zlp1J+QjI7joltSLskIt}qG%d@lfB@0(d>+A&l+Glwv&La86NxDmfT zNv>`p7eT?@iBSF8R6M^wCx1D;HRt!F#6s8>2mF;&B-MF;2m~@G4CaiZ!p=4aG-$V0 zYR+PtSNvY$YwW0OPYxL-i+8&!G0&s(?(IcQ&Iv2 z0Nx*-7_~pZT6#2L-so8nF7QMgH5}#22w+dCGMyllm->HAO8q%eYuJ_BHB7343cyG+ zgo9$W05T7{CPl`Zw^P=q+#rx_`T2%M zMCeCJLfZT%fI{csusPnQ7Xv@XSzVNmPU{iX2w134>~=VfgQ82*rq^p^97wA647vgT`a# z85e!NpbSl#8uA*dnopv4RMby4F4MY{UFn^r{Li3l%Ume;QtBh5?8wCixw0*zSQ${* z6)@M`djm|Nz;H2K_j1ACvx90`pqKN#`9b8Cd=@J|$6R{ZYc5yw){(D1GtABWH=Zy` z-HxQuV(8LOB`UjI4iAOJ34LY@KVEmPb@XIC)FfA6m5B&*8T*hQyR{mweAL1#*kA9n z;O}eZUE%DcD;yjrQM!F!8~hPzPrCH2Fvr-ItjJE$$pV*gv9>ye(q2lsB=uQP$h%X% zlekK6q~fP4niGy&O9mR~_I;)G@;?e;L8#rja{}{3_rR(d$+fAsX?PiFx`2ashkOGP zw9A><#);kE3G}H}!W&WxH1$sg*P@*n!{=#L{PK)y~GHI;RsgpA$#8cpY~ zct*9kjG$l!k{*0T43n={dVV!idt6Zw;lPW%!2K;#E>?J>D|V%r^A`&*)MdYZJT>jL z*;x5TTDFevc8OARtqyN`Wyt;0MTTO-DDG|wtNxUqM1$~ye0&&wUtZ&eqI0=0|Y{WT*|Ia1An)J!bjzf9y3P874R^|FamuD zD47YqkS6Zsd3^fEq_zq1i3zN7fM#ldxb7Z@0Y;<&n|qFI`e8q;TO3t$s`geh?U*oK zp&F$0CKJFD-a%BYO^4KA!5J4T1f9rK@Izkpt4qui#^S_s8AE_pvL7$dKQ z*TXfMJYx+MCq$g?pCj@15ZQdjbAm~v`@A?MCg`$$;e!iKvcv423 z^QOF{_mgOGh3-cDZ={Gyr z_&&UYqVw>f(5K`SHp~Mm5XB0N9$~=XOXd$uQNj=bO95ChnZX9K@n&#T?vXPDfqt07xJZVvBuujM>H*4hP6HvbJ~#$K=z-vNQnRCryVz5?3YqR02@1#K{#%aX?h4VQ45b zcmM<+1V?|eCnx}P7(IWh<1mpP1d4*Z4r1WAfB;C4dhrfKPC^**Pz;nD$YOJ0I9i3T zdQ`v*UjtnCM$WL`J8L<$;~1_X+Oyzj(IKG(tLOn!YS8Vny{ z@>lc1XCA-~hhrD7h1@0O)T))gw+GcvsVwxcnaCv{EQzu|qcwKGyiwb`TTP(}njGXHh$KxOryTWq$B1F6I8!hh2O<$rL^FOXZoKME=~3M&0eN93bd- zfpL<(mU)+asMc@#Mvb?Ws^Rw;E;iny$Mb$bu)1ovt0lOm4f(~cAmY<65o0ePN*$EX zrmHUhGI1J_t=@d`{#mmFd?eV^Q&jw>g^;Pf)7JHdLzQB*87{77?Kto0xMvGjC=&M5EOW+c zXpXOY6|Uf)0am19ZLde+hX5J6c11*#mSinvk^A4NWc#m5P)?v~|Bppv*0~T;-^rI9{w3{`~5)bC}`nF?zGx z#@S`#(Q@kl-1Fmze)A@u^#@9=c>MA>$*eslP^G`Zvb5N|sKK{mQ*V?4eX_x+nT?*N zalRRl;P=w1HG57g+d^AJQCZh4&g{?mbJZuj*>jJpGL#!`*C>{MRd4-HML#+BNUG#EHx5`rs8QUMda13u9eMG(lKCYTHCS2gO0L&PIU zkkI-^jv5$aR|blKRsJ6xJ^?au7%A7>eD6+l!ALkEL&*RPl442Nll#UeUv)cn5=YV~ zP)$eQ=SZYMG+hSAy@o*c95}KXP7(~*M%`ovFuZos#RM5t0XkRn?DdjD!7zh+HMGoz6C^Gk*}xdzg{VaE0-2L4An_I# z_)DVjA|u=a+{fkuUkWg+!HA~@f87&ENbQ{u_}}LPin9T}}BZ5K1W#~XT5z0gcc+cy7@$?+tH6Ta*1qVBL@ zBwd%m=LAwRv8~~Cx3MfLmwax@N%=M`ciGYizcDPi#Qug{`#^)V(iZGpR*3ayNFiWv zCT;%Yg?Tn;SO3Pvyu6Dolgt$Pq@8;O(nD{uHM<__6!t9UUP@K#N73GQB){T~9Hpci z<4P6T>Kb;ktBMTne4`e~@)E&sIdENQj5G9OYu`7~bvsRTeRl1z?i^aI{)?VNlekCC zXJKVy+B;Z0|Abe1cpfcW)93y`*4%NW#+1!-OVtut{#3Q5fvBQ-b<*gu4x4f6pmz-x)Q8wc+4G^!kGq??b_{28Zdu9+dS0=wgR`1Va^@f*j96v zE?=;Q{AtjKXi>F3-EkrPfL<`s@S z(Cl$t|NBt^_k;7j{U(%~9iLt{7g5yFfhq?^mE$`_Z>W$9l{seeXUdzmz8$X$3_fz0 zNc_d*naeGkU7&S83}C%)Owd-QTjWCq)4F3puS?Y*tOH3*JX`9t7=HyB%;}BFw)~fX zP3M8Ef?E#|5Tf;EuVktd)#&vh7trJcyxkI{{O|eok{tE^hzi3_4LW$*rN)J?Qmy@$ z@GmJ)5nOLC0(h_C(Ayd(aO3hP5pxuMsRZfvoFgBCNNrsu!(1gLl_W1XDWi)1KiM4& z4TFIN4Z44?71-@F^TGn<^DjNF#jfDTD;qdJ36mB3{oK$>kk1T9x32)H^4{v<&J$?GFZQeeKn zog^e?9JHCkaVAg{99*Xytpn)yWZ-y+!;hT(I=Fwaat_Fckc87LJ*r7!)y;@7k^fUK zxl{eySNWG_U%a8X+L`q+Pwk<%iyJN!iw;Q%=1>$p(4~A8CwtPS13^pt$BA_79TEm3 z!hx@gB4KmstaCTszUdc8*ch3y0f@{;*awP0cxYg(J0u?XLQsFzBA;#(`vHd`I*lBM z;(99!j{626=)R8+$DgEz-MfuzaGI&_b*%9#-BUQaw^>IHgp<=gob@UA0r`@#>-qw0 zpfFP4HZ?#}t^J2jFG?J|6<^ALo3?t>Oz5`IuInteCESw+$NTFo3L77A?}>NbqA$vz z-v81kRTwtLT8^1Hkf#X&iRsn`fKmr-Mu&N{*qwp;$qBXyT}BAQ@L;wB^UWEXX)3_b zh&*ke8czIhFd!IxCi_N!jnrKGIQpfPR2xJo1%*JNF^PvDwB;>G~7@ zQVZ23Q}9_P0C|)?QPY(DS0!&Y!!b^`S|XCy zKNy*Kil!;HIXgI}+mn{ko*V0S7_|JPJm`{p{nOe9Vi^>B;a*toh zNY>_;v-=$AgIA44ebwp@a!75wJN7K9j;+SW z8uoQjVUb03=55d=@#Y_9`Fs=Ut|9xs?0ce>@0mn&q+oSJdb^!tTO8;mb$%l));(4- zKPebA@3lPn z@G1otTd9DCo-AAllf-ruy4anJn=H{RXLG>6j;g|@m(&__Lzek=U-sRZzRO1lOrtOJ zm+5k9slTfFKsku7%a$T6ENphjA3uy9eG=kh6ii90n}D&mc!E$-XY)ycsx6qljq9PY zpDzzbG!`4}xmvrE+7f*Jx351b!!}L5XmvDjt;&0$*g9U$nbVZwscA2!5>S?vG~K*d zPzXIIrnkt|yfEO5^dk>cVc0*&Hh$%zYA8nPL(Hwwk?vVuZpJ+&#LxCsujZ^dalGUq zk8X*2y(traI^+1KZEu-(_j%t<)w?tI>hVd#CUfisw!-|mSM{#>X=67C83>oRW^)Nc z_@hYvV5!q}p#c+`qTV9*kqk5GkA6Z;&)MXHw7m;gzS)ito45k#Ejt_oX>5cfTLfXUX@_N^+#UicK@ zbUwcCAj!Nyi??H{sraN8NiTB?aleSuG-iy_c^*{zg2xn*m1e+7rBnP~o!PuP9z$Gcf(C!4f_G&|`v9JI zHr460gE4qwW4yYiYMyx4c#(d_<1JDCcBZLe=D9DE4fC#q8)2D2Dpnaszf0h1)i*7) zxyKd8y*&dyiKySsH2Uj5(~gfdkoWmaI$)6ycN3CquawfZ+R8$$x+k;L>%Fd*;XYy0 zkq~3{maC~f(~h3ZUsXWo-EodvK!+KO{DW8g|IOnpPq%l@9Ky`Dd0%sz0@6$Ox`Aei I20H400LcNok^lez literal 0 HcmV?d00001 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..beed3cdd2c32af5114a7dc70b9ef5b698eb8797e GIT binary patch literal 15132 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..69b22338 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + #008577 + #00574B + #D81B60 + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..8a3bad8d --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Doric + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..5885930d --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 00000000..dca93c07 --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..143e9239 --- /dev/null +++ b/build.gradle @@ -0,0 +1,30 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + repositories { + google() + jcenter() + + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + maven { + url "https://dl.bintray.com/osborn/Android" + } + maven { url 'https://jitpack.io' } + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/devkit/.gitignore b/devkit/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/devkit/.gitignore @@ -0,0 +1 @@ +/build diff --git a/devkit/build.gradle b/devkit/build.gradle new file mode 100644 index 00000000..76882f07 --- /dev/null +++ b/devkit/build.gradle @@ -0,0 +1,52 @@ +apply plugin: 'com.android.library' + +def projectHome = project.rootDir.getParent() + "/demo" + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + + debug { + buildConfigField "String", "PROJECT_HOME", "\"${projectHome}\"" + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'com.github.pengfeizhou:jsc4a:0.1.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation "com.google.android.material:material:1.0.0" + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.google.code.gson:gson:2.8.6' + implementation 'cn.bingoogolapple:bga-qrcode-zbar:1.3.7' + implementation 'com.github.tbruyelle:rxpermissions:0.10.2' + implementation "io.reactivex.rxjava2:rxjava:2.2.15" + api 'org.greenrobot:eventbus:3.1.1' + implementation 'com.lahm.library:easy-protector-release:1.1.0' + api 'org.nanohttpd:nanohttpd:2.3.1' + api project(':doric') + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/devkit/consumer-rules.pro b/devkit/consumer-rules.pro new file mode 100644 index 00000000..e69de29b diff --git a/devkit/proguard-rules.pro b/devkit/proguard-rules.pro new file mode 100644 index 00000000..f1b42451 --- /dev/null +++ b/devkit/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/devkit/src/androidTest/java/pub/doric/devkit/ExampleInstrumentedTest.java b/devkit/src/androidTest/java/pub/doric/devkit/ExampleInstrumentedTest.java new file mode 100644 index 00000000..f4908b83 --- /dev/null +++ b/devkit/src/androidTest/java/pub/doric/devkit/ExampleInstrumentedTest.java @@ -0,0 +1,27 @@ +package pub.doric.devkit; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + assertEquals("pub.doric.devkit.test", appContext.getPackageName()); + } +} diff --git a/devkit/src/main/AndroidManifest.xml b/devkit/src/main/AndroidManifest.xml new file mode 100644 index 00000000..70d23e89 --- /dev/null +++ b/devkit/src/main/AndroidManifest.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/devkit/src/main/java/pub/doric/devkit/DevKit.java b/devkit/src/main/java/pub/doric/devkit/DevKit.java new file mode 100644 index 00000000..332b171d --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/DevKit.java @@ -0,0 +1,44 @@ +package pub.doric.devkit; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +public class DevKit implements IDevKit { + + public static boolean isRunningInEmulator = false; + public static String ip = ""; + private static Gson gson = new Gson(); + + private static class Inner { + private static final DevKit sInstance = new DevKit(); + } + + private DevKit() { + } + + public static DevKit getInstance() { + return Inner.sInstance; + } + + + private WSClient wsClient; + + @Override + public void connectDevKit(String url) { + wsClient = new WSClient(url); + } + + @Override + public void sendDevCommand(IDevKit.Command command, JsonObject jsonObject) { + JsonObject result = new JsonObject(); + result.addProperty("cmd", command.toString()); + result.add("data", jsonObject); + wsClient.send(gson.toJson(result)); + } + + @Override + public void disconnectDevKit() { + wsClient.close(); + wsClient = null; + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/DoricContextDebuggable.java b/devkit/src/main/java/pub/doric/devkit/DoricContextDebuggable.java new file mode 100644 index 00000000..1ff0b20a --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/DoricContextDebuggable.java @@ -0,0 +1,29 @@ +package pub.doric.devkit; + +import pub.doric.DoricContext; +import pub.doric.DoricNativeDriver; + +public class DoricContextDebuggable { + private DoricContext doricContext; + private DoricDebugDriver doricDebugDriver; + + public DoricContextDebuggable(DoricContext doricContext) { + this.doricContext = doricContext; + } + + public void startDebug() { + doricDebugDriver = new DoricDebugDriver(new IStatusCallback() { + @Override + public void start() { + doricContext.setDriver(doricDebugDriver); + doricContext.reInit(); + } + }); + } + + public void stopDebug() { + doricDebugDriver.destroy(); + doricContext.setDriver(DoricNativeDriver.getInstance()); + doricContext.reInit(); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/DoricDebugDriver.java b/devkit/src/main/java/pub/doric/devkit/DoricDebugDriver.java new file mode 100644 index 00000000..28d1b3b6 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/DoricDebugDriver.java @@ -0,0 +1,135 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.devkit; + +import android.os.Handler; +import android.os.Looper; + +import com.github.pengfeizhou.jscore.JSDecoder; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import pub.doric.DoricRegistry; +import pub.doric.IDoricDriver; +import pub.doric.async.AsyncCall; +import pub.doric.async.AsyncResult; +import pub.doric.engine.DoricJSEngine; +import pub.doric.utils.DoricConstant; +import pub.doric.utils.DoricLog; +import pub.doric.utils.ThreadMode; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricDebugDriver implements IDoricDriver { + private final DoricJSEngine doricJSEngine; + private final ExecutorService mBridgeExecutor; + private final Handler mUIHandler; + private final Handler mJSHandler; + + + public DoricDebugDriver(IStatusCallback statusCallback) { + doricJSEngine = new DoricDebugJSEngine(statusCallback); + mBridgeExecutor = Executors.newCachedThreadPool(); + mUIHandler = new Handler(Looper.getMainLooper()); + mJSHandler = doricJSEngine.getJSHandler(); + } + + @Override + public AsyncResult invokeContextEntityMethod(final String contextId, final String method, final Object... args) { + final Object[] nArgs = new Object[args.length + 2]; + nArgs[0] = contextId; + nArgs[1] = method; + if (args.length > 0) { + System.arraycopy(args, 0, nArgs, 2, args.length); + } + return invokeDoricMethod(DoricConstant.DORIC_CONTEXT_INVOKE, nArgs); + } + + @Override + public AsyncResult invokeDoricMethod(final String method, final Object... args) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public JSDecoder call() { + try { + return doricJSEngine.invokeDoricMethod(method, args); + } catch (Exception e) { + DoricLog.e("invokeDoricMethod(%s,...),error is %s", method, e.getLocalizedMessage()); + return new JSDecoder(null); + } + } + }); + } + + @Override + public AsyncResult asyncCall(Callable callable, ThreadMode threadMode) { + switch (threadMode) { + case JS: + return AsyncCall.ensureRunInHandler(mJSHandler, callable); + case UI: + return AsyncCall.ensureRunInHandler(mUIHandler, callable); + case INDEPENDENT: + default: + return AsyncCall.ensureRunInExecutor(mBridgeExecutor, callable); + } + } + + @Override + public AsyncResult createContext(final String contextId, final String script, final String source) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public Boolean call() { + try { + doricJSEngine.prepareContext(contextId, script, source); + return true; + } catch (Exception e) { + DoricLog.e("createContext %s error is %s", source, e.getLocalizedMessage()); + return false; + } + } + }); + } + + @Override + public AsyncResult destroyContext(final String contextId) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public Boolean call() { + try { + doricJSEngine.destroyContext(contextId); + return true; + } catch (Exception e) { + DoricLog.e("destroyContext %s error is %s", contextId, e.getLocalizedMessage()); + return false; + } + } + }); + } + + @Override + public DoricRegistry getRegistry() { + return doricJSEngine.getRegistry(); + } + + public void destroy() { + doricJSEngine.teardown(); + mBridgeExecutor.shutdown(); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/DoricDebugJSEngine.java b/devkit/src/main/java/pub/doric/devkit/DoricDebugJSEngine.java new file mode 100644 index 00000000..dde27193 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/DoricDebugJSEngine.java @@ -0,0 +1,19 @@ +package pub.doric.devkit; + +import pub.doric.devkit.remote.DoricRemoteJSExecutor; +import pub.doric.engine.DoricJSEngine; + +public class DoricDebugJSEngine extends DoricJSEngine { + + private IStatusCallback statusCallback; + + public DoricDebugJSEngine(IStatusCallback statusCallback) { + super(); + this.statusCallback = statusCallback; + } + + @Override + protected void initJSEngine() { + mDoricJSE = new DoricRemoteJSExecutor(statusCallback); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/DoricDev.java b/devkit/src/main/java/pub/doric/devkit/DoricDev.java new file mode 100644 index 00000000..d122da11 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/DoricDev.java @@ -0,0 +1,17 @@ +package pub.doric.devkit; + +import com.google.gson.JsonObject; + +public class DoricDev { + public static void connectDevKit(String url) { + DevKit.getInstance().connectDevKit(url); + } + + public static void sendDevCommand(IDevKit.Command command, JsonObject jsonObject) { + DevKit.getInstance().sendDevCommand(command, jsonObject); + } + + public static void disconnectDevKit() { + DevKit.getInstance().disconnectDevKit(); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/IDevKit.java b/devkit/src/main/java/pub/doric/devkit/IDevKit.java new file mode 100644 index 00000000..3d1f422a --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/IDevKit.java @@ -0,0 +1,16 @@ +package pub.doric.devkit; + +import com.google.gson.JsonObject; + +public interface IDevKit { + + enum Command { + DEBUG, HOT_RELOAD + } + + void connectDevKit(String url); + + void sendDevCommand(IDevKit.Command command, JsonObject jsonObject); + + void disconnectDevKit(); +} diff --git a/devkit/src/main/java/pub/doric/devkit/IStatusCallback.java b/devkit/src/main/java/pub/doric/devkit/IStatusCallback.java new file mode 100644 index 00000000..88c9a701 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/IStatusCallback.java @@ -0,0 +1,5 @@ +package pub.doric.devkit; + +public interface IStatusCallback { + void start(); +} diff --git a/devkit/src/main/java/pub/doric/devkit/LocalServer.java b/devkit/src/main/java/pub/doric/devkit/LocalServer.java new file mode 100644 index 00000000..a85417f6 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/LocalServer.java @@ -0,0 +1,181 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.devkit; + +import android.content.Context; +import android.content.res.AssetManager; +import android.net.Uri; +import android.util.Log; +import android.webkit.MimeTypeMap; + +import com.github.pengfeizhou.jscore.JSONBuilder; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.net.Inet4Address; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import fi.iki.elonen.NanoHTTPD; +import pub.doric.DoricContext; +import pub.doric.DoricContextManager; + +/** + * @Description: com.github.penfeizhou.doricdemo + * @Author: pengfei.zhou + * @CreateDate: 2019-08-03 + */ +public class LocalServer extends NanoHTTPD { + private final Context context; + private Map commandMap = new HashMap<>(); + + public LocalServer(Context context, int port) { + super(port); + this.context = context; + commandMap.put("allContexts", new APICommand() { + @Override + public String name() { + return "allContexts"; + } + + @Override + public Object exec(IHTTPSession session) { + Collection ret = DoricContextManager.aliveContexts(); + JSONArray jsonArray = new JSONArray(); + for (DoricContext doricContext : ret) { + JSONBuilder jsonBuilder = new JSONBuilder(); + jsonBuilder.put("source", doricContext.getSource()); + jsonBuilder.put("id", doricContext.getContextId()); + jsonArray.put(jsonBuilder.toJSONObject()); + } + return jsonArray; + } + }); + commandMap.put("context", new APICommand() { + @Override + public String name() { + return "context"; + } + + @Override + public Object exec(IHTTPSession session) { + String id = session.getParms().get("id"); + DoricContext doricContext = DoricContextManager.getContext(id); + if (doricContext != null) { + return new JSONBuilder() + .put("id", doricContext.getContextId()) + .put("source", doricContext.getSource()) + .put("script", doricContext.getScript()) + .toJSONObject(); + } + return "{}"; + } + }); + commandMap.put("reload", new APICommand() { + @Override + public String name() { + return "reload"; + } + + @Override + public Object exec(IHTTPSession session) { + Map files = new HashMap<>(); + try { + session.parseBody(files); + } catch (Exception e) { + e.printStackTrace(); + } + String id = session.getParms().get("id"); + DoricContext doricContext = DoricContextManager.getContext(id); + if (doricContext != null) { + try { + JSONObject jsonObject = new JSONObject(files.get("postData")); + doricContext.reload(jsonObject.optString("script")); + } catch (Exception e) { + e.printStackTrace(); + } + return "success"; + } + return "fail"; + } + }); + + } + + private static String getIpAddressString() { + try { + for (Enumeration enNetI = NetworkInterface + .getNetworkInterfaces(); enNetI.hasMoreElements(); ) { + NetworkInterface netI = enNetI.nextElement(); + for (Enumeration enumIpAddr = netI + .getInetAddresses(); enumIpAddr.hasMoreElements(); ) { + InetAddress inetAddress = enumIpAddr.nextElement(); + if (inetAddress instanceof Inet4Address && !inetAddress.isLoopbackAddress()) { + return inetAddress.getHostAddress(); + } + } + } + } catch (SocketException e) { + e.printStackTrace(); + } + return "0.0.0.0"; + } + + @Override + public void start() throws IOException { + super.start(); + Log.d("Debugger", String.format("Open http://%s:8910/index.html to start debug", getIpAddressString())); + } + + @Override + public Response serve(IHTTPSession session) { + Uri uri = Uri.parse(session.getUri()); + List segments = uri.getPathSegments(); + if (segments.size() > 1 && "api".equals(segments.get(0))) { + String cmd = segments.get(1); + APICommand apiCommand = commandMap.get(cmd); + if (apiCommand != null) { + Object ret = apiCommand.exec(session); + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, "application/json", ret.toString()); + } + } + String fileName = session.getUri().substring(1); + AssetManager assetManager = context.getAssets(); + try { + InputStream inputStream = assetManager.open("debugger/" + fileName); + String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(fileName.substring(fileName.lastIndexOf(".") + 1)); + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, mimeType, inputStream, inputStream.available()); + } catch (IOException e) { + e.printStackTrace(); + } + return NanoHTTPD.newFixedLengthResponse(Response.Status.OK, "text/html", "HelloWorld"); + } + + public interface APICommand { + String name(); + + Object exec(IHTTPSession session); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/WSClient.java b/devkit/src/main/java/pub/doric/devkit/WSClient.java new file mode 100644 index 00000000..dd81e587 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/WSClient.java @@ -0,0 +1,119 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.devkit; + +import org.greenrobot.eventbus.EventBus; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.EOFException; +import java.net.ConnectException; +import java.util.concurrent.TimeUnit; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import pub.doric.devkit.event.ConnectExceptionEvent; +import pub.doric.devkit.event.EOFExceptionEvent; +import pub.doric.devkit.event.EnterDebugEvent; +import pub.doric.devkit.event.OpenEvent; +import pub.doric.devkit.event.ReloadEvent; +import pub.doric.devkit.ui.DevPanel; + +/** + * @Description: com.github.penfeizhou.doric.dev + * @Author: pengfei.zhou + * @CreateDate: 2019-08-03 + */ +public class WSClient extends WebSocketListener { + private final WebSocket webSocket; + + public WSClient(String url) { + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + Request request = new Request.Builder().url(url).build(); + webSocket = okHttpClient.newWebSocket(request, this); + } + + public void close() { + webSocket.close(-1, "Close"); + } + + @Override + public void onOpen(WebSocket webSocket, Response response) { + super.onOpen(webSocket, response); + + DevPanel.isDevConnected = true; + EventBus.getDefault().post(new OpenEvent()); + } + + @Override + public void onMessage(WebSocket webSocket, String text) { + super.onMessage(webSocket, text); + try { + JSONObject jsonObject = new JSONObject(text); + String cmd = jsonObject.optString("cmd"); + switch (cmd) { + case "RELOAD": { + String source = jsonObject.optString("source"); + String script = jsonObject.optString("script"); + EventBus.getDefault().post(new ReloadEvent(source, script)); + } + break; + case "SWITCH_TO_DEBUG": { + EventBus.getDefault().post(new EnterDebugEvent()); + } + break; + } + + } catch (JSONException e) { + e.printStackTrace(); + } + + } + + @Override + public void onClosing(WebSocket webSocket, int code, String reason) { + super.onClosing(webSocket, code, reason); + } + + @Override + public void onClosed(WebSocket webSocket, int code, String reason) { + super.onClosed(webSocket, code, reason); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + super.onFailure(webSocket, t, response); + + if (t instanceof EOFException) { + DevPanel.isDevConnected = false; + EventBus.getDefault().post(new EOFExceptionEvent()); + } else if (t instanceof ConnectException) { + DevPanel.isDevConnected = false; + EventBus.getDefault().post(new ConnectExceptionEvent()); + } + } + + public void send(String command) { + webSocket.send(command); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/ConnectExceptionEvent.java b/devkit/src/main/java/pub/doric/devkit/event/ConnectExceptionEvent.java new file mode 100644 index 00000000..8dec1b30 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/ConnectExceptionEvent.java @@ -0,0 +1,4 @@ +package pub.doric.devkit.event; + +public class ConnectExceptionEvent { +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/EOFExceptionEvent.java b/devkit/src/main/java/pub/doric/devkit/event/EOFExceptionEvent.java new file mode 100644 index 00000000..b0c3dd3d --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/EOFExceptionEvent.java @@ -0,0 +1,4 @@ +package pub.doric.devkit.event; + +public class EOFExceptionEvent { +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/EnterDebugEvent.java b/devkit/src/main/java/pub/doric/devkit/event/EnterDebugEvent.java new file mode 100644 index 00000000..5e02b0d0 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/EnterDebugEvent.java @@ -0,0 +1,4 @@ +package pub.doric.devkit.event; + +public class EnterDebugEvent { +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/OpenEvent.java b/devkit/src/main/java/pub/doric/devkit/event/OpenEvent.java new file mode 100644 index 00000000..d878baf4 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/OpenEvent.java @@ -0,0 +1,4 @@ +package pub.doric.devkit.event; + +public class OpenEvent { +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/QuitDebugEvent.java b/devkit/src/main/java/pub/doric/devkit/event/QuitDebugEvent.java new file mode 100644 index 00000000..188f0339 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/QuitDebugEvent.java @@ -0,0 +1,4 @@ +package pub.doric.devkit.event; + +public class QuitDebugEvent { +} diff --git a/devkit/src/main/java/pub/doric/devkit/event/ReloadEvent.java b/devkit/src/main/java/pub/doric/devkit/event/ReloadEvent.java new file mode 100644 index 00000000..3f33289c --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/event/ReloadEvent.java @@ -0,0 +1,11 @@ +package pub.doric.devkit.event; + +public class ReloadEvent { + public String source; + public String script; + + public ReloadEvent(String source, String script) { + this.source = source; + this.script = script; + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/remote/DoricRemoteJSExecutor.java b/devkit/src/main/java/pub/doric/devkit/remote/DoricRemoteJSExecutor.java new file mode 100644 index 00000000..3e9c0de3 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/remote/DoricRemoteJSExecutor.java @@ -0,0 +1,63 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.devkit.remote; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSRuntimeException; +import com.github.pengfeizhou.jscore.JavaFunction; +import com.github.pengfeizhou.jscore.JavaValue; + +import pub.doric.devkit.IStatusCallback; +import pub.doric.engine.IDoricJSE; + +public class DoricRemoteJSExecutor implements IDoricJSE { + + private final RemoteJSExecutor mRemoteJSExecutor; + + public DoricRemoteJSExecutor(IStatusCallback statusCallback) { + this.mRemoteJSExecutor = new RemoteJSExecutor(statusCallback); + } + + @Override + public String loadJS(String script, String source) throws JSRuntimeException { + return mRemoteJSExecutor.loadJS(script, source); + } + + @Override + public JSDecoder evaluateJS(String script, String source, boolean hashKey) throws JSRuntimeException { + return mRemoteJSExecutor.evaluateJS(script, source, hashKey); + } + + @Override + public void injectGlobalJSFunction(String name, JavaFunction javaFunction) { + mRemoteJSExecutor.injectGlobalJSFunction(name, javaFunction); + } + + @Override + public void injectGlobalJSObject(String name, JavaValue javaValue) { + mRemoteJSExecutor.injectGlobalJSObject(name, javaValue); + } + + @Override + public JSDecoder invokeMethod(String objectName, String functionName, JavaValue[] javaValues, boolean hashKey) throws JSRuntimeException { + return mRemoteJSExecutor.invokeMethod(objectName, functionName, javaValues, hashKey); + } + + @Override + public void teardown() { + mRemoteJSExecutor.destroy(); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/remote/RemoteJSExecutor.java b/devkit/src/main/java/pub/doric/devkit/remote/RemoteJSExecutor.java new file mode 100644 index 00000000..73039511 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/remote/RemoteJSExecutor.java @@ -0,0 +1,148 @@ +package pub.doric.devkit.remote; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JavaFunction; +import com.github.pengfeizhou.jscore.JavaValue; + +import org.greenrobot.eventbus.EventBus; +import org.jetbrains.annotations.NotNull; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.EOFException; +import java.net.ConnectException; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.LockSupport; + +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.WebSocket; +import okhttp3.WebSocketListener; +import pub.doric.devkit.DevKit; +import pub.doric.devkit.IStatusCallback; +import pub.doric.devkit.event.QuitDebugEvent; + +public class RemoteJSExecutor { + private final WebSocket webSocket; + private final Map globalFunctions = new HashMap<>(); + private JSDecoder temp; + + public RemoteJSExecutor(final IStatusCallback statusCallback) { + OkHttpClient okHttpClient = new OkHttpClient + .Builder() + .readTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .build(); + final Request request = new Request.Builder().url("ws://" + DevKit.ip + ":2080").build(); + + final Thread current = Thread.currentThread(); + webSocket = okHttpClient.newWebSocket(request, new WebSocketListener() { + @Override + public void onOpen(WebSocket webSocket, Response response) { + LockSupport.unpark(current); + statusCallback.start(); + } + + @Override + public void onFailure(WebSocket webSocket, Throwable t, Response response) { + if (t instanceof ConnectException) { + // 连接remote异常 + LockSupport.unpark(current); + throw new RuntimeException("remote js executor cannot connect"); + } else if (t instanceof EOFException) { + // 被远端强制断开 + System.out.println("remote js executor eof"); + + LockSupport.unpark(current); + EventBus.getDefault().post(new QuitDebugEvent()); + } + } + + @Override + public void onMessage(@NotNull WebSocket webSocket, @NotNull String text) { + try { + JSONObject jsonObject = new JSONObject(text); + String cmd = jsonObject.optString("cmd"); + switch (cmd) { + case "injectGlobalJSFunction": { + String name = jsonObject.optString("name"); + JSONArray arguments = jsonObject.optJSONArray("arguments"); + assert arguments != null; + JSDecoder[] decoders = new JSDecoder[arguments.length()]; + for (int i = 0; i < arguments.length(); i++) { + Object o = arguments.get(i); + decoders[i] = new JSDecoder(new ValueBuilder(o).build()); + } + globalFunctions.get(name).exec(decoders); + } + + break; + case "invokeMethod": { + try { + Object result = jsonObject.opt("result"); + ValueBuilder vb = new ValueBuilder(result); + temp = new JSDecoder(vb.build()); + System.out.println(result); + } catch (Exception ex) { + ex.printStackTrace(); + } finally { + LockSupport.unpark(current); + } + } + break; + } + } catch (JSONException e) { + e.printStackTrace(); + } + } + }); + LockSupport.park(current); + } + + public String loadJS(String script, String source) { + return null; + } + + public JSDecoder evaluateJS(String script, String source, boolean hashKey) { + return null; + } + + public void injectGlobalJSFunction(String name, JavaFunction javaFunction) { + globalFunctions.put(name, javaFunction); + webSocket.send(new JSONBuilder().put("cmd", "injectGlobalJSFunction") + .put("name", name).toString() + ); + } + + public void injectGlobalJSObject(String name, JavaValue javaValue) { + } + + public JSDecoder invokeMethod(String objectName, String functionName, JavaValue[] javaValues, boolean hashKey) { + JSONArray jsonArray = new JSONArray(); + for (JavaValue javaValue : javaValues) { + jsonArray.put(new JSONBuilder() + .put("type", javaValue.getType()) + .put("value", javaValue.getValue()) + .toJSONObject()); + } + webSocket.send(new JSONBuilder() + .put("cmd", "invokeMethod") + .put("objectName", objectName) + .put("functionName", functionName) + .put("javaValues", jsonArray) + .put("hashKey", hashKey) + .toString()); + + LockSupport.park(Thread.currentThread()); + return temp; + } + + public void destroy() { + webSocket.close(1000, "destroy"); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/remote/ValueBuilder.java b/devkit/src/main/java/pub/doric/devkit/remote/ValueBuilder.java new file mode 100644 index 00000000..63ddae03 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/remote/ValueBuilder.java @@ -0,0 +1,91 @@ +package pub.doric.devkit.remote; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.util.Iterator; + +/** + * Created by pengfei.zhou on 2018/4/17. + */ +public class ValueBuilder { + private final Object val; + + private void writeBoolean(ByteArrayOutputStream output, boolean b) { + output.write((byte) (b ? 1 : 0)); + } + + private void writeInt(ByteArrayOutputStream output, int i) { + output.write((byte) (i >>> 24)); + output.write((byte) (i >>> 16)); + output.write((byte) (i >>> 8)); + output.write((byte) i); + } + + private void writeDouble(ByteArrayOutputStream output, double d) { + long l = Double.doubleToRawLongBits(d); + output.write((byte) (l >>> 56)); + output.write((byte) (l >>> 48)); + output.write((byte) (l >>> 40)); + output.write((byte) (l >>> 32)); + output.write((byte) (l >>> 24)); + output.write((byte) (l >>> 16)); + output.write((byte) (l >>> 8)); + output.write((byte) l); + } + + private void writeString(ByteArrayOutputStream output, String S) { + byte[] buf; + try { + buf = S.getBytes("UTF-8"); + } catch (Exception e) { + buf = new byte[0]; + } + int i = buf.length; + writeInt(output, i); + output.write(buf, 0, i); + } + + + private void write(ByteArrayOutputStream output, Object O) { + if (O instanceof Number) { + output.write((byte) 'D'); + writeDouble(output, Double.valueOf(String.valueOf(O))); + } else if (O instanceof String) { + output.write((byte) 'S'); + writeString(output, (String) O); + } else if (O instanceof Boolean) { + output.write((byte) 'B'); + writeBoolean(output, (Boolean) O); + } else if (O instanceof JSONObject) { + output.write((byte) 'O'); + //writeBoolean(output, (Boolean) O); + Iterator iterator = ((JSONObject) O).keys(); + while (iterator.hasNext()) { + String key = iterator.next(); + write(output, key); + write(output, ((JSONObject) O).opt(key)); + } + output.write((byte) 'N'); + } else if (O instanceof JSONArray) { + output.write((byte) 'A'); + writeInt(output, ((JSONArray) O).length()); + for (int i = 0; i < ((JSONArray) O).length(); i++) { + write(output, ((JSONArray) O).opt(i)); + } + } else { + output.write((byte) 'N'); + } + } + + public ValueBuilder(Object o) { + this.val = o; + } + + public byte[] build() { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + write(outputStream, val); + return outputStream.toByteArray(); + } +} \ No newline at end of file diff --git a/devkit/src/main/java/pub/doric/devkit/ui/DebugContextPanel.java b/devkit/src/main/java/pub/doric/devkit/ui/DebugContextPanel.java new file mode 100644 index 00000000..3e372463 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/ui/DebugContextPanel.java @@ -0,0 +1,84 @@ +package pub.doric.devkit.ui; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; + +import com.google.gson.JsonObject; + +import pub.doric.DoricContext; +import pub.doric.DoricContextManager; +import pub.doric.devkit.BuildConfig; +import pub.doric.devkit.DoricDev; +import pub.doric.devkit.IDevKit; +import pub.doric.devkit.R; + +public class DebugContextPanel extends DialogFragment { + + public DebugContextPanel() { + } + + @Nullable + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState + ) { + return inflater.inflate(R.layout.layout_debug_context, container, false); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.getWindow().requestFeature(Window.FEATURE_NO_TITLE); + + return dialog; + } + + @Override + public void onStart() { + super.onStart(); + + Dialog dialog = getDialog(); + if (dialog != null) { + dialog.getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); + } + + LinearLayout container = getView().findViewById(R.id.container); + LayoutInflater inflater = LayoutInflater.from(getContext()); + + for (final DoricContext doricContext : DoricContextManager.aliveContexts()) { + View cell = inflater.inflate(R.layout.layout_debug_context_cell, container, false); + + TextView contextIdTextView = cell.findViewById(R.id.context_id_text_view); + contextIdTextView.setText(doricContext.getContextId()); + + TextView sourceTextView = cell.findViewById(R.id.source_text_view); + sourceTextView.setText(doricContext.getSource()); + + cell.findViewById(R.id.debug_text_view).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("contextId", doricContext.getContextId()); + jsonObject.addProperty("projectHome", BuildConfig.PROJECT_HOME); + jsonObject.addProperty("source", doricContext.getSource().replace(".js", ".ts")); + DoricDev.sendDevCommand(IDevKit.Command.DEBUG, jsonObject); + dismissAllowingStateLoss(); + } + }); + + container.addView(cell); + } + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/ui/DemoDebugActivity.java b/devkit/src/main/java/pub/doric/devkit/ui/DemoDebugActivity.java new file mode 100644 index 00000000..135e4cf0 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/ui/DemoDebugActivity.java @@ -0,0 +1,113 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.devkit.ui; + +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import pub.doric.DoricContext; +import pub.doric.DoricContextManager; +import pub.doric.DoricPanel; +import pub.doric.devkit.DoricContextDebuggable; +import pub.doric.devkit.event.EnterDebugEvent; +import pub.doric.devkit.event.QuitDebugEvent; +import pub.doric.devkit.event.ReloadEvent; +import pub.doric.devkit.util.SensorManagerHelper; +import pub.doric.utils.DoricUtils; + +/** + * @Description: pub.doric.demo + * @Author: pengfei.zhou + * @CreateDate: 2019-11-19 + */ +public class DemoDebugActivity extends AppCompatActivity { + private DoricContext doricContext; + private SensorManagerHelper sensorHelper; + private DoricContextDebuggable doricContextDebuggable; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + String source = getIntent().getStringExtra("source"); + DoricPanel doricPanel = new DoricPanel(this); + addContentView(doricPanel, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + doricPanel.config(DoricUtils.readAssetFile("demo/" + source), source); + doricContext = doricPanel.getDoricContext(); + doricContextDebuggable = new DoricContextDebuggable(doricContext); + sensorHelper = new SensorManagerHelper(this); + sensorHelper.setOnShakeListener(new SensorManagerHelper.OnShakeListener() { + @Override + public void onShake() { + Fragment devPanel = getSupportFragmentManager().findFragmentByTag("DevPanel"); + if (devPanel != null && devPanel.isAdded()) { + return; + } + new DevPanel().show(getSupportFragmentManager(), "DevPanel"); + } + }); + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + EventBus.getDefault().register(this); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + sensorHelper.stop(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEnterDebugEvent(EnterDebugEvent enterDebugEvent) { + doricContextDebuggable.startDebug(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onReloadEvent(ReloadEvent reloadEvent) { + for (DoricContext context : DoricContextManager.aliveContexts()) { + if (reloadEvent.source.contains(context.getSource())) { + context.reload(reloadEvent.script); + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onQuitDebugEvent(QuitDebugEvent quitDebugEvent) { + doricContextDebuggable.stopDebug(); + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + if (KeyEvent.KEYCODE_MENU == event.getKeyCode()) { + new DevPanel().show(getSupportFragmentManager(), "DevPanel"); + } + return super.onKeyDown(keyCode, event); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/ui/DevPanel.java b/devkit/src/main/java/pub/doric/devkit/ui/DevPanel.java new file mode 100644 index 00000000..5c3c925f --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/ui/DevPanel.java @@ -0,0 +1,136 @@ +package pub.doric.devkit.ui; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.lahm.library.EasyProtectorLib; +import com.lahm.library.EmulatorCheckCallback; +import com.tbruyelle.rxpermissions2.RxPermissions; + +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import io.reactivex.disposables.Disposable; +import io.reactivex.functions.Consumer; +import pub.doric.devkit.DevKit; +import pub.doric.devkit.DoricDev; +import pub.doric.devkit.R; +import pub.doric.devkit.event.ConnectExceptionEvent; +import pub.doric.devkit.event.EOFExceptionEvent; +import pub.doric.devkit.event.OpenEvent; + +public class DevPanel extends BottomSheetDialogFragment { + + public static boolean isDevConnected = false; + + public DevPanel() { + } + + @Nullable + @Override + public View onCreateView( + @NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState + ) { + return inflater.inflate(R.layout.layout_dev, container, false); + } + + @Override + public void onStart() { + super.onStart(); + + updateUI(); + + getView().findViewById(R.id.connect_dev_kit_text_view).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (DevKit.isRunningInEmulator) { + DevKit.ip = "10.0.2.2"; + DoricDev.connectDevKit("ws://" + DevKit.ip + ":7777"); + } else { + final RxPermissions rxPermissions = new RxPermissions(DevPanel.this); + Disposable disposable = rxPermissions + .request(Manifest.permission.CAMERA) + .subscribe(new Consumer() { + @Override + public void accept(Boolean grant) throws Exception { + if (grant) { + Intent intent = new Intent(getContext(), ScanQRCodeActivity.class); + getContext().startActivity(intent); + } + } + }); + } + } + }); + + getView().findViewById(R.id.debug_text_view).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + DebugContextPanel debugContextPanel = new DebugContextPanel(); + debugContextPanel.show(getActivity().getSupportFragmentManager(), "DebugContextPanel"); + dismissAllowingStateLoss(); + } + }); + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + EventBus.getDefault().register(this); + DevKit.isRunningInEmulator = EasyProtectorLib.checkIsRunningInEmulator(context, new EmulatorCheckCallback() { + @Override + public void findEmulator(String emulatorInfo) { + System.out.println(emulatorInfo); + } + }); + } + + @Override + public void onDetach() { + super.onDetach(); + EventBus.getDefault().unregister(this); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onOpenEvent(OpenEvent openEvent) { + updateUI(); + Toast.makeText(getContext(), "dev kit connected", Toast.LENGTH_LONG).show(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onEOFEvent(EOFExceptionEvent eofExceptionEvent) { + updateUI(); + Toast.makeText(getContext(), "dev kit eof exception", Toast.LENGTH_LONG).show(); + } + + @Subscribe(threadMode = ThreadMode.MAIN) + public void onConnectExceptionEvent(ConnectExceptionEvent connectExceptionEvent) { + updateUI(); + Toast.makeText(getContext(), "dev kit connection exception", Toast.LENGTH_LONG).show(); + } + + private void updateUI() { + if (isDevConnected) { + getView().findViewById(R.id.connect_dev_kit_text_view).setVisibility(View.GONE); + getView().findViewById(R.id.debug_text_view).setVisibility(View.VISIBLE); + getView().findViewById(R.id.hot_reload_text_view).setVisibility(View.VISIBLE); + } else { + getView().findViewById(R.id.connect_dev_kit_text_view).setVisibility(View.VISIBLE); + getView().findViewById(R.id.debug_text_view).setVisibility(View.GONE); + getView().findViewById(R.id.hot_reload_text_view).setVisibility(View.GONE); + } + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/ui/ScanQRCodeActivity.java b/devkit/src/main/java/pub/doric/devkit/ui/ScanQRCodeActivity.java new file mode 100644 index 00000000..21547411 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/ui/ScanQRCodeActivity.java @@ -0,0 +1,79 @@ +package pub.doric.devkit.ui; + +import android.os.Bundle; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +import cn.bingoogolapple.qrcode.core.QRCodeView; +import cn.bingoogolapple.qrcode.zbar.ZBarView; +import pub.doric.devkit.DevKit; +import pub.doric.devkit.DoricDev; +import pub.doric.devkit.R; + +public class ScanQRCodeActivity extends AppCompatActivity implements QRCodeView.Delegate { + + private ZBarView mZbarView; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + setContentView(R.layout.layout_scan_qrcode); + mZbarView = findViewById(R.id.zbar_view); + mZbarView.setDelegate(this); + } + + @Override + protected void onStart() { + super.onStart(); + + mZbarView.startCamera(); + mZbarView.startSpotAndShowRect(); + } + + @Override + protected void onStop() { + super.onStop(); + + mZbarView.stopCamera(); + super.onStop(); + } + + @Override + protected void onDestroy() { + mZbarView.onDestroy(); + super.onDestroy(); + } + + @Override + public void onScanQRCodeSuccess(String result) { + setTitle("扫描结果为:" + result); + DevKit.ip = result; + Toast.makeText(this, "dev kit connecting to " + result, Toast.LENGTH_LONG).show(); + DoricDev.connectDevKit("ws://" + result + ":7777"); + finish(); + } + + @Override + public void onCameraAmbientBrightnessChanged(boolean isDark) { + String tipText = mZbarView.getScanBoxView().getTipText(); + String ambientBrightnessTip = "\n环境过暗,请打开闪光灯"; + if (isDark) { + if (!tipText.contains(ambientBrightnessTip)) { + mZbarView.getScanBoxView().setTipText(tipText + ambientBrightnessTip); + } + } else { + if (tipText.contains(ambientBrightnessTip)) { + tipText = tipText.substring(0, tipText.indexOf(ambientBrightnessTip)); + mZbarView.getScanBoxView().setTipText(tipText); + } + } + } + + @Override + public void onScanQRCodeOpenCameraError() { + System.out.println(); + } +} diff --git a/devkit/src/main/java/pub/doric/devkit/util/SensorManagerHelper.java b/devkit/src/main/java/pub/doric/devkit/util/SensorManagerHelper.java new file mode 100644 index 00000000..c9ae1838 --- /dev/null +++ b/devkit/src/main/java/pub/doric/devkit/util/SensorManagerHelper.java @@ -0,0 +1,113 @@ +package pub.doric.devkit.util; + +import android.content.Context; +import android.hardware.Sensor; +import android.hardware.SensorEvent; +import android.hardware.SensorEventListener; +import android.hardware.SensorManager; + +public class SensorManagerHelper implements SensorEventListener { + + // 速度阈值,当摇晃速度达到这值后产生作用 + private final int SPEED_SHRESHOLD = 5000; + // 两次检测的时间间隔 + private final int UPTATE_INTERVAL_TIME = 50; + // 传感器管理器 + private SensorManager sensorManager; + // 传感器 + private Sensor sensor; + // 重力感应监听器 + private OnShakeListener onShakeListener; + // 上下文对象context + private Context context; + // 手机上一个位置时重力感应坐标 + private float lastX; + private float lastY; + private float lastZ; + // 上次检测时间 + private long lastUpdateTime; + + public SensorManagerHelper(Context context) { + this.context = context; + start(); + } + + /** + * 开始检测 + */ + public void start() { + // 获得传感器管理器 + sensorManager = (SensorManager) context + .getSystemService(Context.SENSOR_SERVICE); + if (sensorManager != null) { + // 获得重力传感器 + sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER); + } + // 注册 + if (sensor != null) { + sensorManager.registerListener(this, sensor, + SensorManager.SENSOR_DELAY_GAME); + } + } + + /** + * 停止检测 + */ + public void stop() { + sensorManager.unregisterListener(this); + } + + /** + * 摇晃监听接口 + */ + public interface OnShakeListener { + void onShake(); + } + + /** + * 设置重力感应监听器 + */ + public void setOnShakeListener(OnShakeListener listener) { + onShakeListener = listener; + } + + @Override + public void onAccuracyChanged(Sensor sensor, int accuracy) { + } + + /** + * 重力感应器感应获得变化数据 + * android.hardware.SensorEventListener#onSensorChanged(android.hardware + * .SensorEvent) + */ + @Override + public void onSensorChanged(SensorEvent event) { + // 现在检测时间 + long currentUpdateTime = System.currentTimeMillis(); + // 两次检测的时间间隔 + long timeInterval = currentUpdateTime - lastUpdateTime; + // 判断是否达到了检测时间间隔 + if (timeInterval < UPTATE_INTERVAL_TIME) return; + // 现在的时间变成last时间 + lastUpdateTime = currentUpdateTime; + // 获得x,y,z坐标 + float x = event.values[0]; + float y = event.values[1]; + float z = event.values[2]; + // 获得x,y,z的变化值 + float deltaX = x - lastX; + float deltaY = y - lastY; + float deltaZ = z - lastZ; + // 将现在的坐标变成last坐标 + lastX = x; + lastY = y; + lastZ = z; + double speed = Math.sqrt(deltaX * deltaX + deltaY * deltaY + deltaZ + * deltaZ) + / timeInterval * 10000; + // 达到速度阀值,发出提示 + if (speed >= SPEED_SHRESHOLD) { + onShakeListener.onShake(); + } + } +} diff --git a/devkit/src/main/res/layout/layout_debug_context.xml b/devkit/src/main/res/layout/layout_debug_context.xml new file mode 100644 index 00000000..a140656b --- /dev/null +++ b/devkit/src/main/res/layout/layout_debug_context.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/devkit/src/main/res/layout/layout_debug_context_cell.xml b/devkit/src/main/res/layout/layout_debug_context_cell.xml new file mode 100644 index 00000000..05997f56 --- /dev/null +++ b/devkit/src/main/res/layout/layout_debug_context_cell.xml @@ -0,0 +1,38 @@ + + + + + + + + + diff --git a/devkit/src/main/res/layout/layout_dev.xml b/devkit/src/main/res/layout/layout_dev.xml new file mode 100644 index 00000000..2b9e6f63 --- /dev/null +++ b/devkit/src/main/res/layout/layout_dev.xml @@ -0,0 +1,34 @@ + + + + + + + + + \ No newline at end of file diff --git a/devkit/src/main/res/layout/layout_scan_qrcode.xml b/devkit/src/main/res/layout/layout_scan_qrcode.xml new file mode 100644 index 00000000..8bf17883 --- /dev/null +++ b/devkit/src/main/res/layout/layout_scan_qrcode.xml @@ -0,0 +1,22 @@ + + + + + diff --git a/devkit/src/main/res/values/strings.xml b/devkit/src/main/res/values/strings.xml new file mode 100644 index 00000000..5b9a1b96 --- /dev/null +++ b/devkit/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + devkit + diff --git a/doric/.gitignore b/doric/.gitignore new file mode 100644 index 00000000..796b96d1 --- /dev/null +++ b/doric/.gitignore @@ -0,0 +1 @@ +/build diff --git a/doric/build.gradle b/doric/build.gradle new file mode 100644 index 00000000..1b925089 --- /dev/null +++ b/doric/build.gradle @@ -0,0 +1,59 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion 29 + buildToolsVersion '29.0.2' + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + consumerProguardFiles 'proguard-rules.pro' + } + } +} + +afterEvaluate { + buildJSBundle.exec() + buildDemo.exec() + //buildDebugger.exec() +} + +task buildJSBundle(type: Exec) { + workingDir project.rootDir.getParent() + "/js-framework" + commandLine 'npm', 'run', 'build' +} + +task buildDemo(type: Exec) { + workingDir project.rootDir.getParent() + "/demo" + commandLine 'npm', 'run', 'build' +} + +task buildDebugger(type: Exec) { + workingDir project.rootDir.getParent() + "/debugger" + commandLine 'npm', 'run', 'build' +} +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + + implementation 'androidx.appcompat:appcompat:1.1.0' + api 'com.github.pengfeizhou:jsc4a:0.1.0' + implementation 'com.squareup.okhttp3:okhttp:4.2.2' + implementation 'com.github.penfeizhou.android.animation:glide-plugin:1.3.1' + implementation 'com.google.code.gson:gson:2.8.6' + implementation "com.google.android.material:material:1.0.0" + + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test.ext:junit:1.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' +} diff --git a/doric/proguard-rules.pro b/doric/proguard-rules.pro new file mode 100644 index 00000000..20aedfa0 --- /dev/null +++ b/doric/proguard-rules.pro @@ -0,0 +1,31 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile +-keep class com.github.penfeizhou.doric.extension.bridge.DoricPlugin +-keep class com.github.penfeizhou.doric.extension.bridge.DoricMethod + +-keep @com.github.penfeizhou.doric.extension.bridge.DoricPlugin class * {*;} + +-keepclasseswithmembers @com.github.penfeizhou.doric.extension.bridge.DoricPlugin class * {*;} + +-keep class * { +@com.github.penfeizhou.doric.extension.bridge.DoricMethod ; +} \ No newline at end of file diff --git a/doric/src/main/AndroidManifest.xml b/doric/src/main/AndroidManifest.xml new file mode 100644 index 00000000..44741555 --- /dev/null +++ b/doric/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + diff --git a/doric/src/main/assets/bundle b/doric/src/main/assets/bundle new file mode 120000 index 00000000..bc33c95e --- /dev/null +++ b/doric/src/main/assets/bundle @@ -0,0 +1 @@ +../../../../../js-framework/bundle \ No newline at end of file diff --git a/doric/src/main/assets/debugger b/doric/src/main/assets/debugger new file mode 120000 index 00000000..f3754c9a --- /dev/null +++ b/doric/src/main/assets/debugger @@ -0,0 +1 @@ +../../../../../debugger/dist \ No newline at end of file diff --git a/doric/src/main/java/pub/doric/Doric.java b/doric/src/main/java/pub/doric/Doric.java new file mode 100644 index 00000000..5ecdfec7 --- /dev/null +++ b/doric/src/main/java/pub/doric/Doric.java @@ -0,0 +1,35 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.app.Application; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class Doric { + private static Application sApplication; + + public static void init(Application application) { + sApplication = application; + } + + public static Application application() { + return sApplication; + } +} diff --git a/doric/src/main/java/pub/doric/DoricActivity.java b/doric/src/main/java/pub/doric/DoricActivity.java new file mode 100644 index 00000000..ad225b6d --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricActivity.java @@ -0,0 +1,53 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.os.Bundle; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; + +/** + * @Description: pub.doric.demo + * @Author: pengfei.zhou + * @CreateDate: 2019-11-19 + */ +public class DoricActivity extends AppCompatActivity { + private DoricFragment doricFragment; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.doric_activity); + if (savedInstanceState == null) { + String scheme = getIntent().getStringExtra("scheme"); + String alias = getIntent().getStringExtra("alias"); + doricFragment = DoricFragment.newInstance(scheme, alias); + getSupportFragmentManager().beginTransaction() + .add(R.id.container, doricFragment) + .commit(); + } + } + + @Override + public void onBackPressed() { + if (doricFragment.canPop()) { + doricFragment.pop(); + } else { + super.onBackPressed(); + } + } +} diff --git a/doric/src/main/java/pub/doric/DoricComponent.java b/doric/src/main/java/pub/doric/DoricComponent.java new file mode 100644 index 00000000..4a9d327b --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricComponent.java @@ -0,0 +1,34 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DoricComponent { + String name() default ""; +} diff --git a/doric/src/main/java/pub/doric/DoricContext.java b/doric/src/main/java/pub/doric/DoricContext.java new file mode 100644 index 00000000..a324769c --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricContext.java @@ -0,0 +1,209 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.content.Context; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; + +import org.json.JSONObject; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import pub.doric.async.AsyncResult; +import pub.doric.navbar.IDoricNavBar; +import pub.doric.navigator.IDoricNavigator; +import pub.doric.plugin.DoricJavaPlugin; +import pub.doric.shader.RootNode; +import pub.doric.shader.ViewNode; +import pub.doric.utils.DoricConstant; +import pub.doric.utils.DoricMetaInfo; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricContext { + private final String mContextId; + private final Map mPluginMap = new HashMap<>(); + private final Context mContext; + private RootNode mRootNode = new RootNode(this); + private final String source; + private String script; + private JSONObject initParams; + private IDoricDriver doricDriver; + private final Map mHeadNodes = new HashMap<>(); + + public Collection allHeadNodes() { + return mHeadNodes.values(); + } + + public void addHeadNode(ViewNode viewNode) { + mHeadNodes.put(viewNode.getId(), viewNode); + } + + public void removeHeadNode(ViewNode viewNode) { + mHeadNodes.remove(viewNode.getId()); + } + + public ViewNode targetViewNode(String id) { + if (id.equals(mRootNode.getId())) { + return mRootNode; + } + return mHeadNodes.get(id); + } + + protected DoricContext(Context context, String contextId, String source) { + this.mContext = context; + this.mContextId = contextId; + this.source = source; + } + + public String getSource() { + return source; + } + + public String getScript() { + return script; + } + + public static DoricContext create(Context context, String script, String source) { + DoricContext doricContext = DoricContextManager.getInstance().createContext(context, script, source); + doricContext.script = script; + return doricContext; + } + + public void init(float width, float height) { + this.initParams = new JSONBuilder() + .put("width", width) + .put("height", height).toJSONObject(); + callEntity(DoricConstant.DORIC_ENTITY_INIT, this.initParams); + callEntity(DoricConstant.DORIC_ENTITY_CREATE); + } + + public void reInit() { + callEntity(DoricConstant.DORIC_ENTITY_INIT, this.initParams); + callEntity(DoricConstant.DORIC_ENTITY_CREATE); + } + + public AsyncResult callEntity(String methodName, Object... args) { + return getDriver().invokeContextEntityMethod(mContextId, methodName, args); + } + + public IDoricDriver getDriver() { + if (doricDriver == null) { + doricDriver = DoricNativeDriver.getInstance(); + } + return doricDriver; + } + + public void setDriver(IDoricDriver doricDriver) { + this.doricDriver = doricDriver; + } + + public RootNode getRootNode() { + return mRootNode; + } + + public Context getContext() { + return mContext; + } + + public String getContextId() { + return mContextId; + } + + public void teardown() { + callEntity(DoricConstant.DORIC_ENTITY_DESTROY); + DoricContextManager.getInstance().destroyContext(this).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Boolean result) { + + } + + @Override + public void onError(Throwable t) { + + } + + @Override + public void onFinish() { + mPluginMap.clear(); + } + }); + } + + public DoricJavaPlugin obtainPlugin(DoricMetaInfo doricMetaInfo) { + DoricJavaPlugin plugin = mPluginMap.get(doricMetaInfo.getName()); + if (plugin == null) { + plugin = doricMetaInfo.createInstance(this); + mPluginMap.put(doricMetaInfo.getName(), plugin); + } + return plugin; + } + + public void reload(String script) { + this.script = script; + this.mRootNode.setId(""); + getDriver().createContext(mContextId, script, source); + callEntity(DoricConstant.DORIC_ENTITY_INIT, this.initParams); + } + + public void onShow() { + callEntity(DoricConstant.DORIC_ENTITY_SHOW); + } + + public void onHidden() { + callEntity(DoricConstant.DORIC_ENTITY_HIDDEN); + } + + private IDoricNavigator doricNavigator; + + public void setDoricNavigator(IDoricNavigator doricNavigator) { + this.doricNavigator = doricNavigator; + } + + public IDoricNavigator getDoricNavigator() { + return this.doricNavigator; + } + + private IDoricNavBar doricNavBar; + + public void setDoricNavBar(IDoricNavBar navBar) { + this.doricNavBar = navBar; + } + + public IDoricNavBar getDoricNavBar() { + return this.doricNavBar; + } + + private AnimatorSet animatorSet; + + public void setAnimatorSet(AnimatorSet animatorSet) { + this.animatorSet = animatorSet; + } + + public AnimatorSet getAnimatorSet() { + return this.animatorSet; + } + +} diff --git a/doric/src/main/java/pub/doric/DoricContextManager.java b/doric/src/main/java/pub/doric/DoricContextManager.java new file mode 100644 index 00000000..3176d4b8 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricContextManager.java @@ -0,0 +1,91 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.content.Context; + +import pub.doric.async.AsyncResult; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @Description: com.github.penfeizhou.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-19 + */ +public class DoricContextManager { + + private final AtomicInteger counter = new AtomicInteger(); + private final Map doricContextMap = new ConcurrentHashMap<>(); + + + private static class Inner { + private static final DoricContextManager sInstance = new DoricContextManager(); + } + + private DoricContextManager() { + + } + + public static DoricContextManager getInstance() { + return Inner.sInstance; + } + + DoricContext createContext(Context context, final String script, final String source) { + final String contextId = String.valueOf(counter.incrementAndGet()); + final DoricContext doricContext = new DoricContext(context, contextId, source); + doricContextMap.put(contextId, doricContext); + doricContext.getDriver().createContext(contextId, script, source); + return doricContext; + } + + AsyncResult destroyContext(final DoricContext context) { + final AsyncResult result = new AsyncResult<>(); + context.getDriver().destroyContext(context.getContextId()).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Boolean b) { + result.setResult(b); + } + + @Override + public void onError(Throwable t) { + result.setError(t); + } + + @Override + public void onFinish() { + doricContextMap.remove(context.getContextId()); + } + }); + return result; + } + + public static DoricContext getContext(String contextId) { + return getInstance().doricContextMap.get(contextId); + } + + public static Set getKeySet() { + return getInstance().doricContextMap.keySet(); + } + + public static Collection aliveContexts() { + return getInstance().doricContextMap.values(); + } +} diff --git a/doric/src/main/java/pub/doric/DoricFragment.java b/doric/src/main/java/pub/doric/DoricFragment.java new file mode 100644 index 00000000..e64117b3 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricFragment.java @@ -0,0 +1,84 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import pub.doric.navigator.IDoricNavigator; + +/** + * @Description: pub.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public class DoricFragment extends Fragment implements IDoricNavigator { + + public static DoricFragment newInstance(String scheme, String alias) { + Bundle args = new Bundle(); + args.putString("scheme", scheme); + args.putString("alias", alias); + DoricFragment fragment = new DoricFragment(); + fragment.setArguments(args); + return fragment; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.doric_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + Bundle argument = getArguments(); + if (argument != null) { + String alias = argument.getString("alias"); + String scheme = argument.getString("scheme"); + push(scheme, alias); + } + } + + @Override + public void push(String scheme, String alias) { + getChildFragmentManager().beginTransaction() + .add(R.id.root, DoricPanelFragment.newInstance(scheme, alias)) + .addToBackStack(scheme) + .commit(); + } + + @Override + public void pop() { + if (canPop()) { + getChildFragmentManager().popBackStack(); + } else { + if (getActivity() != null) { + getActivity().finish(); + } + } + } + + public boolean canPop() { + return getChildFragmentManager().getBackStackEntryCount() > 1; + } +} diff --git a/doric/src/main/java/pub/doric/DoricLibrary.java b/doric/src/main/java/pub/doric/DoricLibrary.java new file mode 100644 index 00000000..6fc18c00 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricLibrary.java @@ -0,0 +1,25 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +/** + * @Description: com.github.penfeizhou.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public abstract class DoricLibrary { + public abstract void load(DoricRegistry registry); +} diff --git a/doric/src/main/java/pub/doric/DoricNativeDriver.java b/doric/src/main/java/pub/doric/DoricNativeDriver.java new file mode 100644 index 00000000..7fb4d328 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricNativeDriver.java @@ -0,0 +1,135 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.os.Handler; +import android.os.Looper; + +import com.github.pengfeizhou.jscore.JSDecoder; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import pub.doric.async.AsyncCall; +import pub.doric.async.AsyncResult; +import pub.doric.engine.DoricJSEngine; +import pub.doric.utils.DoricConstant; +import pub.doric.utils.DoricLog; +import pub.doric.utils.ThreadMode; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricNativeDriver implements IDoricDriver { + private final DoricJSEngine doricJSEngine; + private final ExecutorService mBridgeExecutor; + private final Handler mUIHandler; + private final Handler mJSHandler; + + private static class Inner { + private static final DoricNativeDriver sInstance = new DoricNativeDriver(); + } + + private DoricNativeDriver() { + doricJSEngine = new DoricJSEngine(); + mBridgeExecutor = Executors.newCachedThreadPool(); + mUIHandler = new Handler(Looper.getMainLooper()); + mJSHandler = doricJSEngine.getJSHandler(); + } + + public static DoricNativeDriver getInstance() { + return Inner.sInstance; + } + + @Override + public AsyncResult invokeContextEntityMethod(final String contextId, final String method, final Object... args) { + final Object[] nArgs = new Object[args.length + 2]; + nArgs[0] = contextId; + nArgs[1] = method; + if (args.length > 0) { + System.arraycopy(args, 0, nArgs, 2, args.length); + } + return invokeDoricMethod(DoricConstant.DORIC_CONTEXT_INVOKE, nArgs); + } + + @Override + public AsyncResult invokeDoricMethod(final String method, final Object... args) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public JSDecoder call() { + try { + return doricJSEngine.invokeDoricMethod(method, args); + } catch (Exception e) { + DoricLog.e("invokeDoricMethod(%s,...),error is %s", method, e.getLocalizedMessage()); + return new JSDecoder(null); + } + } + }); + } + + @Override + public AsyncResult asyncCall(Callable callable, ThreadMode threadMode) { + switch (threadMode) { + case JS: + return AsyncCall.ensureRunInHandler(mJSHandler, callable); + case UI: + return AsyncCall.ensureRunInHandler(mUIHandler, callable); + case INDEPENDENT: + default: + return AsyncCall.ensureRunInExecutor(mBridgeExecutor, callable); + } + } + + @Override + public AsyncResult createContext(final String contextId, final String script, final String source) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public Boolean call() { + try { + doricJSEngine.prepareContext(contextId, script, source); + return true; + } catch (Exception e) { + DoricLog.e("createContext %s error is %s", source, e.getLocalizedMessage()); + return false; + } + } + }); + } + + @Override + public AsyncResult destroyContext(final String contextId) { + return AsyncCall.ensureRunInHandler(mJSHandler, new Callable() { + @Override + public Boolean call() { + try { + doricJSEngine.destroyContext(contextId); + return true; + } catch (Exception e) { + DoricLog.e("destroyContext %s error is %s", contextId, e.getLocalizedMessage()); + return false; + } + } + }); + } + + @Override + public DoricRegistry getRegistry() { + return doricJSEngine.getRegistry(); + } +} diff --git a/doric/src/main/java/pub/doric/DoricPanel.java b/doric/src/main/java/pub/doric/DoricPanel.java new file mode 100644 index 00000000..a9e578c5 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricPanel.java @@ -0,0 +1,113 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; + +import android.util.AttributeSet; +import android.widget.FrameLayout; + +import pub.doric.utils.DoricUtils; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricPanel extends FrameLayout implements LifecycleObserver { + + private DoricContext mDoricContext; + + public DoricPanel(@NonNull Context context) { + this(context, null); + } + + public DoricPanel(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public DoricPanel(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + if (getContext() instanceof LifecycleOwner) { + ((LifecycleOwner) getContext()).getLifecycle().addObserver(this); + } + } + + + public void config(String script, String alias) { + DoricContext doricContext = DoricContext.create(getContext(), script, alias); + config(doricContext); + } + + public void config(DoricContext doricContext) { + mDoricContext = doricContext; + mDoricContext.getRootNode().setRootView(this); + if (getMeasuredState() != 0) { + mDoricContext.init(DoricUtils.px2dp(getMeasuredWidth()), DoricUtils.px2dp(getMeasuredHeight())); + } + if (getContext() instanceof LifecycleOwner + && ((LifecycleOwner) getContext()).getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) { + mDoricContext.onShow(); + } + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + } + + public DoricContext getDoricContext() { + return mDoricContext; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (oldw != w || oldh != h) { + if (mDoricContext != null) { + mDoricContext.init(DoricUtils.px2dp(w), DoricUtils.px2dp(h)); + } + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_RESUME) + public void onActivityResume() { + if (mDoricContext != null) { + mDoricContext.onShow(); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) + public void onActivityPause() { + if (mDoricContext != null) { + mDoricContext.onHidden(); + } + } + + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + public void onActivityDestroy() { + if (mDoricContext != null) { + mDoricContext.teardown(); + } + } +} diff --git a/doric/src/main/java/pub/doric/DoricPanelFragment.java b/doric/src/main/java/pub/doric/DoricPanelFragment.java new file mode 100644 index 00000000..12653e8d --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricPanelFragment.java @@ -0,0 +1,92 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import pub.doric.async.AsyncResult; +import pub.doric.loader.DoricJSLoaderManager; +import pub.doric.navbar.BaseDoricNavBar; +import pub.doric.navigator.IDoricNavigator; +import pub.doric.utils.DoricLog; + +/** + * @Description: pub.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public class DoricPanelFragment extends Fragment { + private DoricPanel doricPanel; + private BaseDoricNavBar navBar; + + public static DoricPanelFragment newInstance(String scheme, String alias) { + Bundle args = new Bundle(); + args.putString("scheme", scheme); + args.putString("alias", alias); + DoricPanelFragment fragment = new DoricPanelFragment(); + fragment.setArguments(args); + return fragment; + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.doric_framgent_panel, container, false); + } + + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + doricPanel = view.findViewById(R.id.doric_panel); + navBar = view.findViewById(R.id.doric_nav_bar); + Bundle argument = getArguments(); + if (argument == null) { + DoricLog.e("DoricPanelFragment argument is null"); + return; + } + final String alias = argument.getString("alias"); + String scheme = argument.getString("scheme"); + DoricJSLoaderManager.getInstance().loadJSBundle(scheme).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(String result) { + doricPanel.config(result, alias); + DoricContext context = doricPanel.getDoricContext(); + Fragment fragment = getParentFragment(); + if (fragment instanceof IDoricNavigator) { + context.setDoricNavigator((IDoricNavigator) fragment); + } + context.setDoricNavBar(navBar); + } + + @Override + public void onError(Throwable t) { + DoricLog.e("DoricPanelFragment load JS error:" + t.getLocalizedMessage()); + } + + @Override + public void onFinish() { + + } + }); + } +} diff --git a/doric/src/main/java/pub/doric/DoricRegistry.java b/doric/src/main/java/pub/doric/DoricRegistry.java new file mode 100644 index 00000000..75f087e5 --- /dev/null +++ b/doric/src/main/java/pub/doric/DoricRegistry.java @@ -0,0 +1,149 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + +import android.text.TextUtils; + +import pub.doric.loader.DoricAssetJSLoader; +import pub.doric.loader.DoricHttpJSLoader; +import pub.doric.loader.IDoricJSLoader; +import pub.doric.plugin.AnimatePlugin; +import pub.doric.plugin.NavBarPlugin; +import pub.doric.plugin.NavigatorPlugin; +import pub.doric.plugin.NetworkPlugin; +import pub.doric.plugin.PopoverPlugin; +import pub.doric.plugin.ShaderPlugin; +import pub.doric.plugin.StoragePlugin; +import pub.doric.refresh.RefreshableNode; +import pub.doric.shader.HLayoutNode; +import pub.doric.shader.ImageNode; +import pub.doric.shader.ScrollerNode; +import pub.doric.shader.flowlayout.FlowLayoutItemNode; +import pub.doric.shader.flowlayout.FlowLayoutNode; +import pub.doric.shader.list.ListItemNode; +import pub.doric.shader.list.ListNode; +import pub.doric.shader.RootNode; +import pub.doric.shader.StackNode; +import pub.doric.shader.TextNode; +import pub.doric.shader.VLayoutNode; +import pub.doric.shader.ViewNode; +import pub.doric.shader.slider.SlideItemNode; +import pub.doric.shader.slider.SliderNode; +import pub.doric.utils.DoricMetaInfo; +import pub.doric.plugin.DoricJavaPlugin; +import pub.doric.plugin.ModalPlugin; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Description: pub.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public class DoricRegistry { + private static Map bundles = new ConcurrentHashMap<>(); + private static Set doricLibraries = new HashSet<>(); + private static Set jsLoaders = new HashSet<>(); + + static { + addJSLoader(new DoricAssetJSLoader()); + addJSLoader(new DoricHttpJSLoader()); + } + + private Map> pluginInfoMap = new HashMap<>(); + private Map> nodeInfoMap = new HashMap<>(); + + private static void initRegistry(DoricRegistry doricRegistry) { + for (DoricLibrary library : doricLibraries) { + library.load(doricRegistry); + } + } + + + public static void register(DoricLibrary doricLibrary) { + doricLibraries.add(doricLibrary); + } + + public DoricRegistry() { + this.registerNativePlugin(ShaderPlugin.class); + this.registerNativePlugin(ModalPlugin.class); + this.registerNativePlugin(NetworkPlugin.class); + this.registerNativePlugin(StoragePlugin.class); + this.registerNativePlugin(NavigatorPlugin.class); + this.registerNativePlugin(NavBarPlugin.class); + this.registerNativePlugin(PopoverPlugin.class); + this.registerNativePlugin(AnimatePlugin.class); + + this.registerViewNode(RootNode.class); + this.registerViewNode(TextNode.class); + this.registerViewNode(ImageNode.class); + this.registerViewNode(StackNode.class); + this.registerViewNode(VLayoutNode.class); + this.registerViewNode(HLayoutNode.class); + this.registerViewNode(ListNode.class); + this.registerViewNode(ListItemNode.class); + this.registerViewNode(ScrollerNode.class); + this.registerViewNode(SliderNode.class); + this.registerViewNode(SlideItemNode.class); + this.registerViewNode(RefreshableNode.class); + this.registerViewNode(FlowLayoutNode.class); + this.registerViewNode(FlowLayoutItemNode.class); + initRegistry(this); + } + + public void registerJSBundle(String name, String bundle) { + bundles.put(name, bundle); + } + + public void registerNativePlugin(Class pluginClass) { + DoricMetaInfo doricMetaInfo = new DoricMetaInfo<>(pluginClass); + if (!TextUtils.isEmpty(doricMetaInfo.getName())) { + pluginInfoMap.put(doricMetaInfo.getName(), doricMetaInfo); + } + } + + public DoricMetaInfo acquirePluginInfo(String name) { + return pluginInfoMap.get(name); + } + + public void registerViewNode(Class pluginClass) { + DoricMetaInfo doricMetaInfo = new DoricMetaInfo<>(pluginClass); + if (!TextUtils.isEmpty(doricMetaInfo.getName())) { + nodeInfoMap.put(doricMetaInfo.getName(), doricMetaInfo); + } + } + + public DoricMetaInfo acquireViewNodeInfo(String name) { + return nodeInfoMap.get(name); + } + + public String acquireJSBundle(String name) { + return bundles.get(name); + } + + public static void addJSLoader(IDoricJSLoader jsLoader) { + jsLoaders.add(jsLoader); + } + + public static Collection getJSLoaders() { + return jsLoaders; + } +} diff --git a/doric/src/main/java/pub/doric/IDoricDriver.java b/doric/src/main/java/pub/doric/IDoricDriver.java new file mode 100644 index 00000000..afaf8625 --- /dev/null +++ b/doric/src/main/java/pub/doric/IDoricDriver.java @@ -0,0 +1,44 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric; + + +import com.github.pengfeizhou.jscore.JSDecoder; + +import java.util.concurrent.Callable; + +import pub.doric.async.AsyncResult; +import pub.doric.utils.ThreadMode; + +/** + * @Description: com.github.penfeizhou.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-19 + */ +public interface IDoricDriver { + + AsyncResult invokeContextEntityMethod(final String contextId, final String method, final Object... args); + + AsyncResult invokeDoricMethod(final String method, final Object... args); + + AsyncResult asyncCall(Callable callable, ThreadMode threadMode); + + AsyncResult createContext(final String contextId, final String script, final String source); + + AsyncResult destroyContext(final String contextId); + + DoricRegistry getRegistry(); +} diff --git a/doric/src/main/java/pub/doric/async/AsyncCall.java b/doric/src/main/java/pub/doric/async/AsyncCall.java new file mode 100644 index 00000000..90e1f7c6 --- /dev/null +++ b/doric/src/main/java/pub/doric/async/AsyncCall.java @@ -0,0 +1,70 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.async; + +import android.os.Handler; +import android.os.Looper; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; + +/** + * @Description: com.github.penfeizhou.doric.async + * @Author: pengfei.zhou + * @CreateDate: 2019-07-19 + */ +public class AsyncCall { + + public static AsyncResult ensureRunInHandler(Handler handler, final Callable callable) { + final AsyncResult asyncResult = new AsyncResult<>(); + if (Looper.myLooper() == handler.getLooper()) { + try { + asyncResult.setResult(callable.call()); + } catch (Exception e) { + e.printStackTrace(); + asyncResult.setError(e); + } + } else { + handler.post(new Runnable() { + @Override + public void run() { + try { + asyncResult.setResult(callable.call()); + } catch (Exception e) { + e.printStackTrace(); + asyncResult.setError(e); + } + } + }); + } + return asyncResult; + } + + public static AsyncResult ensureRunInExecutor(ExecutorService executorService, final Callable callable) { + final AsyncResult asyncResult = new AsyncResult<>(); + executorService.execute(new Runnable() { + @Override + public void run() { + try { + asyncResult.setResult(callable.call()); + } catch (Exception e) { + asyncResult.setError(e); + } + } + }); + return asyncResult; + } +} diff --git a/doric/src/main/java/pub/doric/async/AsyncResult.java b/doric/src/main/java/pub/doric/async/AsyncResult.java new file mode 100644 index 00000000..2f6af37a --- /dev/null +++ b/doric/src/main/java/pub/doric/async/AsyncResult.java @@ -0,0 +1,100 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.async; + +/** + * @Description: com.github.penfeizhou.doric.async + * @Author: pengfei.zhou + * @CreateDate: 2019-07-19 + */ +public class AsyncResult { + private static Object EMPTY = new Object(); + private Object result = EMPTY; + + private Callback callback = null; + + public AsyncResult() { + } + + public AsyncResult(R r) { + this.result = r; + } + + public void setResult(R result) { + this.result = result; + if (this.callback != null) { + this.callback.onResult(result); + this.callback.onFinish(); + } + } + + public void setError(Throwable result) { + this.result = result; + if (this.callback != null) { + this.callback.onError(result); + this.callback.onFinish(); + } + } + + public boolean hasResult() { + return result != EMPTY; + } + + public R getResult() { + return (R) result; + } + + public void setCallback(Callback callback) { + this.callback = callback; + if (result instanceof Throwable) { + this.callback.onError((Throwable) result); + this.callback.onFinish(); + } else if (result != EMPTY) { + this.callback.onResult((R) result); + this.callback.onFinish(); + } + } + + public SettableFuture synchronous() { + final SettableFuture settableFuture = new SettableFuture<>(); + + setCallback(new Callback() { + @Override + public void onResult(R result) { + settableFuture.set(result); + } + + @Override + public void onError(Throwable t) { + settableFuture.set(null); + } + + @Override + public void onFinish() { + + } + }); + return settableFuture; + } + + public interface Callback { + void onResult(R result); + + void onError(Throwable t); + + void onFinish(); + } +} diff --git a/doric/src/main/java/pub/doric/async/SettableFuture.java b/doric/src/main/java/pub/doric/async/SettableFuture.java new file mode 100644 index 00000000..1764eb8e --- /dev/null +++ b/doric/src/main/java/pub/doric/async/SettableFuture.java @@ -0,0 +1,82 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.async; + +/** + * @Description: com.github.penfeizhou.doric.async + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * A super simple Future-like class that can safely notify another Thread when a value is ready. + * Does not support setting errors or canceling. + */ +public class SettableFuture { + + private final CountDownLatch mReadyLatch = new CountDownLatch(1); + private volatile + T mResult; + + /** + * Sets the result. If another thread has called {@link #get}, they will immediately receive the + * value. Must only be called once. + */ + public void set(T result) { + if (mReadyLatch.getCount() == 0) { + throw new RuntimeException("Result has already been set!"); + } + mResult = result; + mReadyLatch.countDown(); + } + + /** + * Wait up to the timeout time for another Thread to set a value on this future. If a value has + * already been set, this method will return immediately. + *

+ * NB: For simplicity, we catch and wrap InterruptedException. Do NOT use this class if you + * are in the 1% of cases where you actually want to handle that. + */ + public T get(long timeoutMS) { + try { + if (!mReadyLatch.await(timeoutMS, TimeUnit.MILLISECONDS)) { + throw new TimeoutException(); + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return mResult; + } + + public T get() { + try { + mReadyLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + return mResult; + } + + public static class TimeoutException extends RuntimeException { + + public TimeoutException() { + super("Timed out waiting for future"); + } + } +} diff --git a/doric/src/main/java/pub/doric/engine/DoricJSEngine.java b/doric/src/main/java/pub/doric/engine/DoricJSEngine.java new file mode 100644 index 00000000..a4bf460f --- /dev/null +++ b/doric/src/main/java/pub/doric/engine/DoricJSEngine.java @@ -0,0 +1,228 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.engine; + +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JavaFunction; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.util.ArrayList; + +import pub.doric.DoricRegistry; +import pub.doric.extension.bridge.DoricBridgeExtension; +import pub.doric.extension.timer.DoricTimerExtension; +import pub.doric.utils.DoricConstant; +import pub.doric.utils.DoricLog; +import pub.doric.utils.DoricUtils; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricJSEngine implements Handler.Callback, DoricTimerExtension.TimerCallback { + + private HandlerThread handlerThread; + private final Handler mJSHandler; + private final DoricBridgeExtension mDoricBridgeExtension = new DoricBridgeExtension(); + protected IDoricJSE mDoricJSE; + private final DoricTimerExtension mTimerExtension; + private final DoricRegistry mDoricRegistry = new DoricRegistry(); + + public DoricJSEngine() { + handlerThread = new HandlerThread(this.getClass().getSimpleName()); + handlerThread.start(); + Looper looper = handlerThread.getLooper(); + mJSHandler = new Handler(looper, this); + mJSHandler.post(new Runnable() { + @Override + public void run() { + initJSEngine(); + injectGlobal(); + initDoricRuntime(); + } + }); + mTimerExtension = new DoricTimerExtension(looper, this); + } + + public Handler getJSHandler() { + return mJSHandler; + } + + protected void initJSEngine() { + mDoricJSE = new DoricNativeJSExecutor(); + } + + private void injectGlobal() { + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_LOG, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + try { + String type = args[0].string(); + String message = args[1].string(); + switch (type) { + case "w": + DoricLog.suffix_w("_js", message); + break; + case "e": + DoricLog.suffix_e("_js", message); + break; + default: + DoricLog.suffix_d("_js", message); + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }); + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_EMPTY, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + return null; + } + }); + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_REQUIRE, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + try { + String name = args[0].string(); + String content = mDoricRegistry.acquireJSBundle(name); + if (TextUtils.isEmpty(content)) { + DoricLog.e("require js bundle:%s is empty", name); + return new JavaValue(false); + } + mDoricJSE.loadJS(packageModuleScript(name, content), "Module://" + name); + return new JavaValue(true); + } catch (Exception e) { + e.printStackTrace(); + return new JavaValue(false); + } + } + }); + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_TIMER_SET, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + try { + mTimerExtension.setTimer( + args[0].number().longValue(), + args[1].number().longValue(), + args[2].bool()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }); + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_TIMER_CLEAR, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + try { + mTimerExtension.clearTimer(args[0].number().longValue()); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }); + mDoricJSE.injectGlobalJSFunction(DoricConstant.INJECT_BRIDGE, new JavaFunction() { + @Override + public JavaValue exec(JSDecoder[] args) { + try { + String contextId = args[0].string(); + String module = args[1].string(); + String method = args[2].string(); + String callbackId = args[3].string(); + JSDecoder jsDecoder = args[4]; + return mDoricBridgeExtension.callNative(contextId, module, method, callbackId, jsDecoder); + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } + }); + } + + private void initDoricRuntime() { + loadBuiltinJS(DoricConstant.DORIC_BUNDLE_SANDBOX); + String libName = DoricConstant.DORIC_MODULE_LIB; + String libJS = DoricUtils.readAssetFile(DoricConstant.DORIC_BUNDLE_LIB); + mDoricJSE.loadJS(packageModuleScript(libName, libJS), "Module://" + libName); + } + + @Override + public boolean handleMessage(Message msg) { + return false; + } + + public void teardown() { + mDoricJSE.teardown(); + mTimerExtension.teardown(); + handlerThread.quit(); + mJSHandler.removeCallbacksAndMessages(null); + } + + private void loadBuiltinJS(String assetName) { + String script = DoricUtils.readAssetFile(assetName); + mDoricJSE.loadJS(script, "Assets://" + assetName); + } + + public void prepareContext(final String contextId, final String script, final String source) { + mDoricJSE.loadJS(packageContextScript(contextId, script), "Context://" + source); + } + + public void destroyContext(final String contextId) { + mDoricJSE.loadJS(String.format(DoricConstant.TEMPLATE_CONTEXT_DESTROY, contextId), "_Context://" + contextId); + } + + private String packageContextScript(String contextId, String content) { + return String.format(DoricConstant.TEMPLATE_CONTEXT_CREATE, content, contextId, contextId, contextId); + } + + private String packageModuleScript(String moduleName, String content) { + return String.format(DoricConstant.TEMPLATE_MODULE, moduleName, content); + } + + public JSDecoder invokeDoricMethod(final String method, final Object... args) { + ArrayList values = new ArrayList<>(); + for (Object arg : args) { + values.add(DoricUtils.toJavaValue(arg)); + } + return mDoricJSE.invokeMethod(DoricConstant.GLOBAL_DORIC, method, + values.toArray(new JavaValue[values.size()]), false); + } + + @Override + public void callback(long timerId) { + try { + invokeDoricMethod(DoricConstant.DORIC_TIMER_CALLBACK, timerId); + } catch (Exception e) { + e.printStackTrace(); + DoricLog.e("Timer Callback error:%s", e.getLocalizedMessage()); + } + } + + public DoricRegistry getRegistry() { + return mDoricRegistry; + } +} diff --git a/doric/src/main/java/pub/doric/engine/DoricNativeJSExecutor.java b/doric/src/main/java/pub/doric/engine/DoricNativeJSExecutor.java new file mode 100644 index 00000000..503cae7e --- /dev/null +++ b/doric/src/main/java/pub/doric/engine/DoricNativeJSExecutor.java @@ -0,0 +1,66 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.engine; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSExecutor; +import com.github.pengfeizhou.jscore.JSRuntimeException; +import com.github.pengfeizhou.jscore.JavaFunction; +import com.github.pengfeizhou.jscore.JavaValue; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricNativeJSExecutor implements IDoricJSE { + + private final JSExecutor mJSExecutor; + + public DoricNativeJSExecutor() { + this.mJSExecutor = JSExecutor.create(); + } + + @Override + public String loadJS(String script, String source) throws JSRuntimeException { + return mJSExecutor.loadJS(script, source); + } + + @Override + public JSDecoder evaluateJS(String script, String source, boolean hashKey) throws JSRuntimeException { + return mJSExecutor.evaluateJS(script, source, hashKey); + } + + @Override + public void injectGlobalJSFunction(String name, JavaFunction javaFunction) { + mJSExecutor.injectGlobalJSFunction(name, javaFunction); + } + + @Override + public void injectGlobalJSObject(String name, JavaValue javaValue) { + mJSExecutor.injectGlobalJSObject(name, javaValue); + } + + @Override + public JSDecoder invokeMethod(String objectName, String functionName, JavaValue[] javaValues, boolean hashKey) throws JSRuntimeException { + return mJSExecutor.invokeMethod(objectName, functionName, javaValues, hashKey); + } + + @Override + public void teardown() { + mJSExecutor.destroy(); + } +} diff --git a/doric/src/main/java/pub/doric/engine/IDoricJSE.java b/doric/src/main/java/pub/doric/engine/IDoricJSE.java new file mode 100644 index 00000000..0c4e8f3e --- /dev/null +++ b/doric/src/main/java/pub/doric/engine/IDoricJSE.java @@ -0,0 +1,79 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.engine; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSRuntimeException; +import com.github.pengfeizhou.jscore.JavaFunction; +import com.github.pengfeizhou.jscore.JavaValue; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public interface IDoricJSE { + /** + * 执行JS语句 + * + * @param script 执行的JS语句 + * @param source 该JS语句对应的文件名,在输出错误的堆栈信息时有用 + * @return 返回JS语句的执行结果,以String形式返回 + * @throws JSRuntimeException 如果执行的脚本有异常,会抛出包含堆栈的JSRuntimeException + */ + String loadJS(String script, String source) throws JSRuntimeException; + + /** + * 执行JS语句 + * + * @param script 执行的JS语句 + * @param source 该JS语句对应的文件名,在输出错误的堆栈信息时有用 + * @param hashKey 是否在返回对象序列化时将key hash化 + * @return 返回JS语句的执行结果,以二进制数据的形式返回 + * @throws JSRuntimeException 如果执行的脚本有异常,会抛出包含堆栈的JSRuntimeException + */ + JSDecoder evaluateJS(String script, String source, boolean hashKey) throws JSRuntimeException; + + + /** + * 向JS注入全局方法,由java实现 + * + * @param name js的方法名 + * @param javaFunction java中对应的实现类 + */ + void injectGlobalJSFunction(String name, JavaFunction javaFunction); + + /** + * 向JS注入全局变量 + * + * @param name js中的变量名 + * @param javaValue 注入的全局变量,按Value进行组装 + */ + void injectGlobalJSObject(String name, JavaValue javaValue); + + /** + * 执行JS某个方法 + * + * @param objectName 执行的方法所属的变量名,如果方法为全局方法,该参数传null + * @param functionName 执行的方法名 + * @param javaValues 方法需要的参数列表,按数组传入 + * @param hashKey 是否在返回对象序列化时将key hash化 + * @throws JSRuntimeException 如果执行的方法有异常,会抛出包含堆栈的JSRuntimeException + */ + JSDecoder invokeMethod(String objectName, String functionName, JavaValue[] javaValues, boolean hashKey) throws JSRuntimeException; + + void teardown(); +} diff --git a/doric/src/main/java/pub/doric/extension/bridge/DoricBridgeExtension.java b/doric/src/main/java/pub/doric/extension/bridge/DoricBridgeExtension.java new file mode 100644 index 00000000..e968a96d --- /dev/null +++ b/doric/src/main/java/pub/doric/extension/bridge/DoricBridgeExtension.java @@ -0,0 +1,101 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.extension.bridge; + +import pub.doric.DoricContext; + +import pub.doric.async.AsyncResult; +import pub.doric.plugin.DoricJavaPlugin; +import pub.doric.DoricContextManager; +import pub.doric.utils.DoricLog; +import pub.doric.utils.DoricMetaInfo; +import pub.doric.utils.DoricUtils; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricBridgeExtension { + + public DoricBridgeExtension() { + } + + public JavaValue callNative(String contextId, String module, String methodName, final String callbackId, final JSDecoder jsDecoder) { + final DoricContext context = DoricContextManager.getContext(contextId); + DoricMetaInfo pluginInfo = context.getDriver().getRegistry().acquirePluginInfo(module); + if (pluginInfo == null) { + DoricLog.e("Cannot find plugin class:%s", module); + return new JavaValue(false); + } + final DoricJavaPlugin doricJavaPlugin = context.obtainPlugin(pluginInfo); + if (doricJavaPlugin == null) { + DoricLog.e("Cannot obtain plugin instance:%s,method:%", module); + return new JavaValue(false); + } + final Method method = pluginInfo.getMethod(methodName); + if (method == null) { + DoricLog.e("Cannot find plugin method in class:%s,method:%s", module, methodName); + return new JavaValue(false); + } + DoricMethod doricMethod = method.getAnnotation(DoricMethod.class); + if (doricMethod == null) { + DoricLog.e("Cannot find DoricMethod annotation in class:%s,method:%s", module, methodName); + return new JavaValue(false); + } + Callable callable = new Callable() { + @Override + public JavaValue call() throws Exception { + Class[] classes = method.getParameterTypes(); + Object ret; + if (classes.length == 0) { + ret = method.invoke(doricJavaPlugin); + } else if (classes.length == 1) { + ret = method.invoke(doricJavaPlugin, createParam(context, classes[0], callbackId, jsDecoder)); + } else { + ret = method.invoke(doricJavaPlugin, + createParam(context, classes[0], callbackId, jsDecoder), + createParam(context, classes[1], callbackId, jsDecoder)); + } + return DoricUtils.toJavaValue(ret); + } + }; + AsyncResult asyncResult = context.getDriver().asyncCall(callable, doricMethod.thread()); + if (asyncResult.hasResult()) { + return asyncResult.getResult(); + } + return new JavaValue(true); + } + + private Object createParam(DoricContext context, Class clz, String callbackId, JSDecoder jsDecoder) { + if (clz == DoricPromise.class) { + return new DoricPromise(context, callbackId); + } else { + try { + return DoricUtils.toJavaObject(clz, jsDecoder); + } catch (Exception e) { + DoricLog.e("createParam error:%s", e.getLocalizedMessage()); + } + return null; + } + } +} diff --git a/doric/src/main/java/pub/doric/extension/bridge/DoricMethod.java b/doric/src/main/java/pub/doric/extension/bridge/DoricMethod.java new file mode 100644 index 00000000..0fb7e7f8 --- /dev/null +++ b/doric/src/main/java/pub/doric/extension/bridge/DoricMethod.java @@ -0,0 +1,38 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.extension.bridge; + +import pub.doric.utils.ThreadMode; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +@Documented +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface DoricMethod { + String name() default ""; + + ThreadMode thread() default ThreadMode.INDEPENDENT; +} diff --git a/doric/src/main/java/pub/doric/extension/bridge/DoricPlugin.java b/doric/src/main/java/pub/doric/extension/bridge/DoricPlugin.java new file mode 100644 index 00000000..5963a111 --- /dev/null +++ b/doric/src/main/java/pub/doric/extension/bridge/DoricPlugin.java @@ -0,0 +1,34 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.extension.bridge; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +@Documented +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +public @interface DoricPlugin { + String name(); +} diff --git a/doric/src/main/java/pub/doric/extension/bridge/DoricPromise.java b/doric/src/main/java/pub/doric/extension/bridge/DoricPromise.java new file mode 100644 index 00000000..bdb136cf --- /dev/null +++ b/doric/src/main/java/pub/doric/extension/bridge/DoricPromise.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.extension.bridge; + +import pub.doric.DoricContext; +import pub.doric.utils.DoricConstant; + +import com.github.pengfeizhou.jscore.JavaValue; + +/** + * @Description: com.github.penfeizhou.doric.extension.bridge + * @Author: pengfei.zhou + * @CreateDate: 2019-07-19 + */ +public class DoricPromise { + private final DoricContext context; + private final String callbackId; + + public DoricPromise(DoricContext context, String callbackId) { + this.context = context; + this.callbackId = callbackId; + } + + public void resolve(JavaValue... javaValue) { + Object[] params = new Object[javaValue.length + 2]; + params[0] = context.getContextId(); + params[1] = callbackId; + System.arraycopy(javaValue, 0, params, 2, javaValue.length); + context.getDriver().invokeDoricMethod( + DoricConstant.DORIC_BRIDGE_RESOLVE, + params); + } + + public void reject(JavaValue... javaValue) { + Object[] params = new Object[javaValue.length + 2]; + params[0] = context.getContextId(); + params[1] = callbackId; + System.arraycopy(javaValue, 0, params, 2, javaValue.length); + context.getDriver().invokeDoricMethod( + DoricConstant.DORIC_BRIDGE_REJECT, + params); + } +} diff --git a/doric/src/main/java/pub/doric/extension/timer/DoricTimerExtension.java b/doric/src/main/java/pub/doric/extension/timer/DoricTimerExtension.java new file mode 100644 index 00000000..7e3b1745 --- /dev/null +++ b/doric/src/main/java/pub/doric/extension/timer/DoricTimerExtension.java @@ -0,0 +1,85 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.extension.timer; + +import android.os.Handler; +import android.os.Looper; +import android.os.Message; + +import java.util.HashSet; +import java.util.Set; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricTimerExtension implements Handler.Callback { + + private static final int MSG_TIMER = 0; + private final Handler mTimerHandler; + private final TimerCallback mTimerCallback; + private Set mDeletedTimerIds = new HashSet<>(); + + public DoricTimerExtension(Looper looper, TimerCallback timerCallback) { + mTimerHandler = new Handler(looper, this); + mTimerCallback = timerCallback; + } + + public void setTimer(long timerId, long time, boolean repeat) { + TimerInfo timerInfo = new TimerInfo(); + timerInfo.timerId = timerId; + timerInfo.time = time; + timerInfo.repeat = repeat; + mTimerHandler.sendMessageDelayed(Message.obtain(mTimerHandler, MSG_TIMER, timerInfo), timerInfo.time); + } + + public void clearTimer(long timerId) { + mDeletedTimerIds.add(timerId); + } + + @Override + public boolean handleMessage(Message msg) { + if (msg.obj instanceof TimerInfo) { + TimerInfo timerInfo = (TimerInfo) msg.obj; + if (mDeletedTimerIds.contains(timerInfo.timerId)) { + mDeletedTimerIds.remove(timerInfo.timerId); + } else { + mTimerCallback.callback(timerInfo.timerId); + if (timerInfo.repeat) { + mTimerHandler.sendMessageDelayed(Message.obtain(mTimerHandler, MSG_TIMER, timerInfo), timerInfo.time); + } else { + mDeletedTimerIds.remove(timerInfo.timerId); + } + } + } + return true; + } + + public void teardown() { + mTimerHandler.removeCallbacksAndMessages(null); + } + + private class TimerInfo { + long timerId; + long time; + boolean repeat; + } + + public interface TimerCallback { + void callback(long timerId); + } +} diff --git a/doric/src/main/java/pub/doric/loader/DoricAssetJSLoader.java b/doric/src/main/java/pub/doric/loader/DoricAssetJSLoader.java new file mode 100644 index 00000000..2352d971 --- /dev/null +++ b/doric/src/main/java/pub/doric/loader/DoricAssetJSLoader.java @@ -0,0 +1,36 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.loader; + +import pub.doric.async.AsyncResult; +import pub.doric.utils.DoricUtils; + +/** + * @Description: handle "assets://asset-file-path" + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public class DoricAssetJSLoader implements IDoricJSLoader { + @Override + public boolean filter(String scheme) { + return scheme.startsWith("assets"); + } + + @Override + public AsyncResult request(String scheme) { + return new AsyncResult<>(DoricUtils.readAssetFile(scheme.substring("assets://".length()))); + } +} diff --git a/doric/src/main/java/pub/doric/loader/DoricHttpJSLoader.java b/doric/src/main/java/pub/doric/loader/DoricHttpJSLoader.java new file mode 100644 index 00000000..ca3732a6 --- /dev/null +++ b/doric/src/main/java/pub/doric/loader/DoricHttpJSLoader.java @@ -0,0 +1,64 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.loader; + +import com.bumptech.glide.RequestBuilder; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import pub.doric.async.AsyncResult; + +/** + * @Description: handle like "https://xxxx.js" + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public class DoricHttpJSLoader implements IDoricJSLoader { + private OkHttpClient okHttpClient = new OkHttpClient(); + + @Override + public boolean filter(String scheme) { + return scheme.startsWith("http"); + } + + @Override + public AsyncResult request(String scheme) { + final AsyncResult ret = new AsyncResult<>(); + okHttpClient.newCall(new Request.Builder().url(scheme).build()).enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + ret.setError(e); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) { + try { + ret.setResult(response.body().string()); + } catch (Exception e) { + ret.setError(e); + } + } + }); + return ret; + } +} diff --git a/doric/src/main/java/pub/doric/loader/DoricJSLoaderManager.java b/doric/src/main/java/pub/doric/loader/DoricJSLoaderManager.java new file mode 100644 index 00000000..f3c01898 --- /dev/null +++ b/doric/src/main/java/pub/doric/loader/DoricJSLoaderManager.java @@ -0,0 +1,51 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.loader; + + +import java.util.Collection; + +import pub.doric.DoricRegistry; +import pub.doric.async.AsyncResult; + +/** + * @Description: pub.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public class DoricJSLoaderManager { + private DoricJSLoaderManager() { + } + + private static class Inner { + private static final DoricJSLoaderManager sInstance = new DoricJSLoaderManager(); + } + + public static DoricJSLoaderManager getInstance() { + return Inner.sInstance; + } + + public AsyncResult loadJSBundle(String scheme) { + Collection jsLoaders = DoricRegistry.getJSLoaders(); + for (IDoricJSLoader jsLoader : jsLoaders) { + if (jsLoader.filter(scheme)) { + return jsLoader.request(scheme); + } + } + return new AsyncResult<>(""); + } + +} diff --git a/doric/src/main/java/pub/doric/loader/IDoricJSLoader.java b/doric/src/main/java/pub/doric/loader/IDoricJSLoader.java new file mode 100644 index 00000000..c26f453f --- /dev/null +++ b/doric/src/main/java/pub/doric/loader/IDoricJSLoader.java @@ -0,0 +1,29 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.loader; + +import pub.doric.async.AsyncResult; + +/** + * @Description: pub.doric + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public interface IDoricJSLoader { + boolean filter(String scheme); + + AsyncResult request(String scheme); +} diff --git a/doric/src/main/java/pub/doric/navbar/BaseDoricNavBar.java b/doric/src/main/java/pub/doric/navbar/BaseDoricNavBar.java new file mode 100644 index 00000000..12ec34f6 --- /dev/null +++ b/doric/src/main/java/pub/doric/navbar/BaseDoricNavBar.java @@ -0,0 +1,105 @@ +package pub.doric.navbar; + +import android.app.Activity; +import android.content.Context; +import android.text.Layout; +import android.text.StaticLayout; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import pub.doric.R; + +/** + * @Description: pub.doric.navbar + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +public class BaseDoricNavBar extends FrameLayout implements IDoricNavBar { + private ViewGroup mTitleContainer; + private ViewGroup mRightContainer; + private ViewGroup mLeftContainer; + private TextView mTvTitle; + + public BaseDoricNavBar(@NonNull Context context) { + this(context, null); + } + + public BaseDoricNavBar(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public BaseDoricNavBar(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + setup(); + } + + private void setup() { + LayoutInflater.from(getContext()).inflate(R.layout.doric_navigator, this); + mTitleContainer = findViewById(R.id.container_title); + mLeftContainer = findViewById(R.id.container_left); + mRightContainer = findViewById(R.id.container_right); + mTvTitle = findViewById(R.id.tv_title); + findViewById(R.id.tv_back).setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (getContext() instanceof Activity) { + ((Activity) getContext()).onBackPressed(); + } + } + }); + } + + @Override + public boolean isHidden() { + return getVisibility() != VISIBLE; + } + + @Override + public void setHidden(boolean b) { + setVisibility(b ? GONE : VISIBLE); + } + + @Override + public void setTitle(String title) { + mTvTitle.setText(title); + } + + private void updateTitleMargins() { + try { + int width = mRightContainer.getRight() - mLeftContainer.getLeft(); + int leftWidth = mLeftContainer.getWidth(); + int rightWidth = mRightContainer.getWidth(); + int margin = Math.max(leftWidth, rightWidth); + if (leftWidth + rightWidth > width) { + mTitleContainer.setVisibility(GONE); + } else { + mTitleContainer.setVisibility(VISIBLE); + StaticLayout staticLayout = new StaticLayout(mTvTitle.getText(), + mTvTitle.getPaint(), Integer.MAX_VALUE, Layout.Alignment.ALIGN_NORMAL, + 1.0f, 0.0f, false); + float textWidth = (staticLayout.getLineCount() > 0 ? staticLayout.getLineWidth(0) : 0.0f); + if (width - 2 * margin >= textWidth) { + mTitleContainer.setPadding(margin, 0, margin, 0); + } else { + mTitleContainer.setPadding(leftWidth, 0, rightWidth, 0); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + super.onLayout(changed, left, top, right, bottom); + updateTitleMargins(); + } +} \ No newline at end of file diff --git a/doric/src/main/java/pub/doric/navbar/IDoricNavBar.java b/doric/src/main/java/pub/doric/navbar/IDoricNavBar.java new file mode 100644 index 00000000..ff35769d --- /dev/null +++ b/doric/src/main/java/pub/doric/navbar/IDoricNavBar.java @@ -0,0 +1,31 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.navbar; + +/** + * @Description: pub.doric.navbar + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +public interface IDoricNavBar { + boolean isHidden(); + + void setHidden(boolean hidden); + + void setTitle(String title); + + void setBackgroundColor(int color); +} diff --git a/doric/src/main/java/pub/doric/navigator/IDoricNavigator.java b/doric/src/main/java/pub/doric/navigator/IDoricNavigator.java new file mode 100644 index 00000000..e260cd5f --- /dev/null +++ b/doric/src/main/java/pub/doric/navigator/IDoricNavigator.java @@ -0,0 +1,27 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.navigator; + +/** + * @Description: pub.doric.navigator + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +public interface IDoricNavigator { + void push(String scheme, String alias); + + void pop(); +} diff --git a/doric/src/main/java/pub/doric/plugin/AnimatePlugin.java b/doric/src/main/java/pub/doric/plugin/AnimatePlugin.java new file mode 100644 index 00000000..1f9383fd --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/AnimatePlugin.java @@ -0,0 +1,86 @@ +package pub.doric.plugin; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.text.TextUtils; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.util.concurrent.Callable; + +import pub.doric.DoricContext; +import pub.doric.async.AsyncResult; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.shader.RootNode; +import pub.doric.shader.ViewNode; +import pub.doric.utils.DoricLog; +import pub.doric.utils.ThreadMode; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-29 + */ +@DoricPlugin(name = "animate") +public class AnimatePlugin extends DoricJavaPlugin { + public AnimatePlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod + public void submit(DoricPromise promise) { + promise.resolve(); + } + + @DoricMethod + public void animateRender(final JSObject jsObject, final DoricPromise promise) { + getDoricContext().getDriver().asyncCall(new Callable() { + @Override + public Object call() throws Exception { + final long duration = jsObject.getProperty("duration").asNumber().toLong(); + AnimatorSet animatorSet = new AnimatorSet(); + getDoricContext().setAnimatorSet(animatorSet); + String viewId = jsObject.getProperty("id").asString().value(); + RootNode rootNode = getDoricContext().getRootNode(); + if (TextUtils.isEmpty(rootNode.getId())) { + rootNode.setId(viewId); + rootNode.blend(jsObject.getProperty("props").asObject()); + } else { + ViewNode viewNode = getDoricContext().targetViewNode(viewId); + if (viewNode != null) { + viewNode.blend(jsObject.getProperty("props").asObject()); + } + } + getDoricContext().setAnimatorSet(null); + animatorSet.setDuration(duration); + animatorSet.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + promise.resolve(); + } + }); + animatorSet.start(); + return null; + } + }, ThreadMode.UI).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Object result) { + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + DoricLog.e("Shader.render:error%s", t.getLocalizedMessage()); + promise.reject(new JavaValue(t.getLocalizedMessage())); + } + + @Override + public void onFinish() { + } + }); + } +} diff --git a/doric/src/main/java/pub/doric/plugin/DoricJavaPlugin.java b/doric/src/main/java/pub/doric/plugin/DoricJavaPlugin.java new file mode 100644 index 00000000..4dffe8c5 --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/DoricJavaPlugin.java @@ -0,0 +1,30 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import pub.doric.DoricContext; +import pub.doric.utils.DoricContextHolder; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public abstract class DoricJavaPlugin extends DoricContextHolder { + public DoricJavaPlugin(DoricContext doricContext) { + super(doricContext); + } +} diff --git a/doric/src/main/java/pub/doric/plugin/ModalPlugin.java b/doric/src/main/java/pub/doric/plugin/ModalPlugin.java new file mode 100644 index 00000000..845a387f --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/ModalPlugin.java @@ -0,0 +1,211 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import pub.doric.DoricContext; +import pub.doric.R; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.utils.DoricUtils; +import pub.doric.utils.ThreadMode; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +@DoricPlugin(name = "modal") +public class ModalPlugin extends DoricJavaPlugin { + + public ModalPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod(thread = ThreadMode.UI) + public void toast(JSObject jsObject) { + try { + String msg = jsObject.getProperty("msg").asString().value(); + JSValue gravityVal = jsObject.getProperty("gravity"); + int gravity = Gravity.BOTTOM; + if (gravityVal.isNumber()) { + gravity = gravityVal.asNumber().toInt(); + } + Toast toast = Toast.makeText(getDoricContext().getContext(), + jsObject.getProperty("msg").asString().value(), + Toast.LENGTH_SHORT); + if ((gravity & Gravity.TOP) == Gravity.TOP) { + toast.setGravity(Gravity.TOP | Gravity.CENTER_HORIZONTAL, 0, DoricUtils.dp2px(50)); + } else if ((gravity & Gravity.BOTTOM) == Gravity.BOTTOM) { + toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, DoricUtils.dp2px(50)); + } else { + toast.setGravity(Gravity.CENTER | Gravity.CENTER_HORIZONTAL, 0, 0); + + } + toast.show(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @DoricMethod(thread = ThreadMode.UI) + public void alert(JSObject jsObject, final DoricPromise promise) { + try { + JSValue titleVal = jsObject.getProperty("title"); + JSValue msgVal = jsObject.getProperty("msg"); + JSValue okBtn = jsObject.getProperty("okLabel"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getDoricContext().getContext(), R.style.Theme_Doric_Modal_Alert); + if (titleVal.isString()) { + builder.setTitle(titleVal.asString().value()); + } + String btnTitle = getDoricContext().getContext().getString(android.R.string.ok); + if (okBtn.isString()) { + btnTitle = okBtn.asString().value(); + } + builder.setMessage(msgVal.asString().value()) + .setPositiveButton(btnTitle, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + promise.resolve(); + } + }); + builder.setCancelable(false); + builder.show(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + + @DoricMethod(thread = ThreadMode.UI) + public void confirm(JSObject jsObject, final DoricPromise promise) { + try { + JSValue titleVal = jsObject.getProperty("title"); + JSValue msgVal = jsObject.getProperty("msg"); + JSValue okBtn = jsObject.getProperty("okLabel"); + JSValue cancelBtn = jsObject.getProperty("cancelLabel"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getDoricContext().getContext(), R.style.Theme_Doric_Modal_Confirm); + if (titleVal.isString()) { + builder.setTitle(titleVal.asString().value()); + } + String okLabel = getDoricContext().getContext().getString(android.R.string.ok); + if (okBtn.isString()) { + okLabel = okBtn.asString().value(); + } + String cancelLabel = getDoricContext().getContext().getString(android.R.string.cancel); + if (cancelBtn.isString()) { + cancelLabel = cancelBtn.asString().value(); + } + builder.setMessage(msgVal.asString().value()) + .setPositiveButton(okLabel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + promise.resolve(); + } + }) + .setNegativeButton(cancelLabel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + promise.reject(); + } + }); + builder.setCancelable(false); + builder.show(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + + @DoricMethod(thread = ThreadMode.UI) + public void prompt(JSObject jsObject, final DoricPromise promise) { + try { + JSValue titleVal = jsObject.getProperty("title"); + JSValue msgVal = jsObject.getProperty("msg"); + JSValue okBtn = jsObject.getProperty("okLabel"); + JSValue cancelBtn = jsObject.getProperty("cancelLabel"); + JSValue defaultVal = jsObject.getProperty("defaultText"); + JSValue text = jsObject.getProperty("text"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getDoricContext().getContext(), R.style.Theme_Doric_Modal_Prompt); + if (titleVal.isString()) { + builder.setTitle(titleVal.asString().value()); + } + String okLabel = getDoricContext().getContext().getString(android.R.string.ok); + if (okBtn.isString()) { + okLabel = okBtn.asString().value(); + } + String cancelLabel = getDoricContext().getContext().getString(android.R.string.cancel); + if (cancelBtn.isString()) { + cancelLabel = cancelBtn.asString().value(); + } + + + View v = LayoutInflater.from(getDoricContext().getContext()).inflate(R.layout.doric_modal_prompt, null); + TextView tvMsg = v.findViewById(R.id.tv_msg); + if (msgVal.isString()) { + tvMsg.setText(msgVal.asString().value()); + } + final EditText editText = v.findViewById(R.id.edit_input); + if (defaultVal.isString()) { + editText.setHint(defaultVal.asString().value()); + } + if (text.isString()) { + editText.setText(text.asString().value()); + editText.setSelection(text.asString().value().length()); + } + builder.setView(v); + builder + .setPositiveButton(okLabel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + promise.resolve(new JavaValue(editText.getText().toString())); + } + }) + .setNegativeButton(cancelLabel, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + promise.reject(new JavaValue(editText.getText().toString())); + } + }); + builder.setCancelable(false); + builder.show(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + + + } +} diff --git a/doric/src/main/java/pub/doric/plugin/NavBarPlugin.java b/doric/src/main/java/pub/doric/plugin/NavBarPlugin.java new file mode 100644 index 00000000..f1b9a7b8 --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/NavBarPlugin.java @@ -0,0 +1,114 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import android.view.View; +import android.view.ViewGroup; + +import com.github.pengfeizhou.jscore.ArchiveException; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JavaValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.navbar.IDoricNavBar; +import pub.doric.utils.ThreadMode; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +@DoricPlugin(name = "navbar") +public class NavBarPlugin extends DoricJavaPlugin { + public NavBarPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod(thread = ThreadMode.UI) + public void isHidden(DoricPromise promise) { + IDoricNavBar navBar = getDoricContext().getDoricNavBar(); + if (navBar == null) { + promise.reject(new JavaValue("Not implement NavBar")); + } else { + promise.resolve(new JavaValue(navBar.isHidden())); + } + } + + @DoricMethod(thread = ThreadMode.UI) + public void setHidden(JSDecoder jsDecoder, DoricPromise promise) { + IDoricNavBar navBar = getDoricContext().getDoricNavBar(); + if (navBar == null) { + promise.reject(new JavaValue("Not implement NavBar")); + } else { + try { + JSObject jsObject = jsDecoder.decode().asObject(); + boolean hidden = jsObject.getProperty("hidden").asBoolean().value(); + navBar.setHidden(hidden); + View v = getDoricContext().getRootNode().getNodeView(); + ViewGroup.LayoutParams params = v.getLayoutParams(); + if (params instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) params).topMargin = + hidden ? 0 + : ((View) navBar).getMeasuredHeight(); + } + promise.resolve(); + } catch (ArchiveException e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + } + + @DoricMethod(thread = ThreadMode.UI) + public void setTitle(JSDecoder jsDecoder, DoricPromise promise) { + IDoricNavBar navBar = getDoricContext().getDoricNavBar(); + if (navBar == null) { + promise.reject(new JavaValue("Not implement NavBar")); + } else { + try { + JSObject jsObject = jsDecoder.decode().asObject(); + String title = jsObject.getProperty("title").asString().value(); + navBar.setTitle(title); + promise.resolve(); + } catch (ArchiveException e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + } + + @DoricMethod(thread = ThreadMode.UI) + public void setBgColor(JSDecoder jsDecoder, DoricPromise promise) { + IDoricNavBar navBar = getDoricContext().getDoricNavBar(); + if (navBar == null) { + promise.reject(new JavaValue("Not implement NavBar")); + } else { + try { + JSObject jsObject = jsDecoder.decode().asObject(); + int color = jsObject.getProperty("color").asNumber().toInt(); + navBar.setBackgroundColor(color); + promise.resolve(); + } catch (ArchiveException e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + } +} diff --git a/doric/src/main/java/pub/doric/plugin/NavigatorPlugin.java b/doric/src/main/java/pub/doric/plugin/NavigatorPlugin.java new file mode 100644 index 00000000..372529ec --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/NavigatorPlugin.java @@ -0,0 +1,61 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import com.github.pengfeizhou.jscore.ArchiveException; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.navigator.IDoricNavigator; +import pub.doric.utils.ThreadMode; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-23 + */ +@DoricPlugin(name = "navigator") +public class NavigatorPlugin extends DoricJavaPlugin { + public NavigatorPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod(thread = ThreadMode.UI) + public void push(JSDecoder jsDecoder) { + IDoricNavigator navigator = getDoricContext().getDoricNavigator(); + if (navigator != null) { + try { + JSObject jsObject = jsDecoder.decode().asObject(); + navigator.push(jsObject.getProperty("scheme").asString().value(), + jsObject.getProperty("alias").asString().value() + ); + } catch (ArchiveException e) { + e.printStackTrace(); + } + } + } + + @DoricMethod(thread = ThreadMode.UI) + public void pop() { + IDoricNavigator navigator = getDoricContext().getDoricNavigator(); + if (navigator != null) { + navigator.pop(); + } + } +} diff --git a/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java b/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java new file mode 100644 index 00000000..ec36fb34 --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/NetworkPlugin.java @@ -0,0 +1,115 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import android.text.TextUtils; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import org.jetbrains.annotations.NotNull; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import okhttp3.Call; +import okhttp3.Callback; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.internal.http.HttpMethod; +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-21 + */ +@DoricPlugin(name = "network") +public class NetworkPlugin extends DoricJavaPlugin { + private OkHttpClient okHttpClient = new OkHttpClient(); + + public NetworkPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod + public void request(JSObject requestVal, final DoricPromise promise) { + try { + String url = requestVal.getProperty("url").asString().value(); + String method = requestVal.getProperty("method").asString().value(); + JSValue headerVal = requestVal.getProperty("headers"); + JSValue dataVal = requestVal.getProperty("data"); + JSValue timeoutVal = requestVal.getProperty("timeout"); + + Headers.Builder headersBuilder = new Headers.Builder(); + if (headerVal.isObject()) { + JSObject headerObject = headerVal.asObject(); + Set headerKeys = headerObject.propertySet(); + for (String key : headerKeys) { + headersBuilder.add(key, headerObject.getProperty(key).asString().value()); + } + } + Headers headers = headersBuilder.build(); + String contentType = headers.get("Content-Type"); + MediaType mediaType = MediaType.parse(TextUtils.isEmpty(contentType) ? "application/json; charset=utf-8" : contentType); + RequestBody requestBody = HttpMethod.permitsRequestBody(method) ? RequestBody.create(mediaType, dataVal.isString() ? dataVal.asString().value() : "") : null; + Request.Builder requestBuilder = new Request.Builder(); + requestBuilder.url(url) + .headers(headers) + .method(method, requestBody); + if (timeoutVal.isNumber() && okHttpClient.connectTimeoutMillis() != timeoutVal.asNumber().toLong()) { + okHttpClient = okHttpClient.newBuilder().connectTimeout(timeoutVal.asNumber().toLong(), TimeUnit.MILLISECONDS).build(); + } + okHttpClient.newCall(requestBuilder.build()).enqueue(new Callback() { + @Override + public void onFailure(@NotNull Call call, @NotNull IOException e) { + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + + @Override + public void onResponse(@NotNull Call call, @NotNull Response response) throws IOException { + JSONBuilder header = new JSONBuilder(); + for (String key : response.headers().names()) { + header.put(key, response.headers().get(key)); + } + JSONObject jsonObject = new JSONBuilder() + .put("status", response.code()) + .put("headers", header.toJSONObject()) + .put("data", response.body() == null ? "" : response.body().string()) + .toJSONObject(); + promise.resolve(new JavaValue(jsonObject)); + } + }); + + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + +} diff --git a/doric/src/main/java/pub/doric/plugin/PopoverPlugin.java b/doric/src/main/java/pub/doric/plugin/PopoverPlugin.java new file mode 100644 index 00000000..457b168b --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/PopoverPlugin.java @@ -0,0 +1,139 @@ +package pub.doric.plugin; + +import android.app.Activity; +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.util.concurrent.Callable; + +import pub.doric.DoricContext; +import pub.doric.async.AsyncCall; +import pub.doric.async.AsyncResult; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.shader.ViewNode; +import pub.doric.utils.ThreadMode; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-29 + */ +@DoricPlugin(name = "popover") +public class PopoverPlugin extends DoricJavaPlugin { + private FrameLayout mFullScreenView; + + public PopoverPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod + public void show(JSDecoder decoder, final DoricPromise promise) { + try { + final JSObject jsObject = decoder.decode().asObject(); + getDoricContext().getDriver().asyncCall(new Callable() { + @Override + public Object call() throws Exception { + if (mFullScreenView == null) { + mFullScreenView = new FrameLayout(getDoricContext().getContext()); + ViewGroup decorView = (ViewGroup) getDoricContext().getRootNode().getNodeView().getRootView(); + decorView.addView(mFullScreenView, new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT)); + } + mFullScreenView.setVisibility(View.VISIBLE); + mFullScreenView.bringToFront(); + String viewId = jsObject.getProperty("id").asString().value(); + String type = jsObject.getProperty("type").asString().value(); + ViewNode node = ViewNode.create(getDoricContext(), type); + node.setId(viewId); + node.init(new FrameLayout.LayoutParams(0, 0)); + node.blend(jsObject.getProperty("props").asObject()); + mFullScreenView.addView(node.getNodeView()); + getDoricContext().addHeadNode(node); + return null; + } + }, ThreadMode.UI).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Object result) { + promise.resolve(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + promise.reject(new JavaValue(t.getLocalizedMessage())); + } + + @Override + public void onFinish() { + + } + }); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + @DoricMethod + public void dismiss(final JSValue value, final DoricPromise promise) { + try { + getDoricContext().getDriver().asyncCall(new Callable() { + @Override + public Object call() throws Exception { + if (value.isObject()) { + String viewId = value.asObject().getProperty("id").asString().value(); + ViewNode node = getDoricContext().targetViewNode(viewId); + dismissViewNode(node); + } else { + dismissPopover(); + } + return null; + } + }, ThreadMode.UI).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Object result) { + promise.resolve(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + promise.reject(new JavaValue(t.getLocalizedMessage())); + } + + @Override + public void onFinish() { + + } + }); + + + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + private void dismissViewNode(ViewNode node) { + getDoricContext().removeHeadNode(node); + mFullScreenView.removeView(node.getNodeView()); + if (getDoricContext().allHeadNodes().isEmpty()) { + mFullScreenView.setVisibility(View.GONE); + } + } + + private void dismissPopover() { + for (ViewNode node : getDoricContext().allHeadNodes()) { + dismissViewNode(node); + } + } +} diff --git a/doric/src/main/java/pub/doric/plugin/ShaderPlugin.java b/doric/src/main/java/pub/doric/plugin/ShaderPlugin.java new file mode 100644 index 00000000..67163a86 --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/ShaderPlugin.java @@ -0,0 +1,186 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import android.text.TextUtils; + +import pub.doric.DoricContext; +import pub.doric.async.AsyncResult; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; +import pub.doric.utils.DoricLog; +import pub.doric.utils.DoricMetaInfo; +import pub.doric.utils.DoricUtils; +import pub.doric.utils.ThreadMode; +import pub.doric.shader.RootNode; + +import com.github.pengfeizhou.jscore.ArchiveException; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.lang.reflect.Method; +import java.util.concurrent.Callable; + +/** + * @Description: com.github.penfeizhou.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-07-22 + */ +@DoricPlugin(name = "shader") +public class ShaderPlugin extends DoricJavaPlugin { + public ShaderPlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod + public void render(JSDecoder jsDecoder) { + try { + final JSObject jsObject = jsDecoder.decode().asObject(); + getDoricContext().getDriver().asyncCall(new Callable() { + @Override + public Object call() throws Exception { + String viewId = jsObject.getProperty("id").asString().value(); + RootNode rootNode = getDoricContext().getRootNode(); + if (TextUtils.isEmpty(rootNode.getId())) { + rootNode.setId(viewId); + rootNode.blend(jsObject.getProperty("props").asObject()); + } else { + ViewNode viewNode = getDoricContext().targetViewNode(viewId); + if (viewNode != null) { + viewNode.blend(jsObject.getProperty("props").asObject()); + } + } + return null; + } + }, ThreadMode.UI).setCallback(new AsyncResult.Callback() { + @Override + public void onResult(Object result) { + + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + DoricLog.e("Shader.render:error%s", t.getLocalizedMessage()); + } + + @Override + public void onFinish() { + + } + }); + } catch (Exception e) { + e.printStackTrace(); + DoricLog.e("Shader.render:error%s", e.getLocalizedMessage()); + } + } + + @DoricMethod + public JavaValue command(JSDecoder jsDecoder, final DoricPromise doricPromise) { + try { + final JSObject jsObject = jsDecoder.decode().asObject(); + final JSValue[] viewIds = jsObject.getProperty("viewIds").asArray().toArray(); + final String name = jsObject.getProperty("name").asString().value(); + final JSValue args = jsObject.getProperty("args"); + ViewNode viewNode = null; + for (JSValue value : viewIds) { + if (viewNode == null) { + viewNode = getDoricContext().targetViewNode(value.asString().value()); + } else { + if (value.isString() && viewNode instanceof SuperNode) { + String viewId = value.asString().value(); + viewNode = ((SuperNode) viewNode).getSubNodeById(viewId); + } + } + } + if (viewNode == null) { + doricPromise.reject(new JavaValue("Cannot find opposite view")); + } else { + final ViewNode targetViewNode = viewNode; + DoricMetaInfo pluginInfo = getDoricContext().getDriver().getRegistry() + .acquireViewNodeInfo(viewNode.getType()); + final Method method = pluginInfo.getMethod(name); + if (method == null) { + String errMsg = String.format( + "Cannot find plugin method in class:%s,method:%s", + viewNode.getClass(), + name); + doricPromise.reject(new JavaValue(errMsg)); + } else { + Callable callable = new Callable() { + @Override + public JavaValue call() throws Exception { + Class[] classes = method.getParameterTypes(); + Object ret; + if (classes.length == 0) { + ret = method.invoke(targetViewNode); + } else if (classes.length == 1) { + ret = method.invoke(targetViewNode, + createParam(classes[0], doricPromise, args)); + } else { + ret = method.invoke(targetViewNode, + createParam(classes[0], doricPromise, args), + createParam(classes[1], doricPromise, args)); + } + return DoricUtils.toJavaValue(ret); + } + }; + AsyncResult asyncResult = getDoricContext().getDriver() + .asyncCall(callable, ThreadMode.UI); + if (!method.getReturnType().equals(Void.TYPE)) { + asyncResult.setCallback(new AsyncResult.Callback() { + @Override + public void onResult(JavaValue result) { + doricPromise.resolve(result); + } + + @Override + public void onError(Throwable t) { + doricPromise.resolve(new JavaValue(t.getLocalizedMessage())); + } + + @Override + public void onFinish() { + + } + }); + } + } + } + } catch (ArchiveException e) { + e.printStackTrace(); + } + return new JavaValue(true); + } + + + private Object createParam(Class clz, DoricPromise doricPromise, JSValue jsValue) { + if (clz == DoricPromise.class) { + return doricPromise; + } else { + try { + return DoricUtils.toJavaObject(clz, jsValue); + } catch (Exception e) { + return jsValue; + } + } + } +} diff --git a/doric/src/main/java/pub/doric/plugin/StoragePlugin.java b/doric/src/main/java/pub/doric/plugin/StoragePlugin.java new file mode 100644 index 00000000..746ffd29 --- /dev/null +++ b/doric/src/main/java/pub/doric/plugin/StoragePlugin.java @@ -0,0 +1,110 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.plugin; + +import android.content.Context; + +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; + +/** + * @Description: pub.doric.plugin + * @Author: pengfei.zhou + * @CreateDate: 2019-11-22 + */ +@DoricPlugin(name = "storage") +public class StoragePlugin extends DoricJavaPlugin { + private static final String PREF_NAME = "pref_doric"; + + public StoragePlugin(DoricContext doricContext) { + super(doricContext); + } + + @DoricMethod + public void setItem(JSObject jsObject, final DoricPromise promise) { + try { + JSValue zone = jsObject.getProperty("zone"); + String key = jsObject.getProperty("key").asString().value(); + String value = jsObject.getProperty("value").asString().value(); + String prefName = zone.isString() ? PREF_NAME + "_" + zone.asString() : PREF_NAME; + getDoricContext().getContext().getSharedPreferences( + prefName, + Context.MODE_PRIVATE).edit().putString(key, value).apply(); + promise.resolve(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + @DoricMethod + public void getItem(JSObject jsObject, final DoricPromise promise) { + try { + JSValue zone = jsObject.getProperty("zone"); + String key = jsObject.getProperty("key").asString().value(); + String prefName = zone.isString() ? PREF_NAME + "_" + zone.asString() : PREF_NAME; + String ret = getDoricContext().getContext().getSharedPreferences( + prefName, + Context.MODE_PRIVATE).getString(key, ""); + promise.resolve(new JavaValue(ret)); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + @DoricMethod + public void remove(JSObject jsObject, final DoricPromise promise) { + try { + JSValue zone = jsObject.getProperty("zone"); + String key = jsObject.getProperty("key").asString().value(); + String prefName = zone.isString() ? PREF_NAME + "_" + zone.asString() : PREF_NAME; + getDoricContext().getContext().getSharedPreferences( + prefName, + Context.MODE_PRIVATE).edit().remove(key).apply(); + promise.resolve(); + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } + + @DoricMethod + public void clear(JSObject jsObject, final DoricPromise promise) { + try { + JSValue zone = jsObject.getProperty("zone"); + if (zone.isString()) { + String prefName = PREF_NAME + "_" + zone.asString(); + getDoricContext().getContext().getSharedPreferences( + prefName, + Context.MODE_PRIVATE).edit().clear().apply(); + promise.resolve(); + } else { + promise.reject(new JavaValue("Zone is empty")); + } + } catch (Exception e) { + e.printStackTrace(); + promise.reject(new JavaValue(e.getLocalizedMessage())); + } + } +} diff --git a/doric/src/main/java/pub/doric/refresh/DoricRefreshView.java b/doric/src/main/java/pub/doric/refresh/DoricRefreshView.java new file mode 100644 index 00000000..43bd5b1a --- /dev/null +++ b/doric/src/main/java/pub/doric/refresh/DoricRefreshView.java @@ -0,0 +1,103 @@ +package pub.doric.refresh; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.widget.FrameLayout; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @Description: pub.doric.pullable + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +public class DoricRefreshView extends FrameLayout implements PullingListener { + private View content; + private Animation.AnimationListener mListener; + + private PullingListener mPullingListener; + + public DoricRefreshView(@NonNull Context context) { + super(context); + } + + public DoricRefreshView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DoricRefreshView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setContent(View v) { + removeAllViews(); + content = v; + ViewGroup.LayoutParams params = v.getLayoutParams(); + if (params instanceof LayoutParams) { + ((LayoutParams) params).gravity = Gravity.BOTTOM; + } else { + LayoutParams layoutParams = new LayoutParams( + params == null ? ViewGroup.LayoutParams.WRAP_CONTENT : params.width, + params == null ? ViewGroup.LayoutParams.WRAP_CONTENT : params.height); + layoutParams.gravity = Gravity.CENTER; + v.setLayoutParams(layoutParams); + } + addView(v); + } + + public View getContent() { + return content; + } + + + public void setPullingListener(PullingListener listener) { + this.mPullingListener = listener; + } + + @Override + public void startAnimation() { + if (mPullingListener != null) { + mPullingListener.startAnimation(); + } + } + + @Override + public void stopAnimation() { + if (mPullingListener != null) { + mPullingListener.stopAnimation(); + } + } + + @Override + public void setPullingDistance(float distance) { + if (mPullingListener != null) { + mPullingListener.setPullingDistance(distance); + } + } + + public void setAnimationListener(Animation.AnimationListener listener) { + mListener = listener; + } + + @Override + protected void onAnimationStart() { + super.onAnimationStart(); + if (mListener != null) { + mListener.onAnimationStart(getAnimation()); + } + } + + @Override + protected void onAnimationEnd() { + super.onAnimationEnd(); + if (mListener != null) { + mListener.onAnimationEnd(getAnimation()); + } + } +} \ No newline at end of file diff --git a/doric/src/main/java/pub/doric/refresh/DoricSwipeLayout.java b/doric/src/main/java/pub/doric/refresh/DoricSwipeLayout.java new file mode 100644 index 00000000..fca764d5 --- /dev/null +++ b/doric/src/main/java/pub/doric/refresh/DoricSwipeLayout.java @@ -0,0 +1,920 @@ +package pub.doric.refresh; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.TypedArray; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.animation.Animation; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Transformation; +import android.widget.AbsListView; +import android.widget.ListView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; +import androidx.core.view.NestedScrollingChild; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.NestedScrollingParent; +import androidx.core.view.NestedScrollingParentHelper; +import androidx.core.view.ViewCompat; +import androidx.core.widget.ListViewCompat; +import androidx.swiperefreshlayout.widget.CircularProgressDrawable; + +import android.view.animation.Animation.AnimationListener; + +import pub.doric.utils.DoricUtils; + +/** + * @Description: pub.doric.pullable + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +public class DoricSwipeLayout extends ViewGroup implements NestedScrollingParent, + NestedScrollingChild { + // Maps to ProgressBar.Large style + public static final int LARGE = CircularProgressDrawable.LARGE; + // Maps to ProgressBar default style + public static final int DEFAULT = CircularProgressDrawable.DEFAULT; + + public static final int DEFAULT_SLINGSHOT_DISTANCE = -1; + + @VisibleForTesting + static final int CIRCLE_DIAMETER = 40; + @VisibleForTesting + static final int CIRCLE_DIAMETER_LARGE = 56; + + private static final String LOG_TAG = DoricSwipeLayout.class.getSimpleName(); + + private static final int MAX_ALPHA = 255; + private static final int STARTING_PROGRESS_ALPHA = (int) (.3f * MAX_ALPHA); + + private static final float DECELERATE_INTERPOLATION_FACTOR = 2f; + private static final int INVALID_POINTER = -1; + private static final float DRAG_RATE = .5f; + + // Max amount of circle that can be filled by progress during swipe gesture, + // where 1.0 is a full circle + private static final float MAX_PROGRESS_ANGLE = .8f; + + private static final int SCALE_DOWN_DURATION = 150; + + private static final int ALPHA_ANIMATION_DURATION = 300; + + private static final int ANIMATE_TO_TRIGGER_DURATION = 200; + + private static final int ANIMATE_TO_START_DURATION = 200; + + // Default offset in dips from the top of the view to where the progress spinner should stop + private static final int DEFAULT_CIRCLE_TARGET = 64; + + private View mTarget; // the target of the gesture + OnRefreshListener mListener; + boolean mRefreshing = false; + private int mTouchSlop; + private float mTotalDragDistance = -1; + + // If nested scrolling is enabled, the total amount that needed to be + // consumed by this as the nested scrolling parent is used in place of the + // overscroll determined by MOVE events in the onTouch handler + private float mTotalUnconsumed; + private final NestedScrollingParentHelper mNestedScrollingParentHelper; + private final NestedScrollingChildHelper mNestedScrollingChildHelper; + private final int[] mParentScrollConsumed = new int[2]; + private final int[] mParentOffsetInWindow = new int[2]; + private boolean mNestedScrollInProgress; + + private int mMediumAnimationDuration; + int mCurrentTargetOffsetTop; + + private float mInitialMotionY; + private float mInitialDownY; + private boolean mIsBeingDragged; + private int mActivePointerId = INVALID_POINTER; + + // Target is returning to its start offset because it was cancelled or a + // refresh was triggered. + private boolean mReturningToStart; + private final DecelerateInterpolator mDecelerateInterpolator; + private static final int[] LAYOUT_ATTRS = new int[]{ + android.R.attr.enabled + }; + + private int mCircleViewIndex = -1; + + protected int mFrom; + + float mStartingScale; + + protected int mOriginalOffsetTop; + + int mSpinnerOffsetEnd; + + int mCustomSlingshotDistance; + + + boolean mNotify; + + // Whether the client has set a custom starting position; + boolean mUsingCustomStart; + + private OnChildScrollUpCallback mChildScrollUpCallback; + + private DoricRefreshView mRefreshView; + + private AnimationListener mRefreshListener = new AnimationListener() { + @Override + public void onAnimationStart(Animation animation) { + } + + @Override + public void onAnimationRepeat(Animation animation) { + } + + @Override + public void onAnimationEnd(Animation animation) { + if (mRefreshing) { + mRefreshView.startAnimation(); + if (mNotify) { + if (mListener != null) { + mListener.onRefresh(); + } + } + mCurrentTargetOffsetTop = mRefreshView.getTop(); + } else { + reset(); + } + } + }; + private int mPullDownHeight = 0; + private ValueAnimator headerViewAnimator; + + private void onRefreshAnimationEnd() { + if (mRefreshing) { + mRefreshView.startAnimation(); + if (mNotify) { + if (mListener != null) { + mListener.onRefresh(); + } + } + mCurrentTargetOffsetTop = mRefreshView.getTop(); + } else { + reset(); + } + } + + void reset() { + mRefreshing = false; + if (headerViewAnimator != null && headerViewAnimator.isRunning()) { + headerViewAnimator.cancel(); + } + headerViewAnimator = ValueAnimator.ofInt(mRefreshView.getBottom(), 0); + headerViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + mCurrentTargetOffsetTop = (int) animation.getAnimatedValue() + - mRefreshView.getMeasuredHeight(); + if (mRefreshView.getMeasuredHeight() > 0) { + mRefreshView.setPullingDistance(DoricUtils.px2dp(mRefreshView.getBottom())); + } + mRefreshView.requestLayout(); + } + }); + headerViewAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + + } + + @Override + public void onAnimationEnd(Animator animation) { + mRefreshView.stopAnimation(); + mRefreshView.setVisibility(View.GONE); + // Return the circle to its start position + + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mCurrentTargetOffsetTop); + mCurrentTargetOffsetTop = mRefreshView.getTop(); + } + + @Override + public void onAnimationCancel(Animator animation) { + + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + headerViewAnimator.setDuration(SCALE_DOWN_DURATION); + headerViewAnimator.start(); + } + + @Override + public void setEnabled(boolean enabled) { + super.setEnabled(enabled); + if (!enabled) { + reset(); + } + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + reset(); + } + + /** + * Simple constructor to use when creating a SwipeRefreshLayout from code. + * + * @param context + */ + public DoricSwipeLayout(@NonNull Context context) { + this(context, null); + } + + /** + * Constructor that is called when inflating SwipeRefreshLayout from XML. + * + * @param context + * @param attrs + */ + public DoricSwipeLayout(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + + mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); + + mMediumAnimationDuration = getResources().getInteger( + android.R.integer.config_mediumAnimTime); + + setWillNotDraw(false); + mDecelerateInterpolator = new DecelerateInterpolator(DECELERATE_INTERPOLATION_FACTOR); + + final DisplayMetrics metrics = getResources().getDisplayMetrics(); + + createProgressView(); + setChildrenDrawingOrderEnabled(true); + // the absolute offset has to take into account that the circle starts at an offset + mSpinnerOffsetEnd = (int) (DEFAULT_CIRCLE_TARGET * metrics.density); + mTotalDragDistance = mSpinnerOffsetEnd; + mNestedScrollingParentHelper = new NestedScrollingParentHelper(this); + + mNestedScrollingChildHelper = new NestedScrollingChildHelper(this); + setNestedScrollingEnabled(true); + + moveToStart(1.0f); + + final TypedArray a = context.obtainStyledAttributes(attrs, LAYOUT_ATTRS); + setEnabled(a.getBoolean(0, true)); + a.recycle(); + } + + public void setPullDownHeight(int height) { + mPullDownHeight = height; + mOriginalOffsetTop = mCurrentTargetOffsetTop = -height; + mSpinnerOffsetEnd = height; + mTotalDragDistance = height; + } + + @Override + protected int getChildDrawingOrder(int childCount, int i) { + if (mCircleViewIndex < 0) { + return i; + } else if (i == childCount - 1) { + // Draw the selected child last + return mCircleViewIndex; + } else if (i >= mCircleViewIndex) { + // Move the children after the selected child earlier one + return i + 1; + } else { + // Keep the children before the selected child the same + return i; + } + } + + private void createProgressView() { + mRefreshView = new DoricRefreshView(getContext()); + ViewGroup.LayoutParams layoutParams = new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + addView(mRefreshView, layoutParams); + } + + public DoricRefreshView getRefreshView() { + return mRefreshView; + } + + /** + * Set the listener to be notified when a refresh is triggered via the swipe + * gesture. + */ + public void setOnRefreshListener(@Nullable OnRefreshListener listener) { + mListener = listener; + } + + /** + * Notify the widget that refresh state has changed. Do not call this when + * refresh is triggered by a swipe gesture. + * + * @param refreshing Whether or not the view should show refresh progress. + */ + public void setRefreshing(boolean refreshing) { + if (refreshing && mRefreshing != refreshing) { + // scale and show + mRefreshing = refreshing; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = mSpinnerOffsetEnd + mOriginalOffsetTop; + } else { + endTarget = mSpinnerOffsetEnd; + } + setTargetOffsetTopAndBottom(endTarget - mCurrentTargetOffsetTop); + mNotify = false; + mRefreshView.setVisibility(View.VISIBLE); + onRefreshAnimationEnd(); + } else { + setRefreshing(refreshing, false /* notify */); + } + } + + + private void setRefreshing(boolean refreshing, final boolean notify) { + if (mRefreshing != refreshing) { + mNotify = notify; + ensureTarget(); + mRefreshing = refreshing; + if (mRefreshing) { + animateOffsetToCorrectPosition(mCurrentTargetOffsetTop, mRefreshListener); + } else { + onRefreshAnimationEnd(); + } + } + } + + /** + * @return Whether the SwipeRefreshWidget is actively showing refresh + * progress. + */ + public boolean isRefreshing() { + return mRefreshing; + } + + private void ensureTarget() { + // Don't bother getting the parent height if the parent hasn't been laid + // out yet. + if (mTarget == null) { + for (int i = 0; i < getChildCount(); i++) { + View child = getChildAt(i); + if (!child.equals(mRefreshView)) { + mTarget = child; + break; + } + } + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, int bottom) { + final int width = getMeasuredWidth(); + final int height = getMeasuredHeight(); + if (getChildCount() == 0) { + return; + } + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + + int circleWidth = mRefreshView.getMeasuredWidth(); + int circleHeight = mRefreshView.getMeasuredHeight(); + + mRefreshView.layout((width / 2 - circleWidth / 2), mCurrentTargetOffsetTop, + (width / 2 + circleWidth / 2), mCurrentTargetOffsetTop + circleHeight); + + final View child = mTarget; + final int childLeft = getPaddingLeft(); + final int childTop = getPaddingTop() + mRefreshView.getBottom(); + final int childWidth = width - getPaddingLeft() - getPaddingRight(); + final int childHeight = height - getPaddingTop() - getPaddingBottom(); + child.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (mTarget == null) { + ensureTarget(); + } + if (mTarget == null) { + return; + } + mTarget.measure(MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec( + getMeasuredHeight() - getPaddingTop() - getPaddingBottom(), MeasureSpec.EXACTLY)); + mRefreshView.measure( + MeasureSpec.makeMeasureSpec( + getMeasuredWidth() - getPaddingLeft() - getPaddingRight(), + MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec( + (getMeasuredHeight() - getPaddingTop() - getPaddingBottom()) / 3, + MeasureSpec.UNSPECIFIED)); + if (mPullDownHeight != mRefreshView.getMeasuredHeight()) { + setPullDownHeight(mRefreshView.getMeasuredHeight()); + } + mCircleViewIndex = -1; + // Get the index of the circleview. + for (int index = 0; index < getChildCount(); index++) { + if (getChildAt(index) == mRefreshView) { + mCircleViewIndex = index; + break; + } + } + } + + /** + * @return Whether it is possible for the child view of this layout to + * scroll up. Override this if the child view is a custom view. + */ + public boolean canChildScrollUp() { + if (mChildScrollUpCallback != null) { + return mChildScrollUpCallback.canChildScrollUp(this, mTarget); + } + if (mTarget instanceof ListView) { + return ListViewCompat.canScrollList((ListView) mTarget, -1); + } + return mTarget.canScrollVertically(-1); + } + + /** + * Set a callback to override {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method. Non-null + * callback will return the value provided by the callback and ignore all internal logic. + * + * @param callback Callback that should be called when canChildScrollUp() is called. + */ + public void setOnChildScrollUpCallback(@Nullable OnChildScrollUpCallback callback) { + mChildScrollUpCallback = callback; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + ensureTarget(); + + final int action = ev.getActionMasked(); + int pointerIndex; + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp() + || mRefreshing || mNestedScrollInProgress) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + setTargetOffsetTopAndBottom(mOriginalOffsetTop - mRefreshView.getTop()); + mActivePointerId = ev.getPointerId(0); + mIsBeingDragged = false; + + pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex < 0) { + return false; + } + mInitialDownY = ev.getY(pointerIndex); + break; + + case MotionEvent.ACTION_MOVE: + if (mActivePointerId == INVALID_POINTER) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but don't have an active pointer id."); + return false; + } + + pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex < 0) { + return false; + } + final float y = ev.getY(pointerIndex); + startDragging(y); + break; + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + break; + } + + return mIsBeingDragged; + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean b) { + // if this is a List < L or another view that doesn't support nested + // scrolling, ignore this request so that the vertical scroll event + // isn't stolen + if ((android.os.Build.VERSION.SDK_INT < 21 && mTarget instanceof AbsListView) + || (mTarget != null && !ViewCompat.isNestedScrollingEnabled(mTarget))) { + // Nope. + } else { + super.requestDisallowInterceptTouchEvent(b); + } + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return isEnabled() && !mReturningToStart && !mRefreshing + && (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int axes) { + // Reset the counter of how much leftover scroll needs to be consumed. + mNestedScrollingParentHelper.onNestedScrollAccepted(child, target, axes); + // Dispatch up to the nested parent + startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL); + mTotalUnconsumed = 0; + mNestedScrollInProgress = true; + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + // If we are in the middle of consuming, a scroll, then we want to move the spinner back up + // before allowing the list to scroll + if (dy > 0 && mTotalUnconsumed > 0) { + if (dy > mTotalUnconsumed) { + consumed[1] = dy - (int) mTotalUnconsumed; + mTotalUnconsumed = 0; + } else { + if (dy > 3) { + mTotalUnconsumed -= dy; + consumed[1] = dy; + } + } + moveSpinner(mTotalUnconsumed); + } + + // If a client layout is using a custom start position for the circle + // view, they mean to hide it again before scrolling the child view + // If we get back to mTotalUnconsumed == 0 and there is more to go, hide + // the circle so it isn't exposed if its blocking content is moved + if (mUsingCustomStart && dy > 0 && mTotalUnconsumed == 0 + && Math.abs(dy - consumed[1]) > 0) { + mRefreshView.setVisibility(View.GONE); + } + + // Now let our nested parent consume the leftovers + final int[] parentConsumed = mParentScrollConsumed; + if (dispatchNestedPreScroll(dx - consumed[0], dy - consumed[1], parentConsumed, null)) { + consumed[0] += parentConsumed[0]; + consumed[1] += parentConsumed[1]; + } + } + + @Override + public int getNestedScrollAxes() { + return mNestedScrollingParentHelper.getNestedScrollAxes(); + } + + @Override + public void onStopNestedScroll(View target) { + mNestedScrollingParentHelper.onStopNestedScroll(target); + mNestedScrollInProgress = false; + // Finish the spinner for nested scrolling if we ever consumed any + // unconsumed nested scroll + if (mTotalUnconsumed > 0) { + finishSpinner(mTotalUnconsumed); + mTotalUnconsumed = 0; + } + // Dispatch up our nested parent + stopNestedScroll(); + } + + @Override + public void onNestedScroll(final View target, final int dxConsumed, final int dyConsumed, + final int dxUnconsumed, final int dyUnconsumed) { + // Dispatch up to the nested parent first + dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + mParentOffsetInWindow); + + // This is a bit of a hack. Nested scrolling works from the bottom up, and as we are + // sometimes between two nested scrolling views, we need a way to be able to know when any + // nested scrolling parent has stopped handling events. We do that by using the + // 'offset in window 'functionality to see if we have been moved from the event. + // This is a decent indication of whether we should take over the event stream or not. + final int dy = dyUnconsumed + mParentOffsetInWindow[1]; + if (dy < 0 && !canChildScrollUp()) { + mTotalUnconsumed += Math.abs(dy); + moveSpinner(mTotalUnconsumed); + } + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mNestedScrollingChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mNestedScrollingChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mNestedScrollingChildHelper.startNestedScroll(axes); + } + + @Override + public void stopNestedScroll() { + mNestedScrollingChildHelper.stopNestedScroll(); + } + + @Override + public boolean hasNestedScrollingParent() { + return mNestedScrollingChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mNestedScrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, + dxUnconsumed, dyUnconsumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mNestedScrollingChildHelper.dispatchNestedPreScroll( + dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, + float velocityY) { + return dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, + boolean consumed) { + return dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mNestedScrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mNestedScrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + private boolean isAnimationRunning(Animation animation) { + return animation != null && animation.hasStarted() && !animation.hasEnded(); + } + + private void moveSpinner(float overscrollTop) { + float originalDragPercent = overscrollTop / mTotalDragDistance; + + float dragPercent = Math.min(1f, Math.abs(originalDragPercent)); + float extraOS = Math.abs(overscrollTop) - mTotalDragDistance; + float slingshotDist = mCustomSlingshotDistance > 0 + ? mCustomSlingshotDistance + : (mUsingCustomStart + ? mSpinnerOffsetEnd - mOriginalOffsetTop + : mSpinnerOffsetEnd); + float tensionSlingshotPercent = Math.max(0, Math.min(extraOS, slingshotDist * 2) + / slingshotDist); + float tensionPercent = (float) ((tensionSlingshotPercent / 4) - Math.pow( + (tensionSlingshotPercent / 4), 2)) * 2f; + float extraMove = (slingshotDist) * tensionPercent * 2; + + int targetY = mOriginalOffsetTop + (int) ((slingshotDist * dragPercent) + extraMove); + // where 1.0f is a full circle + if (mRefreshView.getVisibility() != View.VISIBLE) { + mRefreshView.setVisibility(View.VISIBLE); + } + mRefreshView.setScaleX(1f); + mRefreshView.setScaleY(1f); + + setTargetOffsetTopAndBottom(targetY - mCurrentTargetOffsetTop); + } + + private void finishSpinner(float overscrollTop) { + if (overscrollTop > mTotalDragDistance) { + setRefreshing(true, true /* notify */); + } else { + // cancel refresh + mRefreshing = false; + animateOffsetToStartPosition(mCurrentTargetOffsetTop, null); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + final int action = ev.getActionMasked(); + int pointerIndex = -1; + + if (mReturningToStart && action == MotionEvent.ACTION_DOWN) { + mReturningToStart = false; + } + + if (!isEnabled() || mReturningToStart || canChildScrollUp() + || mRefreshing || mNestedScrollInProgress) { + // Fail fast if we're not in a state where a swipe is possible + return false; + } + + switch (action) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + mIsBeingDragged = false; + break; + + case MotionEvent.ACTION_MOVE: { + pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex < 0) { + Log.e(LOG_TAG, "Got ACTION_MOVE event but have an invalid active pointer id."); + return false; + } + + final float y = ev.getY(pointerIndex); + startDragging(y); + + if (mIsBeingDragged) { + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + if (overscrollTop > 0) { + moveSpinner(overscrollTop); + } else { + return false; + } + } + break; + } + case MotionEvent.ACTION_POINTER_DOWN: { + pointerIndex = ev.getActionIndex(); + if (pointerIndex < 0) { + Log.e(LOG_TAG, + "Got ACTION_POINTER_DOWN event but have an invalid action index."); + return false; + } + mActivePointerId = ev.getPointerId(pointerIndex); + break; + } + + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + + case MotionEvent.ACTION_UP: { + pointerIndex = ev.findPointerIndex(mActivePointerId); + if (pointerIndex < 0) { + Log.e(LOG_TAG, "Got ACTION_UP event but don't have an active pointer id."); + return false; + } + + if (mIsBeingDragged) { + final float y = ev.getY(pointerIndex); + final float overscrollTop = (y - mInitialMotionY) * DRAG_RATE; + mIsBeingDragged = false; + finishSpinner(overscrollTop); + } + mActivePointerId = INVALID_POINTER; + return false; + } + case MotionEvent.ACTION_CANCEL: + return false; + } + + return true; + } + + private void startDragging(float y) { + final float yDiff = y - mInitialDownY; + if (yDiff > mTouchSlop && !mIsBeingDragged) { + mInitialMotionY = mInitialDownY + mTouchSlop; + mIsBeingDragged = true; + } + } + + private void animateOffsetToCorrectPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToCorrectPosition.reset(); + mAnimateToCorrectPosition.setDuration(ANIMATE_TO_TRIGGER_DURATION); + mAnimateToCorrectPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mRefreshView.setAnimationListener(listener); + } + mRefreshView.clearAnimation(); + mRefreshView.startAnimation(mAnimateToCorrectPosition); + } + + private void animateOffsetToStartPosition(int from, AnimationListener listener) { + mFrom = from; + mAnimateToStartPosition.reset(); + mAnimateToStartPosition.setDuration(ANIMATE_TO_START_DURATION); + mAnimateToStartPosition.setInterpolator(mDecelerateInterpolator); + if (listener != null) { + mRefreshView.setAnimationListener(listener); + } + mRefreshView.clearAnimation(); + mRefreshView.startAnimation(mAnimateToStartPosition); + } + + private final Animation mAnimateToCorrectPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + int targetTop = 0; + int endTarget = 0; + if (!mUsingCustomStart) { + endTarget = mSpinnerOffsetEnd - Math.abs(mOriginalOffsetTop); + } else { + endTarget = mSpinnerOffsetEnd; + } + targetTop = (mFrom + (int) ((endTarget - mFrom) * interpolatedTime)); + int offset = targetTop - mRefreshView.getTop(); + setTargetOffsetTopAndBottom(offset); + } + }; + + void moveToStart(float interpolatedTime) { + int targetTop = 0; + targetTop = (mFrom + (int) ((mOriginalOffsetTop - mFrom) * interpolatedTime)); + int offset = targetTop - mRefreshView.getTop(); + setTargetOffsetTopAndBottom(offset); + } + + private final Animation mAnimateToStartPosition = new Animation() { + @Override + public void applyTransformation(float interpolatedTime, Transformation t) { + moveToStart(interpolatedTime); + } + }; + + + void setTargetOffsetTopAndBottom(int offset) { + mRefreshView.bringToFront(); + ViewCompat.offsetTopAndBottom(mRefreshView, offset); + mCurrentTargetOffsetTop = mRefreshView.getTop(); + if (mRefreshView.getMeasuredHeight() > 0) { + mRefreshView.setPullingDistance(DoricUtils.px2dp(mRefreshView.getBottom())); + } + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + } + } + + /** + * Classes that wish to be notified when the swipe gesture correctly + * triggers a refresh should implement this interface. + */ + public interface OnRefreshListener { + /** + * Called when a swipe gesture triggers a refresh. + */ + void onRefresh(); + } + + /** + * Classes that wish to override {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method + * behavior should implement this interface. + */ + public interface OnChildScrollUpCallback { + /** + * Callback that will be called when {@link androidx.swiperefreshlayout.widget.SwipeRefreshLayout#canChildScrollUp()} method + * is called to allow the implementer to override its behavior. + * + * @param parent SwipeRefreshLayout that this callback is overriding. + * @param child The child view of SwipeRefreshLayout. + * @return Whether it is possible for the child view of parent layout to scroll up. + */ + boolean canChildScrollUp(@NonNull DoricSwipeLayout parent, @Nullable View child); + } + + +} \ No newline at end of file diff --git a/doric/src/main/java/pub/doric/refresh/PullingListener.java b/doric/src/main/java/pub/doric/refresh/PullingListener.java new file mode 100644 index 00000000..16a2c69d --- /dev/null +++ b/doric/src/main/java/pub/doric/refresh/PullingListener.java @@ -0,0 +1,20 @@ +package pub.doric.refresh; + +/** + * @Description: pub.doric.pullable + * @Author: pengfei.zhou + * @CreateDate: 2019-11-25 + */ +public interface PullingListener { + + void startAnimation(); + + void stopAnimation(); + + /** + * Set the amount of rotation to apply to the progress spinner. + * + * @param rotation Rotation is from [0..2] + */ + void setPullingDistance(float rotation); +} diff --git a/doric/src/main/java/pub/doric/refresh/RefreshableNode.java b/doric/src/main/java/pub/doric/refresh/RefreshableNode.java new file mode 100644 index 00000000..a073803b --- /dev/null +++ b/doric/src/main/java/pub/doric/refresh/RefreshableNode.java @@ -0,0 +1,196 @@ +package pub.doric.refresh; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; + +/** + * @Description: pub.doric.pullable + * @Author: pengfei.zhou + * @CreateDate: 2019-11-26 + */ +@DoricPlugin(name = "Refreshable") +public class RefreshableNode extends SuperNode implements PullingListener { + + private String mContentViewId; + private ViewNode mContentNode; + + private String mHeaderViewId; + private ViewNode mHeaderNode; + + public RefreshableNode(DoricContext doricContext) { + super(doricContext); + } + + + @Override + protected DoricSwipeLayout build() { + DoricSwipeLayout doricSwipeLayout = new DoricSwipeLayout(getContext()); + doricSwipeLayout.getRefreshView().setPullingListener(this); + return doricSwipeLayout; + } + + @Override + protected void blend(DoricSwipeLayout view, String name, JSValue prop) { + if ("content".equals(name)) { + mContentViewId = prop.asString().value(); + } else if ("header".equals(name)) { + mHeaderViewId = prop.asString().value(); + } else if ("onRefresh".equals(name)) { + final String funcId = prop.asString().value(); + mView.setOnRefreshListener(new DoricSwipeLayout.OnRefreshListener() { + @Override + public void onRefresh() { + callJSResponse(funcId); + } + }); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + blendContentNode(); + blendHeadNode(); + } + + + private void blendContentNode() { + JSObject contentModel = getSubModel(mContentViewId); + if (contentModel == null) { + return; + } + String viewId = contentModel.getProperty("id").asString().value(); + String type = contentModel.getProperty("type").asString().value(); + JSObject props = contentModel.getProperty("props").asObject(); + if (mContentNode != null) { + if (mContentNode.getId().equals(viewId)) { + //skip + } else { + if (mReusable && type.equals(mContentNode.getType())) { + mContentNode.setId(viewId); + mContentNode.blend(props); + } else { + mView.removeAllViews(); + mContentNode = ViewNode.create(getDoricContext(), type); + mContentNode.setId(viewId); + mContentNode.init(this); + mContentNode.blend(props); + mView.addView(mContentNode.getNodeView()); + } + } + } else { + mContentNode = ViewNode.create(getDoricContext(), type); + mContentNode.setId(viewId); + mContentNode.init(this); + mContentNode.blend(props); + mView.addView(mContentNode.getNodeView()); + } + } + + private void blendHeadNode() { + JSObject headerModel = getSubModel(mHeaderViewId); + if (headerModel == null) { + return; + } + String viewId = headerModel.getProperty("id").asString().value(); + String type = headerModel.getProperty("type").asString().value(); + JSObject props = headerModel.getProperty("props").asObject(); + if (mHeaderNode != null) { + if (mHeaderNode.getId().equals(viewId)) { + //skip + } else { + if (mReusable && type.equals(mHeaderNode.getType())) { + mHeaderNode.setId(viewId); + mHeaderNode.blend(props); + } else { + mHeaderNode = ViewNode.create(getDoricContext(), type); + mHeaderNode.setId(viewId); + mHeaderNode.init(this); + mHeaderNode.blend(props); + mView.getRefreshView().setContent(mHeaderNode.getNodeView()); + } + } + } else { + mHeaderNode = ViewNode.create(getDoricContext(), type); + mHeaderNode.setId(viewId); + mHeaderNode.init(this); + mHeaderNode.blend(props); + mView.getRefreshView().setContent(mHeaderNode.getNodeView()); + } + } + + @Override + public ViewNode getSubNodeById(String id) { + if (id.equals(mContentViewId)) { + return mContentNode; + } + if (id.equals(mHeaderViewId)) { + return mHeaderNode; + } + return null; + } + + @Override + protected void blendSubNode(JSObject subProperties) { + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } + } + + @DoricMethod + public void setRefreshable(JSValue jsValue, DoricPromise doricPromise) { + boolean refreshable = jsValue.asBoolean().value(); + this.mView.setEnabled(refreshable); + doricPromise.resolve(); + } + + @DoricMethod + public void setRefreshing(JSValue jsValue, DoricPromise doricPromise) { + boolean refreshing = jsValue.asBoolean().value(); + this.mView.setRefreshing(refreshing); + doricPromise.resolve(); + } + + @DoricMethod + public void isRefreshable(DoricPromise doricPromise) { + doricPromise.resolve(new JavaValue(this.mView.isEnabled())); + } + + @DoricMethod + public void isRefreshing(DoricPromise doricPromise) { + doricPromise.resolve(new JavaValue(this.mView.isRefreshing())); + } + + @Override + public void startAnimation() { + if (mHeaderNode != null) { + mHeaderNode.callJSResponse("startAnimation"); + } + } + + @Override + public void stopAnimation() { + if (mHeaderNode != null) { + mHeaderNode.callJSResponse("stopAnimation"); + } + } + + @Override + public void setPullingDistance(float rotation) { + if (mHeaderNode != null) { + mHeaderNode.callJSResponse("setPullingDistance", rotation); + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/DoricLayer.java b/doric/src/main/java/pub/doric/shader/DoricLayer.java new file mode 100644 index 00000000..6da46ddd --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/DoricLayer.java @@ -0,0 +1,161 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.graphics.Region; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * @Description: com.github.penfeizhou.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-07-31 + */ +public class DoricLayer extends FrameLayout { + private Path mCornerPath = new Path(); + private Paint mShadowPaint; + private Paint mBorderPaint; + private RectF mRect = new RectF(); + private float[] mCornerRadii; + + public DoricLayer(@NonNull Context context) { + super(context); + } + + public DoricLayer(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public DoricLayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public DoricLayer(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + } + + @Override + protected boolean drawChild(Canvas canvas, View child, long drawingTime) { + return super.drawChild(canvas, child, drawingTime); + } + + @Override + protected void dispatchDraw(Canvas canvas) { + mRect.left = 0; + mRect.right = getWidth(); + mRect.top = 0; + mRect.bottom = getHeight(); + canvas.save(); + if (mCornerRadii != null) { + mCornerPath.reset(); + mCornerPath.addRoundRect(mRect, mCornerRadii, Path.Direction.CW); + canvas.clipPath(mCornerPath); + } + + super.dispatchDraw(canvas); + canvas.restore(); + // draw border + if (mBorderPaint != null) { + ((ViewGroup) getParent()).setClipChildren(false); + if (mCornerRadii != null) { + canvas.drawRoundRect(mRect, mCornerRadii[0], mCornerRadii[1], mBorderPaint); + } else { + canvas.drawRect(mRect, mBorderPaint); + } + } + if (mShadowPaint != null) { + ((ViewGroup) getParent()).setClipChildren(false); + canvas.save(); + if (mCornerRadii != null) { + canvas.clipPath(mCornerPath, Region.Op.DIFFERENCE); + canvas.drawRoundRect(mRect, mCornerRadii[0], mCornerRadii[1], mShadowPaint); + } else { + canvas.clipRect(mRect, Region.Op.DIFFERENCE); + canvas.drawRect(mRect, mShadowPaint); + } + canvas.restore(); + } + } + + public void setShadow(int sdColor, int sdOpacity, int sdRadius, int offsetX, int offsetY) { + if (mShadowPaint == null) { + mShadowPaint = new Paint(); + mShadowPaint.setAntiAlias(true); + mShadowPaint.setStyle(Paint.Style.FILL); + } + mShadowPaint.setColor(sdColor); + mShadowPaint.setAlpha(sdOpacity); + mShadowPaint.setShadowLayer(sdRadius, offsetX, offsetY, sdColor); + } + + public void setBorder(int borderWidth, int borderColor) { + if (borderWidth == 0) { + mBorderPaint = null; + } + if (mBorderPaint == null) { + mBorderPaint = new Paint(); + mBorderPaint.setAntiAlias(true); + mBorderPaint.setStyle(Paint.Style.STROKE); + } + mBorderPaint.setStrokeWidth(borderWidth); + mBorderPaint.setColor(borderColor); + } + + public void setCornerRadius(int corner) { + setCornerRadius(corner, corner, corner, corner); + } + + public void setCornerRadius(int leftTop, int rightTop, int rightBottom, int leftBottom) { + mCornerRadii = new float[]{ + leftTop, leftTop, + rightTop, rightTop, + rightBottom, rightBottom, + leftBottom, leftBottom, + }; + } + + public float getCornerRadius() { + if (mCornerRadii != null) { + return mCornerRadii[0]; + } + return 0; + } + +} diff --git a/doric/src/main/java/pub/doric/shader/GroupNode.java b/doric/src/main/java/pub/doric/shader/GroupNode.java new file mode 100644 index 00000000..bf6a958a --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/GroupNode.java @@ -0,0 +1,157 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.view.ViewGroup; + +import pub.doric.DoricContext; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import java.util.ArrayList; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public abstract class GroupNode extends SuperNode { + private ArrayList mChildNodes = new ArrayList<>(); + private ArrayList mChildViewIds = new ArrayList<>(); + + public GroupNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected void blend(F view, String name, JSValue prop) { + if ("children".equals(name)) { + JSArray ids = prop.asArray(); + mChildViewIds.clear(); + for (int i = 0; i < ids.size(); i++) { + mChildViewIds.add(ids.get(i).asString().value()); + } + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + configChildNode(); + } + + private void configChildNode() { + for (int idx = 0; idx < mChildViewIds.size(); idx++) { + String id = mChildViewIds.get(idx); + JSObject model = getSubModel(id); + String type = model.getProperty("type").asString().value(); + if (idx < mChildNodes.size()) { + ViewNode oldNode = mChildNodes.get(idx); + if (id.equals(oldNode.getId())) { + //The same,skip + } else { + if (mReusable) { + if (oldNode.getType().equals(type)) { + //Same type,can be reused + oldNode.setId(id); + oldNode.blend(model.getProperty("props").asObject()); + } else { + //Replace this view + mChildNodes.remove(idx); + mView.removeView(oldNode.getNodeView()); + ViewNode newNode = ViewNode.create(getDoricContext(), type); + newNode.setId(id); + newNode.init(this); + newNode.blend(model.getProperty("props").asObject()); + mChildNodes.add(idx, newNode); + mView.addView(newNode.getNodeView(), idx, newNode.getLayoutParams()); + } + } else { + //Find in remain nodes + int position = -1; + for (int start = idx + 1; start < mChildNodes.size(); start++) { + ViewNode node = mChildNodes.get(start); + if (id.equals(node.getId())) { + //Found + position = start; + break; + } + } + if (position >= 0) { + //Found swap idx,position + ViewNode reused = mChildNodes.remove(position); + ViewNode abandoned = mChildNodes.remove(idx); + mChildNodes.set(idx, reused); + mChildNodes.set(position, abandoned); + //View swap index + mView.removeView(reused.getNodeView()); + mView.addView(reused.getNodeView(), idx); + mView.removeView(abandoned.getNodeView()); + mView.addView(abandoned.getNodeView(), position); + } else { + //Not found,insert + ViewNode newNode = ViewNode.create(getDoricContext(), type); + newNode.setId(id); + newNode.init(this); + newNode.blend(model.getProperty("props").asObject()); + + mChildNodes.add(idx, newNode); + mView.addView(newNode.getNodeView(), idx, newNode.getLayoutParams()); + } + } + } + } else { + //Insert + ViewNode newNode = ViewNode.create(getDoricContext(), type); + newNode.setId(id); + newNode.init(this); + newNode.blend(model.getProperty("props").asObject()); + mChildNodes.add(newNode); + mView.addView(newNode.getNodeView(), idx, newNode.getLayoutParams()); + } + } + int size = mChildNodes.size(); + for (int idx = mChildViewIds.size(); idx < size; idx++) { + ViewNode viewNode = mChildNodes.remove(mChildViewIds.size()); + mView.removeView(viewNode.getNodeView()); + } + } + + @Override + protected void blendSubNode(JSObject subProp) { + String subNodeId = subProp.getProperty("id").asString().value(); + for (ViewNode node : mChildNodes) { + if (subNodeId.equals(node.getId())) { + node.blend(subProp.getProperty("props").asObject()); + break; + } + } + } + + @Override + public ViewNode getSubNodeById(String id) { + for (ViewNode node : mChildNodes) { + if (id.equals(node.getId())) { + return node; + } + } + return null; + } +} diff --git a/doric/src/main/java/pub/doric/shader/HLayoutNode.java b/doric/src/main/java/pub/doric/shader/HLayoutNode.java new file mode 100644 index 00000000..240158d8 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/HLayoutNode.java @@ -0,0 +1,42 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.widget.LinearLayout; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; + +import com.github.pengfeizhou.jscore.JSObject; + +/** + * @Description: com.github.penfeizhou.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-07-23 + */ +@DoricPlugin(name = "HLayout") +public class HLayoutNode extends LinearNode { + public HLayoutNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected LinearLayout build() { + LinearLayout linearLayout = super.build(); + linearLayout.setOrientation(LinearLayout.HORIZONTAL); + return linearLayout; + } +} diff --git a/doric/src/main/java/pub/doric/shader/ImageNode.java b/doric/src/main/java/pub/doric/shader/ImageNode.java new file mode 100644 index 00000000..9157c80d --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/ImageNode.java @@ -0,0 +1,129 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +import androidx.annotation.Nullable; + +import android.text.TextUtils; +import android.util.Base64; +import android.widget.ImageView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.Target; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.utils.DoricUtils; + +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JSValue; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +@DoricPlugin(name = "Image") +public class ImageNode extends ViewNode { + private String loadCallbackId = ""; + + public ImageNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected ImageView build() { + return new ImageView(getContext()); + } + + @Override + protected void blend(ImageView view, String name, JSValue prop) { + switch (name) { + case "imageUrl": + Glide.with(getContext()).load(prop.asString().value()) + .listener(new RequestListener() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, Object model, Target target, boolean isFirstResource) { + if (!TextUtils.isEmpty(loadCallbackId)) { + callJSResponse(loadCallbackId); + } + return false; + } + + @Override + public boolean onResourceReady(Drawable resource, Object model, Target target, DataSource dataSource, boolean isFirstResource) { + if (!TextUtils.isEmpty(loadCallbackId)) { + callJSResponse(loadCallbackId, new JSONBuilder() + .put("width", DoricUtils.px2dp(resource.getIntrinsicWidth())) + .put("height", DoricUtils.px2dp(resource.getIntrinsicHeight())) + .toJSONObject()); + } + return false; + } + }) + .into(view); + break; + case "scaleType": + int scaleType = prop.asNumber().toInt(); + switch (scaleType) { + case 1: + view.setScaleType(ImageView.ScaleType.FIT_CENTER); + break; + case 2: + view.setScaleType(ImageView.ScaleType.CENTER_CROP); + break; + default: + view.setScaleType(ImageView.ScaleType.FIT_XY); + break; + } + break; + case "loadCallback": + this.loadCallbackId = prop.asString().value(); + break; + case "imageBase64": + Pattern r = Pattern.compile("data:image/(\\S+?);base64,(\\S+)"); + Matcher m = r.matcher(prop.asString().value()); + if (m.find()) { + String imageType = m.group(1); + String base64 = m.group(2); + if (!TextUtils.isEmpty(imageType) && !TextUtils.isEmpty(base64)) { + try { + byte[] data = Base64.decode(base64, Base64.DEFAULT); + Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length); + view.setImageBitmap(bitmap); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + break; + default: + super.blend(view, name, prop); + break; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/LinearNode.java b/doric/src/main/java/pub/doric/shader/LinearNode.java new file mode 100644 index 00000000..078ebf4d --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/LinearNode.java @@ -0,0 +1,89 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.graphics.drawable.ShapeDrawable; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import pub.doric.DoricContext; +import pub.doric.utils.DoricUtils; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +/** + * @Description: com.github.penfeizhou.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-07-23 + */ +public class LinearNode extends GroupNode { + public LinearNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected void blendSubLayoutConfig(ViewNode viewNode, JSObject layoutConfig) { + super.blendSubLayoutConfig(viewNode, layoutConfig); + JSValue jsValue = layoutConfig.getProperty("alignment"); + if (jsValue.isNumber()) { + ((LinearLayout.LayoutParams) viewNode.getLayoutParams()).gravity = jsValue.asNumber().toInt(); + } + JSValue weight = layoutConfig.getProperty("weight"); + if (weight.isNumber()) { + ((LinearLayout.LayoutParams) viewNode.getLayoutParams()).weight = weight.asNumber().toInt(); + } + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new LinearLayout.LayoutParams(0, 0); + } + + @Override + protected LinearLayout build() { + return new LinearLayout(getContext()); + } + + @Override + protected void blend(LinearLayout view, String name, JSValue prop) { + switch (name) { + case "space": + ShapeDrawable shapeDrawable; + if (view.getDividerDrawable() == null) { + shapeDrawable = new ShapeDrawable(); + shapeDrawable.setAlpha(0); + view.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE); + } else { + shapeDrawable = (ShapeDrawable) view.getDividerDrawable(); + view.setDividerDrawable(null); + } + if (view.getOrientation() == LinearLayout.VERTICAL) { + shapeDrawable.setIntrinsicHeight(DoricUtils.dp2px(prop.asNumber().toFloat())); + } else { + shapeDrawable.setIntrinsicWidth(DoricUtils.dp2px(prop.asNumber().toFloat())); + } + view.setDividerDrawable(shapeDrawable); + break; + case "gravity": + view.setGravity(prop.asNumber().toInt()); + break; + default: + super.blend(view, name, prop); + break; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/RootNode.java b/doric/src/main/java/pub/doric/shader/RootNode.java new file mode 100644 index 00000000..ec8d1539 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/RootNode.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; + +import com.github.pengfeizhou.jscore.JSObject; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +@DoricPlugin(name = "Root") +public class RootNode extends StackNode { + public RootNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + public View getNodeView() { + return mView; + } + + public void setRootView(FrameLayout rootView) { + this.mView = rootView; + mLayoutParams = rootView.getLayoutParams(); + if (mLayoutParams == null) { + mLayoutParams = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + rootView.setLayoutParams(mLayoutParams); + } + } + + @Override + public ViewGroup.LayoutParams getLayoutParams() { + return mView.getLayoutParams(); + } +} diff --git a/doric/src/main/java/pub/doric/shader/ScrollerNode.java b/doric/src/main/java/pub/doric/shader/ScrollerNode.java new file mode 100644 index 00000000..18ccf135 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/ScrollerNode.java @@ -0,0 +1,101 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.widget.HVScrollView; + +/** + * @Description: pub.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-11-18 + */ +@DoricPlugin(name = "Scroller") +public class ScrollerNode extends SuperNode { + private String mChildViewId; + private ViewNode mChildNode; + + public ScrollerNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + public ViewNode getSubNodeById(String id) { + return id.equals(mChildNode.getId()) ? mChildNode : null; + } + + @Override + protected void blendSubNode(JSObject subProperties) { + if (mChildNode != null) { + mChildNode.blend(subProperties.getProperty("props").asObject()); + } + } + + @Override + protected HVScrollView build() { + return new HVScrollView(getContext()); + } + + @Override + protected void blend(HVScrollView view, String name, JSValue prop) { + if ("content".equals(name)) { + mChildViewId = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + JSObject contentModel = getSubModel(mChildViewId); + if (contentModel == null) { + return; + } + String viewId = contentModel.getProperty("id").asString().value(); + String type = contentModel.getProperty("type").asString().value(); + JSObject props = contentModel.getProperty("props").asObject(); + if (mChildNode != null) { + if (mChildNode.getId().equals(viewId)) { + //skip + } else { + if (mReusable && type.equals(mChildNode.getType())) { + mChildNode.setId(viewId); + mChildNode.blend(props); + } else { + mView.removeAllViews(); + mChildNode = ViewNode.create(getDoricContext(), type); + mChildNode.setId(viewId); + mChildNode.init(this); + mChildNode.blend(props); + mView.addView(mChildNode.getNodeView()); + } + } + } else { + mChildNode = ViewNode.create(getDoricContext(), type); + mChildNode.setId(viewId); + mChildNode.init(this); + mChildNode.blend(props); + mView.addView(mChildNode.getNodeView()); + } + } + +} diff --git a/doric/src/main/java/pub/doric/shader/StackNode.java b/doric/src/main/java/pub/doric/shader/StackNode.java new file mode 100644 index 00000000..4e235a75 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/StackNode.java @@ -0,0 +1,61 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +@DoricPlugin(name = "Stack") +public class StackNode extends GroupNode { + public StackNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) { + super.blendSubLayoutConfig(viewNode, jsObject); + JSValue jsValue = jsObject.getProperty("alignment"); + if (jsValue.isNumber()) { + ((FrameLayout.LayoutParams) viewNode.getLayoutParams()).gravity = jsValue.asNumber().toInt(); + } + } + + @Override + protected FrameLayout build() { + return new FrameLayout(getContext()); + } + + @Override + protected void blend(FrameLayout view, String name, JSValue prop) { + super.blend(view, name, prop); + } + + @Override + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new FrameLayout.LayoutParams(0, 0); + } +} diff --git a/doric/src/main/java/pub/doric/shader/SuperNode.java b/doric/src/main/java/pub/doric/shader/SuperNode.java new file mode 100644 index 00000000..bf8ed08c --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/SuperNode.java @@ -0,0 +1,180 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.view.View; +import android.view.ViewGroup; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import java.util.HashMap; +import java.util.Map; + +import pub.doric.DoricContext; +import pub.doric.utils.DoricUtils; + +/** + * @Description: pub.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-11-13 + */ +public abstract class SuperNode extends ViewNode { + private Map subNodes = new HashMap<>(); + protected boolean mReusable = false; + + public SuperNode(DoricContext doricContext) { + super(doricContext); + } + + public abstract ViewNode getSubNodeById(String id); + + protected ViewGroup.LayoutParams generateDefaultLayoutParams() { + return new ViewGroup.LayoutParams(0, 0); + } + + @Override + protected void blend(V view, String name, JSValue prop) { + if (name.equals("subviews")) { + if (prop.isArray()) { + JSArray subviews = prop.asArray(); + for (int i = 0; i < subviews.size(); i++) { + JSObject subNode = subviews.get(i).asObject(); + mixinSubNode(subNode); + blendSubNode(subNode); + } + } + } else { + super.blend(view, name, prop); + } + } + + private void mixinSubNode(JSObject subNode) { + String id = subNode.getProperty("id").asString().value(); + JSObject targetNode = subNodes.get(id); + if (targetNode == null) { + subNodes.put(id, subNode); + } else { + mixin(subNode, targetNode); + } + } + + public JSObject getSubModel(String id) { + return subNodes.get(id); + } + + public void setSubModel(String id, JSObject model) { + subNodes.put(id, model); + } + + public void clearSubModel() { + subNodes.clear(); + } + + protected abstract void blendSubNode(JSObject subProperties); + + protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) { + JSValue margin = jsObject.getProperty("margin"); + JSValue widthSpec = jsObject.getProperty("widthSpec"); + JSValue heightSpec = jsObject.getProperty("heightSpec"); + ViewGroup.LayoutParams layoutParams = viewNode.getLayoutParams(); + if (widthSpec.isNumber()) { + switch (widthSpec.asNumber().toInt()) { + case 1: + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case 2: + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + break; + } + } + if (heightSpec.isNumber()) { + switch (heightSpec.asNumber().toInt()) { + case 1: + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case 2: + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + break; + } + } + if (margin.isObject() && layoutParams instanceof ViewGroup.MarginLayoutParams) { + JSValue topVal = margin.asObject().getProperty("top"); + if (topVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).topMargin = DoricUtils.dp2px(topVal.asNumber().toFloat()); + } + JSValue leftVal = margin.asObject().getProperty("left"); + if (leftVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin = DoricUtils.dp2px(leftVal.asNumber().toFloat()); + } + JSValue rightVal = margin.asObject().getProperty("right"); + if (rightVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).rightMargin = DoricUtils.dp2px(rightVal.asNumber().toFloat()); + } + JSValue bottomVal = margin.asObject().getProperty("bottom"); + if (bottomVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = DoricUtils.dp2px(bottomVal.asNumber().toFloat()); + } + } + } + + private void mixin(JSObject src, JSObject target) { + JSObject srcProps = src.getProperty("props").asObject(); + JSObject targetProps = target.getProperty("props").asObject(); + for (String key : srcProps.propertySet()) { + JSValue jsValue = srcProps.getProperty(key); + if ("subviews".equals(key) && jsValue.isArray()) { + continue; + } + targetProps.asObject().setProperty(key, jsValue); + } + } + + private boolean viewIdIsEqual(JSObject src, JSObject target) { + String srcId = src.asObject().getProperty("id").asString().value(); + String targetId = target.asObject().getProperty("id").asString().value(); + return srcId.equals(targetId); + } + + protected void recursiveMixin(JSObject src, JSObject target) { + JSObject srcProps = src.getProperty("props").asObject(); + JSObject targetProps = target.getProperty("props").asObject(); + JSValue oriSubviews = targetProps.getProperty("subviews"); + for (String key : srcProps.propertySet()) { + JSValue jsValue = srcProps.getProperty(key); + if ("subviews".equals(key) && jsValue.isArray()) { + JSValue[] subviews = jsValue.asArray().toArray(); + for (JSValue subview : subviews) { + if (oriSubviews.isArray()) { + for (JSValue targetSubview : oriSubviews.asArray().toArray()) { + if (viewIdIsEqual(subview.asObject(), targetSubview.asObject())) { + recursiveMixin(subview.asObject(), targetSubview.asObject()); + break; + } + } + } + } + continue; + } + targetProps.asObject().setProperty(key, jsValue); + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/TextNode.java b/doric/src/main/java/pub/doric/shader/TextNode.java new file mode 100644 index 00000000..78f95906 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/TextNode.java @@ -0,0 +1,67 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.util.TypedValue; +import android.view.Gravity; +import android.widget.TextView; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; + +import com.github.pengfeizhou.jscore.JSValue; + +/** + * @Description: widget + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +@DoricPlugin(name = "Text") +public class TextNode extends ViewNode { + public TextNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected TextView build() { + TextView tv = new TextView(getContext()); + tv.setGravity(Gravity.CENTER); + return tv; + } + + @Override + protected void blend(TextView view, String name, JSValue prop) { + switch (name) { + case "text": + view.setText(prop.asString().toString()); + break; + case "textSize": + view.setTextSize(TypedValue.COMPLEX_UNIT_DIP, prop.asNumber().toFloat()); + break; + case "textColor": + view.setTextColor(prop.asNumber().toInt()); + break; + case "textAlignment": + view.setGravity(prop.asNumber().toInt() | Gravity.CENTER_VERTICAL); + break; + case "numberOfLines": + break; + default: + super.blend(view, name, prop); + break; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/VLayoutNode.java b/doric/src/main/java/pub/doric/shader/VLayoutNode.java new file mode 100644 index 00000000..6c6ba948 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/VLayoutNode.java @@ -0,0 +1,41 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.widget.LinearLayout; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; + +/** + * @Description: com.github.penfeizhou.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-07-23 + */ +@DoricPlugin(name = "VLayout") +public class VLayoutNode extends LinearNode { + public VLayoutNode(DoricContext doricContext) { + super(doricContext); + } + + @Override + protected LinearLayout build() { + LinearLayout linearLayout = super.build(); + linearLayout.setOrientation(LinearLayout.VERTICAL); + return linearLayout; + } + +} diff --git a/doric/src/main/java/pub/doric/shader/ViewNode.java b/doric/src/main/java/pub/doric/shader/ViewNode.java new file mode 100644 index 00000000..664253fc --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/ViewNode.java @@ -0,0 +1,829 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ArgbEvaluator; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.AccelerateInterpolator; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.interpolator.view.animation.LinearOutSlowInInterpolator; + +import pub.doric.DoricContext; +import pub.doric.DoricRegistry; +import pub.doric.async.AsyncResult; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPromise; +import pub.doric.utils.DoricContextHolder; +import pub.doric.utils.DoricConstant; +import pub.doric.utils.DoricLog; +import pub.doric.utils.DoricMetaInfo; +import pub.doric.utils.DoricUtils; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import java.util.LinkedList; + +/** + * @Description: Render + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public abstract class ViewNode extends DoricContextHolder { + protected T mView; + SuperNode mSuperNode; + String mId; + protected ViewGroup.LayoutParams mLayoutParams; + private String mType; + + public ViewNode(DoricContext doricContext) { + super(doricContext); + } + + private DoricLayer doricLayer; + + public void init(SuperNode superNode) { + if (this instanceof SuperNode) { + ((SuperNode) this).mReusable = superNode.mReusable; + } + this.mSuperNode = superNode; + this.mLayoutParams = superNode.generateDefaultLayoutParams(); + this.mView = build(); + this.mView.setLayoutParams(mLayoutParams); + } + + public void init(ViewGroup.LayoutParams layoutParams) { + this.mLayoutParams = layoutParams; + this.mView = build(); + this.mView.setLayoutParams(layoutParams); + } + + public void setId(String id) { + this.mId = id; + } + + public String getType() { + return mType; + } + + public View getNodeView() { + if (doricLayer != null) { + return doricLayer; + } else { + return mView; + } + } + + public Context getContext() { + return getDoricContext().getContext(); + } + + protected abstract T build(); + + public void blend(JSObject jsObject) { + if (jsObject != null) { + for (String prop : jsObject.propertySet()) { + blend(mView, prop, jsObject.getProperty(prop)); + } + } + if (doricLayer != null) { + ViewGroup.LayoutParams params = mView.getLayoutParams(); + if (params != null) { + params.width = mLayoutParams.width; + params.height = mLayoutParams.height; + } else { + params = mLayoutParams; + } + if (mLayoutParams instanceof LinearLayout.LayoutParams && ((LinearLayout.LayoutParams) mLayoutParams).weight > 0) { + if (mSuperNode instanceof VLayoutNode) { + params.height = ViewGroup.LayoutParams.MATCH_PARENT; + } else if (mSuperNode instanceof HLayoutNode) { + params.width = ViewGroup.LayoutParams.MATCH_PARENT; + } + } + + mView.setLayoutParams(params); + } + } + + protected void blend(T view, String name, JSValue prop) { + switch (name) { + case "width": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getWidth(), + prop.asNumber().toFloat())); + } else { + setWidth(prop.asNumber().toFloat()); + } + break; + case "height": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getHeight(), + prop.asNumber().toFloat())); + } else { + setHeight(prop.asNumber().toFloat()); + } + break; + case "x": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getX(), + prop.asNumber().toFloat())); + } else { + setX(prop.asNumber().toFloat()); + } + break; + case "y": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getY(), + prop.asNumber().toFloat())); + } else { + setY(prop.asNumber().toFloat()); + } + break; + case "backgroundColor": + if (isAnimating()) { + ObjectAnimator animator = ObjectAnimator.ofInt( + this, + name, + getBackgroundColor(), + prop.asNumber().toInt()); + animator.setEvaluator(new ArgbEvaluator()); + addAnimator(animator); + } else { + setBackgroundColor(prop.asNumber().toInt()); + } + break; + case "onClick": + final String functionId = prop.asString().value(); + view.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + callJSResponse(functionId); + } + }); + break; + case "layoutConfig": + setLayoutConfig(prop.asObject()); + break; + case "border": + if (prop.isObject()) { + requireDoricLayer().setBorder(DoricUtils.dp2px(prop.asObject().getProperty("width").asNumber().toFloat()), + prop.asObject().getProperty("color").asNumber().toInt()); + } + break; + case "alpha": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getAlpha(), + prop.asNumber().toFloat())); + } else { + setAlpha(prop.asNumber().toFloat()); + } + break; + case "corners": + if (prop.isNumber()) { + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getCorners(), + prop.asNumber().toFloat())); + } else { + setCorners(prop.asNumber().toFloat()); + } + } else if (prop.isObject()) { + JSValue lt = prop.asObject().getProperty("leftTop"); + JSValue rt = prop.asObject().getProperty("rightTop"); + JSValue rb = prop.asObject().getProperty("rightBottom"); + JSValue lb = prop.asObject().getProperty("leftBottom"); + requireDoricLayer().setCornerRadius( + DoricUtils.dp2px(lt.isNumber() ? lt.asNumber().toFloat() : 0), + DoricUtils.dp2px(rt.isNumber() ? rt.asNumber().toFloat() : 0), + DoricUtils.dp2px(rb.isNumber() ? rb.asNumber().toFloat() : 0), + DoricUtils.dp2px(lb.isNumber() ? lb.asNumber().toFloat() : 0) + ); + } + break; + case "shadow": + if (prop.isObject()) { + requireDoricLayer().setShadow( + prop.asObject().getProperty("color").asNumber().toInt(), + (int) (prop.asObject().getProperty("opacity").asNumber().toFloat() * 255), + DoricUtils.dp2px(prop.asObject().getProperty("radius").asNumber().toFloat()), + DoricUtils.dp2px(prop.asObject().getProperty("offsetX").asNumber().toFloat()), + DoricUtils.dp2px(prop.asObject().getProperty("offsetY").asNumber().toFloat()) + ); + } + break; + case "translationX": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getTranslationX(), + prop.asNumber().toFloat())); + } else { + setTranslationX(prop.asNumber().toFloat()); + } + break; + case "translationY": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getTranslationY(), + prop.asNumber().toFloat())); + } else { + setTranslationY(prop.asNumber().toFloat()); + } + break; + case "scaleX": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getScaleX(), + prop.asNumber().toFloat())); + } else { + setScaleX(prop.asNumber().toFloat()); + } + break; + case "scaleY": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getScaleY(), + prop.asNumber().toFloat())); + } else { + setScaleY(prop.asNumber().toFloat()); + } + break; + case "pivotX": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getPivotX(), + prop.asNumber().toFloat())); + } else { + setPivotX(prop.asNumber().toFloat()); + } + break; + case "pivotY": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getPivotY(), + prop.asNumber().toFloat())); + } else { + setPivotY(prop.asNumber().toFloat()); + } + break; + case "rotation": + if (isAnimating()) { + addAnimator(ObjectAnimator.ofFloat( + this, + name, + getRotation(), + prop.asNumber().toFloat())); + } else { + setRotation(prop.asNumber().toFloat()); + } + break; + default: + break; + } + } + + @NonNull + private DoricLayer requireDoricLayer() { + if (doricLayer == null) { + doricLayer = new DoricLayer(getContext()); + doricLayer.setLayoutParams(mLayoutParams); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mLayoutParams.width, mLayoutParams.height); + if (mView.getParent() instanceof ViewGroup) { + //Already added in + ViewGroup superview = (ViewGroup) mView.getParent(); + int index = superview.indexOfChild(mView); + superview.removeView(mView); + doricLayer.addView(mView, params); + superview.addView(doricLayer, index); + } else { + doricLayer.addView(mView, params); + } + } + return doricLayer; + } + + String[] getIdList() { + LinkedList ids = new LinkedList<>(); + ViewNode viewNode = this; + do { + ids.push(viewNode.mId); + viewNode = viewNode.mSuperNode; + } while (viewNode != null); + + return ids.toArray(new String[0]); + } + + public AsyncResult callJSResponse(String funcId, Object... args) { + final Object[] nArgs = new Object[args.length + 2]; + nArgs[0] = getIdList(); + nArgs[1] = funcId; + if (args.length > 0) { + System.arraycopy(args, 0, nArgs, 2, args.length); + } + return getDoricContext().callEntity(DoricConstant.DORIC_ENTITY_RESPONSE, nArgs); + } + + public static ViewNode create(DoricContext doricContext, String type) { + DoricRegistry registry = doricContext.getDriver().getRegistry(); + DoricMetaInfo clz = registry.acquireViewNodeInfo(type); + ViewNode ret = clz.createInstance(doricContext); + ret.mType = type; + return ret; + } + + public ViewGroup.LayoutParams getLayoutParams() { + return mLayoutParams; + } + + public String getId() { + return mId; + } + + protected void setLayoutConfig(JSObject layoutConfig) { + if (mSuperNode != null) { + mSuperNode.blendSubLayoutConfig(this, layoutConfig); + } else { + blendLayoutConfig(layoutConfig); + } + } + + private void blendLayoutConfig(JSObject jsObject) { + JSValue margin = jsObject.getProperty("margin"); + JSValue widthSpec = jsObject.getProperty("widthSpec"); + JSValue heightSpec = jsObject.getProperty("heightSpec"); + ViewGroup.LayoutParams layoutParams = getLayoutParams(); + if (widthSpec.isNumber()) { + switch (widthSpec.asNumber().toInt()) { + case 1: + layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case 2: + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + break; + } + } + if (heightSpec.isNumber()) { + switch (heightSpec.asNumber().toInt()) { + case 1: + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + break; + case 2: + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + break; + default: + break; + } + } + if (margin.isObject() && layoutParams instanceof ViewGroup.MarginLayoutParams) { + JSValue topVal = margin.asObject().getProperty("top"); + if (topVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).topMargin = DoricUtils.dp2px(topVal.asNumber().toFloat()); + } + JSValue leftVal = margin.asObject().getProperty("left"); + if (leftVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).leftMargin = DoricUtils.dp2px(leftVal.asNumber().toFloat()); + } + JSValue rightVal = margin.asObject().getProperty("right"); + if (rightVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).rightMargin = DoricUtils.dp2px(rightVal.asNumber().toFloat()); + } + JSValue bottomVal = margin.asObject().getProperty("bottom"); + if (bottomVal.isNumber()) { + ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = DoricUtils.dp2px(bottomVal.asNumber().toFloat()); + } + } + JSValue jsValue = jsObject.getProperty("alignment"); + if (jsValue.isNumber() && layoutParams instanceof FrameLayout.LayoutParams) { + ((FrameLayout.LayoutParams) layoutParams).gravity = jsValue.asNumber().toInt(); + } + } + + protected boolean isAnimating() { + return getDoricContext().getAnimatorSet() != null; + } + + protected void addAnimator(Animator animator) { + if (getDoricContext().getAnimatorSet() == null) { + return; + } + getDoricContext().getAnimatorSet().play(animator); + } + + @DoricMethod + public float getWidth() { + if (mLayoutParams.width >= 0) { + return DoricUtils.px2dp(mLayoutParams.width); + } else { + return mView.getMeasuredWidth(); + } + } + + @DoricMethod + public float getHeight() { + if (mLayoutParams.width >= 0) { + return DoricUtils.px2dp(mLayoutParams.height); + } else { + return mView.getMeasuredHeight(); + } + } + + @DoricMethod + protected void setWidth(float width) { + if (mLayoutParams.width >= 0) { + mLayoutParams.width = DoricUtils.dp2px(width); + if (mView.getLayoutParams() != mLayoutParams) { + mView.getLayoutParams().width = mLayoutParams.width; + } + getNodeView().requestLayout(); + } + } + + @DoricMethod + protected void setHeight(float height) { + if (mLayoutParams.height >= 0) { + mLayoutParams.height = DoricUtils.dp2px(height); + if (mView.getLayoutParams() != mLayoutParams) { + mView.getLayoutParams().height = mLayoutParams.height; + } + getNodeView().requestLayout(); + } + } + + @DoricMethod + protected void setX(float x) { + if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) mLayoutParams).leftMargin = DoricUtils.dp2px(x); + getNodeView().requestLayout(); + } + } + + @DoricMethod + protected void setY(float y) { + if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) { + ((ViewGroup.MarginLayoutParams) mLayoutParams).topMargin = DoricUtils.dp2px(y); + getNodeView().requestLayout(); + } + } + + @DoricMethod + public float getX() { + if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) { + return DoricUtils.px2dp(((ViewGroup.MarginLayoutParams) mLayoutParams).leftMargin); + } + return 0; + } + + @DoricMethod + public float getY() { + if (mLayoutParams instanceof ViewGroup.MarginLayoutParams) { + return DoricUtils.px2dp(((ViewGroup.MarginLayoutParams) mLayoutParams).topMargin); + } + return 0; + } + + @DoricMethod + public int getBackgroundColor() { + if (mView.getBackground() instanceof ColorDrawable) { + return ((ColorDrawable) mView.getBackground()).getColor(); + } + return Color.TRANSPARENT; + } + + @DoricMethod + public void setBackgroundColor(int color) { + mView.setBackgroundColor(color); + } + + @DoricMethod + public void setAlpha(float alpha) { + getNodeView().setAlpha(alpha); + } + + @DoricMethod + public float getAlpha() { + return getNodeView().getAlpha(); + } + + @DoricMethod + public void setCorners(float corner) { + requireDoricLayer().setCornerRadius(DoricUtils.dp2px(corner)); + getNodeView().invalidate(); + } + + @DoricMethod + public float getCorners() { + return DoricUtils.px2dp((int) requireDoricLayer().getCornerRadius()); + } + + @DoricMethod + public void setTranslationX(float v) { + getNodeView().setTranslationX(DoricUtils.dp2px(v)); + } + + @DoricMethod + public float getTranslationX() { + return DoricUtils.px2dp((int) getNodeView().getTranslationX()); + } + + @DoricMethod + public void setTranslationY(float v) { + getNodeView().setTranslationY(DoricUtils.dp2px(v)); + } + + @DoricMethod + public float getTranslationY() { + return DoricUtils.px2dp((int) getNodeView().getTranslationY()); + } + + @DoricMethod + public void setScaleX(float v) { + getNodeView().setScaleX(v); + } + + @DoricMethod + public float getScaleX() { + return getNodeView().getScaleX(); + } + + @DoricMethod + public void setScaleY(float v) { + getNodeView().setScaleY(v); + } + + @DoricMethod + public float getScaleY() { + return getNodeView().getScaleY(); + } + + @DoricMethod + public void setRotation(float rotation) { + getNodeView().setRotation(rotation * 180); + } + + @DoricMethod + public float getRotation() { + return getNodeView().getRotation() / 180; + } + + @DoricMethod + public void setPivotX(float v) { + getNodeView().setPivotX(v * getNodeView().getWidth()); + } + + @DoricMethod + public float getPivotX() { + return getNodeView().getPivotX() / getNodeView().getWidth(); + } + + @DoricMethod + public void setPivotY(float v) { + getNodeView().setPivotY(v * getNodeView().getHeight()); + } + + @DoricMethod + public float getPivotY() { + return getNodeView().getPivotY() / getNodeView().getHeight(); + } + + private String[] animatedKeys = { + "translationX", + "translationY", + "scaleX", + "scaleY", + "rotation", + }; + + @DoricMethod + public void doAnimation(JSValue value, final DoricPromise promise) { + Animator animator = parseAnimator(value); + if (animator != null) { + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + JSONBuilder jsonBuilder = new JSONBuilder(); + for (String key : animatedKeys) { + jsonBuilder.put(key, getAnimatedValue(key)); + } + promise.resolve(new JavaValue(jsonBuilder.toJSONObject())); + } + }); + animator.start(); + } + } + + private Animator parseAnimator(JSValue value) { + if (!value.isObject()) { + DoricLog.e("parseAnimator error"); + return null; + } + JSValue animations = value.asObject().getProperty("animations"); + if (animations.isArray()) { + AnimatorSet animatorSet = new AnimatorSet(); + + for (int i = 0; i < animations.asArray().size(); i++) { + animatorSet.play(parseAnimator(animations.asArray().get(i))); + } + + JSValue delayJS = value.asObject().getProperty("delay"); + if (delayJS.isNumber()) { + animatorSet.setStartDelay(delayJS.asNumber().toLong()); + } + return animatorSet; + } else if (value.isObject()) { + JSArray changeables = value.asObject().getProperty("changeables").asArray(); + AnimatorSet animatorSet = new AnimatorSet(); + + JSValue repeatCount = value.asObject().getProperty("repeatCount"); + + JSValue repeatMode = value.asObject().getProperty("repeatMode"); + JSValue fillMode = value.asObject().getProperty("fillMode"); + JSValue timingFunction = value.asObject().getProperty("timingFunction"); + for (int j = 0; j < changeables.size(); j++) { + ObjectAnimator animator = parseChangeable(changeables.get(j).asObject(), fillMode); + if (repeatCount.isNumber()) { + animator.setRepeatCount(repeatCount.asNumber().toInt()); + } + if (repeatMode.isNumber()) { + animator.setRepeatMode(repeatMode.asNumber().toInt()); + } + if (timingFunction.isNumber()) { + animator.setInterpolator(getTimingInterpolator(timingFunction.asNumber().toInt())); + } + animatorSet.play(animator); + } + long duration = value.asObject().getProperty("duration").asNumber().toLong(); + animatorSet.setDuration(duration); + JSValue delayJS = value.asObject().getProperty("delay"); + if (delayJS.isNumber()) { + animatorSet.setStartDelay(delayJS.asNumber().toLong()); + } + return animatorSet; + } else { + return null; + } + } + + private Interpolator getTimingInterpolator(int timingFunction) { + switch (timingFunction) { + case 1: + return new LinearInterpolator(); + case 2: + return new AccelerateInterpolator(); + case 3: + return new DecelerateInterpolator(); + case 4: + return new FastOutSlowInInterpolator(); + default: + return new AccelerateDecelerateInterpolator(); + } + } + + private ObjectAnimator parseChangeable(JSObject jsObject, JSValue fillMode) { + String key = jsObject.getProperty("key").asString().value(); + float startVal = jsObject.getProperty("fromValue").asNumber().toFloat(); + float endVal = jsObject.getProperty("toValue").asNumber().toFloat(); + ObjectAnimator animator = ObjectAnimator.ofFloat(this, + key, + startVal, + endVal + ); + setFillMode(animator, key, startVal, endVal, fillMode); + return animator; + } + + private void setFillMode(ObjectAnimator animator, + final String key, + float startVal, + float endVal, + JSValue jsValue) { + int fillMode = 0; + if (jsValue.isNumber()) { + fillMode = jsValue.asNumber().toInt(); + } + if ((fillMode & 2) == 2) { + setAnimatedValue(key, startVal); + } + final int finalFillMode = fillMode; + animator.addListener(new AnimatorListenerAdapter() { + private float originVal; + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + originVal = getAnimatedValue(key); + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if ((finalFillMode & 1) != 1) { + setAnimatedValue(key, originVal); + } + } + }); + } + + private void setAnimatedValue(String key, float value) { + switch (key) { + case "translationX": + setTranslationX(value); + break; + case "translationY": + setTranslationY(value); + break; + case "scaleX": + setScaleX(value); + break; + case "scaleY": + setScaleY(value); + break; + case "rotation": + setRotation(value); + break; + default: + break; + } + } + + private float getAnimatedValue(String key) { + switch (key) { + case "translationX": + return getTranslationX(); + case "translationY": + return getTranslationY(); + case "scaleX": + return getScaleX(); + case "scaleY": + return getScaleY(); + case "rotation": + return getRotation(); + default: + return 0; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/flowlayout/FlowAdapter.java b/doric/src/main/java/pub/doric/shader/flowlayout/FlowAdapter.java new file mode 100644 index 00000000..00c2f9fa --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/flowlayout/FlowAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.flowlayout; + +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSNull; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.async.AsyncResult; +import pub.doric.shader.ViewNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +class FlowAdapter extends RecyclerView.Adapter { + + private final FlowLayoutNode flowLayoutNode; + String renderItemFuncId; + int itemCount = 0; + int batchCount = 15; + SparseArray itemValues = new SparseArray<>(); + + FlowAdapter(FlowLayoutNode flowLayoutNode) { + this.flowLayoutNode = flowLayoutNode; + } + + @NonNull + @Override + public DoricViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + FlowLayoutItemNode node = (FlowLayoutItemNode) ViewNode.create(flowLayoutNode.getDoricContext(), "FlowLayoutItem"); + node.init(flowLayoutNode); + return new DoricViewHolder(node, node.getNodeView()); + } + + @Override + public void onBindViewHolder(@NonNull DoricViewHolder holder, int position) { + JSValue jsValue = getItemModel(position); + if (jsValue.isObject()) { + JSObject jsObject = jsValue.asObject(); + holder.flowLayoutItemNode.setId(jsObject.getProperty("id").asString().value()); + holder.flowLayoutItemNode.blend(jsObject.getProperty("props").asObject()); + } + } + + @Override + public int getItemCount() { + return itemCount; + } + + @Override + public int getItemViewType(int position) { + JSValue value = getItemModel(position); + if (value.isObject()) { + if (value.asObject().getProperty("identifier").isString()) { + return value.asObject().getProperty("identifier").asString().value().hashCode(); + } + } + return super.getItemViewType(position); + } + + private JSValue getItemModel(final int position) { + String id = itemValues.get(position); + if (TextUtils.isEmpty(id)) { + AsyncResult asyncResult = flowLayoutNode.callJSResponse( + "renderBunchedItems", + position, + batchCount); + try { + JSDecoder jsDecoder = asyncResult.synchronous().get(); + JSValue result = jsDecoder.decode(); + if (result.isArray()) { + JSArray jsArray = result.asArray(); + for (int i = 0; i < jsArray.size(); i++) { + JSObject itemModel = jsArray.get(i).asObject(); + String itemId = itemModel.getProperty("id").asString().value(); + itemValues.put(i + position, itemId); + flowLayoutNode.setSubModel(itemId, itemModel); + } + return flowLayoutNode.getSubModel(itemValues.get(position)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return new JSNull(); + } else { + JSObject childModel = flowLayoutNode.getSubModel(id); + if (childModel == null) { + return new JSNull(); + } else { + return childModel; + } + } + } + + + void blendSubNode(JSObject subProperties) { + for (int i = 0; i < itemValues.size(); i++) { + if (subProperties.getProperty("id").asString().value().equals(itemValues.valueAt(i))) { + notifyItemChanged(i); + } + } + } + + static class DoricViewHolder extends RecyclerView.ViewHolder { + FlowLayoutItemNode flowLayoutItemNode; + + DoricViewHolder(FlowLayoutItemNode node, @NonNull View itemView) { + super(itemView); + flowLayoutItemNode = node; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutItemNode.java b/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutItemNode.java new file mode 100644 index 00000000..643f9b87 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutItemNode.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.flowlayout; + +import android.widget.FrameLayout; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.StackNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +@DoricPlugin(name = "FlowLayoutItem") +public class FlowLayoutItemNode extends StackNode { + public String identifier = ""; + + public FlowLayoutItemNode(DoricContext doricContext) { + super(doricContext); + this.mReusable = true; + } + + @Override + protected void blend(FrameLayout view, String name, JSValue prop) { + if ("identifier".equals(name)) { + this.identifier = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + getNodeView().getLayoutParams().width = getLayoutParams().width; + getNodeView().getLayoutParams().height = getLayoutParams().height; + } +} diff --git a/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutNode.java b/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutNode.java new file mode 100644 index 00000000..f2b46b09 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/flowlayout/FlowLayoutNode.java @@ -0,0 +1,146 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.flowlayout; + +import android.graphics.Rect; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.StaggeredGridLayoutManager; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; +import pub.doric.utils.DoricUtils; + +/** + * @Description: pub.doric.shader.flowlayout + * @Author: pengfei.zhou + * @CreateDate: 2019-11-28 + */ +@DoricPlugin(name = "FlowLayout") +public class FlowLayoutNode extends SuperNode { + private final FlowAdapter flowAdapter; + private final StaggeredGridLayoutManager staggeredGridLayoutManager = new StaggeredGridLayoutManager( + 2, + StaggeredGridLayoutManager.VERTICAL); + private int columnSpace = 0; + private int rowSpace = 0; + private final RecyclerView.ItemDecoration spacingItemDecoration = new RecyclerView.ItemDecoration() { + @Override + public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state) { + outRect.set(columnSpace / 2, rowSpace / 2, columnSpace / 2, rowSpace / 2); + } + }; + + public FlowLayoutNode(DoricContext doricContext) { + super(doricContext); + this.flowAdapter = new FlowAdapter(this); + } + + @Override + public ViewNode getSubNodeById(String id) { + RecyclerView.LayoutManager manager = mView.getLayoutManager(); + if (manager == null) { + return null; + } + for (int i = 0; i < manager.getChildCount(); i++) { + View view = manager.getChildAt(i); + if (view == null) { + continue; + } + FlowAdapter.DoricViewHolder viewHolder = (FlowAdapter.DoricViewHolder) mView.getChildViewHolder(view); + if (id.equals(viewHolder.flowLayoutItemNode.getId())) { + return viewHolder.flowLayoutItemNode; + } + } + return null; + } + + @Override + protected void blend(RecyclerView view, String name, JSValue prop) { + switch (name) { + case "columnSpace": + columnSpace = DoricUtils.dp2px(prop.asNumber().toFloat()); + mView.setPadding(-columnSpace / 2, mView.getPaddingTop(), -columnSpace / 2, mView.getPaddingBottom()); + break; + case "rowSpace": + rowSpace = DoricUtils.dp2px(prop.asNumber().toFloat()); + mView.setPadding(mView.getPaddingLeft(), -rowSpace / 2, mView.getPaddingRight(), -rowSpace / 2); + break; + case "columnCount": + staggeredGridLayoutManager.setSpanCount(prop.asNumber().toInt()); + break; + case "itemCount": + this.flowAdapter.itemCount = prop.asNumber().toInt(); + break; + case "renderItem": + this.flowAdapter.renderItemFuncId = prop.asString().value(); + // If reset renderItem,should reset native cache. + this.flowAdapter.itemValues.clear(); + clearSubModel(); + break; + case "batchCount": + this.flowAdapter.batchCount = prop.asNumber().toInt(); + break; + default: + super.blend(view, name, prop); + break; + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + if (mView != null) { + mView.post(new Runnable() { + @Override + public void run() { + flowAdapter.notifyDataSetChanged(); + } + }); + } + } + + @Override + protected void blendSubNode(JSObject subProperties) { + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } else { + JSObject oldModel = getSubModel(viewId); + if (oldModel != null) { + recursiveMixin(subProperties, oldModel); + } + flowAdapter.blendSubNode(subProperties); + } + } + + @Override + protected RecyclerView build() { + RecyclerView recyclerView = new RecyclerView(getContext()); + recyclerView.setLayoutManager(staggeredGridLayoutManager); + recyclerView.setAdapter(flowAdapter); + recyclerView.addItemDecoration(spacingItemDecoration); + return recyclerView; + } +} diff --git a/doric/src/main/java/pub/doric/shader/list/ListAdapter.java b/doric/src/main/java/pub/doric/shader/list/ListAdapter.java new file mode 100644 index 00000000..5b96a3c0 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/list/ListAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.list; + +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSNull; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.async.AsyncResult; +import pub.doric.shader.ViewNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +class ListAdapter extends RecyclerView.Adapter { + + private final ListNode listNode; + String renderItemFuncId; + int itemCount = 0; + int batchCount = 15; + SparseArray itemValues = new SparseArray<>(); + + ListAdapter(ListNode listNode) { + this.listNode = listNode; + } + + @NonNull + @Override + public DoricViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + ListItemNode node = (ListItemNode) ViewNode.create(listNode.getDoricContext(), "ListItem"); + node.init(listNode); + return new DoricViewHolder(node, node.getNodeView()); + } + + @Override + public void onBindViewHolder(@NonNull DoricViewHolder holder, int position) { + JSValue jsValue = getItemModel(position); + if (jsValue.isObject()) { + JSObject jsObject = jsValue.asObject(); + holder.listItemNode.setId(jsObject.getProperty("id").asString().value()); + holder.listItemNode.blend(jsObject.getProperty("props").asObject()); + } + } + + @Override + public int getItemCount() { + return itemCount; + } + + @Override + public int getItemViewType(int position) { + JSValue value = getItemModel(position); + if (value.isObject()) { + if (value.asObject().getProperty("identifier").isString()) { + return value.asObject().getProperty("identifier").asString().value().hashCode(); + } + } + return super.getItemViewType(position); + } + + private JSValue getItemModel(final int position) { + String id = itemValues.get(position); + if (TextUtils.isEmpty(id)) { + AsyncResult asyncResult = listNode.callJSResponse( + "renderBunchedItems", + position, + batchCount); + try { + JSDecoder jsDecoder = asyncResult.synchronous().get(); + JSValue result = jsDecoder.decode(); + if (result.isArray()) { + JSArray jsArray = result.asArray(); + for (int i = 0; i < jsArray.size(); i++) { + JSObject itemModel = jsArray.get(i).asObject(); + String itemId = itemModel.getProperty("id").asString().value(); + itemValues.put(i + position, itemId); + listNode.setSubModel(itemId, itemModel); + } + return listNode.getSubModel(itemValues.get(position)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return new JSNull(); + } else { + JSObject childModel = listNode.getSubModel(id); + if (childModel == null) { + return new JSNull(); + } else { + return childModel; + } + } + } + + + void blendSubNode(JSObject subProperties) { + for (int i = 0; i < itemValues.size(); i++) { + if (subProperties.getProperty("id").asString().value().equals(itemValues.valueAt(i))) { + notifyItemChanged(i); + } + } + } + + static class DoricViewHolder extends RecyclerView.ViewHolder { + ListItemNode listItemNode; + + DoricViewHolder(ListItemNode node, @NonNull View itemView) { + super(itemView); + listItemNode = node; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/list/ListItemNode.java b/doric/src/main/java/pub/doric/shader/list/ListItemNode.java new file mode 100644 index 00000000..7d2a4dfc --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/list/ListItemNode.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.list; + +import android.widget.FrameLayout; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.StackNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +@DoricPlugin(name = "ListItem") +public class ListItemNode extends StackNode { + public String identifier = ""; + + public ListItemNode(DoricContext doricContext) { + super(doricContext); + this.mReusable = true; + } + + @Override + protected void blend(FrameLayout view, String name, JSValue prop) { + if ("identifier".equals(name)) { + this.identifier = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + getNodeView().getLayoutParams().width = getLayoutParams().width; + getNodeView().getLayoutParams().height = getLayoutParams().height; + } +} diff --git a/doric/src/main/java/pub/doric/shader/list/ListNode.java b/doric/src/main/java/pub/doric/shader/list/ListNode.java new file mode 100644 index 00000000..3349c844 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/list/ListNode.java @@ -0,0 +1,125 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.list; + +import android.view.View; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +@DoricPlugin(name = "List") +public class ListNode extends SuperNode { + private final ListAdapter listAdapter; + + public ListNode(DoricContext doricContext) { + super(doricContext); + this.listAdapter = new ListAdapter(this); + } + + @Override + protected void blendSubNode(JSObject subProperties) { + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } else { + JSObject oldModel = getSubModel(viewId); + if (oldModel != null) { + recursiveMixin(subProperties, oldModel); + } + listAdapter.blendSubNode(subProperties); + } + } + + @Override + protected RecyclerView build() { + RecyclerView recyclerView = new RecyclerView(getContext()); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(this.listAdapter); + return recyclerView; + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + if (mView != null) { + mView.post(new Runnable() { + @Override + public void run() { + listAdapter.notifyDataSetChanged(); + } + }); + } + } + + @Override + protected void blend(RecyclerView view, String name, JSValue prop) { + switch (name) { + case "itemCount": + this.listAdapter.itemCount = prop.asNumber().toInt(); + break; + case "renderItem": + this.listAdapter.renderItemFuncId = prop.asString().value(); + // If reset renderItem,should reset native cache. + this.listAdapter.itemValues.clear(); + clearSubModel(); + break; + case "batchCount": + this.listAdapter.batchCount = prop.asNumber().toInt(); + break; + default: + super.blend(view, name, prop); + break; + } + } + + @Override + protected void blendSubLayoutConfig(ViewNode viewNode, JSObject jsObject) { + super.blendSubLayoutConfig(viewNode, jsObject); + } + + @Override + public ViewNode getSubNodeById(String id) { + RecyclerView.LayoutManager manager = mView.getLayoutManager(); + if (manager == null) { + return null; + } + for (int i = 0; i < manager.getChildCount(); i++) { + View view = manager.getChildAt(i); + if (view == null) { + continue; + } + ListAdapter.DoricViewHolder viewHolder = (ListAdapter.DoricViewHolder) mView.getChildViewHolder(view); + if (id.equals(viewHolder.listItemNode.getId())) { + return viewHolder.listItemNode; + } + } + return null; + } +} diff --git a/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java b/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java new file mode 100644 index 00000000..f68d38ba --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/slider/SlideAdapter.java @@ -0,0 +1,136 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.slider; + +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSNull; +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.async.AsyncResult; +import pub.doric.shader.ViewNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +class SlideAdapter extends RecyclerView.Adapter { + + private final SliderNode sliderNode; + int itemCount = 0; + int batchCount = 3; + SparseArray itemValues = new SparseArray<>(); + + SlideAdapter(SliderNode sliderNode) { + this.sliderNode = sliderNode; + } + + @NonNull + @Override + public DoricViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + SlideItemNode node = (SlideItemNode) ViewNode.create(sliderNode.getDoricContext(), "SlideItem"); + node.init(sliderNode); + return new DoricViewHolder(node, node.getNodeView()); + } + + @Override + public void onBindViewHolder(@NonNull DoricViewHolder holder, int position) { + JSValue jsValue = getItemModel(position); + if (jsValue.isObject()) { + JSObject jsObject = jsValue.asObject(); + holder.slideItemNode.setId(jsObject.getProperty("id").asString().value()); + holder.slideItemNode.blend(jsObject.getProperty("props").asObject()); + } + } + + @Override + public int getItemCount() { + return itemCount; + } + + @Override + public int getItemViewType(int position) { + JSValue value = getItemModel(position); + if (value.isObject()) { + if (value.asObject().getProperty("identifier").isString()) { + return value.asObject().getProperty("identifier").asString().value().hashCode(); + } + } + return super.getItemViewType(position); + } + + private JSValue getItemModel(final int position) { + String id = itemValues.get(position); + if (TextUtils.isEmpty(id)) { + AsyncResult asyncResult = sliderNode.callJSResponse( + "renderBunchedItems", + position, + batchCount); + try { + JSDecoder jsDecoder = asyncResult.synchronous().get(); + JSValue result = jsDecoder.decode(); + if (result.isArray()) { + JSArray jsArray = result.asArray(); + for (int i = 0; i < jsArray.size(); i++) { + JSObject itemModel = jsArray.get(i).asObject(); + String itemId = itemModel.getProperty("id").asString().value(); + itemValues.put(i + position, itemId); + sliderNode.setSubModel(itemId, itemModel); + } + return sliderNode.getSubModel(itemValues.get(position)); + } + } catch (Exception e) { + e.printStackTrace(); + } + return new JSNull(); + } else { + JSObject childModel = sliderNode.getSubModel(id); + if (childModel == null) { + return new JSNull(); + } else { + return childModel; + } + } + } + + + void blendSubNode(JSObject subProperties) { + for (int i = 0; i < itemValues.size(); i++) { + if (subProperties.getProperty("id").asString().value().equals(itemValues.valueAt(i))) { + notifyItemChanged(i); + } + } + } + + static class DoricViewHolder extends RecyclerView.ViewHolder { + SlideItemNode slideItemNode; + + DoricViewHolder(SlideItemNode node, @NonNull View itemView) { + super(itemView); + slideItemNode = node; + } + } +} diff --git a/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java b/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java new file mode 100644 index 00000000..dbd25c9e --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/slider/SlideItemNode.java @@ -0,0 +1,56 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.slider; + +import android.widget.FrameLayout; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.StackNode; + +/** + * @Description: com.github.penfeizhou.doric.widget + * @Author: pengfei.zhou + * @CreateDate: 2019-11-12 + */ +@DoricPlugin(name = "SlideItem") +public class SlideItemNode extends StackNode { + public String identifier = ""; + + public SlideItemNode(DoricContext doricContext) { + super(doricContext); + this.mReusable = true; + } + + @Override + protected void blend(FrameLayout view, String name, JSValue prop) { + if ("identifier".equals(name)) { + this.identifier = prop.asString().value(); + } else { + super.blend(view, name, prop); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + getNodeView().getLayoutParams().width = getLayoutParams().width; + getNodeView().getLayoutParams().height = getLayoutParams().height; + } +} diff --git a/doric/src/main/java/pub/doric/shader/slider/SliderNode.java b/doric/src/main/java/pub/doric/shader/slider/SliderNode.java new file mode 100644 index 00000000..8cfceaa5 --- /dev/null +++ b/doric/src/main/java/pub/doric/shader/slider/SliderNode.java @@ -0,0 +1,125 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.shader.slider; + +import android.view.View; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.PagerSnapHelper; +import androidx.recyclerview.widget.RecyclerView; + +import com.github.pengfeizhou.jscore.JSObject; +import com.github.pengfeizhou.jscore.JSValue; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricPlugin; +import pub.doric.shader.SuperNode; +import pub.doric.shader.ViewNode; + +/** + * @Description: pub.doric.shader + * @Author: pengfei.zhou + * @CreateDate: 2019-11-19 + */ +@DoricPlugin(name = "Slider") +public class SliderNode extends SuperNode { + private final SlideAdapter slideAdapter; + + public SliderNode(DoricContext doricContext) { + super(doricContext); + this.slideAdapter = new SlideAdapter(this); + } + + @Override + protected RecyclerView build() { + RecyclerView recyclerView = new RecyclerView(getContext()); + + LinearLayoutManager layoutManager = new LinearLayoutManager(getContext()); + layoutManager.setOrientation(LinearLayoutManager.HORIZONTAL); + recyclerView.setLayoutManager(layoutManager); + PagerSnapHelper mPagerSnapHelper = new PagerSnapHelper(); + mPagerSnapHelper.attachToRecyclerView(recyclerView); + recyclerView.setAdapter(this.slideAdapter); + return recyclerView; + } + + @Override + public ViewNode getSubNodeById(String id) { + RecyclerView.LayoutManager manager = mView.getLayoutManager(); + if (manager == null) { + return null; + } + for (int i = 0; i < manager.getChildCount(); i++) { + View view = manager.getChildAt(i); + if (view == null) { + continue; + } + SlideAdapter.DoricViewHolder viewHolder = (SlideAdapter.DoricViewHolder) mView.getChildViewHolder(view); + if (id.equals(viewHolder.slideItemNode.getId())) { + return viewHolder.slideItemNode; + } + } + return null; + } + + @Override + protected void blendSubNode(JSObject subProperties) { + String viewId = subProperties.getProperty("id").asString().value(); + ViewNode node = getSubNodeById(viewId); + if (node != null) { + node.blend(subProperties.getProperty("props").asObject()); + } else { + JSObject oldModel = getSubModel(viewId); + if (oldModel != null) { + recursiveMixin(subProperties, oldModel); + } + slideAdapter.blendSubNode(subProperties); + } + } + + @Override + public void blend(JSObject jsObject) { + super.blend(jsObject); + if (mView != null) { + mView.post(new Runnable() { + @Override + public void run() { + slideAdapter.notifyDataSetChanged(); + } + }); + } + } + + @Override + protected void blend(RecyclerView view, String name, JSValue prop) { + switch (name) { + case "itemCount": + this.slideAdapter.itemCount = prop.asNumber().toInt(); + break; + case "renderItem": + // If reset renderItem,should reset native cache. + this.slideAdapter.itemValues.clear(); + clearSubModel(); + break; + case "batchCount": + this.slideAdapter.batchCount = prop.asNumber().toInt(); + break; + default: + super.blend(view, name, prop); + break; + } + } +} diff --git a/doric/src/main/java/pub/doric/utils/DoricConstant.java b/doric/src/main/java/pub/doric/utils/DoricConstant.java new file mode 100644 index 00000000..08a814b1 --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/DoricConstant.java @@ -0,0 +1,71 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricConstant { + public static final String DORIC_BUNDLE_SANDBOX = "bundle/doric-sandbox.js"; + public static final String DORIC_BUNDLE_LIB = "bundle/doric-lib.js"; + public static final String DORIC_MODULE_LIB = "doric"; + + + public static final String INJECT_LOG = "nativeLog"; + public static final String INJECT_REQUIRE = "nativeRequire"; + public static final String INJECT_TIMER_SET = "nativeSetTimer"; + public static final String INJECT_TIMER_CLEAR = "nativeClearTimer"; + public static final String INJECT_BRIDGE = "nativeBridge"; + public static final String INJECT_EMPTY = "nativeEmpty"; + + public static final String TEMPLATE_CONTEXT_CREATE = "Reflect.apply(" + + "function(doric,context,Entry,require,exports){" + "\n" + + "%s" + "\n" + + "},doric.jsObtainContext(\"%s\"),[" + + "undefined," + + "doric.jsObtainContext(\"%s\")," + + "doric.jsObtainEntry(\"%s\")," + + "doric.__require__" + + ",{}" + + "])"; + + public static final String TEMPLATE_MODULE = "Reflect.apply(doric.jsRegisterModule,this,[" + + "\"%s\"," + + "Reflect.apply(function(__module){" + + "(function(module,exports,require){" + "\n" + + "%s" + "\n" + + "})(__module,__module.exports,doric.__require__);" + + "\nreturn __module.exports;" + + "},this,[{exports:{}}])" + + "])"; + public static final String TEMPLATE_CONTEXT_DESTROY = "doric.jsReleaseContext(\"%s\")"; + public static final String GLOBAL_DORIC = "doric"; + public static final String DORIC_CONTEXT_RELEASE = "jsReleaseContext"; + public static final String DORIC_CONTEXT_INVOKE = "jsCallEntityMethod"; + public static final String DORIC_TIMER_CALLBACK = "jsCallbackTimer"; + public static final String DORIC_BRIDGE_RESOLVE = "jsCallResolve"; + public static final String DORIC_BRIDGE_REJECT = "jsCallReject"; + + + public static final String DORIC_ENTITY_RESPONSE = "__response__"; + public static final String DORIC_ENTITY_INIT = "__init__"; + public static final String DORIC_ENTITY_CREATE = "__onCreate__"; + public static final String DORIC_ENTITY_DESTROY = "__onDestroy__"; + public static final String DORIC_ENTITY_SHOW = "__onShow__"; + public static final String DORIC_ENTITY_HIDDEN = "__onHidden__"; +} diff --git a/doric/src/main/java/pub/doric/utils/DoricContextHolder.java b/doric/src/main/java/pub/doric/utils/DoricContextHolder.java new file mode 100644 index 00000000..c75e6710 --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/DoricContextHolder.java @@ -0,0 +1,35 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +import pub.doric.DoricContext; + +/** + * @Description: com.github.penfeizhou.doric.utils + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public abstract class DoricContextHolder { + private final DoricContext doricContext; + + public DoricContextHolder(DoricContext doricContext) { + this.doricContext = doricContext; + } + + public DoricContext getDoricContext() { + return doricContext; + } +} diff --git a/doric/src/main/java/pub/doric/utils/DoricLog.java b/doric/src/main/java/pub/doric/utils/DoricLog.java new file mode 100644 index 00000000..325ef2fb --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/DoricLog.java @@ -0,0 +1,64 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +import android.text.TextUtils; +import android.util.Log; + +import pub.doric.Doric; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricLog { + private static String TAG = Doric.class.getSimpleName(); + + + public static void d(String message, Object... args) { + Log.d(suffixTag(null), format(message, args)); + } + + public static void w(String message, Object... args) { + Log.w(suffixTag(null), format(message, args)); + + } + + public static void e(String message, Object... args) { + Log.e(suffixTag(null), format(message, args)); + } + + public static void suffix_d(String suffix, String message, Object... args) { + Log.d(suffixTag(suffix), format(message, args)); + } + + public static void suffix_w(String suffix, String message, Object... args) { + Log.w(suffixTag(suffix), format(message, args)); + } + + public static void suffix_e(String suffix, String message, Object... args) { + Log.e(suffixTag(suffix), format(message, args)); + } + + private static String suffixTag(String suffix) { + return TextUtils.isEmpty(suffix) ? TAG : TAG + suffix; + } + + private static String format(String message, Object... args) { + return String.format(message, args); + } +} diff --git a/doric/src/main/java/pub/doric/utils/DoricMetaInfo.java b/doric/src/main/java/pub/doric/utils/DoricMetaInfo.java new file mode 100644 index 00000000..e765631b --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/DoricMetaInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +import android.text.TextUtils; + +import pub.doric.DoricContext; +import pub.doric.extension.bridge.DoricMethod; +import pub.doric.extension.bridge.DoricPlugin; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricMetaInfo { + + private Constructor pluginConstructor; + + private Map methodMap = new ConcurrentHashMap<>(); + private String name; + + public DoricMetaInfo(Class pluginClass) { + try { + this.pluginConstructor = pluginClass.getDeclaredConstructor(DoricContext.class); + DoricPlugin doricPlugin = pluginClass.getAnnotation(DoricPlugin.class); + this.name = doricPlugin.name(); + Method[] methods = pluginClass.getMethods(); + for (Method method : methods) { + DoricMethod doricMethod = method.getAnnotation(DoricMethod.class); + if (doricMethod != null) { + if (TextUtils.isEmpty(doricMethod.name())) { + methodMap.put(method.getName(), method); + } else { + methodMap.put(doricMethod.name(), method); + } + } + } + } catch (Exception e) { + DoricLog.e("Error to create doric for " + e.getLocalizedMessage()); + } + } + + public String getName() { + return name; + } + + public T createInstance(DoricContext doricContext) { + try { + return pluginConstructor.newInstance(doricContext); + } catch (Exception e) { + DoricLog.e("Error to create doric plugin for " + e.getLocalizedMessage()); + return null; + } + } + + public Method getMethod(String name) { + return methodMap.get(name); + } +} diff --git a/doric/src/main/java/pub/doric/utils/DoricUtils.java b/doric/src/main/java/pub/doric/utils/DoricUtils.java new file mode 100644 index 00000000..6f1b237d --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/DoricUtils.java @@ -0,0 +1,235 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +import android.content.Context; +import android.content.res.AssetManager; +import android.util.DisplayMetrics; +import android.view.Display; +import android.view.WindowManager; + +import androidx.annotation.NonNull; + +import com.github.pengfeizhou.jscore.JSArray; +import com.github.pengfeizhou.jscore.JSDecoder; +import com.github.pengfeizhou.jscore.JSONBuilder; +import com.github.pengfeizhou.jscore.JSValue; +import com.github.pengfeizhou.jscore.JavaValue; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Array; +import java.lang.reflect.Field; + +import pub.doric.Doric; + +/** + * @Description: Doric + * @Author: pengfei.zhou + * @CreateDate: 2019-07-18 + */ +public class DoricUtils { + public static String readAssetFile(String assetFile) { + InputStream inputStream = null; + try { + AssetManager assetManager = Doric.application().getAssets(); + inputStream = assetManager.open(assetFile); + int length = inputStream.available(); + byte[] buffer = new byte[length]; + inputStream.read(buffer); + return new String(buffer); + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + return ""; + } + + public static JavaValue toJavaValue(Object arg) { + if (arg == null) { + return new JavaValue(); + } else if (arg instanceof JSONBuilder) { + return new JavaValue(((JSONBuilder) arg).toJSONObject()); + } else if (arg instanceof JSONObject) { + return new JavaValue((JSONObject) arg); + } else if (arg instanceof String) { + return new JavaValue((String) arg); + } else if (arg instanceof Integer) { + return new JavaValue((Integer) arg); + } else if (arg instanceof Long) { + return new JavaValue((Long) arg); + } else if (arg instanceof Float) { + return new JavaValue((Float) arg); + } else if (arg instanceof Double) { + return new JavaValue((Double) arg); + } else if (arg instanceof Boolean) { + return new JavaValue((Boolean) arg); + } else if (arg instanceof JavaValue) { + return (JavaValue) arg; + } else if (arg instanceof Object[]) { + JSONArray jsonArray = new JSONArray(); + for (Object o : (Object[]) arg) { + jsonArray.put(o); + } + return new JavaValue(jsonArray); + } else { + return new JavaValue(String.valueOf(arg)); + } + } + + public static Object toJavaObject(@NonNull Class clz, JSDecoder decoder) throws Exception { + if (clz == JSDecoder.class) { + return decoder; + } else { + return toJavaObject(clz, decoder.decode()); + } + } + + public static Object toJavaObject(@NonNull Class clz, JSValue jsValue) throws Exception { + if (clz == JSValue.class || JSValue.class.isAssignableFrom(clz)) { + return jsValue; + } else if (clz == String.class) { + return jsValue.asString().value(); + } else if (clz == boolean.class || clz == Boolean.class) { + return jsValue.asBoolean().value(); + } else if (clz == int.class || clz == Integer.class) { + return jsValue.asNumber().toInt(); + } else if (clz == long.class || clz == Long.class) { + return jsValue.asNumber().toLong(); + } else if (clz == float.class || clz == Float.class) { + return jsValue.asNumber().toFloat(); + } else if (clz == double.class || clz == Double.class) { + return jsValue.asNumber().toDouble(); + } else if (clz.isArray()) { + Class elementClass = clz.getComponentType(); + Object ret; + if (jsValue.isArray()) { + JSArray jsArray = jsValue.asArray(); + ret = Array.newInstance(clz, jsArray.size()); + for (int i = 0; i < jsArray.size(); i++) { + Array.set(ret, i, toJavaObject(elementClass, jsArray.get(i))); + } + } else if (jsValue.isNull()) { + ret = Array.newInstance(clz, 0); + } else { + ret = null; + } + return ret; + } + return null; + } + + private static int sScreenWidthPixels; + private static int sScreenHeightPixels; + + public static int getScreenWidth(Context context) { + if (context == null) { + context = Doric.application(); + } + if (sScreenWidthPixels > 0) { + return sScreenWidthPixels; + } + DisplayMetrics dm = new DisplayMetrics(); + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + Display display = manager.getDefaultDisplay(); + if (display != null) { + display.getMetrics(dm); + sScreenWidthPixels = dm.widthPixels; + sScreenHeightPixels = dm.heightPixels; + } + return sScreenWidthPixels; + } + + public static int getScreenWidth() { + return getScreenWidth(null); + } + + public static int getScreenHeight(Context context) { + if (context == null) { + context = Doric.application(); + } + if (sScreenHeightPixels > 0) { + return sScreenHeightPixels; + } + DisplayMetrics dm = new DisplayMetrics(); + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + + Display display = manager.getDefaultDisplay(); + if (display != null) { + display.getMetrics(dm); + sScreenWidthPixels = dm.widthPixels; + sScreenHeightPixels = dm.heightPixels; + } + return sScreenHeightPixels; + } + + public static int getScreenHeight() { + return getScreenHeight(null); + } + + public static float px2dp(int pxValue) { + return px2dp(null, pxValue); + } + + public static float px2dp(Context context, int pxValue) { + if (context == null) { + context = Doric.application(); + } + final float scale = context.getResources().getDisplayMetrics().density; + return pxValue / scale; + } + + public static int dp2px(float dpValue) { + return dp2px(null, dpValue); + } + + public static int dp2px(Context context, float dipValue) { + if (context == null) { + context = Doric.application(); + } + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dipValue * scale + (dipValue > 0 ? 0.5f : -0.5f)); + } + + + public static int getStatusBarHeight(Context context) { + Class c = null; + Object obj = null; + Field field = null; + int x = 0, sbar = 0; + try { + c = Class.forName("com.android.internal.R$dimen"); + obj = c.newInstance(); + field = c.getField("status_bar_height"); + x = Integer.parseInt(field.get(obj).toString()); + sbar = context.getResources().getDimensionPixelSize(x); + } catch (Exception E) { + E.printStackTrace(); + } + return sbar; + } +} diff --git a/doric/src/main/java/pub/doric/utils/ThreadMode.java b/doric/src/main/java/pub/doric/utils/ThreadMode.java new file mode 100644 index 00000000..298e034a --- /dev/null +++ b/doric/src/main/java/pub/doric/utils/ThreadMode.java @@ -0,0 +1,27 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.utils; + +/** + * @Description: com.github.penfeizhou.doric.utils + * @Author: pengfei.zhou + * @CreateDate: 2019-07-20 + */ +public enum ThreadMode { + UI, + JS, + INDEPENDENT, +} diff --git a/doric/src/main/java/pub/doric/widget/HVScrollView.java b/doric/src/main/java/pub/doric/widget/HVScrollView.java new file mode 100644 index 00000000..7b86cd49 --- /dev/null +++ b/doric/src/main/java/pub/doric/widget/HVScrollView.java @@ -0,0 +1,2250 @@ +/* + * Copyright [2019] [Doric.Pub] + * + * 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 pub.doric.widget; + + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.FocusFinder; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.VelocityTracker; +import android.view.View; +import android.view.ViewConfiguration; +import android.view.ViewGroup; +import android.view.ViewParent; +import android.view.accessibility.AccessibilityEvent; +import android.view.animation.AnimationUtils; +import android.widget.EdgeEffect; +import android.widget.FrameLayout; +import android.widget.OverScroller; +import android.widget.ScrollView; + +import androidx.annotation.RestrictTo; +import androidx.core.view.AccessibilityDelegateCompat; +import androidx.core.view.InputDeviceCompat; +import androidx.core.view.NestedScrollingChild2; +import androidx.core.view.NestedScrollingChildHelper; +import androidx.core.view.NestedScrollingParent; +import androidx.core.view.NestedScrollingParentHelper; +import androidx.core.view.ScrollingView; +import androidx.core.view.ViewCompat; +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; +import androidx.core.view.accessibility.AccessibilityRecordCompat; +import androidx.core.widget.EdgeEffectCompat; + +import java.util.List; + +/** + * NestedScrollView is just like {@link android.widget.ScrollView}, but it supports acting + * as both a nested scrolling parent and child on both new and old versions of Android. + * Nested scrolling is enabled by default. + */ +public class HVScrollView extends FrameLayout implements NestedScrollingParent, + NestedScrollingChild2, ScrollingView { + static final int ANIMATED_SCROLL_GAP = 250; + + static final float MAX_SCROLL_FACTOR = 0.5f; + + private static final String TAG = "NestedScrollView"; + + /** + * Interface definition for a callback to be invoked when the scroll + * X or Y positions of a view change. + *

+ *

This version of the interface works on all versions of Android, back to API v4.

+ * + * @see #setOnScrollChangeListener(OnScrollChangeListener) + */ + public interface OnScrollChangeListener { + /** + * Called when the scroll position of a view changes. + * + * @param v The view whose scroll position has changed. + * @param scrollX Current horizontal scroll origin. + * @param scrollY Current vertical scroll origin. + * @param oldScrollX Previous horizontal scroll origin. + * @param oldScrollY Previous vertical scroll origin. + */ + void onScrollChange(HVScrollView v, int scrollX, int scrollY, + int oldScrollX, int oldScrollY); + } + + private long mLastScroll; + + private final Rect mTempRect = new Rect(); + private OverScroller mScroller; + private EdgeEffect mEdgeGlowTop; + private EdgeEffect mEdgeGlowBottom; + private EdgeEffect mEdgeGlowLeft; + private EdgeEffect mEdgeGlowRight; + private int scrollModeFlag = 0; + public static final int DISABLE_VERTICAL_SCROLL = 1; + public static final int DISABLE_HORIZONTAL_SCROLL = 2; + + /** + * Position of the last motion event. + */ + private int mLastMotionX; + private int mLastMotionY; + + /** + * True when the layout has changed but the traversal has not come through yet. + * Ideally the view hierarchy would keep track of this for us. + */ + private boolean mIsLayoutDirty = true; + private boolean mIsLaidOut = false; + + /** + * The child to give focus to in the event that a child has requested focus while the + * layout is dirty. This prevents the scroll from being wrong if the child has not been + * laid out before requesting focus. + */ + private View mChildToScrollTo = null; + + /** + * True if the user is currently dragging this ScrollView around. This is + * not the same as 'is being flinged', which can be checked by + * mScroller.isFinished() (flinging begins when the user lifts his finger). + */ + private boolean mIsBeingDragged = false; + + /** + * Determines speed during touch scrolling + */ + private VelocityTracker mVelocityTracker; + + /** + * Whether arrow scrolling is animated. + */ + private boolean mSmoothScrollingEnabled = true; + + private int mTouchSlop; + private int mMinimumVelocity; + private int mMaximumVelocity; + + /** + * ID of the active pointer. This is used to retain consistency during + * drags/flings if multiple pointers are used. + */ + private int mActivePointerId = INVALID_POINTER; + + /** + * Used during scrolling to retrieve the new offset within the window. + */ + private final int[] mScrollOffset = new int[2]; + private final int[] mScrollConsumed = new int[2]; + private int mNestedXOffset; + private int mNestedYOffset; + + private int mLastScrollerX; + private int mLastScrollerY; + + /** + * Sentinel value for no current active pointer. + * Used by {@link #mActivePointerId}. + */ + private static final int INVALID_POINTER = -1; + + private SavedState mSavedState; + + private static final AccessibilityDelegate ACCESSIBILITY_DELEGATE = new AccessibilityDelegate(); + + private static final int[] SCROLLVIEW_STYLEABLE = new int[]{ + android.R.attr.fillViewport + }; + + private final NestedScrollingParentHelper mParentHelper; + private final NestedScrollingChildHelper mChildHelper; + + private float mVerticalScrollFactor; + private float mHorizontalScrollFactor; + + private OnScrollChangeListener mOnScrollChangeListener; + + public HVScrollView(Context context) { + this(context, null); + } + + public HVScrollView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public HVScrollView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initScrollView(); + + final TypedArray a = context.obtainStyledAttributes( + attrs, SCROLLVIEW_STYLEABLE, defStyleAttr, 0); + a.recycle(); + + mParentHelper = new NestedScrollingParentHelper(this); + mChildHelper = new NestedScrollingChildHelper(this); + + // ...because why else would you be using this widget? + setNestedScrollingEnabled(true); + + ViewCompat.setAccessibilityDelegate(this, ACCESSIBILITY_DELEGATE); + } + + // NestedScrollingChild + + @Override + public void setNestedScrollingEnabled(boolean enabled) { + mChildHelper.setNestedScrollingEnabled(enabled); + } + + @Override + public boolean isNestedScrollingEnabled() { + return mChildHelper.isNestedScrollingEnabled(); + } + + @Override + public boolean startNestedScroll(int axes) { + return mChildHelper.startNestedScroll(axes); + } + + @Override + public boolean startNestedScroll(int axes, int type) { + return mChildHelper.startNestedScroll(axes, type); + } + + @Override + public void stopNestedScroll() { + mChildHelper.stopNestedScroll(); + } + + @Override + public void stopNestedScroll(int type) { + mChildHelper.stopNestedScroll(type); + } + + @Override + public boolean hasNestedScrollingParent() { + return mChildHelper.hasNestedScrollingParent(); + } + + @Override + public boolean hasNestedScrollingParent(int type) { + return mChildHelper.hasNestedScrollingParent(type); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow); + } + + @Override + public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed, int[] offsetInWindow, int type) { + return mChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, + offsetInWindow, type); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow); + } + + @Override + public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow, + int type) { + return mChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type); + } + + @Override + public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) { + return mChildHelper.dispatchNestedFling(velocityX, velocityY, consumed); + } + + @Override + public boolean dispatchNestedPreFling(float velocityX, float velocityY) { + return mChildHelper.dispatchNestedPreFling(velocityX, velocityY); + } + + // NestedScrollingParent + + @Override + public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) { + return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0 + || (nestedScrollAxes & ViewCompat.SCROLL_AXIS_HORIZONTAL) != 0; + } + + @Override + public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes) { + mParentHelper.onNestedScrollAccepted(child, target, nestedScrollAxes); + startNestedScroll(nestedScrollAxes); + } + + @Override + public void onStopNestedScroll(View target) { + mParentHelper.onStopNestedScroll(target); + stopNestedScroll(); + } + + @Override + public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, + int dyUnconsumed) { + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + scrollBy(dxUnconsumed, dyUnconsumed); + final int myConsumedX = getScrollX() - oldScrollX; + final int myUnconsumedX = dxUnconsumed - myConsumedX; + final int myConsumedY = getScrollY() - oldScrollY; + final int myUnconsumedY = dyUnconsumed - myConsumedY; + dispatchNestedScroll(myConsumedX, myConsumedY, myUnconsumedX, myUnconsumedY, null); + } + + @Override + public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) { + dispatchNestedPreScroll(dx, dy, consumed, null); + } + + @Override + public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) { + if (!consumed) { + flingWithNestedDispatch((int) velocityX, (int) velocityY); + return true; + } + return false; + } + + @Override + public boolean onNestedPreFling(View target, float velocityX, float velocityY) { + return dispatchNestedPreFling(velocityX, velocityY); + } + + @Override + public int getNestedScrollAxes() { + return mParentHelper.getNestedScrollAxes(); + } + + // ScrollView import + + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + protected float getTopFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int scrollY = getScrollY(); + if (scrollY < length) { + return scrollY / (float) length; + } + + return 1.0f; + } + + @Override + protected float getBottomFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getVerticalFadingEdgeLength(); + final int bottomEdge = getHeight() - getPaddingBottom(); + final int span = getChildAt(0).getBottom() - getScrollY() - bottomEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + @Override + protected float getLeftFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int scrollX = getScrollX(); + if (scrollX < length) { + return scrollX / (float) length; + } + + return 1.0f; + } + + @Override + protected float getRightFadingEdgeStrength() { + if (getChildCount() == 0) { + return 0.0f; + } + + final int length = getHorizontalFadingEdgeLength(); + final int rightEdge = getWidth() - getPaddingRight(); + final int span = getChildAt(0).getRight() - getScrollY() - rightEdge; + if (span < length) { + return span / (float) length; + } + + return 1.0f; + } + + /** + * @return The maximum amount this scroll view will scroll in response to + * an arrow event. + */ + public int getMaxScrollAmount() { + return (int) (MAX_SCROLL_FACTOR * getHeight()); + } + + private void initScrollView() { + mScroller = new OverScroller(getContext()); + setFocusable(true); + setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); + setWillNotDraw(false); + final ViewConfiguration configuration = ViewConfiguration.get(getContext()); + mTouchSlop = configuration.getScaledTouchSlop(); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mMaximumVelocity = configuration.getScaledMaximumFlingVelocity(); + } + + @Override + public void addView(View child) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child); + } + + @Override + public void addView(View child, int index) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index); + } + + @Override + public void addView(View child, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, params); + } + + @Override + public void addView(View child, int index, ViewGroup.LayoutParams params) { + if (getChildCount() > 0) { + throw new IllegalStateException("ScrollView can host only one direct child"); + } + + super.addView(child, index, params); + } + + /** + * Register a callback to be invoked when the scroll X or Y positions of + * this view change. + *

This version of the method works on all versions of Android, back to API v4.

+ * + * @param l The listener to notify when the scroll X or Y position changes. + * @see android.view.View#getScrollX() + * @see android.view.View#getScrollY() + */ + public void setOnScrollChangeListener(OnScrollChangeListener l) { + mOnScrollChangeListener = l; + } + + /** + * @return Returns true this ScrollView can be scrolled + */ + private boolean canScroll() { + return canScrollHorizontally() || canScrollVertically(); + } + + private boolean canScrollVertically() { + View child = getChildAt(0); + if (child != null) { + int childHeight = child.getHeight(); + return getHeight() < childHeight + getPaddingTop() + getPaddingBottom(); + } + return false; + } + + private boolean canScrollHorizontally() { + View child = getChildAt(0); + if (child != null) { + int childWidth = child.getWidth(); + return getWidth() < childWidth + getPaddingLeft() + getPaddingRight(); + } + return false; + } + + /** + * @return Whether arrow scrolling will animate its transition. + */ + public boolean isSmoothScrollingEnabled() { + return mSmoothScrollingEnabled; + } + + /** + * Set whether arrow scrolling will animate its transition. + * + * @param smoothScrollingEnabled whether arrow scrolling will animate its transition + */ + public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) { + mSmoothScrollingEnabled = smoothScrollingEnabled; + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + super.onScrollChanged(l, t, oldl, oldt); + + if (mOnScrollChangeListener != null) { + mOnScrollChangeListener.onScrollChange(this, l, t, oldl, oldt); + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + if (heightMode == MeasureSpec.UNSPECIFIED && widthMode == MeasureSpec.UNSPECIFIED) { + return; + } + + if (getChildCount() > 0) { + final View child = getChildAt(0); + int width = getMeasuredWidth(); + int height = getMeasuredHeight(); + width -= getPaddingLeft(); + width -= getPaddingRight(); + height -= getPaddingTop(); + height -= getPaddingBottom(); + int childWidthMeasureSpec; + int childHeightMeasureSpec; + final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams(); + if (lp.width != ViewGroup.LayoutParams.MATCH_PARENT && lp.height != ViewGroup.LayoutParams.MATCH_PARENT) { + return; + } + if (child.getMeasuredWidth() < width && lp.width == ViewGroup.LayoutParams.MATCH_PARENT) { + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY); + } else { + widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec, + getPaddingLeft() + getPaddingRight(), lp.width); + } + if (child.getMeasuredHeight() < height && lp.height == ViewGroup.LayoutParams.MATCH_PARENT) { + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY); + } else { + heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, + getPaddingTop() + getPaddingBottom(), lp.height); + } + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + } + + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // Let the focused view and/or our descendants get the key first + return super.dispatchKeyEvent(event) || executeKeyEvent(event); + } + + /** + * You can call this function yourself to have the scroll view perform + * scrolling from a key event, just as if the event had been dispatched to + * it by the view hierarchy. + * + * @param event The key event to execute. + * @return Return true if the event was handled, else false. + */ + public boolean executeKeyEvent(KeyEvent event) { + mTempRect.setEmpty(); + + if (!canScroll()) { + if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) { + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + View nextFocused = FocusFinder.getInstance().findNextFocus(this, + currentFocused, View.FOCUS_DOWN); + return nextFocused != null + && nextFocused != this + && nextFocused.requestFocus(View.FOCUS_DOWN); + } + return false; + } + + boolean handled = false; + if (event.getAction() == KeyEvent.ACTION_DOWN) { + switch (event.getKeyCode()) { + case KeyEvent.KEYCODE_DPAD_UP: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_UP); + } else { + handled = fullScroll(View.FOCUS_UP); + } + break; + case KeyEvent.KEYCODE_DPAD_DOWN: + if (!event.isAltPressed()) { + handled = arrowScroll(View.FOCUS_DOWN); + } else { + handled = fullScroll(View.FOCUS_DOWN); + } + break; + case KeyEvent.KEYCODE_SPACE: + pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN); + break; + } + } + + return handled; + } + + private boolean inChild(int x, int y) { + if (getChildCount() > 0) { + final int scrollY = getScrollY(); + final int scrollX = getScrollX(); + final View child = getChildAt(0); + return !(y < child.getTop() - scrollY + || y >= child.getBottom() - scrollY + || x < child.getLeft() - scrollX + || x >= child.getRight() - scrollX); + } + return false; + } + + private void initOrResetVelocityTracker() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } else { + mVelocityTracker.clear(); + } + } + + private void initVelocityTrackerIfNotExists() { + if (mVelocityTracker == null) { + mVelocityTracker = VelocityTracker.obtain(); + } + } + + private void recycleVelocityTracker() { + if (mVelocityTracker != null) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + } + + @Override + public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + if (disallowIntercept) { + recycleVelocityTracker(); + } + super.requestDisallowInterceptTouchEvent(disallowIntercept); + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + /* + * This method JUST determines whether we want to intercept the motion. + * If we return true, onMotionEvent will be called and we do the actual + * scrolling there. + */ + + /* + * Shortcut the most recurring case: the user is in the dragging + * state and he is moving his finger. We want to intercept this + * motion. + */ + final int action = ev.getAction(); + if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) { + return true; + } + + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_MOVE: { + /* + * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check + * whether the user has moved far enough from his original down touch. + */ + + /* + * Locally do absolute value. mLastMotionY is set to the y value + * of the down event. + */ + final int activePointerId = mActivePointerId; + if (activePointerId == INVALID_POINTER) { + // If we don't have a valid id, the touch down wasn't on content. + break; + } + + final int pointerIndex = ev.findPointerIndex(activePointerId); + if (pointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + activePointerId + + " in onInterceptTouchEvent"); + break; + } + + final int x = (int) ev.getX(pointerIndex); + final int y = (int) ev.getY(pointerIndex); + final int xDiff = Math.abs(x - mLastMotionX); + final int yDiff = Math.abs(y - mLastMotionY); + if ((xDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_HORIZONTAL) == 0) + || (yDiff > mTouchSlop && (getNestedScrollAxes() & ViewCompat.SCROLL_AXIS_VERTICAL) == 0)) { + mIsBeingDragged = true; + mLastMotionX = x; + mLastMotionY = y; + initVelocityTrackerIfNotExists(); + mVelocityTracker.addMovement(ev); + mNestedXOffset = 0; + mNestedYOffset = 0; + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + break; + } + + case MotionEvent.ACTION_DOWN: { + final int x = (int) ev.getX(); + final int y = (int) ev.getY(); + if (!inChild(x, y)) { + mIsBeingDragged = false; + recycleVelocityTracker(); + break; + } + + /* + * Remember location of down touch. + * ACTION_DOWN always refers to pointer index 0. + */ + mLastMotionX = x; + mLastMotionY = y; + mActivePointerId = ev.getPointerId(0); + + initOrResetVelocityTracker(); + mVelocityTracker.addMovement(ev); + /* + * If being flinged and user touches the screen, initiate drag; + * otherwise don't. mScroller.isFinished should be false when + * being flinged. We need to call computeScrollOffset() first so that + * isFinished() is correct. + */ + mScroller.computeScrollOffset(); + mIsBeingDragged = !mScroller.isFinished(); + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_TOUCH); + break; + } + + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + /* Release the drag */ + mIsBeingDragged = false; + mActivePointerId = INVALID_POINTER; + recycleVelocityTracker(); + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + stopNestedScroll(ViewCompat.TYPE_TOUCH); + break; + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + break; + } + + /* + * The only time we want to intercept motion events is if we are in the + * drag mode. + */ + return mIsBeingDragged; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + initVelocityTrackerIfNotExists(); + + MotionEvent vtev = MotionEvent.obtain(ev); + + final int actionMasked = ev.getActionMasked(); + + if (actionMasked == MotionEvent.ACTION_DOWN) { + mNestedXOffset = 0; + mNestedYOffset = 0; + } + vtev.offsetLocation(mNestedXOffset, mNestedYOffset); + + switch (actionMasked) { + case MotionEvent.ACTION_DOWN: { + if (getChildCount() == 0) { + return false; + } + if ((mIsBeingDragged = !mScroller.isFinished())) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + + /* + * If being flinged and user touches, stop the fling. isFinished + * will be false if being flinged. + */ + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + + // Remember where the motion event started + mLastMotionX = (int) ev.getX(); + mLastMotionY = (int) ev.getY(); + mActivePointerId = ev.getPointerId(0); + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_TOUCH); + break; + } + case MotionEvent.ACTION_MOVE: + final int activePointerIndex = ev.findPointerIndex(mActivePointerId); + if (activePointerIndex == -1) { + Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent"); + break; + } + + final int x = (int) ev.getX(activePointerIndex); + final int y = (int) ev.getY(activePointerIndex); + int deltaX = mLastMotionX - x; + int deltaY = mLastMotionY - y; + if (dispatchNestedPreScroll(deltaX, deltaY, mScrollConsumed, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + deltaX -= mScrollConsumed[0]; + deltaY -= mScrollConsumed[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } + if (!mIsBeingDragged && (Math.abs(deltaX) > mTouchSlop || Math.abs(deltaY) > mTouchSlop)) { + final ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + mIsBeingDragged = true; + if (deltaX > 0) { + deltaX -= mTouchSlop; + } else { + deltaX += mTouchSlop; + } + if (deltaY > 0) { + deltaY -= mTouchSlop; + } else { + deltaY += mTouchSlop; + } + } + if (mIsBeingDragged) { + // Scroll to follow the motion event + mLastMotionX = x - mScrollOffset[0]; + mLastMotionY = y - mScrollOffset[1]; + + final int oldX = getScrollX(); + final int oldY = getScrollY(); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int overscrollMode = getOverScrollMode(); + boolean canOverscroll = overscrollMode == View.OVER_SCROLL_ALWAYS + || (overscrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + + // Calling overScrollByCompat will call onOverScrolled, which + // calls onScrollChanged if applicable. + if (overScrollByCompat(deltaX, deltaY, getScrollX(), getScrollY(), rangeX, rangeY, 0, + 0, true) && !hasNestedScrollingParent(ViewCompat.TYPE_TOUCH)) { + // Break our velocity if we hit a scroll barrier. + mVelocityTracker.clear(); + } + + final int scrolledDeltaX = getScrollX() - oldX; + final int scrolledDeltaY = getScrollY() - oldY; + final int unconsumedX = deltaX - scrolledDeltaX; + final int unconsumedY = deltaY - scrolledDeltaY; + if (dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, mScrollOffset, + ViewCompat.TYPE_TOUCH)) { + mLastMotionX -= mScrollOffset[0]; + mLastMotionY -= mScrollOffset[1]; + vtev.offsetLocation(mScrollOffset[0], mScrollOffset[1]); + mNestedXOffset += mScrollOffset[0]; + mNestedYOffset += mScrollOffset[1]; + } else if (canOverscroll) { + ensureGlows(); + final int pulledToX = oldX + deltaX; + final int pulledToY = oldY + deltaY; + if (pulledToX < 0) { + EdgeEffectCompat.onPull(mEdgeGlowLeft, (float) deltaX / getWidth(), + ev.getY(activePointerIndex) / getHeight()); + if (!mEdgeGlowRight.isFinished()) { + mEdgeGlowRight.onRelease(); + } + } else if (pulledToX > rangeX) { + EdgeEffectCompat.onPull(mEdgeGlowRight, (float) deltaX / getWidth(), + 1.f - ev.getY(activePointerIndex) + / getHeight()); + if (!mEdgeGlowLeft.isFinished()) { + mEdgeGlowLeft.onRelease(); + } + } + if (pulledToY < 0) { + EdgeEffectCompat.onPull(mEdgeGlowTop, (float) deltaY / getHeight(), + ev.getX(activePointerIndex) / getWidth()); + if (!mEdgeGlowBottom.isFinished()) { + mEdgeGlowBottom.onRelease(); + } + } else if (pulledToY > rangeY) { + EdgeEffectCompat.onPull(mEdgeGlowBottom, (float) deltaY / getHeight(), + 1.f - ev.getX(activePointerIndex) + / getWidth()); + if (!mEdgeGlowTop.isFinished()) { + mEdgeGlowTop.onRelease(); + } + } + if (mEdgeGlowTop != null + && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished() + || !mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + } + break; + case MotionEvent.ACTION_UP: + final VelocityTracker velocityTracker = mVelocityTracker; + velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity); + int initialVelocityX = (int) velocityTracker.getXVelocity(mActivePointerId); + int initialVelocityY = (int) velocityTracker.getYVelocity(mActivePointerId); + if ((Math.abs(initialVelocityX) > mMinimumVelocity) || (Math.abs(initialVelocityY) > mMinimumVelocity)) { + flingWithNestedDispatch(-initialVelocityX, -initialVelocityY); + } else if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_CANCEL: + if (mIsBeingDragged && getChildCount() > 0) { + if (mScroller.springBack(getScrollX(), getScrollY(), 0, getScrollRangeX(), 0, + getScrollRangeY())) { + ViewCompat.postInvalidateOnAnimation(this); + } + } + mActivePointerId = INVALID_POINTER; + endDrag(); + break; + case MotionEvent.ACTION_POINTER_DOWN: { + final int index = ev.getActionIndex(); + mLastMotionX = (int) ev.getX(index); + mLastMotionY = (int) ev.getY(index); + mActivePointerId = ev.getPointerId(index); + break; + } + case MotionEvent.ACTION_POINTER_UP: + onSecondaryPointerUp(ev); + mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId)); + mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId)); + break; + } + + if (mVelocityTracker != null) { + mVelocityTracker.addMovement(vtev); + } + vtev.recycle(); + return true; + } + + private int getAxes() { + int axes = ViewCompat.SCROLL_AXIS_NONE; + if ((scrollModeFlag & DISABLE_VERTICAL_SCROLL) != DISABLE_VERTICAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_VERTICAL; + } + if ((scrollModeFlag & DISABLE_HORIZONTAL_SCROLL) != DISABLE_HORIZONTAL_SCROLL) { + axes |= ViewCompat.SCROLL_AXIS_HORIZONTAL; + } + return axes; + } + + private void onSecondaryPointerUp(MotionEvent ev) { + final int pointerIndex = ev.getActionIndex(); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + // TODO: Make this decision more intelligent. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mLastMotionY = (int) ev.getY(newPointerIndex); + mLastMotionX = (int) ev.getX(newPointerIndex); + mActivePointerId = ev.getPointerId(newPointerIndex); + if (mVelocityTracker != null) { + mVelocityTracker.clear(); + } + } + } + + @Override + public boolean onGenericMotionEvent(MotionEvent event) { + if ((event.getSource() & InputDeviceCompat.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + if (!mIsBeingDragged) { + final float hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + if (vscroll != 0 || hscroll != 0) { + final int deltaX = (int) (hscroll * getHorizontalScrollFactorCompat()); + final int deltaY = (int) (vscroll * getVerticalScrollFactorCompat()); + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + int oldScrollX = getScrollX(); + int oldScrollY = getScrollY(); + int newScrollX = Math.max(0, Math.min(rangeX, oldScrollX - deltaX)); + int newScrollY = Math.max(0, Math.min(rangeY, oldScrollY - deltaY)); + if (newScrollY != oldScrollY || newScrollX != oldScrollX) { + super.scrollTo(newScrollX, newScrollY); + return true; + } + } + } + } + } + } + return false; + } + + private float getHorizontalScrollFactorCompat() { + if (mHorizontalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mHorizontalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mHorizontalScrollFactor; + } + + private float getVerticalScrollFactorCompat() { + if (mVerticalScrollFactor == 0) { + TypedValue outValue = new TypedValue(); + final Context context = getContext(); + if (!context.getTheme().resolveAttribute( + android.R.attr.listPreferredItemHeight, outValue, true)) { + throw new IllegalStateException( + "Expected theme to define listPreferredItemHeight."); + } + mVerticalScrollFactor = outValue.getDimension( + context.getResources().getDisplayMetrics()); + } + return mVerticalScrollFactor; + } + + @Override + protected void onOverScrolled(int scrollX, int scrollY, + boolean clampedX, boolean clampedY) { + super.scrollTo(scrollX, scrollY); + } + + boolean overScrollByCompat(int deltaX, int deltaY, + int scrollX, int scrollY, + int scrollRangeX, int scrollRangeY, + int maxOverScrollX, int maxOverScrollY, + boolean isTouchEvent) { + final int overScrollMode = getOverScrollMode(); + final boolean canScrollHorizontal = + computeHorizontalScrollRange() > computeHorizontalScrollExtent(); + final boolean canScrollVertical = + computeVerticalScrollRange() > computeVerticalScrollExtent(); + final boolean overScrollHorizontal = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollHorizontal); + final boolean overScrollVertical = overScrollMode == View.OVER_SCROLL_ALWAYS + || (overScrollMode == View.OVER_SCROLL_IF_CONTENT_SCROLLS && canScrollVertical); + + int newScrollX = scrollX + deltaX; + if (!overScrollHorizontal) { + maxOverScrollX = 0; + } + + int newScrollY = scrollY + deltaY; + if (!overScrollVertical) { + maxOverScrollY = 0; + } + + // Clamp values if at the limits and record + final int left = -maxOverScrollX; + final int right = maxOverScrollX + scrollRangeX; + final int top = -maxOverScrollY; + final int bottom = maxOverScrollY + scrollRangeY; + + boolean clampedX = false; + if (newScrollX > right) { + newScrollX = right; + clampedX = true; + } else if (newScrollX < left) { + newScrollX = left; + clampedX = true; + } + + boolean clampedY = false; + if (newScrollY > bottom) { + newScrollY = bottom; + clampedY = true; + } else if (newScrollY < top) { + newScrollY = top; + clampedY = true; + } + + if ((clampedX || clampedY) && !hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + mScroller.springBack(newScrollX, newScrollY, 0, getScrollRangeX(), 0, getScrollRangeY()); + } + + onOverScrolled(newScrollX, newScrollY, clampedX, clampedY); + + return clampedX || clampedY; + } + + int getScrollRangeX() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getWidth() - (getWidth() - getPaddingLeft() - getPaddingRight())); + } + return scrollRange; + } + + int getScrollRangeY() { + int scrollRange = 0; + if (getChildCount() > 0) { + View child = getChildAt(0); + scrollRange = Math.max(0, + child.getHeight() - (getHeight() - getPaddingBottom() - getPaddingTop())); + } + return scrollRange; + } + + /** + *

+ * Finds the next focusable component that fits in the specified bounds. + *

+ * + * @param topFocus look for a candidate is the one at the top of the bounds + * if topFocus is true, or at the bottom of the bounds if topFocus is + * false + * @param top the top offset of the bounds in which a focusable must be + * found + * @param bottom the bottom offset of the bounds in which a focusable must + * be found + * @return the next focusable component in the bounds or null if none can + * be found + */ + private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) { + + List focusables = getFocusables(View.FOCUS_FORWARD); + View focusCandidate = null; + + /* + * A fully contained focusable is one where its top is below the bound's + * top, and its bottom is above the bound's bottom. A partially + * contained focusable is one where some part of it is within the + * bounds, but it also has some part that is not within bounds. A fully contained + * focusable is preferred to a partially contained focusable. + */ + boolean foundFullyContainedFocusable = false; + + int count = focusables.size(); + for (int i = 0; i < count; i++) { + View view = focusables.get(i); + int viewTop = view.getTop(); + int viewBottom = view.getBottom(); + + if (top < viewBottom && viewTop < bottom) { + /* + * the focusable is in the target area, it is a candidate for + * focusing + */ + + final boolean viewIsFullyContained = (top < viewTop) && (viewBottom < bottom); + + if (focusCandidate == null) { + /* No candidate, take this one */ + focusCandidate = view; + foundFullyContainedFocusable = viewIsFullyContained; + } else { + final boolean viewIsCloserToBoundary = + (topFocus && viewTop < focusCandidate.getTop()) + || (!topFocus && viewBottom > focusCandidate.getBottom()); + + if (foundFullyContainedFocusable) { + if (viewIsFullyContained && viewIsCloserToBoundary) { + /* + * We're dealing with only fully contained views, so + * it has to be closer to the boundary to beat our + * candidate + */ + focusCandidate = view; + } + } else { + if (viewIsFullyContained) { + /* Any fully contained view beats a partially contained view */ + focusCandidate = view; + foundFullyContainedFocusable = true; + } else if (viewIsCloserToBoundary) { + /* + * Partially contained view beats another partially + * contained view if it's closer + */ + focusCandidate = view; + } + } + } + } + } + + return focusCandidate; + } + + /** + *

Handles scrolling in response to a "page up/down" shortcut press. This + * method will scroll the view by one page up or down and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go one page up or + * {@link android.view.View#FOCUS_DOWN} to go one page down + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean pageScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + if (down) { + mTempRect.top = getScrollY() + height; + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + if (mTempRect.top + height > view.getBottom()) { + mTempRect.top = view.getBottom() - height; + } + } + } else { + mTempRect.top = getScrollY() - height; + if (mTempRect.top < 0) { + mTempRect.top = 0; + } + } + mTempRect.bottom = mTempRect.top + height; + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

Handles scrolling in response to a "home/end" shortcut press. This + * method will scroll the view to the top or bottom and give the focus + * to the topmost/bottommost component in the new visible area. If no + * component is a good candidate for focus, this scrollview reclaims the + * focus.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go the top of the view or + * {@link android.view.View#FOCUS_DOWN} to go the bottom + * @return true if the key event is consumed by this method, false otherwise + */ + public boolean fullScroll(int direction) { + boolean down = direction == View.FOCUS_DOWN; + int height = getHeight(); + + mTempRect.top = 0; + mTempRect.bottom = height; + + if (down) { + int count = getChildCount(); + if (count > 0) { + View view = getChildAt(count - 1); + mTempRect.bottom = view.getBottom() + getPaddingBottom(); + mTempRect.top = mTempRect.bottom - height; + } + } + + return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom); + } + + /** + *

Scrolls the view to make the area defined by top and + * bottom visible. This method attempts to give the focus + * to a component visible in this area. If no component can be focused in + * the new visible area, the focus is reclaimed by this ScrollView.

+ * + * @param direction the scroll direction: {@link android.view.View#FOCUS_UP} + * to go upward, {@link android.view.View#FOCUS_DOWN} to downward + * @param top the top offset of the new area to be made visible + * @param bottom the bottom offset of the new area to be made visible + * @return true if the key event is consumed by this method, false otherwise + */ + private boolean scrollAndFocus(int direction, int top, int bottom) { + boolean handled = true; + + int height = getHeight(); + int containerTop = getScrollY(); + int containerBottom = containerTop + height; + boolean up = direction == View.FOCUS_UP; + + View newFocused = findFocusableViewInBounds(up, top, bottom); + if (newFocused == null) { + newFocused = this; + } + + if (top >= containerTop && bottom <= containerBottom) { + handled = false; + } else { + int delta = up ? (top - containerTop) : (bottom - containerBottom); + doScroll(new int[]{0, delta}); + } + + if (newFocused != findFocus()) newFocused.requestFocus(direction); + + return handled; + } + + /** + * Handle scrolling in response to an up or down arrow click. + * + * @param direction The direction corresponding to the arrow key that was + * pressed + * @return True if we consumed the event, false otherwise + */ + public boolean arrowScroll(int direction) { + + View currentFocused = findFocus(); + if (currentFocused == this) currentFocused = null; + + View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction); + + final int maxJump = getMaxScrollAmount(); + + if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) { + nextFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(nextFocused, mTempRect); + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta); + nextFocused.requestFocus(direction); + } else { + // no new focus + int scrollDelta = maxJump; + + if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) { + scrollDelta = getScrollY(); + } else if (direction == View.FOCUS_DOWN) { + if (getChildCount() > 0) { + int daBottom = getChildAt(0).getBottom(); + int screenBottom = getScrollY() + getHeight() - getPaddingBottom(); + if (daBottom - screenBottom < maxJump) { + scrollDelta = daBottom - screenBottom; + } + } + } + if (scrollDelta == 0) { + return false; + } + doScroll(new int[]{0, direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta}); + } + + if (currentFocused != null && currentFocused.isFocused() + && isOffScreen(currentFocused)) { + // previously focused item still has focus and is off screen, give + // it up (take it back to ourselves) + // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are + // sure to + // get it) + final int descendantFocusability = getDescendantFocusability(); // save + setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); + requestFocus(); + setDescendantFocusability(descendantFocusability); // restore + } + return true; + } + + /** + * @return whether the descendant of this scroll view is scrolled off + * screen. + */ + private boolean isOffScreen(View descendant) { + return !isWithinDeltaOfScreen(descendant, 0, getHeight()); + } + + /** + * @return whether the descendant of this scroll view is within delta + * pixels of being on the screen. + */ + private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) { + descendant.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(descendant, mTempRect); + + return (mTempRect.bottom + delta) >= getScrollY() + && (mTempRect.top - delta) <= (getScrollY() + height); + } + + /** + * Smooth scroll + * + * @param delta the number of pixels to scroll by on the X or Y axis + */ + private void doScroll(int[] delta) { + if (delta[0] != 0 || delta[1] != 0) { + if (mSmoothScrollingEnabled) { + smoothScrollBy(delta[0], delta[1]); + } else { + scrollBy(delta[0], delta[1]); + } + } + } + + /** + * Like {@link View#scrollBy}, but scroll smoothly instead of immediately. + * + * @param dx the number of pixels to scroll by on the X axis + * @param dy the number of pixels to scroll by on the Y axis + */ + public final void smoothScrollBy(int dx, int dy) { + if (getChildCount() == 0) { + // Nothing to do. + return; + } + long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll; + if (duration > ANIMATED_SCROLL_GAP) { + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight() - getPaddingBottom() - getPaddingTop(); + final int right = getChildAt(0).getWidth(); + final int bottom = getChildAt(0).getHeight(); + final int maxX = Math.max(0, right - width); + final int maxY = Math.max(0, bottom - height); + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX; + dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY; + mScroller.startScroll(scrollX, scrollY, dx, dy); + ViewCompat.postInvalidateOnAnimation(this); + } else { + if (!mScroller.isFinished()) { + mScroller.abortAnimation(); + } + scrollBy(dx, dy); + } + mLastScroll = AnimationUtils.currentAnimationTimeMillis(); + } + + /** + * Like {@link #scrollTo}, but scroll smoothly instead of immediately. + * + * @param x the position where to scroll on the X axis + * @param y the position where to scroll on the Y axis + */ + public final void smoothScrollTo(int x, int y) { + smoothScrollBy(x - getScrollX(), y - getScrollY()); + } + + /** + *

The scroll range of a scroll view is the overall height of all of its + * children.

+ * + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollRange() { + final int count = getChildCount(); + final int contentHeight = getHeight() - getPaddingBottom() - getPaddingTop(); + if (count == 0) { + return contentHeight; + } + + int scrollRange = getChildAt(0).getBottom(); + final int scrollY = getScrollY(); + final int overscrollBottom = Math.max(0, scrollRange - contentHeight); + if (scrollY < 0) { + scrollRange -= scrollY; + } else if (scrollY > overscrollBottom) { + scrollRange += scrollY - overscrollBottom; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollOffset() { + return Math.max(0, super.computeVerticalScrollOffset()); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeVerticalScrollExtent() { + return super.computeVerticalScrollExtent(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollRange() { + final int count = getChildCount(); + final int contentWidth = getWidth() - getPaddingLeft() - getPaddingRight(); + if (count == 0) { + return contentWidth; + } + + int scrollRange = getChildAt(0).getRight(); + final int scrollX = getScrollX(); + final int overscrollRight = Math.max(0, scrollRange - contentWidth); + if (scrollX < 0) { + scrollRange -= scrollX; + } else if (scrollX > overscrollRight) { + scrollRange += scrollX - overscrollRight; + } + + return scrollRange; + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollOffset() { + return super.computeHorizontalScrollOffset(); + } + + /** + * @hide + */ + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + @Override + public int computeHorizontalScrollExtent() { + return super.computeHorizontalScrollExtent(); + } + + @Override + protected void measureChild(View child, int parentWidthMeasureSpec, + int parentHeightMeasureSpec) { + ViewGroup.LayoutParams lp = child.getLayoutParams(); + + int childWidthMeasureSpec; + int childHeightMeasureSpec; + + childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, + int parentHeightMeasureSpec, int heightUsed) { + final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); + + final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.leftMargin + lp.rightMargin, MeasureSpec.UNSPECIFIED); + final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( + lp.topMargin + lp.bottomMargin, MeasureSpec.UNSPECIFIED); + + child.measure(childWidthMeasureSpec, childHeightMeasureSpec); + } + + @Override + public void computeScroll() { + if (mScroller.computeScrollOffset()) { + final int x = mScroller.getCurrX(); + final int y = mScroller.getCurrY(); + + int dx = x - mLastScrollerX; + int dy = y - mLastScrollerY; + + // Dispatch up to parent + if (dispatchNestedPreScroll(dx, dy, mScrollConsumed, null, ViewCompat.TYPE_NON_TOUCH)) { + dx -= mScrollConsumed[0]; + dy -= mScrollConsumed[1]; + } + + if (dy != 0 || dx != 0) { + final int rangeX = getScrollRangeX(); + final int rangeY = getScrollRangeY(); + final int oldScrollX = getScrollX(); + final int oldScrollY = getScrollY(); + + overScrollByCompat(dx, dy, oldScrollX, oldScrollY, rangeX, rangeY, 0, 0, false); + + final int scrolledDeltaX = getScrollX() - oldScrollX; + final int scrolledDeltaY = getScrollY() - oldScrollY; + final int unconsumedX = dx - scrolledDeltaX; + final int unconsumedY = dy - scrolledDeltaY; + + if (!dispatchNestedScroll(scrolledDeltaX, scrolledDeltaY, unconsumedX, unconsumedY, null, + ViewCompat.TYPE_NON_TOUCH)) { + final int mode = getOverScrollMode(); + final boolean canOverscroll = mode == OVER_SCROLL_ALWAYS + || (mode == OVER_SCROLL_IF_CONTENT_SCROLLS && (rangeX > 0 || rangeY > 0)); + if (canOverscroll) { + ensureGlows(); + if (x <= 0 && oldScrollX > 0) { + mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (x >= rangeX && oldScrollX < rangeX) { + mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity()); + } + if (y <= 0 && oldScrollY > 0) { + mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity()); + } else if (y >= rangeY && oldScrollY < rangeY) { + mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity()); + } + } + } + } + + // Finally update the scroll positions and post an invalidation + mLastScrollerX = x; + mLastScrollerY = y; + ViewCompat.postInvalidateOnAnimation(this); + } else { + // We can't scroll any more, so stop any indirect scrolling + if (hasNestedScrollingParent(ViewCompat.TYPE_NON_TOUCH)) { + stopNestedScroll(ViewCompat.TYPE_NON_TOUCH); + } + // and reset the scroller y + mLastScrollerX = 0; + mLastScrollerY = 0; + } + } + + /** + * Scrolls the view to the given child. + * + * @param child the View to scroll to + */ + private void scrollToChild(View child) { + child.getDrawingRect(mTempRect); + + /* Offset from child's local coordinates to ScrollView coordinates */ + offsetDescendantRectToMyCoords(child, mTempRect); + + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + + if (scrollDelta[0] != 0 || scrollDelta[1] != 0) { + scrollBy(scrollDelta[0], scrollDelta[1]); + } + } + + /** + * If rect is off screen, scroll just enough to get it (or at least the + * first screen size chunk of it) on screen. + * + * @param rect The rectangle. + * @param immediate True to scroll immediately without animation + * @return true if scrolling was performed + */ + private boolean scrollToChildRect(Rect rect, boolean immediate) { + final int[] delta = computeScrollDeltaToGetChildRectOnScreen(rect); + final boolean scroll = delta[0] != 0 || delta[1] != 0; + if (scroll) { + if (immediate) { + scrollBy(delta[0], delta[1]); + } else { + smoothScrollBy(delta[0], delta[1]); + } + } + return scroll; + } + + /** + * Compute the amount to scroll in the Y direction in order to get + * a rectangle completely on the screen (or, if taller than the screen, + * at least the first screen size chunk of it). + * + * @param rect The rect. + * @return The scroll delta. + */ + protected int[] computeScrollDeltaToGetChildRectOnScreen(Rect rect) { + if (getChildCount() == 0) return new int[]{0, 0}; + + int width = getWidth(); + int height = getHeight(); + int screenTop = getScrollY(); + int screenLeft = getScrollX(); + int screenRight = screenLeft + width; + int screenBottom = screenTop + height; + + int fadingEdgeX = getHorizontalFadingEdgeLength(); + int fadingEdgeY = getVerticalFadingEdgeLength(); + + // leave room for top fading edge as long as rect isn't at very top + if (rect.top > 0) { + screenTop += fadingEdgeY; + } + if (rect.left > 0) { + screenLeft += fadingEdgeX; + } + if (rect.right < getChildAt(0).getHeight()) { + screenRight -= fadingEdgeX; + } + // leave room for bottom fading edge as long as rect isn't at very bottom + if (rect.bottom < getChildAt(0).getHeight()) { + screenBottom -= fadingEdgeY; + } + + int scrollXDelta = 0; + int scrollYDelta = 0; + if (rect.right > screenRight && rect.left > screenLeft) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.width() > width) { + // just enough to get screen size chunk on + scrollXDelta += (rect.left - screenLeft); + } else { + // get entire rect at bottom of screen + scrollXDelta += (rect.right - screenRight); + } + + // make sure we aren't scrolling beyond the end of our content + int right = getChildAt(0).getRight(); + int distanceToRight = right - screenRight; + scrollXDelta = Math.min(scrollXDelta, distanceToRight); + } else if (rect.left < screenLeft && rect.right < screenRight) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.width() > width) { + // screen size chunk + scrollXDelta -= (screenRight - rect.right); + } else { + // entire rect at top + scrollXDelta -= (screenLeft - rect.left); + } + + // make sure we aren't scrolling any further than the top our content + scrollXDelta = Math.max(scrollXDelta, -getScrollX()); + } + if (rect.bottom > screenBottom && rect.top > screenTop) { + // need to move down to get it in view: move down just enough so + // that the entire rectangle is in view (or at least the first + // screen size chunk). + + if (rect.height() > height) { + // just enough to get screen size chunk on + scrollYDelta += (rect.top - screenTop); + } else { + // get entire rect at bottom of screen + scrollYDelta += (rect.bottom - screenBottom); + } + + // make sure we aren't scrolling beyond the end of our content + int bottom = getChildAt(0).getBottom(); + int distanceToBottom = bottom - screenBottom; + scrollYDelta = Math.min(scrollYDelta, distanceToBottom); + + } else if (rect.top < screenTop && rect.bottom < screenBottom) { + // need to move up to get it in view: move up just enough so that + // entire rectangle is in view (or at least the first screen + // size chunk of it). + + if (rect.height() > height) { + // screen size chunk + scrollYDelta -= (screenBottom - rect.bottom); + } else { + // entire rect at top + scrollYDelta -= (screenTop - rect.top); + } + + // make sure we aren't scrolling any further than the top our content + scrollYDelta = Math.max(scrollYDelta, -getScrollY()); + } + return new int[]{scrollXDelta, scrollYDelta}; + } + + @Override + public void requestChildFocus(View child, View focused) { + if (!mIsLayoutDirty) { + scrollToChild(focused); + } else { + // The child may not be laid out yet, we can't compute the scroll yet + mChildToScrollTo = focused; + } + super.requestChildFocus(child, focused); + } + + + /** + * When looking for focus in children of a scroll view, need to be a little + * more careful not to give focus to something that is scrolled off screen. + *

+ * This is more expensive than the default {@link android.view.ViewGroup} + * implementation, otherwise this behavior might have been made the default. + */ + @Override + protected boolean onRequestFocusInDescendants(int direction, + Rect previouslyFocusedRect) { + + // convert from forward / backward notation to up / down / left / right + // (ugh). + if (direction == View.FOCUS_FORWARD) { + direction = View.FOCUS_DOWN; + } else if (direction == View.FOCUS_BACKWARD) { + direction = View.FOCUS_UP; + } + + final View nextFocus = previouslyFocusedRect == null + ? FocusFinder.getInstance().findNextFocus(this, null, direction) + : FocusFinder.getInstance().findNextFocusFromRect( + this, previouslyFocusedRect, direction); + + if (nextFocus == null) { + return false; + } + + if (isOffScreen(nextFocus)) { + return false; + } + + return nextFocus.requestFocus(direction, previouslyFocusedRect); + } + + @Override + public boolean requestChildRectangleOnScreen(View child, Rect rectangle, + boolean immediate) { + // offset into coordinate space of this scroll view + rectangle.offset(child.getLeft() - child.getScrollX(), + child.getTop() - child.getScrollY()); + + return scrollToChildRect(rectangle, immediate); + } + + @Override + public void requestLayout() { + mIsLayoutDirty = true; + super.requestLayout(); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mIsLayoutDirty = false; + // Give a child focus if it needs it + if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) { + scrollToChild(mChildToScrollTo); + } + mChildToScrollTo = null; + + if (!mIsLaidOut) { + if (mSavedState != null) { + scrollTo(mSavedState.scrollPositionX, mSavedState.scrollPositionY); + mSavedState = null; + } // mScrollY default value is "0" + + final int childWidth = (getChildCount() > 0) ? getChildAt(0).getMeasuredWidth() : 0; + final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0; + final int scrollRangeX = Math.max(0, + childWidth - (b - t - getPaddingLeft() - getPaddingRight())); + final int scrollRangeY = Math.max(0, + childHeight - (b - t - getPaddingBottom() - getPaddingTop())); + + // Don't forget to clamp + int scrollX = Math.max(0, Math.min(scrollRangeX, getScrollX())); + int scrollY = Math.max(0, Math.min(scrollRangeY, getScrollY())); + scrollTo(scrollX, scrollY); + } + + // Calling this with the present values causes it to re-claim them + scrollTo(getScrollX(), getScrollY()); + mIsLaidOut = true; + } + + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + + mIsLaidOut = false; + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + View currentFocused = findFocus(); + if (null == currentFocused || this == currentFocused) { + return; + } + + // If the currently-focused view was visible on the screen when the + // screen was at the old height, then scroll the screen to make that + // view visible with the new screen height. + if (isWithinDeltaOfScreen(currentFocused, oldw, oldh)) { + currentFocused.getDrawingRect(mTempRect); + offsetDescendantRectToMyCoords(currentFocused, mTempRect); + int[] scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); + doScroll(scrollDelta); + } + } + + /** + * Return true if child is a descendant of parent, (or equal to the parent). + */ + private static boolean isViewDescendantOf(View child, View parent) { + if (child == parent) { + return true; + } + + final ViewParent theParent = child.getParent(); + return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent); + } + + /** + * Fling the scroll view + * + * @param velocityX The initial velocity in the X direction. Positive + * numbers mean that the finger/cursor is moving right to the screen, + * which means we want to scroll towards the left. + * @param velocityY The initial velocity in the Y direction. Positive + * numbers mean that the finger/cursor is moving down the screen, + * which means we want to scroll towards the top. + */ + public void fling(int velocityX, int velocityY) { + if (getChildCount() > 0) { + int axes = getAxes(); + startNestedScroll(axes, ViewCompat.TYPE_NON_TOUCH); + mScroller.fling(getScrollX(), getScrollY(), // start + velocityX, velocityY, // velocities + Integer.MIN_VALUE, Integer.MAX_VALUE, // x + Integer.MIN_VALUE, Integer.MAX_VALUE, // y + 0, 0); // overscroll + mLastScrollerX = getScrollX(); + mLastScrollerY = getScrollY(); + ViewCompat.postInvalidateOnAnimation(this); + } + } + + private void flingWithNestedDispatch(int velocityX, int velocityY) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + final boolean canFling = ((scrollY > 0 || velocityY > 0) + && (scrollY < getScrollRangeY() || velocityY < 0)) + || ((scrollX > 0 || velocityX > 0) + && (scrollX < getScrollRangeX() || velocityX < 0)); + if (!dispatchNestedPreFling(velocityX, velocityY)) { + dispatchNestedFling(velocityX, velocityY, canFling); + fling(velocityX, velocityY); + } + + } + + private void endDrag() { + mIsBeingDragged = false; + + recycleVelocityTracker(); + stopNestedScroll(ViewCompat.TYPE_TOUCH); + + if (mEdgeGlowTop != null) { + mEdgeGlowTop.onRelease(); + mEdgeGlowBottom.onRelease(); + mEdgeGlowLeft.onRelease(); + mEdgeGlowRight.onRelease(); + } + } + + /** + * {@inheritDoc} + *

+ *

This version also clamps the scrolling to the bounds of our child. + */ + @Override + public void scrollTo(int x, int y) { + // we rely on the fact the View.scrollBy calls scrollTo. + if (getChildCount() > 0) { + View child = getChildAt(0); + x = clamp(x, getWidth() - getPaddingRight() - getPaddingLeft(), child.getWidth()); + y = clamp(y, getHeight() - getPaddingBottom() - getPaddingTop(), child.getHeight()); + if (x != getScrollX() || y != getScrollY()) { + super.scrollTo(x, y); + } + } + } + + private void ensureGlows() { + if (getOverScrollMode() != View.OVER_SCROLL_NEVER) { + if (mEdgeGlowTop == null) { + Context context = getContext(); + mEdgeGlowTop = new EdgeEffect(context); + mEdgeGlowBottom = new EdgeEffect(context); + mEdgeGlowLeft = new EdgeEffect(context); + mEdgeGlowRight = new EdgeEffect(context); + } + } else { + mEdgeGlowTop = null; + mEdgeGlowBottom = null; + mEdgeGlowLeft = null; + mEdgeGlowRight = null; + } + } + + @Override + public void draw(Canvas canvas) { + super.draw(canvas); + if (mEdgeGlowTop != null) { + final int scrollX = getScrollX(); + final int scrollY = getScrollY(); + if (!mEdgeGlowTop.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + + canvas.translate(getPaddingLeft(), Math.min(0, scrollY)); + mEdgeGlowTop.setSize(width, getHeight()); + if (mEdgeGlowTop.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowLeft.isFinished()) { + final int restoreCount = canvas.save(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.translate(getPaddingTop(), Math.min(0, scrollX)); + mEdgeGlowLeft.setSize(getWidth(), height); + if (mEdgeGlowLeft.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowRight.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth(); + final int height = getHeight() - getPaddingTop() - getPaddingBottom(); + + canvas.translate(Math.max(getScrollRangeX(), scrollX) + width, -height + getPaddingTop()); + canvas.rotate(180, 0, height); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + if (!mEdgeGlowBottom.isFinished()) { + final int restoreCount = canvas.save(); + final int width = getWidth() - getPaddingLeft() - getPaddingRight(); + final int height = getHeight(); + + canvas.translate(-width + getPaddingLeft(), + Math.max(getScrollRangeY(), scrollY) + height); + canvas.rotate(180, width, 0); + mEdgeGlowBottom.setSize(width, height); + if (mEdgeGlowBottom.draw(canvas)) { + ViewCompat.postInvalidateOnAnimation(this); + } + canvas.restoreToCount(restoreCount); + } + } + } + + private static int clamp(int n, int my, int child) { + if (my >= child || n < 0) { + /* my >= child is this case: + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * or + * |--------------- me ---------------| + * |------ child ------| + * + * n < 0 is this case: + * |------ me ------| + * |-------- child --------| + * |-- mScrollX --| + */ + return 0; + } + if ((my + n) > child) { + /* this case: + * |------ me ------| + * |------ child ------| + * |-- mScrollX --| + */ + return child - my; + } + return n; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + if (!(state instanceof SavedState)) { + super.onRestoreInstanceState(state); + return; + } + + SavedState ss = (SavedState) state; + super.onRestoreInstanceState(ss.getSuperState()); + mSavedState = ss; + requestLayout(); + } + + @Override + protected Parcelable onSaveInstanceState() { + Parcelable superState = super.onSaveInstanceState(); + SavedState ss = new SavedState(superState); + ss.scrollPositionX = getScrollX(); + ss.scrollPositionY = getScrollY(); + return ss; + } + + static class SavedState extends BaseSavedState { + public int scrollPositionX; + public int scrollPositionY; + + SavedState(Parcelable superState) { + super(superState); + } + + SavedState(Parcel source) { + super(source); + scrollPositionX = source.readInt(); + scrollPositionY = source.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(scrollPositionX); + dest.writeInt(scrollPositionY); + } + + @Override + public String toString() { + return "HorizontalScrollView.SavedState{" + + Integer.toHexString(System.identityHashCode(this)) + + " scrollPositionX=" + scrollPositionX + + ", scrollPositionY=" + scrollPositionY + + "}"; + } + + public static final Parcelable.Creator CREATOR = + new Parcelable.Creator() { + @Override + public SavedState createFromParcel(Parcel in) { + return new SavedState(in); + } + + @Override + public SavedState[] newArray(int size) { + return new SavedState[size]; + } + }; + } + + static class AccessibilityDelegate extends AccessibilityDelegateCompat { + @Override + public boolean performAccessibilityAction(View host, int action, Bundle arguments) { + if (super.performAccessibilityAction(host, action, arguments)) { + return true; + } + final HVScrollView nsvHost = (HVScrollView) host; + if (!nsvHost.isEnabled()) { + return false; + } + switch (action) { + case AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.min(nsvHost.getScrollY() + viewportHeight, + nsvHost.getScrollRangeY()); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + case AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD: { + final int viewportHeight = nsvHost.getHeight() - nsvHost.getPaddingBottom() + - nsvHost.getPaddingTop(); + final int targetScrollY = Math.max(nsvHost.getScrollY() - viewportHeight, 0); + if (targetScrollY != nsvHost.getScrollY()) { + nsvHost.smoothScrollTo(0, targetScrollY); + return true; + } + } + return false; + } + return false; + } + + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + super.onInitializeAccessibilityNodeInfo(host, info); + final HVScrollView nsvHost = (HVScrollView) host; + info.setClassName(ScrollView.class.getName()); + if (nsvHost.isEnabled()) { + final int scrollRange = nsvHost.getScrollRangeY(); + if (scrollRange > 0) { + info.setScrollable(true); + if (nsvHost.getScrollY() > 0) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + } + if (nsvHost.getScrollY() < scrollRange) { + info.addAction(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + } + } + } + } + + @Override + public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) { + super.onInitializeAccessibilityEvent(host, event); + final HVScrollView nsvHost = (HVScrollView) host; + event.setClassName(ScrollView.class.getName()); + final boolean scrollable = nsvHost.getScrollRangeY() > 0; + event.setScrollable(scrollable); + event.setScrollX(nsvHost.getScrollX()); + event.setScrollY(nsvHost.getScrollY()); + AccessibilityRecordCompat.setMaxScrollX(event, nsvHost.getScrollX()); + AccessibilityRecordCompat.setMaxScrollY(event, nsvHost.getScrollRangeY()); + } + } +} \ No newline at end of file diff --git a/doric/src/main/res/drawable-xhdpi/doric_icon_back.png b/doric/src/main/res/drawable-xhdpi/doric_icon_back.png new file mode 100644 index 0000000000000000000000000000000000000000..6679febdbe96c991775bbcc24409019685024c18 GIT binary patch literal 1098 zcmV-Q1hxB#P)Px(1W80eRA>e5SNa^nKxz<0b0-x-SOi!R6g^=HC0h z@0|1QJ?D+jSG}qhs7wnqH8u4&Ha5<&MWfMPyDa>6Cpwu2&?n2~@*S)Z;5vT4e{N}M zDPd>L-wW88Xm<|4p9lE6m@l7g+mLb*yO>PRL)Z)f{54FFfQx}Z;Ed@-l!0;~(A?|;m9a&sld@l7?`ys0ntc*{k zQbAb$9_E}-=6!{H2;ko+dds3V4|H^NY?+;%eE<&(D5$cbP^f2NVPQ(q+Y+@9h{xl# zD=RAx0e;i|j*$6`b^w-tZwu`oCb>?axw$z~EEf4vb}7$G*VNP`=jZ1?DSAhutJ`QI zk%+FXt&IUumqZ1t&!Ba>4W7?%%>ykhE!$RCSD#_cb_pK+X;{7o;J+w(S43AlfNe5f zC=^}+(I)V)`b2$w{eE5GX#vL`faQ0>^4!Z?xuCdSVz=qTZu4E}T<<#a04%>7mVXUY ztr8h5KibgH(5o9fpJ|%_cAGueCdbjXE8s`k+S-n$)9Jj_>+0Jg0L$;iR`~*;ViFiE zKb%Y^k4;Wa7NlNh-|{z zBQyEHpviH*a5(%C9!O!lL7Lt>2z@b^%keDuFPCWoNRMYCk;q$+=m*k`LI`pW)9-+R>@VBsZM+Yj??gs882J8qU$0?%Fpy=Tyaqw;KFdg{)ZMr20Rs?I37p1E zpViZtsl;)wPGj2yNCXz&g?C^KP0fc!>+0$TGMP-yAgt@|hyX!VbUIA}EE92{b)wBs z7FQR`OO}~J=X%$%2Mk0NT~d++%SM)u@Wg~cYW?2Ma+77JqIX4f%>xFSivFxZ5-cNG zPQnwT2C4OXE6Y!op^Dy-X!#&B^E8%|EGt1`M2_cni#ER1~l{Kd;|_e$$^AAm+;Fb!{FnhyZAr0l_?nVwpiZ Qe*gdg07*qoM6N<$f?O;2W&i*H literal 0 HcmV?d00001 diff --git a/doric/src/main/res/drawable/divider.xml b/doric/src/main/res/drawable/divider.xml new file mode 100644 index 00000000..5efa3f64 --- /dev/null +++ b/doric/src/main/res/drawable/divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/doric/src/main/res/layout/doric_activity.xml b/doric/src/main/res/layout/doric_activity.xml new file mode 100644 index 00000000..54b71a1b --- /dev/null +++ b/doric/src/main/res/layout/doric_activity.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/doric/src/main/res/layout/doric_fragment.xml b/doric/src/main/res/layout/doric_fragment.xml new file mode 100644 index 00000000..7af124c8 --- /dev/null +++ b/doric/src/main/res/layout/doric_fragment.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/doric/src/main/res/layout/doric_framgent_panel.xml b/doric/src/main/res/layout/doric_framgent_panel.xml new file mode 100644 index 00000000..237f04c3 --- /dev/null +++ b/doric/src/main/res/layout/doric_framgent_panel.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/doric/src/main/res/layout/doric_modal_prompt.xml b/doric/src/main/res/layout/doric_modal_prompt.xml new file mode 100644 index 00000000..1e0c88f0 --- /dev/null +++ b/doric/src/main/res/layout/doric_modal_prompt.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/doric/src/main/res/layout/doric_navigator.xml b/doric/src/main/res/layout/doric_navigator.xml new file mode 100644 index 00000000..fbc384af --- /dev/null +++ b/doric/src/main/res/layout/doric_navigator.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doric/src/main/res/values-v21/styles.xml b/doric/src/main/res/values-v21/styles.xml new file mode 100644 index 00000000..5c99d8c1 --- /dev/null +++ b/doric/src/main/res/values-v21/styles.xml @@ -0,0 +1,5 @@ + + + + + +