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