media_notification_background_impl.cc 19 KB

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