Android車載空調(diào)系統(tǒng)(HVAC)開發(fā)方法分析
HVAC 全稱:供暖通風(fēng)與空氣調(diào)節(jié)(Heating Ventilation and Air Conditioning)。用戶可以通過他來控制整個汽車的空調(diào)系統(tǒng),是汽車中非常重要的一個功能。
汽車的空調(diào)HMI雖然并不復(fù)雜,但是大多都是用符號來表示功能,對于還沒有實際用過汽車空調(diào)系統(tǒng)的開發(fā)者來說,理解空調(diào)的各個符號表示的含義也是非常有必要。
1. HVAC 功能介紹
下面就以Android 12中的HVAC來介紹空調(diào)系統(tǒng)中包含的最基礎(chǔ)的功能。
1.1 雙區(qū)溫度調(diào)節(jié)
空調(diào)的溫度調(diào)節(jié)功能,默認是華氏度,可以在系統(tǒng)設(shè)置修改溫度單位。可調(diào)節(jié)范圍是61 - 82華氏度,對應(yīng)16 - 28 攝氏度。
左側(cè)按鈕用來調(diào)節(jié)主駕,右側(cè)按鈕用來調(diào)節(jié)副駕。在以往都是只有高配車型才有雙區(qū)空調(diào),現(xiàn)在的車上雙區(qū)空調(diào)幾乎已經(jīng)是標(biāo)配了。
1.2 空調(diào)開關(guān)
開啟關(guān)閉空調(diào)的開關(guān)
1.3 內(nèi)/外循環(huán)
內(nèi)循環(huán)是汽車空氣調(diào)節(jié)系統(tǒng)的一種狀態(tài)。這種狀態(tài)下,車內(nèi)外的換氣通道關(guān)閉,風(fēng)機關(guān)閉時車內(nèi)氣流不循環(huán),風(fēng)機開啟時,吸入的氣流也僅來自車內(nèi),形成車輛內(nèi)部的氣流循環(huán)。
外循環(huán)則相反,風(fēng)機開啟時,吸入的氣流也僅來自車外,可以更新車內(nèi)的空氣質(zhì)量,代價是會更耗電。
1.4 風(fēng)量調(diào)節(jié)
用于增大或減小空調(diào)的風(fēng)量。
1.5 風(fēng)向調(diào)節(jié)
從左到右分別是吹臉、吹臉+吹腳、吹腳、吹腳+吹擋風(fēng)玻璃
1.6 A/C開關(guān)
A/C按鍵,它就是制冷開關(guān),按下A/C按鍵,也就啟動了壓縮機,通俗地說就是開冷氣。
1.7 主副駕座椅加熱
左邊的按鈕用于調(diào)節(jié)主駕座椅加熱,右邊的按鈕用于調(diào)節(jié)副駕座椅加熱
1.8 除霜
左邊的按鈕是開啟/關(guān)閉 前擋風(fēng)玻璃加熱,開啟后用來除去前擋風(fēng)玻璃上的霧氣。右邊的按鈕是開啟/關(guān)閉后擋風(fēng)玻璃加熱,開啟后用來除去后擋風(fēng)玻璃上的霧氣。
1.9 自動模式
自動空調(diào)其實就是省略了風(fēng)速、風(fēng)向等調(diào)節(jié)功能,自動空調(diào)是全自動調(diào)節(jié),只需要選擇風(fēng)向和設(shè)定溫度。AUTO按鍵按下后,就會根據(jù)車內(nèi)傳感器來控制出風(fēng)的溫度,冬天熱風(fēng),夏天冷風(fēng)。會保持車內(nèi)有較適宜的溫度,如果溫度過高或過低,空調(diào)也會自動改變出風(fēng)口的溫度及風(fēng)速,調(diào)整車內(nèi)溫度。
以上就是車載空調(diào)系統(tǒng)中最基礎(chǔ)的功能了,實際開發(fā)中我們還會遇到如座椅通風(fēng)、座椅按摩、智能新風(fēng)、負離子等等一些近幾年才出現(xiàn)的空調(diào)新功能,在應(yīng)用開發(fā)上無非就是多幾個界面或按鈕。
2. HVAC 源碼結(jié)構(gòu)
本文中的源碼基于Android 12下HVAC APP,源碼請見:https://github.com/linux-link/CarHvac
原生的Hvac App中不存在Activity、Fragment等傳統(tǒng)意義上用來顯示HMI的組件,取而代之是使用Service來顯示一個Window。主要原因在于Hvac的界面層級比一般的HMI的層級要高,呼出Hvac時需要部分或全部覆蓋其他的應(yīng)用上(當(dāng)然IVI中還是有應(yīng)用比Hvac的層級要高的),這時候使用Activity就顯不合適了。
需要注意的是,Havc在Android 12中雖然有一個獨立的app,但是上圖展示空調(diào)并沒有使用這個獨立的app,它的HMI和邏輯實現(xiàn)都是直接寫在SystemUI中的。
我們可以通過adb發(fā)送一個廣播來調(diào)出獨立的Hvac應(yīng)用。
adb shell am broadcast -a android.car.intent.action.TOGGLE_HVAC_CONTROLS
3. HVAC 核心源碼分析
3.1 AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.car.hvac">
<uses-sdk
android:minSdkVersion="22"
android:targetSdkVersion="29" />
<uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- Required to use the TYPE_DISPLAY_OVERLAY layout param for the overlay hvac ui-->
<uses-permission android:name="android.permission.INTERNAL_SYSTEM_WINDOW" />
<!-- Allow Hvac to go across all users-->
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
<protected-broadcast android:name="android.car.intent.action.TOGGLE_HVAC_CONTROLS" />
<application
android:icon="@drawable/ic_launcher_hvac"
android:label="@string/hvac_label"
android:persistent="true">
<!--用于控制空調(diào)功能的Service-->
<service
android:name=".HvacController"
android:exported="false"
android:singleUser="true" />
<!-- 用于顯示UI的Service-->
<service
android:name=".HvacUiService"
android:exported="false"
android:singleUser="true" />
<!-- 監(jiān)聽開機廣播 -->
<receiver
android:name=".BootCompleteReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
3.2 BootCompleteReceiver
用于監(jiān)聽開機的廣播,當(dāng)前收到系統(tǒng)的開機廣播后,會將HvacUiService拉起。
public class BootCompleteReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Intent hvacUiService = new Intent(context, HvacUiService.class);
context.startService(hvacUiService);
}
}
3.3 HvacUiService
HvacUiService 用來托管Hvac UI的Service。從名字上也能看出,整個HvacUiService都是圍繞著如何將Hvac準(zhǔn)確的繪制出來,基本不含其他的邏輯。
@Override
public void onCreate() {
...
// 由于不存在從服務(wù)內(nèi)部獲取系統(tǒng)ui可見性的方法,因此我們將全屏放置一些東西,并檢查其最終測量結(jié)果,作為獲取該信息的黑客手段。
// 一旦我們有了初始狀態(tài),我們就可以安全地從那時開始注冊更改事件。
View windowSizeTest = new View(this) {
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
Log.i(TAG, "onLayout: changed" + changed + ";left:" + left + ";top:" + top + ";right:" + right + ";bottom" + bottom);
boolean sysUIShowing = (mDisplayMetrics.heightPixels != bottom);
mInitialYOffset = (sysUIShowing) ? -mNavBarHeight : 0;
Log.i(TAG, "onLayout: sysUIShowing:" + sysUIShowing + ";mInitialYOffset" + mInitialYOffset);
layoutHvacUi();
// 我們現(xiàn)在有了初始狀態(tài),因此不再需要這個空視圖。
mWindowManager.removeView(this);
mAddedViews.remove(this);
}
};
addViewToWindowManagerAndTrack(windowSizeTest, testparams);
// 接收事件的廣播
IntentFilter filter = new IntentFilter();
filter.addAction(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS);
filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
// 注冊接收器,以便任何具有CONTROL_CAR_CLIMATE權(quán)限的用戶都可以調(diào)用它。
registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL, filter,
Car.PERMISSION_CONTROL_CAR_CLIMATE, null);
}
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
Log.i(TAG, "onReceive: " + action);
// 自定義廣播,用于展開Hvac的HMI
if (action.equals(CAR_INTENT_ACTION_TOGGLE_HVAC_CONTROLS)) {
mHvacPanelController.toggleHvacUi();
} else if (action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)) {
// home 按鍵的廣播,收起Hvac的HMI
mHvacPanelController.collapseHvacUi();
}
}
};
// 添加View到WindowManager中
private void addViewToWindowManagerAndTrack(View view, WindowManager.LayoutParams params) {
mWindowManager.addView(view, params);
mAddedViews.add(view);
}
HvacUIService在onCreate()中主要完成兩件事:
1.注冊事件廣播。這個事件實際并沒有發(fā)送源,因為SystemUI中額外寫了一個Hvac,不過正是這個廣播讓我們可以把這個單獨的Hvac調(diào)出。
2.繪制UI。HvacUIService在被拉起后并沒有立即開始UI的繪制,而是在屏幕上臨時放置一個用于測量窗口的 windowSizeTest ,當(dāng)windowSizeTestView開始測量后,通過比對View的高度和屏幕的高度,即可判斷出systemUI是否已經(jīng)顯示,這時就可以開始著手繪制真正的Hvac的UI了,并且可以更安全的操作UI。
接下來就是繪制真正的Hvac界面:
/**
* 在確定最小偏移量后調(diào)用。
* 這將生成HVAC UI所需的所有組件的布局。
* 啟動時,折疊視圖所需的所有窗口都可見,而展開視圖的窗口已創(chuàng)建并調(diào)整大小,但不可見。
*/
private void layoutHvacUi() {
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
WindowManager.LayoutParams params = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
| WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
& ~WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
PixelFormat.TRANSLUCENT);
params.packageName = this.getPackageName();
params.gravity = Gravity.BOTTOM | Gravity.LEFT;
params.x = 0;
params.y = mInitialYOffset;
params.width = mScreenWidth;
params.height = mScreenBottom;
params.setTitle("HVAC Container");
disableAnimations(params);
// required of the sysui visiblity listener is not triggered.
params.hasSystemUiListeners = true;
mContainer = inflater.inflate(R.layout.hvac_panel, null);
mContainer.setLayoutParams(params);
mContainer.setOnSystemUiVisibilityChangeListener(visibility -> {
Log.i(TAG, "layoutHvacUi: visibility:" + visibility);
boolean systemUiVisible = (visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0;
int y = 0;
if (systemUiVisible) {
// 當(dāng)systemUi可見時,窗口系統(tǒng)坐標(biāo)從系統(tǒng)導(dǎo)航欄上方的0開始。因此,如果我們想獲得屏幕底部的實際高度,我們需要將y值設(shè)置為導(dǎo)航欄高度的負值。
y = -mNavBarHeight;
}
setYPosition(mDriverTemperatureBar, y);
setYPosition(mPassengerTemperatureBar, y);
setYPosition(mDriverTemperatureBarCollapsed, y);
setYPosition(mPassengerTemperatureBarCollapsed, y);
setYPosition(mContainer, y);
});
// 頂部填充應(yīng)根據(jù)屏幕高度和擴展hvac面板的高度進行計算。由填充物定義的空間意味著可以單擊以關(guān)閉hvac面板。
int topPadding = mScreenBottom - mPanelFullExpandedHeight;
mContainer.setPadding(0, topPadding, 0, 0);
mContainer.setFocusable(false);
mContainer.setFocusableInTouchMode(false);
View panel = mContainer.findViewById(R.id.hvac_center_panel);
panel.getLayoutParams().height = mPanelCollapsedHeight;
addViewToWindowManagerAndTrack(mContainer, params);
// 創(chuàng)建溫度計bar
createTemperatureBars(inflater);
// UI狀態(tài)控制器,用來控制展開/收起時UI的各種狀態(tài)并執(zhí)行動畫
mHvacPanelController = new HvacPanelController(this /* context */, mContainer,
mWindowManager, mDriverTemperatureBar, mPassengerTemperatureBar,
mDriverTemperatureBarCollapsed, mPassengerTemperatureBarCollapsed
);
// 綁定 HvacController Service
Intent bindIntent = new Intent(this /* context */, HvacController.class);
if (!bindService(bindIntent, mServiceConnection, Context.BIND_AUTO_CREATE)) {
Log.e(TAG, "Failed to connect to HvacController.");
}
}
HvacPanelController是空調(diào)的面板控制器,在與HvacController綁定成功后,將HvacController的實例傳遞給HvacPanelController。
private ServiceConnection mServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
mHvacController = ((HvacController.LocalBinder) service).getService();
final Context context = HvacUiService.this;
final Runnable r = () -> {
// hvac控制器從車輛刷新其值后,綁定所有值。
mHvacPanelController.updateHvacController(mHvacController);
};
if (mHvacController != null) {
mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
}
}
@Override
public void onServiceDisconnected(ComponentName className) {
mHvacController = null;
mHvacPanelController.updateHvacController(null);
//TODO:b/29126575重新啟動后重新連接控制器
}
};
我們接著看HvacPanelController
3.4 HvacPanelController
HvacPanelController 主要作用是初始化其他界面Controller,并從HvacController中獲取數(shù)據(jù),顯示在UI上。
private FanSpeedBarController mFanSpeedBarController;
private FanDirectionButtonsController mFanDirectionButtonsController;
private TemperatureController mTemperatureController;
private TemperatureController mTemperatureControllerCollapsed;
private SeatWarmerController mSeatWarmerController;
public void updateHvacController(HvacController controller) {
mHvacController = controller;
mFanSpeedBarController = new FanSpeedBarController(mFanSpeedBar, mHvacController);
mFanDirectionButtonsController
= new FanDirectionButtonsController(mFanDirectionButtons, mHvacController);
mTemperatureController = new TemperatureController(
mPassengerTemperatureBarExpanded,
mDriverTemperatureBarExpanded,
mPassengerTemperatureBarCollapsed,
mDriverTemperatureBarCollapsed,
mHvacController);
mSeatWarmerController = new SeatWarmerController(mPassengerSeatWarmer,
mDriverSeatWarmer, mHvacController);
// 切換按鈕不需要額外的邏輯來映射硬件和UI設(shè)置。只需使用ToggleListener來處理點擊。
mAcButton.setIsOn(mHvacController.getAcState());
mAcButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setAcState(isOn);
}
});
...
setAutoMode(mHvacController.getAutoModeState());
mHvacPowerSwitch.setIsOn(mHvacController.getHvacPowerState());
mHvacPowerSwitch.setToggleListener(isOn -> mHvacController.setHvacPowerState(isOn));
mHvacController.registerCallback(mToggleButtonCallbacks);
mToggleButtonCallbacks.onHvacPowerChange(mHvacController.getHvacPowerState());
}
Hvac界面展開和收起的動畫也是在HvacPanelController 中處理的,不過關(guān)于動畫部分打算以后再開個新坑講一講。
3.5 HvacController
HvacController是HvacApp與CarService之間的信息傳輸控制器,本質(zhì)上也是一個Service。
public class HvacController extends Service {
private final Binder mBinder = new LocalBinder();
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
public class LocalBinder extends Binder {
HvacController getService() {
return HvacController.this;
}
}
...
}
在Hvac中的設(shè)置及獲取數(shù)據(jù)的操作都是通過HvacController進行的,在HvacController啟動時會獲取一個Car實例,并通過connect方法連接CarService。當(dāng)連接CarService成功后初始化CarHvacManager并通過CarHvacManager獲取車輛支持的屬性列表,以及獲取界面所需的基礎(chǔ)數(shù)據(jù)。
@Override
public void onCreate() {
super.onCreate();
if (getPackageManager().hasSystemFeature(PackageManager.FEATURE_AUTOMOTIVE)) {
// 連接 CarService
mCarApiClient = Car.createCar(this, mCarServiceConnection);
mCarApiClient.connect();
}
}
private final ServiceConnection mCarServiceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
synchronized (mHvacManagerReady) {
try {
// 連接上CarService后,獲取到其中的HvacManager.
initHvacManager((CarHvacManager) mCarApiClient.getCarManager(Car.HVAC_SERVICE));
// 連接成功后,喚醒正在等待CarHvacManager的線程
mHvacManagerReady.notifyAll();
} catch (CarNotConnectedException e) {
Log.e(TAG, "Car not connected in onServiceConnected");
}
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
向CarService獲取數(shù)據(jù)需要先得到CarHvacManager的實例,所以在連接成功后,調(diào)用mHvacManagerReady.notifyAll() 喚醒所有之前等待CarHvacManager實例的線程
// HvacUiService.java - mServiceConnection
{
final Runnable r = () -> {
// hvac控制器從車輛刷新其值后,綁定所有值。
mHvacPanelController.updateHvacController(mHvacController);
};
if (mHvacController != null) {
mHvacController.requestRefresh(r, new Handler(context.getMainLooper()));
}
}
// HvacController.java
public void requestRefresh(final Runnable r, final Handler h) {
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... unused) {
synchronized (mHvacManagerReady) {
while (mHvacManager == null) {
try {
mHvacManagerReady.wait();
} catch (InterruptedException e) {
// We got interrupted so we might be shutting down.
return null;
}
}
}
// 刷新數(shù)據(jù)
fetchTemperature(DRIVER_ZONE_ID);
fetchTemperature(PASSENGER_ZONE_ID);
fetchFanSpeed();
...
return null;
}
@Override
protected void onPostExecute(Void unused) {
// 切換到主線程中執(zhí)行runnable
h.post(r);
}
};
task.execute();
}
private void fetchFanSpeed() {
if (mHvacManager != null) {
int zone = SEAT_ALL; //特定于汽車的解決方法。
try {
int speed = mHvacManager.getIntProperty(CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
mDataStore.setFanSpeed(speed);
} catch (android.car.CarNotConnectedException e) {
Log.e(TAG, "Car not connected in fetchFanSpeed");
}
}
}
上面的代碼就是利用AsyncTask在子線程中等待CarHvacManager的實例,然后刷新數(shù)據(jù)并存儲在DatStore中。
需要注意一點的是while (mHvacManager == null)不能替換成if(mHvacManager == null),這是因為Java有個叫“spurious wakeup”的現(xiàn)象,即線程在不該醒過來的時候醒過來。
A thread can wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied.
一個線程有可能會在未被通知、打斷、或超時的情況下醒來,這就是所謂的“spurious wakeup”。盡管實際上這種情況很少發(fā)生,應(yīng)用程序仍然必須對此有所防范,手段是檢查正常的導(dǎo)致線程被喚醒的條件是否滿足,如果不滿足就繼續(xù)等待。
3.6 Car API
Car是Android汽車平臺最高等級的API,為外界提供汽車所有服務(wù)和數(shù)據(jù)訪問的接口,提供了一系列與汽車有關(guān)的API。它不僅僅可以提供HvacManger,像車輛的速度、檔位狀態(tài)等等所有與汽車有關(guān)的信息都可以從Car API中獲取。
Hvac中的CarHvacManager實現(xiàn)了CarManagerBase接口,并且只要是作為CarXXXManager, 都需要實現(xiàn)CarManagerBase接口,如CarCabinManager,CarSensorManager等都實現(xiàn)了該接口。
CarHvacManager的控制操作是通過CarPropertyManager來完成的,CarPropertyManager統(tǒng)一控制汽車屬性相關(guān)的操作。CarHvacManager只是控制與Hvac相關(guān)的操作,在汽車中還有很多屬性控制的Manager,如傳感器,座艙等屬性的控制,他們都是通過CarPropertyManager進行屬性操作,通過在操作時傳入的屬性ID,屬性區(qū)域以及屬性值,在CarPropertyManager中會將這些參數(shù)轉(zhuǎn)化為一個CarPropertyValue對象繼續(xù)往CarService傳遞。
mHvacManager.getIntProperty(CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
private final CarPropertyManager mCarPropertyMgr;
public int getIntProperty(int propertyId, int area) {
return this.mCarPropertyMgr.getIntProperty(propertyId, area);
}
CarHvacManager也是通過注冊一個callback來得到 Car API 的數(shù)據(jù)回調(diào)。
mHvacManager.registerCallback(mHardwareCallback);
private final CarHvacManager.CarHvacEventCallback mHardwareCallback = new CarHvacManager.CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
int areaId = val.getAreaId();
switch (val.getPropertyId()) {
case CarHvacManager.ID_ZONED_AC_ON:
handleAcStateUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_FAN_DIRECTION:
handleFanPositionUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT:
handleFanSpeedUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_TEMP_SETPOINT:
handleTempUpdate(val);
break;
case CarHvacManager.ID_WINDOW_DEFROSTER_ON:
handleDefrosterUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_AIR_RECIRCULATION_ON:
handleAirCirculationUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_SEAT_TEMP:
handleSeatWarmerUpdate(areaId, getValue(val));
break;
case CarHvacManager.ID_ZONED_AUTOMATIC_MODE_ON:
handleAutoModeUpdate(getValue(val));
break;
case CarHvacManager.ID_ZONED_HVAC_POWER_ON:
handleHvacPowerOn(getValue(val));
break;
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unhandled HVAC event, id: " + val.getPropertyId());
}
}
}
@Override
public void onErrorEvent(final int propertyId, final int zone) {
}
};
Hvac中每個Property對應(yīng)的含義如下:
// 全局屬性,只有一個 ID_MIRROR_DEFROSTER_ON //視鏡除霧 ID_STEERING_WHEEL_HEAT //方向盤溫度 ID_OUTSIDE_AIR_TEMP //室外溫度 ID_TEMPERATURE_DISPLAY_UNITS //在使用的溫度 // 區(qū)域?qū)傩?,可在不同區(qū)域設(shè)置 ID_ZONED_TEMP_SETPOINT //用戶設(shè)置的溫度 ID_ZONED_TEMP_ACTUAL //區(qū)域?qū)嶋H溫度 ID_ZONED_HVAC_POWER_ON //HVAC系統(tǒng)電源開關(guān) ID_ZONED_FAN_SPEED_SETPOINT //風(fēng)扇設(shè)置的速度 ID_ZONED_FAN_SPEED_RPM //風(fēng)扇實際的速度 ID_ZONED_FAN_DIRECTION_AVAILABLE //風(fēng)扇可設(shè)置的方向 ID_ZONED_FAN_DIRECTION //現(xiàn)在風(fēng)扇設(shè)置的方向 ID_ZONED_SEAT_TEMP //座椅溫度 ID_ZONED_AC_ON //空調(diào)開關(guān) ID_ZONED_AUTOMATIC_MODE_ON //HVAC自動模式開關(guān) ID_ZONED_AIR_RECIRCULATION_ON //空氣循環(huán)開關(guān) ID_ZONED_MAX_AC_ON //空調(diào)最大速度開關(guān) ID_ZONED_DUAL_ZONE_ON //雙區(qū)模式開關(guān) ID_ZONED_MAX_DEFROST_ON //最大除霧開關(guān) ID_ZONED_HVAC_AUTO_RECIRC_ON //自動循環(huán)模式開關(guān) ID_WINDOW_DEFROSTER_ON //除霧模式開關(guān)
使用Car API時務(wù)必需要注意,注冊的callback是有可能會非常頻繁的產(chǎn)生回調(diào)的,應(yīng)用層需要先將數(shù)據(jù)存儲在DataStore中進行過濾,才能更新到UI上。而且也不要實時的打印日志,否則可能會導(dǎo)致日志緩沖區(qū)EOF,也會嚴重干擾其它進程的日志輸出。
3.7 DataStore
DataStore 用于存儲HvacController從 Car API 中獲取的屬性值。
用戶操作IVI界面和使用硬按鍵,都會更新Hvac的相關(guān)屬性。這兩種不同的更新方式都是從不同的線程更新到當(dāng)前狀態(tài)。此外,在某些情況下,Hvac系統(tǒng)可能會發(fā)送虛假的更新,因此這個類將所有內(nèi)容更新管理合并,從而確保在用戶看來應(yīng)用程序的界面是正常的
@GuardedBy("mFanSpeed")
private Integer mFanSpeed = 0;
private static final long COALESCE_TIME_MS = 0L;
public int getFanSpeed() {
synchronized (mFanSpeed) {
return mFanSpeed;
}
}
// 僅用于主動 獲取、設(shè)定 數(shù)據(jù)時更新speed數(shù)據(jù)。
public void setFanSpeed(int speed) {
synchronized (mFanSpeed) {
mFanSpeed = speed;
mLastFanSpeedSet = SystemClock.uptimeMillis();
}
}
// 從callback中得到數(shù)據(jù)時,因為數(shù)據(jù)可能會刷新的很頻繁,所以需要先判斷時間戳,確定數(shù)據(jù)是否真的需要更新
public boolean shouldPropagateFanSpeedUpdate(int zone, int speed) {
// TODO:我們暫時忽略風(fēng)扇速度區(qū)域,因為我們沒有多區(qū)域車。
synchronized (mFanSpeed) {
if (SystemClock.uptimeMillis() - mLastFanSpeedSet < COALESCE_TIME_MS) {
return false;
}
mFanSpeed = speed;
}
return true;
}
在HvacController中我們從callback得到數(shù)據(jù)刷新時,先通過DataStore判斷以下是否需要更新數(shù)據(jù),如果確實需要更新,再將更新后的數(shù)據(jù)回調(diào)給其他的UI控制器。
// HvacController.java
private final CarHvacManager.CarHvacEventCallback mHardwareCallback = new CarHvacManager.CarHvacEventCallback() {
@Override
public void onChangeEvent(final CarPropertyValue val) {
int areaId = val.getAreaId();
switch (val.getPropertyId()) {
case CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT:
// 處理來自callback的數(shù)據(jù)
handleFanSpeedUpdate(areaId, getValue(val));
break;
// ... 省略
default:
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Unhandled HVAC event, id: " + val.getPropertyId());
}
}
}
};
private void handleFanSpeedUpdate(int zone, int speed) {
// 判斷是否需要更新本地的數(shù)據(jù)
boolean shouldPropagate = mDataStore.shouldPropagateFanSpeedUpdate(zone, speed);
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Fan Speed Update, zone: " + zone + " speed: " + speed +
" should propagate: " + shouldPropagate);
}
if (shouldPropagate) {
// 將更新后的數(shù)據(jù)回調(diào)給各個UI控制器
synchronized (mCallbacks) {
for (int i = 0; i < mCallbacks.size(); i++) {
mCallbacks.get(i).onFanSpeedChange(speed);
}
}
}
}
public void setFanSpeed(final int fanSpeed) {
// 更新當(dāng)前的數(shù)據(jù)
mDataStore.setFanSpeed(fanSpeed);
final AsyncTask<Void, Void, Void> task = new AsyncTask<Void, Void, Void>() {
int newFanSpeed;
protected Void doInBackground(Void... unused) {
if (mHvacManager != null) {
int zone = SEAT_ALL; // Car specific workaround.
try {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Setting fanspeed to: " + fanSpeed);
}
mHvacManager.setIntProperty(
CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone, fanSpeed);
newFanSpeed = mHvacManager.getIntProperty(
CarHvacManager.ID_ZONED_FAN_SPEED_SETPOINT, zone);
} catch (android.car.CarNotConnectedException e) {
Log.e(TAG, "Car not connected in setFanSpeed");
}
}
return null;
}
};
task.execute();
}
4. 總結(jié)
最后我們以一張從Car API的callback中的數(shù)據(jù)更新界面的偽時序圖來把Hvac的幾個核心組件串起來
以上就是車載空調(diào)部分的講解,實際開發(fā)中,空調(diào)模塊功能性需求一般不會出現(xiàn)什么太大的技術(shù)性困難,空調(diào)模塊的技術(shù)性難度幾乎都體現(xiàn)在復(fù)雜的動畫和交互上,有關(guān)車載應(yīng)用的復(fù)雜動畫技術(shù),我們以后在來細講解決方案。
相關(guān)文章
Android中兩個Activity之間數(shù)據(jù)傳遞及返回問題
本篇文章主要介紹了Android中兩個Activity之間數(shù)據(jù)傳遞及返回問題,這里整理了詳細的代碼,具有一定的參考價值,感興趣的小伙伴們可以參考一下。2017-02-02
詳解android項目由Gradle 2.2 切換到 3.0的坑
本篇文章主要介紹了詳解android項目由Gradle 2.2 切換到 3.0的坑,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02
Android liveData與viewBinding使用教程
LiveData是一種可觀察的數(shù)據(jù)存儲器類,LiveData使用觀察者模式,每當(dāng)數(shù)據(jù)發(fā)生變化時,LiveData會通知 Observer對象,我們可以在這些 Observer 對象中更新UI,ViewModel對象為特定的界面組件提供數(shù)據(jù),并包含數(shù)據(jù)處理業(yè)務(wù)邏輯,會配合LiveData一起使用2022-11-11
Android設(shè)置當(dāng)TextView中的文字超過TextView的容量時用省略號代替
這篇文章主要介紹了Android設(shè)置當(dāng)TextView中的文字超過TextView的容量時用省略號代替 ,需要的朋友可以參考下2017-03-03
Android布局耗時監(jiān)測的三種實現(xiàn)方式
在Android應(yīng)用開發(fā)中,性能優(yōu)化是一個至關(guān)重要的方面,為了更好地監(jiān)測布局渲染的耗時,我們需要一種可靠的實現(xiàn)方案,本文將介紹三種針對Android布局耗時監(jiān)測的實現(xiàn)方案,幫助開發(fā)者及時發(fā)現(xiàn)并解決布局性能問題,需要的朋友可以參考下2024-03-03

