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