Changed the transformation from when switching notification views

Change-Id: I2af3c2f36787d208be7745dabae96903df256156
This commit is contained in:
Selim Cinek
2015-12-29 15:12:23 +01:00
parent b65c6dbb2c
commit 4ffd63611a
17 changed files with 1047 additions and 76 deletions

View File

@@ -305,4 +305,13 @@ public class NotificationHeaderView extends LinearLayout {
}
return this;
}
public ImageView getExpandButton() {
return mExpandButton;
}
@Override
public boolean hasOverlappingRendering() {
return false;
}
}

View File

@@ -50,6 +50,10 @@
<!-- For notification icons for which targetSdk < L, this caches whether the icon is grayscale -->
<item type="id" name="icon_is_grayscale" />
<item type="id" name="clip_children_tag" />
<item type="id" name="clip_children_set_tag" />
<item type="id" name="clip_to_padding_tag" />
<item type="id" name="image_icon_tag" />
<item type="id" name="is_clicked_heads_up_tag" />
</resources>

View File

@@ -0,0 +1,64 @@
/*
* 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;
import android.view.View;
import com.android.systemui.statusbar.phone.PhoneStatusBar;
/**
* A helper to fade views in and out.
*/
public class CrossFadeHelper {
public static final long ANIMATION_DURATION_LENGTH = 210;
public static void fadeOut(final View view, final Runnable endRunnable) {
view.animate().cancel();
view.animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_LENGTH)
.setInterpolator(PhoneStatusBar.ALPHA_OUT)
.withEndAction(new Runnable() {
@Override
public void run() {
if (endRunnable != null) {
endRunnable.run();
}
view.setVisibility(View.INVISIBLE);
}
});
if (view.hasOverlappingRendering()) {
view.animate().withLayer();
}
}
public static void fadeIn(final View view) {
view.animate().cancel();
if (view.getVisibility() == View.INVISIBLE) {
view.setAlpha(0.0f);
view.setVisibility(View.VISIBLE);
}
view.animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION_LENGTH)
.setInterpolator(PhoneStatusBar.ALPHA_IN);
if (view.hasOverlappingRendering()) {
view.animate().withLayer();
}
}
}

View File

@@ -635,6 +635,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
mPrivateLayout.updateExpandButtons(isExpandable());
}
@Override
public void setClipToActualHeight(boolean clipToActualHeight) {
super.setClipToActualHeight(clipToActualHeight);
getShowingLayout().setClipToActualHeight(clipToActualHeight);
}
/**
* @return whether the user has changed the expansion state
*/
@@ -1040,7 +1046,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView {
addView(mNotificationHeader, indexOfChild(mChildrenContainer) + 1);
} else {
header.reapply(getContext(), mNotificationHeader);
mNotificationHeaderWrapper.notifyContentUpdated();
mNotificationHeaderWrapper.notifyContentUpdated(mEntry.notification);
}
updateHeaderExpandButton();
updateChildrenHeaderAppearance();

View File

@@ -45,6 +45,7 @@ public abstract class ExpandableView extends FrameLayout {
private static Rect mClipRect = new Rect();
private boolean mWillBeGone;
private int mMinClipTopAmount = 0;
private boolean mClipToActualHeight = true;
public ExpandableView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -326,12 +327,21 @@ public abstract class ExpandableView extends FrameLayout {
}
private void updateClipping() {
int top = mClipTopOptimization;
if (top >= getActualHeight()) {
top = getActualHeight() - 1;
if (mClipToActualHeight) {
int top = mClipTopOptimization;
if (top >= getActualHeight()) {
top = getActualHeight() - 1;
}
mClipRect.set(0, top, getWidth(), getActualHeight());
setClipBounds(mClipRect);
} else {
setClipBounds(null);
}
mClipRect.set(0, top, getWidth(), getActualHeight());
setClipBounds(mClipRect);
}
public void setClipToActualHeight(boolean clipToActualHeight) {
mClipToActualHeight = clipToActualHeight;
updateClipping();
}
public int getClipTopOptimization() {

View File

@@ -20,9 +20,6 @@ import android.app.Notification;
import android.app.RemoteInput;
import android.content.Context;
import android.graphics.Outline;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.os.Build;
import android.service.notification.StatusBarNotification;
@@ -32,8 +29,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.FrameLayout;
import com.android.systemui.R;
@@ -49,7 +44,6 @@ import com.android.systemui.statusbar.policy.RemoteInputView;
*/
public class NotificationContentView extends FrameLayout {
private static final long ANIMATION_DURATION_LENGTH = 170;
private static final int VISIBLE_TYPE_CONTRACTED = 0;
private static final int VISIBLE_TYPE_EXPANDED = 1;
private static final int VISIBLE_TYPE_HEADSUP = 2;
@@ -57,7 +51,6 @@ public class NotificationContentView extends FrameLayout {
private final Rect mClipBounds = new Rect();
private final int mRoundRectRadius;
private final Interpolator mLinearInterpolator = new LinearInterpolator();
private final boolean mRoundRectClippingEnabled;
private final int mMinContractedHeight;
@@ -76,7 +69,6 @@ public class NotificationContentView extends FrameLayout {
private int mUnrestrictedContentHeight;
private int mVisibleType = VISIBLE_TYPE_CONTRACTED;
private boolean mDark;
private final Paint mFadePaint = new Paint();
private boolean mAnimate;
private boolean mIsHeadsUp;
private boolean mShowingLegacyBackground;
@@ -108,11 +100,11 @@ public class NotificationContentView extends FrameLayout {
private OnClickListener mExpandClickListener;
private boolean mBeforeN;
private boolean mExpandable;
private boolean mClipToActualHeight = true;
public NotificationContentView(Context context, AttributeSet attrs) {
super(context, attrs);
mHybridViewManager = new HybridNotificationViewManager(getContext(), this);
mFadePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.ADD));
mRoundRectRadius = getResources().getDimensionPixelSize(
R.dimen.notification_material_rounded_rect_radius);
mRoundRectClippingEnabled = getResources().getBoolean(
@@ -357,8 +349,17 @@ public class NotificationContentView extends FrameLayout {
}
private void updateClipping() {
mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
setClipBounds(mClipBounds);
if (mClipToActualHeight) {
mClipBounds.set(0, mClipTopAmount, getWidth(), mContentHeight);
setClipBounds(mClipBounds);
} else {
setClipBounds(null);
}
}
public void setClipToActualHeight(boolean clipToActualHeight) {
mClipToActualHeight = clipToActualHeight;
updateClipping();
}
private void selectLayout(boolean animate, boolean force) {
@@ -371,7 +372,7 @@ public class NotificationContentView extends FrameLayout {
|| (visibleType == VISIBLE_TYPE_HEADSUP && mHeadsUpChild != null)
|| (visibleType == VISIBLE_TYPE_SINGLELINE && mSingleLineView != null)
|| visibleType == VISIBLE_TYPE_CONTRACTED)) {
runSwitchAnimation(visibleType);
animateToVisibleType(visibleType);
} else {
updateViewVisibilities(visibleType);
}
@@ -381,57 +382,53 @@ public class NotificationContentView extends FrameLayout {
private void updateViewVisibilities(int visibleType) {
boolean contractedVisible = visibleType == VISIBLE_TYPE_CONTRACTED;
mContractedChild.setVisibility(contractedVisible ? View.VISIBLE : View.INVISIBLE);
mContractedChild.setAlpha(contractedVisible ? 1f : 0f);
mContractedChild.setLayerType(LAYER_TYPE_NONE, null);
mContractedWrapper.setVisible(contractedVisible);
if (mExpandedChild != null) {
boolean expandedVisible = visibleType == VISIBLE_TYPE_EXPANDED;
mExpandedChild.setVisibility(expandedVisible ? View.VISIBLE : View.INVISIBLE);
mExpandedChild.setAlpha(expandedVisible ? 1f : 0f);
mExpandedChild.setLayerType(LAYER_TYPE_NONE, null);
mExpandedWrapper.setVisible(expandedVisible);
}
if (mHeadsUpChild != null) {
boolean headsUpVisible = visibleType == VISIBLE_TYPE_HEADSUP;
mHeadsUpChild.setVisibility(headsUpVisible ? View.VISIBLE : View.INVISIBLE);
mHeadsUpChild.setAlpha(headsUpVisible ? 1f : 0f);
mHeadsUpChild.setLayerType(LAYER_TYPE_NONE, null);
mHeadsUpWrapper.setVisible(headsUpVisible);
}
if (mSingleLineView != null) {
boolean singleLineVisible = visibleType == VISIBLE_TYPE_SINGLELINE;
mSingleLineView.setVisibility(singleLineVisible ? View.VISIBLE : View.INVISIBLE);
mSingleLineView.setAlpha(singleLineVisible ? 1f : 0f);
mSingleLineView.setLayerType(LAYER_TYPE_NONE, null);
mSingleLineView.setVisible(singleLineVisible);
}
setLayerType(LAYER_TYPE_NONE, null);
updateRoundRectClipping();
}
private void runSwitchAnimation(int visibleType) {
View shownView = getViewForVisibleType(visibleType);
View hiddenView = getViewForVisibleType(mVisibleType);
shownView.setVisibility(View.VISIBLE);
hiddenView.setVisibility(View.VISIBLE);
shownView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
hiddenView.setLayerType(LAYER_TYPE_HARDWARE, mFadePaint);
setLayerType(LAYER_TYPE_HARDWARE, null);
hiddenView.animate()
.alpha(0f)
.setDuration(ANIMATION_DURATION_LENGTH)
.setInterpolator(mLinearInterpolator)
.withEndAction(null); // In case we have multiple changes in one frame.
shownView.animate()
.alpha(1f)
.setDuration(ANIMATION_DURATION_LENGTH)
.setInterpolator(mLinearInterpolator)
.withEndAction(new Runnable() {
@Override
public void run() {
updateViewVisibilities(mVisibleType);
}
});
private void animateToVisibleType(int visibleType) {
final TransformableView shownView = getTransformableViewForVisibleType(visibleType);
final TransformableView hiddenView = getTransformableViewForVisibleType(mVisibleType);
shownView.transformFrom(hiddenView);
getViewForVisibleType(visibleType).setVisibility(View.VISIBLE);
hiddenView.transformTo(shownView, new Runnable() {
@Override
public void run() {
hiddenView.setVisible(false);
}
});
updateRoundRectClipping();
}
/**
* @param visibleType one of the static enum types in this view
* @return the corresponding transformable view according to the given visible type
*/
private TransformableView getTransformableViewForVisibleType(int visibleType) {
switch (visibleType) {
case VISIBLE_TYPE_EXPANDED:
return mExpandedWrapper;
case VISIBLE_TYPE_HEADSUP:
return mHeadsUpWrapper;
case VISIBLE_TYPE_SINGLELINE:
return mSingleLineView;
default:
return mContractedWrapper;
}
}
/**
* @param visibleType one of the static enum types in this view
* @return the corresponding view according to the given visible type
@@ -520,14 +517,14 @@ public class NotificationContentView extends FrameLayout {
applyRemoteInput(entry);
selectLayout(false /* animate */, true /* force */);
if (mContractedChild != null) {
mContractedWrapper.notifyContentUpdated();
mContractedWrapper.notifyContentUpdated(entry.notification);
mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
}
if (mExpandedChild != null) {
mExpandedWrapper.notifyContentUpdated();
mExpandedWrapper.notifyContentUpdated(entry.notification);
}
if (mHeadsUpChild != null) {
mHeadsUpWrapper.notifyContentUpdated();
mHeadsUpWrapper.notifyContentUpdated(entry.notification);
}
updateRoundRectClipping();
}

View File

@@ -27,6 +27,8 @@ import android.graphics.ColorMatrixColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.drawable.Drawable;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.view.NotificationHeaderView;
import android.view.View;
import android.view.animation.AnimationUtils;
@@ -35,6 +37,7 @@ import android.widget.ImageView;
import com.android.systemui.R;
import com.android.systemui.ViewInvertHelper;
import com.android.systemui.statusbar.notification.TransformState;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import java.util.ArrayList;
@@ -52,6 +55,8 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
protected final Interpolator mLinearOutSlowInInterpolator;
protected final ViewInvertHelper mInvertHelper;
protected final ViewTransformationHelper mTransformationHelper;
protected int mColor;
private ImageView mIcon;
@@ -64,7 +69,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(ctx,
android.R.interpolator.linear_out_slow_in);
mInvertHelper = new ViewInvertHelper(ctx, NotificationPanelView.DOZE_ANIMATION_DURATION);
mTransformationHelper = new ViewTransformationHelper();
resolveHeaderViews();
updateInvertHelper();
}
protected void resolveHeaderViews() {
@@ -73,12 +80,6 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
mColor = resolveColor(mExpandButton);
mNotificationHeader = (NotificationHeaderView) mView.findViewById(
com.android.internal.R.id.notification_header);
for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
View child = mNotificationHeader.getChildAt(i);
if (child != mIcon) {
mInvertHelper.addTarget(child);
}
}
}
private int resolveColor(ImageView icon) {
@@ -92,10 +93,26 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
}
@Override
public void notifyContentUpdated() {
mInvertHelper.clearTargets();
public void notifyContentUpdated(StatusBarNotification notification) {
// Reinspect the notification.
resolveHeaderViews();
updateInvertHelper();
updateTransformedTypes();
}
protected void updateInvertHelper() {
mInvertHelper.clearTargets();
for (int i = 0; i < mNotificationHeader.getChildCount(); i++) {
View child = mNotificationHeader.getChildAt(i);
if (child != mIcon) {
mInvertHelper.addTarget(child);
}
}
}
protected void updateTransformedTypes() {
mTransformationHelper.reset();
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_HEADER, mNotificationHeader);
}
@Override
@@ -236,4 +253,25 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper {
public NotificationHeaderView getNotificationHeader() {
return mNotificationHeader;
}
@Override
public TransformState getCurrentState(int fadingView) {
return mTransformationHelper.getCurrentState(fadingView);
}
@Override
public void transformTo(TransformableView notification, Runnable endRunnable) {
mTransformationHelper.transformTo(notification, endRunnable);
}
@Override
public void transformFrom(TransformableView notification) {
mTransformationHelper.transformFrom(notification);
}
@Override
public void setVisible(boolean visible) {
super.setVisible(visible);
mTransformationHelper.setVisible(visible);
}
}

View File

@@ -19,9 +19,17 @@ package com.android.systemui.statusbar;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.service.notification.StatusBarNotification;
import android.util.ArrayMap;
import android.view.View;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.systemui.statusbar.notification.ImageTransformState;
import com.android.systemui.statusbar.notification.TransformState;
import java.util.ArrayList;
/**
* Wraps a notification view inflated from a template.
@@ -32,15 +40,21 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
protected ImageView mPicture;
private ProgressBar mProgressBar;
private TextView mTitle;
private TextView mText;
protected NotificationTemplateViewWrapper(Context ctx, View view) {
super(ctx, view);
resolveTemplateViews();
}
private void resolveTemplateViews() {
View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
private void resolveTemplateViews(StatusBarNotification notification) {
mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
if (notification != null) {
mPicture.setTag(ImageTransformState.ICON_TAG,
notification.getNotification().getLargeIcon());
}
mTitle = (TextView) mView.findViewById(com.android.internal.R.id.title);
mText = (TextView) mView.findViewById(com.android.internal.R.id.text);
final View progress = mView.findViewById(com.android.internal.R.id.progress);
if (progress instanceof ProgressBar) {
mProgressBar = (ProgressBar) progress;
@@ -48,17 +62,50 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
// It's still a viewstub
mProgressBar = null;
}
}
@Override
public void notifyContentUpdated(StatusBarNotification notification) {
// Reinspect the notification.
resolveTemplateViews(notification);
super.notifyContentUpdated(notification);
addRemainingTransformTypes();
}
/**
* Adds the remaining TransformTypes to the TransformHelper. This is done to make sure that each
* child is faded automatically and doesn't have to be manually added.
* The keys used for the views are the ids.
*/
private void addRemainingTransformTypes() {
}
@Override
protected void updateInvertHelper() {
super.updateInvertHelper();
View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
if (mainColumn != null) {
mInvertHelper.addTarget(mainColumn);
}
}
@Override
public void notifyContentUpdated() {
super.notifyContentUpdated();
// Reinspect the notification.
resolveTemplateViews();
protected void updateTransformedTypes() {
// This also clears the existing types
super.updateTransformedTypes();
if (mTitle != null) {
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitle);
}
if (mText != null) {
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mText);
}
if (mPicture != null) {
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_IMAGE, mPicture);
}
if (mProgressBar != null) {
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_PROGRESS, mProgressBar);
}
}
@Override

View File

@@ -17,14 +17,17 @@
package com.android.systemui.statusbar;
import android.content.Context;
import android.service.notification.StatusBarNotification;
import android.view.NotificationHeaderView;
import android.view.View;
import com.android.systemui.statusbar.notification.TransformState;
/**
* Wraps the actual notification content view; used to implement behaviors which are different for
* the individual templates and custom views.
*/
public abstract class NotificationViewWrapper {
public abstract class NotificationViewWrapper implements TransformableView {
protected final View mView;
@@ -53,8 +56,9 @@ public abstract class NotificationViewWrapper {
/**
* Notifies this wrapper that the content of the view might have changed.
* @param notification
*/
public void notifyContentUpdated() {};
public void notifyContentUpdated(StatusBarNotification notification) {};
/**
* @return true if this template might need to be clipped with a round rect to make it look
@@ -78,4 +82,26 @@ public abstract class NotificationViewWrapper {
public NotificationHeaderView getNotificationHeader() {
return null;
}
@Override
public TransformState getCurrentState(int fadingView) {
return null;
}
@Override
public void transformTo(TransformableView notification, Runnable endRunnable) {
// By default we are fading out completely
CrossFadeHelper.fadeOut(mView, endRunnable);
}
@Override
public void transformFrom(TransformableView notification) {
// By default we are fading in completely
CrossFadeHelper.fadeIn(mView);
}
@Override
public void setVisible(boolean visible) {
mView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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;
import com.android.systemui.statusbar.notification.TransformState;
/**
* A view that can be transformed to and from.
*/
public interface TransformableView {
int TRANSFORMING_VIEW_HEADER = 0;
int TRANSFORMING_VIEW_TITLE = 1;
int TRANSFORMING_VIEW_TEXT = 2;
int TRANSFORMING_VIEW_IMAGE = 3;
int TRANSFORMING_VIEW_PROGRESS = 4;
/**
* Get the current state of a view in a transform animation
* @param fadingView which view we are interested in
* @return the current transform state of this viewtype
*/
TransformState getCurrentState(int fadingView);
/**
* Transform to the given view
* @param notification the view to transform to
*/
void transformTo(TransformableView notification, Runnable endRunnable);
/**
* Transform to this view from the given view
* @param notification the view to transform from
*/
void transformFrom(TransformableView notification);
/**
* Set this view to be fully visible or gone
* @param visible
*/
void setVisible(boolean visible);
}

View File

@@ -0,0 +1,108 @@
/*
* 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;
import android.os.Handler;
import android.util.ArrayMap;
import android.view.View;
import com.android.systemui.statusbar.notification.TransformState;
/**
* A view that can be transformed to and from.
*/
public class ViewTransformationHelper implements TransformableView {
private final Handler mHandler = new Handler();
private ArrayMap<Integer, View> mTransformedViews = new ArrayMap<>();
public void addTransformedView(int key, View transformedView) {
mTransformedViews.put(key, transformedView);
}
public void reset() {
mTransformedViews.clear();
}
@Override
public TransformState getCurrentState(int fadingView) {
View view = mTransformedViews.get(fadingView);
if (view != null && view.getVisibility() != View.GONE) {
return TransformState.createFrom(view);
}
return null;
}
@Override
public void transformTo(TransformableView notification, Runnable endRunnable) {
Runnable runnable = endRunnable;
for (Integer viewType : mTransformedViews.keySet()) {
TransformState ownState = getCurrentState(viewType);
if (ownState != null) {
TransformState otherState = notification.getCurrentState(viewType);
if (otherState != null) {
boolean run = ownState.transformViewTo(otherState, runnable);
otherState.recycle();
if (run) {
runnable = null;
}
} else {
// there's no other view available
CrossFadeHelper.fadeOut(mTransformedViews.get(viewType), runnable);
runnable = null;
}
ownState.recycle();
}
}
if (runnable != null) {
// We need to post, since the visible type is only set after the transformation is
// started
mHandler.post(runnable);
}
}
@Override
public void transformFrom(TransformableView notification) {
for (Integer viewType : mTransformedViews.keySet()) {
TransformState ownState = getCurrentState(viewType);
if (ownState != null) {
TransformState otherState = notification.getCurrentState(viewType);
if (otherState != null) {
ownState.transformViewFrom(otherState);
otherState.recycle();
} else {
// There's no other view, lets fade us in
// Certain views need to prepare the fade in and make sure its children are
// completely visible. An example is the notification header.
ownState.prepareFadeIn();
CrossFadeHelper.fadeIn(mTransformedViews.get(viewType));
}
ownState.recycle();
}
}
}
@Override
public void setVisible(boolean visible) {
for (Integer viewType : mTransformedViews.keySet()) {
TransformState ownState = getCurrentState(viewType);
if (ownState != null) {
ownState.setVisible(visible);
ownState.recycle();
}
}
}
}

View File

@@ -0,0 +1,148 @@
/*
* 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.notification;
import android.util.Pools;
import android.view.NotificationHeaderView;
import android.view.View;
import com.android.systemui.statusbar.CrossFadeHelper;
/**
* A transform state of a text view.
*/
public class HeaderTransformState extends TransformState {
private static Pools.SimplePool<HeaderTransformState> sInstancePool
= new Pools.SimplePool<>(40);
private View mExpandButton;
@Override
public void initFrom(View view) {
super.initFrom(view);
if (view instanceof NotificationHeaderView) {
NotificationHeaderView header = (NotificationHeaderView) view;
mExpandButton = header.getExpandButton();
}
}
@Override
public boolean transformViewTo(TransformState otherState, Runnable endRunnable) {
// if the transforming notification has a header, we have ensured that it looks the same
// but the expand button, so lets fade just that one.
if (!(mTransformedView instanceof NotificationHeaderView)) {
return false;
}
NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
int childCount = header.getChildCount();
for (int i = 0; i < childCount; i++) {
View headerChild = header.getChildAt(i);
if (headerChild.getVisibility() == View.GONE) {
continue;
}
if (headerChild != mExpandButton) {
headerChild.setVisibility(View.INVISIBLE);
} else {
CrossFadeHelper.fadeOut(mExpandButton, endRunnable);
}
}
return true;
}
@Override
public void transformViewFrom(TransformState otherState) {
// if the transforming notification has a header, we have ensured that it looks the same
// but the expand button, so lets fade just that one.
if (!(mTransformedView instanceof NotificationHeaderView)) {
return;
}
NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
header.setVisibility(View.VISIBLE);
header.setAlpha(1.0f);
int childCount = header.getChildCount();
for (int i = 0; i < childCount; i++) {
View headerChild = header.getChildAt(i);
if (headerChild.getVisibility() == View.GONE) {
continue;
}
if (headerChild != mExpandButton) {
headerChild.setVisibility(View.VISIBLE);
} else {
CrossFadeHelper.fadeIn(mExpandButton);
}
}
return;
}
public static HeaderTransformState obtain() {
HeaderTransformState instance = sInstancePool.acquire();
if (instance != null) {
return instance;
}
return new HeaderTransformState();
}
@Override
public void recycle() {
sInstancePool.release(this);
}
@Override
protected void reset() {
super.reset();
mExpandButton = null;
}
public void setVisible(boolean visible) {
super.setVisible(visible);
if (!(mTransformedView instanceof NotificationHeaderView)) {
return;
}
NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
int childCount = header.getChildCount();
for (int i = 0; i < childCount; i++) {
View headerChild = header.getChildAt(i);
if (headerChild.getVisibility() == View.GONE) {
continue;
}
headerChild.animate().cancel();
headerChild.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
if (headerChild == mExpandButton) {
headerChild.setAlpha(visible ? 1.0f : 0.0f);
}
}
}
@Override
public void prepareFadeIn() {
super.prepareFadeIn();
if (!(mTransformedView instanceof NotificationHeaderView)) {
return;
}
NotificationHeaderView header = (NotificationHeaderView) mTransformedView;
int childCount = header.getChildCount();
for (int i = 0; i < childCount; i++) {
View headerChild = header.getChildAt(i);
if (headerChild.getVisibility() == View.GONE) {
continue;
}
headerChild.animate().cancel();
headerChild.setVisibility(View.VISIBLE);
headerChild.setAlpha(1.0f);
}
}
}

View File

@@ -18,18 +18,28 @@ package com.android.systemui.statusbar.notification;
import android.annotation.Nullable;
import android.content.Context;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.AttributeSet;
import android.view.View;
import android.widget.TextView;
import com.android.keyguard.AlphaOptimizedLinearLayout;
import com.android.systemui.R;
import com.android.systemui.ViewInvertHelper;
import com.android.systemui.statusbar.TransformableView;
import com.android.systemui.statusbar.ViewTransformationHelper;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import java.util.ArrayList;
/**
* A hybrid view which may contain information about one ore more notifications.
*/
public class HybridNotificationView extends AlphaOptimizedLinearLayout {
public class HybridNotificationView extends AlphaOptimizedLinearLayout
implements TransformableView {
private ViewTransformationHelper mTransformationHelper;
protected TextView mTitleView;
protected TextView mTextView;
@@ -58,6 +68,9 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout {
mTitleView = (TextView) findViewById(R.id.notification_title);
mTextView = (TextView) findViewById(R.id.notification_text);
mInvertHelper = new ViewInvertHelper(this, NotificationPanelView.DOZE_ANIMATION_DURATION);
mTransformationHelper = new ViewTransformationHelper();
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TITLE, mTitleView);
mTransformationHelper.addTransformedView(TRANSFORMING_VIEW_TEXT, mTextView);
}
public void bind(CharSequence title) {
@@ -66,11 +79,38 @@ public class HybridNotificationView extends AlphaOptimizedLinearLayout {
public void bind(CharSequence title, CharSequence text) {
mTitleView.setText(title);
if (TextUtils.isEmpty(title)) {
mTitleView.setVisibility(GONE);
}
mTextView.setText(text);
if (TextUtils.isEmpty(text)) {
mTextView.setVisibility(GONE);
}
requestLayout();
}
public void setDark(boolean dark, boolean fade, long delay) {
mInvertHelper.setInverted(dark, fade, delay);
}
@Override
public TransformState getCurrentState(int fadingView) {
return mTransformationHelper.getCurrentState(fadingView);
}
@Override
public void transformTo(TransformableView notification, Runnable endRunnable) {
mTransformationHelper.transformTo(notification, endRunnable);
}
@Override
public void transformFrom(TransformableView notification) {
mTransformationHelper.transformFrom(notification);
}
@Override
public void setVisible(boolean visible) {
setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
mTransformationHelper.setVisible(visible);
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.notification;
import android.graphics.drawable.Icon;
import android.util.Pools;
import android.view.View;
import android.widget.ImageView;
import com.android.systemui.R;
/**
* A transform state of a image view.
*/
public class ImageTransformState extends TransformState {
public static final int ICON_TAG = R.id.image_icon_tag;
private static Pools.SimplePool<ImageTransformState> sInstancePool
= new Pools.SimplePool<>(40);
private Icon mIcon;
@Override
public void initFrom(View view) {
super.initFrom(view);
if (view instanceof ImageView) {
mIcon = (Icon) view.getTag(ICON_TAG);
}
}
@Override
protected boolean sameAs(TransformState otherState) {
if (otherState instanceof ImageTransformState) {
return mIcon.sameAs(((ImageTransformState) otherState).getIcon());
}
return super.sameAs(otherState);
}
public Icon getIcon() {
return mIcon;
}
public static ImageTransformState obtain() {
ImageTransformState instance = sInstancePool.acquire();
if (instance != null) {
return instance;
}
return new ImageTransformState();
}
@Override
public void recycle() {
sInstancePool.release(this);
}
@Override
protected void reset() {
super.reset();
mIcon = null;
}
}

View File

@@ -0,0 +1,49 @@
/*
* 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.notification;
import android.util.Pools;
/**
* A transform state of a progress view.
*/
public class ProgressTransformState extends TransformState {
private static Pools.SimplePool<ProgressTransformState> sInstancePool
= new Pools.SimplePool<>(40);
@Override
protected boolean sameAs(TransformState otherState) {
if (otherState instanceof ProgressTransformState) {
return true;
}
return super.sameAs(otherState);
}
public static ProgressTransformState obtain() {
ProgressTransformState instance = sInstancePool.acquire();
if (instance != null) {
return instance;
}
return new ProgressTransformState();
}
@Override
public void recycle() {
sInstancePool.release(this);
}
}

View File

@@ -0,0 +1,69 @@
/*
* 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.notification;
import android.text.TextUtils;
import android.util.Pools;
import android.view.View;
import android.widget.TextView;
/**
* A transform state of a mText view.
*/
public class TextViewTransformState extends TransformState {
private static Pools.SimplePool<TextViewTransformState> sInstancePool
= new Pools.SimplePool<>(40);
private CharSequence mText;
@Override
public void initFrom(View view) {
super.initFrom(view);
if (view instanceof TextView) {
TextView txt = (TextView) view;
mText = txt.getText();
}
}
@Override
protected boolean sameAs(TransformState otherState) {
if (otherState instanceof TextViewTransformState) {
TextViewTransformState otherTvs = (TextViewTransformState) otherState;
return TextUtils.equals(otherTvs.mText, mText);
}
return super.sameAs(otherState);
}
public static TextViewTransformState obtain() {
TextViewTransformState instance = sInstancePool.acquire();
if (instance != null) {
return instance;
}
return new TextViewTransformState();
}
@Override
public void recycle() {
sInstancePool.release(this);
}
@Override
protected void reset() {
super.reset();
mText = null;
}
}

View File

@@ -0,0 +1,227 @@
/*
* 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.notification;
import android.util.ArraySet;
import android.view.NotificationHeaderView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.statusbar.CrossFadeHelper;
import com.android.systemui.statusbar.ExpandableNotificationRow;
/**
* A transform state of a view.
*/
public abstract class TransformState {
private static final int CLIP_CLIPPING_SET = R.id.clip_children_set_tag;
private static final int CLIP_CHILDREN_TAG = R.id.clip_children_tag;
private static final int CLIP_TO_PADDING = R.id.clip_to_padding_tag;
protected View mTransformedView;
private int[] mOwnPosition = new int[2];
public void initFrom(View view) {
mTransformedView = view;
}
/**
* Transforms the {@link #mTransformedView} from the given transformviewstate
* @param otherState the state to transform from
*/
public void transformViewFrom(TransformState otherState) {
mTransformedView.animate().cancel();
if (sameAs(otherState)) {
// We have the same content, lets show ourselves
mTransformedView.setAlpha(1.0f);
mTransformedView.setVisibility(View.VISIBLE);
} else {
CrossFadeHelper.fadeIn(mTransformedView);
}
// lets animate the positions correctly
int[] otherPosition = otherState.getLocationOnScreen();
int[] ownStablePosition = getLaidOutLocationOnScreen();
mTransformedView.setTranslationX(otherPosition[0] - ownStablePosition[0]);
mTransformedView.setTranslationY(otherPosition[1] - ownStablePosition[1]);
mTransformedView.animate()
.translationX(0)
.translationY(0)
.setDuration(CrossFadeHelper.ANIMATION_DURATION_LENGTH)
.withEndAction(new Runnable() {
@Override
public void run() {
setClippingDeactivated(false);
}
});
setClippingDeactivated(true);
}
/**
* Transforms the {@link #mTransformedView} to the given transformviewstate
* @param otherState the state to transform from
* @param endRunnable a runnable to run at the end of the animation
* @return whether an animation was started
*/
public boolean transformViewTo(TransformState otherState, final Runnable endRunnable) {
mTransformedView.animate().cancel();
if (sameAs(otherState)) {
// We have the same text, lets show ourselfs
mTransformedView.setAlpha(0.0f);
mTransformedView.setVisibility(View.INVISIBLE);
return false;
} else {
CrossFadeHelper.fadeOut(mTransformedView, endRunnable);
}
// lets animate the positions correctly
int[] otherStablePosition = otherState.getLaidOutLocationOnScreen();
int[] ownPosition = getLaidOutLocationOnScreen();
mTransformedView.animate()
.translationX(otherStablePosition[0] - ownPosition[0])
.translationY(otherStablePosition[1] - ownPosition[1])
.setDuration(CrossFadeHelper.ANIMATION_DURATION_LENGTH)
.withEndAction(new Runnable() {
@Override
public void run() {
if (endRunnable != null) {
endRunnable.run();
}
setClippingDeactivated(false);
}
});
setClippingDeactivated(true);
return true;
}
private void setClippingDeactivated(boolean deactivated) {
ViewGroup view = (ViewGroup) mTransformedView.getParent();
while (true) {
ArraySet<View> clipSet = (ArraySet<View>) view.getTag(CLIP_CLIPPING_SET);
if (clipSet == null) {
clipSet = new ArraySet<>();
view.setTag(CLIP_CLIPPING_SET, clipSet);
}
Boolean clipChildren = (Boolean) view.getTag(CLIP_CHILDREN_TAG);
if (clipChildren == null) {
clipChildren = view.getClipChildren();
view.setTag(CLIP_CHILDREN_TAG, clipChildren);
}
Boolean clipToPadding = (Boolean) view.getTag(CLIP_TO_PADDING);
if (clipToPadding == null) {
clipToPadding = view.getClipToPadding();
view.setTag(CLIP_TO_PADDING, clipToPadding);
}
ExpandableNotificationRow row = view instanceof ExpandableNotificationRow
? (ExpandableNotificationRow) view
: null;
if (!deactivated) {
clipSet.remove(mTransformedView);
if (clipSet.isEmpty()) {
view.setClipChildren(clipChildren);
view.setClipToPadding(clipToPadding);
view.setTag(CLIP_CLIPPING_SET, null);
if (row != null) {
row.setClipToActualHeight(true);
}
}
} else {
clipSet.add(mTransformedView);
view.setClipChildren(false);
view.setClipToPadding(false);
if (row != null && row.isChildInGroup()) {
// We still want to clip to the parent's height
row.setClipToActualHeight(false);
}
}
if (row != null && !row.isChildInGroup()) {
return;
}
final ViewParent parent = view.getParent();
if (parent instanceof ViewGroup) {
view = (ViewGroup) parent;
} else {
return;
}
}
}
private int[] getLaidOutLocationOnScreen() {
int[] location = getLocationOnScreen();
location[0] -= mTransformedView.getTranslationX();
location[1] -= mTransformedView.getTranslationY();
return location;
}
private int[] getLocationOnScreen() {
mTransformedView.getLocationOnScreen(mOwnPosition);
return mOwnPosition;
}
protected boolean sameAs(TransformState otherState) {
return false;
}
public static TransformState createFrom(View view) {
if (view instanceof TextView) {
TextViewTransformState result = TextViewTransformState.obtain();
result.initFrom(view);
return result;
}
if (view instanceof NotificationHeaderView) {
HeaderTransformState result = HeaderTransformState.obtain();
result.initFrom(view);
return result;
}
if (view instanceof ImageView) {
ImageTransformState result = ImageTransformState.obtain();
result.initFrom(view);
return result;
}
if (view instanceof ProgressBar) {
ProgressTransformState result = ProgressTransformState.obtain();
result.initFrom(view);
return result;
}
return null;
}
public void recycle() {
reset();
}
protected void reset() {
mTransformedView = null;
}
public void setVisible(boolean visible) {
mTransformedView.animate().cancel();
mTransformedView.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
mTransformedView.setAlpha(visible ? 1.0f : 0.0f);
if (visible) {
mTransformedView.setTranslationX(0);
mTransformedView.setTranslationY(0);
}
}
public void prepareFadeIn() {
}
}