blob: 20e089be374c6e174c457fa40c272a6be558b7ae [file] [log] [blame]
/*
* Copyright (C) 2014 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.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Color;
import android.graphics.ColorFilter;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.text.TextUtils;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import android.widget.ImageView;
import android.widget.TextView;
import com.android.systemui.R;
import com.android.systemui.ViewInvertHelper;
import com.android.systemui.statusbar.phone.NotificationPanelView;
import java.util.ArrayList;
/**
* Wraps a notification view inflated from a template.
*/
public class NotificationTemplateViewWrapper extends NotificationViewWrapper {
private final ColorMatrix mGrayscaleColorMatrix = new ColorMatrix();
private final PorterDuffColorFilter mIconColorFilter = new PorterDuffColorFilter(
0, PorterDuff.Mode.SRC_ATOP);
private final int mIconDarkAlpha;
private final int mIconDarkColor;
private final Interpolator mLinearOutSlowInInterpolator;
private int mColor;
private ViewInvertHelper mInvertHelper;
private ImageView mIcon;
protected ImageView mPicture;
/**
* Whether the icon needs to be forced grayscale when in dark mode.
*/
private boolean mIconForceGraysaleWhenDark;
private TextView mSubText;
private TextView mInfoText;
private View mProfileBadge;
private View mThirdLine;
private ImageView mExpandButton;
private View mNotificationHeader;
private View.OnClickListener mExpandClickListener;
private HeaderTouchListener mHeaderTouchListener;
protected NotificationTemplateViewWrapper(Context ctx, View view) {
super(view);
mIconDarkAlpha = ctx.getResources().getInteger(R.integer.doze_small_icon_alpha);
mIconDarkColor =
ctx.getColor(R.color.doze_small_icon_background_color);
mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(ctx,
android.R.interpolator.linear_out_slow_in);
resolveViews();
}
private void resolveViews() {
View mainColumn = mView.findViewById(com.android.internal.R.id.notification_main_column);
mInvertHelper = mainColumn != null
? new ViewInvertHelper(mainColumn, NotificationPanelView.DOZE_ANIMATION_DURATION)
: null;
mIcon = (ImageView) mView.findViewById(com.android.internal.R.id.icon);
mPicture = (ImageView) mView.findViewById(com.android.internal.R.id.right_icon);
mSubText = (TextView) mView.findViewById(com.android.internal.R.id.text);
mInfoText = (TextView) mView.findViewById(com.android.internal.R.id.info);
mProfileBadge = mView.findViewById(com.android.internal.R.id.profile_badge_line3);
mThirdLine = mView.findViewById(com.android.internal.R.id.line3);
mExpandButton = (ImageView) mView.findViewById(com.android.internal.R.id.expand_button);
mColor = resolveColor(mExpandButton);
mNotificationHeader = mView.findViewById(com.android.internal.R.id.notification_header);
// Post to make sure the parent lays out its children before we get their bounds
mHeaderTouchListener = new HeaderTouchListener();
mExpandButton.post(new Runnable() {
@Override
public void run() {
// let's set up our touch regions
mHeaderTouchListener.bindTouchRects(mNotificationHeader, mIcon, mExpandButton);
}
});
// If the icon already has a color filter, we assume that we already forced the icon to be
// white when we created the notification.
final Drawable iconDrawable = mIcon != null ? mIcon.getDrawable() : null;
mIconForceGraysaleWhenDark = iconDrawable != null && iconDrawable.getColorFilter() != null;
}
private int resolveColor(ImageView icon) {
if (icon != null && icon.getDrawable() != null) {
ColorFilter filter = icon.getDrawable().getColorFilter();
if (filter instanceof PorterDuffColorFilter) {
return ((PorterDuffColorFilter) filter).getColor();
}
}
return 0;
}
@Override
public void notifyContentUpdated() {
super.notifyContentUpdated();
// Reinspect the notification.
resolveViews();
}
@Override
public void setDark(boolean dark, boolean fade, long delay) {
if (mInvertHelper != null) {
if (fade) {
mInvertHelper.fade(dark, delay);
} else {
mInvertHelper.update(dark);
}
}
if (mIcon != null) {
if (fade) {
fadeIconColorFilter(mIcon, dark, delay);
fadeIconAlpha(mIcon, dark, delay);
if (!mIconForceGraysaleWhenDark) {
fadeGrayscale(mIcon, dark, delay);
}
} else {
updateIconColorFilter(mIcon, dark);
updateIconAlpha(mIcon, dark);
if (!mIconForceGraysaleWhenDark) {
updateGrayscale(mIcon, dark);
}
}
}
setPictureGrayscale(dark, fade, delay);
}
protected void setPictureGrayscale(boolean grayscale, boolean fade, long delay) {
if (mPicture != null) {
if (fade) {
fadeGrayscale(mPicture, grayscale, delay);
} else {
updateGrayscale(mPicture, grayscale);
}
}
}
private void startIntensityAnimation(ValueAnimator.AnimatorUpdateListener updateListener,
boolean dark, long delay, Animator.AnimatorListener listener) {
float startIntensity = dark ? 0f : 1f;
float endIntensity = dark ? 1f : 0f;
ValueAnimator animator = ValueAnimator.ofFloat(startIntensity, endIntensity);
animator.addUpdateListener(updateListener);
animator.setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION);
animator.setInterpolator(mLinearOutSlowInInterpolator);
animator.setStartDelay(delay);
if (listener != null) {
animator.addListener(listener);
}
animator.start();
}
private void fadeIconColorFilter(final ImageView target, boolean dark, long delay) {
startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateIconColorFilter(target, (Float) animation.getAnimatedValue());
}
}, dark, delay, null /* listener */);
}
private void fadeIconAlpha(final ImageView target, boolean dark, long delay) {
startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float t = (float) animation.getAnimatedValue();
target.setImageAlpha((int) (255 * (1f - t) + mIconDarkAlpha * t));
}
}, dark, delay, null /* listener */);
}
protected void fadeGrayscale(final ImageView target, final boolean dark, long delay) {
startIntensityAnimation(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
updateGrayscaleMatrix((float) animation.getAnimatedValue());
target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix));
}
}, dark, delay, new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (!dark) {
target.setColorFilter(null);
}
}
});
}
private void updateIconColorFilter(ImageView target, boolean dark) {
updateIconColorFilter(target, dark ? 1f : 0f);
}
private void updateIconColorFilter(ImageView target, float intensity) {
int color = interpolateColor(mColor, mIconDarkColor, intensity);
mIconColorFilter.setColor(color);
Drawable iconDrawable = target.getDrawable();
// The background might be null for legacy notifications. Also, the notification might have
// been modified during the animation, so background might be null here.
if (iconDrawable != null) {
iconDrawable.mutate().setColorFilter(mIconColorFilter);
}
}
private void updateIconAlpha(ImageView target, boolean dark) {
target.setImageAlpha(dark ? mIconDarkAlpha : 255);
}
protected void updateGrayscale(ImageView target, boolean dark) {
if (dark) {
updateGrayscaleMatrix(1f);
target.setColorFilter(new ColorMatrixColorFilter(mGrayscaleColorMatrix));
} else {
target.setColorFilter(null);
}
}
@Override
public void setSubTextVisible(boolean visible) {
if (mSubText == null) {
return;
}
boolean subTextAvailable = !TextUtils.isEmpty(mSubText.getText());
if (visible && subTextAvailable) {
mSubText.setVisibility(View.VISIBLE);
} else {
mSubText.setVisibility(View.GONE);
}
// TODO: figure out what to do with the number (same place as contentInfo)
// work profile badge. For now we hide it since it looks nicer.
boolean infoAvailable = !TextUtils.isEmpty(mInfoText.getText());
if (visible && infoAvailable) {
mInfoText.setVisibility(View.VISIBLE);
} else {
mInfoText.setVisibility(View.GONE);
}
boolean showThirdLine = (visible && (infoAvailable || subTextAvailable))
|| mProfileBadge.getVisibility() == View.VISIBLE;
if (mThirdLine != null) {
if (showThirdLine) {
mThirdLine.setVisibility(View.VISIBLE);
} else {
mThirdLine.setVisibility(View.GONE);
}
}
}
@Override
public void updateExpandability(boolean expandable, View.OnClickListener onClickListener) {
mExpandButton.setVisibility(expandable ? View.VISIBLE : View.GONE);
mNotificationHeader.setOnTouchListener(expandable ? mHeaderTouchListener : null);
mExpandClickListener = onClickListener;
}
private void updateGrayscaleMatrix(float intensity) {
mGrayscaleColorMatrix.setSaturation(1 - intensity);
}
private static int interpolateColor(int source, int target, float t) {
int aSource = Color.alpha(source);
int rSource = Color.red(source);
int gSource = Color.green(source);
int bSource = Color.blue(source);
int aTarget = Color.alpha(target);
int rTarget = Color.red(target);
int gTarget = Color.green(target);
int bTarget = Color.blue(target);
return Color.argb(
(int) (aSource * (1f - t) + aTarget * t),
(int) (rSource * (1f - t) + rTarget * t),
(int) (gSource * (1f - t) + gTarget * t),
(int) (bSource * (1f - t) + bTarget * t));
}
public class HeaderTouchListener implements View.OnTouchListener {
private final ArrayList<Rect> mTouchRects = new ArrayList<>();
private int mTouchSlop;
private boolean mTrackGesture;
private float mDownX;
private float mDownY;
public HeaderTouchListener() {
}
public void bindTouchRects(View parent, View icon, View expandButton) {
mTouchRects.clear();
addRectAroundViewView(icon);
addRectAroundViewView(expandButton);
addInBetweenRect(parent);
mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
}
private void addInBetweenRect(View parent) {
final Rect r = new Rect();
r.top = 0;
r.bottom = (int) (32 * parent.getResources().getDisplayMetrics().density);
Rect leftRect = mTouchRects.get(0);
r.left = leftRect.right;
Rect rightRect = mTouchRects.get(1);
r.right = rightRect.left;
mTouchRects.add(r);
}
private void addRectAroundViewView(View view) {
final Rect r = getRectAroundView(view);
mTouchRects.add(r);
}
private Rect getRectAroundView(View view) {
float size = 48 * view.getResources().getDisplayMetrics().density;
final Rect r = new Rect();
r.top = (int) ((view.getTop() + view.getBottom()) / 2.0f - size / 2.0f);
r.bottom = (int) (r.top + size);
r.left = (int) ((view.getLeft() + view.getRight()) / 2.0f - size / 2.0f);
r.right = (int) (r.left + size);
return r;
}
@Override
public boolean onTouch(View v, MotionEvent event) {
float x = event.getX();
float y = event.getY();
switch (event.getActionMasked() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mTrackGesture = false;
if (isInside(x, y)) {
mTrackGesture = true;
return true;
}
break;
case MotionEvent.ACTION_MOVE:
if (mTrackGesture) {
if (Math.abs(mDownX - x) > mTouchSlop
|| Math.abs(mDownY - y) > mTouchSlop) {
mTrackGesture = false;
}
}
break;
case MotionEvent.ACTION_UP:
if (mTrackGesture) {
mExpandClickListener.onClick(mNotificationHeader);
}
break;
}
return mTrackGesture;
}
private boolean isInside(float x, float y) {
for (int i = 0; i < mTouchRects.size(); i++) {
Rect r = mTouchRects.get(i);
if (r.contains((int) x, (int) y)) {
mDownX = x;
mDownY = y;
return true;
}
}
return false;
}
}
}