繼上一篇《新手上手:Rokid移動端+眼鏡端最小實踐》之後,本文將帶你實現一個完整的天氣應用,充分利用Rokid眼鏡的特性:自定義界面顯示天氣信息,TTS語音播報天氣摘要,讓天氣信息在眼鏡端呈現得更直觀、更智能。 如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點贊~,歡迎在評論、私信或郵件中提出,非常感謝您的支持。🙏 那麼你將獲得 完整天氣應用:移動端獲取天氣 → 眼鏡端自定義界面顯示 → TTS語音播報 可直接複製的 Kotlin 代碼片段(天氣API調用、自定義界面JSON生成、TTS播報) 眼鏡特性深度應用:Custom View、全局TTS、界面更新 高頻踩坑與排錯清單

一、總體流程 天氣應用的完整流程如下: 圖片: https://uploader.shimo.im/f/CV8MaL2kJ7mOk1QD.png!thumbnail?source=all&accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3NjM5ODM2NTAsImZpbGVHVUlEIjoiV3IzRHBwYVFvSkZldnAzSiIsImlhdCI6MTc2Mzk4MzM1MCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwicGFhIjoiYWxsOmFsbDoiLCJ1c2VySWQiOjYxOTg3NjQ2fQ.cIN5qOZiJw8mxO9RPQqJpBHTEfJnR_Ehr_ykQ_Sgz1U 從移動端獲取天氣到眼鏡端顯示的完整路徑: 天氣數據獲取:移動端調用高德天氣API → 解析JSON響應 → 封裝天氣數據模型 自定義界面顯示:生成自定義界面JSON → 通過openCustomView()在眼鏡端打開天氣界面 → 實時顯示天氣信息 TTS語音播報:生成天氣摘要文本 → 通過sendGlobalTtsContent()在眼鏡端播報天氣 界面動態更新:使用updateCustomView()更新天氣界面,無需重新打開 雙向交互(可選):眼鏡端可發送刷新請求 → 移動端接收並刷新天氣數據 關鍵特性: 自定義界面(Custom View):在眼鏡端顯示結構化的天氣卡片 TTS語音播報:無需查看界面即可獲取天氣信息 界面更新:支持動態更新天氣數據,提升用户體驗 雙向通信:眼鏡端可主動請求移動端刷新數據 交互流程圖: 圖片: https://uploader.shimo.im/f/uySyHaGKhorfhssI.png!thumbnail?source=all&accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3NjM5ODM2NTAsImZpbGVHVUlEIjoiV3IzRHBwYVFvSkZldnAzSiIsImlhdCI6MTc2Mzk4MzM1MCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwicGFhIjoiYWxsOmFsbDoiLCJ1c2VySWQiOjYxOTg3NjQ2fQ.cIN5qOZiJw8mxO9RPQqJpBHTEfJnR_Ehr_ykQ_Sgz1U

二、技術架構 2.1 核心組件 WeatherModel.kt:天氣數據模型(基於高德API響應格式) WeatherApiHelper.kt:天氣API調用封裝(OkHttp + Gson) WeatherViewHelper.kt:自定義界面JSON生成工具 WeatherActivity.kt:移動端主Activity(整合所有功能) 2.2 數據流程 圖片: https://uploader.shimo.im/f/FzL627gzHTyger1Q.png!thumbnail?source=all&accessToken=eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ.eyJleHAiOjE3NjM5ODM2NTAsImZpbGVHVUlEIjoiV3IzRHBwYVFvSkZldnAzSiIsImlhdCI6MTc2Mzk4MzM1MCwiaXNzIjoidXBsb2FkZXJfYWNjZXNzX3Jlc291cmNlIiwicGFhIjoiYWxsOmFsbDoiLCJ1c2VySWQiOjYxOTg3NjQ2fQ.cIN5qOZiJw8mxO9RPQqJpBHTEfJnR_Ehr_ykQ_Sgz1U

三、移動端實現 3.1 天氣數據模型 基於高德天氣API的響應格式,定義Kotlin數據類:

import com.google.gson.annotations.SerializedName ​ /**

  • 高德天氣API響應數據模型 / data class WeatherApiResponse( /*
    • 返回狀態 值為0或1 1:成功;0:失敗 / @SerializedName("status") val status: String? = null, ​ /*
    • 返回結果總數目 / @SerializedName("count") val count: String? = null, ​ /*
    • 返回的狀態信息 / @SerializedName("info") val info: String? = null, ​ /*
    • 返回狀態説明,10000代表正確 / @SerializedName("infocode") val infocode: String? = null, ​ /*
    • 實況天氣數據信息 / @SerializedName("lives") val lives: List<Live>? = null, ​ /*
    • 預報天氣數據信息 / @SerializedName("forecasts") val forecasts: List<Forecast>? = null ) ​ /*
  • 實況天氣數據信息 / data class Live( /*
    • 省份名 / @SerializedName("province") val province: String? = null, ​ /*
    • 城市名 / @SerializedName("city") val city: String? = null, ​ /*
    • 區域編碼 / @SerializedName("adcode") val adcode: String? = null, ​ /*
    • 天氣現象(漢字描述) / @SerializedName("weather") val weather: String? = null, ​ /*
    • 實時氣温,單位:攝氏度 / @SerializedName("temperature") val temperature: String? = null, ​ /*
    • 風向描述 / @SerializedName("winddirection") val winddirection: String? = null, ​ /*
    • 風力級別,單位:級 / @SerializedName("windpower") val windpower: String? = null, ​ /*
    • 空氣濕度 / @SerializedName("humidity") val humidity: String? = null, ​ /*
    • 數據發佈的時間 / @SerializedName("reporttime") val reporttime: String? = null ) ​ /*
  • 預報天氣信息數據 / data class Forecast( /*
    • 城市編碼 / @SerializedName("adcode") val adcode: String? = null, ​ /*
    • 省份名稱 / @SerializedName("province") val province: String? = null, ​ /*
    • 城市名稱 / @SerializedName("city") val city: String? = null, ​ /*
    • 預報發佈時間 / @SerializedName("reporttime") val reporttime: String? = null, ​ /*
    • 預報數據list結構,元素cast,按順序為當天、第二天、第三天的預報數據 / @SerializedName("casts") val casts: List<Cast>? = null ) ​ /*
  • 預報天氣信息數據項 / data class Cast( /*
    • 日期 / @SerializedName("date") val date: String? = null, ​ /*
    • 星期幾 / @SerializedName("week") val week: String? = null, ​ /*
    • 白天天氣現象 / @SerializedName("dayweather") val dayweather: String? = null, ​ /*
    • 晚上天氣現象 / @SerializedName("nightweather") val nightweather: String? = null, ​ /*
    • 白天温度 / @SerializedName("daytemp") val daytemp: String? = null, ​ /*
    • 晚上温度 / @SerializedName("nighttemp") val nighttemp: String? = null, ​ /*
    • 白天風向 / @SerializedName("daywind") val daywind: String? = null, ​ /*
    • 晚上風向 / @SerializedName("nightwind") val nightwind: String? = null, ​ /*
    • 白天風力 / @SerializedName("daypower") val daypower: String? = null, ​ /*
    • 晚上風力 */ @SerializedName("nightpower") val nightpower: String? = null ) ​ ​ 3.2 天氣API調用封裝 使用OkHttp封裝高德天氣API調用:

​ import android.util.Log import com.google.gson.Gson import okhttp3.* import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.toRequestBody import java.io.IOException import java.util.concurrent.TimeUnit ​ /**

  • 天氣API調用輔助類

  • 封裝高德地圖天氣API的調用邏輯 */ class WeatherApiHelper { companion object { private const val TAG = "WeatherApiHelper"

     // 高德天氣API基礎URL
     private const val BASE_URL = "https://restapi.amap.com/v3/weather/weatherInfo"
     
     // 注意:實際使用時需要在應用中配置API Key
     // 這裏使用佔位符,實際使用時應該從配置文件或環境變量讀取
     private const val API_KEY = "YOUR_AMAP_API_KEY"
     
     // HTTP客户端
     private val client = OkHttpClient.Builder()
         .connectTimeout(10, TimeUnit.SECONDS)
         .readTimeout(10, TimeUnit.SECONDS)
         .writeTimeout(10, TimeUnit.SECONDS)
         .build()
         
     private val gson = Gson()
    

    } ​ /**

    • 獲取實時天氣信息(基礎版)

    • @param cityCode 城市編碼(adcode),例如:110101(北京東城區)

    • @param callback 回調接口,返回天氣數據或錯誤信息 / fun getWeatherLive( cityCode: String, callback: WeatherCallback ) { getWeather(cityCode, "base", callback) } ​ /*

    • 獲取天氣預報信息(包含未來3天)

    • @param cityCode 城市編碼(adcode)

    • @param callback 回調接口,返回天氣數據或錯誤信息 / fun getWeatherForecast( cityCode: String, callback: WeatherCallback ) { getWeather(cityCode, "all", callback) } ​ /*

    • 獲取天氣信息

    • @param cityCode 城市編碼

    • @param extensions base:返回實時天氣, all:返回預報天氣

    • @param callback 回調接口 */ private fun getWeather( cityCode: String, extensions: String, callback: WeatherCallback ) { if (API_KEY == "YOUR_AMAP_API_KEY") { callback.onError("請先配置高德地圖API Key") return } ​ val url = "$BASE_URL?city=$cityCode&extensions=$extensions&output=JSON&key=$API_KEY"

      Log.d(TAG, "請求天氣API: $url")

      val request = Request.Builder() .url(url) .get() .build() ​ client.newCall(request).enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { Log.e(TAG, "天氣API請求失敗", e) callback.onError("網絡請求失敗: ${e.message}") } ​ override fun onResponse(call: Call, response: Response) { try { val responseBody = response.body?.string() if (!response.isSuccessful || responseBody == null) { callback.onError("API請求失敗: HTTP ${response.code}") return } ​ Log.d(TAG, "天氣API響應: $responseBody") ​ val weatherResponse = gson.fromJson(responseBody, WeatherApiResponse::class.java)

               if (weatherResponse.status == "1" && weatherResponse.info == "OK") {
                   callback.onSuccess(weatherResponse)
               } else {
                   callback.onError("API返回錯誤: ${weatherResponse.info} (${weatherResponse.infocode})")
               }
           } catch (e: Exception) {
               Log.e(TAG, "解析天氣數據失敗", e)
               callback.onError("解析數據失敗: ${e.message}")
           } finally {
               response.close()
           }
       }
      

      }) } ​ /**

    • 天氣API回調接口 / interface WeatherCallback { /*

      • 請求成功
      • @param response 天氣響應數據 / fun onSuccess(response: WeatherApiResponse) ​ /*
      • 請求失敗
      • @param error 錯誤信息 / fun onError(error: String) } ​ /*
    • 常用城市編碼(部分示例) */ object CityCodes { // 北京 const val BEIJING_DONGCHENG = "110101" // 東城區 const val BEIJING_HAIDIAN = "110108" // 海淀區

      // 上海 const val SHANGHAI_HUANGPU = "310101" // 黃浦區 const val SHANGHAI_PUDONG = "310115" // 浦東新區

      // 深圳 const val SHENZHEN_FUTIAN = "440304" // 福田區 const val SHENZHEN_NANSHAN = "440305" // 南山區

      // 杭州 const val HANGZHOU_XIHU = "330106" // 西湖區 const val HANGZHOU_SHANGCHENG = "330102" // 上城區

      // 廣州 const val GUANGZHOU_TIANHE = "440106" // 天河區 } } 注意事項: 需要在高德開放平台申請API Key 城市編碼(adcode)可通過高德API獲取,常用編碼參考WeatherApiHelper.CityCodes 3.3 自定義界面JSON生成 根據天氣數據生成Rokid眼鏡自定義界面的JSON格式:

import android.util.Log import com.google.gson.Gson import com.google.gson.JsonObject import org.json.JSONArray import org.json.JSONObject ​ /**

  • 天氣界面JSON生成工具

  • 根據天氣數據生成Rokid眼鏡自定義界面的JSON格式 */ class WeatherViewHelper { companion object { private const val TAG = "WeatherViewHelper"

     // 自定義界面JSON中的控件ID
     object ViewIds {
         const val TV_CITY = "tv_city"
         const val TV_TEMPERATURE = "tv_temperature"
         const val TV_WEATHER = "tv_weather"
         const val TV_WIND = "tv_wind"
         const val TV_HUMIDITY = "tv_humidity"
         const val TV_TIME = "tv_time"
         const val TV_FORECAST_DAY1 = "tv_forecast_day1"
         const val TV_FORECAST_DAY2 = "tv_forecast_day2"
         const val TV_FORECAST_DAY3 = "tv_forecast_day3"
     }
    

    } ​ /**

    • 生成天氣界面的初始化JSON

    • @param live 實時天氣數據(可為null)

    • @param forecast 預報天氣數據(可為null)

    • @return 自定義界面的JSON字符串 */ fun generateWeatherViewJson( live: Live? = null, forecast: Forecast? = null ): String { val root = JSONObject()

      // 根佈局:LinearLayout(垂直方向) root.put("type", "LinearLayout")

      val props = JSONObject() props.put("layout_width", "match_parent") props.put("layout_height", "match_parent") props.put("orientation", "vertical") props.put("gravity", "center_horizontal") props.put("paddingTop", "80dp") props.put("paddingBottom", "80dp") props.put("paddingStart", "20dp") props.put("paddingEnd", "20dp") props.put("backgroundColor", "#FF000000") // 黑色背景 root.put("props", props)

      val children = JSONArray()

      // 1. 城市名稱 children.put(createTextView( id = ViewIds.TV_CITY, text = live?.city ?: "未知城市", textSize = "20sp", textStyle = "bold", marginBottom = "20dp" ))

      // 2. 温度(大字體顯示) children.put(createTextView( id = ViewIds.TV_TEMPERATURE, text = "${live?.temperature ?: "--"}°", textSize = "48sp", textStyle = "bold", marginBottom = "15dp" ))

      // 3. 天氣狀況 children.put(createTextView( id = ViewIds.TV_WEATHER, text = live?.weather ?: "--", textSize = "18sp", marginBottom = "15dp" ))

      // 4. 風向風力(水平佈局) val windLayout = createRelativeLayout( layoutHeight = "wrap_content", marginBottom = "10dp" ) val windLayoutChildren = JSONArray()

      windLayoutChildren.put(createTextView( id = ViewIds.TV_WIND, text = "${live?.winddirection ?: "--"} ${live?.windpower ?: "--"}", textSize = "14sp", layoutWidth = "wrap_content", layoutHeight = "wrap_content" )) windLayout.put("children", windLayoutChildren) children.put(windLayout)

      // 5. 濕度 children.put(createTextView( id = ViewIds.TV_HUMIDITY, text = "濕度: ${live?.humidity ?: "--"}%", textSize = "14sp", marginBottom = "15dp" ))

      // 6. 更新時間 children.put(createTextView( id = ViewIds.TV_TIME, text = "更新: ${live?.reporttime ?: "--"}", textSize = "12sp", textColor = "#FF808080", // 灰色 marginTop = "30dp", marginBottom = "20dp" ))

      // 7. 未來3天預報(如果有預報數據) forecast?.casts?.take(3)?.let { casts -> casts.forEachIndexed { index, cast -> val dayText = when (index) { 0 -> "今天" 1 -> "明天" else -> "後天" } val forecastText = "$dayText ${cast.daytemp ?: "--"}°/${cast.nighttemp ?: "--"}° ${cast.dayweather ?: "--"}"

           val viewId = when (index) {
               0 -> ViewIds.TV_FORECAST_DAY1
               1 -> ViewIds.TV_FORECAST_DAY2
               else -> ViewIds.TV_FORECAST_DAY3
           }
           
           children.put(createTextView(
               id = viewId,
               text = forecastText,
               textSize = "14sp",
               marginBottom = if (index < 2) "8dp" else "0dp"
           ))
       }
      

      }

      root.put("children", children)

      val jsonString = root.toString() Log.d(TAG, "生成的天氣界面JSON: $jsonString") return jsonString } ​ /**

    • 生成更新天氣界面的JSON(僅更新部分控件)

    • @param live 實時天氣數據

    • @param forecast 預報天氣數據

    • @return 更新操作的JSON數組 */ fun generateWeatherUpdateJson( live: Live? = null, forecast: Forecast? = null ): String { val updates = JSONArray()

      live?.let { // 更新城市 if (!it.city.isNullOrEmpty()) { updates.put(createUpdateAction(ViewIds.TV_CITY, "text", it.city)) }

       // 更新温度
       if (!it.temperature.isNullOrEmpty()) {
           updates.put(createUpdateAction(ViewIds.TV_TEMPERATURE, "text", "${it.temperature}°"))
       }
       
       // 更新天氣
       if (!it.weather.isNullOrEmpty()) {
           updates.put(createUpdateAction(ViewIds.TV_WEATHER, "text", it.weather))
       }
       
       // 更新風向風力
       val windText = "${it.winddirection ?: "--"} ${it.windpower ?: "--"}"
       updates.put(createUpdateAction(ViewIds.TV_WIND, "text", windText))
       
       // 更新濕度
       if (!it.humidity.isNullOrEmpty()) {
           updates.put(createUpdateAction(ViewIds.TV_HUMIDITY, "text", "濕度: ${it.humidity}%"))
       }
       
       // 更新時間
       if (!it.reporttime.isNullOrEmpty()) {
           updates.put(createUpdateAction(ViewIds.TV_TIME, "text", "更新: ${it.reporttime}"))
       }
      

      }

      // 更新預報 forecast?.casts?.take(3)?.forEachIndexed { index, cast -> val dayText = when (index) { 0 -> "今天" 1 -> "明天" else -> "後天" } val forecastText = "$dayText ${cast.daytemp ?: "--"}°/${cast.nighttemp ?: "--"}° ${cast.dayweather ?: "--"}"

       val viewId = when (index) {
           0 -> ViewIds.TV_FORECAST_DAY1
           1 -> ViewIds.TV_FORECAST_DAY2
           else -> ViewIds.TV_FORECAST_DAY3
       }
       
       updates.put(createUpdateAction(viewId, "text", forecastText))
      

      }

      val jsonString = updates.toString() Log.d(TAG, "生成的天氣更新JSON: $jsonString") return jsonString } ​ /**

    • 創建TextView控件 */ private fun createTextView( id: String, text: String, textSize: String = "16sp", textColor: String = "#FF00FF00", // 綠色(眼鏡端顯示) textStyle: String? = null, layoutWidth: String = "wrap_content", layoutHeight: String = "wrap_content", gravity: String = "center", marginTop: String? = null, marginBottom: String? = null, marginStart: String? = null, marginEnd: String? = null ): JSONObject { val view = JSONObject() view.put("type", "TextView")

      val props = JSONObject() props.put("id", id) props.put("layout_width", layoutWidth) props.put("layout_height", layoutHeight) props.put("text", text) props.put("textSize", textSize) props.put("textColor", textColor) props.put("gravity", gravity)

      textStyle?.let { props.put("textStyle", it) } marginTop?.let { props.put("marginTop", it) } marginBottom?.let { props.put("marginBottom", it) } marginStart?.let { props.put("marginStart", it) } marginEnd?.let { props.put("marginEnd", it) }

      view.put("props", props) return view } ​ /**

    • 創建RelativeLayout佈局 */ private fun createRelativeLayout( layoutWidth: String = "match_parent", layoutHeight: String = "wrap_content", backgroundColor: String = "#00000000", marginTop: String? = null, marginBottom: String? = null ): JSONObject { val layout = JSONObject() layout.put("type", "RelativeLayout")

      val props = JSONObject() props.put("layout_width", layoutWidth) props.put("layout_height", layoutHeight) props.put("backgroundColor", backgroundColor)

      marginTop?.let { props.put("marginTop", it) } marginBottom?.let { props.put("marginBottom", it) }

      layout.put("props", props) return layout } ​ /**

    • 創建更新操作 */ private fun createUpdateAction( id: String, propName: String, propValue: Any ): JSONObject { val action = JSONObject() action.put("action", "update") action.put("id", id)

      val props = JSONObject() props.put(propName, propValue) action.put("props", props)

      return action } ​ /**

    • 生成天氣TTS播報文本

    • @param live 實時天氣數據

    • @param forecast 預報天氣數據

    • @return TTS播報文本 */ fun generateWeatherTtsText( live: Live?, forecast: Forecast? = null ): String { if (live == null) { return "天氣數據獲取失敗" } ​ val city = live.city ?: "當前城市" val temperature = live.temperature ?: "--" val weather = live.weather ?: "未知" val wind = "${live.winddirection ?: ""} ${live.windpower ?: ""}".trim()

      val ttsText = StringBuilder() ttsText.append("$city 當前天氣,") ttsText.append("温度 $temperature 度,") ttsText.append("$weather")

      if (wind.isNotEmpty()) { ttsText.append(",$wind") }

      // 添加預報信息 forecast?.casts?.firstOrNull()?.let { cast -> val tomorrowTemp = cast.daytemp ?: "--" val tomorrowWeather = cast.dayweather ?: "--" ttsText.append("。明天 $tomorrowWeather,温度 $tomorrowTemp 度") }

      return ttsText.toString() } } ​ 自定義界面JSON格式説明: 支持佈局:LinearLayout、RelativeLayout 支持控件:TextView、ImageView 顏色格式:#FF00FF00(ARGB,綠色在眼鏡端顯示) 尺寸單位:dp(佈局)、sp(文字) 3.4 TTS播報文本生成 生成適合TTS播報的天氣摘要文本:

// WeatherViewHelper.kt fun generateWeatherTtsText( live: Live?, forecast: Forecast? = null ): String { if (live == null) { return "天氣數據獲取失敗" } ​ val city = live.city ?: "當前城市" val temperature = live.temperature ?: "--" val weather = live.weather ?: "未知" val wind = "${live.winddirection ?: ""} ${live.windpower ?: ""}".trim()

val ttsText = StringBuilder()
ttsText.append("$city 當前天氣,")
ttsText.append("温度 $temperature 度,")
ttsText.append("$weather")

if (wind.isNotEmpty()) {
    ttsText.append(",$wind")
}

// 添加明天預報
forecast?.casts?.firstOrNull()?.let { cast ->
    val tomorrowTemp = cast.daytemp ?: "--"
    val tomorrowWeather = cast.dayweather ?: "--"
    ttsText.append("。明天 $tomorrowWeather,温度 $tomorrowTemp 度")
}

return ttsText.toString()

} 3.5 主Activity整合 在WeatherActivity中整合所有功能:

/**

  • 天氣應用Activity - Rokid眼鏡端天氣顯示示例

  • 功能:

    1. 調用高德天氣API獲取天氣數據
    1. 在眼鏡端使用自定義界面顯示天氣信息
    1. 使用TTS語音播報天氣信息
    1. 支持更新天氣界面 */ class WeatherActivity : AppCompatActivity() { companion object { private const val TAG = "WeatherActivity" } ​ private lateinit var tvStatus: TextView private lateinit var etCityCode: EditText private lateinit var btnQueryWeather: Button private lateinit var btnShowWeather: Button private lateinit var btnUpdateWeather: Button private lateinit var btnTtsWeather: Button private lateinit var btnCloseView: Button ​ private val weatherApiHelper = WeatherApiHelper() private val weatherViewHelper = WeatherViewHelper()

    private var currentWeatherResponse: WeatherApiResponse? = null private var isCustomViewOpened = false ​ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_weather) ​ initViews() setupCustomViewListener()

     // 默認城市編碼:北京東城區
     etCityCode.setText(WeatherApiHelper.CityCodes.BEIJING_DONGCHENG)
     
     updateStatus("天氣應用已啓動,請先連接Rokid設備")
    

    } ​ private fun initViews() { tvStatus = findViewById(R.id.tvStatus) etCityCode = findViewById(R.id.etCityCode) btnQueryWeather = findViewById(R.id.btnQueryWeather) btnShowWeather = findViewById(R.id.btnShowWeather) btnUpdateWeather = findViewById(R.id.btnUpdateWeather) btnTtsWeather = findViewById(R.id.btnTtsWeather) btnCloseView = findViewById(R.id.btnCloseView) ​ btnQueryWeather.setOnClickListener { queryWeather() } btnShowWeather.setOnClickListener { showWeatherOnGlasses() } btnUpdateWeather.setOnClickListener { updateWeatherOnGlasses() } btnTtsWeather.setOnClickListener { ttsWeatherOnGlasses() } btnCloseView.setOnClickListener { closeCustomView() }

     // 初始狀態:只有查詢天氣按鈕可用
     btnShowWeather.isEnabled = false
     btnUpdateWeather.isEnabled = false
     btnTtsWeather.isEnabled = false
     btnCloseView.isEnabled = false
    

    } ​ /**

    • 設置自定義界面監聽器

    • 監聽眼鏡端自定義界面的狀態變化 / private fun setupCustomViewListener() { val customViewListener = object : CustomViewListener { override fun onOpened() { Log.d(TAG, "自定義界面已打開") runOnUiThread { updateStatus("自定義界面已打開") isCustomViewOpened = true btnUpdateWeather.isEnabled = true btnCloseView.isEnabled = true } } ​ override fun onClosed() { Log.d(TAG, "自定義界面已關閉") runOnUiThread { updateStatus("自定義界面已關閉") isCustomViewOpened = false btnUpdateWeather.isEnabled = false btnCloseView.isEnabled = false } } ​ override fun onUpdated() { Log.d(TAG, "自定義界面已更新") runOnUiThread { updateStatus("自定義界面已更新") } } ​ override fun onOpenFailed(errorCode: Int) { Log.e(TAG, "自定義界面打開失敗: $errorCode") runOnUiThread { updateStatus("自定義界面打開失敗: $errorCode") isCustomViewOpened = false } } ​ override fun onIconsSent() { Log.d(TAG, "圖標已發送") runOnUiThread { updateStatus("圖標已發送") } } } ​ CxrApi.getInstance().setCustomViewListener(customViewListener) } ​ /*

    • 查詢天氣數據 */ private fun queryWeather() { val cityCode = etCityCode.text.toString().trim() if (cityCode.isEmpty()) { updateStatus("請輸入城市編碼") return } ​ updateStatus("正在查詢天氣...") btnQueryWeather.isEnabled = false ​ // 獲取實時天氣和預報天氣 weatherApiHelper.getWeatherForecast(cityCode, object : WeatherApiHelper.WeatherCallback { override fun onSuccess(response: WeatherApiResponse) { Log.d(TAG, "天氣查詢成功: $response") currentWeatherResponse = response ​ runOnUiThread { val live = response.lives?.firstOrNull() val cityName = live?.city ?: "未知城市" val temperature = live?.temperature ?: "--" val weather = live?.weather ?: "--"

               updateStatus("查詢成功: $cityName $temperature° $weather")
               
               btnQueryWeather.isEnabled = true
               btnShowWeather.isEnabled = true
               btnTtsWeather.isEnabled = true
           }
       }
      

​ override fun onError(error: String) { Log.e(TAG, "天氣查詢失敗: $error") runOnUiThread { updateStatus("查詢失敗: $error") btnQueryWeather.isEnabled = true btnShowWeather.isEnabled = false btnTtsWeather.isEnabled = false } } }) } ​ /** * 在眼鏡端顯示天氣界面 * 使用自定義界面(Custom View)功能 */ private fun showWeatherOnGlasses() { if (!checkBluetoothConnected()) { return } ​ val response = currentWeatherResponse if (response == null) { updateStatus("請先查詢天氣數據") return } ​ val live = response.lives?.firstOrNull() val forecast = response.forecasts?.firstOrNull() ​ // 生成自定義界面JSON val viewJson = weatherViewHelper.generateWeatherViewJson(live, forecast)

    Log.d(TAG, "打開自定義界面: $viewJson")
    
    // 打開自定義界面
    val status = CxrApi.getInstance().openCustomView(viewJson)
    handleRequestStatus(
        status = status,
        successMessage = "正在打開天氣界面...",
        waitingMessage = "請求處理中,請稍候...",
        failedMessage = "打開天氣界面失敗"
    )
}

​ /** * 更新眼鏡端的天氣界面 */ private fun updateWeatherOnGlasses() { if (!checkBluetoothConnected()) { return } ​ if (!isCustomViewOpened) { updateStatus("請先打開天氣界面") return } ​ val response = currentWeatherResponse if (response == null) { updateStatus("請先查詢天氣數據") return } ​ val live = response.lives?.firstOrNull() val forecast = response.forecasts?.firstOrNull() ​ // 生成更新JSON val updateJson = weatherViewHelper.generateWeatherUpdateJson(live, forecast)

    Log.d(TAG, "更新天氣界面: $updateJson")
    
    // 更新自定義界面
    val status = CxrApi.getInstance().updateCustomView(updateJson)
    handleRequestStatus(
        status = status,
        successMessage = "正在更新天氣界面...",
        waitingMessage = "更新請求處理中,請稍候...",
        failedMessage = "更新天氣界面失敗"
    )
}

​ /** * 在眼鏡端使用TTS播報天氣 * 使用全局TTS功能 */ private fun ttsWeatherOnGlasses() { if (!checkBluetoothConnected()) { return } ​ val response = currentWeatherResponse if (response == null) { updateStatus("請先查詢天氣數據") return } ​ val live = response.lives?.firstOrNull() val forecast = response.forecasts?.firstOrNull() ​ // 生成TTS文本 val ttsText = weatherViewHelper.generateWeatherTtsText(live, forecast)

    Log.d(TAG, "播報天氣TTS: $ttsText")
    
    // 發送全局TTS消息
    val status = CxrApi.getInstance().sendGlobalTtsContent(ttsText)
    handleRequestStatus(
        status = status,
        successMessage = "正在播報天氣...",
        waitingMessage = "TTS請求處理中,請稍候...",
        failedMessage = "TTS播報失敗"
    )
}

​ /** * 關閉眼鏡端的自定義界面 / private fun closeCustomView() { if (!checkBluetoothConnected()) { return } ​ val status = CxrApi.getInstance().closeCustomView() handleRequestStatus( status = status, successMessage = "正在關閉天氣界面...", waitingMessage = "關閉請求處理中,請稍候...", failedMessage = "關閉天氣界面失敗" ) } ​ /* * 統一處理API請求狀態 * * @param status API返回的狀態 * @param successMessage 成功時的提示信息 * @param waitingMessage 等待時的提示信息 * @param failedMessage 失敗時的提示信息 / private fun handleRequestStatus( status: ValueUtil.CxrStatus, successMessage: String, waitingMessage: String = "請求處理中,請稍候...", failedMessage: String = "操作失敗" ) { when (status) { ValueUtil.CxrStatus.REQUEST_SUCCEED -> { updateStatus(successMessage) } ValueUtil.CxrStatus.REQUEST_WAITING -> { updateStatus(waitingMessage) } ValueUtil.CxrStatus.REQUEST_FAILED -> { updateStatus(failedMessage) } else -> { // 處理意外狀態(理論上不應該出現) Log.w(TAG, "收到意外的狀態: $status") updateStatus("未知狀態: $status") } } } ​ /* * 檢查藍牙連接狀態 / private fun checkBluetoothConnected(): Boolean { val isConnected = CxrApi.getInstance().isBluetoothConnected() if (!isConnected) { updateStatus("請先連接Rokid設備") } return isConnected } ​ /* * 更新狀態顯示 */ private fun updateStatus(message: String) { Log.d(TAG, message) tvStatus.text = "狀態: $message" } ​ override fun onDestroy() { super.onDestroy() // 清理自定義界面監聽器 CxrApi.getInstance().setCustomViewListener(null) } } ​ ​

四、眼鏡端交互處理 4.1 自定義界面系統級支持 重要説明:Rokid眼鏡端的自定義界面(Custom View)是系統級功能,不需要在眼鏡端編寫額外的代碼。移動端通過openCustomView()發送的JSON會自動在眼鏡端渲染顯示。 交互流程:

移動端 openCustomView(json) ↓ 藍牙/WiFi傳輸 ↓ 眼鏡端系統自動渲染顯示 4.2 消息通道交互方式 如果需要在眼鏡端實現更復雜的交互(比如接收天氣數據更新、發送反饋等),可以使用消息通道方式。 4.2.1 接收移動端消息 眼鏡端通過CustomCmdListener接收來自移動端的消息:

// GlassesMainActivity.kt (眼鏡端) class GlassesMainActivity : AppCompatActivity() { companion object { private const val TAG = "GlassesMain" private const val CHANNEL_WEATHER = "weather_update" private const val CHANNEL_WEATHER_REFRESH = "weather_refresh" } ​ private val gson = Gson() ​ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setupMessageListener() } ​ /** * 設置消息監聽器 * 接收來自移動端的天氣相關消息 */ private fun setupMessageListener() { CxrApi.getInstance().setCustomCmdListener(object : CustomCmdListener { override fun onCustomCmd(name: String, args: Caps?) { Log.d(TAG, "收到命令: channel=$name, args.size=${args?.size()}")

            when (name) {
                CHANNEL_WEATHER -> handleWeatherUpdate(args)
                CHANNEL_WEATHER_REFRESH -> handleWeatherRefresh(args)
                else -> Log.w(TAG, "未知通道: $name")
            }
        }
    })
}

​ /** * 處理天氣數據更新 */ private fun handleWeatherUpdate(caps: Caps?) { if (caps == null || caps.size() < 2) { Log.e(TAG, "handleWeatherUpdate: caps 格式錯誤") return } ​ try { // 按發送端寫入順序解析: // 第1個:子命令 (如 "WEATHER_UPDATE") // 第2個:載荷 (JSON字符串) val subCommand = caps.at(0).getString() val payloadJson = caps.at(1).getString()

        Log.d(TAG, "子命令: $subCommand, 載荷: $payloadJson")

​ when (subCommand) { "WEATHER_UPDATE" -> { // 解析天氣數據JSON val weatherData = gson.fromJson(payloadJson, Map::class.java) as? Map<String, Any?> val city = weatherData?.get("city") as? String val temperature = weatherData?.get("temperature") as? String val weather = weatherData?.get("weather") as? String

                // 更新UI顯示
                updateWeatherDisplay(city, temperature, weather)
                
                // 可選:發送ACK確認
                sendAckToMobile("天氣數據已接收: $city $temperature°")
            }
            "WEATHER_FORECAST" -> {
                // 處理預報數據
                handleForecastUpdate(payloadJson)
            }
            else -> {
                Log.w(TAG, "未知子命令: $subCommand")
            }
        }
    } catch (e: Exception) {
        Log.e(TAG, "解析天氣更新失敗", e)
    }
}

​ /** * 處理天氣刷新請求 */ private fun handleWeatherRefresh(caps: Caps?) { // 眼鏡端可以請求移動端刷新天氣 // 例如:用户通過眼鏡按鍵觸發刷新 Log.d(TAG, "收到天氣刷新請求")

    // 可選:發送請求到移動端
    requestMobileRefresh()
}

​ /** * 更新天氣顯示 / private fun updateWeatherDisplay(city: String?, temp: String?, weather: String?) { runOnUiThread { // 更新UI顯示 // 注意:如果使用自定義界面,這部分由系統自動處理 Log.d(TAG, "更新顯示: $city $temp° $weather") } } ​ /* * 發送確認消息給移動端 */ private fun sendAckToMobile(message: String) { try { val ackJson = gson.toJson(mapOf( "code" to 0, "message" to message, "timestamp" to System.currentTimeMillis() ))

        val caps = Caps().apply {
            write("WEATHER_ACK")
            write(ackJson)
        }
        
        CxrApi.getInstance().sendCustomCmd("glass_ack", caps)
        Log.d(TAG, "已發送 ACK: $message")
    } catch (e: Exception) {
        Log.e(TAG, "發送 ACK 失敗", e)
    }
}

​ /** * 請求移動端刷新天氣 */ private fun requestMobileRefresh() { try { val requestJson = gson.toJson(mapOf( "action" to "refresh_weather", "timestamp" to System.currentTimeMillis() ))

        val caps = Caps().apply {
            write("REFRESH_REQUEST")
            write(requestJson)
        }
        
        CxrApi.getInstance().sendCustomCmd("glass_weather_request", caps)
        Log.d(TAG, "已發送刷新請求")
    } catch (e: Exception) {
        Log.e(TAG, "發送刷新請求失敗", e)
    }
}

​ override fun onDestroy() { super.onDestroy() CxrApi.getInstance().setCustomCmdListener(null) } } 4.2.2 移動端發送天氣消息(可選擴展) 如果需要在移動端通過消息通道發送天氣數據(而不是使用自定義界面),可以在WeatherActivity中添加:

// WeatherActivity.kt (移動端擴展) private const val CHANNEL_WEATHER = "weather_update" ​ /**

  • 通過消息通道發送天氣數據到眼鏡端 */ private fun sendWeatherViaMessage(live: Live?, forecast: Forecast?) { if (!checkBluetoothConnected()) { return } ​ val weatherData = mapOf( "city" to (live?.city ?: "--"), "temperature" to (live?.temperature ?: "--"), "weather" to (live?.weather ?: "--"), "wind" to "${live?.winddirection ?: ""} ${live?.windpower ?: ""}".trim(), "humidity" to (live?.humidity ?: "--"), "timestamp" to System.currentTimeMillis() ) ​ val json = gson.toJson(weatherData)

    val caps = Caps().apply { write("WEATHER_UPDATE") // 子命令 write(json) // 載荷 }

    val status = CxrApi.getInstance().sendCustomCmd(CHANNEL_WEATHER, caps) handleRequestStatus( status = status, successMessage = "天氣數據已發送", failedMessage = "發送天氣數據失敗" ) } 4.3 雙向交互完整示例 場景:眼鏡端顯示天氣 → 用户操作刷新 → 眼鏡端請求移動端 → 移動端刷新並更新顯示 移動端:監聽眼鏡端刷新請求

// WeatherActivity.kt (移動端) private const val CHANNEL_WEATHER_REQUEST = "glass_weather_request" ​ private fun setupCustomCmdListener() { CxrApi.getInstance().setCustomCmdListener(object : CustomCmdListener { override fun onCustomCmd(name: String, args: Caps?) { when (name) { CHANNEL_WEATHER_REQUEST -> { // 收到眼鏡端的刷新請求 val subCommand = args?.at(0)?.getString() if (subCommand == "REFRESH_REQUEST") { // 自動刷新天氣並更新眼鏡端顯示 refreshWeatherAndUpdateGlasses() } } } } }) } ​ private fun refreshWeatherAndUpdateGlasses() { val cityCode = etCityCode.text.toString().trim() queryWeather() // 刷新天氣數據

// 刷新成功後自動更新眼鏡端顯示
if (isCustomViewOpened) {
    updateWeatherOnGlasses()
}

} 眼鏡端:發送刷新請求

// GlassesMainActivity.kt (眼鏡端) /**

  • 用户觸發刷新(例如:按鍵、手勢等) */ fun onUserRefreshRequest() { requestMobileRefresh() } ​ private fun requestMobileRefresh() { val requestJson = gson.toJson(mapOf( "action" to "refresh_weather", "timestamp" to System.currentTimeMillis() ))

    val caps = Caps().apply { write("REFRESH_REQUEST") write(requestJson) }

    CxrApi.getInstance().sendCustomCmd("glass_weather_request", caps) } 4.4 交互方式對比

五、踩坑與排錯速查 5.1 天氣API相關 API Key未配置:在WeatherApiHelper中配置API_KEY 城市編碼錯誤:使用高德API獲取正確的adcode,或參考WeatherApiHelper.CityCodes 網絡請求失敗:檢查網絡權限、網絡連接、API配額 JSON解析失敗:檢查API響應格式,確保數據模型匹配 5.2 自定義界面相關 界面未顯示:檢查藍牙連接狀態、JSON格式是否正確、界面是否打開成功 JSON格式錯誤:參考CXR-M(移動端)自定義界面場景.md,確保格式符合規範 顏色不顯示:使用綠色通道(#FF00FF00),其他顏色在眼鏡端可能不顯示 更新不生效:確保使用正確的控件ID,更新JSON格式正確 5.3 TTS播報相關 TTS不播報:檢查藍牙連接狀態、文本內容是否為空 播報順序混亂:TTS自動處理播放隊列,避免快速連續發送 中文亂碼:確保使用UTF-8編碼 5.4 藍牙連接相關 設備未連接:使用CxrApi.getInstance().isBluetoothConnected()檢查連接狀態 連接斷開:監聽BluetoothStatusCallback.onDisconnected(),處理重連邏輯 請求失敗:確保在連接成功後再調用openCustomView()、sendGlobalTtsContent()等API 5.5 常見錯誤碼 REQUEST_SUCCEED:請求成功 REQUEST_WAITING:請求處理中,不要重複請求 REQUEST_FAILED:請求失敗,檢查連接狀態和參數

六、擴展功能建議 6.1 定時刷新

// 使用Handler或協程定時刷新天氣 private val handler = Handler(Looper.getMainLooper()) private val refreshRunnable = object : Runnable { override fun run() { queryWeather() handler.postDelayed(this, 30 * 60 * 1000) // 30分鐘刷新一次 } } 6.2 位置定位 集成Android定位服務,自動獲取當前城市編碼:

// 使用FusedLocationProviderClient獲取位置 // 然後通過高德逆地理編碼API獲取adcode 6.3 天氣圖標 使用sendCustomViewIcons()上傳天氣圖標(晴、雨、雪等),在自定義界面中使用ImageView顯示。 6.4 全局消息通知 使用sendGlobalMsgContent()或sendGlobalToastContent()在天氣變化時發送通知。 6.5 多城市管理 支持添加多個城市,切換顯示不同城市的天氣信息。

七、最後 本文實現了一個不太完整的天氣應用,充分利用了Rokid眼鏡的自定義界面和TTS語音播報特性。通過這個示例,你可以: 學會調用第三方API並解析數據 掌握自定義界面的JSON格式定義 實現界面動態更新機制 使用TTS進行語音播報 處理眼鏡端連接狀態和錯誤 下一步,你可以基於這個框架實現更多應用場景,比如新聞播報、股票顯示、日程提醒等。只要掌握了自定義界面和TTS的使用,就能快速開發出實用的眼鏡端應用。 如果您有任何疑問、對文章寫的不滿意、發現錯誤或者有更好的方法,如果你想支持下一期請務必點贊~,歡迎在評論、私信或郵件中提出,這對我真的很重要,非常感謝您的支持。🙏

所有代碼均已包含在項目中,可直接參考使用。