/* * 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 animatorList, List listenerList, AnimatorSet set) { ArrayList list = set.getChildAnimations(); if (list != null) { int size = list.size(); for (int i = 0; i < size; i++) { animatorList.add(list.get(i)); } } ArrayList 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 animationList = new ArrayList<>(); List 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; } } }