以前我很不看好uni-app,现在又点后悔没有早一点使用了,他可以打包我们的web成js放入资源,供ios和android使用。
如果再结合activity不销毁,隐藏的方法,像里面传递参数,来改变页面,不销毁webview,我发现这样比原生的都要快。这样又能用于app端,又能生成小程序,何乐而不为
正式应为uni-app最基础的功能,可以帮我完成最流程的功能。嘻嘻。
下面看怎么使用吧。
我还是不倾向与用uni-app直接开发原生app的,因为这样确实体验不好,也没有那么自由,所有我用这种混合式开发。
本来是使用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" />1234567891011
目录结构
软件运行流程
初次安装软件后,打开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>12345678910111213
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(); }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337
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>1234567891011121314151617181920212223242526272829303132333435
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'}12345678910111213141516171819202122232425262728293031323334353637
com.jaeger.statusbarutil:library:1.4.0
就是前面说的statusbar
打包教程
https://blog.csdn.net/weixin_43373239/article/details/107833035
注意事项
正式打包生成apk的时候,如果使用的不是同一个签名证书,更新app的时候会出现签名不同,无法安装,只要把原app卸载即可。