1 /* 2 * Copyright (c) 2016, The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.car.hvac.ui; 17 18 import android.animation.Animator; 19 import android.animation.AnimatorSet; 20 import android.animation.ObjectAnimator; 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.drawable.GradientDrawable; 25 import android.util.AttributeSet; 26 import android.view.View; 27 import android.widget.FrameLayout; 28 import android.widget.ImageView; 29 import android.widget.TextView; 30 import com.android.car.hvac.R; 31 32 import java.util.ArrayList; 33 import java.util.List; 34 35 /** 36 * An expandable temperature control bar. Note this UI is meant to only support Fahrenheit. 37 */ 38 public class TemperatureBarOverlay extends FrameLayout { 39 /** 40 * A listener that observes clicks on the temperature bar. 41 */ 42 public interface TemperatureAdjustClickListener { onTemperatureChanged(int temperature)43 void onTemperatureChanged(int temperature); 44 } 45 46 private static final int EXPAND_ANIMATION_TIME_MS = 500; 47 private static final int COLLAPSE_ANIMATION_TIME_MS = 200; 48 49 private static final int TEXT_ALPHA_ANIMATION_TIME_DELAY_MS = 400; 50 private static final int TEXT_ALPHA_FADE_OUT_ANIMATION_TIME_MS = 100; 51 private static final int TEXT_ALPHA_FADE_IN_ANIMATION_TIME_MS = 300; 52 53 private static final int COLOR_CHANGE_ANIMATION_TIME_MS = 200; 54 55 private static final float BUTTON_ALPHA_COLLAPSED = 0f; 56 private static final float BUTTON_ALPHA_EXPANDED = 1.0f; 57 58 private static final int DEFAULT_TEMPERATURE = 32; 59 private static final int MAX_TEMPERATURE = 85; 60 private static final int MIN_TEMPERATURE = 60; 61 62 private String mInvalidTemperature; 63 64 private int mTempColor1; 65 private int mTempColor2; 66 private int mTempColor3; 67 private int mTempColor4; 68 private int mTempColor5; 69 70 private int mOffColor; 71 72 private ImageView mIncreaseButton; 73 private ImageView mDecreaseButton; 74 private TextView mText; 75 private TextView mFloatingText; 76 private TextView mOffText; 77 private View mTemperatureBar; 78 private View mCloseButton; 79 80 private int mTemperature = DEFAULT_TEMPERATURE; 81 82 private int mCollapsedWidth; 83 private int mExpandedWidth; 84 private int mCollapsedHeight; 85 private int mExpandedHeight; 86 private int mCollapsedYShift; 87 private int mExpandedYShift; 88 89 private boolean mIsOpen; 90 private boolean mIsOn = true; 91 92 private TemperatureAdjustClickListener mListener; 93 TemperatureBarOverlay(Context context)94 public TemperatureBarOverlay(Context context) { 95 super(context); 96 } 97 TemperatureBarOverlay(Context context, AttributeSet attrs)98 public TemperatureBarOverlay(Context context, AttributeSet attrs) { 99 super(context, attrs); 100 } 101 TemperatureBarOverlay(Context context, AttributeSet attrs, int defStyleAttr)102 public TemperatureBarOverlay(Context context, AttributeSet attrs, int defStyleAttr) { 103 super(context, attrs, defStyleAttr); 104 } 105 106 @Override onFinishInflate()107 protected void onFinishInflate() { 108 super.onFinishInflate(); 109 110 Resources res = getResources(); 111 112 mCollapsedHeight = res.getDimensionPixelSize(R.dimen.temperature_bar_collapsed_height); 113 mExpandedHeight = res.getDimensionPixelSize(R.dimen.temperature_bar_expanded_height); 114 // Push the collapsed circle all the way down to the bottom of the screen and leave 115 // half of it visible. 116 mCollapsedYShift 117 = res.getDimensionPixelSize(R.dimen.car_hvac_panel_full_expanded_height) 118 - (mCollapsedHeight / 2); 119 mExpandedYShift = res.getDimensionPixelSize(R.dimen.hvac_panel_row_margin); 120 121 mCollapsedWidth = res.getDimensionPixelSize(R.dimen.temperature_bar_width_collapsed); 122 mExpandedWidth = res.getDimensionPixelSize(R.dimen.temperature_bar_width_expanded); 123 124 mInvalidTemperature = getContext().getString(R.string.hvac_invalid_temperature); 125 126 mTempColor1 = res.getColor(R.color.temperature_1); 127 mTempColor2 = res.getColor(R.color.temperature_2); 128 mTempColor3 = res.getColor(R.color.temperature_3); 129 mTempColor4 = res.getColor(R.color.temperature_4); 130 mTempColor5 = res.getColor(R.color.temperature_5); 131 132 mOffColor = res.getColor(R.color.hvac_temperature_off_text_bg_color); 133 134 mIncreaseButton = (ImageView) findViewById(R.id.increase_button); 135 mDecreaseButton = (ImageView) findViewById(R.id.decrease_button); 136 137 mFloatingText = (TextView) findViewById(R.id.floating_temperature_text); 138 mText = (TextView) findViewById(R.id.temperature_text); 139 mOffText = (TextView) findViewById(R.id.temperature_off_text); 140 141 mTemperatureBar = findViewById(R.id.temperature_bar); 142 mTemperatureBar.setTranslationY(mCollapsedYShift); 143 144 mCloseButton = findViewById(R.id.close_button); 145 146 mText.setText(getContext().getString(R.string.hvac_temperature_template, 147 mInvalidTemperature)); 148 mFloatingText.setText(getContext() 149 .getString(R.string.hvac_temperature_template, 150 mInvalidTemperature)); 151 152 mIncreaseButton.setOnTouchListener(new PressAndHoldTouchListener(temperatureClickListener)); 153 mDecreaseButton.setOnTouchListener(new PressAndHoldTouchListener(temperatureClickListener)); 154 155 if (!mIsOpen) { 156 mIncreaseButton.setAlpha(BUTTON_ALPHA_COLLAPSED); 157 mDecreaseButton.setAlpha(BUTTON_ALPHA_COLLAPSED); 158 mText.setAlpha(BUTTON_ALPHA_COLLAPSED); 159 160 mDecreaseButton.setVisibility(GONE); 161 mIncreaseButton.setVisibility(GONE); 162 mText.setVisibility(GONE); 163 } 164 } 165 setTemperatureChangeListener(TemperatureAdjustClickListener listener)166 public void setTemperatureChangeListener(TemperatureAdjustClickListener listener) { 167 mListener = listener; 168 } 169 setBarOnClickListener(OnClickListener l)170 public void setBarOnClickListener(OnClickListener l) { 171 mFloatingText.setOnClickListener(l); 172 mTemperatureBar.setOnClickListener(l); 173 } 174 setCloseButtonOnClickListener(OnClickListener l)175 public void setCloseButtonOnClickListener(OnClickListener l) { 176 mCloseButton.setOnClickListener(l); 177 } 178 getExpandAnimatons()179 public AnimatorSet getExpandAnimatons() { 180 List<Animator> list = new ArrayList(); 181 AnimatorSet animation = new AnimatorSet(); 182 if (mIsOpen) { 183 return animation; 184 } 185 186 list.add(getAlphaAnimator(mIncreaseButton, false /* fade */, EXPAND_ANIMATION_TIME_MS)); 187 list.add(getAlphaAnimator(mDecreaseButton, false /* fade */, EXPAND_ANIMATION_TIME_MS)); 188 list.add(getAlphaAnimator(mText, false /* fade */, EXPAND_ANIMATION_TIME_MS)); 189 list.add(getAlphaAnimator(mFloatingText, true /* fade */, 190 TEXT_ALPHA_FADE_OUT_ANIMATION_TIME_MS)); 191 192 ValueAnimator widthAnimator = new ValueAnimator().ofInt(mCollapsedWidth, mExpandedWidth) 193 .setDuration(EXPAND_ANIMATION_TIME_MS); 194 widthAnimator.addUpdateListener(mWidthUpdateListener); 195 list.add(widthAnimator); 196 197 ValueAnimator heightAnimator = new ValueAnimator().ofInt(mCollapsedHeight, 198 mExpandedHeight) 199 .setDuration(EXPAND_ANIMATION_TIME_MS); 200 heightAnimator.addUpdateListener(mHeightUpdateListener); 201 list.add(heightAnimator); 202 203 204 ValueAnimator translationYAnimator 205 = new ValueAnimator().ofFloat(mCollapsedYShift, mExpandedYShift); 206 translationYAnimator.addUpdateListener(mTranslationYListener); 207 list.add(translationYAnimator); 208 209 animation.playTogether(list); 210 animation.addListener(mStateListener); 211 212 return animation; 213 } 214 getCollapseAnimations()215 public AnimatorSet getCollapseAnimations() { 216 217 List<Animator> list = new ArrayList(); 218 AnimatorSet animation = new AnimatorSet(); 219 220 if (!mIsOpen) { 221 return animation; 222 } 223 list.add(getAlphaAnimator(mIncreaseButton, true /* fade */, COLLAPSE_ANIMATION_TIME_MS)); 224 list.add(getAlphaAnimator(mDecreaseButton, true /* fade */, COLLAPSE_ANIMATION_TIME_MS)); 225 list.add(getAlphaAnimator(mText, true /* fade */, COLLAPSE_ANIMATION_TIME_MS)); 226 227 ObjectAnimator floatingTextAnimator = getAlphaAnimator(mFloatingText, 228 false /* fade */, TEXT_ALPHA_FADE_IN_ANIMATION_TIME_MS); 229 floatingTextAnimator.setStartDelay(TEXT_ALPHA_ANIMATION_TIME_DELAY_MS); 230 231 list.add(floatingTextAnimator); 232 233 ValueAnimator widthAnimator = new ValueAnimator().ofInt(mExpandedWidth, mCollapsedWidth) 234 .setDuration(COLLAPSE_ANIMATION_TIME_MS); 235 widthAnimator.addUpdateListener(mWidthUpdateListener); 236 list.add(widthAnimator); 237 238 ValueAnimator heightAnimator = new ValueAnimator().ofInt(mExpandedHeight, mCollapsedHeight) 239 .setDuration(COLLAPSE_ANIMATION_TIME_MS); 240 heightAnimator.addUpdateListener(mHeightUpdateListener); 241 list.add(heightAnimator); 242 243 ValueAnimator translationYAnimator 244 = new ValueAnimator().ofFloat(mExpandedYShift, mCollapsedYShift); 245 translationYAnimator.addUpdateListener(mTranslationYListener); 246 list.add(translationYAnimator); 247 248 animation.playTogether(list); 249 animation.addListener(mStateListener); 250 251 return animation; 252 } 253 254 private ValueAnimator.AnimatorListener mStateListener = new ValueAnimator.AnimatorListener() { 255 @Override 256 public void onAnimationStart(Animator animation) { 257 if (!mIsOpen) { 258 mDecreaseButton.setVisibility(VISIBLE); 259 mIncreaseButton.setVisibility(VISIBLE); 260 mText.setVisibility(VISIBLE); 261 mCloseButton.setVisibility(VISIBLE); 262 } else { 263 mCloseButton.setVisibility(GONE); 264 } 265 } 266 267 @Override 268 public void onAnimationEnd(Animator animation) { 269 if (mIsOpen) { 270 //Finished closing, make sure the buttons are now gone, 271 //so they are no longer touchable 272 mDecreaseButton.setVisibility(GONE); 273 mIncreaseButton.setVisibility(GONE); 274 mText.setVisibility(GONE); 275 mIsOpen = false; 276 } else { 277 //Finished opening 278 mIsOpen = true; 279 } 280 } 281 282 @Override 283 public void onAnimationCancel(Animator animation) { 284 } 285 286 @Override 287 public void onAnimationRepeat(Animator animation) { 288 } 289 }; 290 changeTemperatureColor(int startColor, int endColor)291 private void changeTemperatureColor(int startColor, int endColor) { 292 if (endColor != startColor) { 293 ValueAnimator animator = ValueAnimator.ofArgb(startColor, endColor); 294 animator.addUpdateListener(mTemperatureColorListener); 295 animator.setDuration(COLOR_CHANGE_ANIMATION_TIME_MS); 296 animator.start(); 297 } else { 298 ((GradientDrawable) mTemperatureBar.getBackground()).setColor(endColor); 299 } 300 } 301 302 private final View.OnClickListener temperatureClickListener = new View.OnClickListener() { 303 @Override 304 public void onClick(View v) { 305 int startColor = getTemperatureColor(mTemperature); 306 307 if (v == mIncreaseButton && mTemperature < MAX_TEMPERATURE) { 308 mTemperature++; 309 } else if (v == mDecreaseButton && mTemperature > MIN_TEMPERATURE) { 310 mTemperature--; 311 } 312 int endColor = getTemperatureColor(mTemperature); 313 changeTemperatureColor(startColor, endColor); 314 315 mText.setText(getContext().getString(R.string.hvac_temperature_template, mTemperature)); 316 mFloatingText.setText(getContext() 317 .getString(R.string.hvac_temperature_template, mTemperature)); 318 mListener.onTemperatureChanged(mTemperature); 319 } 320 }; 321 setTemperature(int temperature)322 public void setTemperature(int temperature) { 323 int startColor = getTemperatureColor(mTemperature); 324 int endColor = getTemperatureColor(temperature); 325 mTemperature = temperature; 326 String temperatureString; 327 328 if (mTemperature < MIN_TEMPERATURE || mTemperature > MAX_TEMPERATURE) { 329 temperatureString = mInvalidTemperature; 330 } else { 331 temperatureString = String.valueOf(mTemperature); 332 } 333 334 mText.setText(getContext().getString(R.string.hvac_temperature_template, 335 temperatureString)); 336 mFloatingText.setText(getContext() 337 .getString(R.string.hvac_temperature_template, temperatureString)); 338 339 // Only animate the color if the button is currently enabled. 340 if (mIsOn) { 341 changeTemperatureColor(startColor, endColor); 342 } 343 } 344 345 /** 346 * Sets whether or not the temperature bar is on. If it is off, it should show "off" instead 347 * of the temperature. 348 */ setIsOn(boolean isOn)349 public void setIsOn(boolean isOn) { 350 mIsOn = isOn; 351 GradientDrawable temperatureBall 352 = (GradientDrawable) mTemperatureBar.getBackground(); 353 if (mIsOn) { 354 mFloatingText.setVisibility(VISIBLE); 355 mOffText.setVisibility(GONE); 356 temperatureBall.setColor(getTemperatureColor(mTemperature)); 357 setAlpha(1.0f); 358 } else { 359 mOffText.setVisibility(VISIBLE); 360 mFloatingText.setVisibility(GONE); 361 temperatureBall.setColor(mOffColor); 362 setAlpha(.2f); 363 } 364 } 365 getTemperatureColor(int temperature)366 private int getTemperatureColor(int temperature) { 367 if (temperature >= 78) { 368 return mTempColor1; 369 } else if (temperature >= 74 && temperature < 78) { 370 return mTempColor2; 371 } else if (temperature >= 70 && temperature < 74) { 372 return mTempColor3; 373 } else if (temperature >= 66 && temperature < 70) { 374 return mTempColor4; 375 } else { 376 return mTempColor5; 377 } 378 } 379 380 private final ValueAnimator.AnimatorUpdateListener mTranslationYListener 381 = new ValueAnimator.AnimatorUpdateListener() { 382 @Override 383 public void onAnimationUpdate(ValueAnimator animation) { 384 float translation = (float) animation.getAnimatedValue(); 385 mTemperatureBar.setTranslationY(translation); 386 } 387 }; 388 389 private final ValueAnimator.AnimatorUpdateListener mWidthUpdateListener 390 = new ValueAnimator.AnimatorUpdateListener() { 391 @Override 392 public void onAnimationUpdate(ValueAnimator animation) { 393 int width = (Integer) animation.getAnimatedValue(); 394 mTemperatureBar.getLayoutParams().width = width; 395 mTemperatureBar.requestLayout(); 396 } 397 }; 398 399 private final ValueAnimator.AnimatorUpdateListener mHeightUpdateListener 400 = new ValueAnimator.AnimatorUpdateListener() { 401 @Override 402 public void onAnimationUpdate(ValueAnimator animation) { 403 int height = (Integer) animation.getAnimatedValue(); 404 int currentHeight = mTemperatureBar.getLayoutParams().height; 405 mTemperatureBar.getLayoutParams().height = height; 406 mTemperatureBar.setTop(mTemperatureBar.getTop() + height - currentHeight); 407 mTemperatureBar.requestLayout(); 408 } 409 }; 410 411 private final ValueAnimator.AnimatorUpdateListener mTemperatureColorListener 412 = new ValueAnimator.AnimatorUpdateListener() { 413 @Override 414 public void onAnimationUpdate(ValueAnimator animation) { 415 int color = (Integer) animation.getAnimatedValue(); 416 ((GradientDrawable) mTemperatureBar.getBackground()).setColor(color); 417 } 418 }; 419 getAlphaAnimator(View view, boolean fade)420 private ObjectAnimator getAlphaAnimator(View view, boolean fade) { 421 return getAlphaAnimator(view, fade, EXPAND_ANIMATION_TIME_MS); 422 } 423 getAlphaAnimator(View view, boolean fade, int duration)424 private ObjectAnimator getAlphaAnimator(View view, boolean fade, int duration) { 425 426 float startingAlpha = BUTTON_ALPHA_COLLAPSED; 427 float endingAlpha = BUTTON_ALPHA_EXPANDED; 428 429 if (fade) { 430 startingAlpha = BUTTON_ALPHA_EXPANDED; 431 endingAlpha = BUTTON_ALPHA_COLLAPSED; 432 } 433 434 return ObjectAnimator.ofFloat(view, View.ALPHA, 435 startingAlpha, endingAlpha).setDuration(duration); 436 } 437 } 438