3

代码

在android内打开一个网页的时候,有时我们会要求与网页有一些交互。而这些交互是在基于javaScript的基础上。那么我们来学习一下android如何与网页进行JS交互。完整代码如下:

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.app.FragmentActivity;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.JavascriptInterface;
import android.webkit.URLUtil;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

/**
 * 软件内通用打开网页的容器页面
 *
 * @author ZRP
 */
public class CommonWebActivity extends FragmentActivity {

    private ProgressBar progress_bar;
    protected CustomFrameLayout customFrameLayout;
    protected TextView titleText, errorTxt;
    protected View refresh;// 刷新按钮
    protected WebView webView;

    protected String url = "";// 网址url
    protected String param = "";// 交互参数,如json字符串

    protected WebChromeClient chromeClient = new WebChromeClient() {
        public void onProgressChanged(WebView view, int newProgress) {
            if (newProgress == 100) {
                progress_bar.setVisibility(View.GONE);
                // 判断有无网络
                if (!NetUtils.isAvailable(CommonWebActivity.this)) {
                    customFrameLayout.show(R.id.common_net_error);
                    refresh.setVisibility(View.VISIBLE);
                    refresh.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            webView.loadUrl(url);
                        }
                    });
                } else {
                    // 判断网络请求网址是否有效
                    if (!URLUtil.isValidUrl(url)) {
                        customFrameLayout.show(R.id.common_net_error);
                        errorTxt.setText("无效网址");
                    } else {
                        customFrameLayout.show(R.id.common_web);
                    }
                }
            } else {
                progress_bar.setVisibility(View.VISIBLE);
                progress_bar.setProgress(newProgress);
            }
        }

        // 获取到url打开页面的标题
        public void onReceivedTitle(WebView view, String title) {
            titleText.setText(title);
        }

        // js交互提示
        public boolean onJsAlert(WebView view, String url, String message, android.webkit.JsResult result) {
            return super.onJsAlert(view, url, message, result);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        url = getIntent().getStringExtra("url");
        param = getIntent().getStringExtra("param");
        setContentView(R.layout.common_web_activity);
        initView();
    }

    @SuppressLint("SetJavaScriptEnabled")
    private void initView() {
        findViewById(R.id.back).setOnClickListener(new OnClickListener() {
            @Override
            public void onClick(View v) {
                finish();
            }
        });

        titleText = (TextView) findViewById(R.id.title);
        progress_bar = (ProgressBar) findViewById(R.id.progress_bar);
        customFrameLayout = (CustomFrameLayout) findViewById(R.id.web_fram);
        customFrameLayout.setList(new int[]{R.id.common_web, R.id.common_net_error});
        refresh = findViewById(R.id.error_btn);
        errorTxt = (TextView) findViewById(R.id.error_txt);

        webView = (WebView) findViewById(R.id.common_web);
        webView.getSettings().setDefaultTextEncodingName("utf-8");
        webView.getSettings().setJavaScriptEnabled(true);
        synCookies();//格式化写入cookie,需写在setJavaScriptEnabled之后
        webView.setWebChromeClient(chromeClient);
        webView.setWebViewClient(new WebViewClient() {// 让webView内的链接在当前页打开,不调用系统浏览器
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });
        webView.addJavascriptInterface(new JavaScriptInterface(), "zrp");
        webView.loadUrl(url);

        new Handler().postDelayed(new Runnable() {//异步传本地数据给网页
            @Override
            public void run() {
                onNotifyListener(param);
            }
        }, 1000);
    }

    /**
     * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
     * 需要在当前用户退出登录的时候进行清除
     */
    private void synCookies() {
        String[] split = App.cookie.split(";");
        for (int i = 0; i < split.length; i++) {
            CookieSyncManager.createInstance(CommonWebActivity.this);
            CookieManager.getInstance().setCookie(url, split[i]);
            CookieSyncManager.getInstance().sync();
        }
    }

    /**
     * 回调网页中的脚本接口。
     *
     * @param notify 传给网页的通知内容。
     */
    public void onNotifyListener(String notify) {
        if (webView != null) {
            webView.loadUrl("javascript:test('" + notify + "')");
        }
    }

    /**
     * android js交互实现:
     * - window.zrp.command("");//在网页的方法中添加该代码获取android内容
     * - webView.loadUrl("javascript:test('param')");//android给网页传值,须异步执行
     */
    public class JavaScriptInterface {
        
        @JavascriptInterface
        public void command(String jsonString) {
            if (TextUtils.isEmpty(jsonString)) {
                return;
            }
            //根据网页交互回传的json串进行操作
            Toast.makeText(CommonWebActivity.this, jsonString, Toast.LENGTH_LONG).show();
        }
    }
}

切记:需要设置联网权限与网络状态获取权限!

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

其中CustomFrameLayout为界面切换控件,分别在无网络,网址错误等情况下进行提示:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;

/**
 * 用于状态切换的布局,只显示1个状态
 */
public class CustomFrameLayout extends FrameLayout {
    private int[] list;

    public CustomFrameLayout(Context context) {
        super(context);
        initView();
    }

    public CustomFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public CustomFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    /**
     * 设置子面板id数组
     *
     * @param list
     */
    public void setList(int[] list) {
        this.list = list;
        show(0);
    }

    /**
     * 显示某个面板
     *
     * @param id
     */
    public void show(int id) {
        if (list == null) {
            for (int i = 0; i < getChildCount(); ++i) {
                View view = getChildAt(i);

                if (id == view.getId()) {
                    view.setVisibility(View.VISIBLE);
                } else {
                    view.setVisibility(View.GONE);
                }
            }
            return;
        }

        for (int aList : list) {
            View item = findViewById(aList);
            if (item == null) {
                continue;
            }
            if (aList == id) {
                item.setVisibility(View.VISIBLE);
            } else {
                item.setVisibility(View.GONE);
            }
        }
    }

    /**
     * 隐藏所有面板
     */
    public void GoneAll() {
        if (list == null) {
            for (int i = 0; i < getChildCount(); ++i) {
                View view = getChildAt(i);
                view.setVisibility(View.GONE);
            }
            return;
        }

        for (int aList : list) {
            View item = findViewById(aList);
            if (item == null) {
                continue;
            }
            item.setVisibility(View.GONE);
        }
    }

    /**
     * 切换。
     *
     * @param index 布局在fram中的index
     */
    public void showOfIndex(int index) {
        for (int i = 0; i < getChildCount(); ++i) {
            View view = getChildAt(i);

            if (index == i) {
                view.setVisibility(View.VISIBLE);
            } else {
                view.setVisibility(View.GONE);
            }
        }
    }

    public void initView() {
    }
}

界面布局为:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <LinearLayout
        android:id="@+id/back"
        android:layout_width="wrap_content"
        android:layout_height="50dp"
        android:gravity="center"
        android:orientation="horizontal"
        android:paddingLeft="10dp">

        <ImageView
            android:layout_width="15dp"
            android:layout_height="15dp"
            android:src="@drawable/ic_back" />

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="返回"
            android:textSize="15sp" />
    </LinearLayout>

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:gravity="center"
        android:text="加载中"
        android:textSize="15sp" />

    <View
        android:layout_width="match_parent"
        android:layout_height="1dp"
        android:layout_below="@+id/title"
        android:background="#666666" />

    <ProgressBar
        android:id="@+id/progress_bar"
        style="@style/update_progress"
        android:layout_width="match_parent"
        android:layout_height="3dp"
        android:layout_marginTop="51dp"
        android:max="100" />

    <com.zrp.webviewdemo.web.CustomFrameLayout
        android:id="@+id/web_fram"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_marginTop="51dp">

        <WebView
            android:id="@+id/common_web"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

        <include layout="@layout/common_net_error" />

    </com.zrp.webviewdemo.web.CustomFrameLayout>

</RelativeLayout>

混淆打包

JavaScript是通过调用android中指定的方法名来进行值传递的,所以android部分的代码不能进行混淆,需要添加keep语句:-keep class com.zrp.webviewdemo.web.CommonWebActivity$JavaScriptInterface {*;}

测试分析

好了,这时候我们来添加一些数据进行测试:

用上面的方法为web页面中的param赋值,在asset文件夹中放入要进行测试的html文件,url赋值为:url = "file:///android_asset/html.html";
如上html.html文件源码为:

<!DOCTYPE html>
<html>
    <head>
        <title>测试容器调用</title>
    </head>
    <body>
        <div class="main">
            <button onclick="test2()">获取网页的值</button>
        </div>

        <script type="text/javascript">

           function test(json){
               alert(json);
           }

            function test2(){
                alert("网页传值给android");
                window.zrp.command("{'type':1,'text':'hello boy'}");
            }
        </script>
    </body>
</html>
android传值给web:可通过在java代码中异步执行webView.loadUrl("javascript:test("这是需要传给网页的数据")");来调用网页代码中的test()方法传值给web端。

web传值给android:在网页代码中可以看到button的点击事件添加了window.zrp.command("{'type':1,'text':'hello boy'}");来回传一个json串给java页面,即js通过以上的方法调用了java的方法。

如果webView展示页面的时候需要给网页传递cookie,即各种登录信息,则需要在调用webView.loadUrl(url)之前一句调用如下方法:注意!多个cookie值必须多次进行setCookie!

设置cookie方法一:

/**
 * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
 * 需要在当前用户退出登录的时候进行清除
 */
private void synCookies() {
    CookieSyncManager.createInstance(this);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.removeSessionCookie();

    String[] split = App.cookie.split(";");
    for (int i = 0; i < split.length; i++) {
        cookieManager.setCookie(url, split[i]);
    }
    CookieSyncManager.getInstance().sync();
}

在当前用户退出登录的时候清除掉保存的cookie信息。

/**
 * 清除当前用户存储到cookies表中的所有数据
 */
private void removeCookie() {
    CookieSyncManager.createInstance(this);
    CookieManager.getInstance().removeAllCookie();
    CookieSyncManager.getInstance().sync();
}

注意:在调用设置Cookie之后不能再设置如下的这类属性,否则设置cookie无效。

webView.getSettings().setBuiltInZoomControls(true);  
webView.getSettings().setJavaScriptEnabled(true);  

但是!在如上设置cookie之后,每次设置cookie的时候都有延时,要是点击频率较快的时候,会出现cookie没有被设置进去的问题!

设置方法二:
经过一番查找,找到如下两个解决办法:

  1. http://blog.csdn.net/swust_chenpeng/article/details/37699841,这个是开了一个异步任务来进行等待,然后再次设置cookie,感觉没有从根源上解决这个问题啊。。。但是思路值得参考
  2. http://stackoverflow.com/questions/22637409/android-how-to-pass-cookie-to-load-url-with-webview,按照第二个回答进行测试,可行。但是我用一个2.3.6的低版本手机去测的时候报错崩溃。java.lang.IllegalStateException: CookieSyncManager::createInstance() needs to be called before CookieSyncManager::getInstance(),按照提示加上如下代码之后正常。
/**
 * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
 * 需要在当前用户退出登录的时候进行清除
 */
private void synCookies() {
    String[] split = App.cookie.split(";");
    for (int i = 0; i < split.length; i++) {
        CookieSyncManager.createInstance(CommonWebActivity.this);
        CookieManager.getInstance().setCookie(url, split[i]);
        CookieSyncManager.getInstance().sync();
    }
}

同时,在退出登录的时候清除cookie。

CookieSyncManager.createInstance(this);
CookieManager.getInstance().removeAllCookie();
CookieSyncManager.getInstance().sync();

清除当前用户存储到cookies表中的所有数据。

附:从浏览器网页打开应用

这部分网上也有很多博客与笔记。此处仅作记录。
添加步骤如下:

  • AndroidMainfext.xml中你需要接收跳转信息的Activity标签中添加如下代码,则该Activity可以接收和处理网页中类似于zrp://zrp_test.net/的链接:
<intent-filter>
    <action android:name="android.intent.action.VIEW" />

    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />

    <data
        android:host="zrp_test.net"
        android:scheme="zrp" />
</intent-filter>
  • 在你接收跳转信息的activity中添加数据接收的代码:
private void initScheme(Intent intent) {
    Log.d(TAG, "initScheme: ---DataString--->" + intent.getDataString());
    Uri uri = intent.getData();
    if (uri != null && "zrp_test.net".equals(uri.getHost())) {
        Log.d(TAG, "initScheme: ---data--->" + uri.getQueryParameter("data"));
    }
}
  • 添加一个测试网页:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

<head>
    <meta content="text/html; charset=utf-8" http-equiv="Content-Type" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"/>
    <title>test</title>
</head>

<body>
<p>从浏览器打开应用的测试网址。</p>
<script LANGUAGE='javascript'>
var str = {"uid":123456,"nickname":"sex lady","sex":"female"};
window.location = "zrp://zrp_test.net?data="+JSON.stringify(str);
</script>
</body>

</html>

该网页文件我在云存储放了一份,大家可以通过访问这个网址来进行测试:http://sanchi.56ef923d5f32c.d01.nanoyun.com/blog_test/app_browser_test.html

  • 通过浏览器网页打开应用之后,可查看控制台的打印输出:
D/MainActivity: initScheme: ---DataString--->zrp://zrp_test.net?data={"uid":123456,"nickname":"sex lady","sex":"female"}
D/MainActivity: initScheme: ---host--->zrp_test.net
D/MainActivity: initScheme: ---data--->{"uid":123456,"nickname":"sex lady","sex":"female"}

一切OK。

需要源码的同学看这里


Sanchi
364 声望14 粉丝

01001001001001110110110100100000011101110110100101110100011010000010000001111001011011110111010100101110