Implementing gradual image fade for the media template

Test: play music
Merged-In: I525513ce1da1237c4edad32c0ed31e79d5eacd32
Change-Id: I525513ce1da1237c4edad32c0ed31e79d5eacd32
Fixes: 36561228
This commit is contained in:
Selim Cinek
2017-04-24 19:48:29 -07:00
parent 5fb73f8629
commit 0847acd4d4
9 changed files with 289 additions and 163 deletions

View File

@@ -4959,6 +4959,22 @@ public class Notification implements Parcelable
return false;
}
/**
* @return true if this is a media notification
*
* @hide
*/
public boolean isMediaNotification() {
Class<? extends Style> style = getNotificationStyle();
if (MediaStyle.class.equals(style)) {
return true;
} else if (DecoratedMediaCustomViewStyle.class.equals(style)) {
return true;
}
return false;
}
private boolean hasLargeIcon() {
return mLargeIcon != null || largeIcon != null;
}

View File

@@ -23,7 +23,6 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.RemoteViews;
/**
@@ -34,8 +33,7 @@ import android.widget.RemoteViews;
@RemoteViews.RemoteView
public class MediaNotificationView extends FrameLayout {
private final int mMaxImageSize;
private final int mImageMinTopMargin;
private final int mSmallImageSize;
private final int mNotificationContentMarginEnd;
private final int mNotificationContentImageMarginEnd;
private ImageView mRightIcon;
@@ -57,72 +55,68 @@ public class MediaNotificationView extends FrameLayout {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mode = MeasureSpec.getMode(widthMeasureSpec);
boolean hasIcon = mRightIcon.getVisibility() != GONE;
if (!hasIcon) {
resetHeaderIndention();
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int mode = MeasureSpec.getMode(widthMeasureSpec);
boolean reMeasure = false;
if (hasIcon && mode != MeasureSpec.UNSPECIFIED) {
measureChild(mActions, widthMeasureSpec, heightMeasureSpec);
int size = MeasureSpec.getSize(widthMeasureSpec);
size = size - mActions.getMeasuredWidth();
ViewGroup.MarginLayoutParams layoutParams =
(MarginLayoutParams) mRightIcon.getLayoutParams();
int imageEndMargin = layoutParams.getMarginEnd();
size -= imageEndMargin;
size = Math.min(size, mMaxImageSize);
size = Math.max(size, mRightIcon.getMinimumWidth());
layoutParams.width = size;
layoutParams.height = size;
mRightIcon.setLayoutParams(layoutParams);
// lets ensure that the main column doesn't run into the image
ViewGroup.MarginLayoutParams mainParams
= (MarginLayoutParams) mMainColumn.getLayoutParams();
int marginEnd = size + imageEndMargin + mNotificationContentMarginEnd;
if (marginEnd != mainParams.getMarginEnd()) {
mainParams.setMarginEnd(marginEnd);
mMainColumn.setLayoutParams(mainParams);
int fullHeight = getMeasuredHeight();
if (size < fullHeight) {
size = mSmallImageSize;
} else {
size = fullHeight;
}
}
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
ViewGroup.MarginLayoutParams iconParams =
(MarginLayoutParams) mRightIcon.getLayoutParams();
int topMargin = getMeasuredHeight() - mRightIcon.getMeasuredHeight()
- iconParams.bottomMargin;
// If the topMargin is high enough we can also remove the header constraint!
boolean reMeasure = false;
if (!hasIcon || topMargin >= mImageMinTopMargin) {
reMeasure = resetHeaderIndention();
} else {
int paddingEnd = mNotificationContentImageMarginEnd;
ViewGroup.MarginLayoutParams headerParams =
(MarginLayoutParams) mHeader.getLayoutParams();
int newMarginEnd = mRightIcon.getMeasuredWidth() + iconParams.getMarginEnd();
if (headerParams.getMarginEnd() != newMarginEnd) {
headerParams.setMarginEnd(newMarginEnd);
mHeader.setLayoutParams(headerParams);
if (layoutParams.width != size || layoutParams.height != size) {
layoutParams.width = size;
layoutParams.height = size;
mRightIcon.setLayoutParams(layoutParams);
reMeasure = true;
}
if (mHeader.getPaddingEnd() != paddingEnd) {
// lets ensure that the main column doesn't run into the image
ViewGroup.MarginLayoutParams params
= (MarginLayoutParams) mMainColumn.getLayoutParams();
int marginEnd = size + imageEndMargin + mNotificationContentMarginEnd;
if (marginEnd != params.getMarginEnd()) {
params.setMarginEnd(marginEnd);
mMainColumn.setLayoutParams(params);
reMeasure = true;
}
int headerMarginEnd = size + imageEndMargin;
params = (MarginLayoutParams) mHeader.getLayoutParams();
if (params.getMarginEnd() != headerMarginEnd) {
params.setMarginEnd(headerMarginEnd);
mHeader.setLayoutParams(params);
reMeasure = true;
}
if (mHeader.getPaddingEnd() != mNotificationContentImageMarginEnd) {
mHeader.setPaddingRelative(mHeader.getPaddingStart(),
mHeader.getPaddingTop(),
paddingEnd,
mNotificationContentImageMarginEnd,
mHeader.getPaddingBottom());
reMeasure = true;
}
}
if (reMeasure) {
measureChildWithMargins(mHeader, widthMeasureSpec, 0, heightMeasureSpec, 0);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
private boolean resetHeaderIndention() {
boolean remeasure = false;
private void resetHeaderIndention() {
if (mHeader.getPaddingEnd() != mNotificationContentMarginEnd) {
mHeader.setPaddingRelative(mHeader.getPaddingStart(),
mHeader.getPaddingTop(),
mNotificationContentMarginEnd,
mHeader.getPaddingBottom());
remeasure = true;
}
ViewGroup.MarginLayoutParams headerParams =
(MarginLayoutParams) mHeader.getLayoutParams();
@@ -130,19 +124,14 @@ public class MediaNotificationView extends FrameLayout {
if (headerParams.getMarginEnd() != 0) {
headerParams.setMarginEnd(0);
mHeader.setLayoutParams(headerParams);
remeasure = true;
}
return remeasure;
}
public MediaNotificationView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
mMaxImageSize = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.media_notification_expanded_image_max_size);
mImageMinTopMargin = (int) (context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.notification_content_margin_top)
+ getResources().getDisplayMetrics().density * 2);
mSmallImageSize = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.media_notification_expanded_image_small_size);
mNotificationContentMarginEnd = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.notification_content_margin_end);
mNotificationContentImageMarginEnd = context.getResources().getDimensionPixelSize(

View File

@@ -23,6 +23,13 @@
android:background="#00000000"
android:tag="bigMediaNarrow"
>
<!-- The size will actually be determined at runtime -->
<ImageView android:id="@+id/right_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_gravity="top|end"
android:scaleType="centerCrop"
/>
<include layout="@layout/notification_template_header"
android:layout_width="match_parent"
android:layout_height="53dp"
@@ -60,14 +67,4 @@
<!-- media buttons will be added here -->
</LinearLayout>
</LinearLayout>
<ImageView android:id="@+id/right_icon"
android:layout_width="@dimen/media_notification_expanded_image_max_size"
android:layout_height="@dimen/media_notification_expanded_image_max_size"
android:minWidth="40dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="20dp"
android:layout_gravity="bottom|end"
android:scaleType="centerCrop"
/>
</com.android.internal.widget.MediaNotificationView>

View File

@@ -23,6 +23,13 @@
android:background="#00000000"
android:tag="media"
>
<ImageView android:id="@+id/right_icon"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:layout_gravity="top|end"
android:scaleType="centerCrop"
/>
<include layout="@layout/notification_template_header"
android:layout_width="fill_parent"
android:layout_height="53dp" />
@@ -61,5 +68,4 @@
<!-- media buttons will be added here -->
</LinearLayout>
</LinearLayout>
<include layout="@layout/notification_template_right_icon" />
</FrameLayout>

View File

@@ -208,8 +208,8 @@
<!-- The minimum height of the content if there are at least two lines or a picture-->
<dimen name="notification_min_content_height">41dp</dimen>
<!-- The maximum size of the image in the expanded media notification -->
<dimen name="media_notification_expanded_image_max_size">94dp</dimen>
<!-- The small size of the image if the height drawing doesn't work anymore -->
<dimen name="media_notification_expanded_image_small_size">72dp</dimen>
<!-- The maximum size of the image in the expanded media notification -->
<dimen name="media_notification_expanded_image_margin_bottom">20dp</dimen>

View File

@@ -2616,7 +2616,7 @@
<java-symbol type="string" name="new_sms_notification_title" />
<java-symbol type="string" name="new_sms_notification_content" />
<java-symbol type="dimen" name="media_notification_expanded_image_max_size" />
<java-symbol type="dimen" name="media_notification_expanded_image_small_size" />
<java-symbol type="dimen" name="media_notification_expanded_image_margin_bottom" />
<java-symbol type="dimen" name="notification_content_image_margin_end" />

View File

@@ -0,0 +1,96 @@
/*
* Copyright (C) 2017 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.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Shader;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
/**
* A utility class to colorize bitmaps with a color gradient and a special blending mode
*/
public class ImageGradientColorizer {
public Bitmap colorize(Drawable drawable, int backgroundColor) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
int size = Math.min(width, height);
int widthInset = (width - size) / 2;
int heightInset = (height - size) / 2;
drawable = drawable.mutate();
drawable.setBounds(- widthInset, - heightInset, width - widthInset, height - heightInset);
Bitmap newBitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(newBitmap);
// Values to calculate the luminance of a color
float lr = 0.2126f;
float lg = 0.7152f;
float lb = 0.0722f;
// Extract the red, green, blue components of the color extraction color in
// float and int form
int tri = Color.red(backgroundColor);
int tgi = Color.green(backgroundColor);
int tbi = Color.blue(backgroundColor);
float tr = tri / 255f;
float tg = tgi / 255f;
float tb = tbi / 255f;
// Calculate the luminance of the color extraction color
float cLum = (tr * lr + tg * lg + tb * lb) * 255;
ColorMatrix m = new ColorMatrix(new float[] {
lr, lg, lb, 0, tri - cLum,
lr, lg, lb, 0, tgi - cLum,
lr, lg, lb, 0, tbi - cLum,
0, 0, 0, 1, 0,
});
drawable.setColorFilter(new ColorMatrixColorFilter(m));
drawable.draw(canvas);
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
LinearGradient linearGradient = new LinearGradient(0, 0, size, 0,
new int[] {0, Color.argb(0.5f, 1, 1, 1), Color.BLACK},
new float[] {0.0f, 0.4f, 1.0f}, Shader.TileMode.CLAMP);
paint.setShader(linearGradient);
Bitmap fadeIn = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
Canvas fadeInCanvas = new Canvas(fadeIn);
drawable.clearColorFilter();
drawable.draw(fadeInCanvas);
paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
fadeInCanvas.drawPaint(paint);
canvas.drawBitmap(fadeIn, 0, 0, null);
linearGradient = new LinearGradient(0, 0, size, 0,
new int[] {backgroundColor, Color.argb(0.5f, tr, tg, tb), 0},
new float[] {0.0f, 0.6f, 1.0f}, Shader.TileMode.CLAMP);
paint.setShader(linearGradient);
paint.setXfermode(null);
canvas.drawPaint(paint);
return newBitmap;
}
}

View File

@@ -26,6 +26,8 @@ import android.graphics.drawable.Icon;
import android.support.v4.graphics.ColorUtils;
import android.support.v7.graphics.Palette;
import com.android.systemui.R;
import java.util.List;
/**
@@ -41,6 +43,8 @@ public class MediaNotificationProcessor {
private static final float BLACK_MAX_LIGHTNESS = 0.08f;
private static final float WHITE_MIN_LIGHTNESS = 0.92f;
private static final int RESIZE_BITMAP_AREA = 150 * 150;
private final ImageGradientColorizer mColorizer;
private final Context mContext;
private float[] mFilteredBackgroundHsl = null;
private Palette.Filter mBlackWhiteFilter = (rgb, hsl) -> !isWhiteOrBlack(hsl);
@@ -48,10 +52,13 @@ public class MediaNotificationProcessor {
* The context of the notification. This is the app context of the package posting the
* notification.
*/
private final Context mContext;
private final Context mPackageContext;
private boolean mIsLowPriority;
public MediaNotificationProcessor(Context context) {
public MediaNotificationProcessor(Context context, Context packageContext) {
mContext = context;
mPackageContext = packageContext;
mColorizer = new ImageGradientColorizer();
}
/**
@@ -64,114 +71,124 @@ public class MediaNotificationProcessor {
public void processNotification(Notification notification, Notification.Builder builder) {
Icon largeIcon = notification.getLargeIcon();
Bitmap bitmap = null;
Drawable drawable = null;
if (largeIcon != null) {
Drawable drawable = largeIcon.loadDrawable(mContext);
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
int area = width * height;
if (area > RESIZE_BITMAP_AREA) {
double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
width = (int) (factor * width);
height = (int) (factor * height);
}
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
}
if (bitmap != null) {
// for the background we only take the left side of the image to ensure
// a smooth transition
Palette.Builder paletteBuilder = Palette.from(bitmap)
.setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
.clearFilters() // we want all colors, red / white / black ones too!
.resizeBitmapArea(RESIZE_BITMAP_AREA);
Palette palette = paletteBuilder.generate();
int backgroundColor = findBackgroundColorAndFilter(palette);
// we want the full region again
paletteBuilder.setRegion(0, 0, bitmap.getWidth(), bitmap.getHeight());
if (mFilteredBackgroundHsl != null) {
paletteBuilder.addFilter((rgb, hsl) -> {
// at least 10 degrees hue difference
float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
return diff > 10 && diff < 350;
});
}
paletteBuilder.addFilter(mBlackWhiteFilter);
palette = paletteBuilder.generate();
int foregroundColor;
if (ColorUtils.calculateLuminance(backgroundColor) > 0.5) {
Palette.Swatch first = palette.getDarkVibrantSwatch();
Palette.Swatch second = palette.getVibrantSwatch();
if (first != null && second != null) {
int firstPopulation = first.getPopulation();
int secondPopulation = second.getPopulation();
if (firstPopulation / secondPopulation
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
foregroundColor = second.getRgb();
} else {
foregroundColor = first.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
first = palette.getMutedSwatch();
second = palette.getDarkMutedSwatch();
drawable = largeIcon.loadDrawable(mPackageContext);
int backgroundColor = 0;
if (notification.isColorizedMedia()) {
int width = drawable.getIntrinsicWidth();
int height = drawable.getIntrinsicHeight();
int area = width * height;
if (area > RESIZE_BITMAP_AREA) {
double factor = Math.sqrt((float) RESIZE_BITMAP_AREA / area);
width = (int) (factor * width);
height = (int) (factor * height);
}
bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, width, height);
drawable.draw(canvas);
// for the background we only take the left side of the image to ensure
// a smooth transition
Palette.Builder paletteBuilder = Palette.from(bitmap)
.setRegion(0, 0, bitmap.getWidth() / 2, bitmap.getHeight())
.clearFilters() // we want all colors, red / white / black ones too!
.resizeBitmapArea(RESIZE_BITMAP_AREA);
Palette palette = paletteBuilder.generate();
backgroundColor = findBackgroundColorAndFilter(palette);
// we want the full region again
paletteBuilder.setRegion(0, 0, bitmap.getWidth(), bitmap.getHeight());
if (mFilteredBackgroundHsl != null) {
paletteBuilder.addFilter((rgb, hsl) -> {
// at least 10 degrees hue difference
float diff = Math.abs(hsl[0] - mFilteredBackgroundHsl[0]);
return diff > 10 && diff < 350;
});
}
paletteBuilder.addFilter(mBlackWhiteFilter);
palette = paletteBuilder.generate();
int foregroundColor;
if (ColorUtils.calculateLuminance(backgroundColor) > 0.5) {
Palette.Swatch first = palette.getDarkVibrantSwatch();
Palette.Swatch second = palette.getVibrantSwatch();
if (first != null && second != null) {
float firstSaturation = first.getHsl()[1];
float secondSaturation = second.getHsl()[1];
if (firstSaturation > secondSaturation) {
foregroundColor = first.getRgb();
} else {
int firstPopulation = first.getPopulation();
int secondPopulation = second.getPopulation();
if (firstPopulation / secondPopulation
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
foregroundColor = second.getRgb();
} else {
foregroundColor = first.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
foregroundColor = Color.BLACK;
first = palette.getMutedSwatch();
second = palette.getDarkMutedSwatch();
if (first != null && second != null) {
float firstSaturation = first.getHsl()[1];
float secondSaturation = second.getHsl()[1];
if (firstSaturation > secondSaturation) {
foregroundColor = first.getRgb();
} else {
foregroundColor = second.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
foregroundColor = Color.BLACK;
}
}
} else {
Palette.Swatch first = palette.getLightVibrantSwatch();
Palette.Swatch second = palette.getVibrantSwatch();
if (first != null && second != null) {
int firstPopulation = first.getPopulation();
int secondPopulation = second.getPopulation();
if (firstPopulation / secondPopulation
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
foregroundColor = second.getRgb();
} else {
foregroundColor = first.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
first = palette.getMutedSwatch();
second = palette.getLightMutedSwatch();
if (first != null && second != null) {
float firstSaturation = first.getHsl()[1];
float secondSaturation = second.getHsl()[1];
if (firstSaturation > secondSaturation) {
foregroundColor = first.getRgb();
} else {
foregroundColor = second.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
foregroundColor = Color.WHITE;
}
}
}
builder.setColorPalette(backgroundColor, foregroundColor);
} else {
Palette.Swatch first = palette.getLightVibrantSwatch();
Palette.Swatch second = palette.getVibrantSwatch();
if (first != null && second != null) {
int firstPopulation = first.getPopulation();
int secondPopulation = second.getPopulation();
if (firstPopulation / secondPopulation
< POPULATION_FRACTION_FOR_MORE_VIBRANT) {
foregroundColor = second.getRgb();
} else {
foregroundColor = first.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
first = palette.getMutedSwatch();
second = palette.getLightMutedSwatch();
if (first != null && second != null) {
float firstSaturation = first.getHsl()[1];
float secondSaturation = second.getHsl()[1];
if (firstSaturation > secondSaturation) {
foregroundColor = first.getRgb();
} else {
foregroundColor = second.getRgb();
}
} else if (first != null) {
foregroundColor = first.getRgb();
} else if (second != null) {
foregroundColor = second.getRgb();
} else {
foregroundColor = Color.WHITE;
}
}
int id = mIsLowPriority
? R.color.notification_material_background_low_priority_color
: R.color.notification_material_background_color;
backgroundColor = mContext.getColor(id);
}
builder.setColorPalette(backgroundColor, foregroundColor);
Bitmap colorized = mColorizer.colorize(drawable, backgroundColor);
builder.setLargeIcon(Icon.createWithBitmap(colorized));
}
}
@@ -235,4 +252,8 @@ public class MediaNotificationProcessor {
private boolean isWhite(float[] hslColor) {
return hslColor[2] >= WHITE_MIN_LIGHTNESS;
}
public void setIsLowPriority(boolean isLowPriority) {
mIsLowPriority = isLowPriority;
}
}

View File

@@ -310,9 +310,10 @@ public class NotificationInflater {
mSbn.getNotification());
mPackageContext = mSbn.getPackageContext(mContext);
Notification notification = mSbn.getNotification();
if (notification.isColorizedMedia()) {
MediaNotificationProcessor processor = new MediaNotificationProcessor(
if (notification.isMediaNotification()) {
MediaNotificationProcessor processor = new MediaNotificationProcessor(mContext,
mPackageContext);
processor.setIsLowPriority(mIsLowPriority);
processor.processNotification(notification, recoveredBuilder);
}
return recoveredBuilder;