├── .gitignore ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── cxz │ │ └── jswebview │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ └── index.html │ ├── java │ │ └── com │ │ │ └── cxz │ │ │ └── jswebview │ │ │ └── sample │ │ │ ├── JsBridge.java │ │ │ ├── JsInterface.java │ │ │ └── MainActivity.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_main.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── cxz │ └── jswebview │ └── sample │ └── ExampleUnitTest.java ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── sample ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── cxz │ │ └── webview │ │ └── sample │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── index1.html │ │ └── js-native.js │ ├── java │ │ └── com │ │ │ └── cxz │ │ │ └── webview │ │ │ └── sample │ │ │ ├── JsBridge.java │ │ │ ├── JsInterface.java │ │ │ ├── SampleActivity.java │ │ │ └── util │ │ │ ├── Base64Util.java │ │ │ └── RealPathUtil.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ └── ic_launcher_background.xml │ │ ├── layout │ │ └── activity_sample.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── com │ └── cxz │ └── webview │ └── sample │ └── ExampleUnitTest.java └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | .externalNativeBuild 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JsWebViewSample(Android 和 JS 交互实践) 2 | 3 | `Android` 与 `JS` 交互实际上是通过 `WebView` 互相调用方法: 4 | 5 | - `Android` 去调用 `JS` 的代码; 6 | - `JS` 去调用 `Android` 的代码。 7 | 8 | ## 一、JS 调用 Android 方法 9 | 10 | #### 方法一:通过 WebView 的 addJavascriptInterface() 进行对象映射 11 | 12 | > 优点:使用简单,仅将Android对象和JS对象映射即可 13 | 14 | > 缺点:存在漏洞问题 15 | 16 | ###### 1)允许 WebView 加载 JS 17 | `webView.getSettings().setJavaScriptEnabled(true);` 18 | 19 | ###### 2)编写 JS 接口 20 | ``` 21 | public class JsInterface { 22 | private static final String TAG = "JsInterface"; 23 | private JsBridge jsBridge; 24 | public JsInterface(JsBridge jsBridge) { 25 | this.jsBridge = jsBridge; 26 | } 27 | /** 28 | * 这个方法由 JS 调用, 不在主线程执行 29 | * 30 | * @param value 31 | */ 32 | @JavascriptInterface 33 | public void callAndroid(String value) { 34 | Log.i(TAG, "value = " + value); 35 | jsBridge.setTextValue(value); 36 | } 37 | } 38 | ``` 39 | 40 | ###### 3)给 WebView 添加 JS 接口 41 | `webView.addJavascriptInterface(new JsInterface(this), "launcher");// 此处的 launcher 可以自定义,最终是 JS 中要使用的对象 ` 42 | 43 | ###### 4)JS 代码中调用 Java 方法 44 | ``` 45 | if (window.launcher){ // 判断 launcher 对象是否存在 46 | // 此处的 launcher 要和 第3步中定义的 launcher 保持一致 47 | // JS 调用 Android 的方法 48 | launcher.callAndroid(str); 49 | }else{ 50 | alert("launcher not found!"); 51 | } 52 | ``` 53 | 54 | #### 方法二:通过 WebViewClient 的 shouldOverrideUrlLoading() 方法回调拦截 url 55 | 56 | > 优点:不存在方式一的漏洞; 57 | 58 | > 缺点:JS获取Android方法的返回值复杂。 59 | 60 | ###### 1)JS 代码中,约定协议 61 | ``` 62 | function callAndroid(){ 63 | // 约定的 url 协议为:js://webview?arg1=111&arg2=222 64 | document.location = "js://webview?arg1="+inputEle.value+"&arg2=222"; 65 | } 66 | ``` 67 | 68 | ###### 2)Android 代码中,通过设置 WebViewClient 对协议进行拦截处理 69 | 70 | ``` 71 | webView.setWebViewClient(new WebViewClient() { 72 | @Override 73 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 74 | // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) 75 | // 例如:url = "js://webview?arg1=111&arg2=222" 76 | Uri uri = Uri.parse(url); 77 | // 如果url的协议 = 预先约定的 js 协议 78 | if (uri.getScheme().equals("js")) { 79 | // 拦截url,下面JS开始调用Android需要的方法 80 | if (uri.getAuthority().equals("webview")) { 81 | // 执行JS所需要调用的逻辑 82 | Log.e("TAG", "JS 调用了 Android 的方法"); 83 | Set collection = uri.getQueryParameterNames(); 84 | Iterator it = collection.iterator(); 85 | String result = ""; 86 | while (it.hasNext()) { 87 | result += uri.getQueryParameter(it.next()) + ","; 88 | } 89 | tv_result.setText(result); 90 | } 91 | return true; 92 | } 93 | return super.shouldOverrideUrlLoading(view, url); 94 | } 95 | }); 96 | ``` 97 | 98 | #### 方法三:通过 WebChromeClient 的 onJsAlert() 、 onJsConfirm() 、 onJsPrompt()方法回调拦截 JS 对话框 alert() 、 confirm() 、 prompt() 消息 99 | 100 | > 处理方式和方法二差不多 101 | 102 | ###### 1)JS代码中,约定协议 103 | ``` 104 | // 调用 prompt() 105 | var result=prompt("js://prompt?arg1="+inputEle.value+"&arg2=222"); 106 | alert("prompt:" + result); 107 | ``` 108 | 109 | ###### 2)Android 代码中,通过设置 WebChromeClient 对协议进行拦截处理 110 | ``` 111 | webView.setWebChromeClient(new WebChromeClient() { 112 | @Override 113 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 114 | // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) 115 | // 例如:url = "js://webview?arg1=111&arg2=222" 116 | Uri uri = Uri.parse(message); 117 | Log.e("TAG", "----onJsPrompt--->>" + url + "," + message); 118 | // 如果url的协议 = 预先约定的 js 协议 119 | if (uri.getScheme().equals("js")) { 120 | // 拦截url,下面JS开始调用Android需要的方法 121 | if (uri.getAuthority().equals("prompt")) { 122 | // 执行JS所需要调用的逻辑 123 | Log.e("TAG", "JS 调用了 Android 的方法"); 124 | Set collection = uri.getQueryParameterNames(); 125 | Iterator it = collection.iterator(); 126 | String result2 = ""; 127 | while (it.hasNext()) { 128 | result2 += uri.getQueryParameter(it.next()) + ","; 129 | } 130 | tv_result.setText(result2); 131 | } 132 | return true; 133 | } 134 | return super.onJsPrompt(view, url, message, defaultValue, result); 135 | } 136 | 137 | @Override 138 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 139 | Log.e("TAG", "----onJsAlert--->>" + url+ "," + message); 140 | return super.onJsAlert(view, url, message, result); 141 | } 142 | 143 | @Override 144 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 145 | Log.e("TAG", "----onJsConfirm--->>" + url+ "," + message); 146 | return super.onJsConfirm(view, url, message, result); 147 | } 148 | }); 149 | ``` 150 | 151 | ## 二、 Android 调用 JS 方法 152 | 153 | #### 方法一: 通过 WebView 的 loadUrl() 154 | 155 | ###### 1)编写 JS 方法 156 | ``` 157 | var callJS = function(str){ 158 | inputEle.value = str; 159 | } 160 | ``` 161 | 162 | ###### 2)使用 webView.loadUrl() 调用 JS 方法 163 | ``` 164 | // Android 调用 JS 方法 165 | webView.loadUrl("javascript:if(window.callJS){window.callJS('" + str + "');}"); 166 | ``` 167 | 168 | #### 方法二: 通过 WebView 的 evaluateJavascript() 169 | > - 该方法比第一种方法效率更高,使用更简洁; 170 | > - 该方法执行不会刷新页面,而第一种方法( loadUrl )则会; 171 | > - Android 4.4 以后才能使用。 172 | 173 | ``` 174 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 175 | webView.evaluateJavascript("javascript:if(window.callJS){window.callJS('" + str + "');}", new ValueCallback() { 176 | @Override 177 | public void onReceiveValue(String value) { 178 | Log.e("TAG", "--------->>" + value); 179 | } 180 | }); 181 | } 182 | ``` 183 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | defaultConfig { 6 | applicationId "com.cxz.jswebview.sample" 7 | minSdkVersion 15 8 | targetSdkVersion 28 9 | versionCode 1 10 | versionName "1.0" 11 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | implementation fileTree(dir: 'libs', include: ['*.jar']) 23 | implementation 'com.android.support:appcompat-v7:28.0.0' 24 | implementation 'com.android.support.constraint:constraint-layout:1.1.3' 25 | testImplementation 'junit:junit:4.12' 26 | androidTestImplementation 'com.android.support.test:runner:1.0.2' 27 | androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' 28 | } 29 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/cxz/jswebview/sample/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.cxz.jswebview.sample; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumented test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("com.cxz.jswebview.sample", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/assets/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | WebView 7 | 8 | 15 | 16 | 17 | 18 | 19 |

WebView

20 | 21 |
22 | 请输入要传递的值: 23 |
24 |
25 | 26 | 27 | 28 |
29 | 30 | 64 | 65 | -------------------------------------------------------------------------------- /app/src/main/java/com/cxz/jswebview/sample/JsBridge.java: -------------------------------------------------------------------------------- 1 | package com.cxz.jswebview.sample; 2 | 3 | /** 4 | * @author chenxz 5 | * @date 2018/9/12 6 | * @desc 7 | */ 8 | public interface JsBridge { 9 | 10 | void setTextValue(String value); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/com/cxz/jswebview/sample/JsInterface.java: -------------------------------------------------------------------------------- 1 | package com.cxz.jswebview.sample; 2 | 3 | import android.util.Log; 4 | import android.webkit.JavascriptInterface; 5 | 6 | /** 7 | * @author chenxz 8 | * @date 2018/9/12 9 | * @desc 编写 JS 接口类 10 | */ 11 | public class JsInterface { 12 | 13 | private static final String TAG = "JsInterface"; 14 | 15 | private JsBridge jsBridge; 16 | 17 | public JsInterface(JsBridge jsBridge) { 18 | this.jsBridge = jsBridge; 19 | } 20 | 21 | /** 22 | * 这个方法由 JS 调用, 不在主线程执行 23 | * 24 | * @param value 25 | */ 26 | @JavascriptInterface 27 | public void callAndroid(String value) { 28 | Log.i(TAG, "value = " + value); 29 | jsBridge.setTextValue(value); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/cxz/jswebview/sample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.cxz.jswebview.sample; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.os.Handler; 7 | import android.support.v7.app.AppCompatActivity; 8 | import android.util.Log; 9 | import android.view.View; 10 | import android.webkit.JsPromptResult; 11 | import android.webkit.JsResult; 12 | import android.webkit.WebChromeClient; 13 | import android.webkit.WebSettings; 14 | import android.webkit.WebView; 15 | import android.webkit.WebViewClient; 16 | import android.widget.Button; 17 | import android.widget.EditText; 18 | import android.widget.TextView; 19 | 20 | import java.util.Iterator; 21 | import java.util.Set; 22 | 23 | public class MainActivity extends AppCompatActivity implements JsBridge { 24 | 25 | 26 | private WebView webView; 27 | private TextView tv_result; 28 | private EditText editText; 29 | private Button button; 30 | private Handler mHandler; 31 | 32 | @Override 33 | protected void onCreate(Bundle savedInstanceState) { 34 | super.onCreate(savedInstanceState); 35 | setContentView(R.layout.activity_main); 36 | 37 | mHandler = new Handler(); 38 | 39 | initViews(); 40 | 41 | } 42 | 43 | @SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"}) 44 | private void initViews() { 45 | webView = findViewById(R.id.webView); 46 | tv_result = findViewById(R.id.tv_result); 47 | editText = findViewById(R.id.editText); 48 | button = findViewById(R.id.button); 49 | 50 | WebSettings settings = webView.getSettings(); 51 | 52 | // 允许 WebView 加载 JS 代码 53 | settings.setJavaScriptEnabled(true); 54 | // 允许 JS 弹窗 55 | settings.setJavaScriptCanOpenWindowsAutomatically(true); 56 | 57 | // 给 WebView 添加 JS 接口 58 | // 此处的 launcher 可以自定义,最终是 JS 中要使用的对象 59 | webView.addJavascriptInterface(new JsInterface(this), "launcher"); 60 | 61 | webView.loadUrl("file:///android_asset/index.html"); 62 | 63 | button.setOnClickListener(new View.OnClickListener() { 64 | @Override 65 | public void onClick(View v) { 66 | String str = editText.getText().toString(); 67 | // Android 调用 JS 方法 68 | webView.loadUrl("javascript:if(window.callJS){window.callJS('" + str + "');}"); 69 | 70 | // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 71 | // webView.evaluateJavascript("javascript:if(window.callJS){window.callJS('" + str + "');}", new ValueCallback() { 72 | // @Override 73 | // public void onReceiveValue(String value) { 74 | // Log.e("TAG", "--------->>" + value); 75 | // } 76 | // }); 77 | // } 78 | } 79 | }); 80 | 81 | webView.setWebViewClient(new WebViewClient() { 82 | @Override 83 | public boolean shouldOverrideUrlLoading(WebView view, String url) { 84 | // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) 85 | // 例如:url = "js://webview?arg1=111&arg2=222" 86 | Uri uri = Uri.parse(url); 87 | // 如果url的协议 = 预先约定的 js 协议 88 | if (uri.getScheme().equals("js")) { 89 | // 拦截url,下面JS开始调用Android需要的方法 90 | if (uri.getAuthority().equals("webview")) { 91 | // 执行JS所需要调用的逻辑 92 | Log.e("TAG", "JS 调用了 Android 的方法"); 93 | Set collection = uri.getQueryParameterNames(); 94 | Iterator it = collection.iterator(); 95 | String result = ""; 96 | while (it.hasNext()) { 97 | result += uri.getQueryParameter(it.next()) + ","; 98 | } 99 | tv_result.setText(result); 100 | } 101 | return true; 102 | } 103 | return super.shouldOverrideUrlLoading(view, url); 104 | } 105 | }); 106 | 107 | webView.setWebChromeClient(new WebChromeClient() { 108 | @Override 109 | public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { 110 | // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数) 111 | // 例如:url = "js://webview?arg1=111&arg2=222" 112 | Uri uri = Uri.parse(message); 113 | Log.e("TAG", "----onJsPrompt--->>" + url + "," + message); 114 | // 如果url的协议 = 预先约定的 js 协议 115 | if (uri.getScheme().equals("js")) { 116 | // 拦截url,下面JS开始调用Android需要的方法 117 | if (uri.getAuthority().equals("prompt")) { 118 | // 执行JS所需要调用的逻辑 119 | Log.e("TAG", "JS 调用了 Android 的方法"); 120 | Set collection = uri.getQueryParameterNames(); 121 | Iterator it = collection.iterator(); 122 | String result2 = ""; 123 | while (it.hasNext()) { 124 | result2 += uri.getQueryParameter(it.next()) + ","; 125 | } 126 | tv_result.setText(result2); 127 | } 128 | return true; 129 | } 130 | return super.onJsPrompt(view, url, message, defaultValue, result); 131 | } 132 | 133 | @Override 134 | public boolean onJsAlert(WebView view, String url, String message, JsResult result) { 135 | Log.e("TAG", "----onJsAlert--->>" + url+ "," + message); 136 | return super.onJsAlert(view, url, message, result); 137 | } 138 | 139 | @Override 140 | public boolean onJsConfirm(WebView view, String url, String message, JsResult result) { 141 | Log.e("TAG", "----onJsConfirm--->>" + url+ "," + message); 142 | return super.onJsConfirm(view, url, message, result); 143 | } 144 | }); 145 | 146 | } 147 | 148 | @Override 149 | public void setTextValue(final String value) { 150 | mHandler.post(new Runnable() { 151 | @Override 152 | public void run() { 153 | tv_result.setText(value); 154 | } 155 | }); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 13 | 14 | 22 | 23 | 27 | 28 | 33 | 34 | 33 |
34 | 35 |
36 | 37 | 38 | -------------------------------------------------------------------------------- /sample/src/main/assets/js-native.js: -------------------------------------------------------------------------------- 1 | //callback容器 2 | var SDKNativeEvents = {} 3 | 4 | /** 5 | * 存入传来的callback回调,加入唯一键名,使调用相同的功能可以并发 6 | * funcName native的方法名 7 | * data 参数 8 | * callback 回调匿名方法 9 | */ 10 | function sdk_launchFunc(funcName,data,callback){ 11 | 12 | if(!data){ 13 | alert("必须传入data"); 14 | return; 15 | } 16 | 17 | if(!callback){ 18 | alert("必须传入回调function"); 19 | return; 20 | } 21 | var newName = funcName += getUniqueKey(); 22 | SDKNativeEvents[newName] = callback; 23 | 24 | launcher.native_launchFunc(newName,JSON.stringify(data)); 25 | } 26 | 27 | /** 28 | * native回调 本地做完事情后将funcName和data传过来,调用之前h5预留的匿名函数 29 | * funcName 对应的触发方法名 30 | * data 参数 31 | */ 32 | function sdk_nativeCallback(funcName,data){ 33 | var obj= JSON.parse(data); 34 | try{ 35 | if(SDKNativeEvents[funcName]){ 36 | SDKNativeEvents[funcName](obj); 37 | SDKNativeEvents[funcName] = null; 38 | } 39 | }catch(e){ 40 | alert(e); 41 | } 42 | } 43 | 44 | //工具--生成唯一键 45 | 46 | var chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; 47 | 48 | function randomStr() { 49 | var res = ""; 50 | for(var i = 0; i < 5; i++) { 51 | var id = Math.ceil(Math.random() * 35); 52 | res += chars[id]; 53 | } 54 | return res; 55 | } 56 | 57 | function getUniqueKey(){ 58 | return randomStr()+""+(new Date()).getTime(); 59 | } 60 | 61 | window.launcher.pickPhoto = function(data, callback) { 62 | sdk_launchFunc("pickPhoto", data, callback); 63 | } 64 | 65 | -------------------------------------------------------------------------------- /sample/src/main/java/com/cxz/webview/sample/JsBridge.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample; 2 | 3 | /** 4 | * @author chenxz 5 | * @date 2018/9/12 6 | * @desc 7 | */ 8 | public interface JsBridge { 9 | 10 | void goPickPhoto(String funcName, String jsonStr); 11 | 12 | } 13 | -------------------------------------------------------------------------------- /sample/src/main/java/com/cxz/webview/sample/JsInterface.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample; 2 | 3 | import android.util.Log; 4 | import android.webkit.JavascriptInterface; 5 | 6 | /** 7 | * @author chenxz 8 | * @date 2018/9/12 9 | * @desc 编写 JS 接口类 10 | */ 11 | public class JsInterface { 12 | 13 | private static final String TAG = "JsInterface"; 14 | 15 | private JsBridge jsBridge; 16 | 17 | public JsInterface(JsBridge jsBridge) { 18 | this.jsBridge = jsBridge; 19 | } 20 | 21 | /** 22 | * 暴露给sdk的本地方法 23 | * 这个方法由 JS 调用, 不在主线程执行 24 | * 25 | * @param funcName 26 | * @param jsonStr 27 | */ 28 | @JavascriptInterface 29 | public void native_launchFunc(final String funcName, final String jsonStr) { 30 | Log.e(TAG, "funcName::" + funcName + ",json::" + jsonStr); 31 | if (funcName.startsWith("pickPhoto")) { 32 | jsBridge.goPickPhoto(funcName, jsonStr); 33 | } 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /sample/src/main/java/com/cxz/webview/sample/SampleActivity.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample; 2 | 3 | import android.Manifest; 4 | import android.annotation.TargetApi; 5 | import android.content.Intent; 6 | import android.net.Uri; 7 | import android.os.Build; 8 | import android.os.Bundle; 9 | import android.provider.MediaStore; 10 | import android.support.v7.app.AppCompatActivity; 11 | import android.text.TextUtils; 12 | import android.util.Log; 13 | import android.webkit.WebResourceRequest; 14 | import android.webkit.WebResourceResponse; 15 | import android.webkit.WebSettings; 16 | import android.webkit.WebView; 17 | import android.webkit.WebViewClient; 18 | import android.widget.Toast; 19 | 20 | import com.cxz.webview.sample.util.Base64Util; 21 | import com.cxz.webview.sample.util.RealPathUtil; 22 | import com.google.gson.JsonObject; 23 | import com.tbruyelle.rxpermissions2.RxPermissions; 24 | 25 | import java.io.IOException; 26 | 27 | import io.reactivex.functions.Consumer; 28 | 29 | public class SampleActivity extends AppCompatActivity implements JsBridge { 30 | 31 | private static final int REQUEST_PICK_IMAGE = 0x0001111; 32 | private WebView mWebView; 33 | 34 | private String pickPhotoName; 35 | 36 | @Override 37 | protected void onCreate(Bundle savedInstanceState) { 38 | super.onCreate(savedInstanceState); 39 | setContentView(R.layout.activity_sample); 40 | 41 | mWebView = findViewById(R.id.webView); 42 | 43 | initWebView(); 44 | 45 | } 46 | 47 | private void initWebView() { 48 | mWebView.setWebViewClient(mWebViewClient); 49 | WebSettings settings = mWebView.getSettings(); 50 | // 允许 WebView 加载 JS 代码 51 | settings.setJavaScriptEnabled(true); 52 | // 允许 JS 弹窗 53 | settings.setJavaScriptCanOpenWindowsAutomatically(true); 54 | 55 | mWebView.addJavascriptInterface(new JsInterface(this), "launcher"); 56 | 57 | mWebView.loadUrl("file:///android_asset/index1.html"); 58 | } 59 | 60 | WebViewClient mWebViewClient = new WebViewClient() { 61 | //将约定好的空js文件替换为本地的 62 | @Override 63 | public WebResourceResponse shouldInterceptRequest(WebView view, String url) { 64 | WebResourceResponse webResourceResponse = super.shouldInterceptRequest(view, url); 65 | if (url == null) { 66 | return webResourceResponse; 67 | } 68 | if (url.endsWith("native-app.js")) { 69 | try { 70 | webResourceResponse = new WebResourceResponse("text/javascript", 71 | "UTF-8", SampleActivity.this.getAssets().open("js-native.js")); 72 | } catch (IOException e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | return webResourceResponse; 77 | } 78 | 79 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 80 | @Override 81 | public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) { 82 | WebResourceResponse webResourceResponse = super.shouldInterceptRequest(view, request); 83 | if (request == null) { 84 | return webResourceResponse; 85 | } 86 | String url = request.getUrl().toString(); 87 | if (url != null && url.endsWith("native-app.js")) { 88 | try { 89 | webResourceResponse = new WebResourceResponse("text/javascript", 90 | "UTF-8", SampleActivity.this.getAssets().open("js-native.js")); 91 | } catch (IOException e) { 92 | e.printStackTrace(); 93 | } 94 | } 95 | 96 | return webResourceResponse; 97 | } 98 | }; 99 | 100 | @Override 101 | public void goPickPhoto(final String funcName, String jsonStr) { 102 | runOnUiThread(new Runnable() { 103 | @Override 104 | public void run() { 105 | pickPhotoName = funcName; 106 | new RxPermissions(SampleActivity.this) 107 | .request(Manifest.permission.WRITE_EXTERNAL_STORAGE) 108 | .subscribe(new Consumer() { 109 | @Override 110 | public void accept(Boolean aBoolean) throws Exception { 111 | if (aBoolean) { 112 | Intent intent = new Intent(Intent.ACTION_GET_CONTENT); 113 | intent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*"); 114 | startActivityForResult(intent, REQUEST_PICK_IMAGE); 115 | } else { 116 | Toast.makeText(SampleActivity.this, "请给予权限,谢谢", Toast.LENGTH_SHORT).show(); 117 | } 118 | } 119 | }); 120 | } 121 | }); 122 | } 123 | 124 | @Override 125 | protected void onActivityResult(int requestCode, int resultCode, Intent data) { 126 | if (requestCode == REQUEST_PICK_IMAGE && resultCode == RESULT_OK) { 127 | //系统相册选取完成 128 | Uri uri = data.getData(); 129 | if (uri != null) { 130 | String filePath; 131 | if (!TextUtils.isEmpty(uri.toString()) && uri.toString().startsWith("file")) { 132 | filePath = uri.getPath(); 133 | } else { 134 | filePath = RealPathUtil.getRealPathFromURI(this, uri); 135 | } 136 | String base64Image = Base64Util.encodeBase64ImageFile(filePath); 137 | JsonObject jsonObject = new JsonObject(); 138 | jsonObject.addProperty("image64", base64Image); 139 | jsonObject.addProperty("message", "图片获取成功"); 140 | Log.d("SampleActivity", "jsonObject:" + jsonObject); 141 | mWebView.loadUrl("javascript:sdk_nativeCallback(\'" + pickPhotoName + "\',\'" + jsonObject + "\')"); 142 | } 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /sample/src/main/java/com/cxz/webview/sample/util/Base64Util.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample.util; 2 | 3 | import android.graphics.Bitmap; 4 | import android.graphics.BitmapFactory; 5 | import android.util.Base64; 6 | 7 | import java.io.ByteArrayOutputStream; 8 | import java.io.IOException; 9 | 10 | public class Base64Util { 11 | 12 | public static String encodeBase64ImageFile(String filePath) { 13 | Bitmap bitmap = BitmapFactory.decodeFile(filePath); 14 | return bitmapToBase64(bitmap); 15 | } 16 | 17 | /** 18 | * bitmap转为base64 19 | * 20 | * @param bitmap 21 | * @return 22 | */ 23 | public static String bitmapToBase64(Bitmap bitmap) { 24 | 25 | String result = null; 26 | ByteArrayOutputStream baos = null; 27 | try { 28 | if (bitmap != null) { 29 | baos = new ByteArrayOutputStream(); 30 | 31 | bitmap.compress(Bitmap.CompressFormat.JPEG, 72, baos); 32 | bitmap.recycle(); 33 | baos.flush(); 34 | baos.close(); 35 | 36 | byte[] bitmapBytes = baos.toByteArray(); 37 | result = Base64.encodeToString(bitmapBytes, Base64.NO_WRAP); 38 | } 39 | } catch (IOException e) { 40 | e.printStackTrace(); 41 | } finally { 42 | try { 43 | if (baos != null) { 44 | baos.flush(); 45 | baos.close(); 46 | } 47 | } catch (IOException e) { 48 | e.printStackTrace(); 49 | } 50 | } 51 | return result; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /sample/src/main/java/com/cxz/webview/sample/util/RealPathUtil.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.content.ContentUris; 5 | import android.content.Context; 6 | import android.content.CursorLoader; 7 | import android.database.Cursor; 8 | import android.graphics.Bitmap; 9 | import android.net.Uri; 10 | import android.os.Build; 11 | import android.os.Environment; 12 | import android.provider.DocumentsContract; 13 | import android.provider.MediaStore; 14 | import android.text.TextUtils; 15 | 16 | import java.io.File; 17 | import java.io.FileNotFoundException; 18 | import java.io.FileOutputStream; 19 | import java.io.IOException; 20 | 21 | public class RealPathUtil { 22 | 23 | public static String getRealPathFromURI(Context context, Uri uri) { 24 | String realPath = RealPathUtil.getRealPathFromURI_BelowAPI11(context, uri); 25 | if (TextUtils.isEmpty(realPath)) { 26 | realPath = RealPathUtil.getRealPathFromURI_API11to18(context, uri); 27 | } 28 | if (TextUtils.isEmpty(realPath)) { 29 | realPath = RealPathUtil.getRealPathFromURI_API19(context, uri); 30 | } 31 | return realPath; 32 | } 33 | 34 | @SuppressLint("NewApi") 35 | public static String getRealPathFromURI_API19(Context context, Uri uri) { 36 | String filePath = ""; 37 | try { 38 | String wholeID = DocumentsContract.getDocumentId(uri); 39 | // Split at colon, use second item in the array 40 | String id = wholeID.split(":")[1]; 41 | String[] column = {MediaStore.Images.Media.DATA}; 42 | // where id is equal to 43 | String sel = MediaStore.Images.Media._ID + "=?"; 44 | Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 45 | column, sel, new String[]{id}, null); 46 | int columnIndex = cursor.getColumnIndex(column[0]); 47 | if (cursor.moveToFirst()) { 48 | filePath = cursor.getString(columnIndex); 49 | } 50 | cursor.close(); 51 | } catch (Exception e) { 52 | 53 | } 54 | return filePath; 55 | } 56 | 57 | 58 | @SuppressLint("NewApi") 59 | public static String getRealPathFromURI_API11to18(Context context, Uri contentUri) { 60 | String result = null; 61 | try { 62 | String[] proj = {MediaStore.Images.Media.DATA}; 63 | CursorLoader cursorLoader = new CursorLoader(context, contentUri, proj, null, null, null); 64 | Cursor cursor = cursorLoader.loadInBackground(); 65 | if (cursor != null) { 66 | int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 67 | cursor.moveToFirst(); 68 | result = cursor.getString(column_index); 69 | } 70 | } catch (Exception e) { 71 | 72 | } 73 | return result; 74 | } 75 | 76 | public static String getRealPathFromURI_BelowAPI11(Context context, Uri contentUri) { 77 | String result = null; 78 | try { 79 | String[] proj = {MediaStore.Images.Media.DATA}; 80 | Cursor cursor = context.getContentResolver().query(contentUri, proj, null, null, null); 81 | int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); 82 | cursor.moveToFirst(); 83 | result = cursor.getString(column_index); 84 | } catch (Exception e) { 85 | 86 | } 87 | return result; 88 | } 89 | 90 | /** 91 | * 重新创建小图 92 | * 93 | * @param bmp 原图 94 | * @param f 保存路径 95 | * @param outWidth 目标宽度 96 | * @param outHeight 目标高度 97 | */ 98 | public static void saveScaledImageFile(Bitmap bmp, File f, int outWidth, int outHeight) { 99 | 100 | // 原始高宽 101 | int origWidth = bmp.getWidth(); 102 | int origHeight = bmp.getHeight(); 103 | 104 | outWidth = Math.min(outWidth, origWidth); 105 | outHeight = Math.min(outHeight, origHeight); 106 | 107 | Bitmap outBitmap = null; 108 | 109 | if (f.exists() && f.isFile()) { 110 | f.delete(); 111 | } 112 | 113 | try { 114 | outBitmap = Bitmap.createScaledBitmap(bmp, outWidth, outHeight, false); 115 | FileOutputStream fos = new FileOutputStream(f); 116 | outBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos); 117 | try { 118 | fos.flush(); 119 | } catch (IOException e) { 120 | e.printStackTrace(); 121 | } 122 | fos.close(); 123 | } catch (FileNotFoundException e) { 124 | e.printStackTrace(); 125 | } catch (IOException e) { 126 | e.printStackTrace(); 127 | } catch (OutOfMemoryError e) { 128 | e.printStackTrace(); 129 | } finally { 130 | /* 131 | * if (outBitmap != null) { //在三星GT N7100,android 132 | * 4.1.2上,(如果选择很小的图片,)裁减之后回来界面空白,报错Cannot dray recycled bitmaps 133 | * outBitmap.recycle(); } 134 | */ 135 | } 136 | 137 | } 138 | 139 | 140 | @SuppressLint("NewApi") 141 | public static String getPath(final Context context, final Uri uri) { 142 | final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; 143 | 144 | // DocumentProvider 145 | if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { 146 | // ExternalStorageProvider 147 | if (isExternalStorageDocument(uri)) { 148 | final String docId = DocumentsContract.getDocumentId(uri); 149 | final String[] split = docId.split(":"); 150 | final String type = split[0]; 151 | 152 | if ("primary".equalsIgnoreCase(type)) { 153 | return Environment.getExternalStorageDirectory() + "/" + split[1]; 154 | } 155 | 156 | } 157 | // DownloadsProvider 158 | else if (isDownloadsDocument(uri)) { 159 | final String id = DocumentsContract.getDocumentId(uri); 160 | final Uri contentUri = ContentUris.withAppendedId( 161 | Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); 162 | 163 | return getDataColumn(context, contentUri, null, null); 164 | } 165 | // MediaProvider 166 | else if (isMediaDocument(uri)) { 167 | final String docId = DocumentsContract.getDocumentId(uri); 168 | final String[] split = docId.split(":"); 169 | final String type = split[0]; 170 | 171 | Uri contentUri = null; 172 | if ("image".equals(type)) { 173 | contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; 174 | } else if ("video".equals(type)) { 175 | contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; 176 | } else if ("audio".equals(type)) { 177 | contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; 178 | } 179 | 180 | final String selection = "_id=?"; 181 | final String[] selectionArgs = new String[]{ 182 | split[1] 183 | }; 184 | 185 | return getDataColumn(context, contentUri, selection, selectionArgs); 186 | } 187 | } 188 | // MediaStore (and general) 189 | else if ("content".equalsIgnoreCase(uri.getScheme())) { 190 | // Return the remote address 191 | if (isGooglePhotosUri(uri)) 192 | return uri.getLastPathSegment(); 193 | 194 | return getDataColumn(context, uri, null, null); 195 | } 196 | // File 197 | else if ("file".equalsIgnoreCase(uri.getScheme())) { 198 | return uri.getPath(); 199 | } 200 | 201 | return null; 202 | } 203 | 204 | public static String getDataColumn(Context context, Uri uri, String selection, 205 | String[] selectionArgs) { 206 | 207 | Cursor cursor = null; 208 | final String column = "_data"; 209 | final String[] projection = { 210 | column 211 | }; 212 | 213 | try { 214 | cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, 215 | null); 216 | if (cursor != null && cursor.moveToFirst()) { 217 | final int index = cursor.getColumnIndexOrThrow(column); 218 | return cursor.getString(index); 219 | } 220 | } finally { 221 | if (cursor != null) 222 | cursor.close(); 223 | } 224 | return null; 225 | } 226 | 227 | public static boolean isExternalStorageDocument(Uri uri) { 228 | return "com.android.externalstorage.documents".equals(uri.getAuthority()); 229 | } 230 | 231 | public static boolean isDownloadsDocument(Uri uri) { 232 | return "com.android.providers.downloads.documents".equals(uri.getAuthority()); 233 | } 234 | 235 | public static boolean isMediaDocument(Uri uri) { 236 | return "com.android.providers.media.documents".equals(uri.getAuthority()); 237 | } 238 | 239 | public static boolean isGooglePhotosUri(Uri uri) { 240 | return "com.google.android.apps.photos.content".equals(uri.getAuthority()); 241 | } 242 | 243 | 244 | } 245 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /sample/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample/src/main/res/layout/activity_sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iceCola7/JsWebViewSample/1750dff62d70b4fa193145aa543cff336a17a086/sample/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #3F51B5 4 | #303F9F 5 | #FF4081 6 | 7 | -------------------------------------------------------------------------------- /sample/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Sample 3 | 4 | -------------------------------------------------------------------------------- /sample/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /sample/src/test/java/com/cxz/webview/sample/ExampleUnitTest.java: -------------------------------------------------------------------------------- 1 | package com.cxz.webview.sample; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | /** 8 | * Example local unit test, which will execute on the development machine (host). 9 | * 10 | * @see Testing documentation 11 | */ 12 | public class ExampleUnitTest { 13 | @Test 14 | public void addition_isCorrect() { 15 | assertEquals(4, 2 + 2); 16 | } 17 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':sample' 2 | --------------------------------------------------------------------------------