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 00000000..898f3ed5 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ 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 00000000..dffca360 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 00000000..64ba76f7 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 00000000..dae5e082 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 00000000..e5ed4659 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ 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 00000000..14ed0af3 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ 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 00000000..b0907cac Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ 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 00000000..d8ae0315 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ 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 00000000..2c18de9e Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ 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 00000000..beed3cdd Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ 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 00000000..6679febd Binary files /dev/null and b/doric/src/main/res/drawable-xhdpi/doric_icon_back.png differ 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 @@ + + + + + +