diff --git a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml index 26152cdac136d..8df2c28060fbb 100644 --- a/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml +++ b/packages/SystemUI/res/layout/quick_status_bar_expanded_header.xml @@ -32,7 +32,6 @@ > - + android:padding="12dp" /> + @@ -116,7 +116,6 @@ android:id="@+id/date_time_group" android:layout_width="wrap_content" android:layout_height="19dp" - android:layout_marginTop="4dp" android:orientation="horizontal"> 26dp 14sp 16sp + 120dp 12dp 24dp 12sp diff --git a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java index f208470a2927c..d0f7e6eb805ab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QuickQSPanel.java @@ -17,12 +17,10 @@ package com.android.systemui.qs; import android.content.Context; -import android.content.res.ColorStateList; import android.content.res.Configuration; import android.util.AttributeSet; import android.view.Gravity; import android.view.View; -import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.Space; import com.android.systemui.R; @@ -103,7 +101,7 @@ public class QuickQSPanel extends QSPanel { private static class HeaderTileLayout extends LinearLayout implements QSTileLayout { - private final ImageView mDownArrow; + private final Space mEndSpacer; public HeaderTileLayout(Context context) { super(context); @@ -112,16 +110,10 @@ public class QuickQSPanel extends QSPanel { setGravity(Gravity.CENTER_VERTICAL); setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)); - int padding = - mContext.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding); - mDownArrow = new ImageView(context); - mDownArrow.setImageResource(R.drawable.ic_expand_more); - mDownArrow.setImageTintList(ColorStateList.valueOf(context.getResources().getColor( - android.R.color.white, null))); - mDownArrow.setLayoutParams(generateLayoutParams()); - mDownArrow.setPadding(padding, padding, padding, padding); + mEndSpacer = new Space(context); + mEndSpacer.setLayoutParams(generateLayoutParams()); updateDownArrowMargin(); - addView(mDownArrow); + addView(mEndSpacer); setOrientation(LinearLayout.HORIZONTAL); } @@ -132,10 +124,10 @@ public class QuickQSPanel extends QSPanel { } private void updateDownArrowMargin() { - LayoutParams params = (LayoutParams) mDownArrow.getLayoutParams(); + LayoutParams params = (LayoutParams) mEndSpacer.getLayoutParams(); params.setMarginStart(mContext.getResources().getDimensionPixelSize( R.dimen.qs_expand_margin)); - mDownArrow.setLayoutParams(params); + mEndSpacer.setLayoutParams(params); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java new file mode 100644 index 0000000000000..5e6b52b3b1af3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/TouchAnimator.java @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2016 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.qs; + +import android.animation.Keyframe; +import android.util.MathUtils; +import android.util.Property; +import android.view.animation.Interpolator; + +import java.util.ArrayList; +import java.util.List; + +/** + * Helper class, that handles similar properties as animators (delay, interpolators) + * but can have a float input as to the amount they should be in effect. This allows + * easier animation that tracks input. + * + * All "delays" and "times" are as fractions from 0-1. + */ +public class TouchAnimator { + + private final Object[] mTargets; + private final Property[] mProperties; + private final KeyframeSet[] mKeyframeSets; + private final float mStartDelay; + private final float mEndDelay; + private final float mSpan; + private final Interpolator mInterpolator; + private final Listener mListener; + private float mLastT; + + private TouchAnimator(Object[] targets, Property[] properties, KeyframeSet[] keyframeSets, + float startDelay, float endDelay, Interpolator interpolator, Listener listener) { + mTargets = targets; + mProperties = properties; + mKeyframeSets = keyframeSets; + mStartDelay = startDelay; + mEndDelay = endDelay; + mSpan = (1 - mEndDelay - mStartDelay); + mInterpolator = interpolator; + mListener = listener; + } + + public void setPosition(float fraction) { + float t = MathUtils.constrain((fraction - mStartDelay) / mSpan, 0, 1); + if (mInterpolator != null) { + t = mInterpolator.getInterpolation(t); + } + if (mListener != null) { + if (mLastT == 0 || mLastT == 1) { + if (t != 0) { + mListener.onAnimationStarted(); + } + } else if (t == 1) { + mListener.onAnimationAtEnd(); + } else if (t == 0) { + mListener.onAnimationAtStart(); + } + mLastT = t; + } + for (int i = 0; i < mTargets.length; i++) { + Object value = mKeyframeSets[i].getValue(t); + mProperties[i].set(mTargets[i], value); + } + } + + public static class ListenerAdapter implements Listener { + @Override + public void onAnimationAtStart() { } + + @Override + public void onAnimationAtEnd() { } + + @Override + public void onAnimationStarted() { } + } + + public interface Listener { + /** + * Called when the animator moves into a position of "0". Start and end delays are + * taken into account, so this position may cover a range of fractional inputs. + */ + void onAnimationAtStart(); + + /** + * Called when the animator moves into a position of "0". Start and end delays are + * taken into account, so this position may cover a range of fractional inputs. + */ + void onAnimationAtEnd(); + + /** + * Called when the animator moves out of the start or end position and is in a transient + * state. + */ + void onAnimationStarted(); + } + + public static class Builder { + private List mTargets = new ArrayList<>(); + private List mProperties = new ArrayList<>(); + private List mValues = new ArrayList<>(); + + private float mStartDelay; + private float mEndDelay; + private Interpolator mInterpolator; + private Listener mListener; + + public Builder addFloat(Object target, String property, float... values) { + add(target, property, KeyframeSet.ofFloat(values)); + return this; + } + + public Builder addInt(Object target, String property, int... values) { + add(target, property, KeyframeSet.ofInt(values)); + return this; + } + + private void add(Object target, String property, KeyframeSet keyframeSet) { + mTargets.add(target); + // TODO: Optimize the properties here, to use those in View when possible. + mProperties.add(Property.of(target.getClass(), float.class, property)); + mValues.add(keyframeSet); + } + + public Builder setStartDelay(float startDelay) { + mStartDelay = startDelay; + return this; + } + + public Builder setEndDelay(float endDelay) { + mEndDelay = endDelay; + return this; + } + + public Builder setInterpolator(Interpolator intepolator) { + mInterpolator = intepolator; + return this; + } + + public Builder setListener(Listener listener) { + mListener = listener; + return this; + } + + public TouchAnimator build() { + return new TouchAnimator(mTargets.toArray(new Object[mTargets.size()]), + mProperties.toArray(new Property[mProperties.size()]), + mValues.toArray(new KeyframeSet[mValues.size()]), + mStartDelay, mEndDelay, mInterpolator, mListener); + } + } + + private static abstract class KeyframeSet { + + private final Keyframe[] mKeyframes; + + public KeyframeSet(Keyframe[] keyframes) { + mKeyframes = keyframes; + } + + Object getValue(float fraction) { + int i; + for (i = 1; i < mKeyframes.length && fraction > mKeyframes[i].getFraction(); i++) ; + Keyframe first = mKeyframes[i - 1]; + Keyframe second = mKeyframes[i]; + float amount = (fraction - first.getFraction()) + / (second.getFraction() - first.getFraction()); + return interpolate(first, second, amount); + } + + protected abstract Object interpolate(Keyframe first, Keyframe second, float amount); + + public static KeyframeSet ofInt(int... values) { + int numKeyframes = values.length; + Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes, 2)]; + if (numKeyframes == 1) { + keyframes[0] = Keyframe.ofInt(0f); + keyframes[1] = Keyframe.ofInt(1f, values[0]); + } else { + keyframes[0] = Keyframe.ofInt(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + keyframes[i] = Keyframe.ofInt((float) i / (numKeyframes - 1), values[i]); + } + } + return new IntKeyframeSet(keyframes); + } + + public static KeyframeSet ofFloat(float... values) { + int numKeyframes = values.length; + Keyframe keyframes[] = new Keyframe[Math.max(numKeyframes, 2)]; + if (numKeyframes == 1) { + keyframes[0] = Keyframe.ofFloat(0f); + keyframes[1] = Keyframe.ofFloat(1f, values[0]); + } else { + keyframes[0] = Keyframe.ofFloat(0f, values[0]); + for (int i = 1; i < numKeyframes; ++i) { + keyframes[i] = Keyframe.ofFloat((float) i / (numKeyframes - 1), values[i]); + } + } + return new FloatKeyframeSet(keyframes); + } + } + + public static class FloatKeyframeSet extends KeyframeSet { + public FloatKeyframeSet(Keyframe[] keyframes) { + super(keyframes); + } + + @Override + protected Object interpolate(Keyframe first, Keyframe second, float amount) { + float firstFloat = (float) first.getValue(); + float secondFloat = (float) second.getValue(); + return firstFloat + (secondFloat - firstFloat) * amount; + } + } + + public static class IntKeyframeSet extends KeyframeSet { + public IntKeyframeSet(Keyframe[] keyframes) { + super(keyframes); + } + + @Override + protected Object interpolate(Keyframe first, Keyframe second, float amount) { + int firstFloat = (int) first.getValue(); + int secondFloat = (int) second.getValue(); + return (int) (firstFloat + (secondFloat - firstFloat) * amount); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ExpandableIndicator.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ExpandableIndicator.java new file mode 100644 index 0000000000000..8c7c71f111594 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ExpandableIndicator.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2016 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.phone; + +import android.content.Context; +import android.graphics.drawable.AnimatedVectorDrawable; +import android.util.AttributeSet; +import android.widget.ImageView; +import com.android.systemui.R; + +public class ExpandableIndicator extends ImageView { + + private boolean mExpanded; + + public ExpandableIndicator(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + final int res = mExpanded ? R.drawable.ic_volume_collapse_animation + : R.drawable.ic_volume_expand_animation; + setImageResource(res); + } + + public void setExpanded(boolean expanded) { + if (expanded == mExpanded) return; + mExpanded = expanded; + final int res = mExpanded ? R.drawable.ic_volume_expand_animation + : R.drawable.ic_volume_collapse_animation; + // workaround to reset drawable + final AnimatedVectorDrawable avd = (AnimatedVectorDrawable) getContext() + .getDrawable(res).getConstantState().newDrawable(); + setImageDrawable(avd); + avd.start(); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java index 3bb141a3621bc..bd5bac2506fb9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QuickStatusBarHeader.java @@ -26,6 +26,7 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.RippleDrawable; import android.util.AttributeSet; import android.view.View; +import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; @@ -36,15 +37,21 @@ import com.android.systemui.R; import com.android.systemui.qs.QSPanel; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QuickQSPanel; +import com.android.systemui.qs.TouchAnimator; +import com.android.systemui.qs.TouchAnimator.Listener; import com.android.systemui.statusbar.policy.BatteryController; import com.android.systemui.statusbar.policy.NextAlarmController; +import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback; import com.android.systemui.statusbar.policy.UserInfoController; import com.android.systemui.tuner.TunerService; public class QuickStatusBarHeader extends BaseStatusBarHeader implements - NextAlarmController.NextAlarmChangeCallback, View.OnClickListener { + NextAlarmChangeCallback, OnClickListener, Listener { private static final String TAG = "QuickStatusBarHeader"; + + private static final float EXPAND_INDICATOR_THRESHOLD = .8f; + private ActivityStarter mActivityStarter; private NextAlarmController mNextAlarmController; private SettingsButton mSettingsButton; @@ -58,11 +65,12 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements private boolean mExpanded; private boolean mAlarmShowing; - private ViewGroup mExpandedGroup; private ViewGroup mDateTimeGroup; private ViewGroup mDateTimeAlarmGroup; private TextView mEmergencyOnly; + private ExpandableIndicator mExpandIndicator; + private boolean mListening; private AlarmManager.AlarmClockInfo mNextAlarm; @@ -73,8 +81,15 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements private float mDateTimeTranslation; private float mDateTimeAlarmTranslation; - private float mExpansionFraction; private float mDateScaleFactor; + private float mGearTranslation; + + private TouchAnimator mAnimator; + private TouchAnimator mSecondHalfAnimator; + private TouchAnimator mFirstHalfAnimator; + private TouchAnimator mDateSizeAnimator; + private TouchAnimator mAlarmTranslation; + private float mExpansionAmount; public QuickStatusBarHeader(Context context, AttributeSet attrs) { super(context, attrs); @@ -89,8 +104,10 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements mDateTimeAlarmGroup = (ViewGroup) findViewById(R.id.date_time_alarm_group); mDateTimeAlarmGroup.findViewById(R.id.empty_time_view).setVisibility(View.GONE); mDateTimeGroup = (ViewGroup) findViewById(R.id.date_time_group); + mDateTimeGroup.setPivotX(0); + mDateTimeGroup.setPivotY(0); - mExpandedGroup = (ViewGroup) findViewById(R.id.expanded_group); + mExpandIndicator = (ExpandableIndicator) findViewById(R.id.expand_indicator); mHeaderQsPanel = (QuickQSPanel) findViewById(R.id.quick_qs_panel); @@ -131,6 +148,8 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements FontSizeUtils.updateFontSize(mAlarmStatus, R.dimen.qs_date_collapsed_size); FontSizeUtils.updateFontSize(mEmergencyOnly, R.dimen.qs_emergency_calls_only_text_size); + mGearTranslation = mContext.getResources().getDimension(R.dimen.qs_header_gear_translation); + mDateTimeTranslation = mContext.getResources().getDimension( R.dimen.qs_date_anim_translation); mDateTimeAlarmTranslation = mContext.getResources().getDimension( @@ -139,8 +158,31 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements R.dimen.qs_date_collapsed_text_size); float dateExpandedSize = mContext.getResources().getDimension( R.dimen.qs_date_text_size); - mDateScaleFactor = dateExpandedSize / dateCollapsedSize - 1; + mDateScaleFactor = dateExpandedSize / dateCollapsedSize; updateDateTimePosition(); + + mAnimator = new TouchAnimator.Builder() + .addFloat(mSettingsContainer, "translationY", -mGearTranslation, 0) + .addFloat(mMultiUserSwitch, "translationY", -mGearTranslation, 0) + .addFloat(mSettingsButton, "rotation", -90, 0) + .setListener(this) + .build(); + mSecondHalfAnimator = new TouchAnimator.Builder() + .addFloat(mSettingsButton, "rotation", -90, 0) + .addFloat(mAlarmStatus, "alpha", 0, 1) + .addFloat(mEmergencyOnly, "alpha", 0, 1) + .setStartDelay(.5f) + .build(); + mFirstHalfAnimator = new TouchAnimator.Builder() + .addFloat(mAlarmStatusCollapsed, "alpha", 1, 0) + .addFloat(mHeaderQsPanel, "alpha", 1, 0) + .setEndDelay(.5f) + .build(); + mDateSizeAnimator = new TouchAnimator.Builder() + .addFloat(mDateTimeGroup, "scaleX", 1, mDateScaleFactor) + .addFloat(mDateTimeGroup, "scaleY", 1, mDateScaleFactor) + .setStartDelay(.36f) + .build(); } @Override @@ -165,45 +207,52 @@ public class QuickStatusBarHeader extends BaseStatusBarHeader implements if (nextAlarm != null) { mAlarmStatus.setText(KeyguardStatusView.formatNextAlarm(getContext(), nextAlarm)); } - mAlarmShowing = nextAlarm != null; - updateEverything(); + if (mAlarmShowing != (nextAlarm != null)) { + mAlarmShowing = nextAlarm != null; + updateEverything(); + } } @Override public void setExpansion(float headerExpansionFraction) { - mExpansionFraction = headerExpansionFraction; + mExpansionAmount = headerExpansionFraction; + mAnimator.setPosition(headerExpansionFraction); + mSecondHalfAnimator.setPosition(headerExpansionFraction); + mFirstHalfAnimator.setPosition(headerExpansionFraction); + mDateSizeAnimator.setPosition(headerExpansionFraction); + mAlarmTranslation.setPosition(headerExpansionFraction); - mExpandedGroup.setAlpha(headerExpansionFraction); - mExpandedGroup.setVisibility(headerExpansionFraction > 0 ? View.VISIBLE : View.INVISIBLE); - - mHeaderQsPanel.setAlpha(1 - headerExpansionFraction); - mHeaderQsPanel.setVisibility(headerExpansionFraction < 1 ? View.VISIBLE : View.INVISIBLE); - - mAlarmStatus.setAlpha(headerExpansionFraction); - mAlarmStatusCollapsed.setAlpha(1 - headerExpansionFraction); updateAlarmVisibilities(); - float textScale = headerExpansionFraction * mDateScaleFactor; - mDateTimeGroup.setScaleX(1 + textScale); - mDateTimeGroup.setScaleY(1 + textScale); - mDateTimeGroup.setTranslationX(textScale * mDateTimeGroup.getWidth() / 2); - mDateTimeGroup.setTranslationY(textScale * mDateTimeGroup.getHeight() / 2); - updateDateTimePosition(); + mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD); + } - mEmergencyOnly.setAlpha(headerExpansionFraction); + @Override + public void onAnimationAtStart() { + } + + @Override + public void onAnimationAtEnd() { + mHeaderQsPanel.setVisibility(View.INVISIBLE); + } + + @Override + public void onAnimationStarted() { + mHeaderQsPanel.setVisibility(View.VISIBLE); } private void updateAlarmVisibilities() { - mAlarmStatus.setVisibility(mAlarmShowing && mExpansionFraction > 0 - ? View.VISIBLE : View.INVISIBLE); - mAlarmStatusCollapsed.setVisibility(mAlarmShowing && mExpansionFraction < 1 - ? View.VISIBLE : View.INVISIBLE); + mAlarmStatus.setVisibility(mAlarmShowing ? View.VISIBLE : View.INVISIBLE); + mAlarmStatusCollapsed.setVisibility(mAlarmShowing ? View.VISIBLE : View.INVISIBLE); } private void updateDateTimePosition() { - float translation = mAlarmShowing ? mDateTimeAlarmTranslation - : mDateTimeTranslation; - mDateTimeAlarmGroup.setTranslationY(mExpansionFraction * translation); + // This one has its own because we have to rebuild it every time the alarm state changes. + mAlarmTranslation = new TouchAnimator.Builder() + .addFloat(mDateTimeAlarmGroup, "translationY", 0, mAlarmShowing + ? mDateTimeAlarmTranslation : mDateTimeTranslation) + .build(); + mAlarmTranslation.setPosition(mExpansionAmount); } public void setListening(boolean listening) {