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:
Brad Stenning
2018-06-15 15:29:19 -07:00
parent 7beddc3a26
commit 034a04937e
10 changed files with 1260 additions and 0 deletions

View 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))

View 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>

View File

@@ -0,0 +1 @@
-keep class com.android.systemui.CarSystemUIFactory

View 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>

View 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>

View File

@@ -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));
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
});
}
}

View File

@@ -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.
}
}

View File

@@ -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;
}
}