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