集成资源交付 (Kotlin 和 Java)

使用本指南中的步骤从 Java 代码访问应用的资源包。

构建 Kotlin 和 Java 版本

按照以下步骤将 Play 资源交付集成到项目的 Android 应用包中。您无需使用 Android Studio 执行这些步骤。

  1. 将项目 build.gradle 文件中的 Android Gradle 插件版本更新到 4.0.0 或更高版本。

  2. 在项目的顶级目录中,为资源包创建一个目录。此目录名称用作资源包名称。资源包名称必须以字母开头,并且只能包含字母、数字和下划线。

  3. 在资源包目录中,创建一个 build.gradle 文件并添加以下代码。确保指定资源包的名称,并且只指定一种交付类型

    Groovy

    // In the asset pack's build.gradle file:
    plugins {
      id 'com.android.asset-pack'
    }
    
    assetPack {
        packName = "asset-pack-name" // Directory name for the asset pack
        dynamicDelivery {
            deliveryType = "[ install-time | fast-follow | on-demand ]"
        }
    }
    

    Kotlin

    // In the asset pack's build.gradle.kts file:
    plugins {
      id("com.android.asset-pack")
    }
    
    assetPack {
      packName.set("asset-pack-name") // Directory name for the asset pack
      dynamicDelivery {
        deliveryType.set("[ install-time | fast-follow | on-demand ]")
      }
    }
    
  4. 在项目的 app build.gradle 文件中,添加项目中每个资源包的名称,如下所示

    Groovy

    // In the app build.gradle file:
    android {
        ...
        assetPacks = [":asset-pack-name", ":asset-pack2-name"]
    }
    

    Kotlin

    // In the app build.gradle.kts file:
    android {
        ...
        assetPacks += listOf(":asset-pack-name", ":asset-pack2-name")
    }
    
  5. 在项目的 settings.gradle 文件中,包含项目中的所有资源包,如下所示

    Groovy

    // In the settings.gradle file:
    include ':app'
    include ':asset-pack-name'
    include ':asset-pack2-name'
    

    Kotlin

    // In the settings.gradle.kts file:
    include(":app")
    include(":asset-pack-name")
    include(":asset-pack2-name")
    
  6. 在资源包目录中,创建以下子目录:src/main/assets

  7. 将资源放在 src/main/assets 目录中。您也可以在此处创建子目录。应用的目录结构现在应如下所示

    • build.gradle
    • settings.gradle
    • app/
    • 资源包名称/build.gradle
    • 资源包名称/src/main/assets/您的资源目录
  8. 使用 Gradle 构建 Android 应用包。在生成的应用包中,根级目录现在包含以下内容

    • 资源包名称/manifest/AndroidManifest.xml:配置资源包的标识符和交付模式
    • 资源包名称/assets/您的资源目录:包含作为资源包一部分交付的所有资源的目录

    Gradle 会为每个资源包生成清单,并为您输出 assets/ 目录。

  9. (可选) 如果你计划使用快速跟进和按需交付,请包含 Play Asset Delivery 库

    Groovy

    implementation "com.google.android.play:asset-delivery:2.2.2"
    // For Kotlin use asset-delivery-ktx
    implementation "com.google.android.play:asset-delivery-ktx:2.2.2"
    

    Kotlin

    implementation("com.google.android.play:asset-delivery:2.2.2")
    // For Kotlin use core-ktx
    implementation("com.google.android.play:asset-delivery-ktx:2.2.2")
    

  10. (可选) 配置你的应用包以支持不同的 纹理压缩格式

集成 Play Asset Delivery API

Play Asset Delivery Java API 提供了 AssetPackManager 类用于请求资源包、管理下载和访问资源。请确保首先 将 Play Asset Delivery 库添加到你的项目中

你需要根据你希望访问的资源包的交付类型来实现此 API。以下流程图显示了这些步骤。

Asset pack flow diagram for the Java programming language

图 1. 访问资源包的流程图

安装时交付

配置为 install-time 的资源包在应用启动时即可立即使用。使用 Java AssetManager API 访问以这种模式提供的资源

Kotlin

import android.content.res.AssetManager
...
val context: Context = createPackageContext("com.example.app", 0)
val assetManager: AssetManager = context.assets
val stream: InputStream = assetManager.open("asset-name")

Java

import android.content.res.AssetManager;
...
Context context = createPackageContext("com.example.app", 0);
AssetManager assetManager = context.getAssets();
InputStream is = assetManager.open("asset-name");

快速跟进和按需交付

以下部分展示了如何在下载资源包之前获取有关资源包的信息,如何调用 API 启动下载,以及如何访问已下载的包。这些部分适用于 fast-followon-demand 资源包。

检查状态

每个资源包都存储在应用内部存储中的单独文件夹中。使用 getPackLocation() 方法确定资源包的根文件夹。此方法返回以下值

返回值 状态
一个有效的 AssetPackLocation 对象 资源包根文件夹已准备好通过 assetsPath() 立即访问
null 未知资源包或资源不可用

获取有关资源包的下载信息

应用需要在获取资源包之前公开下载的大小。使用 requestPackStates()getPackStates() 方法确定下载的大小以及包是否正在下载。

Kotlin

suspend fun requestPackStates(packNames: List<String>): AssetPackStates

Java

Task<AssetPackStates> getPackStates(List<String> packNames)

requestPackStates() 是一个返回 AssetPackStates 对象的挂起函数,而 getPackStates() 是一个返回 Task<AssetPackStates> 的异步方法。packStates() 方法返回 Map<String, AssetPackState>。此映射包含每个请求的资源包的状态,以其名称为键

Kotlin

AssetPackStates#packStates(): Map<String, AssetPackState>

Java

Map<String, AssetPackState> AssetPackStates#packStates()

最终请求如下所示

Kotlin

const val assetPackName = "assetPackName"
coroutineScope.launch {
  try {
    val assetPackStates: AssetPackStates =
      manager.requestPackStates(listOf(assetPackName))
    val assetPackState: AssetPackState =
      assetPackStates.packStates()[assetPackName]
  } catch (e: RuntimeExecutionException) {
    Log.d("MainActivity", e.message)
  }
}

Java

final String assetPackName = "myasset";

assetPackManager
    .getPackStates(Collections.singletonList(assetPackName))
    .addOnCompleteListener(new OnCompleteListener<AssetPackStates>() {
        @Override
        public void onComplete(Task<AssetPackStates> task) {
            AssetPackStates assetPackStates;
            try {
                assetPackStates = task.getResult();
                AssetPackState assetPackState =
                    assetPackStates.packStates().get(assetPackName);
            } catch (RuntimeExecutionException e) {
                Log.d("MainActivity", e.getMessage());
                return;
            })

以下 AssetPackState 方法提供了资源包的大小、迄今为止已下载的数量(如果请求)以及已传输到应用的数量

要获取资源包的状态,请使用 status() 方法,该方法将状态作为整数返回,该整数对应于 AssetPackStatus 类中的常量字段。尚未安装的资源包的状态为 AssetPackStatus.NOT_INSTALLED

如果请求失败,请使用 errorCode() 方法,其返回值对应于 AssetPackErrorCode 类中的常量字段。

安装

使用 requestFetch()fetch() 方法首次下载资源包或调用更新资源包以完成

Kotlin

suspend fun AssetPackManager.requestFetch(packs: List<String>): AssetPackStates

Java

Task<AssetPackStates> fetch(List<String> packNames)

此方法返回一个 AssetPackStates 对象,其中包含包列表及其初始下载状态和大小。如果通过 requestFetch()fetch() 请求的资源包已在下载,则返回下载状态,并且不会启动其他下载。

监控下载状态

你应该实现一个 AssetPackStateUpdatedListener 来跟踪资源包的安装进度。状态更新按包细分,以支持跟踪各个资源包的状态。你可以在所有其他下载完成之前开始使用可用的资源包。

Kotlin

fun registerListener(listener: AssetPackStateUpdatedListener)
fun unregisterListener(listener: AssetPackStateUpdatedListener)

Java

void registerListener(AssetPackStateUpdatedListener listener)
void unregisterListener(AssetPackStateUpdatedListener listener)

大型下载

如果下载大于 200 MB 且用户未连接 Wi-Fi,则在用户明确同意使用移动数据连接继续下载之前,下载不会开始。类似地,如果下载很大且用户断开 Wi-Fi 连接,则下载将暂停,并且需要明确同意才能使用移动数据连接继续。已暂停的包的状态为 WAITING_FOR_WIFI。要触发 UI 流程以提示用户确认,请使用 showConfirmationDialog() 方法。

请注意,如果应用未调用此方法,则下载将暂停,并且仅当用户重新连接到 Wi-Fi 连接时才会自动恢复。

所需的用户信息

如果包具有 REQUIRES_USER_CONFIRMATION 状态,则在用户接受使用 showConfirmationDialog() 显示的对话框之前,下载不会继续。当 Play 未识别应用时,例如应用被旁加载时,可能会出现此状态。请注意,在这种情况下调用 showConfirmationDialog() 将导致应用更新。更新后,你需要再次请求资源。

以下是一个侦听器实现示例

Kotlin

private val activityResultLauncher = registerForActivityResult(
    ActivityResultContracts.StartIntentSenderForResult()
) { result ->
    if (result.resultCode == RESULT_OK) {
        Log.d(TAG, "Confirmation dialog has been accepted.")
    } else if (result.resultCode == RESULT_CANCELED) {
        Log.d(TAG, "Confirmation dialog has been denied by the user.")
    }
}

assetPackManager.registerListener { assetPackState ->
  when(assetPackState.status()) {
    AssetPackStatus.PENDING -> {
      Log.i(TAG, "Pending")
    }
    AssetPackStatus.DOWNLOADING -> {
      val downloaded = assetPackState.bytesDownloaded()
      val totalSize = assetPackState.totalBytesToDownload()
      val percent = 100.0 * downloaded / totalSize

      Log.i(TAG, "PercentDone=" + String.format("%.2f", percent))
    }
    AssetPackStatus.TRANSFERRING -> {
      // 100% downloaded and assets are being transferred.
      // Notify user to wait until transfer is complete.
    }
    AssetPackStatus.COMPLETED -> {
      // Asset pack is ready to use. Start the game.
    }
    AssetPackStatus.FAILED -> {
      // Request failed. Notify user.
      Log.e(TAG, assetPackState.errorCode())
    }
    AssetPackStatus.CANCELED -> {
      // Request canceled. Notify user.
    }
    AssetPackStatus.WAITING_FOR_WIFI,
    AssetPackStatus.REQUIRES_USER_CONFIRMATION -> {
      if (!confirmationDialogShown) {
        assetPackManager.showConfirmationDialog(activityResultLauncher);
        confirmationDialogShown = true
      }
    }
    AssetPackStatus.NOT_INSTALLED -> {
      // Asset pack is not downloaded yet.
    }
    AssetPackStatus.UNKNOWN -> {
      Log.wtf(TAG, "Asset pack status unknown")
    }
  }
}

Java

assetPackStateUpdateListener = new AssetPackStateUpdateListener() {
    private final ActivityResultLauncher<IntentSenderRequest> activityResultLauncher =
      registerForActivityResult(
          new ActivityResultContracts.StartIntentSenderForResult(),
          new ActivityResultCallback<ActivityResult>() {
            @Override
            public void onActivityResult(ActivityResult result) {
              if (result.getResultCode() == RESULT_OK) {
                Log.d(TAG, "Confirmation dialog has been accepted.");
              } else if (result.getResultCode() == RESULT_CANCELED) {
                Log.d(TAG, "Confirmation dialog has been denied by the user.");
              }
            }
          });

    @Override
    public void onStateUpdate(AssetPackState assetPackState) {
      switch (assetPackState.status()) {
        case AssetPackStatus.PENDING:
          Log.i(TAG, "Pending");
          break;

        case AssetPackStatus.DOWNLOADING:
          long downloaded = assetPackState.bytesDownloaded();
          long totalSize = assetPackState.totalBytesToDownload();
          double percent = 100.0 * downloaded / totalSize;

          Log.i(TAG, "PercentDone=" + String.format("%.2f", percent));
          break;

        case AssetPackStatus.TRANSFERRING:
          // 100% downloaded and assets are being transferred.
          // Notify user to wait until transfer is complete.
          break;

        case AssetPackStatus.COMPLETED:
          // Asset pack is ready to use. Start the game.
          break;

        case AssetPackStatus.FAILED:
          // Request failed. Notify user.
          Log.e(TAG, assetPackState.errorCode());
          break;

        case AssetPackStatus.CANCELED:
          // Request canceled. Notify user.
          break;

        case AssetPackStatus.WAITING_FOR_WIFI:
        case AssetPackStatus.REQUIRES_USER_CONFIRMATION:
          if (!confirmationDialogShown) {
            assetPackManager.showConfirmationDialog(activityResultLauncher);
            confirmationDialogShown = true;
          }
          break;

        case AssetPackStatus.NOT_INSTALLED:
          // Asset pack is not downloaded yet.
          break;
        case AssetPackStatus.UNKNOWN:
          Log.wtf(TAG, "Asset pack status unknown")
          break;
      }
    }
}

或者,你可以使用 getPackStates() 方法获取当前下载的状态。AssetPackStates 包含下载进度、下载状态和任何失败错误代码。

访问资源包

下载请求达到 COMPLETED 状态后,你可以使用文件系统调用访问资源包。使用 getPackLocation() 方法获取资源包的根文件夹。

资源存储在资源包根目录中的 assets 目录中。你可以使用便捷方法 assetsPath() 获取 assets 目录的路径。使用以下方法获取特定资源的路径

Kotlin

private fun getAbsoluteAssetPath(assetPack: String, relativeAssetPath: String): String? {
    val assetPackPath: AssetPackLocation =
      assetPackManager.getPackLocation(assetPack)
      // asset pack is not ready
      ?: return null

    val assetsFolderPath = assetPackPath.assetsPath()
    // equivalent to: FilenameUtils.concat(assetPackPath.path(), "assets")
    return FilenameUtils.concat(assetsFolderPath, relativeAssetPath)
}

Java

private String getAbsoluteAssetPath(String assetPack, String relativeAssetPath) {
    AssetPackLocation assetPackPath = assetPackManager.getPackLocation(assetPack);

    if (assetPackPath == null) {
        // asset pack is not ready
        return null;
    }

    String assetsFolderPath = assetPackPath.assetsPath();
    // equivalent to: FilenameUtils.concat(assetPackPath.path(), "assets");
    String assetPath = FilenameUtils.concat(assetsFolderPath, relativeAssetPath);
    return assetPath;
}

其他 Play Asset Delivery API 方法

以下是一些你可能希望在你的应用中使用的其他 API 方法。

取消请求

使用 cancel() 取消活动的资源包请求。请注意,此请求是尽力而为的操作。

删除资源包

使用 requestRemovePack()removePack() 安排删除资源包。

获取多个资源包的位置

使用 getPackLocations() 批量查询多个资源包的状态,它返回资源包及其位置的映射。getPackLocations() 返回的映射包含当前已下载并处于最新状态的每个包的条目。

下一步

在本地和 Google Play 上测试 Play Asset Delivery