Initial include of the CarSystemUI package
This package extends the core SystemUI and adds widgets and windowing structure useful for the Automotive case. Bug:110275838 Test: Update a target to include this project, build and deploy Change-Id: Id70ebe4ed652d6a09ab50b0555fb4471bf3e1d7c
This commit is contained in:
86
packages/CarSystemUI/Android.mk
Normal file
86
packages/CarSystemUI/Android.mk
Normal file
@@ -0,0 +1,86 @@
|
||||
# LOCAL_PATH is not the current directory of this file.
|
||||
# If you need the local path, use CAR_SYSUI_PATH.
|
||||
# This tweak ensures that the resource overlay that is device-specific still works
|
||||
# which requires that LOCAL_PATH match the original path (which must be frameworks/base/packages/SystemUI).
|
||||
#
|
||||
# For clarity, we also define SYSTEM_UI_AOSP_PATH to frameworks/base/packages/SystemUI and refer to that
|
||||
SYSTEM_UI_AOSP_PATH := frameworks/base/packages/SystemUI
|
||||
SYSTEM_UI_CAR_PATH := frameworks/base/packages/CarSystemUI
|
||||
LOCAL_PATH := $(call my-dir)
|
||||
|
||||
include $(CLEAR_VARS)
|
||||
|
||||
LOCAL_USE_AAPT2 := true
|
||||
|
||||
LOCAL_MODULE_TAGS := optional
|
||||
|
||||
# The same as SYSTEM_UI_AOSP_PATH but based on the value of LOCAL_PATH which is
|
||||
# frameworks/base/packages/CarSystemUI.
|
||||
RELATIVE_SYSTEM_UI_AOSP_PATH := ../../../../$(SYSTEM_UI_AOSP_PATH)
|
||||
|
||||
|
||||
LOCAL_SRC_FILES := \
|
||||
$(call all-java-files-under, src) \
|
||||
$(call all-Iaidl-files-under, src) \
|
||||
$(call all-java-files-under, $(RELATIVE_SYSTEM_UI_AOSP_PATH)/src) \
|
||||
$(call all-Iaidl-files-under, $(RELATIVE_SYSTEM_UI_AOSP_PATH)/src)
|
||||
|
||||
LOCAL_STATIC_ANDROID_LIBRARIES := \
|
||||
SystemUIPluginLib \
|
||||
SystemUISharedLib \
|
||||
androidx.car_car \
|
||||
androidx.legacy_legacy-support-v4 \
|
||||
androidx.recyclerview_recyclerview \
|
||||
androidx.preference_preference \
|
||||
androidx.appcompat_appcompat \
|
||||
androidx.mediarouter_mediarouter \
|
||||
androidx.palette_palette \
|
||||
androidx.legacy_legacy-preference-v14 \
|
||||
androidx.leanback_leanback \
|
||||
androidx.slice_slice-core \
|
||||
androidx.slice_slice-view \
|
||||
androidx.slice_slice-builders \
|
||||
androidx.arch.core_core-runtime \
|
||||
androidx.lifecycle_lifecycle-extensions \
|
||||
|
||||
LOCAL_STATIC_JAVA_LIBRARIES := \
|
||||
SystemUI-tags \
|
||||
SystemUI-proto
|
||||
|
||||
LOCAL_JAVA_LIBRARIES := telephony-common \
|
||||
android.car
|
||||
|
||||
LOCAL_FULL_LIBS_MANIFEST_FILES := $(SYSTEM_UI_AOSP_PATH)/AndroidManifest.xml
|
||||
LOCAL_MANIFEST_FILE := AndroidManifest.xml
|
||||
|
||||
LOCAL_MODULE_OWNER := google
|
||||
LOCAL_PACKAGE_NAME := CarSystemUI
|
||||
LOCAL_PRIVATE_PLATFORM_APIS := true
|
||||
LOCAL_CERTIFICATE := platform
|
||||
LOCAL_PRIVILEGED_MODULE := true
|
||||
|
||||
LOCAL_PROGUARD_FLAG_FILES := $(RELATIVE_SYSTEM_UI_AOSP_PATH)/proguard.flags \
|
||||
proguard.flags
|
||||
|
||||
LOCAL_RESOURCE_DIR := \
|
||||
$(LOCAL_PATH)/res \
|
||||
$(SYSTEM_UI_AOSP_PATH)/res-keyguard \
|
||||
$(SYSTEM_UI_AOSP_PATH)/res
|
||||
|
||||
ifneq ($(INCREMENTAL_BUILDS),)
|
||||
LOCAL_PROGUARD_ENABLED := disabled
|
||||
LOCAL_JACK_ENABLED := incremental
|
||||
endif
|
||||
|
||||
LOCAL_DX_FLAGS := --multi-dex
|
||||
LOCAL_JACK_FLAGS := --multi-dex native
|
||||
|
||||
include frameworks/base/packages/SettingsLib/common.mk
|
||||
|
||||
LOCAL_OVERRIDES_PACKAGES := SystemUI
|
||||
|
||||
LOCAL_AAPT_FLAGS := --extra-packages com.android.keyguard
|
||||
|
||||
include $(BUILD_PACKAGE)
|
||||
|
||||
include $(call all-makefiles-under, $(SYSTEM_UI_CAR_PATH))
|
||||
24
packages/CarSystemUI/AndroidManifest.xml
Normal file
24
packages/CarSystemUI/AndroidManifest.xml
Normal file
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Copyright (C) 2018 The Android Open Source Project
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
|
||||
package="com.android.systemui"
|
||||
android:sharedUserId="android.uid.systemui"
|
||||
coreApp="true">
|
||||
|
||||
|
||||
</manifest>
|
||||
1
packages/CarSystemUI/proguard.flags
Normal file
1
packages/CarSystemUI/proguard.flags
Normal file
@@ -0,0 +1 @@
|
||||
-keep class com.android.systemui.CarSystemUIFactory
|
||||
35
packages/CarSystemUI/res/values/attrs.xml
Normal file
35
packages/CarSystemUI/res/values/attrs.xml
Normal file
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Copyright 2018 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
|
||||
<!-- Custom attributes to configure hvac values -->
|
||||
<declare-styleable name="AnimatedTemperatureView">
|
||||
<attr name="hvacAreaId" format="integer"/>
|
||||
<attr name="hvacPropertyId" format="integer"/>
|
||||
<attr name="hvacTempFormat" format="string"/>
|
||||
<!-- how far away the animations should center around -->
|
||||
<attr name="hvacPivotOffset" format="dimension"/>
|
||||
<attr name="hvacMinValue" format="float"/>
|
||||
<attr name="hvacMaxValue" format="float"/>
|
||||
<attr name="hvacMinText" format="string|reference"/>
|
||||
<attr name="hvacMaxText" format="string|reference"/>
|
||||
<attr name="android:gravity"/>
|
||||
<attr name="android:minEms"/>
|
||||
<attr name="android:textAppearance"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
83
packages/CarSystemUI/res/values/config.xml
Normal file
83
packages/CarSystemUI/res/values/config.xml
Normal file
@@ -0,0 +1,83 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~
|
||||
~ Copyright 2018, The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<resources>
|
||||
<string name="config_statusBarComponent" translatable="false">com.android.systemui.statusbar.car.CarStatusBar</string>
|
||||
<string name="config_systemUIFactoryComponent" translatable="false">com.android.systemui.CarSystemUIFactory</string>
|
||||
<bool name="config_enableFullscreenUserSwitcher">true</bool>
|
||||
|
||||
<!-- Notifications on the car should not show the gear icon. Swiping should only dismiss the
|
||||
cards. -->
|
||||
<bool name="config_showNotificationGear">false</bool>
|
||||
|
||||
<!-- No need to draw a background around a notification because there is no gear icon. -->
|
||||
<bool name="config_drawNotificationBackground">false</bool>
|
||||
|
||||
<!-- The notification shade should only be shown on a facet click and not by dragging. -->
|
||||
<bool name="config_enableNotificationShadeDrag">false</bool>
|
||||
|
||||
<!-- There should not be the ability to clear all notifications with a button. -->
|
||||
<bool name="config_enableNotificationsClearAll">false</bool>
|
||||
|
||||
<!-- Hide the notification shelf so that the cards in the notification center scroll smoothly
|
||||
off-screen. -->
|
||||
<bool name="config_showNotificationShelf">false</bool>
|
||||
|
||||
<!-- The notifications should always fade when being dismissed. -->
|
||||
<bool name="config_fadeNotificationsOnDismiss">true</bool>
|
||||
|
||||
<!-- The entire notification row should be translated because the cards are smaller than the
|
||||
width of the screen. If the row is not translated, then they will be clipped. -->
|
||||
<bool name="config_translateNotificationContentsOnSwipe">false</bool>
|
||||
|
||||
<!-- The notifications should fade as they are being swiped off screen. -->
|
||||
<bool name="config_fadeDependingOnAmountSwiped">true</bool>
|
||||
|
||||
<!-- The expand icon should be displayed at the top right corner of the notifications. -->
|
||||
<bool name="config_showNotificationExpandButtonAtEnd">true</bool>
|
||||
|
||||
<!-- A notification card that has been scrolled off screen should not be clipped in height. This
|
||||
maintains the illusion that the cards are being scrolled underneath the status bar
|
||||
shelf. -->
|
||||
<bool name="config_clipNotificationScrollToTop">false</bool>
|
||||
|
||||
<!-- The auto notification have rounded corners. Ensure that any content is clipped to these
|
||||
corners. -->
|
||||
<bool name="config_clipNotificationsToOutline">true</bool>
|
||||
|
||||
<!-- Notifications should always be in their expanded state so that the actions are visible.
|
||||
This will make it easier for an auto user to interact with them. -->
|
||||
<bool name="config_alwaysExpandNonGroupedNotifications">true</bool>
|
||||
|
||||
<!-- Auto does not allow notifications to be toggled to and from their expanded states to
|
||||
reduce driver distraction. -->
|
||||
<bool name="config_enableNonGroupedNotificationExpand">false</bool>
|
||||
|
||||
<!-- There should always be a dividing line between notifications. -->
|
||||
<bool name="config_showDividersWhenGroupNotificationExpanded">true</bool>
|
||||
|
||||
<!--- Hide the dividing lines when the notification group is expanding. -->
|
||||
<bool name="config_hideDividersDuringTransition">true</bool>
|
||||
|
||||
<!-- Child notifications are displayed with no dividing space between them in auto, so disable
|
||||
the shadow. -->
|
||||
<bool name="config_enableShadowOnChildNotifications">false</bool>
|
||||
|
||||
<!-- Keep the notification background when the container has been expanded. The children will
|
||||
expand inline within the container, so it can keep its original background. -->
|
||||
<bool name="config_showGroupNotificationBgWhenExpanded">true</bool>
|
||||
</resources>
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.android.systemui;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.ArrayMap;
|
||||
|
||||
import com.android.internal.widget.LockPatternUtils;
|
||||
import com.android.keyguard.ViewMediatorCallback;
|
||||
import com.android.systemui.Dependency.DependencyProvider;
|
||||
import com.android.systemui.car.CarNotificationEntryManager;
|
||||
import com.android.systemui.statusbar.NotificationEntryManager;
|
||||
import com.android.systemui.statusbar.car.CarFacetButtonController;
|
||||
import com.android.systemui.statusbar.car.CarStatusBarKeyguardViewManager;
|
||||
import com.android.systemui.statusbar.car.hvac.HvacController;
|
||||
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
|
||||
|
||||
/**
|
||||
* Class factory to provide car specific SystemUI components.
|
||||
*/
|
||||
public class CarSystemUIFactory extends SystemUIFactory {
|
||||
|
||||
public StatusBarKeyguardViewManager createStatusBarKeyguardViewManager(Context context,
|
||||
ViewMediatorCallback viewMediatorCallback, LockPatternUtils lockPatternUtils) {
|
||||
return new CarStatusBarKeyguardViewManager(context, viewMediatorCallback, lockPatternUtils);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void injectDependencies(ArrayMap<Object, DependencyProvider> providers,
|
||||
Context context) {
|
||||
super.injectDependencies(providers, context);
|
||||
providers.put(NotificationEntryManager.class,
|
||||
() -> new CarNotificationEntryManager(context));
|
||||
providers.put(CarFacetButtonController.class, () -> new CarFacetButtonController(context));
|
||||
providers.put(HvacController.class, () -> new HvacController(context));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
/*
|
||||
* Copyright (c) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.android.systemui.statusbar.hvac;
|
||||
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import com.android.systemui.statusbar.car.hvac.TemperatureView;
|
||||
import com.android.systemui.statusbar.car.hvac.HvacController;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.Property;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextSwitcher;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.systemui.Dependency;
|
||||
import com.android.systemui.R;
|
||||
|
||||
/**
|
||||
* Simple text display of HVAC properties, It is designed to show mTemperature and is configured in
|
||||
* the XML.
|
||||
* XML properties:
|
||||
* hvacPropertyId - Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
|
||||
* hvacAreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
|
||||
* hvacTempFormat - Example: "%.1f\u00B0" (1 decimal and the degree symbol)
|
||||
* hvacOrientaion = Example: left
|
||||
* <p>
|
||||
* Note: It registers itself with {@link HvacController}
|
||||
*/
|
||||
public class AnimatedTemperatureView extends FrameLayout implements TemperatureView {
|
||||
|
||||
private static final float TEMPERATURE_EQUIVALENT_DELTA = .01f;
|
||||
private static final Property<ColorDrawable, Integer> COLOR_PROPERTY =
|
||||
new Property<ColorDrawable, Integer>(Integer.class, "color") {
|
||||
|
||||
@Override
|
||||
public Integer get(ColorDrawable object) {
|
||||
return object.getColor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(ColorDrawable object, Integer value) {
|
||||
object.setColor(value);
|
||||
}
|
||||
};
|
||||
|
||||
static boolean isHorizontal(int gravity) {
|
||||
return Gravity.isHorizontal(gravity)
|
||||
&& (gravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.CENTER_HORIZONTAL;
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
static boolean isLeft(int gravity, int layoutDirection) {
|
||||
return Gravity
|
||||
.getAbsoluteGravity(gravity & Gravity.HORIZONTAL_GRAVITY_MASK, layoutDirection)
|
||||
== Gravity.LEFT;
|
||||
}
|
||||
|
||||
static boolean isVertical(int gravity) {
|
||||
return Gravity.isVertical(gravity)
|
||||
&& (gravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.CENTER_VERTICAL;
|
||||
}
|
||||
|
||||
static boolean isTop(int gravity) {
|
||||
return (gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.TOP;
|
||||
}
|
||||
|
||||
private final int mAreaId;
|
||||
private final int mPropertyId;
|
||||
private final int mPivotOffset;
|
||||
private final int mGravity;
|
||||
private final int mTextAppearanceRes;
|
||||
private final int mMinEms;
|
||||
private final Rect mPaddingRect;
|
||||
private final float mMinValue;
|
||||
private final float mMaxValue;
|
||||
|
||||
private final ColorDrawable mBackgroundColor;
|
||||
|
||||
private final TemperatureColorStore mColorStore = new TemperatureColorStore();
|
||||
private final TemperatureBackgroundAnimator mBackgroundAnimator;
|
||||
private final TemperatureTextAnimator mTextAnimator;
|
||||
|
||||
public AnimatedTemperatureView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
TypedArray typedArray = context.obtainStyledAttributes(attrs,
|
||||
R.styleable.AnimatedTemperatureView);
|
||||
mAreaId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacAreaId, -1);
|
||||
mPropertyId = typedArray.getInt(R.styleable.AnimatedTemperatureView_hvacPropertyId, -1);
|
||||
mPivotOffset =
|
||||
typedArray.getDimensionPixelOffset(
|
||||
R.styleable.AnimatedTemperatureView_hvacPivotOffset, 0);
|
||||
mGravity = typedArray.getInt(R.styleable.AnimatedTemperatureView_android_gravity,
|
||||
Gravity.START);
|
||||
mTextAppearanceRes =
|
||||
typedArray.getResourceId(R.styleable.AnimatedTemperatureView_android_textAppearance,
|
||||
0);
|
||||
mMinEms = typedArray.getInteger(R.styleable.AnimatedTemperatureView_android_minEms, 0);
|
||||
mMinValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMinValue,
|
||||
Float.NaN);
|
||||
mMaxValue = typedArray.getFloat(R.styleable.AnimatedTemperatureView_hvacMaxValue,
|
||||
Float.NaN);
|
||||
|
||||
|
||||
mPaddingRect =
|
||||
new Rect(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
|
||||
setPadding(0, 0, 0, 0);
|
||||
|
||||
setClipChildren(false);
|
||||
setClipToPadding(false);
|
||||
|
||||
// init Views
|
||||
TextSwitcher textSwitcher = new TextSwitcher(context);
|
||||
textSwitcher.setFactory(this::generateTextView);
|
||||
ImageView background = new ImageView(context);
|
||||
mBackgroundColor = new ColorDrawable(Color.TRANSPARENT);
|
||||
background.setImageDrawable(mBackgroundColor);
|
||||
background.setVisibility(View.GONE);
|
||||
|
||||
mBackgroundAnimator = new TemperatureBackgroundAnimator(this, background);
|
||||
|
||||
|
||||
String format = typedArray.getString(R.styleable.AnimatedTemperatureView_hvacTempFormat);
|
||||
format = (format == null) ? "%.1f\u00B0" : format;
|
||||
CharSequence minText = typedArray.getString(
|
||||
R.styleable.AnimatedTemperatureView_hvacMinText);
|
||||
CharSequence maxText = typedArray.getString(
|
||||
R.styleable.AnimatedTemperatureView_hvacMaxText);
|
||||
mTextAnimator = new TemperatureTextAnimator(this, textSwitcher, format, mPivotOffset,
|
||||
minText, maxText);
|
||||
|
||||
addView(background, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
addView(textSwitcher, ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
||||
typedArray.recycle();
|
||||
|
||||
// register with controller
|
||||
HvacController hvacController = Dependency.get(HvacController.class);
|
||||
hvacController.addHvacTextView(this);
|
||||
}
|
||||
|
||||
private TextView generateTextView() {
|
||||
TextView textView = new TextView(getContext());
|
||||
textView.setTextAppearance(mTextAppearanceRes);
|
||||
textView.setAllCaps(true);
|
||||
textView.setMinEms(mMinEms);
|
||||
textView.setGravity(mGravity);
|
||||
textView.setPadding(mPaddingRect.left, mPaddingRect.top, mPaddingRect.right,
|
||||
mPaddingRect.bottom);
|
||||
textView.getViewTreeObserver()
|
||||
.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||
@Override
|
||||
public boolean onPreDraw() {
|
||||
if (isHorizontal(mGravity)) {
|
||||
if (isLeft(mGravity, getLayoutDirection())) {
|
||||
textView.setPivotX(-mPivotOffset);
|
||||
} else {
|
||||
textView.setPivotX(textView.getWidth() + mPivotOffset);
|
||||
}
|
||||
}
|
||||
textView.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||
return false;
|
||||
}
|
||||
});
|
||||
textView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
|
||||
return textView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the float for display
|
||||
*
|
||||
* @param temp - The current temp or NaN
|
||||
*/
|
||||
@Override
|
||||
public void setTemp(float temp) {
|
||||
mTextAnimator.setTemp(temp);
|
||||
if (Float.isNaN(temp)) {
|
||||
mBackgroundAnimator.hideCircle();
|
||||
return;
|
||||
}
|
||||
int color;
|
||||
if (isMinValue(temp)) {
|
||||
color = mColorStore.getMinColor();
|
||||
} else if (isMaxValue(temp)) {
|
||||
color = mColorStore.getMaxColor();
|
||||
} else {
|
||||
color = mColorStore.getColorForTemperature(temp);
|
||||
}
|
||||
if (mBackgroundAnimator.isOpen()) {
|
||||
ObjectAnimator colorAnimator =
|
||||
ObjectAnimator.ofInt(mBackgroundColor, COLOR_PROPERTY, color);
|
||||
colorAnimator.setEvaluator((fraction, startValue, endValue) -> mColorStore
|
||||
.lerpColor(fraction, (int) startValue, (int) endValue));
|
||||
colorAnimator.start();
|
||||
} else {
|
||||
mBackgroundColor.setColor(color);
|
||||
}
|
||||
|
||||
mBackgroundAnimator.animateOpen();
|
||||
}
|
||||
|
||||
boolean isMinValue(float temp) {
|
||||
return !Float.isNaN(mMinValue) && isApproxEqual(temp, mMinValue);
|
||||
}
|
||||
|
||||
boolean isMaxValue(float temp) {
|
||||
return !Float.isNaN(mMaxValue) && isApproxEqual(temp, mMaxValue);
|
||||
}
|
||||
|
||||
private boolean isApproxEqual(float left, float right) {
|
||||
return Math.abs(left - right) <= TEMPERATURE_EQUIVALENT_DELTA;
|
||||
}
|
||||
|
||||
int getGravity() {
|
||||
return mGravity;
|
||||
}
|
||||
|
||||
int getPivotOffset() {
|
||||
return mPivotOffset;
|
||||
}
|
||||
|
||||
Rect getPaddingRect() {
|
||||
return mPaddingRect;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return propertiyId Example: CarHvacManager.ID_ZONED_TEMP_SETPOINT (16385)
|
||||
*/
|
||||
@Override
|
||||
public int getPropertyId() {
|
||||
return mPropertyId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return hvac AreaId - Example: VehicleSeat.SEAT_ROW_1_LEFT (1)
|
||||
*/
|
||||
@Override
|
||||
public int getAreaId() {
|
||||
return mAreaId;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow();
|
||||
mBackgroundAnimator.stopAnimations();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
/*
|
||||
* Copyright (c) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.android.systemui.statusbar.hvac;
|
||||
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal;
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft;
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isTop;
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isVertical;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorListenerAdapter;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ObjectAnimator;
|
||||
import android.annotation.IntDef;
|
||||
import android.graphics.Rect;
|
||||
import android.view.View;
|
||||
import android.view.ViewAnimationUtils;
|
||||
import android.view.animation.AnticipateInterpolator;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Controls circular reveal animation of temperature background
|
||||
*/
|
||||
class TemperatureBackgroundAnimator {
|
||||
|
||||
private static final AnticipateInterpolator ANTICIPATE_INTERPOLATOR =
|
||||
new AnticipateInterpolator();
|
||||
private static final float MAX_OPACITY = .6f;
|
||||
|
||||
private final View mAnimatedView;
|
||||
|
||||
private int mPivotX;
|
||||
private int mPivotY;
|
||||
private int mGoneRadius;
|
||||
private int mOvershootRadius;
|
||||
private int mRestingRadius;
|
||||
private int mBumpRadius;
|
||||
|
||||
@CircleState
|
||||
private int mCircleState;
|
||||
|
||||
private Animator mCircularReveal;
|
||||
private boolean mAnimationsReady;
|
||||
|
||||
@IntDef({CircleState.GONE, CircleState.ENTERING, CircleState.OVERSHOT, CircleState.RESTING,
|
||||
CircleState.RESTED, CircleState.BUMPING, CircleState.BUMPED, CircleState.EXITING})
|
||||
private @interface CircleState {
|
||||
int GONE = 0;
|
||||
int ENTERING = 1;
|
||||
int OVERSHOT = 2;
|
||||
int RESTING = 3;
|
||||
int RESTED = 4;
|
||||
int BUMPING = 5;
|
||||
int BUMPED = 6;
|
||||
int EXITING = 7;
|
||||
}
|
||||
|
||||
TemperatureBackgroundAnimator(
|
||||
AnimatedTemperatureView parent,
|
||||
ImageView animatedView) {
|
||||
mAnimatedView = animatedView;
|
||||
mAnimatedView.setAlpha(0);
|
||||
|
||||
parent.addOnLayoutChangeListener(
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
||||
setupAnimations(parent.getGravity(), parent.getPivotOffset(),
|
||||
parent.getPaddingRect(), parent.getWidth(), parent.getHeight()));
|
||||
}
|
||||
|
||||
private void setupAnimations(int gravity, int pivotOffset, Rect paddingRect,
|
||||
int width, int height) {
|
||||
int padding;
|
||||
if (isHorizontal(gravity)) {
|
||||
mGoneRadius = pivotOffset;
|
||||
if (isLeft(gravity, mAnimatedView.getLayoutDirection())) {
|
||||
mPivotX = -pivotOffset;
|
||||
padding = paddingRect.right;
|
||||
} else {
|
||||
mPivotX = width + pivotOffset;
|
||||
padding = paddingRect.left;
|
||||
}
|
||||
mPivotY = height / 2;
|
||||
mOvershootRadius = pivotOffset + width;
|
||||
} else if (isVertical(gravity)) {
|
||||
mGoneRadius = pivotOffset;
|
||||
if (isTop(gravity)) {
|
||||
mPivotY = -pivotOffset;
|
||||
padding = paddingRect.bottom;
|
||||
} else {
|
||||
mPivotY = height + pivotOffset;
|
||||
padding = paddingRect.top;
|
||||
}
|
||||
mPivotX = width / 2;
|
||||
mOvershootRadius = pivotOffset + height;
|
||||
} else {
|
||||
mPivotX = width / 2;
|
||||
mPivotY = height / 2;
|
||||
mGoneRadius = 0;
|
||||
if (width > height) {
|
||||
mOvershootRadius = height;
|
||||
padding = Math.max(paddingRect.top, paddingRect.bottom);
|
||||
} else {
|
||||
mOvershootRadius = width;
|
||||
padding = Math.max(paddingRect.left, paddingRect.right);
|
||||
}
|
||||
}
|
||||
mRestingRadius = mOvershootRadius - padding;
|
||||
mBumpRadius = mOvershootRadius - padding / 3;
|
||||
mAnimationsReady = true;
|
||||
}
|
||||
|
||||
boolean isOpen() {
|
||||
return mCircleState != CircleState.GONE;
|
||||
}
|
||||
|
||||
void animateOpen() {
|
||||
if (!mAnimationsReady || mCircleState == CircleState.ENTERING) {
|
||||
return;
|
||||
}
|
||||
|
||||
AnimatorSet set = new AnimatorSet();
|
||||
List<Animator> animators = new ArrayList<>();
|
||||
switch (mCircleState) {
|
||||
case CircleState.ENTERING:
|
||||
throw new AssertionError("Should not be able to reach this statement");
|
||||
case CircleState.GONE: {
|
||||
Animator startCircle = createEnterAnimator();
|
||||
markState(startCircle, CircleState.ENTERING);
|
||||
animators.add(startCircle);
|
||||
Animator holdOvershoot = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius,
|
||||
mOvershootRadius);
|
||||
holdOvershoot.setDuration(50);
|
||||
markState(holdOvershoot, CircleState.OVERSHOT);
|
||||
animators.add(holdOvershoot);
|
||||
Animator rest = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mOvershootRadius,
|
||||
mRestingRadius);
|
||||
markState(rest, CircleState.RESTING);
|
||||
animators.add(rest);
|
||||
Animator holdRest = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius,
|
||||
mRestingRadius);
|
||||
markState(holdRest, CircleState.RESTED);
|
||||
holdRest.setDuration(1000);
|
||||
animators.add(holdRest);
|
||||
Animator exit = createExitAnimator(mRestingRadius);
|
||||
markState(exit, CircleState.EXITING);
|
||||
animators.add(exit);
|
||||
}
|
||||
break;
|
||||
case CircleState.RESTED:
|
||||
case CircleState.RESTING:
|
||||
case CircleState.EXITING:
|
||||
case CircleState.OVERSHOT:
|
||||
int startRadius =
|
||||
mCircleState == CircleState.OVERSHOT ? mOvershootRadius : mRestingRadius;
|
||||
Animator bump = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius,
|
||||
mBumpRadius);
|
||||
bump.setDuration(50);
|
||||
markState(bump, CircleState.BUMPING);
|
||||
animators.add(bump);
|
||||
// fallthrough intentional
|
||||
case CircleState.BUMPED:
|
||||
case CircleState.BUMPING:
|
||||
Animator holdBump = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius,
|
||||
mBumpRadius);
|
||||
holdBump.setDuration(100);
|
||||
markState(holdBump, CircleState.BUMPED);
|
||||
animators.add(holdBump);
|
||||
Animator rest = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mBumpRadius,
|
||||
mRestingRadius);
|
||||
markState(rest, CircleState.RESTING);
|
||||
animators.add(rest);
|
||||
Animator holdRest = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mRestingRadius,
|
||||
mRestingRadius);
|
||||
holdRest.setDuration(1000);
|
||||
markState(holdRest, CircleState.RESTED);
|
||||
animators.add(holdRest);
|
||||
Animator exit = createExitAnimator(mRestingRadius);
|
||||
markState(exit, CircleState.EXITING);
|
||||
animators.add(exit);
|
||||
break;
|
||||
}
|
||||
set.playSequentially(animators);
|
||||
set.addListener(new AnimatorListenerAdapter() {
|
||||
private boolean mCanceled = false;
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
if (mCircularReveal != null) {
|
||||
mCircularReveal.cancel();
|
||||
}
|
||||
mCircularReveal = animation;
|
||||
mAnimatedView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
mCanceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (mCanceled) {
|
||||
return;
|
||||
}
|
||||
mCircularReveal = null;
|
||||
mCircleState = CircleState.GONE;
|
||||
mAnimatedView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
set.start();
|
||||
}
|
||||
|
||||
private Animator createEnterAnimator() {
|
||||
AnimatorSet animatorSet = new AnimatorSet();
|
||||
Animator circularReveal = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, mGoneRadius,
|
||||
mOvershootRadius);
|
||||
Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, MAX_OPACITY);
|
||||
animatorSet.playTogether(circularReveal, fade);
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
private Animator createExitAnimator(int startRadius) {
|
||||
AnimatorSet animatorSet = new AnimatorSet();
|
||||
Animator circularHide = ViewAnimationUtils
|
||||
.createCircularReveal(mAnimatedView, mPivotX, mPivotY, startRadius,
|
||||
(mGoneRadius + startRadius) / 2);
|
||||
circularHide.setInterpolator(ANTICIPATE_INTERPOLATOR);
|
||||
Animator fade = ObjectAnimator.ofFloat(mAnimatedView, View.ALPHA, 0);
|
||||
fade.setStartDelay(50);
|
||||
animatorSet.playTogether(circularHide, fade);
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
void hideCircle() {
|
||||
if (!mAnimationsReady || mCircleState == CircleState.GONE
|
||||
|| mCircleState == CircleState.EXITING) {
|
||||
return;
|
||||
}
|
||||
|
||||
int startRadius;
|
||||
switch (mCircleState) {
|
||||
// Unreachable, but here to exhaust switch cases
|
||||
//noinspection ConstantConditions
|
||||
case CircleState.EXITING:
|
||||
//noinspection ConstantConditions
|
||||
case CircleState.GONE:
|
||||
throw new AssertionError("Should not be able to reach this statement");
|
||||
case CircleState.BUMPED:
|
||||
case CircleState.BUMPING:
|
||||
startRadius = mBumpRadius;
|
||||
break;
|
||||
case CircleState.OVERSHOT:
|
||||
startRadius = mOvershootRadius;
|
||||
break;
|
||||
case CircleState.ENTERING:
|
||||
case CircleState.RESTED:
|
||||
case CircleState.RESTING:
|
||||
startRadius = mRestingRadius;
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException("Unknown CircleState: " + mCircleState);
|
||||
}
|
||||
|
||||
Animator hideAnimation = createExitAnimator(startRadius);
|
||||
if (startRadius == mRestingRadius) {
|
||||
hideAnimation.setInterpolator(ANTICIPATE_INTERPOLATOR);
|
||||
}
|
||||
hideAnimation.addListener(new AnimatorListenerAdapter() {
|
||||
private boolean mCanceled = false;
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
mCircleState = CircleState.EXITING;
|
||||
if (mCircularReveal != null) {
|
||||
mCircularReveal.cancel();
|
||||
}
|
||||
mCircularReveal = animation;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
mCanceled = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
if (mCanceled) {
|
||||
return;
|
||||
}
|
||||
mCircularReveal = null;
|
||||
mCircleState = CircleState.GONE;
|
||||
mAnimatedView.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
hideAnimation.start();
|
||||
}
|
||||
|
||||
void stopAnimations() {
|
||||
if (mCircularReveal != null) {
|
||||
mCircularReveal.end();
|
||||
}
|
||||
}
|
||||
|
||||
private void markState(Animator animator, @CircleState int startState) {
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
mCircleState = startState;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
/*
|
||||
* Copyright (c) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.android.systemui.statusbar.hvac;
|
||||
|
||||
import android.graphics.Color;
|
||||
|
||||
/**
|
||||
* Contains the logic for mapping colors to temperatures
|
||||
*/
|
||||
class TemperatureColorStore {
|
||||
|
||||
private static class TemperatureColorValue {
|
||||
final float mTemperature;
|
||||
final int mColor;
|
||||
|
||||
private TemperatureColorValue(float temperature, int color) {
|
||||
this.mTemperature = temperature;
|
||||
this.mColor = color;
|
||||
}
|
||||
|
||||
float getTemperature() {
|
||||
return mTemperature;
|
||||
}
|
||||
|
||||
int getColor() {
|
||||
return mColor;
|
||||
}
|
||||
}
|
||||
|
||||
private static TemperatureColorValue tempToColor(float temperature, int color) {
|
||||
return new TemperatureColorValue(temperature, color);
|
||||
}
|
||||
|
||||
private static final int COLOR_COLDEST = 0xFF406DFF;
|
||||
private static final int COLOR_COLD = 0xFF4094FF;
|
||||
private static final int COLOR_NEUTRAL = 0xFFF4F4F4;
|
||||
private static final int COLOR_WARM = 0xFFFF550F;
|
||||
private static final int COLOR_WARMEST = 0xFFFF0000;
|
||||
// must be sorted by temperature
|
||||
private static final TemperatureColorValue[] sTemperatureColorValues =
|
||||
{
|
||||
// Celsius
|
||||
tempToColor(19, COLOR_COLDEST),
|
||||
tempToColor(21, COLOR_COLD),
|
||||
tempToColor(23, COLOR_NEUTRAL),
|
||||
tempToColor(25, COLOR_WARM),
|
||||
tempToColor(27, COLOR_WARMEST),
|
||||
|
||||
// Switch over
|
||||
tempToColor(45, COLOR_WARMEST),
|
||||
tempToColor(45.00001f, COLOR_COLDEST),
|
||||
|
||||
// Farenheight
|
||||
tempToColor(66, COLOR_COLDEST),
|
||||
tempToColor(70, COLOR_COLD),
|
||||
tempToColor(74, COLOR_NEUTRAL),
|
||||
tempToColor(76, COLOR_WARM),
|
||||
tempToColor(80, COLOR_WARMEST)
|
||||
};
|
||||
|
||||
private static final int COLOR_UNSET = Color.BLACK;
|
||||
|
||||
private final float[] mTempHsv1 = new float[3];
|
||||
private final float[] mTempHsv2 = new float[3];
|
||||
private final float[] mTempHsv3 = new float[3];
|
||||
|
||||
int getMinColor() {
|
||||
return COLOR_COLDEST;
|
||||
}
|
||||
|
||||
int getMaxColor() {
|
||||
return COLOR_WARMEST;
|
||||
}
|
||||
|
||||
int getColorForTemperature(float temperature) {
|
||||
if (Float.isNaN(temperature)) {
|
||||
return COLOR_UNSET;
|
||||
}
|
||||
TemperatureColorValue bottomValue = sTemperatureColorValues[0];
|
||||
if (temperature <= bottomValue.getTemperature()) {
|
||||
return bottomValue.getColor();
|
||||
}
|
||||
TemperatureColorValue topValue =
|
||||
sTemperatureColorValues[sTemperatureColorValues.length - 1];
|
||||
if (temperature >= topValue.getTemperature()) {
|
||||
return topValue.getColor();
|
||||
}
|
||||
|
||||
int index = binarySearch(temperature);
|
||||
if (index >= 0) {
|
||||
return sTemperatureColorValues[index].getColor();
|
||||
}
|
||||
|
||||
index = -index - 1; // move to the insertion point
|
||||
|
||||
TemperatureColorValue startValue = sTemperatureColorValues[index - 1];
|
||||
TemperatureColorValue endValue = sTemperatureColorValues[index];
|
||||
float fraction = (temperature - startValue.getTemperature()) / (endValue.getTemperature()
|
||||
- startValue.getTemperature());
|
||||
return lerpColor(fraction, startValue.getColor(), endValue.getColor());
|
||||
}
|
||||
|
||||
int lerpColor(float fraction, int startColor, int endColor) {
|
||||
float[] startHsv = mTempHsv1;
|
||||
Color.colorToHSV(startColor, startHsv);
|
||||
float[] endHsv = mTempHsv2;
|
||||
Color.colorToHSV(endColor, endHsv);
|
||||
|
||||
// If a target color is white/gray, it should use the same hue as the other target
|
||||
if (startHsv[1] == 0) {
|
||||
startHsv[0] = endHsv[0];
|
||||
}
|
||||
if (endHsv[1] == 0) {
|
||||
endHsv[0] = startHsv[0];
|
||||
}
|
||||
|
||||
float[] outColor = mTempHsv3;
|
||||
outColor[0] = hueLerp(fraction, startHsv[0], endHsv[0]);
|
||||
outColor[1] = lerp(fraction, startHsv[1], endHsv[1]);
|
||||
outColor[2] = lerp(fraction, startHsv[2], endHsv[2]);
|
||||
|
||||
return Color.HSVToColor(outColor);
|
||||
}
|
||||
|
||||
private float hueLerp(float fraction, float start, float end) {
|
||||
// If in flat part of curve, no interpolation necessary
|
||||
if (start == end) {
|
||||
return start;
|
||||
}
|
||||
|
||||
// If the hues are more than 180 degrees apart, go the other way around the color wheel
|
||||
// by moving the smaller value above 360
|
||||
if (Math.abs(start - end) > 180f) {
|
||||
if (start < end) {
|
||||
start += 360f;
|
||||
} else {
|
||||
end += 360f;
|
||||
}
|
||||
}
|
||||
// Lerp and ensure the final output is within [0, 360)
|
||||
return lerp(fraction, start, end) % 360f;
|
||||
|
||||
}
|
||||
|
||||
private float lerp(float fraction, float start, float end) {
|
||||
// If in flat part of curve, no interpolation necessary
|
||||
if (start == end) {
|
||||
return start;
|
||||
}
|
||||
|
||||
// If outside bounds, use boundary value
|
||||
if (fraction >= 1) {
|
||||
return end;
|
||||
}
|
||||
if (fraction <= 0) {
|
||||
return start;
|
||||
}
|
||||
|
||||
return (end - start) * fraction + start;
|
||||
}
|
||||
|
||||
private int binarySearch(float temperature) {
|
||||
int low = 0;
|
||||
int high = sTemperatureColorValues.length;
|
||||
|
||||
while (low <= high) {
|
||||
int mid = (low + high) >>> 1;
|
||||
float midVal = sTemperatureColorValues[mid].getTemperature();
|
||||
|
||||
if (midVal < temperature) {
|
||||
low = mid + 1; // Neither val is NaN, thisVal is smaller
|
||||
} else if (midVal > temperature) {
|
||||
high = mid - 1; // Neither val is NaN, thisVal is larger
|
||||
} else {
|
||||
int midBits = Float.floatToIntBits(midVal);
|
||||
int keyBits = Float.floatToIntBits(temperature);
|
||||
if (midBits == keyBits) { // Values are equal
|
||||
return mid; // Key found
|
||||
} else if (midBits < keyBits) { // (-0.0, 0.0) or (!NaN, NaN)
|
||||
low = mid + 1;
|
||||
} else { /* (0.0, -0.0) or (NaN, !NaN)*/
|
||||
high = mid - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return -(low + 1); // key not found.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/*
|
||||
* Copyright (c) 2018 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License
|
||||
*/
|
||||
|
||||
package com.android.systemui.statusbar.hvac;
|
||||
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isHorizontal;
|
||||
import static com.android.systemui.statusbar.hvac.AnimatedTemperatureView.isLeft;
|
||||
|
||||
import android.annotation.NonNull;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.AlphaAnimation;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationSet;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.view.animation.TranslateAnimation;
|
||||
import android.widget.TextSwitcher;
|
||||
|
||||
/**
|
||||
* Controls animating TemperatureView's text
|
||||
*/
|
||||
class TemperatureTextAnimator {
|
||||
|
||||
private static final DecelerateInterpolator DECELERATE_INTERPOLATOR =
|
||||
new DecelerateInterpolator();
|
||||
private static final AccelerateDecelerateInterpolator ACCELERATE_DECELERATE_INTERPOLATOR =
|
||||
new AccelerateDecelerateInterpolator();
|
||||
|
||||
private static final int ROTATION_DEGREES = 15;
|
||||
private static final int DURATION_MILLIS = 200;
|
||||
|
||||
private AnimatedTemperatureView mParent;
|
||||
private final TextSwitcher mTextSwitcher;
|
||||
private final String mTempFormat;
|
||||
private final int mPivotOffset;
|
||||
private final CharSequence mMinText;
|
||||
private final CharSequence mMaxText;
|
||||
|
||||
private Animation mTextInAnimationUp;
|
||||
private Animation mTextOutAnimationUp;
|
||||
private Animation mTextInAnimationDown;
|
||||
private Animation mTextOutAnimationDown;
|
||||
private Animation mTextFadeInAnimation;
|
||||
private Animation mTextFadeOutAnimation;
|
||||
|
||||
private float mLastTemp = Float.NaN;
|
||||
|
||||
TemperatureTextAnimator(AnimatedTemperatureView parent, TextSwitcher textSwitcher,
|
||||
String tempFormat, int pivotOffset,
|
||||
CharSequence minText, CharSequence maxText) {
|
||||
mParent = parent;
|
||||
mTextSwitcher = textSwitcher;
|
||||
mTempFormat = tempFormat;
|
||||
mPivotOffset = pivotOffset;
|
||||
mMinText = minText;
|
||||
mMaxText = maxText;
|
||||
|
||||
mParent.addOnLayoutChangeListener(
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) ->
|
||||
setupAnimations(mParent.getGravity()));
|
||||
}
|
||||
|
||||
void setTemp(float temp) {
|
||||
if (Float.isNaN(temp)) {
|
||||
mTextSwitcher.setInAnimation(mTextFadeInAnimation);
|
||||
mTextSwitcher.setOutAnimation(mTextFadeOutAnimation);
|
||||
mTextSwitcher.setText("--");
|
||||
mLastTemp = temp;
|
||||
return;
|
||||
}
|
||||
boolean isMinValue = mParent.isMinValue(temp);
|
||||
boolean isMaxValue = mParent.isMaxValue(temp);
|
||||
if (Float.isNaN(mLastTemp)) {
|
||||
mTextSwitcher.setInAnimation(mTextFadeInAnimation);
|
||||
mTextSwitcher.setOutAnimation(mTextFadeOutAnimation);
|
||||
} else if (!isMinValue && (isMaxValue || temp > mLastTemp)) {
|
||||
mTextSwitcher.setInAnimation(mTextInAnimationUp);
|
||||
mTextSwitcher.setOutAnimation(mTextOutAnimationUp);
|
||||
} else {
|
||||
mTextSwitcher.setInAnimation(mTextInAnimationDown);
|
||||
mTextSwitcher.setOutAnimation(mTextOutAnimationDown);
|
||||
}
|
||||
CharSequence text;
|
||||
if (isMinValue) {
|
||||
text = mMinText;
|
||||
} else if (isMaxValue) {
|
||||
text = mMaxText;
|
||||
} else {
|
||||
text = String.format(mTempFormat, temp);
|
||||
}
|
||||
mTextSwitcher.setText(text);
|
||||
mLastTemp = temp;
|
||||
}
|
||||
|
||||
private void setupAnimations(int gravity) {
|
||||
mTextFadeInAnimation = createFadeAnimation(true);
|
||||
mTextFadeOutAnimation = createFadeAnimation(false);
|
||||
if (!isHorizontal(gravity)) {
|
||||
mTextInAnimationUp = createTranslateFadeAnimation(true, true);
|
||||
mTextOutAnimationUp = createTranslateFadeAnimation(false, true);
|
||||
mTextInAnimationDown = createTranslateFadeAnimation(true, false);
|
||||
mTextOutAnimationDown = createTranslateFadeAnimation(false, false);
|
||||
} else {
|
||||
boolean isLeft = isLeft(gravity, mTextSwitcher.getLayoutDirection());
|
||||
mTextInAnimationUp = createRotateFadeAnimation(true, isLeft, true);
|
||||
mTextOutAnimationUp = createRotateFadeAnimation(false, isLeft, true);
|
||||
mTextInAnimationDown = createRotateFadeAnimation(true, isLeft, false);
|
||||
mTextOutAnimationDown = createRotateFadeAnimation(false, isLeft, false);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Animation createFadeAnimation(boolean in) {
|
||||
AnimationSet set = new AnimationSet(true);
|
||||
AlphaAnimation alphaAnimation = new AlphaAnimation(in ? 0 : 1, in ? 1 : 0);
|
||||
alphaAnimation.setDuration(DURATION_MILLIS);
|
||||
set.addAnimation(new RotateAnimation(0, 0)); // Undo any previous rotation
|
||||
set.addAnimation(alphaAnimation);
|
||||
return set;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Animation createTranslateFadeAnimation(boolean in, boolean up) {
|
||||
AnimationSet set = new AnimationSet(true);
|
||||
set.setInterpolator(ACCELERATE_DECELERATE_INTERPOLATOR);
|
||||
set.setDuration(DURATION_MILLIS);
|
||||
int fromYDelta = in ? (up ? 1 : -1) : 0;
|
||||
int toYDelta = in ? 0 : (up ? -1 : 1);
|
||||
set.addAnimation(
|
||||
new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0, Animation.RELATIVE_TO_SELF, 0,
|
||||
Animation.RELATIVE_TO_SELF, fromYDelta, Animation.RELATIVE_TO_SELF,
|
||||
toYDelta));
|
||||
set.addAnimation(new AlphaAnimation(in ? 0 : 1, in ? 1 : 0));
|
||||
return set;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Animation createRotateFadeAnimation(boolean in, boolean isLeft, boolean up) {
|
||||
AnimationSet set = new AnimationSet(true);
|
||||
set.setInterpolator(DECELERATE_INTERPOLATOR);
|
||||
set.setDuration(DURATION_MILLIS);
|
||||
|
||||
float degrees = isLeft == up ? -ROTATION_DEGREES : ROTATION_DEGREES;
|
||||
int pivotX = isLeft ? -mPivotOffset : mParent.getWidth() + mPivotOffset;
|
||||
set.addAnimation(
|
||||
new RotateAnimation(in ? -degrees : 0f, in ? 0f : degrees, Animation.ABSOLUTE,
|
||||
pivotX, Animation.ABSOLUTE, 0f));
|
||||
set.addAnimation(new AlphaAnimation(in ? 0 : 1, in ? 1 : 0));
|
||||
return set;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user