65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 WebView 构建支持下载/上传功能的简单 Android Web 应用

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 3 月 19 日

CPOL

4分钟阅读

viewsIcon

17064

downloadIcon

437

使用 WebView 将网站转换为 Android Web 应用。

参考文献

本文介绍了一种构建支持上传和下载文件的 Android Web 应用的方法。

首先,请从官方网站下载 **Android Studio**

在撰写本文时,我使用的是最新版本:**Android Studio Electri Eel | 2022.1.1 Patch**

在 Android Studio 中,创建一个新的 **Phone** 项目,选择 **Empty Activity** 模板。

填写基本项目信息

阅读有关 [包名](https://developer.android.com.cn/studio/build/configure-app-module) 的信息。

根据您的应用程序需求,如果您希望您的应用能在大多数 Android 设备上运行,请选择 **API 19: Android 4.4 (KitKat)**(信息基于撰写本文时)。

语言:**Java**(本文将使用 Java。)

点击 **Finish** 开始创建应用。

请稍等片刻,让 Android Studio 加载并创建项目文件。准备就绪后,您应该会看到类似下图的内容

设置应用图标

在继续之前,我们可能需要先设置应用图标(*稍后也可以进行设置)。

关闭 Android Studio,然后访问此网站(应用图标生成器)

将您喜欢的图标上传到网站,它将生成各种尺寸的应用图标,用于构建您的 Android 项目。

*鸣谢:感谢 [icons8.com](https://icons8.com/) 赞助了本文使用的图标。

下载生成的图标

解压 zip 内容,并从 Android 的图标集文件夹复制

zip extracted content folder...\android\

到项目的图标资源文件夹

project_folder...\<project name>\app\src\main\res\

重新打开 Android Studio 和项目。

现在,项目中包含两组图标

  • **第一组**:从 easyappicon.com 下载的
  • **第二组**:Android Studio 添加的原始默认图标

这将导致项目中存在重复的图标,从而导致构建错误。因此,我们需要删除 Android Studio 添加的默认图标。

转到文件夹

app > res > mipmap > ic_launcher

并删除所有 *.webp 文件。

然后转到另一个文件夹

app > res > mipmap > ic_launcher_round

并删除所有 *.webp 文件

在 [ app > res > drawable ] 文件夹中,删除以下文件

  • ic_launcher_background.xml
  • ic_launcher_foreground.xml

并替换为您自己编辑的 PNG 图片

  • ic_launcher_background.png
  • ic_launcher_foreground.png

有关更改应用图标的更多信息,请参阅以下 Android 开发者文档

设置布局

接下来,编辑 `ActionBar`。打开以下主题文件

app > res > values > themes.xml
app > res > values > themes.xml (night)

更改这两个文件中的此行:(从 `DarkActionBar`)

<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">

到 (NoActionBar)

<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">

编辑后,主题文件的内容将类似如下:

<resources xmlns:tools="http://schemas.android.com/tools">
    <!-- Base application theme. -->
    <style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.NoActionBar">
        <!-- Primary brand color. -->
        <item name="colorPrimary">@color/purple_200</item>
        <item name="colorPrimaryVariant">@color/purple_700</item>
        <item name="colorOnPrimary">@color/black</item>
        <!-- Secondary brand color. -->
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorSecondaryVariant">@color/teal_200</item>
        <item name="colorOnSecondary">@color/black</item>
        <!-- Status bar color. -->
        <item name="android:statusBarColor" tools:targetApi="21">
         ?attr/colorPrimaryVariant</item>
        <!-- Customize your theme here. -->
    </style>
</resources>

接下来,编辑布局文件

app > res > layout > activity_main.xml

点击 [**Code**] 查看 XML 设计器代码。

这是初始代码

删除 `TextView`,并添加一个 `WebView`。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
 xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</androidx.constraintlayout.widget.ConstraintLayout>

将 `Layout` 从

androidx.constraintlayout.widget.ConstraintLayout

to

RelativeLayout

编辑后,代码将类似如下:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

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

</RelativeLayout>

设置应用权限

下一步是为应用启用访问互联网以及下载/上传文件的权限。

打开位于 [ app/src/main ] 的 Android Manifest 文件 (AndroidManifest.xml)

app > manifest > AndroidManifest.xml

插入三个 uses-permission 请求行

  • android.permission.INTERNET:访问互联网的权限
  • android.permission.READ_EXTERNAL_STORAGE:能够从 Android 存储中选择文件(用于上传文件)
  • android.permission.WRITE_EXTERNAL_STORAGE:用于保存下载的文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

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

    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/Theme.MyApp"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

添加资源目录和文件

右键单击文件夹 [app] > `New` > `Directory`

选择 src\main\assets

右键单击新创建的 assets 文件夹 > **New** > **File**

将文件命名为 no_internet.html

双击文件并输入一些 `html`,例如

<html>
<head></head>
<body>
<h1>No Internet</h1>
Please check the internet connection.
</body>
</html>

编码 WebView

阅读更多:Android 开发者关于 WebView 的文档

现在,我们进入 `WebView` 编码部分。打开位于 [ app/src/main/java/com/company/product/MainActivity.java ] 的 MainActivity.java 文件

app > java > ..package name... > MainActivity

// example:

app > java > com.company.product > MainActivity

这是初始代码

导入以下类库

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.ProgressDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.webkit.DownloadListener;
import android.webkit.ValueCallback;
import android.webkit.WebChromeClient;
import android.webkit.WebResourceError;
import android.webkit.WebResourceRequest;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

声明 `WebView`、`ProgressDialog` 对象和一些全局变量。

public class MainActivity extends AppCompatActivity {

    // if your website starts with www, exclude it
    private static final String myWebSite = "example.com";

    WebView webView;
    ProgressDialog progressDialog;

    // for handling file upload, set a static value, any number you like
    // this value will be used by WebChromeClient during file upload
    private static final int file_chooser_activity_code = 1;
    private static ValueCallback<Uri[]> mUploadMessageArr;

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

        // initialize the progressDialog
        progressDialog = new ProgressDialog(MainActivity.this);
        progressDialog.setCancelable(true);
        progressDialog.setMessage("Loading...");
        progressDialog.show();
    }
}

处理网页浏览活动

在 `MainActivity` 类中,创建一个 `WebViewClient` 来处理网页浏览活动

class myWebViewClient extends android.webkit.WebViewClient {

    @Override
    public void onPageStarted(WebView view, String url, Bitmap favicon) {
        super.onPageStarted(view, url, favicon);
        //showing the progress bar once the page has started loading
        progressDialog.show();
    }

    @Override
    public void onPageFinished(WebView view, String url) {
        super.onPageFinished(view, url);
        // hide the progress bar once the page has loaded
        progressDialog.dismiss();
    }

    @Override
    public void onReceivedError(WebView view,
           WebResourceRequest request, WebResourceError error) {
        super.onReceivedError(view, request, error);
        // show the error message = no internet access
        webView.loadUrl("file:///android_asset/no_internet.html");
        // hide the progress bar on error in loading
        progressDialog.dismiss();
        Toast.makeText(getApplicationContext(),"Internet issue",
                       Toast.LENGTH_SHORT).show();
    }
}

处理文件上传活动

文件上传将从 `html` 的 `input type` 为 `file` 的元素触发

<html>
<head></head>
<body>
    <form>
        <input type="file" />
    </form>
</body>
</html>

接下来,在 `MainActivity` 类中,创建一个 `WebChromeClient` 对象来处理文件上传任务。

// Calling WebChromeClient to select files from the device
public class myWebChromeClient extends WebChromeClient {
    @SuppressLint("NewApi")
    @Override
    public boolean onShowFileChooser(WebView webView,
    ValueCallback<Uri[]> valueCallback, FileChooserParams fileChooserParams) {

        Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);

        // set single file type, e.g. "image/*" for images
        intent.setType("*/*");

        // set multiple file types
        String[] mimeTypes = {"image/*", "application/pdf"};
        intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
        intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false);

        Intent chooserIntent = Intent.createChooser(intent, "Choose file");
        ((Activity) webView.getContext()).startActivityForResult
                    (chooserIntent, file_chooser_activity_code);

        // Save the callback for handling the selected file
        mUploadMessageArr = valueCallback;

        return true;
    }
}

// after the file chosen handled, variables are returned back to MainActivity
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);

    // check if the chrome activity is a file choosing session
    if (requestCode == file_chooser_activity_code) {
        if (resultCode == Activity.RESULT_OK && data != null) {
            Uri[] results = null;

            // Check if response is a multiple choice selection containing the results
            if (data.getClipData() != null) {
                int count = data.getClipData().getItemCount();
                results = new Uri[count];
                for (int i = 0; i < count; i++) {
                    results[i] = data.getClipData().getItemAt(i).getUri();
                }
            } else if (data.getData() != null) {
                // Response is a single choice selection
                results = new Uri[]{data.getData()};
            }

            mUploadMessageArr.onReceiveValue(results);
            mUploadMessageArr = null;
        } else {
            mUploadMessageArr.onReceiveValue(null);
            mUploadMessageArr = null;
            Toast.makeText(MainActivity.this, "Error getting file",
                           Toast.LENGTH_LONG).show();
        }
    }
}

处理文件下载活动

在 `MainActivity` 类中,创建一个下载事件监听器

DownloadListener downloadListener = new DownloadListener() {
    @Override
    public void onDownloadStart(String url, String userAgent,
    String contentDisposition, String mimetype, long contentLength) {

        progressDialog.dismiss();
        Intent i = new Intent(Intent.ACTION_VIEW);

        // example of URL = https://www.example.com/invoice.pdf
        i.setData(Uri.parse(url));
        startActivity(i);
    }
};

此监听器可能无法处理需要登录会话的下载。

初始化 WebView

回到 `onCreate()` 方法,继续初始化 `WebView`

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

    // initialize the progressDialog
    progressDialog = new ProgressDialog(MainActivity.this);
    progressDialog.setCancelable(true);
    progressDialog.setMessage("Loading...");
    progressDialog.show();

    // get the webview from the layout
    webView = findViewById(R.id.webView);

    // for handling Android Device [Back] key press
    webView.canGoBackOrForward(99);

    // handling web page browsing mechanism
    webView.setWebViewClient(new myWebViewClient());

    // handling file upload mechanism
    webView.setWebChromeClient(new myWebChromeClient());

    // some other settings
    WebSettings settings = webView.getSettings();
    settings.setJavaScriptEnabled(true);
    settings.setAllowFileAccess(true);
    settings.setAllowFileAccessFromFileURLs(true);
    settings.setUserAgentString(new WebView(this).getSettings().getUserAgentString());

    // set the downlaod listner
    webView.setDownloadListener(downloadListener);

    // load the website
    webView.loadUrl("https://" + myWebSite);
}

处理 Android 设备上的 [返回] 键按下

在 `MainActivity` 类中,添加一个函数来处理 [BackPressed]

@Override
public void onBackPressed() {
    if(webView.canGoBack()){
        webView.goBack();
    } else {
        finish();
    }
}

完整的页面代码将类似如下:[https://github.com/adriancs2/android.webview.upload.download/blob/main/src/app/src/main/java/com/company/product/MainActivity.java](https://github.com/adriancs2/android.webview.upload.download/blob/main/src/app/src/main/java/com/company/product/MainActivity.java)

最后,让我们测试一下这个应用。

要将真实的 Android 手机连接到 Android Studio,您可以启用“**开发者模式**”,然后在 Android 设备上打开“USB 调试”。通过 USB 数据线将 Android 连接到 PC,Android Studio 应该能够检测到它并在设备列表中显示。

要分发它,您可以将其构建为 APK。在菜单中,转到 [**Build**] > [**Generate Signed Bundle / APK...**] > 选择 [**APK**],然后按照屏幕上的说明进行操作。

要为上传到 Google Play 商店构建,您可以选择 [**Android App Bundle**],然后按照屏幕上的说明进行操作。

完整的源代码可在本文顶部找到。

谢谢,祝您编码愉快!

*鸣谢:功能/缩略图由 [Denny Müller at Unsplash.com](https://unsplash.com/photos/HfWA-Axq6Ek) 提供。

替代方案

历史

  • 2023 年 3 月 19 日 - 初始发布
© . All rights reserved.