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