之前在工作中,遇到了一个比较奇葩的问题。
就是在webview通过QQ空间分享一篇文章成功后,点击网页“返回继续浏览”无响应,无法返回上一页面。
经排查,是因为网页实现“返回继续浏览”的方法是window.close,window.close的作用是关闭当前窗口页,而当webivew在只有一个窗口页时,当前页面不能关闭,导致方法无效。
当然有同学说可以通过webview的onCreateWindow()方法来创新一个新的窗口页啊,但调用onCreateWindow就要再对新建的webview进行一遍,也就是每新开一个新窗口就得嵌套一次webview初始化,除了代码量惊人,还得清楚最多嵌套多少次。这个对于性能和可读性来说,都是灾难性的。
通过找一些资料和自己实践,就利用一个ArrayList存储webview的方法来实现多窗口页的功能。虽然方法用的有点low,但是起码可行...心痛自己太渣一秒钟。
-----------------------------------------------------------------------------------------
实现的步骤:
1. 创建一个注解代替枚举类
/*
* 每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存
* 较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间
* 特别是分dex多的大型APP,枚举的初始化很容易导致ANR
* 所以使用注解来代替枚举
*/
public class WebEvent {
public static final int OVERRIDE_URL_RESOLVE = 11;
public static final int INTERCEPT_REQUEST_RESOLVE = 12;
public static final int RECEIVED_ERROR = 44;
public static final int WEB_NETWORK_ERROR_OPEN_WIFI = 50;
public static final int WEB_OPEN_EXTERNAL_LINK = 51;
@IntDef({OVERRIDE_URL_RESOLVE,
INTERCEPT_REQUEST_RESOLVE,
RECEIVED_ERROR,
WEB_NETWORK_ERROR_OPEN_WIFI,
WEB_OPEN_EXTERNAL_LINK})
public @interface webEvent {
}
}
2. 创建一个webView公共类,继承WebView:
当执行onCreateWindow()方法时,就new一个WebView的对象添加到List中去。
public class WebViewTest extends WebView {
private static final String _TAG = WebViewTest.class.getSimpleName();
private static final String SCHEME_HTTP = "http";
private static final String SCHEME_HTTPS = "https";
/*
* 定义IEvnentPublish接口,通过接口把对应的webview操作放到主页面执行
*/
public interface IEventPublish {
void onOpen(String url);
boolean onCreateWindow(final WebView view, boolean dialog, boolean userGesture, Message resultMsg);
void onCloseWindow(WebView window);
void onClose(String url);
void onReceivedError(String url);
void onProcessing(boolean processing);
void onProcessChanged(int processValue);
void onEventPush(@WebEvent.webEvent int event, String url);
void onDownload(@NonNull DownloadElement element);
void onOpenFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture);
}
private IEventPublish eventPublish;
/*
* webview子类的构造函数
*/
public WebViewTest(Context context) {
super(context);
}
public WebViewTest(Context context, AttributeSet attrs) {
super(context, attrs);
}
public WebViewTest(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
/*
* 设置webview各项属性
*/
public void initSettings() {
WebSettings settings = this.getSettings();
//设置WebView中加载页面字体变焦百分比,默认100,整型数
settings.setTextZoom(100);
//设置在WebView内部是否允许访问文件,默认允许访问。
settings.setAllowFileAccess(true);
//设置WebView是否允许执行JavaScript脚本,默认false,不允许
settings.setJavaScriptEnabled(true);
//设置WebView是否加载图片资源,默认true,自动加载图片
settings.setLoadsImagesAutomatically(true);
//设置是否开启定位功能,默认true,开启定位
settings.setGeolocationEnabled(true);
//设置WebView是否使用viewport,当该属性被设置为false时,加载页面的宽度总是适应WebView控件宽度;当被设置为true,当前页面包含viewport属性标签,在标签中指定宽度值生效,如果页面不包含viewport标签,无法提供一个宽度值,这个时候该方法将被使用。
settings.setUseWideViewPort(true);
//设置WebView是否使用预览模式加载界面。
settings.setLoadWithOverviewMode(true);
//设置脚本是否允许自动打开弹窗,默认false,不允许
settings.setJavaScriptCanOpenWindowsAutomatically(true);
//设置WebView是否支持使用屏幕控件或手势进行缩放,默认是true,支持缩放。
settings.setSupportZoom(true);
//设置WebView是否使用其内置的变焦机制,该机制集合屏幕缩放控件使用,默认是false,不使用内置变焦机制。
settings.setBuiltInZoomControls(true);
//设置WebView使用内置缩放机制时,是否展现在屏幕缩放控件上,默认true,展现在控件上。
settings.setDisplayZoomControls(false);
//设置是否开启DOM存储API权限,默认false,未开启,设置为true,WebView能够使用DOM storage API
settings.setDomStorageEnabled(true);
//重写缓存被使用到的方法,该方法基于Navigation Type,加载普通的页面,将会检查缓存同时重新验证是否需要加载,如果不需要重新加载,将直接从缓存读取数据,允许客户端通过指定LOAD_DEFAULT、LOAD_CACHE_ELSE_NETWORK、LOAD_NO_CACHE、LOAD_CACHE_ONLY其中之一重写该行为方法,默认值LOAD_DEFAULT
settings.setCacheMode(WebSettings.LOAD_DEFAULT);
//设置WebView加载页面文本内容的编码,默认“UTF-8”。
settings.setDefaultTextEncodingName("UTF-8");
//设置WebView底层的布局算法,参考LayoutAlgorithm#NARROW_COLUMNS,将会重新生成WebView布局
settings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.SINGLE_COLUMN);
//设置WebView是否支持多屏窗口,参考WebChromeClient#onCreateWindow,默认false,不支持。(设置为true,onCreateWindow才会生效)
settings.setSupportMultipleWindows(true);
//设置是否开启数据库存储API权限,默认false,未开启
settings.setDatabaseEnabled(true);
String dir = VocApplication.getVocApplication().getDir("database", Context.MODE_PRIVATE).getPath();
//设置WebView保存地理位置信息数据路径,指定的路径Application具备写入权限
settings.setGeolocationDatabasePath(dir);
//设置当一个安全站点企图加载来自一个不安全站点资源时WebView的行为
settings.setMixedContentMode(WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE);
垂直不显示滚动条
this.setVerticalScrollBarEnabled(false);
this.setWebViewClient(webViewClient);
this.setWebChromeClient(webChromeClient);
this.setDownloadListener(downloadListener);
}
/*
* 传入主页面的IEventPublish接口
*/
public void setEventCallback(@NonNull IEventPublish publish) {
eventPublish = publish;
}
/*
* 创建webViewClient的对象,WebViewClient可以拿到WebView在访问网络各个阶段的回调,包括加载前后,失败等
*/
public final WebViewClient webViewClient = new WebViewClient() {
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
Log.d(_TAG, "onPageStarted url=" + url);
if (eventPublish != null) {
eventPublish.onProcessing(true);
}
}
@Override
public void onPageFinished(WebView view, String url) {
if (url != null) {
Log.d(_TAG, "onPageFinished url = " + url);
}
if (eventPublish != null) {
eventPublish.onProcessing(false);
}
}
//拦截 url 跳转,在里边添加点击链接跳转或者操作
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}
//在每一次请求资源时,都会通过这个函数来回调
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
Log.d(_TAG, "shouldInterceptRequest url = " + url);
eventPublish.onEventPush(INTERCEPT_REQUEST_RESOLVE, url);
return super.shouldInterceptRequest(view, url);
}
//加载错误的时候会回调,在其中可做错误处理,比如再请求加载一次,或者提示404的错误页面
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
Log.d(_TAG, "errorCode:" + errorCode + " description:" + description + " failingUrl:" + failingUrl);
if (errorCode == -2) {
view.stopLoading();
if (view.canGoBack()) {
view.goBack();
}
if (eventPublish != null) {
eventPublish.onEventPush(RECEIVED_ERROR, failingUrl);
}
}
}
//当接收到https错误时,会回调此函数,在其中可以做错误处理
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
handler.proceed();//表示忽略错误继续加载,SslErrorHandler.cancel()表示取消加载。在onReceivedSslError的默认实现中是使用的
}
};
/*
* 可以在其中加载进度条,获取链接的标题等方法。
*/
public final WebChromeClient webChromeClient = new WebChromeClient() {
//通知主程序web内容尝试使用定位API,但是没有相关的权限。主程序需要调用调用指定的定位权限申请的回调。更多说明查看GeolocationPermissions相关API。
@Override
public void onGeolocationPermissionsShowPrompt(final String origin, final GeolocationPermissions.Callback callback) {
callback.invoke(origin, true, false);
super.onGeolocationPermissionsShowPrompt(origin, callback);
}
//请求主程序创建一个新的Window,如果主程序接收请求,返回true并创建一个新的WebView来装载Window,然后添加到View中,发送带有创建的WebView作为参数的resultMsg的给Target。如果主程序拒绝接收请求,则方法返回false。默认不做任何处理,返回false
@Override
public boolean onCreateWindow(WebView view, boolean isDialog, boolean isUserGesture, Message resultMsg) {
Log.d(_TAG, "onCreateWindow");
if (eventPublish != null) {
return eventPublish.onCreateWindow(view, isDialog, isUserGesture, resultMsg);
} else {
return true;
}
}
//通知主程序关闭WebView,并从View中移除,WebCore停止任何的进行中的加载和JS功能。
@Override
public void onCloseWindow(WebView window) {
Log.d(_TAG, "onCloseWindow");
super.onCloseWindow(window);
if (eventPublish != null) {
eventPublish.onCloseWindow(window);
}
}
//通知程序当前页面加载进度
@Override
public void onProgressChanged(WebView view, int newProgress) {
Log.d(_TAG, "onProgressChanged -> " + newProgress);
super.onProgressChanged(view, newProgress);
if (eventPublish != null) {
eventPublish.onProcessChanged(newProgress);
}
}
//告诉客户端显示离开当前页面的导航提示框。如果返回true,由客户端处理确认提示框,调用合适的JsResult方法。如果返回false,则返回默认值true给javascript接受离开当前页面的导航。默认:false。JsResult设置false,当前页面取消导航提示,否则离开当前页面。
@Override
public boolean onJsBeforeUnload(WebView view, String url, String message, final JsResult result) {
Log.d(_TAG, "onJsBeforeUnload url:" + url + " message:" + message + " result:" + result);
return super.onJsBeforeUnload(view, url, message, result);
}
// For Android 3.0+ webview打开手机相册
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType) {
openFileChooser(uploadMsg, "", "filesystem");
}
// For Android < 3.0
public void openFileChooser(ValueCallback<Uri> uploadMsg) {
openFileChooser(uploadMsg, "");
}
// For Android > 4.1.1
public void openFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
if (eventPublish != null) {
eventPublish.onOpenFileChooser(uploadMsg, acceptType, capture);
}
}
// Android 4.4, 4.4.1, 4.4.2
// openFileChooser function is not called on Android 4.4, 4.4.1, 4.4.2
// Android > 5.0.1
public boolean onShowFileChooser(
WebView WebViewTest, ValueCallback<Uri[]> filePathCallback,
FileChooserParams fileChooserParams) {
String acceptTypes[] = fileChooserParams.getAcceptTypes();
String acceptType = "";
for (String acceptType1 : acceptTypes) {
if (acceptType1 != null && acceptType1.length() != 0)
acceptType += acceptType1 + ";";
}
if (acceptType.length() == 0)
acceptType = "*/*";
final ValueCallback<Uri[]> finalFilePathCallback = filePathCallback;
ValueCallback<Uri> vc = new ValueCallback<Uri>() {
@Override
public void onReceiveValue(Uri value) {
Uri[] result;
if (value != null)
result = new Uri[]{value};
else
result = null;
finalFilePathCallback.onReceiveValue(result);
}
};
openFileChooser(vc, acceptType, "filesystem");
return true;
}
};
@Override
public ActionMode startActionMode(ActionMode.Callback callback, int type) {
return null;
}
@Override
public ActionMode startActionMode(ActionMode.Callback callback) {
return null;
}
//实现webview的下载文件功能
public final DownloadListener downloadListener = new DownloadListener() {
@Override
public void onDownloadStart(String url, String userAgent, String contentDisposition, String mimetype, long contentLength) {
String fileName;
if (contentDisposition != null && contentDisposition.contains("filename")) {
fileName = contentDisposition.substring(contentDisposition.indexOf("filename") + 9, contentDisposition.length());
} else {
fileName = url.substring(url.lastIndexOf("/") + 1, url.length());
}
String suffix = fileName.substring(fileName.lastIndexOf(".") + 1, fileName.length());
if (mimetype == null || mimetype.equals("")) {
mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix);
}
if (eventPublish != null) {
eventPublish.onDownload(new DownloadElement(url, fileName, mimetype));
}
}
};
}
3. 在主界面实现webviewList的操作
/*
* webview的viewmodel,对progressBar,加载进度、显示webview的数据绑定。
*/
public class WebViewTestModel extends BaseObservable {
public final ObservableInt progress = new ObservableInt(0);
public final ObservableBoolean progressing = new ObservableBoolean(false);
public final ObservableBoolean webViewVisible = new ObservableBoolean(true);
public WebViewTestModel() {
}
}
public class TestFragment extends Fragment {
private static final String TAG = TestFragment.class.getSimpleName();
private static final int DOCUMENTS_UI_POLICY_SEC = 1;
public static final int REQUEST_WEBVIEW_GET_PHOTO = 3001;
private static final String DOCUMENTS_UI_POLICY = "DocumentsUIPolicy";
private View mRootView;
private FrameLayout mWebViewContainer;
private WebViewTestActivity mActivity = null;
private FragmentWebViewTestBinding mWebBinding;
private WebTestView mWebTestView;
private WebViewTestModel mWebTestViewModel;
private ArrayList<WebTestView> mWebViewList = new ArrayList<>();
/*
* 初始化webview的viewmodel,通过generateWebView()创建WebTestView的对象并赋予mWebTestView
*/
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
mWebBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_test, container, false);
mRootView = mWebBinding.getRoot();
mWebTestViewModel = new WebViewTestModel();
mWebBinding.setClubViewModel(mWebTestViewModel);
mWebViewContainer = mRootView.findViewById(R.id.webViewContainer);
generateWebView();
mWebTestView.loadUrl("www.xxxx.com");
return mRootView;
}
/*
* 实现IEventPublish接口的方法
*/
private WebTestView.IEventPublish eventPublish = new WebTestView.IEventPublish() {
@Override
public void onOpen(String url) {
Log.d(TAG, "onOpen");
}
@Override
public void onClose(String url) {
Log.d(TAG, "onClose");
}
//每当调用到onCreateWindow函数时,就会调用generateWebView来生成一个webview对象并添加到List中
@Override
public boolean onCreateWindow(WebView view, boolean dialog, boolean userGesture, Message resultMsg) {
if (resultMsg != null && resultMsg.obj instanceof WebView.WebViewTransport) {
generateWebView();
WebView.WebViewTransport transport = (WebView.WebViewTransport) resultMsg.obj;
transport.setWebView(mWebTestView);
resultMsg.sendToTarget();
return true;
}
return false;
}
@Override
public void onCloseWindow(WebView window) {
closeWebView();
}
@Override
public void onReceivedError(String url) {
Log.d(TAG, "onReceivedError");
}
/*向与viewmodel绑定的progressBar传入进度数据*/
@Override
public void onProcessing(boolean processing) {
Log.d(TAG, "onProcessing");
mWebTestViewModel.progressing.set(processing);
}
@Override
public void onProcessChanged(int processValue) {
Log.d(TAG, "onProcessChanged");
mWebTestViewModel.progress.set(processValue);
}
@Override
public void onEventPush(int event, String url) {
Log.d(TAG, "onEventPush");
handleEvent(event, url);
}
@Override
public void onDownload(@NonNull DownloadElement element) {
Log.d(TAG, "onDownload");
/*进行download操作*/
}
@Override
public void onOpenFileChooser(ValueCallback<Uri> uploadMsg, String acceptType, String capture) {
Log.d(TAG, "onOpenFileChooser");
final String imageMimeType = "image/*";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)
&& mContext.getExternalCacheDir() != null) {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
intent.putExtra(DOCUMENTS_UI_POLICY, DOCUMENTS_UI_POLICY_SEC);
intent.setType(imageMimeType);
startActivityForResult(intent, REQUEST_WEBVIEW_GET_PHOTO);
}
}
};
/*
* 创建新的webview对象,设置属性和接口后添加到list
* List最多只能有10个webview,超过后就把list后面的5个webview前移并覆盖前面的5个
*/
private final int mMaxWebViewCount = 10;
private void generateWebView() {
if (mWebViewList.size() >= mMaxWebViewCount) {
WebTestView webView;
for (int i = mMaxWebViewCount - 1; i >= 5; i--) {
webView = mWebViewList.get(i);
mWebViewList.set(i - 5, webView);
mWebViewList.remove(i);
}
}
WebTestView webView = new WebTestView(mActivity);
webView.initSettings();
mWebViewList.add(webView);
webView.addJavascriptInterface(new TestDemoInterface(), "TestDemo");
webView.setEventCallback(eventPublish);
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
webView.setLayoutParams(params);
mWebViewContainer.addView(webView);
mWebTestView = webView;
}
/*
* 删除List中的当前webview,并让mWebTestView等下list中下一个位置的webview
*/
private void closeWebView() {
int lastIndex = mWebViewList.size() - 1;
mWebTestViewModel.progress.set(0);
mWebTestViewModel.progressing.set(false);
if (lastIndex >= 1) {
WebTestView webView = mWebViewList.remove(lastIndex);
WebUtil.clearWebView(webView);
mWebTestView = mWebViewList.get(lastIndex - 1);
return;
}
mWebTestView = mWebViewList.get(0);
}
/*
* 将list中的所有webview删除
*/
private void clearWebView() {
for (int i = 0; i < mWebViewList.size(); i++) {
mWebTestView = mWebViewList.get(i);
WebUtil.clearWebView(mWebTestView);
}
mWebViewList.clear();
}
/*
* 处理webview中的事件
*/
private void handleEvent(int event, String url) {
switch (event) {
case WebEvent.OVERRIDE_URL_RESOLVE:
case WebEvent.INTERCEPT_REQUEST_RESOLVE:
try {
Uri uri = Uri.parse(url);
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
if (intent.resolveActivity(mContext.getPackageManager()) == null) {
Log.e(TAG, "target app not install " + url);
} else {
if (!uri.toString().equals("about:blank")) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
mActivity.onBackPressed();
}
}
} catch (Exception e) {
Log.e(TAG, "invalid url " + url);
}
break;
case WebEvent.RECEIVED_ERROR:
/*显示弹出错误warning框*/
break;
case WebEvent.WEB_OPEN_EXTERNAL_LINK:
/*跳转外链*/
break;
case WebEvent.WEB_NETWORK_ERROR_OPEN_WIFI:
/*提示需要打开WIFI*/
break;
}
}
/*
* 退出页面时,将list中的webview清空
*/
@Override
public void onDestroy() {
clearWebView();
super.onDestroy();
}
/*
* 在webview中打开相册并选择后,对返回的图片uri进行处理
*/
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_WEBVIEW_GET_PHOTO) {
Uri result = (data == null || resultCode != Activity.RESULT_OK ? null : data.getData());
/*对返回数据进行操作*/
}
super.onActivityResult(requestCode, resultCode, data);
}
/*
* 点击toolbar的返回箭头时,退出页面
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
mActivity.popFragment();
}
return true;
}
/*
* 点击back键后的处理,当网页可以返回时则goback,否则退出当前窗口页或当前页面
*/
@Override
public void onBackPressed() {
if (mWebTestView != null) {
if (mWebTestView.canGoBack()) {
mWebTestView.goBack();
} else if (mWebViewList.size() > 1){
closeWebView();
} else {
mActivity.popFragment();
}
} else {
mActivity.popFragment();
}
}
}