试用 AppCard Codelab

本页介绍了如何实现 AppCard。

第 1 步:添加所有导入项

  1. 如需添加导入,请执行以下操作:

    static_libs: [
         …
         "car-app-card",
    ],
    

第 2 步:将 SimpleAppCardContentProvider 添加到清单

  1. 请务必将 android:authorities(粗体)替换为您的软件包名称。

    <!-- App Card Content Provider that is required to communicate
         relevant App Cards with the system
         android.car.permission.BIND_APP_CARD_PROVIDER is an essential permission
         that will only allow the system to communicate with the
         AppCardContentProvider
         The content provider must also be exported and enabled -->
         <provider android:name=".SimpleAppCardContentProvider"
             android:authorities="com.example.appcard.sample" 
             android:permission="@string/host_permission"
             android:exported="true"
             android:enabled="true">
             <intent-filter>
                <!-- This intent filter will allow the system to find and
                     connect with the application's AppCardContentProvider -->
                 <action android:name="com.android.car.appcard.APP_CARD_PROVIDER" />
                </intent-filter>
         </provider>
    

第 3 步:将 SimpleAppCardContentProvider 添加到清单

  1. 如需将 SimpleAppCardContentProvider 添加到清单中,请执行以下操作:

    class SimpleAppCardContentProvider : AppCardContentProvider() {
      /** Must return same authority as defined in manifest */
      override val authority: String = AUTHORITY
    
      /** Setup [AppCardContentProvider] and its constituents */
      override fun onCreate(): Boolean {
        return super.onCreate()
      }
    
      /** Setup an [AppCard] that is being requested */
      override fun onAppCardAdded(id: String, ctx: AppCardContext): AppCard {
        return when (id) {
          APPCARD_ID -> //TODO: create app card
    
          else -> throw IllegalStateException("Unidentified app card ID: $id")
        }
      }
    
      /** List of supported [AppCard] IDs */
       override val appCardIds: List<String> = listOf(APPCARD_ID).toMutableList()
    
      /** Clean up when an [AppCard] is removed */
      override fun onAppCardRemoved(id: String) {
        when (id) {
          APPCARD_ID -> //TODO: create app card
        }
      }
    
      /** Handle an [AppCardContext] change for a particular [AppCard] ID */
      override fun onAppCardContextChanged(
        id: String,
        appCardContext: AppCardContext
      ) {
         when (id) {
          APPCARD_ID -> //TODO: update AppCardContext
        }
      }
    
      companion object {
        private const val AUTHORITY = "com.example.appcard.sample"
        private const val APPCARD_ID = "sampleAppCard"
      }
    }
    

第 4 步:创建 AppCard

  1. 如需创建 AppCard,请执行以下操作:

    override fun onAppCardAdded(id: String, ctx: AppCardContext): AppCard {
      return when (id) {
        APPCARD_ID -> createAppCard(ctx)
    
        else -> throw IllegalStateException("Unidentified app card ID: $id")
      }
    }
    
    private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
      return ImageAppCard.newBuilder(APPCARD_ID)
        .setPrimaryText("Hello")
        .setSecondaryText("World")
        .setHeader(
          Header.newBuilder("header")
            .setTitle("Code Lab")
            .build()
        )
        .addButton(
          Button.newBuilder(
            "button",
            Button.ButtonType.PRIMARY,
            object : OnClickListener {
              override fun onClick() {
                //no-op
              }
            }
          )
            .setText("Click me!")
            .build()
        )
       .build()
    }
    

例如:

创建 AppCard

图 1. 创建 AppCard。

第 5 步:启用按钮点击器

如需启用点按器,请执行以下操作:

   private var clickCounter = 0

   private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
      ...
       .addButton(
        Button.newBuilder(
          "button",
           Button.ButtonType.PRIMARY,
           object : OnClickListener {
             override fun onClick() {
               clickCounter++
               sendAppCardUpdate(createAppCard(appCardContext))
             }
           }
         )
           .setText(
             if (clickCounter == 0) "Click me!" else "Clicked: $clickCounter"
           )
           .build()
         )
      ...
    }

    override fun onAppCardRemoved(id: String) {
      when (id) {
        APPCARD_ID -> clickCounter = 0
      }
    }

例如:

启用按钮点击器

图 2. 启用按钮点击器。

第 6 步:更新标头图片

  1. 如需更新标题中的图片,请执行以下操作:

      private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
        val headerImageSize = appCardContext.imageAppCardContext.getMaxImageSize(Header::class.java)
        val logo = resToBitmap(
         android.R.drawable.ic_menu_compass,
           headerImageSize.width,
           headerImageSize.height
        )
    
     ...
       .setHeader(
         Header.newBuilder("header")
           .setTitle("Code Lab")
           .setImage(
             Image.newBuilder("image")
               .setContentScale(Image.ContentScale.FILL_BOUNDS)
               .setColorFilter(Image.ColorFilter.TINT)
               .setImageData(logo)
               .build()
           )
           .build()
        )
      ...
      }
    
      private fun resToBitmap(res: Int, width: Int, height: Int): Bitmap {
        val drawable = context?.getDrawable(res)
    
        return drawableToBitmap(drawable!!, width, height)
      }
    
      private fun drawableToBitmap(d: Drawable, width: Int, height: Int): Bitmap {
        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    
        val canvas = Canvas(bitmap)
        val left = 0
        val top = 0
        d.setBounds(left, top, canvas.width, canvas.height)
        d.draw(canvas)
    
        return bitmap
     }
     ```
    
    For example:
    
    ![Change the image in the header](/docs/automotive/unbundled_apps/appcards/images/ceramic-06.png)
    
    **Figure 3.** Change the name to the header.
    
  2. 重复此流程,即可向支持图片的任何组件添加图片。

第 7 步:添加更多互动

  1. 如需创建带有控制功能的进度条,请执行以下操作:

     private var progressOn = false
    
     private var progressTimer: Timer? = null
    
     private var progressCounter = 0
    
     override fun onAppCardRemoved(id: String) {
       when (id) {
         APPCARD_ID -> {
           clickCounter = 0
           progressCounter = 0
           progressTimer?.cancel()
         }
       }
     }
    
     private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
       val buttonImageSize = appCardContext.imageAppCardContext.getMaxImageSize(Button::class.java)
       val progressPlayPauseImage = resToBitmap(
         if (progressOn) {
           android.R.drawable.ic_media_pause
         } else {
           android.R.drawable.ic_media_play
         },
         buttonImageSize.width,
         buttonImageSize.height
       )
       ...
         .setProgressBar(
           createProgressBar()
         )
         .addButton(
           Button.newBuilder(
             "progressButton",
             Button.ButtonType.NO_BACKGROUND,
             object : OnClickListener {
               override fun onClick() {
                 progressOn = !progressOn
                 if (progressOn) {
                   progressTimer = Timer()
                   progressTimer?.scheduleAtFixedRate(object : TimerTask() {
                     override fun run() {
                     progressCounter++
                     if (progressCounter > 60) progressCounter = 0
                     sendAppCardComponentUpdate(APPCARD_ID, createProgressBar())
                   }
                 }, SECONDS_TO_MS, SECONDS_TO_MS)
               } else {
                 progressTimer?.cancel()
                 progressTimer = null
               }
               sendAppCardUpdate(createAppCard(appCardContext))
             }
           }
         )
           .setImage(
             Image.newBuilder("buttonImage")
               .setContentScale(Image.ContentScale.FILL_BOUNDS)
               .setColorFilter(Image.ColorFilter.TINT)
               .setImageData(progressPlayPauseImage)
               .build()
           )
           .build()
       )
     ...
     }
    
     private fun createProgressBar(): ProgressBar {
       return ProgressBar.newBuilder(PROGRESS_BAR_ID, 0, 60)
         .setProgress(progressCounter)
         .build()
     }
    
     companion object {
       ...
       private const val PROGRESS_BAR_ID = "progress"
       private const val SECONDS_TO_MS = 1000L
     }
    

我们在进度条中添加了播放或暂停按钮,该按钮可以每秒更新一次。由于大小限制,请参阅点击器按钮上添加的 setText 以反映点击次数,.setText("$clickCounter")

图 4. 添加按钮。

图 5. 呈现的按钮。

第 8 步:启动 activity

  1. 如果您的应用符合 background-starts#exceptions,您就可以从按钮的 onClickListener 启动 activity。

    class SampleRoutingActivity : AppCompatActivity() {
      override fun onStart() {
        super.onStart()
        val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS).apply {
          setFlags(FLAG_ACTIVITY_CLEAR_TOP)
        }
        startActivity(intent)
        finish()
      }
    }
    
  2. 如需启动相应 activity,请在 AppCard 中添加一个按钮。如果不存在,请向应用添加一个路由 activity:

    override fun onStart() {
    super.onStart()
    val intent = Intent(ACTION_LOCATION_SOURCE_SETTINGS).apply {
      setFlags(FLAG_ACTIVITY_CLEAR_TOP)
    }
    startActivity(intent)
    finish()
     }
    }
    

    当调用该类时,用户会被重定向到位置信息设置。 应用 setFlags(FLAG_ACTIVITY_CLEAR_TOP) 可确保用户无法返回到原始 activity。

  3. 在 AppCard 中添加一个按钮以启动 activity:

     private fun createAppCard(appCardContext: AppCardContext): ImageAppCard {
       val locationImage = resToBitmap(
         android.R.drawable.ic_menu_call,
         buttonImageSize.width,
         buttonImageSize.height
       )
       ...
         .addButton(
           Button.newBuilder(
            "activityButton",
            Button.ButtonType.SECONDARY,
            object : OnClickListener {
              override fun onClick() {
                // no-op
              }
            }
           )
            .setImage(
              Image.newBuilder("locationButtonImage")
                .setContentScale(Image.ContentScale.FILL_BOUNDS)
                .setColorFilter(Image.ColorFilter.TINT)
                .setImageData(locationImage)
                .build()
            )
            .setIntent(
              RoutingActivityIntent
                .newBuilder("com.example.appcard.sample.SampleRoutingActivity")
                .build()
            )
            .build()
          )
        ...
    }
    

由于空间限制,在 AppCard 主机中移除了点击器按钮。AppCard 如下所示:

已移除按钮

图 6. 没有点击器按钮的 AppCard。