1 /*
2  * Copyright (c) 2018 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 
17 package com.android.systemui.statusbar.hvac;
18 
19 import android.animation.ObjectAnimator;
20 import android.annotation.SuppressLint;
21 import android.content.Context;
22 import android.content.res.TypedArray;
23 import android.graphics.Color;
24 import android.graphics.Rect;
25 import android.graphics.drawable.ColorDrawable;
26 import android.util.AttributeSet;
27 import android.util.Property;
28 import android.view.Gravity;
29 import android.view.View;
30 import android.view.ViewGroup;
31 import android.view.ViewTreeObserver;
32 import android.widget.FrameLayout;
33 import android.widget.ImageView;
34 import android.widget.TextSwitcher;
35 import android.widget.TextView;
36 
37 import com.android.systemui.R;
38 import com.android.systemui.statusbar.car.hvac.TemperatureView;
39 
40 /**
41  * Simple text display of HVAC properties, It is designed to show mTemperature and is configured in
42  * the XML.
43  * XML properties:
44  * hvacPropertyId - Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
45  * hvacAreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
46  * hvacTempFormat - Example: "%.1f\u00B0" (1 decimal and the degree symbol)
47  * hvacOrientaion = Example: left
48  */
49 public class AnimatedTemperatureView extends FrameLayout implements TemperatureView {
50 
51     private static final float TEMPERATURE_EQUIVALENT_DELTA = .01f;
52     private static final Property<ColorDrawable, Integer> COLOR_PROPERTY =
53             new Property<ColorDrawable, Integer>(Integer.class, "color") {
54 
55                 @Override
56                 public Integer get(ColorDrawable object) {
57                     return object.getColor();
58                 }
59 
60                 @Override
61                 public void set(ColorDrawable object, Integer value) {
62                     object.setColor(value);
63                 }
64             };
65 
isHorizontal(int gravity)66     static boolean isHorizontal(int gravity) {
67         return Gravity.isHorizontal(gravity)
68                 && (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.CENTER_HORIZONTAL;
69     }
70 
71     @SuppressLint("RtlHardcoded")
isLeft(int gravity, int layoutDirection)72     static boolean isLeft(int gravity, int layoutDirection) {
73         return Gravity
74                 .getAbsoluteGravity(gravity & Gravity.HORIZONTAL_GRAVITY_MASK, layoutDirection)
75                 == Gravity.LEFT;
76     }
77 
isVertical(int gravity)78     static boolean isVertical(int gravity) {
79         return Gravity.isVertical(gravity)
80                 && (gravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.CENTER_VERTICAL;
81     }
82 
isTop(int gravity)83     static boolean isTop(int gravity) {
84         return (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP;
85     }
86 
87     private final int mAreaId;
88     private final int mPropertyId;
89     private final int mPivotOffset;
90     private final int mGravity;
91     private final int mTextAppearanceRes;
92     private final int mMinEms;
93     private final Rect mPaddingRect;
94     private final float mMinValue;
95     private final float mMaxValue;
96 
97     private final ColorDrawable mBackgroundColor;
98 
99     private final TemperatureColorStore mColorStore = new TemperatureColorStore();
100     private final TemperatureBackgroundAnimator mBackgroundAnimator;
101     private final TemperatureTextAnimator mTextAnimator;
102     boolean mDisplayInFahrenheit = false;
103 
AnimatedTemperatureView(Context context, AttributeSet attrs)104     public AnimatedTemperatureView(Context context, AttributeSet attrs) {
105         super(context, attrs);
106         TypedArray typedArray = context.obtainStyledAttributes(attrs,
107                 R.styleable.AnimatedTemperatureView);
108         mAreaId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacAreaId, -1);
109         mPropertyId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacPropertyId, -1);
110         mPivotOffset =
111                 typedArray.getDimensionPixelOffset(
112                         R.styleable.AnimatedTemperatureView_hvacPivotOffset, 0);
113         mGravity = typedArray.getInt(R.styleable.AnimatedTemperatureView_android_gravity,
114                 Gravity.START);
115         mTextAppearanceRes =
116                 typedArray.getResourceId(R.styleable.AnimatedTemperatureView_android_textAppearance,
117                         0);
118         mMinEms = typedArray.getInteger(R.styleable.AnimatedTemperatureView_android_minEms, 0);
119         mMinValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMinValue,
120                 Float.NaN);
121         mMaxValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMaxValue,
122                 Float.NaN);
123 
124 
125         mPaddingRect =
126                 new Rect(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
127         setPadding(0, 0, 0, 0);
128 
129         setClipChildren(false);
130         setClipToPadding(false);
131 
132         // init Views
133         TextSwitcher textSwitcher = new TextSwitcher(context);
134         textSwitcher.setFactory(this::generateTextView);
135         ImageView background = new ImageView(context);
136         mBackgroundColor = new ColorDrawable(Color.TRANSPARENT);
137         background.setImageDrawable(mBackgroundColor);
138         background.setVisibility(View.GONE);
139 
140         mBackgroundAnimator = new TemperatureBackgroundAnimator(this, background);
141 
142 
143         String format = typedArray.getString(R.styleable.AnimatedTemperatureView_hvacTempFormat);
144         format = (format == null) ? "%.1f\u00B0" : format;
145         CharSequence minText = typedArray.getString(
146                 R.styleable.AnimatedTemperatureView_hvacMinText);
147         CharSequence maxText = typedArray.getString(
148                 R.styleable.AnimatedTemperatureView_hvacMaxText);
149         mTextAnimator = new TemperatureTextAnimator(this, textSwitcher, format, mPivotOffset,
150                 minText, maxText);
151 
152         addView(background, ViewGroup.LayoutParams.MATCH_PARENT,
153                 ViewGroup.LayoutParams.MATCH_PARENT);
154         addView(textSwitcher, ViewGroup.LayoutParams.MATCH_PARENT,
155                 ViewGroup.LayoutParams.MATCH_PARENT);
156 
157         typedArray.recycle();
158     }
159 
160 
generateTextView()161     private TextView generateTextView() {
162         TextView textView = new TextView(getContext());
163         textView.setTextAppearance(mTextAppearanceRes);
164         textView.setAllCaps(true);
165         textView.setMinEms(mMinEms);
166         textView.setGravity(mGravity);
167         textView.setPadding(mPaddingRect.left, mPaddingRect.top, mPaddingRect.right,
168                 mPaddingRect.bottom);
169         textView.getViewTreeObserver()
170                 .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
171                     @Override
172                     public boolean onPreDraw() {
173                         if (isHorizontal(mGravity)) {
174                             if (isLeft(mGravity, getLayoutDirection())) {
175                                 textView.setPivotX(-mPivotOffset);
176                             } else {
177                                 textView.setPivotX(textView.getWidth() + mPivotOffset);
178                             }
179                         }
180                         textView.getViewTreeObserver().removeOnPreDrawListener(this);
181                         return true;
182                     }
183                 });
184         textView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
185                 ViewGroup.LayoutParams.MATCH_PARENT));
186 
187         return textView;
188     }
189 
190     /**
191      * Formats the float for display
192      *
193      * @param temp - The current temp or NaN
194      */
195     @Override
setTemp(float temp)196     public void setTemp(float temp) {
197         if (mDisplayInFahrenheit) {
198             temp = convertToFahrenheit(temp);
199         }
200         mTextAnimator.setTemp(temp);
201         if (Float.isNaN(temp)) {
202             mBackgroundAnimator.hideCircle();
203             return;
204         }
205         int color;
206         if (isMinValue(temp)) {
207             color = mColorStore.getMinColor();
208         } else if (isMaxValue(temp)) {
209             color = mColorStore.getMaxColor();
210         } else {
211             color = mColorStore.getColorForTemperature(temp);
212         }
213         if (mBackgroundAnimator.isOpen()) {
214             ObjectAnimator colorAnimator =
215                     ObjectAnimator.ofInt(mBackgroundColor, COLOR_PROPERTY, color);
216             colorAnimator.setEvaluator((fraction, startValue, endValue) -> mColorStore
217                     .lerpColor(fraction, (int) startValue, (int) endValue));
218             colorAnimator.start();
219         } else {
220             mBackgroundColor.setColor(color);
221         }
222 
223         mBackgroundAnimator.animateOpen();
224     }
225 
226     @Override
setDisplayInFahrenheit(boolean displayInFahrenheit)227     public void setDisplayInFahrenheit(boolean displayInFahrenheit) {
228         mDisplayInFahrenheit = displayInFahrenheit;
229     }
230 
isMinValue(float temp)231     boolean isMinValue(float temp) {
232         return !Float.isNaN(mMinValue) && isApproxEqual(temp, mMinValue);
233     }
234 
isMaxValue(float temp)235     boolean isMaxValue(float temp) {
236         return !Float.isNaN(mMaxValue) && isApproxEqual(temp, mMaxValue);
237     }
238 
isApproxEqual(float left, float right)239     private boolean isApproxEqual(float left, float right) {
240         return Math.abs(left - right) <= TEMPERATURE_EQUIVALENT_DELTA;
241     }
242 
getGravity()243     int getGravity() {
244         return mGravity;
245     }
246 
getPivotOffset()247     int getPivotOffset() {
248         return mPivotOffset;
249     }
250 
getPaddingRect()251     Rect getPaddingRect() {
252         return mPaddingRect;
253     }
254 
255     /**
256      * @return propertiyId  Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (358614275)
257      */
258     @Override
getPropertyId()259     public int getPropertyId() {
260         return mPropertyId;
261     }
262 
263     /**
264      * @return hvac AreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
265      */
266     @Override
getAreaId()267     public int getAreaId() {
268         return mAreaId;
269     }
270 
271     @Override
onDetachedFromWindow()272     protected void onDetachedFromWindow() {
273         super.onDetachedFromWindow();
274         mBackgroundAnimator.stopAnimations();
275     }
276 
277 }
278 
279