将uniapp打包成h5放在安卓webview中(解决uniapp引入第三方地图卡顿问题)
本来是使用uniapp进行开发,然后打包成安卓软件的,因为是用了地图模块(基于天地图),因为uniapp框架的限制,只能使用webview组件引入地图文件,然后出现一个问题,发现地图在浏览器中打开很流畅,打包成app之后非常卡顿,试了很多种办法,包括把地图放在vue文件中来渲染,发现依然卡,然后想到能不能直接把打包成H5,然后整个文件丢在安卓中,用安卓的webview去打开它,尝试之后发现效果还挺好的,下面列出代码(自己需要啥功能需要自己去添加,可以自行添加腾讯的X5内核)
具体步骤
-
HbuilderX把uniapp项目打包成H5手机版
-
新建一个安卓项目
-
新建一个assets目录(src目录单击右键选择,弹出的提示框选择finish就可以自动生成assets目录了)
-
把打包好的文件发到assets目录
-
编写程序
-
运行
uniapp打包注意点
将app打包成H5手机版注意别忘了把运行的基础路径改为./
开发工具
Android Studio版本 4.0.1
Java版本 Java1.8(使用到了lambda表达式,需要去设置为1.8,否者会报错)设置教程:https://blog.csdn.net/weixin_43373239/article/details/88741896
使用到的库
statusbar
状态栏工具类(实现沉浸式状态栏/变色状态栏)
https://jaeger.itscoder.com/android/2016/03/27/statusbar-util.html
使用到的权限
<!--外部存储权限 这个目前没有使用到-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--获取wifi和网络信息-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--网络权限-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!--定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
目录结构
软件运行流程
初次安装软件后,打开app,会弹出申请定位权限提示框,如果用户没有授权,则会弹出“请开启定位权限”提示框,用户点击确定后会自动关闭软件,如果用户授权之后,会弹出申请获取手机信息权限(因为在华为手机上如果没有获取此项权限可能无法使用定位功能),如果用户没有授权会提示“请开启获取手机信息权限”,用户点击确定按钮后会自动关闭软件,当用户开启所有权限后,会进入到登录页面,因为网页是存放在安卓本地,所以不会存在用户断网之后出现404页面的情况。
布局文件(activity_mian.xml)
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<WebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="none" />
</androidx.constraintlayout.widget.ConstraintLayout>
android:scrollbars="none"
不显示滚动条
主要Activity(MainActivity)
package com.example.opeak;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Color;
import android.location.LocationManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.GeolocationPermissions;
import android.webkit.JsPromptResult;
import android.webkit.JsResult;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.jaeger.library.StatusBarUtil;
public class MainActivity extends Activity {
private WebView webView;
private static final int LOCATION_CODE = 1;
private static final int READ_PHONE_CODE = 2;
private boolean isWebLocation = false;
private ValueCallback<Uri[]> valueCallback;
private boolean uploadImage = true;
/**
* 顶部背景色 蓝 灰
*/
private String[] topColors = {"#FFFFFF", "#52A8F9", "#F8F8F8"};
private String indexPageUrl = "file:///android_asset/h5/index.html#/pages/index/index";
private String loginPageUrl = "file:///android_asset/h5/index.html#/";
@SuppressLint("SetJavaScriptEnabled")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getGPSPermission();
String url = "file:///android_asset/h5/index.html";
webView = findViewById(R.id.webView);
WebSettings webSettings = webView.getSettings();
//开启JavaScript
webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);
//设置可以访问文件
webSettings.setAllowFileAccess(true);
webSettings.setLoadsImagesAutomatically(true);
webView.setVerticalScrollBarEnabled(false);
webView.setHorizontalScrollBarEnabled(false);
webSettings.setAppCacheEnabled(true);
webSettings.setDatabaseEnabled(true);
String dir = getApplicationContext().getDir("database", Context.MODE_PRIVATE).getPath();
webSettings.setGeolocationDatabasePath(dir);
webSettings.supportMultipleWindows();
webSettings.setAllowContentAccess(true);
webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
//允许地理位置可用
webSettings.setGeolocationEnabled(true);
webSettings.setJavaScriptCanOpenWindowsAutomatically(true);
//加载url
webView.loadUrl(url);
//不可复制 就是拦截长按事件(如果要改成可以复制 把下面的代码注释掉就可以了)
webView.setOnLongClickListener(v -> true);
webView.setWebChromeClient(new WebChromeClient() {
//网页申请定位后回调
@Override
public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
//判断是否有权限、是否开启了定位功能
locationPermission();
//invoke(申请定位网站的网址,是否同意定位,是否缓存授权)
callback.invoke(origin, isWebLocation, false);
}
//选择文件
@Override
public boolean onShowFileChooser(WebView webView, ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {
showToast("请选择头像!");
MainActivity.this.valueCallback = valueCallback;
showFileChooser();
//如果filePathCallback被调用;返回false,如果忽略处理
return uploadImage;
}
//js弹框 下面的几个分别对应js中的alert弹框、confirm弹框等等
@Override
public boolean onJsAlert(WebView webView, String url, String message, JsResult jsResult) {
jsResult.confirm();
showToast(message);
return true;
}
@Override
public boolean onJsConfirm(WebView webview, String url, String message, JsResult result) {
//可以弹框或进行其它处理,但一定要回调result.confirm或者cancel
showMeDialog(message, (dialogInterface, i) -> {
result.confirm();
},
(dialogInterface, i) -> {
result.cancel();
});
return true;
}
@Override
public boolean onJsBeforeUnload(WebView webview, String url, String message, JsResult result) {
//可以弹框或进行其它处理,但一定要回调result.confirm或者cancel
return true;
}
@Override
public boolean onJsPrompt(WebView webview, String url, String message, String defaultValue, JsPromptResult result) {
//可以弹框或进行其它处理,但一定要回调result.confirm或者cancel,confirm可以将用户输入作为参数
return true;
}
});
webView.setWebViewClient(new WebViewClient() {
/**
* 顶部任务栏颜色变化
* @param webView
* @param s
* @param b
*/
@Override
public void doUpdateVisitedHistory(WebView webView, String s, boolean b) {
if (s.equals(indexPageUrl)) {
StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[1]));
} else if (s.equals(loginPageUrl)) {
StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[0]));
} else {
StatusBarUtil.setColor(MainActivity.this, Color.parseColor(topColors[2]));
}
}
});
}
/**
* 判断是否拥有定位权限
* PERMISSION_GRANTED:有权限
*/
public void locationPermission() {
LocationManager lm = (LocationManager) MainActivity.this.getSystemService(LOCATION_SERVICE);
boolean ok = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
if (ok) {
getGPSPermission();
} else {
isWebLocation = false;
showToast("未开启GPS定位服务,定位功能将受限!");
}
}
/**
* 申请定位权限
*/
public void getGPSPermission() {
//没有权限
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION)
!= PackageManager.PERMISSION_GRANTED) {
// 没有权限,申请权限。
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION}, LOCATION_CODE);
} else {
isWebLocation = true;
phoneInformationPermission();
}
}
/**
* 判断是否有获取手机信息权限(没有此权限,华为手机不能定位)
*/
public void phoneInformationPermission() {
//如果没有权限
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
// 没有权限,申请权限。
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.READ_PHONE_STATE}, READ_PHONE_CODE);
}
}
/**
* 申请权限后会回调
*
* @param requestCode
* @param permissions
* @param grantResults
*/
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == LOCATION_CODE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
isWebLocation = true;
phoneInformationPermission();
} else {
isWebLocation = false;
showMeDialog("请开启手机定位权限!",
(dialogInterface, i) -> {
closeApp();
},
null);
}
} else if (requestCode == READ_PHONE_CODE) {
if (grantResults.length <= 0 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
showMeDialog("请开启获取手机信息权限!",
(dialogInterface, i) -> {
closeApp();
},
null);
}
}
}
/**
* 关闭app
*/
private void closeApp() {
finish();
System.exit(0);
}
/**
* 显示文件选择器
*/
private void showFileChooser() {
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("image/*");//设置类型,我这里是任意类型,任意后缀的可以这样写。
intent.addCategory(Intent.CATEGORY_OPENABLE);
startActivityForResult(intent, 1);
}
/**
* 上传图片后回调
*
* @param requestCode
* @param resultCode
* @param data
*/
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {//是否选择,没选择就不会继续
Uri uri = data.getData();
Uri[] uris = new Uri[]{uri};
valueCallback.onReceiveValue(uris);
uploadImage = false;
showToast("正在上传头像,请稍后!");
} else {
uploadImage = true;
valueCallback.onReceiveValue(null);
showToast("没有选择头像文件!");
}
}
/**
* 编写后退事件
*
* @param keyCode
* @param event
* @return
*/
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {
//时候可以后退
boolean goBack = webView.canGoBack();
if (goBack) {
//后退
webView.goBack();
} else {
showCoverDialog();
}
return true;
}
//继续执行父类的其他点击事件
return false;
}
/**
* 显示退出弹框
*/
private void showCoverDialog() {
showMeDialog("是否退出本程序", (dialogInterface, i) -> {
closeApp();
},
(dialogInterface, i) -> {
});
}
/**
* 显示弹框
*
* @param message 内容
* @param ok 点击确定的事件
* @param cancel 点击取消的事件
*/
private void showMeDialog(String message, DialogInterface.OnClickListener ok, DialogInterface.OnClickListener cancel) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("提示");
builder.setMessage(message);
builder.setPositiveButton("是", ok);
if (cancel != null) {
builder.setNegativeButton("否", cancel);
}
builder.show();
}
@Override
protected void onDestroy() {
//删除地理位置授权,也可以删除某个域名的授权(参考接口类)
GeolocationPermissions.getInstance().clearAll();
webView.destroy();
super.onDestroy();
}
/**
* 显示提示(去除小米手机自带应用名)
*
* @param message 消息
*/
private void showToast(String message) {
Toast toast = Toast.makeText(this, null, Toast.LENGTH_SHORT);
toast.setText(message);
toast.show();
}
}
AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.example.opeak">
<!--外部存储权限-->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!--获取wifi和网络信息-->
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!--网络权限-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<!--定位-->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<application
android:name=".MeApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="true"
tools:targetApi="m">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
android:usesCleartextTraffic=true
别忘了加(如果你的网页都是https的请求那就可以不用加了)。
摘取网络上的一段解释:
android:usesCleartextTraffic
指示应用程序是否打算使用明文网络流量,例如明文HTTP。目标API级别为27或更低的应用程序的默认值为“ true”。面向API级别28或更高级别的应用默认为“ false”。
当属性设置为“ false”时,平台组件(例如,HTTP和FTP堆栈,DownloadManager和MediaPlayer)将拒绝应用程序使用明文流量的请求。强烈建议第三方库也采用此设置。避免明文通信的主要原因是缺乏机密性,真实性和防篡改保护;网络攻击者可以窃听所传输的数据,并且还可以对其进行修改而不会被检测到。
build.gradle文件
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion "30.0.1"
defaultConfig {
applicationId "com.example.opeak"
minSdkVersion defaultMinSdkVersion
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'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
implementation 'com.jaeger.statusbarutil:library:1.4.0'
}
com.jaeger.statusbarutil:library:1.4.0
就是前面说的statusbar
打包教程
https://blog.csdn.net/weixin_43373239/article/details/107833035
注意事项
- 正式打包生成apk的时候,如果使用的不是同一个签名证书,更新app的时候会出现签名不同,无法安装,只要把原app卸载即可。
本文地址:https://blog.csdn.net/weixin_43373239/article/details/107931367
上一篇: 微信编辑器怎么编辑消息并发到公众号中?
下一篇: 北方小年有什么风俗