blob: dbc4cacc206c515196cad9ffcf4c0407f2c7831b [file] [log] [blame]
/*
* Copyright (c) 2016, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.car.hvac.controllers;
import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.WindowManager;
import android.widget.ImageView;
import androidx.annotation.IntDef;
import com.android.car.hvac.HvacController;
import com.android.car.hvac.R;
import com.android.car.hvac.ui.FanDirectionButtons;
import com.android.car.hvac.ui.FanSpeedBar;
import com.android.car.hvac.ui.HvacPanelRow;
import com.android.car.hvac.ui.SeatWarmerButton;
import com.android.car.hvac.ui.TemperatureBarOverlay;
import com.android.car.hvac.ui.ToggleButton;
import java.util.ArrayList;
import java.util.List;
/**
* A state machine to control transition from various HVAC UI layouts.
*/
public class HvacPanelController {
private static final int PANEL_ANIMATION_TIME_MS = 200;
private static final int PANEL_COLLAPSE_ANIMATION_TIME_MS = 500;
private static final int PANEL_ANIMATION_DELAY_MS = 100;
private static final int PANEL_ANIMATION_LONG_DELAY_MS = 3 * PANEL_ANIMATION_DELAY_MS;
private static final float DISABLED_BUTTON_ALPHA = 0.20f;
private static final float ENABLED_BUTTON_ALPHA = 1.0f;
private static final int STATE_COLLAPSED = 0;
private static final int STATE_COLLAPSED_DIMMED = 1;
private static final int STATE_FULL_EXPANDED = 2;
// Allows for delayed invocation of code. Thus we can control UI events to happen after
// others. Example: set something visible but do it after we've complete current UI updates.
private static Handler handler = new Handler();
// We have both a collapsed and expanded version of the overlays due to a bug
// that does not correctly rendering a window resize event. Thus we toggle the the visibility
// of the windows instead. A better solution would be a having separate views collapsed state
// since the it does not need the other elements but this works for now.
private TemperatureBarOverlay mDriverTemperatureBarCollapsed;
private TemperatureBarOverlay mPassengerTemperatureBarCollapsed;
private TemperatureBarOverlay mDriverTemperatureBarExpanded;
private TemperatureBarOverlay mPassengerTemperatureBarExpanded;
private final boolean mShowCollapsed;
@IntDef({STATE_COLLAPSED,
STATE_COLLAPSED_DIMMED,
STATE_FULL_EXPANDED})
private @interface HvacPanelState {}
private @HvacPanelState int mCurrentState;
private int mPanelCollapsedHeight;
private int mPanelFullExpandedHeight;
private View mPanel;
private View mContainer;
private SeatWarmerButton mDriverSeatWarmer;
private SeatWarmerButton mPassengerSeatWarmer;
private ToggleButton mHvacPowerSwitch;
private ToggleButton mAcButton;
private ToggleButton mRecycleAirButton;
private ToggleButton mFrontDefrosterButton;
private ToggleButton mRearDefrosterButton;
private Drawable mAutoOnDrawable;
private Drawable mAutoOffDrawable;
private ImageView mAutoButton;
private HvacPanelRow mPanelTopRow;
private HvacPanelRow mPanelBottomRow;
private FanSpeedBar mFanSpeedBar;
private FanDirectionButtons mFanDirectionButtons;
private float mTopPanelMaxAlpha = 1.0f;
private WindowManager mWindowManager;
private HvacPanelStateTransition mTransition;
private View mHvacFanControlBackground;
private HvacController mHvacController;
private FanSpeedBarController mFanSpeedBarController;
private FanDirectionButtonsController mFanDirectionButtonsController;
private TemperatureController mTemperatureController;
private TemperatureController mTemperatureControllerCollapsed;
private SeatWarmerController mSeatWarmerController;
private boolean mInAnimation;
// TODO: read from shared pref
private boolean mAutoMode;
public HvacPanelController(Context context, View container,
WindowManager windowManager,
TemperatureBarOverlay driverTemperatureExpanded,
TemperatureBarOverlay passengerTemperatureExpanded,
TemperatureBarOverlay driverTemperatureBarCollapsed,
TemperatureBarOverlay passengerTemperatureBarCollapsed) {
Resources res = context.getResources();
mShowCollapsed = res.getBoolean(R.bool.config_showCollapsedBars);
mDriverTemperatureBarCollapsed = driverTemperatureBarCollapsed;
mPassengerTemperatureBarCollapsed = passengerTemperatureBarCollapsed;
mCurrentState = STATE_COLLAPSED;
mWindowManager = windowManager;
mPanelCollapsedHeight = res.getDimensionPixelSize(R.dimen.car_hvac_panel_collapsed_height);
mPanelFullExpandedHeight
= res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height);
mAutoOffDrawable = res.getDrawable(R.drawable.ic_auto_off);
mAutoOnDrawable = res.getDrawable(R.drawable.ic_auto_on);
mDriverTemperatureBarExpanded = driverTemperatureExpanded;
mPassengerTemperatureBarExpanded = passengerTemperatureExpanded;
mDriverTemperatureBarExpanded.setCloseButtonOnClickListener(mCollapseHvac);
mPassengerTemperatureBarExpanded.setCloseButtonOnClickListener(mCollapseHvac);
// Initially the hvac panel is collapsed, hide the expanded version.
mDriverTemperatureBarExpanded.setVisibility(View.INVISIBLE);
mPassengerTemperatureBarExpanded.setVisibility(View.INVISIBLE);
mPassengerTemperatureBarCollapsed.setBarOnClickListener(mExpandHvac);
mDriverTemperatureBarCollapsed.setBarOnClickListener(mExpandHvac);
mContainer = container;
mContainer.setVisibility(View.INVISIBLE);
mContainer.setOnClickListener(mCollapseHvac);
mPanel = mContainer.findViewById(R.id.hvac_center_panel);
mHvacFanControlBackground = mPanel.findViewById(R.id.fan_control_bg);
// set clickable so that clicks are not forward to the mContainer. This way a miss click
// does not close the UI
mPanel.setClickable(true);
// Set up top row buttons
mPanelTopRow = (HvacPanelRow) mContainer.findViewById(R.id.top_row);
mAcButton = (ToggleButton) mPanelTopRow.findViewById(R.id.ac_button);
mAcButton.setToggleIcons(res.getDrawable(R.drawable.ic_ac_on),
res.getDrawable(R.drawable.ic_ac_off));
mRecycleAirButton = (ToggleButton) mPanelTopRow.findViewById(R.id.recycle_air_button);
mRecycleAirButton.setToggleIcons(res.getDrawable(R.drawable.ic_recycle_air_on),
res.getDrawable(R.drawable.ic_recycle_air_off));
// Setup bottom row buttons
mPanelBottomRow = (HvacPanelRow) mContainer.findViewById(R.id.bottom_row);
mAutoButton = (ImageView) mContainer.findViewById(R.id.auto_button);
mAutoButton.setOnClickListener(mAutoButtonClickListener);
mFrontDefrosterButton = (ToggleButton) mPanelBottomRow.findViewById(R.id.front_defroster);
mRearDefrosterButton = (ToggleButton) mPanelBottomRow.findViewById(R.id.rear_defroster);
mFrontDefrosterButton.setToggleIcons(res.getDrawable(R.drawable.ic_front_defroster_on),
res.getDrawable(R.drawable.ic_front_defroster_off));
mRearDefrosterButton.setToggleIcons(res.getDrawable(R.drawable.ic_rear_defroster_on),
res.getDrawable(R.drawable.ic_rear_defroster_off));
mFanSpeedBar = (FanSpeedBar) mContainer.findViewById(R.id.fan_speed_bar);
mFanDirectionButtons = (FanDirectionButtons) mContainer.findViewById(R.id.fan_direction_buttons);
mDriverSeatWarmer = (SeatWarmerButton) mContainer.findViewById(R.id.left_seat_heater);
mPassengerSeatWarmer = (SeatWarmerButton) mContainer.findViewById(R.id.right_seat_heater);
mHvacPowerSwitch = (ToggleButton)mPanelBottomRow.findViewById(R.id.hvac_master_switch);
// TODO: this is not good UX design - just a placeholder
mHvacPowerSwitch.setToggleIcons(res.getDrawable(R.drawable.ac_master_switch_on),
res.getDrawable(R.drawable.ac_master_switch_off));
if (!mShowCollapsed) {
mDriverTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
mPassengerTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
}
}
public void updateHvacController(HvacController controller) {
//TODO: handle disconnected HvacController.
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);
// Toggle buttons do not need additional logic to map between hardware
// and UI settings. Simply use a ToggleListener to handle clicks.
mAcButton.setIsOn(mHvacController.getAcState());
mAcButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setAcState(isOn);
}
});
mFrontDefrosterButton.setIsOn(mHvacController.getFrontDefrosterState());
mFrontDefrosterButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setFrontDefrosterState(isOn);
}
});
mRearDefrosterButton.setIsOn(mHvacController.getRearDefrosterState());
mRearDefrosterButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setRearDefrosterState(isOn);
}
});
mRecycleAirButton.setIsOn(mHvacController.getAirCirculationState());
mRecycleAirButton.setToggleListener(new ToggleButton.ToggleListener() {
@Override
public void onToggled(boolean isOn) {
mHvacController.setAirCirculation(isOn);
}
});
setAutoMode(mHvacController.getAutoModeState());
mHvacPowerSwitch.setIsOn(mHvacController.getHvacPowerState());
mHvacPowerSwitch.setToggleListener(isOn -> mHvacController.setHvacPowerState(isOn));
mHvacController.registerCallback(mToggleButtonCallbacks);
mToggleButtonCallbacks.onHvacPowerChange(mHvacController.getHvacPowerState());
}
private HvacController.Callback mToggleButtonCallbacks
= new HvacController.Callback() {
@Override
public void onAirCirculationChange(boolean isOn) {
mRecycleAirButton.setIsOn(isOn);
}
@Override
public void onFrontDefrosterChange(boolean isOn) {
mFrontDefrosterButton.setIsOn(isOn);
}
@Override
public void onRearDefrosterChange(boolean isOn) {
mRearDefrosterButton.setIsOn(isOn);
}
@Override
public void onAcStateChange(boolean isOn) {
mAcButton.setIsOn(isOn);
}
@Override
public void onAutoModeChange(boolean isOn) {
mAutoMode = isOn;
setAutoMode(mAutoMode);
}
};
/**
* Take the listeners and animators from a {@link AnimatorSet} and merge them to the
* input {@link Animator} and {@link android.animation.Animator.AnimatorListener} lists.
*/
private void combineAnimationSet(List<Animator> animatorList,
List<Animator.AnimatorListener> listenerList, AnimatorSet set) {
ArrayList<Animator> list = set.getChildAnimations();
if (list != null) {
int size = list.size();
for (int i = 0; i < size; i++) {
animatorList.add(list.get(i));
}
}
ArrayList<Animator.AnimatorListener> listeners = set.getListeners();
if (listeners != null) {
int size = listeners.size();
for (int i = 0; i < size; i++) {
listenerList.add(listeners.get(i));
}
}
}
/**
* Play necessary animations between {@link HvacPanelState} transitions
*/
private void transitionState(@HvacPanelState int startState, @HvacPanelState int endState) {
if (startState == endState || mInAnimation) {
return;
}
List<Animator> animationList = new ArrayList<>();
List<Animator.AnimatorListener> listenerList = new ArrayList<>();
ValueAnimator heightAnimator = getPanelHeightAnimator(startState, endState);
mTransition = new HvacPanelStateTransition(startState, endState);
ValueAnimator fanBgAlphaAnimator;
switch (endState) {
case STATE_COLLAPSED:
// Transition to collapsed state:
// 1. Collapse the temperature bars.
// 2. Collapse the top and bottom panel, staggered with a different delay.
// 3. Decrease height of the hvac center panel, but maintain container height.
// 4. Fade the background of the fan controls seperately to create staggered effect.
animationList.add(heightAnimator);
heightAnimator.setDuration(PANEL_COLLAPSE_ANIMATION_TIME_MS);
fanBgAlphaAnimator
= ObjectAnimator.ofFloat(mHvacFanControlBackground, View.ALPHA, 1, 0)
.setDuration(PANEL_COLLAPSE_ANIMATION_TIME_MS);
fanBgAlphaAnimator.setStartDelay(PANEL_ANIMATION_DELAY_MS);
animationList.add(fanBgAlphaAnimator);
ValueAnimator panelAlphaAnimator
= ObjectAnimator.ofFloat(mContainer, View.ALPHA, 1, 0);
panelAlphaAnimator.setDuration(200);
panelAlphaAnimator.setStartDelay(300);
animationList.add(panelAlphaAnimator);
combineAnimationSet(animationList, listenerList,
mDriverTemperatureBarExpanded.getCollapseAnimations());
combineAnimationSet(animationList, listenerList,
mPassengerTemperatureBarExpanded.getCollapseAnimations());
combineAnimationSet(animationList, listenerList,
mPanelTopRow.getCollapseAnimation(PANEL_ANIMATION_DELAY_MS,
mTopPanelMaxAlpha));
combineAnimationSet(animationList, listenerList,
mPanelBottomRow.getCollapseAnimation(PANEL_ANIMATION_DELAY_MS,
mTopPanelMaxAlpha));
break;
case STATE_COLLAPSED_DIMMED:
// Hide the temperature numbers, open arrows and auto state button.
// TODO: determine if this section is still needed.
break;
case STATE_FULL_EXPANDED:
// Transition to expaneded state:
// 1. Expand the temperature bars.
// 2. Expand the top and bottom panel, staggered with a different delay.
// 3. Increase height of the hvac center panel, but maintain container height.
// 4. Fade in fan control background in a staggered manner.
fanBgAlphaAnimator
= ObjectAnimator.ofFloat(mHvacFanControlBackground, View.ALPHA, 0, 1)
.setDuration(PANEL_ANIMATION_TIME_MS);
fanBgAlphaAnimator.setStartDelay(PANEL_ANIMATION_DELAY_MS);
animationList.add(fanBgAlphaAnimator);
animationList.add(heightAnimator);
combineAnimationSet(animationList, listenerList,
mDriverTemperatureBarExpanded.getExpandAnimatons());
combineAnimationSet(animationList, listenerList,
mPassengerTemperatureBarExpanded.getExpandAnimatons());
// During expansion, the bottom panel animation should be delayed
combineAnimationSet(animationList, listenerList,
mPanelTopRow.getExpandAnimation(PANEL_ANIMATION_DELAY_MS,
mTopPanelMaxAlpha));
combineAnimationSet(animationList, listenerList,
mPanelBottomRow.getExpandAnimation(PANEL_ANIMATION_LONG_DELAY_MS, 1f));
break;
default:
}
// If there are animations for the state change, play them all together and ensure
// the animation listeners are attached.
if (animationList.size() > 0) {
AnimatorSet animatorSet = new AnimatorSet();
animatorSet.playTogether(animationList);
for (Animator.AnimatorListener listener : listenerList) {
animatorSet.addListener(listener);
}
animatorSet.addListener(mAnimatorListener);
animatorSet.start();
}
}
private AnimatorSet.AnimatorListener mAnimatorListener = new AnimatorSet.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
mTransition.onTransitionStart();
}
@Override
public void onAnimationEnd(Animator animation) {
mTransition.onTransitionComplete();
}
@Override
public void onAnimationCancel(Animator animation) {}
@Override
public void onAnimationRepeat(Animator animation) {}
};
private ValueAnimator getPanelHeightAnimator(@HvacPanelState int startState,
@HvacPanelState int endState) {
int startHeight = getStateHeight(startState);
int endHeight = getStateHeight(endState);
if (startHeight == endHeight) {
return null;
}
ValueAnimator heightAnimator = new ValueAnimator().ofInt(startHeight, endHeight)
.setDuration(PANEL_ANIMATION_TIME_MS);
heightAnimator.addUpdateListener(mHeightUpdateListener);
return heightAnimator;
}
private int getStateHeight(@HvacPanelState int state) {
switch (state) {
case STATE_COLLAPSED:
case STATE_COLLAPSED_DIMMED:
return mPanelCollapsedHeight;
case STATE_FULL_EXPANDED:
return mPanelFullExpandedHeight;
default:
throw new IllegalArgumentException("No height mapped to HVAC State: " + state);
}
}
private void setAutoMode(boolean isOn) {
if (isOn) {
mPanelTopRow.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return true;
}
});
mAutoMode = true;
mPanelTopRow.disablePanel(true);
mTopPanelMaxAlpha = DISABLED_BUTTON_ALPHA;
mAutoButton.setImageDrawable(mAutoOnDrawable);
} else {
mPanelTopRow.disablePanel(false);
mTopPanelMaxAlpha = ENABLED_BUTTON_ALPHA;
mAutoButton.setImageDrawable(mAutoOffDrawable);
}
mHvacFanControlBackground.setAlpha(mTopPanelMaxAlpha);
mPanelTopRow.setAlpha(mTopPanelMaxAlpha);
}
private View.OnClickListener mAutoButtonClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mAutoMode) {
mAutoMode = false;
} else {
mAutoMode = true;
}
mHvacController.setAutoMode(mAutoMode);
setAutoMode(mAutoMode);
}
};
private View.OnClickListener mCollapseHvac = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mInAnimation) {
return;
}
if (mCurrentState != STATE_COLLAPSED) {
transitionState(mCurrentState, STATE_COLLAPSED);
}
}
};
public void toggleHvacUi() {
if(mCurrentState != STATE_COLLAPSED) {
mCollapseHvac.onClick(null);
} else {
mExpandHvac.onClick(null);
}
}
public View.OnClickListener mExpandHvac = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mInAnimation) {
return;
}
if (mCurrentState != STATE_FULL_EXPANDED) {
transitionState(mCurrentState, STATE_FULL_EXPANDED);
}
}
};
private ValueAnimator.AnimatorUpdateListener mHeightUpdateListener
= new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
int height = (Integer) animation.getAnimatedValue();
int currentHeight = mPanel.getLayoutParams().height;
mPanel.getLayoutParams().height = height;
mPanel.setTop(mPanel.getTop() + height - currentHeight);
mPanel.requestLayout();
}
};
/**
* Handles the necessary setup/clean up before and after a state transition.
*/
private class HvacPanelStateTransition {
private @HvacPanelState int mEndState;
private @HvacPanelState int mStartState;
public HvacPanelStateTransition(@HvacPanelState int startState,
@HvacPanelState int endState) {
mStartState = startState;
mEndState = endState;
}
public void onTransitionStart() {
mInAnimation = true;
if (mEndState == STATE_FULL_EXPANDED) {
mPassengerTemperatureBarExpanded.setVisibility(View.VISIBLE);
mDriverTemperatureBarExpanded.setVisibility(View.VISIBLE);
if (mShowCollapsed) {
mDriverTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
mPassengerTemperatureBarCollapsed.setVisibility(View.INVISIBLE);
}
mContainer.setAlpha(1);
mContainer.setVisibility(View.VISIBLE);
}
}
public void onTransitionComplete() {
if (mEndState == STATE_COLLAPSED) {
if (mShowCollapsed) {
mDriverTemperatureBarCollapsed.setVisibility(View.VISIBLE);
mPassengerTemperatureBarCollapsed.setVisibility(View.VISIBLE);
}
handler.postAtFrontOfQueue(() -> {
mDriverTemperatureBarExpanded.setVisibility(View.INVISIBLE);
mPassengerTemperatureBarExpanded.setVisibility(View.INVISIBLE);
});
}
if (mStartState == STATE_FULL_EXPANDED) {
mContainer.setVisibility(View.INVISIBLE);
}
// set new states
mCurrentState = mEndState;
mInAnimation = false;
}
}
}