使用Android WebView的Android Web应用程序

时间:2020-01-09 10:33:59  来源:igfitidea点击:

Android Web应用程序是使用Android WebView组件呈现Android应用程序GUI的一部分的应用程序。 WebView组件是作为View子类实现的成熟浏览器,因此我们可以将其嵌入到Android应用程序GUI中的任何位置。通常让" WebView"占据大部分屏幕空间,但是我们也可以让浏览器占据一半的屏幕或者适合我们应用的任何分区。

Android Web App或者Android Hybrid App?

调用在内部将" WebView"用于Android Web App的Android应用程序听起来可能会产生误导,因为该应用程序实际上是本机Android应用程序和Web应用程序的混合。该应用程序的某些部分使用本地Android组件,而该应用程序的某些部分则使用WebView(内部)中的网络技术(HTML,CSS,JavaScript,SVG)进行呈现。

Android Web应用程序的另一个常用术语是Android Hybrid App。术语" Android混合应用程序"表示该应用程序是不同应用程序类型之间的混合体。它通常用于将本机应用程序和Web应用程序混合在一起的应用程序。但是,术语" Android混合应用程序"本身并不能明确传达应用程序之间是什么混合体。它可以是本机应用程序,P2P应用程序和客户端/服务器应用程序之间的混合体。我们必须知道,该术语专门指本地应用程序和Web应用程序之间的混合体。因此,我将使用术语Android Web App,因为至少它说该应用程序使用Web技术。

WebView基于Chrome

从Android版本4.4(Kitkat)开始,WebView组件基于与Chrome for Android相同的代码。无论用户在本机Android Web应用程序(混合)中还是通过其Android Chrome浏览器看到它,都可以确保Web应用程序的呈现更加一致。

在Android 4.4之前," WebView"基于内部Android浏览器,但Chrome取代了旧的Android浏览器作为默认/内置浏览器。

WebView需要Internet许可

如果Android Web应用程序需要通过Internet加载网页,则该应用程序需要Android Internet权限。如果应用没有互联网许可,则根本无法创建任何互联网连接。用户安装应用程序时,系统会告知他/她该应用程序需要哪些权限。如果用户接受,则可以安装该应用程序。

通过将Internet许可元素添加到应用的列表文件中,应用可以获得Internet许可。她是具有互联网许可的Android列表文件的示例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          package="com.jenkov.androidwebappexamples" >

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

    <application ...>
    </application>

</manifest>

XML元素" <uses-permission android:name =" android.permission.INTERNET" />"(通过android:name属性)表明该应用程序需要互联网许可。

将WebView插入布局

为了使用AndroidWebView组件,我们必须将其插入应用程序GUI的某个位置。通常,这是通过在要显示" WebView"的布局的布局XML文件中插入" WebView"元素来完成的。下面是一个嵌入了" WebView"的布局文件示例:

<RelativeLayout 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"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/webview"
        android:layout_alignParentTop="true"
        android:layout_alignParentLeft="true"
        android:layout_width="match_parent"
        android:layout_height="match_parent"></WebView>

</RelativeLayout>

这个例子创建了一个带有WebView的RelativeLayout。布局在我有关Android布局的教程中有更详细的说明。

从代码访问WebView

WebView插入某个位置的布局后,即可从代码中访问它。我们需要访问WebView以使其执行任何有趣的操作。通常,我们可以从"活动"内部访问" WebView"。这是一个" Activity"子类的示例,该子类访问嵌入在其布局XML文件中的" WebView":

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = (WebView)
            findViewById(R.id.webview);
    }

}

" Activity"子类称为" MainActivity",而活动布局文件称为" activity_main.xml"。该布局文件类似于上一节中显示的有关在布局中插入" WebView"的示例布局文件。

注意上面代码中的方法调用findViewById(R.id.webview)。正是这个方法调用在布局文件中定位了" WebView"。

还要注意,本示例中没有配置通常配置AndroidActionBar的方法。如果我们希望应用具有ActionBar,请记住将这些方法添加到Activity子类中。

一旦获得对WebView的引用,就可以对其进行配置,并指示它通过HTTP和许多其他有趣的东西来加载URL。本教程的其余部分将深入介绍如何使用WebView进行操作。

将URL加载到WebView中

一旦引用了WebView实例,就可以指示它加载URL。从URL加载的资源(HTML,测试,图像等)将显示在" WebView"内部。这是一个如何在WebView内加载URL的示例:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    WebView webView = (WebView) findViewById(R.id.webview);

    webView.loadUrl("http://theitroad.local");
}

它是对WebView的loadUrl()方法的调用,该方法将URL加载到WebView中。

在WebView中启用JavaScript

默认情况下,Android" WebView"组件已禁用JavaScript。为了在加载的页面中启用执行JavaScript,我们必须获取WebView的WebSettings对象并对其调用setJavaScriptEnabled(true)。这是一个如何在Android的WebView中启用JavaScript的示例:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    WebView webView = (WebView) findViewById(R.id.webview);

    WebSettings webSettings = webView.getSettings();
    webSettings.setJavaScriptEnabled(true);

    webView.loadUrl("http://theitroad.local");
}

从JavaScript调用到Android Web App

在Android" WebView"中运行的JavaScript可能会在Android Web应用程序中调用Java代码。要从JavaScript调用Java,我们需要创建一个JavaScript接口对象,该对象可用于运行在WebView中的JavaScript。

首先,让我们看看这样的JavaScript接口对象是什么样的。这是一个示例JavaScript接口类:

public class AppJavaScriptProxy {

    private Activity activity = null;

    public AppJavaScriptProxy(Activity activity) {
        this.activity = activity;
    }

    @JavascriptInterface
    public void showMessage(String message) {

        Toast toast = Toast.makeText(this.activity.getApplicationContext(),
                message,
                Toast.LENGTH_SHORT);

        toast.show();
    }

}

如我们所见,此JavaScript接口类(我称为Proxy而不是Interface)的showMessage()显示了一个Toast,其中包含在message参数中传递给该方法的消息。

为了使AppJavaScriptProxy类的对象可用于在WebView内运行的JavaScript,必须在WebView实例上调用addJavaScriptInterface()方法。这是一个WebViewaddJavaScriptInterface()示例:

webView.addJavascriptInterface(new AppJavaScriptProxy(this), "androidAppProxy");

传递给addJavaScriptInterface()的第一个参数是JavaScript接口对象本身。第二个参数是绑定JavaScript接口对象的全局JavaScript变量的名称。这是JavaScript如何访问上述JavaScript接口对象的示例:

if(typeof androidAppProxy !== "undefined"){
    androidAppProxy.showMessage("Message from JavaScript");
} else {
    alert("Running outside Android app");
}

请注意,JavaScript如何首先检查是否定义了androidAppProxy全局变量。如果是,则JavaScript正在Android Web应用程序中运行。如果未定义全局变量,则JavaScript不会在Android Web应用程序内运行,并且它将不得不使用另一种机制来显示其消息。

禁用JavaScript接口对象以提高安全性

在" WebView"实例上注册JavaScript接口对象时,JavaScript接口对象可用于加载到" WebView"中的所有页面。这意味着,如果用户导航到我们自己的网站/ Web应用程序之外的页面,并且此页面也显示在同一" WebView"内部,则该外部页面也可以访问JavaScript接口对象。这是潜在的安全风险。

我们可以检查" WebView"的URL,以查看给定的JavaScript接口对象方法是否应可调用。但是,要获取WebView的URL,必须调用其getUrl()方法。但是此方法只能由Android应用程序的UI线程调用,并且在JavaScript接口对象中调用该方法的线程不是UI线程。因此,我们将必须实施URL检查,如下所示:

public class AppJavaScriptProxy {

    private Activity activity = null;
    private WebView  webView  = null;

    public AppJavaScriptProxy(Activity activity, WebView webview) {

        this.activity = activity;
        this.webView  = webview;
    }

    @JavascriptInterface
    public void showMessage(final String message) {

        final Activity theActivity = this.activity;
        final WebView theWebView = this.webView;

        this.activity.runOnUiThread(new Runnable() {

            @Override
            public void run() {
                if(!theWebView.getUrl().startsWith("http://theitroad.local")){
                    return ;
                }

                Toast toast = Toast.makeText(
                        theActivity.getApplicationContext(),
                        message,
                        Toast.LENGTH_SHORT);

                toast.show();
            }
        });
    }
}

首先,请注意,AppJavaScriptProxy构造函数现在同时使用了" Activity"和" WebView"实例。其次,请注意showMessage()现在如何调用Activity方法runOnUiThread(),并通过Runnable执行。在该"可运行的"内部,我们可以安全地访问" WebView"。

在" Runnable"内部,我们首先检查" WebView"中加载的URL是否在我们自己的网站内(在本例中为" http://theitroad.local"),如果没有,则" showMessage()"方法会立即返回做任何事情。

从Android Web App调用到JavaScript

也可以从Android Web应用程序在" WebView"内部调用JavaScript函数。我们有两种方法可以这样做。两者都将在下面介绍。

通过WebView loadUrl()调用JavaScript

在API级别19之前(在Android 4.4 Kitkat之前),我们可以使用如下的WebView` loadUrl()方法:

webView.loadUrl("javascript:theFunction('text')");

这与单击WebView当前加载页面内的JavaScript链接具有相同的效果。它不会导致加载新页面。而是导致JavaScript在当前加载的页面内执行。

此方法的缺点是我们无法从被调用的函数中获取任何返回值。但是,我们可以安排被调用的JavaScript函数以结果返回到Java中(如何从JavaScript调用Java在本教程的前面进行了说明)。

通过WebView调用JavaScript EvaluationJavascript()

第二个选项仅在Android API级别19(Android Kitkat)及更高版本中可用,Android的WebView类包含一个名为evaluateJavascript()的方法。这个方法可以像执行在当前加载到WebView中的页面中一样执行JavaScript。这是一个通过WebView``evaluateJavascript()执行JavaScript的例子:

webView.evaluateJavascript("fromAndroid()", new ValueCallback<String>() {
    @Override
    public void onReceiveValue(String value) {
        //store / process result received from executing Javascript.
    }
});

传递给evaluateJavascript()的第一个参数是要评估(执行)的JavaScript字符串。第二个参数是一个回调对象,其中包含一个名为" onReceiveValue"的方法。对JavaScript进行评估并从中获得结果后,将调用此回调对象的onReceiveValue()方法。然后,Android Web应用程序可以处理从执行JavaScript返回的值。

使用WebViewClient在WebView中保持页面导航

用户单击加载到" WebView"中的网页中的链接,默认行为是将链接的URL加载到系统Android浏览器中。这意味着将打开Android浏览器应用程序,并且链接页面将显示在Android浏览器中,而不是我们应用程序中的" WebView"内部。这破坏了应用程序用户的用户体验。

为了使页面导航保持在WebView以及应用程序内,我们需要创建WebViewClient的子类,并覆盖其shouldOverrideUrlLoading(WebView webView,String url)方法。这是这样的WebViewClient子类的外观:

private class MyWebViewClient extends WebViewClient {
    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        return false;
    }
}

当shouldOverrideUrlLoading()方法返回false时,作为参数传递给该方法的URL将被加载到WebView中,而不是Android标准浏览器中。在上面的示例中,所有URls都将加载到" WebView"内部。

如果我们想区分URL是在WebView内部加载还是在Android浏览器中加载,则shouldOverrideUrlLoading()的实现可以检查作为参数传递给它的URL。这是一个示例,该示例仅加载WebView内包含jenkov.com的URL和Android浏览器中的所有其他URL:

public class WebViewClientImpl extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        if(url.indexOf("jenkov.com") > -1 ) return false;
        return true;
    }

}

奇怪的是,从shouldOverrideUrlLoading()返回true不会导致将URL加载到外部Android浏览器中。而是,它导致URL根本不被加载。要在外部Android浏览器中打开所有其他URL,我们将必须触发一个"意图"。这是WebViewClient子类添加后的样子:

public class WebViewClientImpl extends WebViewClient {

    private Activity activity = null;

    public WebViewClientImpl(Activity activity) {
        this.activity = activity;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView webView, String url) {
        if(url.indexOf("jenkov.com") > -1 ) return false;

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        activity.startActivity(intent);
        return true;
    }

}

注意," WebViewClientImpl"类现在如何在其构造函数中采用"活动"。此活动用于触发"意图",以在Android浏览器中打开URL。

在WebView上设置WebViewClient

在WebViewClient子类生效之前,我们必须在WebView上设置其实例。看起来是这样的:

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        WebView webView = (WebView) findViewById(R.id.webview);

        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        WebViewClientImpl webViewClient = new WebViewClientImpl(this);
        webView.setWebViewClient(webViewClient);

        webView.loadUrl("http://theitroad.local");
    }

}

使用"后退"按钮浏览WebView历史记录

如果我们在运行到目前为止开发的应用程序时单击Android设备的"后退"按钮,则默认反应是该应用程序"返回"至Android操作系统/主屏幕(或者我们之前所做的任何其他操作)启动了网络应用)。即使我们已经浏览了几页进入" WebView"内部加载的网站或者Web应用程序,"后退"按钮仍会将用户带出该应用程序。

与其直接退出该应用程序,不如单击"后退"按钮时该应用程序返回Web视图的浏览历史。因此,"后退"按钮将像浏览器中的"后退"按钮一样起作用。仅当" WebView"返回到加载的第一页并且用户再次单击"返回"按钮时,我们才想退出该应用程序。

为了实现"后退"按钮的这种效果,必须稍微修改前面显示的MainActivity类。我们必须重写Activity类中的onKeyDown()方法。修改后的MainActivity类的外观如下所示:

public class MainActivity extends Activity {

    private WebView webView = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        this.webView = (WebView) findViewById(R.id.webview);

        WebSettings webSettings = webView.getSettings();
        webSettings.setJavaScriptEnabled(true);

        WebViewClientImpl webViewClient = new WebViewClientImpl(this);
        webView.setWebViewClient(webViewClient);

        webView.loadUrl("http://theitroad.local");
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && this.webView.canGoBack()) {
            this.webView.goBack();
            return true;
        }

        return super.onKeyDown(keyCode, event);
    }

}

首先,现在将WebView实例分配给成员变量,因此onKeyDown()方法可以访问它。

其次,onKeyDown()方法已被实现为第一个检查WebView是否可以返回的实现的方法所覆盖。如果用户离开了WebView内加载的第一页,则WebView可以返回。 WebView包含的浏览历史记录与普通浏览器相同。如果" WebView"可以返回(具有浏览历史记录),则指示" WebView"返回。否则,将调用超类中的onKeyDown()实现,这将导致退出应用程序的"后退"按钮的默认行为。

注意,onKeyDown()方法检查按下了什么键。只有按下"后退"按钮,它才会尝试操纵WebView的浏览历史记录。所有其他按钮的按下都由超类onKeyDown()实现来处理。

拦截WebView HTTP请求

加载页面或者页面内使用的资源(图像,JavaScript文件,CSS文件等)时,可以拦截Android" WebView"发出的HTTP请求。拦截HTTP请求时,我们可以决定" WebView"是否应正常加载资源,或者是否要返回该资源的另一个版本,然后在" WebView"内部使用该版本。

要拦截WebView发出的HTTP请求,我们需要在WebViewClient子类中重写shouldInterceptRequest()方法。这是一个shouldInterceptRequest()示例实现:

public class WebViewClientImpl extends WebViewClient {

    private Activity activity = null;

    public WebViewClientImpl(Activity activity) {
        this.activity = activity;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if(url.indexOf("jenkov.com") > -1 ) return false;

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        activity.startActivity(intent);
        return true;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
        if(url.startsWith("http://theitroad.local/images/logo.png")){
            String mimeType = "image/png";
            String encoding = "";
            InputStream input = ...;

            WebResourceResponse response =
                    new WebResourceResponse(mimeType, encoding, input);

            return response;
        }

        return null;
    }
}

注意此代码示例底部的shouldInterceptRequest()方法的实现。这个shouldInterceptRequest()实现查看URL以确定URL是否指向徽标PNG图像。如果是这样,它将创建一个WebResourceResponse实例并返回它。

WebResourceResponse构造函数需要一个InputStream,它可以从中加载与URL匹配的资源。在上面的示例中,InputStream变量未初始化。该示例仅显示...,而不显示如何初始化InputStream。稍后我们将看到如何从嵌入在Android网络应用的APK文件中的assets目录中加载资源。

如果" shouldInterceptRequest()"方法返回null,则" WebView"将正常(通过互联网)加载资源。

这个例子建立在本教程前面显示的WebViewClient子类的基础上。因此,它也包含" shoulldOverrideUrlLoading()"方法,尽管该方法对于拦截" WebView" HTTP请求不是必需的。

从App APK资产加载资源

如果我们想截取HTTP请求并从Web应用程序" assets"目录中加载给定资源,则可以这样做。出于以下原因,从assets目录加载资源比通过网络加载资源更好:

  • 从" assets"目录加载文件比通过网络加载文件更快。
  • 当我们通过无线互联网加载较少的数据(WIFI /移动数据)时,Android设备将消耗较少的电池电量。
  • 当我们通过Internet加载较少的数据时,应用将使用较少的用户Internet带宽配额。

当然,这对于非常静态的数据是有意义的,例如徽标图像文件,JavaScript文件,CSS文件等,它们不会经常更改。

Android应用程序的assets目录位于Android Studio项目内的src / main / assets中。如果项目不包含assets目录,那么我们必须自己创建一个。这只是一个名为assets的常规目录。没魔术

该" assets"目录中的所有文件和文件夹都将打包并嵌入到该应用的APK文件中。因此,当我们将Web应用程序安装在用户的Android设备上时," assets"目录中的所有静态资产也都位于。

要访问" assets"目录中的文件,我们必须获得" AssetManager"的实例。我们可以通过调用Activity``getAssets()方法来实现。这是一个WebViewClient子类实现(基于较早的实现),向我们展示如何拦截HTTP请求并通过AssetManager读取资源:

public class WebViewClientImpl extends WebViewClient {

    private Activity activity = null;

    public WebViewClientImpl(Activity activity) {
        this.activity = activity;
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if(url.indexOf("jenkov.com") > -1 ) return false;

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        activity.startActivity(intent);
        return true;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

        if(url.startsWith("http://theitroad.local/images/logo.png")){
            return loadFromAssets(url, "images/logo.png", "image/png", "");
        }

        return null;
    }

    private WebResourceResponse loadFromAssets( String url,
        String assetPath, String mimeType, String encoding){

        AssetManager assetManager = this.activity.getAssets();
        InputStream input = null;
        try {
            Log.d(Constants.LOG_TAG, "Loading from assets: " + assetPath);

            input = assetManager.open("/images/logo.png");
            WebResourceResponse response =
                    new WebResourceResponse(mimeType, encoding, input);

            return response;
        } catch (IOException e) {
            Log.e("WEB-APP", "Error loading " + assetPath + " from assets: " +
                e.getMessage(), e);
        }
        return null;
    }
}

请注意,shouldInterceptRequest()如何检查URL是否为徽标URL,如果是,则从静态资产而不是通过网络加载徽标。 loadFromAssets()方法通过调用其构造函数中传递给WebViewClientIpml类的Activity实例的getAssets()方法来获取AssetManager实例。一旦获取了" AssetManager",就可以获取所需资源的" InputStream",并将其包含在返回的" WebResourceResponse"中。

该示例的结果是,从徽标的" assets"目录中加载了" logo.png"文件,而不是通过网络加载了该文件。这样可以使徽标加载速度更快,并使应用程序使用起来更舒适。

在Android设备中缓存Web资源

如我们所见,可以拦截WebView发出的HTTP请求。也可以自己通过网络加载资源。因此,可以在Android设备中本地缓存" WebView"使用的资源,然后从缓存而不是通过网络加载它们。效果类似于在APK文件中嵌入资源,不同之处在于我们可以不时地用新版本替换缓存的文件。

我们可以使用标准JavaURL类通过HTTP下载资源。此类在Android中效果很好。稍后我们将看到一个示例。

我们可以将资源存储在Android设备上的内部存储或者外部存储中。内部和外部存储都可以通过标准JavaFile类像文件系统一样进行访问。稍后我们还将看到一个示例。后面的示例使用内部应用程序存储来存储缓存的文件。

为了向我们展示如何下载和缓存" WebView"使用的文件,我实现了一个简单的" UrlCache"类。我们可以将该类用作我们自己的URL缓存类的基础。 " UrlCache"类用于" WebViewClientImpl"(" WebViewClient"子类)内部。首先,这是内置了本地资源缓存的WebViewClientImpl的外观(并且没有加载上一节中的资产):

public class WebViewClientImpl extends WebViewClient {

    private Activity activity = null;
    private UrlCache urlCache = null;

    public WebViewClientImpl(Activity activity) {
        this.activity = activity;
        this.urlCache = new UrlCache(activity);

        this.urlCache.register("http://theitroad.local/", "tutorials-jenkov-com.html",
                "text/html", "UTF-8", 5 * UrlCache.ONE_MINUTE);
        
    }

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        if(url.indexOf("jenkov.com") > -1 ) return false;

        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        activity.startActivity(intent);
        return true;
    }

    @Override
    public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

        return this.urlCache.load(url);
    }
}

这个例子在WebViewClientImpl的构造函数中创建一个新的UrlCache实例。将"活动"传递给" UrlCache"构造函数,因为" UrlCache"需要"活动"来访问内部存储。

创建UrlCache实例后,构造函数将注册一个资源,该资源应在本地缓存。资源已通过其URL,缓存文件名,mime类型,编码和最长期限进行了注册。当资源在UrlCache中注册后,当我们调用UrlCacheload()方法时,将下载并缓存该资源。注册资源不会下载它。只有load()可以。

现在," WebVieClientImpl"的" shouldInterceptRequest()"方法非常简单。它所做的就是返回UrlCache.load()返回的值。 load()方法将返回一个WebResourceResponse对象,该对象将下载并缓存资源;如果资源对于UrlCache未知,则返回null;如果URL还没有通过register()注册进行缓存,则返回null。 `)。因此,如果资源不是由" UrlCache"返回的,则" WebView"将自己下载(因为" UrlCache.load()"返回" null",因此" shouldInterceptRequest()"返回" null")。

这是UrlCache类的代码(不包含import语句):

public class UrlCache {

  public static final long ONE_SECOND = 1000L;
  public static final long ONE_MINUTE = 60L * ONE_SECOND;
  public static final long ONE_HOUR   = 60L * ONE_MINUTE;
  public static final long ONE_DAY    = 24 * ONE_HOUR;

  private static class CacheEntry {
    public String url;
    public String fileName;
    public String mimeType;
    public String encoding;
    public long   maxAgeMillis;

    private CacheEntry(String url, String fileName,
        String mimeType, String encoding, long maxAgeMillis) {

        this.url = url;
        this.fileName = fileName;
        this.mimeType = mimeType;
        this.encoding = encoding;
        this.maxAgeMillis = maxAgeMillis;
    }
  }

  protected Map<String, CacheEntry> cacheEntries = new HashMap<String, CacheEntry>();
  protected Activity activity = null;
  protected File rootDir = null;

  public UrlCache(Activity activity) {
    this.activity = activity;
    this.rootDir  = this.activity.getFilesDir();
  }

  public UrlCache(Activity activity, File rootDir) {
    this.activity = activity;
    this.rootDir  = rootDir;
  }

  public void register(String url, String cacheFileName,
                       String mimeType, String encoding,
                       long maxAgeMillis) {

    CacheEntry entry = new CacheEntry(url, cacheFileName, mimeType, encoding, maxAgeMillis);

    this.cacheEntries.put(url, entry);
  }

  public WebResourceResponse load(String url){
    CacheEntry cacheEntry = this.cacheEntries.get(url);

    if(cacheEntry == null) return null;

    File cachedFile = new File(this.rootDir.getPath() + File.separator + cacheEntry.fileName);

    if(cachedFile.exists()){
      long cacheEntryAge = System.currentTimeMillis() - cachedFile.lastModified();
      if(cacheEntryAge > cacheEntry.maxAgeMillis){
        cachedFile.delete();

        //cached file deleted, call load() again.
        Log.d(Constants.LOG_TAG, "Deleting from cache: " + url);
        return load(url);
      }

      //cached file exists and is not too old. Return file.
      Log.d(Constants.LOG_TAG, "Loading from cache: " + url);
      try {
        return new WebResourceResponse(
                cacheEntry.mimeType, cacheEntry.encoding, new FileInputStream(cachedFile));
      } catch (FileNotFoundException e) {
        Log.d(Constants.LOG_TAG, "Error loading cached file: " + cachedFile.getPath() + " : "
                + e.getMessage(), e);
      }

    } else {
      try{
        downloadAndStore(url, cacheEntry, cachedFile);

        //now the file exists in the cache, so we can just call this method again to read it.
        return load(url);
      } catch(Exception e){
        Log.d(Constants.LOG_TAG, "Error reading file over network: " + cachedFile.getPath(), e);
      }
    }

    return null;
  }

  private void downloadAndStore(String url, CacheEntry cacheEntry, File cachedFile)
    throws IOException {

    URL urlObj = new URL(url);
    URLConnection urlConnection = urlObj.openConnection();
    InputStream urlInput = urlConnection.getInputStream();

    FileOutputStream fileOutputStream =
            this.activity.openFileOutput(cacheEntry.fileName, Context.MODE_PRIVATE);

    int data = urlInput.read();
    while( data != -1 ){
      fileOutputStream.write(data);

      data = urlInput.read();
    }

    urlInput.close();
    fileOutputStream.close();
    Log.d(Constants.LOG_TAG, "Cache file: " + cacheEntry.fileName + " stored. ");
  }
}

在检查内部存储器之前,此类" UrlCache"类不会检查APK文件的" assets"目录中是否包含嵌入式资源。我会把它作为练习留给我们补充,以备我们需要时添加。鉴于本教程分别包含两个选项的代码示例,因此添加起来应该不太困难。我们只需要将一个的代码合并到另一个中即可。

预取Web资源

有时,我们可能想预取我们知道用户可能会在不久的将来加载的Web资源。例如,假设用户启动了Android Web应用程序,并且该应用程序中显示的第一页包含指向其他页面的链接。为了使从第一页链接到的页面加载速度更快,我们可能需要在后台预取这些页面。

有几种方法可以实现Web资源预取。使用哪种方法取决于我们是否提前知道要预取哪些Web资源。

预取已知的Web资源

如果我们已经知道要预取哪些Web资源,则应用程序在启动时就可以这样做。如果我们使用的缓存类似于本教程前面所述,则只需注册要预取的资源的URL,然后调用UrlCache``load()方法。请参阅稍后的"何时开始预取"部分,以了解有关在应用中何时何地开始预取的确切信息。

预取未知的Web资源

如果我们不提前知道要预取的Web资源,则无法将要预取的资源硬编码到Android Web应用程序中。例如,假设加载的第一个网页显示了文章链接的列表,并且文章的URL定期更改,就像在新闻网站的首页上一样。Android应用在开发时无法知道要预读哪些文章。

在这种情况下,实际的网页必须控制Web资源的预取。我们可以通过两种方式进行操作。第一种方法是让网页从JavaScript调用到Android Web应用程序,如前所述。该网页将传递要预取的资源列表。

第二种方法是网页实际上以某种方式在后台(例如,在隐藏的" div"元素内)加载了这些资源。 Android Web应用程序拦截这些资源的加载并将其存储在缓存中。用户导航到这些资源后,将直接从缓存中加载它们,从而快速加载它们。为此,Android Web应用程序需要能够从Web资源的URL中查看是否要对其进行缓存。例如,所有包含/ article /或者以.html结尾并且与首页位于同一域内的URL(例如,在theitroad.local内部)。

何时开始预取

在第一页完全加载之前,不应该开始页面的预取。否则,预取流量可能会减慢首页所需资源的加载。

如果要预取已知页面,则可以通过重写WebViewClient子类的onPageFinished()方法来实现。这是一个非常简单的示例,说明如何覆盖onPageFinished()

@Override
public void onPageFinished(WebView view, String url) {
    super.onPageFinished(view, url);

    if("http://theitroad.local/".equals(url)){
        this.urlCache.load("http://theitroad.local/java/index.html");
    }
}

本示例使用本文前面显示的" UrlCache"。请注意,如果刚完成的页面是我的教程网站的首页,则onPageFinished()方法将如何加载另一个页面。当然,URLhttp:// theitroad.local / java / index.html必须进行缓存才能具有任何缓存效果(我的UrlCache类要求注册要缓存的资源)第一的)。

如果我们要预取未知页面,则可以执行与上述类似的预取,但是要在shouldInterceptRequest()方法内部进行。这是一个看起来如何的示例:

@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {

    if(url.startsWith("http://mydomain.com/article/") {
        String cacheFileName = url.substring(url.lastIndexOf("/"), url.length());
        this.urlCache.register(url, cacheFileName,
                "text/html", "UTF-8", 60 * UrlCache.ONE_MINUTE);

    }

    return this.urlCache.load(url);
}

仅当我们可以在URL本身上看到是否应预取给定资源时,以上预取机制才有效。在上面的示例中,该网址以http://mydomain.com/article/开头的网页(例如,在隐藏的div中)加载的所有URL将被缓存,以便将来直接读取对这些URL的请求从缓存中。

如果我们无法从URL中看到是否应该缓存资源,则可以通过使网页调用Android或者让Android应用执行返回以下内容的JavaScript函数,使网页告诉Android应用要缓存的资源列表。我目前没有工作代码向我们展示如何执行此操作,但是我会在完成时更新本教程。

过滤HTML

有时,我们可能需要过滤从Web服务器加载的HTML,然后再将其显示在Android" WebView"中。我们可能要重用现有的HTML页面,但要删除例如徽标图片或者文本在Android网络应用中占据了太多空间,如果我们已经显示了例如应用程序操作栏中的徽标。

要过滤从Web服务器加载的HTML,我们必须拦截WebView发出的HTTP请求。在本教程的前面,我已经描述了如何做到这一点。截获要过滤的HTTP请求时,我们可以自己下载HTML文件,对其进行修改,然后将其包装在InputStream实现中,该实现可以在WebResourceResponse对象内返回。我们可以使用" ByteArrayInputStream",因为很容易将" String"或者本地文件转换为字节数组。

如果过滤后的资源不经常更改,则可以在本地缓存过滤后的版本,以加快将来对该资源的请求。我们可以在前面显示的UrlCache中构建过滤功能。当注册用于缓存的URL时,我们还可以添加添加" WebResourceFilter"(或者其他任何称为过滤器接口)的可能性。

在HTML 5本地存储中缓存值

HTML 5本地存储使Web应用程序可以在浏览器中本地存储值。这些值可以存储在sessionStorage或者localStorage全局JavaScript对象中。我们可以在《 HTML 5本地存储教程》中阅读有关HTML 5本地存储工作方式的详细信息。

存储在" sessionStorage"中的值仅在打开浏览器窗口(" WebView")的情况下保留。当浏览器窗口关闭时(当应用程序销毁" WebView"或者用户关闭Android应用程序时),所有" sessionStorage"值都将被删除。

存储在" localStorage"中的值会在应用重新启动时保留。如果我们打算在应用程序重新启动时存储值,建议我们使用localStorage。请记住,如果Android OS需要空间,则可能会删除localStorage变量。

要启用HTML 5本地存储,我们必须在" WebView"的" WebSettings"对象上调用" setDomStorageEnabled(true);"。这是一个WebSettings.setDomStorageEnabled()示例:

WebSettings webSettings = webView.getSettings();

webSettings.setJavaScriptEnabled(true);
webSettings.setDomStorageEnabled(true);

该代码通常位于托管WebView的Activity子类的onCreate()方法内部。

设备方向变更处理

当用户将其Android设备的方向从纵向更改为横向时,反之亦然,默认行为是Android销毁可见活动,并以新的设备方向重新创建它。不幸的是,这意味着被破坏活动中的所有"视图"实例也被破坏,包括" WebView"。当" WebView"被销毁时,其内部浏览历史也将被销毁。因此,当我们更改设备方向时,浏览历史记录将被破坏。

此外,如果重新创建" WebView",则必须跟踪被破坏的" WebView"中正在显示的页面,因此我们可以将该页面加载到" WebView"中,而不是加载网站的第一页/网络应用。

我们可以在重新创建的活动实例中重用相同的" WebView"实例,而不必在更改设备方向时销毁并重新创建" WebView"实例。我将在以下各节中说明如何执行此操作。

首先,我们为Android Web应用程序主活动创建两个布局文件,而不是一个。第一个布局文件包含一个带有WebView的" RelativeLayout",第二个布局文件仅包含一个没有子级的" RelativeLayout"元素(没有" WebView")。

这是第一个布局文件(" activity_main.xml")的外观:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/firstViewGroup"
                android:layout_width="match_parent"
                android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <WebView
            android:id="@+id/webview"
            android:layout_alignParentTop="true"
            android:layout_alignParentLeft="true"
            android:layout_width="match_parent"
            android:layout_height="match_parent"></WebView>

</RelativeLayout>

这是第二个布局文件(" activity_main_no_webview")的外观:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
                xmlns:tools="http://schemas.android.com/tools"
                android:id="@+id/secondViewGroup"
                android:layout_width="match_parent"
                android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
                android:paddingRight="@dimen/activity_horizontal_margin"
                android:paddingTop="@dimen/activity_vertical_margin"
                android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

</RelativeLayout>

接下来,我们稍微修改一下MainActivity。对WebView的引用是静态的,因此它将独立于MainActivity的创建和销毁实例。其次,我们向包含WebView的ViewGroup添加静态引用。这样,只要设备方向发生变化,我们就可以从以前的" ViewGroup"中删除" WebView",并将" WebView"添加到新的" ViewGroup"中。这是MainActivity类的外观,其中实现了设备方向更改处理:

public class MainActivity extends Activity {

    private static ViewGroup webViewParentViewGroup = null;
    private static WebView   webView                = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        if(webView != null){
            webViewParentViewGroup.removeView(webView);

            setContentView(R.layout.activity_main_no_webview);

            webViewParentViewGroup = (ViewGroup) findViewById(R.id.secondViewGroup);
            webViewParentViewGroup.addView(this.webView);
        } else {
            setContentView(R.layout.activity_main);

            webViewParentViewGroup = (ViewGroup) findViewById(R.id.firstViewGroup);
            webView                = (WebView) findViewById(R.id.webview);

            //configure WebView - left out here for brevity

            webView.loadUrl("http://theitroad.local");
        }
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) && this.webView.canGoBack()) {
            this.webView.goBack();
            return true;
        }

        return super.onKeyDown(keyCode, event);
    }

}

设备方向更改处理全部发生在onCreate()方法内部。如果静态的" webView"变量为" null",则使用第一个内部带有" WebView"的布局文件。从膨胀的布局中提取" WebView",并配置" WebView"。在此示例中,我省略了" WebView"配置代码,以简化示例。

如果静态webView变量不为null,则说明WebView实例已经存在,并且Android Web应用程序必须重用它。它将首先从新破坏的活动布局的父" ViewGroup"中删除" WebView"实例,并将" WebView"添加到新创建的活动的根" ViewGroup"中。新创建的活动使用第二个布局文件来增加其布局。第二个布局在布局文件中没有" WebView"。只是根ViewGroup元素,我们可以其中插入现有的WebView。

使用loadData()将HTML直接加载到WebView中

可以将HTML直接加载到" WebView"中,而无需从URL加载。我们可以使用WebView的loadData()方法来实现。这是一个WebView` loadData()示例:

String data = "<html><body><h1>HTML Loaded Directly</h1></body></html>";

webView.loadData(data, "text/html", "UTF-16");

loadData()方法还可以用于加载HTML以外的其他类型的数据,例如文本文件,JavaScript等。但是HTML文件是一种非常常见的用例。

使用基本URL将HTML加载到WebView中

如果我们直接加载到Android Web应用程序中" WebView"中的HTML包含具有相对URL的链接,则这些链接可能无法正常工作。当我们直接将HTML加载到" WebView"中时,HTML没有可用来解释相对URL的基本URL。 AndroidWebView组件为此提供了一个解决方案。

我们可以使用基本URL将HTML直接加载到" WebView"中。然后,基本URL用于解析HTML中的所有相对URL。要使用基本URL加载HTML,我们必须使用loadDataWithBaseURL()方法。这是一个WebView的loadDataWithBaseURL()示例:

String baseUrl    = "http://theitroad.local";
String data       = "Relative Link";
String mimeType   = "text/html";
String encoding   = "UTF-8";
String historyUrl = "http://theitroad.local/jquery/index.html";

webView.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);

loadDataWithBaseURL()方法有5个参数。 data参数是要加载到WebView中的HTML。 " mimeType"是加载到" WebView"中的数据的mime类型(在此示例中为" text / html")。 " encoding"是数据的二进制编码(在本例中为" UTF-8")。注意:我尝试使用" UTF-16"作为编码,但是" WebView"中显示的内容看起来很奇怪(例如亚洲字符)。

baseUrl参数是基本URL,从中可以解释所加载HTML中的所有相对URL。

" historyUrl"参数是要写入" WebView"内部导航历史记录中的URL,用于加载到" WebView"中的HTML。如果用户从加载的HTML导航到另一个页面,然后单击"后退"按钮,则WebView将导航回该URL。我们可能必须拦截此URL的加载,因为向后浏览WebView的历史记录不会使我们进入已加载的HTML,而是会转到" historyUrl"参数中指定的URL(如果为historyUrl设置为" null")。

响应式网页设计

响应式网页设计是指可以使其自身(响应)适应显示HTML页面的设备的网页设计。如果我们尝试将网站包装在Android Web应用程序中,并且该网站也显示在其他设备(台式浏览器,平板电脑甚至电视)上,则可以使Web设计具有响应性。

进行响应式网页设计本身就是一个完整的主题。我已经在有关响应式Web设计的教程中解释了基础知识。

进行响应式Web设计的核心技术之一是CSS Media Queries。 CSS媒体查询使我们可以根据显示网站的设备的屏幕宽度,设备方向,像素密度和其他屏幕特定属性来应用不同的CSS样式。