此文仅记录项目开发中遇到的问题及解决方法。
目录
一,下拉刷新
二,H5唤起支付宝
三,H5本地文件选择
四,加载图片失败
五,输入框被软键盘遮挡
一,下拉刷新
页面Reload需要下拉刷新功能,所以使用了SwipeRefreshLayout包裹WebView。但使用时不管页面处在哪个位置只要下拉,都会触发刷新。
于是通过对WebView的位置进行判断,来决定是否允许SwipeRefreshLayout刷新功能生效。
现在H5页面大多都不再是页面本身滚动,反映到日志就是WebView的getScrollY() 得到的值一直是0,无法用于判断,于是采用迂回的方式。
首先自定义WebView的子类控件OverScrollWebView,重写过度滚动监听overScrollBy,当其被触发的时候,允许下拉刷新。注意:向上过度滑动也会触发这个回调,但SwipeRefreshLayout的刷新仅会被下拉触发,所以这里没有考虑方向问题。
private boolean isOverScroll = false;
@Override
protected boolean overScrollBy(int deltaX, int deltaY, int scrollX, int scrollY, int scrollRangeX, int scrollRangeY, int maxOverScrollX, int maxOverScrollY, boolean isTouchEvent) {
if (!isOverScroll) {
isOverScroll = true;
}
return super.overScrollBy(deltaX, deltaY, scrollX, scrollY, scrollRangeX, scrollRangeY, maxOverScrollX, maxOverScrollY, isTouchEvent);
}
public boolean isOverScroll() {
return isOverScroll;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
//在用户开始或结束触屏时复位
isOverScroll = false;
break;
}
return super.onTouchEvent(event);
}
同时,在页面中设置允许WebView过度滚动,并重写触屏事件监听,根据页面是否已处在顶部来设置是否允许SwipeRefreshLayout下拉刷新。
//允许webView过度滚动
webView.setOverScrollMode(View.OVER_SCROLL_ALWAYS);
if (webView instanceof OverScrollWebView) {
webView.setOnTouchListener((v, event) -> {
swipeRefreshLayout.setEnabled(((OverScrollWebView) webView).isOverScroll());
return false;
});
}
二,H5唤起支付宝
项目支付功能由H5页面调用支付宝接口,但无法正确唤起支付宝App,仅生成了吱口令。
因为安卓原生的谷歌浏览器自从 chrome25 版本开始,URL Scheme 就无法启动Android应用了。
所以,需要重写拦截逻辑,手动唤起App。
webView.setWebViewClient(new WebViewClient() {
@Override
public boolean shouldOverrideUrlLoading (WebView view, String url){
JCLog.i(TAG, " shouldOverrideUrlLoading url: " + url);
if (url.startsWith("http:") || url.startsWith("https:")) {
//正常的页面,不拦截不处理
return false;
}
try {
//将H5唤起App,变为App间互相唤起
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
});
这里是笼统的处理,也可以根据页面需求进行精准处理。
参考:android中WebView调用H5页面的支付宝、微信支付失败错误返回ERR_UNKNOWN_URL_SCHEME_webview打开支付宝付款没反应_吕氏春秋i的博客-CSDN博客
三,H5本地文件选择
这个就比较简单了,就是重写WebChromeClient的onShowFileChooser方法,将H5的文件选择请求转换成安卓原声的文件选取。
需要注意几点:
1,项目需要选择多类型文件,所以我在注册回调时用的是OpenMultipleDocuments,如果是单文件、仅图片等要根据具体情况选择。
2,mFilePathCallback的onReceiveValue方法一定要调用,就算没有内容,也要像我这样传一个空数组new Uri[0]给它调用,这个方法如果不调用,onShowFileChooser就不会再工作了,就是说你没法再次进行文件选择了。
3,数组fileChooserParams.getA***eptTypes()是H5提供的要选择文件类型,部分控件给的是文件后缀,但安卓需要的必须是MimeType,所以我代码里有一部分是专门用于转换参数的。
private ValueCallback<Uri[]> mFilePathCallback;
ActivityResultLauncher<String[]> chooseFileLauncher = registerResultCallback(new ActivityResultContracts.OpenMultipleDocuments(), result -> {
JCLog.i(TAG, "onActivityResult result:" + result);
Uri[] uris;
if (null == result || result.isEmpty()) {
uris = new Uri[0];
} else {
uris = new Uri[result.size()];
for (int i = 0; i < result.size(); i++) {
uris[i] = result.get(i);
}
}
JCLog.i(TAG, "onActivityResult uris: " + Arrays.toString(uris));
if (null != mFilePathCallback) {
mFilePathCallback.onReceiveValue(uris);
mFilePathCallback = null;
}
});
webView.setWebChromeClient(new WebChromeClient() {
@Override
public boolean onShowFileChooser (WebView webView, ValueCallback < Uri[]>
filePathCallback, WebChromeClient.FileChooserParams
fileChooserParams){
JCLog.i(TAG, "onShowFileChooser: " + Arrays.toString(fileChooserParams.getA***eptTypes()));
if (null != chooseFileLauncher) {
mFilePathCallback = filePathCallback;
String[] types = fileChooserParams.getA***eptTypes();
if (null != types && types.length > 0) {
for (int i = 0; i < types.length; i++) {
String type = types[i];
if (TextUtils.isEmpty(type) || !type.startsWith(".")) {
continue;
}
type = type.replaceFirst(".", "");
String newType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(type);
if (!TextUtils.isEmpty(newType)) {
types[i] = newType;
}
}
}
JCLog.i(TAG, "onShowFileChooser before launch: " + Arrays.toString(types));
chooseFileLauncher.launch(types);
return true;
}
return super.onShowFileChooser(webView, filePathCallback, fileChooserParams);
}
});
四,加载图片失败
测试时发现某些图片加载失败,与H5开发人员确认,图片使用的scheme是http。
WebView在安卓5以后,则需要增加设置:
WebSettings webSettings = webView.getSettings();
……
webSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
这样就能正常显示http的图片了。
五,输入框被软键盘遮挡
H5页面中有输入框,点击后,安卓弹出的软键盘遮挡了输入框,使用网上找到的AndroidBug5497Workaround解决问题,结果发现手机底部虚拟按键会遮挡页面,于是再次寻找解决办法,最后找到了通过修改AndroidBug5497Workaround完美解决这两个问题的办法。特此记录。因项目开发工作比较忙,此次更新时间间隔较久,所以无法找到代码引用的原文了,见谅。
public class AndroidBug5497Workaround {
private final Activity activity;
// For more information, see https://code.google.***/p/android/issues/detail?id=5497
// To use this class, simply invoke assistActivity() on an Activity that already has its content view set.
public static void assistActivity(Activity activity) {
new AndroidBug5497Workaround(activity);
}
private View mChildOfContent;
private int usableHeightPrevious;
private FrameLayout.LayoutParams frameLayoutParams;
private int statusBarHeight;
//状态栏高度
private AndroidBug5497Workaround(final Activity activity) {
this.activity = activity;
if (checkDeviceHasNavigationBar(activity)) {
//获取状态栏的高度
int resourceId = activity.getResources().getIdentifier("status_bar_height", "dimen", "android");
statusBarHeight = activity.getResources().getDimensionPixelSize(resourceId);
}
//1、找到Activity的最外层布局控件,它其实是一个DecorView,它所用的控件就是FrameLayout
FrameLayout content = (FrameLayout) activity.findViewById(android.R.id.content);
//2、获取到setContentView放进去的View
mChildOfContent = content.getChildAt(0);
//3、给Activity的xml布局设置View树监听,当布局有变化,如键盘弹出或收起时,都会回调此监听
mChildOfContent.getViewTreeObserver().
addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
//4、软键盘弹起会使GlobalLayout发生变化
public void onGlobalLayout() {
//5、当前布局发生变化时,对Activity的xml布局进行重绘
possiblyResizeChildOfContent(checkDeviceHasNavigationBar(activity));
}
});
//6、获取到Activity的xml布局的放置参数
frameLayoutParams = (FrameLayout.LayoutParams) mChildOfContent.getLayoutParams();
}
/*** 重新调整布局高度
* 获取界面可用高度,如果软键盘弹起后,Activity的xml布局可用高度需要减去键盘高度
** @param hasNav*/
private void possiblyResizeChildOfContent(boolean hasNav) {
//1、获取当前界面可用高度,键盘弹起后,当前界面可用布局会减少键盘的高度
int usableHeightNow = ***puteUsableHeight(hasNav);
//2、如果当前可用高度和原始值不一样
if (usableHeightNow != usableHeightPrevious) {
//3、获取Activity中xml中布局在当前界面显示的高度
int usableHeightSansKeyboard;
if (hasNav) usableHeightSansKeyboard = mChildOfContent.getHeight();//兼容华为等机型
else {
usableHeightSansKeyboard = mChildOfContent.getRootView().getHeight();//这个判断是为了解决19之前的版本不支持沉浸式状态栏导致布局显示不完全的问题
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
usableHeightSansKeyboard -= statusBarHeight;
}
}
//4、Activity中xml布局的高度-当前可用高度
int heightDifference = usableHeightSansKeyboard - usableHeightNow;
//5、高度差大于屏幕1/4时,说明键盘弹出
if (heightDifference > (usableHeightSansKeyboard / 4)) {
// keyboard probably just became visible
// 6、键盘弹出了,Activity的xml布局高度应当减去键盘高度
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && hasNav) {
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference + statusBarHeight;
} else {
frameLayoutParams.height = usableHeightSansKeyboard - heightDifference;
}
} else {
if (hasNav) frameLayoutParams.height = usableHeightNow + statusBarHeight;
else frameLayoutParams.height = usableHeightSansKeyboard;
}
//7、 重绘Activity的xml布局
mChildOfContent.requestLayout();
usableHeightPrevious = usableHeightNow;
}
}
/**
* 计算mChildOfContent可见高度
* * @return
*/
private int ***puteUsableHeight(boolean hasNav) {
if (hasNav) {
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
// 全屏模式下:直接返回r.bottom,r.top其实是状态栏的高度
if (r.top < statusBarHeight)
return r.bottom - statusBarHeight;
else return r.bottom - r.top;
} else {
Rect frame = new Rect();
activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
int statusBarHeight = frame.top;
Rect r = new Rect();
mChildOfContent.getWindowVisibleDisplayFrame(r);
//这个判断是为了解决19之后的版本在弹出软键盘时,键盘和推上去的布局(adjustResize)之间有黑色区域的问题
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
return (r.bottom - r.top) + statusBarHeight;
}
return (r.bottom - r.top);
}
}
/*** 通过"qemu.hw.mainkeys"判断是否存在NavigationBar
** @return 是否有NavigationBar
*/
private static boolean checkDeviceHasNavigationBar(Activity activity) {
boolean hasNavigationBar = false;
Resources rs = activity.getResources();
int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
if (id > 0) {
hasNavigationBar = rs.getBoolean(id);
}
try {
Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
Method m = systemPropertiesClass.getMethod("get", String.class);
String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
if ("1".equals(navBarOverride)) {
hasNavigationBar = false;
} else if ("0".equals(navBarOverride)) {
hasNavigationBar = true;
} else {
hasNavigationBar = hasNavBar(activity);
}
} catch (Exception e) {
}
return hasNavigationBar;
}
/***
* 根据屏幕真实宽高-可用宽高>0来判断是否存在NavigationBar
** @param activity 上下文* @return 是否有NavigationBar
*/
private static boolean hasNavBar(Activity activity) {
WindowManager windowManager = activity.getWindowManager();
Display d = windowManager.getDefaultDisplay();
DisplayMetrics realDisplayMetrics = new DisplayMetrics();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
d.getRealMetrics(realDisplayMetrics);
}
int realHeight = realDisplayMetrics.heightPixels;
int realWidth = realDisplayMetrics.widthPixels;
DisplayMetrics displayMetrics = new DisplayMetrics();
d.getMetrics(displayMetrics);
int displayHeight = displayMetrics.heightPixels;
int displayWidth = displayMetrics.widthPixels;
return (realWidth - displayWidth) > 0 || (realHeight - displayHeight) > 0;
}
}