1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #include "components/media_message_center/media_notification_background_impl.h"
6
7 #include <algorithm>
8 #include <vector>
9
10 #include "skia/ext/image_operations.h"
11 #include "ui/gfx/canvas.h"
12 #include "ui/gfx/color_analysis.h"
13 #include "ui/gfx/color_utils.h"
14 #include "ui/gfx/scoped_canvas.h"
15 #include "ui/native_theme/native_theme.h"
16 #include "ui/views/style/typography.h"
17 #include "ui/views/view.h"
18
19 namespace media_message_center {
20
21 namespace {
22
23 constexpr int kMediaImageGradientWidth = 40;
24
25 // The ratio for a background color option to be considered very popular.
26 constexpr double kMediaNotificationBackgroundColorVeryPopularRatio = 2.5;
27
28 // The ratio for the most popular foreground color to be used.
29 constexpr double kMediaNotificationForegroundColorMostPopularRatio = 0.01;
30
31 // The minimum saturation for the most popular foreground color to be used.
32 constexpr double kMediaNotificationForegroundColorMostPopularMinSaturation =
33 0.19;
34
35 // The ratio for the more vibrant foreground color to use.
36 constexpr double kMediaNotificationForegroundColorMoreVibrantRatio = 1.0;
37
38 constexpr float kMediaNotificationMinimumContrastRatio = 7.0;
39
IsNearlyWhiteOrBlack(SkColor color)40 bool IsNearlyWhiteOrBlack(SkColor color) {
41 color_utils::HSL hsl;
42 color_utils::SkColorToHSL(color, &hsl);
43 return hsl.l >= 0.9 || hsl.l <= 0.08;
44 }
45
GetHueDegrees(const SkColor & color)46 int GetHueDegrees(const SkColor& color) {
47 color_utils::HSL hsl;
48 color_utils::SkColorToHSL(color, &hsl);
49 return hsl.h * 360;
50 }
51
GetSaturation(const color_utils::Swatch & swatch)52 double GetSaturation(const color_utils::Swatch& swatch) {
53 color_utils::HSL hsl;
54 color_utils::SkColorToHSL(swatch.color, &hsl);
55 return hsl.s;
56 }
57
IsForegroundColorSwatchAllowed(const SkColor & background,const SkColor & candidate)58 bool IsForegroundColorSwatchAllowed(const SkColor& background,
59 const SkColor& candidate) {
60 if (IsNearlyWhiteOrBlack(candidate))
61 return false;
62
63 if (IsNearlyWhiteOrBlack(background))
64 return true;
65
66 int diff = abs(GetHueDegrees(candidate) - GetHueDegrees(background));
67 return diff > 10 && diff < 350;
68 }
69
GetNotificationBackgroundColor(const SkBitmap * source)70 base::Optional<SkColor> GetNotificationBackgroundColor(const SkBitmap* source) {
71 if (!source || source->empty() || source->isNull())
72 return base::nullopt;
73
74 std::vector<color_utils::Swatch> swatches =
75 color_utils::CalculateColorSwatches(
76 *source, 16, gfx::Rect(source->width() / 2, source->height()),
77 base::nullopt);
78
79 if (swatches.empty())
80 return base::nullopt;
81
82 base::Optional<color_utils::Swatch> most_popular;
83 base::Optional<color_utils::Swatch> non_white_black;
84
85 // Find the most popular color with the most weight and the color which
86 // is the color with the most weight that is not white or black.
87 for (auto& swatch : swatches) {
88 if (!IsNearlyWhiteOrBlack(swatch.color) &&
89 (!non_white_black || swatch.population > non_white_black->population)) {
90 non_white_black = swatch;
91 }
92
93 if (most_popular && swatch.population < most_popular->population)
94 continue;
95
96 most_popular = swatch;
97 }
98
99 DCHECK(most_popular);
100
101 // If the most popular color is not white or black then we should use that.
102 if (!IsNearlyWhiteOrBlack(most_popular->color))
103 return most_popular->color;
104
105 // If we could not find a color that is not white or black then we should
106 // use the most popular color.
107 if (!non_white_black)
108 return most_popular->color;
109
110 // If the most popular color is very popular then we should use that color.
111 if (static_cast<double>(most_popular->population) /
112 non_white_black->population >
113 kMediaNotificationBackgroundColorVeryPopularRatio) {
114 return most_popular->color;
115 }
116
117 return non_white_black->color;
118 }
119
SelectVibrantSwatch(const color_utils::Swatch & more_vibrant,const color_utils::Swatch & vibrant)120 color_utils::Swatch SelectVibrantSwatch(const color_utils::Swatch& more_vibrant,
121 const color_utils::Swatch& vibrant) {
122 if ((static_cast<double>(more_vibrant.population) / vibrant.population) <
123 kMediaNotificationForegroundColorMoreVibrantRatio) {
124 return vibrant;
125 }
126
127 return more_vibrant;
128 }
129
SelectMutedSwatch(const color_utils::Swatch & muted,const color_utils::Swatch & more_muted)130 color_utils::Swatch SelectMutedSwatch(const color_utils::Swatch& muted,
131 const color_utils::Swatch& more_muted) {
132 double population_ratio =
133 static_cast<double>(muted.population) / more_muted.population;
134
135 // Use the swatch with the higher saturation ratio.
136 return (GetSaturation(muted) * population_ratio > GetSaturation(more_muted))
137 ? muted
138 : more_muted;
139 }
140
GetNotificationForegroundColor(const base::Optional<SkColor> & background_color,const SkBitmap * source)141 base::Optional<SkColor> GetNotificationForegroundColor(
142 const base::Optional<SkColor>& background_color,
143 const SkBitmap* source) {
144 if (!background_color || !source || source->empty() || source->isNull())
145 return base::nullopt;
146
147 const bool is_light =
148 color_utils::GetRelativeLuminance(*background_color) > 0.5;
149 const SkColor fallback_color = is_light ? SK_ColorBLACK : SK_ColorWHITE;
150
151 gfx::Rect bitmap_area(source->width(), source->height());
152 bitmap_area.Inset(source->width() * 0.4, 0, 0, 0);
153
154 // If the background color is dark we want to look for colors that are darker
155 // and vice versa.
156 const color_utils::LumaRange more_luma_range =
157 is_light ? color_utils::LumaRange::DARK : color_utils::LumaRange::LIGHT;
158
159 std::vector<color_utils::ColorProfile> color_profiles;
160 color_profiles.push_back(color_utils::ColorProfile(
161 more_luma_range, color_utils::SaturationRange::VIBRANT));
162 color_profiles.push_back(color_utils::ColorProfile(
163 color_utils::LumaRange::NORMAL, color_utils::SaturationRange::VIBRANT));
164 color_profiles.push_back(color_utils::ColorProfile(
165 color_utils::LumaRange::NORMAL, color_utils::SaturationRange::MUTED));
166 color_profiles.push_back(color_utils::ColorProfile(
167 more_luma_range, color_utils::SaturationRange::MUTED));
168 color_profiles.push_back(color_utils::ColorProfile(
169 color_utils::LumaRange::ANY, color_utils::SaturationRange::ANY));
170
171 std::vector<color_utils::Swatch> best_swatches =
172 color_utils::CalculateProminentColorsOfBitmap(
173 *source, color_profiles, &bitmap_area,
174 base::BindRepeating(&IsForegroundColorSwatchAllowed,
175 background_color.value()));
176
177 if (best_swatches.empty())
178 return fallback_color;
179
180 DCHECK_EQ(5u, best_swatches.size());
181
182 const color_utils::Swatch& more_vibrant = best_swatches[0];
183 const color_utils::Swatch& vibrant = best_swatches[1];
184 const color_utils::Swatch& muted = best_swatches[2];
185 const color_utils::Swatch& more_muted = best_swatches[3];
186 const color_utils::Swatch& most_popular = best_swatches[4];
187
188 // We are looking for a fraction that is at least 0.2% of the image.
189 const size_t population_min =
190 std::min(bitmap_area.width() * bitmap_area.height(),
191 color_utils::kMaxConsideredPixelsForSwatches) *
192 0.002;
193
194 // This selection algorithm is an implementation of MediaNotificationProcessor
195 // from Android. It will select more vibrant colors first since they stand out
196 // better against the background. If not, it will fallback to muted colors,
197 // the most popular color and then either white/black. Any swatch has to be
198 // above a minimum population threshold to be determined significant enough in
199 // the artwork to be used.
200 base::Optional<color_utils::Swatch> swatch;
201 if (more_vibrant.population > population_min &&
202 vibrant.population > population_min) {
203 swatch = SelectVibrantSwatch(more_vibrant, vibrant);
204 } else if (more_vibrant.population > population_min) {
205 swatch = more_vibrant;
206 } else if (vibrant.population > population_min) {
207 swatch = vibrant;
208 } else if (muted.population > population_min &&
209 more_muted.population > population_min) {
210 swatch = SelectMutedSwatch(muted, more_muted);
211 } else if (muted.population > population_min) {
212 swatch = muted;
213 } else if (more_muted.population > population_min) {
214 swatch = more_muted;
215 } else if (most_popular.population > population_min) {
216 return most_popular.color;
217 } else {
218 return fallback_color;
219 }
220
221 if (most_popular == *swatch)
222 return swatch->color;
223
224 if (static_cast<double>(swatch->population) / most_popular.population <
225 kMediaNotificationForegroundColorMostPopularRatio &&
226 GetSaturation(most_popular) >
227 kMediaNotificationForegroundColorMostPopularMinSaturation) {
228 return most_popular.color;
229 }
230
231 return swatch->color;
232 }
233
234 } // namespace
235
MediaNotificationBackgroundImpl(int top_radius,int bottom_radius,double artwork_max_width_pct)236 MediaNotificationBackgroundImpl::MediaNotificationBackgroundImpl(
237 int top_radius,
238 int bottom_radius,
239 double artwork_max_width_pct)
240 : top_radius_(top_radius),
241 bottom_radius_(bottom_radius),
242 artwork_max_width_pct_(artwork_max_width_pct) {}
243
244 MediaNotificationBackgroundImpl::~MediaNotificationBackgroundImpl() = default;
245
Paint(gfx::Canvas * canvas,views::View * view) const246 void MediaNotificationBackgroundImpl::Paint(gfx::Canvas* canvas,
247 views::View* view) const {
248 DCHECK(view);
249
250 gfx::ScopedCanvas scoped_canvas(canvas);
251 gfx::Rect bounds = view->GetContentsBounds();
252
253 {
254 // Draw a rounded rectangle which the background will be clipped to. The
255 // radius is provided by the notification and can change based on where in
256 // the list the notification is.
257 const SkScalar top_radius = SkIntToScalar(top_radius_);
258 const SkScalar bottom_radius = SkIntToScalar(bottom_radius_);
259
260 const SkScalar radii[8] = {top_radius, top_radius, top_radius,
261 top_radius, bottom_radius, bottom_radius,
262 bottom_radius, bottom_radius};
263
264 SkPath path;
265 path.addRoundRect(gfx::RectToSkRect(bounds), radii, SkPathDirection::kCW);
266 canvas->ClipPath(path, true);
267 }
268
269 {
270 // Draw the artwork. The artwork is resized to the height of the view while
271 // maintaining the aspect ratio.
272 gfx::Rect source_bounds =
273 gfx::Rect(0, 0, artwork_.width(), artwork_.height());
274 gfx::Rect artwork_bounds = GetArtworkBounds(*view);
275
276 canvas->DrawImageInt(
277 artwork_, source_bounds.x(), source_bounds.y(), source_bounds.width(),
278 source_bounds.height(), artwork_bounds.x(), artwork_bounds.y(),
279 artwork_bounds.width(), artwork_bounds.height(), false /* filter */);
280 }
281
282 // Draw a filled rectangle which will act as the main background of the
283 // notification. This may cover up some of the artwork.
284 const SkColor background_color =
285 background_color_.value_or(GetDefaultBackgroundColor(*view));
286 canvas->FillRect(GetFilledBackgroundBounds(*view), background_color);
287
288 {
289 // Draw a gradient to fade the color background and the image together.
290 gfx::Rect draw_bounds = GetGradientBounds(*view);
291
292 const SkColor colors[2] = {
293 background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
294 const SkPoint points[2] = {GetGradientStartPoint(draw_bounds),
295 GetGradientEndPoint(draw_bounds)};
296
297 cc::PaintFlags flags;
298 flags.setAntiAlias(true);
299 flags.setStyle(cc::PaintFlags::kFill_Style);
300 flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
301 2, SkTileMode::kClamp));
302
303 canvas->DrawRect(draw_bounds, flags);
304 }
305
306 if (audio_device_selector_availability_) {
307 // Draw a gradient to fade the color background of the audio device picker
308 // and the image together.
309 gfx::Rect draw_bounds = GetBottomGradientBounds(*view);
310
311 const SkColor colors[2] = {
312 background_color, SkColorSetA(background_color, SK_AlphaTRANSPARENT)};
313 const SkPoint points[2] = {gfx::PointToSkPoint(draw_bounds.bottom_center()),
314 gfx::PointToSkPoint(draw_bounds.top_center())};
315
316 cc::PaintFlags flags;
317 flags.setAntiAlias(true);
318 flags.setStyle(cc::PaintFlags::kFill_Style);
319 flags.setShader(cc::PaintShader::MakeLinearGradient(points, colors, nullptr,
320 2, SkTileMode::kClamp));
321
322 canvas->DrawRect(draw_bounds, flags);
323 }
324 }
325
UpdateArtwork(const gfx::ImageSkia & image)326 void MediaNotificationBackgroundImpl::UpdateArtwork(
327 const gfx::ImageSkia& image) {
328 if (artwork_.BackedBySameObjectAs(image))
329 return;
330
331 artwork_ = image;
332
333 UpdateColorsInternal();
334 }
335
UpdateCornerRadius(int top_radius,int bottom_radius)336 bool MediaNotificationBackgroundImpl::UpdateCornerRadius(int top_radius,
337 int bottom_radius) {
338 if (top_radius_ == top_radius && bottom_radius_ == bottom_radius)
339 return false;
340
341 top_radius_ = top_radius;
342 bottom_radius_ = bottom_radius;
343 return true;
344 }
345
UpdateArtworkMaxWidthPct(double max_width_pct)346 bool MediaNotificationBackgroundImpl::UpdateArtworkMaxWidthPct(
347 double max_width_pct) {
348 if (artwork_max_width_pct_ == max_width_pct)
349 return false;
350
351 artwork_max_width_pct_ = max_width_pct;
352 return true;
353 }
354
UpdateFavicon(const gfx::ImageSkia & icon)355 void MediaNotificationBackgroundImpl::UpdateFavicon(
356 const gfx::ImageSkia& icon) {
357 if (favicon_.BackedBySameObjectAs(icon))
358 return;
359
360 favicon_ = icon;
361
362 if (!artwork_.isNull())
363 return;
364
365 UpdateColorsInternal();
366 }
367
UpdateDeviceSelectorAvailability(bool availability)368 void MediaNotificationBackgroundImpl::UpdateDeviceSelectorAvailability(
369 bool availability) {
370 if (audio_device_selector_availability_ == availability)
371 return;
372
373 audio_device_selector_availability_ = availability;
374 }
375
GetBackgroundColor(const views::View & owner) const376 SkColor MediaNotificationBackgroundImpl::GetBackgroundColor(
377 const views::View& owner) const {
378 if (background_color_.has_value())
379 return *background_color_;
380 return GetDefaultBackgroundColor(owner);
381 }
382
GetForegroundColor(const views::View & owner) const383 SkColor MediaNotificationBackgroundImpl::GetForegroundColor(
384 const views::View& owner) const {
385 const SkColor foreground =
386 foreground_color_.has_value()
387 ? *foreground_color_
388 : views::style::GetColor(owner, views::style::CONTEXT_LABEL,
389 views::style::STYLE_PRIMARY);
390 return color_utils::BlendForMinContrast(
391 foreground, GetBackgroundColor(owner), base::nullopt,
392 kMediaNotificationMinimumContrastRatio)
393 .color;
394 }
395
GetArtworkWidth(const gfx::Size & view_size) const396 int MediaNotificationBackgroundImpl::GetArtworkWidth(
397 const gfx::Size& view_size) const {
398 if (artwork_.isNull())
399 return 0;
400
401 // Calculate the aspect ratio of the image and determine what the width of the
402 // image should be based on that ratio and the height of the notification.
403 float aspect_ratio = (float)artwork_.width() / artwork_.height();
404 return ceil(view_size.height() * aspect_ratio);
405 }
406
GetArtworkVisibleWidth(const gfx::Size & view_size) const407 int MediaNotificationBackgroundImpl::GetArtworkVisibleWidth(
408 const gfx::Size& view_size) const {
409 // The artwork should only take up a maximum percentage of the notification.
410 return std::min(GetArtworkWidth(view_size),
411 (int)ceil(view_size.width() * artwork_max_width_pct_));
412 }
413
GetArtworkBounds(const views::View & owner) const414 gfx::Rect MediaNotificationBackgroundImpl::GetArtworkBounds(
415 const views::View& owner) const {
416 const gfx::Rect& view_bounds = owner.GetContentsBounds();
417 int width = GetArtworkWidth(view_bounds.size());
418 int visible_width = GetArtworkVisibleWidth(view_bounds.size());
419
420 // This offset is for centering artwork if artwork visible width is smaller
421 // than artwork width.
422 int horizontal_offset = (width - visible_width) / 2;
423
424 // The artwork should be positioned on the far right hand side of the
425 // notification and be the same height.
426 return owner.GetMirroredRect(
427 gfx::Rect(view_bounds.right() - width + horizontal_offset, 0, width,
428 view_bounds.height()));
429 }
430
GetFilledBackgroundBounds(const views::View & owner) const431 gfx::Rect MediaNotificationBackgroundImpl::GetFilledBackgroundBounds(
432 const views::View& owner) const {
433 // The filled background should take up the full notification except the area
434 // taken up by the artwork.
435 const gfx::Rect& view_bounds = owner.GetContentsBounds();
436 gfx::Rect bounds = gfx::Rect(view_bounds);
437 bounds.Inset(0, 0, GetArtworkVisibleWidth(view_bounds.size()), 0);
438 return owner.GetMirroredRect(bounds);
439 }
440
GetGradientBounds(const views::View & owner) const441 gfx::Rect MediaNotificationBackgroundImpl::GetGradientBounds(
442 const views::View& owner) const {
443 if (artwork_.isNull())
444 return gfx::Rect(0, 0, 0, 0);
445
446 // The gradient should appear above the artwork on the left.
447 const gfx::Rect& view_bounds = owner.GetContentsBounds();
448 return owner.GetMirroredRect(gfx::Rect(
449 view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
450 view_bounds.y(), kMediaImageGradientWidth, view_bounds.height()));
451 }
452
GetBottomGradientBounds(const views::View & owner) const453 gfx::Rect MediaNotificationBackgroundImpl::GetBottomGradientBounds(
454 const views::View& owner) const {
455 if (artwork_.isNull())
456 return gfx::Rect(0, 0, 0, 0);
457
458 const gfx::Rect& view_bounds = owner.GetContentsBounds();
459 return owner.GetMirroredRect(gfx::Rect(
460 gfx::Point(
461 view_bounds.width() - GetArtworkVisibleWidth(view_bounds.size()),
462 view_bounds.bottom() - kMediaImageGradientWidth),
463 gfx::Size(GetArtworkVisibleWidth(view_bounds.size()),
464 kMediaImageGradientWidth)));
465 }
466
GetGradientStartPoint(const gfx::Rect & draw_bounds) const467 SkPoint MediaNotificationBackgroundImpl::GetGradientStartPoint(
468 const gfx::Rect& draw_bounds) const {
469 return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.right_center()
470 : draw_bounds.left_center());
471 }
472
GetGradientEndPoint(const gfx::Rect & draw_bounds) const473 SkPoint MediaNotificationBackgroundImpl::GetGradientEndPoint(
474 const gfx::Rect& draw_bounds) const {
475 return gfx::PointToSkPoint(base::i18n::IsRTL() ? draw_bounds.left_center()
476 : draw_bounds.right_center());
477 }
478
GetDefaultBackgroundColor(const views::View & owner) const479 SkColor MediaNotificationBackgroundImpl::GetDefaultBackgroundColor(
480 const views::View& owner) const {
481 return owner.GetNativeTheme()->GetSystemColor(
482 ui::NativeTheme::kColorId_BubbleBackground);
483 }
484
UpdateColorsInternal()485 void MediaNotificationBackgroundImpl::UpdateColorsInternal() {
486 // If there is an artwork, it should be used.
487 // If there is no artwork, neither a favicon, the artwork bitmap will be used
488 // which is going to be a null bitmap and produce a default value.
489 // In the case of there is a favicon and no artwork, the favicon should be
490 // used to generate the colors.
491 if (!artwork_.isNull() || favicon_.isNull()) {
492 background_color_ = GetNotificationBackgroundColor(artwork_.bitmap());
493 foreground_color_ =
494 GetNotificationForegroundColor(background_color_, artwork_.bitmap());
495 return;
496 }
497
498 background_color_ = GetNotificationBackgroundColor(favicon_.bitmap());
499 if (background_color_) {
500 // Apply a shade factor on the color as favicons often are fairly bright.
501 *background_color_ = SkColorSetRGB(
502 SkColorGetR(*background_color_) * kBackgroundFaviconColorShadeFactor,
503 SkColorGetG(*background_color_) * kBackgroundFaviconColorShadeFactor,
504 SkColorGetB(*background_color_) * kBackgroundFaviconColorShadeFactor);
505 }
506 foreground_color_ =
507 GetNotificationForegroundColor(background_color_, favicon_.bitmap());
508 }
509
510 } // namespace media_message_center
511