【手把手】带你撸一个安卓壳子

web前端的小伙伴们大家好,说起APP混合开发,大家首先想到的可能就是类似Cordova的库,可以让我们不写一行安卓代码,就轻松地实现原生APP的一些常用功能,但是对于一些特别的"需求"我们就无能为力了。所以为了提高我们的知识储备,我觉得还是有必要学习一波安卓的知识的。下面就由我带着大家手把手撸一个安卓的壳子出来,由于本人也是第一次写安卓的东西,难免会有些不对的地方,希望各路大神见怪莫怪。

1、下载Android Studio

官网下载地址,开发安卓貌似就只有这一个编辑器可用了,这个IDE风格和webStorm的风格一模一样,应该是一家公司的产品,这个没有深究过。安装过程在此就不详细说明了,有需要的可以自行百度。

2、创建一个新项目

打开IDE,点击 Start a new Android Studio project 创建一个新项目 【手把手】带你撸一个安卓壳子_第1张图片

填写下面的信息(APP名称和域名建议和我填写一样,这样下面就不用改了^_^),点击next
【手把手】带你撸一个安卓壳子_第2张图片

选择APP支持的安卓最低版本,点击next
【手把手】带你撸一个安卓壳子_第3张图片

然后选择一个空白页,点击next
【手把手】带你撸一个安卓壳子_第4张图片

这里直接默认,最后点击 Finish
【手把手】带你撸一个安卓壳子_第5张图片

3、启动项目

至此,我的第一个APP已经创建完成,怎么运行它呢?有两种运行方式,一种是模拟器、一种真机。我建议使用真机,模拟器可能会出现一些意想不到的问题。

选择运行模式
【手把手】带你撸一个安卓壳子_第6张图片

第一个代表模拟器,第二个是真机,这里我们选择真机 USB Device,点击OK
【手把手】带你撸一个安卓壳子_第7张图片

接下来把我们的手机通过USB连接电脑,注意手机要打开 USB调试 ,简单说一下手机怎么打开 USB调试,以华为畅玩9为例,首先我们要打开手机的开发者模式,依次点击 设置>系统>关于手机>连续点击版本号(考验手速的时候到了!)   直到出现 "打开手机开发者模式"字样的提示,然后我们再点击 设置>系统>开发者选项>USB调试开关打开。

然后点击下面的绿色三角,运行程序,开始运行的时候手机上可能会弹出来一次 允许调试的授权框 ,我们点击允许。
【手把手】带你撸一个安卓壳子_第8张图片

大功告成!我们可以看到APP已经成功安装到手机上了
【手把手】带你撸一个安卓壳子_第9张图片

4、修改APP名称和图标

我们可以看到默认的APP图标和名称,作为一名优秀的前端开发,简直不能忍
【手把手】带你撸一个安卓壳子_第10张图片

修改图标:选择 File>New>Image Asset
【手把手】带你撸一个安卓壳子_第11张图片

Name的值不要改,Asset Type 选择 Image,path选择我们的图标路径,再通过 Resize 调整图片到合适大小,点击next。
【手把手】带你撸一个安卓壳子_第12张图片

我们看到了很多红色,不用鸟他,直接Finish
【手把手】带你撸一个安卓壳子_第13张图片

修改名称:打开 app>res>values>strings.xml  修改其中的 app_name 的值
【手把手】带你撸一个安卓壳子_第14张图片

修改完毕,我们再重新运行一下程序,哈哈,非常完美!
【手把手】带你撸一个安卓壳子_第15张图片

5、创建 webView,并去掉标题栏

既然是混合开发,webview是必须的,理论上我们的Android架子只需要一个页面放置webview即可。
首先我们要允许webview联网,打开 app>manifests>AndroidManifest.xml,在 application 上面添加一行:

打开  app>res>layout>activity_main.xml 把默认的 TextView 控件删掉,加上webview控件,代码如下:




    

我们发现还有一个默认的标题,这个也是不需要的,打开 app>manifests>AndroidManifest.xml,找到android:theme="@style/AppTheme"  改成下面的

android:theme="@style/Theme.AppCompat.Light.NoActionBar"

再次运行一下,标题栏没有了,但是白屏,那是因为我们还没有给webview链接。

6、webview的一些基本配置

*设置链接:

我们修改 MainActivity.java 文件中的MainActivity类,如下,(如果某个单词出现红色,说明没有引入包,把鼠标定位到红色单词上,按住 Alt + Enter 按照提示引入相应的包即可,之后不再重复说明此问题。
myWebView.loadUrl( "https://zhengshaoguo.com/static/app/" ); 这行就是webview要加载的地址,我这里放的是一个服务器的地址,当然也可以放本地的,本地怎么放得这里就不做说明了。

public class MainActivity extends AppCompatActivity {
    private WebView myWebView = null;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate( savedInstanceState );
        setContentView( R.layout.activity_main );

        myWebView = (WebView) findViewById( R.id.webview );
        WebSettings webSettings = myWebView.getSettings();
        myWebView.loadUrl( "https://zhengshaoguo.com/static/app/" );
    }
}

服务器上对应的HTML文件如下:js相信大家都看的懂,就不解释了。



   
      
      Carson  
      
   
   
      
      

*启用JS:

我们重新运行APP,当点击按钮之后并没有出现文字,这是因为webview默认是禁用JS的,在 MainActivity.java 中
WebSettings webSettings = myWebView.getSettings(); 后面加一句

webSettings.setJavaScriptEnabled( true );

重新运行APP,一切正常。

*让所有的超链接都在webview中打开:

我们再HTML文件中加入一个超链接

点击去百度

重新运行APP,当我们点击 去百度 之后就会提示我们用默认浏览器打开这个网址,
【手把手】带你撸一个安卓壳子_第16张图片

对于混合开发APP,这样肯定是不行的,我们要让所有的链接都在webview中打开,在 MainActivity.java 中
webSettings.setJavaScriptEnabled( true ); 后面上如下内容

myWebView.setWebViewClient(new WebViewClient() {
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        return false;
    }
});

*允许webview拨打电话:

前端的小伙伴们都知道,给一个a标签的href属性设置为 tel:18538328225 ,在微信中,点击这个标签就可以调起手机通讯录,便于快捷拨号,那我们在我们的APP中试一下。结果却很尴尬。居然毫无反应,不能忍,改!修改 setWebViewClient() 方法如下:

myWebView.setWebViewClient(new WebViewClient() {
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if (url.startsWith("tel:")) {
            Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse(url));
            startActivity(intent);
            return true;
        }
        return false;
    }
});

*开启Storage:

sessionStorage和localStorage在很多项目里都会用到,特别是移动端,webview默认是不允许js访问的需要我们手动开启
在 setWebViewClient() 方法下面加上下面内容

webSettings.setDomStorageEnabled( true );
webSettings.setAppCacheMaxSize( 1024 * 1024 * 8 );
String appCachePath = getApplicationContext().getCacheDir().getAbsolutePath();
webSettings.setAppCachePath( appCachePath );
webSettings.setAllowFileAccess( true );
webSettings.setAppCacheEnabled( true );

*返回键返回webview的历史记录:

当我们点击 去百度 就在本页面就打开了百度的链接,但当我们点击手机的物理返回键后,并没有返回我们的首页,而是直接退出了程序。这个也需要我们去做处理,在 onCreate() 的后面添加

public boolean
onKeyDown(int keyCode, KeyEvent event) {
	if (keyCode == KeyEvent.KEYCODE_BACK && myWebView.canGoBack()) {
		myWebView.goBack();//返回上个页面
		return true;
	}
	return super.onKeyDown(keyCode, event);//退出H5界面
}

7、允许 input type:file 选择文件

这一块的内容还是比较多,也不是必须要开启的功能,所以单独拿出来说,在上传图片的时候,我们会用input标签 type=file 就能实现选择文件,并上传的功能,同样的,这也需要webview提供支持,在 private WebView myWebView = null; 后面加上  

private ValueCallback uploadMessageAboveL;

然后在  setWebViewClient() 后面添加

webSettings.setAllowFileAccess(true);
myWebView.setWebChromeClient( new WebChromeClient(){
	public boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams ) {
		if (uploadMessageAboveL != null) {
			uploadMessageAboveL.onReceiveValue(null);
			uploadMessageAboveL = null;
		}
		uploadMessageAboveL = filePathCallback;
		Intent i = new Intent(Intent.ACTION_GET_CONTENT);
		i.addCategory(Intent.CATEGORY_OPENABLE);
		i.setType("image/*");
		startActivityForResult( Intent.createChooser(i, "Image Chooser"), 2);
		return true;
	}
});

最后在 onKeyDown() 后面加上

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult( requestCode, resultCode, data );
    if (requestCode == 2) {
        if (null == uploadMessageAboveL) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (uploadMessageAboveL != null) {
            onActivityResultAboveL(requestCode, resultCode, data);
        }

    }
}

//webview可以选择文件
private void onActivityResultAboveL(int requestCode, int resultCode, Intent data) {
    System.out.println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
    if (requestCode != 2 || uploadMessageAboveL == null) {
        return;
    }
    Uri[] results = null;
    if (resultCode == Activity.RESULT_OK) {
        if (data == null) {

        } else {
            String dataString = data.getDataString();
            ClipData clipData = data.getClipData();
            if (clipData != null) {
                results = new Uri[clipData.getItemCount()];
                for (int i = 0; i < clipData.getItemCount(); i++) {
                    ClipData.Item item = clipData.getItemAt(i);
                    results[i] = item.getUri();
                }
            }
            if (dataString != null) {
                results = new Uri[]{Uri.parse(dataString)};
            }
        }
    }
    uploadMessageAboveL.onReceiveValue(results);
    uploadMessageAboveL = null;
    return;
}

完美收工!至此一个完整的 webview 已经完成。谢谢阅读!

...

...

...

...

...

陈大海:你这个也太low了,还不如去用cordova,根本不用自己配置!
 

em....,看来只好放大招了!

8、JS 和 Android 实现交互

js和android实现交互有很多种方式,这里我选择了最简单的一种,以供大家快速上手。
首先我们把 Android 的类对象(AndroidtoJs)和JS(test)的对象建立映射关系,在 myWebView.loadUrl() 上面加一行

myWebView.addJavascriptInterface( new AndroidtoJs(), "test" );

然后在 onKeyDown()  下面添加与JS映射的类并添加一个hello方法

public class AndroidtoJs extends Object {
    @JavascriptInterface
    public void hello(String msg) {
        final String content = msg;
        myWebView.post( new Runnable() {
            @RequiresApi(api = Build.VERSION_CODES.KITKAT)
            @Override
            public void run() {
                myWebView.evaluateJavascript( "javascript:test.helloFn('hello,"+content+"')", new ValueCallback() {
                    @Override
                    public void onReceiveValue(String value) { }
                } );
            }
        } );
    }
}

编辑我们的JS文件




  
  Carson
  


  
  
点击去百度 打电话

效果如下:
【手把手】带你撸一个安卓壳子_第17张图片

简单解释一下,因为 JS的 test 对象和Android的 AndroidtoJs 类进行了映射关系,所以当我用 js  调用 test 的 hello 方法,就相当于android 的 AndroidtoJs 去调用 hello 方法一样,反过来也是一样的。
既然js和android已经实现了交互,那么我们就来搞一波事情吧。

9、获取当前的实时GPS位置

首先我们定义一个方法,实现和JS的交互,在 AndroidtoJs 类里添加

//获取当前地理位置
@JavascriptInterface
public void getPosition(String msg) {
    showGPSContacts();
}

然后我们在 AndroidManifest.xml 文件中加上权限



最后在 MainActivity.java 文件中的 onActivityResult() 方法后面加上

//APP授权的回调
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if(requestCode == 100){
        if (grantResults[0] == PERMISSION_GRANTED && grantResults.length > 0) {
            getLocation();
        } else {
            showGPSContacts();
        }
    }
}

//获取地理位置
LocationManager lm;
Boolean positionNum=false;
public void showGPSContacts() {
    lm = (LocationManager) MainActivity.this.getSystemService(MainActivity.this.LOCATION_SERVICE);
    boolean ok = lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
    if (ok) {
        if (Build.VERSION.SDK_INT >= 23) {
            if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.ACCESS_FINE_LOCATION) != PERMISSION_GRANTED) {  // 没有权限,申请权限。
                ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE}, 100);
            } else {
                getLocation();
            }
        } else {
            getLocation();
        }
    } else {
        Intent intent = new Intent();
        intent.setAction(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
        startActivityForResult(intent, 1315);
    }
}
private void getLocation() {
    LocationManager locationManager;
    String serviceName = Context.LOCATION_SERVICE;
    locationManager = (LocationManager) this.getSystemService(serviceName);
    Criteria criteria = new Criteria();
    criteria.setAccuracy(Criteria.ACCURACY_FINE);
    criteria.setAltitudeRequired(false);
    criteria.setBearingRequired(false);
    criteria.setCostAllowed(true);
    criteria.setPowerRequirement(Criteria.POWER_LOW);
    String provider = locationManager.getBestProvider(criteria, true);
    if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PERMISSION_GRANTED && ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_COARSE_LOCATION) != PERMISSION_GRANTED) { return; }
    Location location = locationManager.getLastKnownLocation(provider); // 通过GPS获取位置
    updateLocation(location);
    if(!positionNum){
        positionNum=true;
        lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, 2000, 8, new LocationListener() {
            @Override
            public void onLocationChanged(Location location) {
                updateLocation(location);
            }
            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {

            }
            @SuppressLint("MissingPermission")
            @Override
            public void onProviderEnabled(String provider) {
                updateLocation(lm.getLastKnownLocation(provider));
            }
            @Override
            public void onProviderDisabled(String provider) {
                updateLocation(null);
            }
        });
    }
}
private void updateLocation(Location location) {
    if (location != null) {
        final double latitude = location.getLatitude();
        final double longitude = location.getLongitude();
        System.out.println(latitude+":"+longitude);
        myWebView.post( new Runnable() {
            @RequiresApi(api = Build.VERSION_CODES.KITKAT)
            @Override
            public void run() {
                myWebView.evaluateJavascript( "javascript:test.getPositionFn('" + latitude+"','"+longitude+ "')", new ValueCallback() {
                    @Override
                    public void onReceiveValue(String value) { }
                } );
            }
        } );
    } else {
        System.out.println("无法获取到位置信息");
    }
}

修改JS文件

test.getPosition('');
test.getPositionFn=function(lat,lng){
  document.getElementById("output").innerHTML = "lat:"+lat+",lng:"+lng;
}

重新运行APP查看效果
【手把手】带你撸一个安卓壳子_第18张图片

说明,首先会去判断手机有没有打开GPS,没有打开跳到手机系统打开GPS的页面,打开了GPS的,再去向用户请求获取地理位置的权限,等用户授权之后,每两秒查询一次用户的位置,如果有更新,就调用 js 的方法,把最新的经纬度传过去。

10、实现扫描二维码

这里我们使用一个第三方的包来实现此功能

引入包文件,打开下面文件
【手把手】带你撸一个安卓壳子_第19张图片
修改  其中的 allprojects 如下

allprojects {
    repositories {
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
    }
}

 打开下面文件
【手把手】带你撸一个安卓壳子_第20张图片

在 dependencies 中加入

implementation 'com.github.yuzhiqiang1993:zxing:2.2.1'

注意修改这两个文件之后窗口的顶部会出现如下提示,需要我们点击安装
【手把手】带你撸一个安卓壳子_第21张图片

然后我们在 AndroidManifest.xml 文件中加上权限

 AndroidtoJs 类里定义一个和JS交互的方法

//启动扫描二维码
@JavascriptInterface
public void scanQRCode(String msg) {
	if (ContextCompat.checkSelfPermission( MainActivity.this, android.Manifest.permission.CAMERA ) != PERMISSION_GRANTED) {
		ActivityCompat.requestPermissions( MainActivity.this, new String[]{Manifest.permission.CAMERA}, 1 );
	} else {
		Intent intent = new Intent( MainActivity.this, CaptureActivity.class );
		startActivityForResult( intent, 111 );
	}
}

在 onRequestPermissionsResult() 中加一个打开摄像头的授权方法

//APP授权的回调
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if(requestCode == 100){
        if (grantResults[0] == PERMISSION_GRANTED && grantResults.length > 0) {
            getLocation();
        } else {
            showGPSContacts();
        }
    }else if(requestCode == 1){
        if(grantResults.length > 0 && grantResults[0] == PERMISSION_GRANTED){
            Intent intent = new Intent( MainActivity.this, CaptureActivity.class );
            startActivityForResult( intent, 111 );
        }
    }
}

最后在 onActivityResult() 方法里加上扫描二维码的结果

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult( requestCode, resultCode, data );
    if (requestCode == 2) {
        if (null == uploadMessageAboveL) return;
        Uri result = data == null || resultCode != RESULT_OK ? null : data.getData();
        if (uploadMessageAboveL != null) {
            onActivityResultAboveL(requestCode, resultCode, data);
        }
    }else if (requestCode == 111 && resultCode == RESULT_OK) {
        if (data != null) {
            final String content = data.getStringExtra( Constant.CODED_CONTENT );
            myWebView.post( new Runnable() {
                @RequiresApi(api = Build.VERSION_CODES.KITKAT)
                @Override
                public void run() {
                    myWebView.evaluateJavascript( "javascript:test.scanQRCodeFn('" + content + "')", new ValueCallback() {
                        @Override
                        public void onReceiveValue(String value) { }
                    } );
                }
            } );
        }
    }
}

在修改一下JS的调用

function qrCode(){
	test.scanQRCode('');
}
test.scanQRCodeFn=function(text){
	document.getElementById("qrCode").innerHTML = "扫描二维码的结果是:"+text;
}

最后再重新运行一下
【手把手】带你撸一个安卓壳子_第22张图片【手把手】带你撸一个安卓壳子_第23张图片

已经连续奋战3个小时了,今天就到这里了,存个档,有需要的朋友可以点这里下载全部代码

你可能感兴趣的:(javaScript)