holding_space_tray_icon.cc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527
  1. // Copyright 2020 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 "ash/system/holding_space/holding_space_tray_icon.h"
  5. #include "ash/public/cpp/holding_space/holding_space_constants.h"
  6. #include "ash/public/cpp/holding_space/holding_space_item.h"
  7. #include "ash/public/cpp/holding_space/holding_space_metrics.h"
  8. #include "ash/public/cpp/holding_space/holding_space_prefs.h"
  9. #include "ash/public/cpp/shelf_config.h"
  10. #include "ash/resources/vector_icons/vector_icons.h"
  11. #include "ash/shelf/shelf.h"
  12. #include "ash/strings/grit/ash_strings.h"
  13. #include "ash/system/holding_space/holding_space_tray_icon_preview.h"
  14. #include "ash/system/tray/tray_constants.h"
  15. #include "base/barrier_closure.h"
  16. #include "base/bind.h"
  17. #include "base/containers/adapters.h"
  18. #include "base/containers/contains.h"
  19. #include "base/containers/cxx20_erase.h"
  20. #include "base/containers/unique_ptr_adapters.h"
  21. #include "base/i18n/rtl.h"
  22. #include "ui/base/l10n/l10n_util.h"
  23. #include "ui/base/metadata/metadata_impl_macros.h"
  24. #include "ui/compositor/compositor.h"
  25. #include "ui/compositor/layer.h"
  26. #include "ui/compositor/layer_animator.h"
  27. #include "ui/compositor/scoped_animation_duration_scale_mode.h"
  28. #include "ui/gfx/animation/slide_animation.h"
  29. #include "ui/gfx/paint_vector_icon.h"
  30. #include "ui/views/animation/animation_delegate_views.h"
  31. #include "ui/views/layout/fill_layout.h"
  32. #include "ui/views/widget/widget.h"
  33. namespace ash {
  34. namespace {
  35. // When in drop target state, previews are shifted to indices which are offset
  36. // from their standard positions by this fixed amount.
  37. constexpr int kPreviewIndexOffsetForDropTarget = 3;
  38. // The previews are animated in and shifted with a delay that increases
  39. // incrementally. This is the delay increment.
  40. constexpr base::TimeDelta kPreviewItemUpdateDelayIncrement =
  41. base::Milliseconds(50);
  42. // Helpers ---------------------------------------------------------------------
  43. // Returns the size of previews given the current shelf configuration.
  44. int GetPreviewSize() {
  45. ShelfConfig* const shelf_config = ShelfConfig::Get();
  46. return shelf_config->in_tablet_mode() && shelf_config->is_in_app()
  47. ? kHoldingSpaceTrayIconSmallPreviewSize
  48. : kHoldingSpaceTrayIconDefaultPreviewSize;
  49. }
  50. } // namespace
  51. // HoldingSpaceTrayIcon::ResizeAnimation ---------------------------------------
  52. // Animation for resizing the previews icon. The animation updates the icon
  53. // view's preferred size, which causes the status area (and the shelf) to
  54. // re-layout.
  55. class HoldingSpaceTrayIcon::ResizeAnimation
  56. : public views::AnimationDelegateViews {
  57. public:
  58. ResizeAnimation(HoldingSpaceTrayIcon* icon,
  59. views::View* previews_container,
  60. const gfx::Size& initial_size,
  61. const gfx::Size& target_size)
  62. : views::AnimationDelegateViews(icon),
  63. icon_(icon),
  64. previews_container_(previews_container),
  65. initial_size_(initial_size),
  66. target_size_(target_size),
  67. animation_(this),
  68. animation_throughput_tracker_(
  69. icon->GetWidget()->GetCompositor()->RequestNewThroughputTracker()) {
  70. animation_.SetTweenType(gfx::Tween::FAST_OUT_SLOW_IN);
  71. animation_.SetSlideDuration(
  72. ui::ScopedAnimationDurationScaleMode::duration_multiplier() *
  73. base::Milliseconds(250));
  74. }
  75. ResizeAnimation(const ResizeAnimation&) = delete;
  76. ResizeAnimation operator=(const ResizeAnimation&) = delete;
  77. ~ResizeAnimation() override = default;
  78. // views::AnimationDelegateViews:
  79. void AnimationEnded(const gfx::Animation* animation) override {
  80. icon_->SetPreferredSize(target_size_);
  81. previews_container_->SetTransform(gfx::Transform());
  82. // Record animation smoothness.
  83. animation_throughput_tracker_.Stop();
  84. }
  85. void AnimationProgressed(const gfx::Animation* animation) override {
  86. const gfx::Size current_size = gfx::Tween::SizeValueBetween(
  87. animation->GetCurrentValue(), initial_size_, target_size_);
  88. // Bounds grow from start to the end by default - for holding space tray
  89. // icon, the bounds are expected to grow from end to start. To achieve
  90. // growth from end to start, the position of previews is adjusted for target
  91. // bounds, and the previews container is translated so previews position
  92. // remains constant relative to the end of the icon.
  93. const gfx::Vector2d offset(current_size.width() - target_size_.width(),
  94. current_size.height() - target_size_.height());
  95. gfx::Transform transform;
  96. const int direction = base::i18n::IsRTL() ? -1 : 1;
  97. transform.Translate(direction * offset.x(), offset.y());
  98. previews_container_->SetTransform(transform);
  99. // This will update the shelf and status area layout.
  100. if (icon_->GetPreferredSize() != current_size)
  101. icon_->SetPreferredSize(current_size);
  102. }
  103. void Start() {
  104. animation_throughput_tracker_.Start(
  105. metrics_util::ForSmoothness(base::BindRepeating(
  106. holding_space_metrics::RecordPodResizeAnimationSmoothness)));
  107. animation_.Show();
  108. AnimationProgressed(&animation_);
  109. }
  110. void AdvanceToEnd() { animation_.End(); }
  111. private:
  112. HoldingSpaceTrayIcon* const icon_;
  113. views::View* const previews_container_;
  114. const gfx::Size initial_size_;
  115. const gfx::Size target_size_;
  116. gfx::SlideAnimation animation_;
  117. ui::ThroughputTracker animation_throughput_tracker_;
  118. };
  119. // HoldingSpaceTrayIcon --------------------------------------------------------
  120. HoldingSpaceTrayIcon::HoldingSpaceTrayIcon(Shelf* shelf) : shelf_(shelf) {
  121. SetID(kHoldingSpaceTrayPreviewsIconId);
  122. InitLayout();
  123. shell_observer_.Observe(Shell::Get());
  124. shelf_config_observer_.Observe(ShelfConfig::Get());
  125. }
  126. HoldingSpaceTrayIcon::~HoldingSpaceTrayIcon() = default;
  127. void HoldingSpaceTrayIcon::Clear() {
  128. previews_update_weak_factory_.InvalidateWeakPtrs();
  129. item_ids_.clear();
  130. previews_by_id_.clear();
  131. removed_previews_.clear();
  132. SetPreferredSize(CalculatePreferredSize());
  133. }
  134. int HoldingSpaceTrayIcon::GetHeightForWidth(int width) const {
  135. // The parent for this view (`TrayContainer`) uses a `BoxLayout` for its
  136. // `LayoutManager`. When the shelf orientation is vertical, the `BoxLayout`
  137. // will also have vertical orientation and will invoke `GetHeightForWidth()`
  138. // instead of `GetPreferredSize()` when determining preferred size.
  139. return GetPreferredSize().height();
  140. }
  141. gfx::Size HoldingSpaceTrayIcon::CalculatePreferredSize() const {
  142. const int num_visible_previews =
  143. std::min(kHoldingSpaceTrayIconMaxVisiblePreviews,
  144. static_cast<int>(previews_by_id_.size()));
  145. const int preview_size = GetPreviewSize();
  146. int primary_axis_size = preview_size;
  147. if (num_visible_previews > 1)
  148. primary_axis_size += (num_visible_previews - 1) * preview_size / 2;
  149. return shelf_->PrimaryAxisValue(
  150. /*horizontal=*/gfx::Size(primary_axis_size, kTrayItemSize),
  151. /*vertical=*/gfx::Size(kTrayItemSize, primary_axis_size));
  152. }
  153. void HoldingSpaceTrayIcon::OnThemeChanged() {
  154. views::View::OnThemeChanged();
  155. for (auto& preview_by_id : previews_by_id_)
  156. preview_by_id.second->OnThemeChanged();
  157. for (auto& preview : removed_previews_)
  158. preview->OnThemeChanged();
  159. }
  160. void HoldingSpaceTrayIcon::InitLayout() {
  161. SetLayoutManager(std::make_unique<views::FillLayout>());
  162. const int preview_size = GetPreviewSize();
  163. SetPreferredSize(gfx::Size(preview_size, preview_size));
  164. SetPaintToLayer(ui::LAYER_NOT_DRAWN);
  165. layer()->SetFillsBoundsOpaquely(false);
  166. const int radius = ShelfConfig::Get()->control_border_radius();
  167. gfx::RoundedCornersF rounded_corners(radius);
  168. layer()->SetRoundedCornerRadius(rounded_corners);
  169. layer()->SetIsFastRoundedCorner(true);
  170. previews_container_ = AddChildView(std::make_unique<views::View>());
  171. // As holding space items are added to the model, child layers will be added
  172. // to `previews_container_` view's layer to represent them.
  173. previews_container_->SetPaintToLayer(ui::LAYER_NOT_DRAWN);
  174. }
  175. void HoldingSpaceTrayIcon::UpdateDropTargetState(bool is_drop_target,
  176. bool did_drop_to_pin) {
  177. if (is_drop_target_ == is_drop_target)
  178. return;
  179. is_drop_target_ = is_drop_target;
  180. // If the user performed a drag-and-drop to pin action, no handling is needed
  181. // to transition the holding space tray icon out of drop target state. When
  182. // the model updates, an `UpdatePreviews()` event will follow which will
  183. // restore standard indexing to the new and existing previews.
  184. if (!is_drop_target_ && did_drop_to_pin)
  185. return;
  186. DCHECK(!did_drop_to_pin);
  187. for (size_t i = 0; i < item_ids_.size(); ++i) {
  188. auto* preview = previews_by_id_.find(item_ids_[i])->second.get();
  189. DCHECK(preview->index());
  190. DCHECK(!preview->pending_index());
  191. size_t pending_index = i;
  192. base::TimeDelta delay;
  193. if (is_drop_target_) {
  194. // When in drop target state, preview indices are offset from their
  195. // standard positions by a fixed amount.
  196. pending_index += kPreviewIndexOffsetForDropTarget;
  197. } else {
  198. // When transitioning into drop target state, all previews shift out in
  199. // sync. When transitioning out of drop target state, previews shift in
  200. // with incremental `delay`.
  201. delay = i * kPreviewItemUpdateDelayIncrement;
  202. }
  203. preview->set_pending_index(pending_index);
  204. preview->AnimateShift(delay);
  205. }
  206. EnsurePreviewLayerStackingOrder();
  207. }
  208. void HoldingSpaceTrayIcon::UpdatePreviews(
  209. const std::vector<const HoldingSpaceItem*> items) {
  210. // Cancel any in progress updates.
  211. previews_update_weak_factory_.InvalidateWeakPtrs();
  212. item_ids_.clear();
  213. // When in drop target state, indices are offset from their standard position.
  214. const int offset = is_drop_target_ ? kPreviewIndexOffsetForDropTarget : 0;
  215. // Go over the new item list, create previews for new items, and assign new
  216. // indices to existing items.
  217. std::set<std::string> item_ids;
  218. for (size_t index = 0; index < items.size(); ++index) {
  219. const HoldingSpaceItem* item = items[index];
  220. DCHECK(item->IsInitialized());
  221. item_ids.insert(item->id());
  222. item_ids_.push_back(item->id());
  223. auto preview_it = previews_by_id_.find(item->id());
  224. if (preview_it != previews_by_id_.end()) {
  225. preview_it->second->set_pending_index(index + offset);
  226. continue;
  227. }
  228. auto preview = std::make_unique<HoldingSpaceTrayIconPreview>(
  229. shelf_, previews_container_, item);
  230. preview->set_pending_index(index + offset);
  231. previews_by_id_.emplace(item->id(), std::move(preview));
  232. }
  233. // Collect the list of items that should be removed.
  234. std::vector<std::string> items_to_remove;
  235. for (const auto& preview_pair : previews_by_id_) {
  236. if (!base::Contains(item_ids, preview_pair.first))
  237. items_to_remove.push_back(preview_pair.first);
  238. }
  239. if (!should_animate_updates_) {
  240. for (auto& item_id : items_to_remove)
  241. previews_by_id_.erase(item_id);
  242. items_to_remove.clear();
  243. }
  244. if (items_to_remove.empty()) {
  245. OnOldItemsRemoved();
  246. return;
  247. }
  248. // Animate out all items that should be removed.
  249. base::RepeatingClosure items_removed_callback = base::BarrierClosure(
  250. items_to_remove.size(),
  251. base::BindOnce(&HoldingSpaceTrayIcon::OnOldItemsRemoved,
  252. previews_update_weak_factory_.GetWeakPtr()));
  253. for (auto& item_id : items_to_remove) {
  254. auto preview_it = previews_by_id_.find(item_id);
  255. HoldingSpaceTrayIconPreview* preview_ptr = preview_it->second.get();
  256. removed_previews_.push_back(std::move(preview_it->second));
  257. previews_by_id_.erase(preview_it);
  258. preview_ptr->AnimateOut(base::BindOnce(
  259. &HoldingSpaceTrayIcon::OnOldItemAnimatedOut, base::Unretained(this),
  260. base::Unretained(preview_ptr), items_removed_callback));
  261. }
  262. }
  263. void HoldingSpaceTrayIcon::OnShellDestroying() {
  264. shell_observer_.Reset();
  265. }
  266. void HoldingSpaceTrayIcon::OnShelfAlignmentChanged(
  267. aura::Window* root_window,
  268. ShelfAlignment old_alignment) {
  269. // Each display has its own shelf. The shelf undergoing an alignment change
  270. // may not be the `shelf_` associated with this holding space tray icon.
  271. if (shelf_ != Shelf::ForWindow(root_window))
  272. return;
  273. if (!removed_previews_.empty()) {
  274. removed_previews_.clear();
  275. OnOldItemsRemoved();
  276. }
  277. for (const auto& preview : previews_by_id_)
  278. preview.second->OnShelfAlignmentChanged(old_alignment, shelf_->alignment());
  279. if (resize_animation_) {
  280. resize_animation_->AdvanceToEnd();
  281. resize_animation_.reset();
  282. }
  283. SetPreferredSize(CalculatePreferredSize());
  284. previews_container_->SetTransform(gfx::Transform());
  285. }
  286. void HoldingSpaceTrayIcon::OnShelfConfigUpdated() {
  287. if (!removed_previews_.empty()) {
  288. removed_previews_.clear();
  289. OnOldItemsRemoved();
  290. }
  291. for (const auto& preview : previews_by_id_)
  292. preview.second->OnShelfConfigChanged();
  293. if (resize_animation_) {
  294. resize_animation_->AdvanceToEnd();
  295. resize_animation_.reset();
  296. }
  297. SetPreferredSize(CalculatePreferredSize());
  298. previews_container_->SetTransform(gfx::Transform());
  299. }
  300. void HoldingSpaceTrayIcon::OnOldItemAnimatedOut(
  301. HoldingSpaceTrayIconPreview* preview,
  302. const base::RepeatingClosure& callback) {
  303. base::EraseIf(removed_previews_, base::MatchesUniquePtr(preview));
  304. callback.Run();
  305. }
  306. void HoldingSpaceTrayIcon::OnOldItemsRemoved() {
  307. if (resize_animation_) {
  308. resize_animation_->AdvanceToEnd();
  309. resize_animation_.reset();
  310. }
  311. // Now that the old items have been removed, resize the icon, and update
  312. // previews position within the icon.
  313. const gfx::Size initial_size = size();
  314. const gfx::Size target_size = CalculatePreferredSize();
  315. if (initial_size != target_size) {
  316. // Changing icon bounds changes the relative position of existing item
  317. // layers within the icon (as the icon origin moves). Adjust the
  318. // position of existing items to maintain their position relative to the
  319. // "end" visible bounds.
  320. gfx::Vector2d adjustment(target_size.width() - initial_size.width(),
  321. target_size.height() - initial_size.height());
  322. for (auto& preview_pair : previews_by_id_)
  323. preview_pair.second->AdjustTransformForContainerSizeChange(adjustment);
  324. if (should_animate_updates_) {
  325. resize_animation_ = std::make_unique<ResizeAnimation>(
  326. this, previews_container_, initial_size, target_size);
  327. resize_animation_->Start();
  328. } else {
  329. SetPreferredSize(target_size);
  330. }
  331. }
  332. // Shift existing items to their new positions. Note that this must be done
  333. // *prior* to animating in new items as `CalculateAnimateShiftParams()` relies
  334. // on index being unset in order to distinguish new items from existing items.
  335. for (const PreviewAnimationParams& params : CalculateAnimateShiftParams()) {
  336. params.preview->AnimateShift(params.delay);
  337. if (!should_animate_updates_ && params.preview->layer())
  338. params.preview->layer()->GetAnimator()->StopAnimating();
  339. }
  340. // Add new items. Note that this must be done *after* animating existing items
  341. // as `CalculateAnimateInParams()` relies on index being unset in order to
  342. // distinguish new items from existing items.
  343. for (const PreviewAnimationParams& params : CalculateAnimateInParams()) {
  344. params.preview->AnimateIn(params.delay);
  345. if (!should_animate_updates_ && params.preview->layer())
  346. params.preview->layer()->GetAnimator()->StopAnimating();
  347. }
  348. EnsurePreviewLayerStackingOrder();
  349. }
  350. std::vector<HoldingSpaceTrayIcon::PreviewAnimationParams>
  351. HoldingSpaceTrayIcon::CalculateAnimateShiftParams() {
  352. // Items shift with an incremental delay. For items shifting towards the end
  353. // of the icon, the delay should decrease with the index. For items shifting
  354. // towards the start of the icon, the delay should increase with the index.
  355. // This ensures items moving in the same direction do not fly over each other.
  356. std::vector<PreviewAnimationParams> animation_params;
  357. // Calculate the starting delay for items that will be shifting towards the
  358. // end of the icon.
  359. base::TimeDelta shift_out_delay;
  360. for (size_t i = 0; i < item_ids_.size(); ++i) {
  361. auto* preview = previews_by_id_.find(item_ids_[i])->second.get();
  362. if (!preview->index().has_value())
  363. continue;
  364. DCHECK(preview->pending_index());
  365. if (*preview->index() <= kHoldingSpaceTrayIconMaxVisiblePreviews &&
  366. *preview->index() < *preview->pending_index()) {
  367. shift_out_delay += kPreviewItemUpdateDelayIncrement;
  368. }
  369. }
  370. base::TimeDelta shift_in_delay;
  371. for (auto& item_id : item_ids_) {
  372. auto* preview = previews_by_id_.find(item_id)->second.get();
  373. // Existing items have current index set.
  374. if (preview->index().has_value()) {
  375. const bool shift_out = *preview->index() < *preview->pending_index();
  376. animation_params.emplace_back(PreviewAnimationParams{
  377. .preview = preview,
  378. .delay = shift_out ? shift_out_delay : shift_in_delay,
  379. });
  380. if (shift_out) {
  381. shift_out_delay -= kPreviewItemUpdateDelayIncrement;
  382. if (shift_out_delay < base::TimeDelta())
  383. shift_out_delay = base::TimeDelta();
  384. } else {
  385. shift_in_delay += kPreviewItemUpdateDelayIncrement;
  386. }
  387. }
  388. }
  389. return animation_params;
  390. }
  391. std::vector<HoldingSpaceTrayIcon::PreviewAnimationParams>
  392. HoldingSpaceTrayIcon::CalculateAnimateInParams() {
  393. // Items animating in should do so with a delay that increases in order of
  394. // addition, which is reverse of order in `items_`.
  395. std::vector<PreviewAnimationParams> animation_params;
  396. // Calculate the max delay which will be used for the first item.
  397. // NOTE: When animating in, an extra preview may be visible before items
  398. // before it drops into their position.
  399. base::TimeDelta addition_delay;
  400. for (size_t i = 0;
  401. i < kHoldingSpaceTrayIconMaxVisiblePreviews + 1 && i < item_ids_.size();
  402. ++i) {
  403. auto* preview = previews_by_id_.find(item_ids_[i])->second.get();
  404. if (!preview->index().has_value())
  405. addition_delay += kPreviewItemUpdateDelayIncrement;
  406. }
  407. for (auto& item_id : item_ids_) {
  408. auto* preview = previews_by_id_.find(item_id)->second.get();
  409. // New items do not have current index set.
  410. if (!preview->index().has_value()) {
  411. animation_params.emplace_back(PreviewAnimationParams{
  412. .preview = preview,
  413. .delay = addition_delay,
  414. });
  415. addition_delay -= kPreviewItemUpdateDelayIncrement;
  416. if (addition_delay < base::TimeDelta())
  417. addition_delay = base::TimeDelta();
  418. }
  419. }
  420. return animation_params;
  421. }
  422. void HoldingSpaceTrayIcon::EnsurePreviewLayerStackingOrder() {
  423. for (const auto& item_id : item_ids_) {
  424. auto* preview = previews_by_id_.find(item_id)->second.get();
  425. if (preview->layer())
  426. previews_container_->layer()->StackAtBottom(preview->layer());
  427. }
  428. }
  429. BEGIN_METADATA(HoldingSpaceTrayIcon, views::View)
  430. END_METADATA
  431. } // namespace ash