holding_space_item_views_section.cc 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  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_item_views_section.h"
  5. #include "ash/public/cpp/holding_space/holding_space_constants.h"
  6. #include "ash/public/cpp/holding_space/holding_space_controller.h"
  7. #include "ash/public/cpp/holding_space/holding_space_model.h"
  8. #include "ash/system/holding_space/holding_space_item_view.h"
  9. #include "ash/system/holding_space/holding_space_util.h"
  10. #include "ash/system/holding_space/holding_space_view_delegate.h"
  11. #include "base/auto_reset.h"
  12. #include "base/bind.h"
  13. #include "base/callback_helpers.h"
  14. #include "base/containers/contains.h"
  15. #include "ui/compositor/callback_layer_animation_observer.h"
  16. #include "ui/compositor/layer.h"
  17. #include "ui/compositor/layer_animation_observer.h"
  18. #include "ui/compositor/layer_animator.h"
  19. #include "ui/views/accessibility/view_accessibility.h"
  20. #include "ui/views/controls/focus_ring.h"
  21. #include "ui/views/controls/scroll_view.h"
  22. #include "ui/views/layout/box_layout.h"
  23. namespace ash {
  24. namespace {
  25. using ScrollBarMode = views::ScrollView::ScrollBarMode;
  26. // Animation.
  27. constexpr base::TimeDelta kAnimationDuration = base::Milliseconds(167);
  28. // Helpers ---------------------------------------------------------------------
  29. // Initializes the layer for the specified `view` for animations.
  30. void InitLayerForAnimations(views::View* view) {
  31. view->SetPaintToLayer();
  32. view->layer()->SetFillsBoundsOpaquely(false);
  33. view->layer()->GetAnimator()->set_preemption_strategy(
  34. ui::LayerAnimator::PreemptionStrategy::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
  35. }
  36. // Returns a callback which deletes the associated animation observer after
  37. // running another `callback`.
  38. using AnimationCompletedCallback =
  39. base::OnceCallback<void(const ui::CallbackLayerAnimationObserver&)>;
  40. base::RepeatingCallback<bool(const ui::CallbackLayerAnimationObserver&)>
  41. DeleteObserverAfterRunning(AnimationCompletedCallback callback) {
  42. return base::BindRepeating(
  43. [](AnimationCompletedCallback callback,
  44. const ui::CallbackLayerAnimationObserver& observer) {
  45. // NOTE: It's safe to move `callback` since this code will only run
  46. // once due to deletion of the associated `observer`. The `observer` is
  47. // deleted by returning `true`.
  48. std::move(callback).Run(observer);
  49. return true;
  50. },
  51. base::Passed(std::move(callback)));
  52. }
  53. // HoldingSpaceScrollView ------------------------------------------------------
  54. class HoldingSpaceScrollView : public views::ScrollView,
  55. public views::ViewObserver {
  56. public:
  57. HoldingSpaceScrollView() {
  58. // `HoldingSpaceItemView`s draw a focus ring outside of their view bounds.
  59. // `HoldingSpaceScrollView` needs to paint to a layer to avoid clipping
  60. // these focus rings.
  61. SetPaintToLayer();
  62. layer()->SetFillsBoundsOpaquely(false);
  63. }
  64. views::View* SetContents(std::unique_ptr<views::View> view) {
  65. views::View* contents = views::ScrollView::SetContents(std::move(view));
  66. view_observer_.Observe(contents);
  67. return contents;
  68. }
  69. private:
  70. // views::ScrollView:
  71. void OnBoundsChanged(const gfx::Rect& previous_bounds) override {
  72. // The focus ring for `HoldingSpaceItemView`s is painted just outside of
  73. // their view bounds. The clip rect for this view should be expanded to
  74. // avoid clipping of these focus rings. Note that a clip rect *does* need to
  75. // be applied to prevent this view from painting its contents outside of its
  76. // viewport.
  77. const float kFocusInsets = kHoldingSpaceFocusInsets -
  78. (views::FocusRing::kDefaultHaloThickness / 2.f);
  79. gfx::Rect bounds = GetLocalBounds();
  80. bounds.Inset(gfx::Insets(kFocusInsets));
  81. layer()->SetClipRect(bounds);
  82. }
  83. // views::ViewObserver:
  84. void OnViewPreferredSizeChanged(View* observed_view) override {
  85. PreferredSizeChanged();
  86. }
  87. void OnViewVisibilityChanged(views::View* observed_view,
  88. views::View* starting_view) override {
  89. // Sync scroll view visibility with contents visibility.
  90. if (GetVisible() != observed_view->GetVisible())
  91. SetVisible(observed_view->GetVisible());
  92. }
  93. void OnViewIsDeleting(View* observed_view) override {
  94. view_observer_.Reset();
  95. }
  96. base::ScopedObservation<views::View, views::ViewObserver> view_observer_{
  97. this};
  98. };
  99. } // namespace
  100. // HoldingSpaceItemViewsSection ------------------------------------------------
  101. HoldingSpaceItemViewsSection::HoldingSpaceItemViewsSection(
  102. HoldingSpaceViewDelegate* delegate,
  103. std::set<HoldingSpaceItem::Type> supported_types,
  104. const absl::optional<size_t>& max_count)
  105. : delegate_(delegate),
  106. supported_types_(std::move(supported_types)),
  107. max_count_(max_count) {}
  108. HoldingSpaceItemViewsSection::~HoldingSpaceItemViewsSection() = default;
  109. void HoldingSpaceItemViewsSection::Init() {
  110. // Disable propagation of `PreferredSizeChanged()` while initializing this
  111. // view to reduce the number of layout events bubbling up.
  112. disable_preferred_size_changed_ = true;
  113. SetVisible(false);
  114. auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
  115. views::BoxLayout::Orientation::kVertical, gfx::Insets(),
  116. kHoldingSpaceSectionChildSpacing));
  117. // Header.
  118. header_ = AddChildView(CreateHeader());
  119. InitLayerForAnimations(header_);
  120. header_->layer()->SetOpacity(0.f);
  121. header_->SetVisible(false);
  122. // Container.
  123. // NOTE: If `max_count_` is not present `container_` does not limit the number
  124. // of holding space item views visible to the user at one time. In this case
  125. // `container_` needs to be wrapped in a `views::ScrollView` to allow the user
  126. // access to all contained item views.
  127. if (max_count_.has_value()) {
  128. container_ = AddChildView(CreateContainer());
  129. } else {
  130. auto* scroll = AddChildView(std::make_unique<HoldingSpaceScrollView>());
  131. scroll->SetBackgroundColor(absl::nullopt);
  132. scroll->ClipHeightTo(0, INT_MAX);
  133. scroll->SetDrawOverflowIndicator(false);
  134. scroll->SetVerticalScrollBarMode(ScrollBarMode::kHiddenButEnabled);
  135. layout->SetFlexForView(scroll, 1);
  136. container_ = scroll->SetContents(CreateContainer());
  137. scroll_view_ = scroll;
  138. }
  139. InitLayerForAnimations(container_);
  140. container_->SetVisible(false);
  141. // The `container_`'s children should be announced "List item X of Y", where
  142. // X is the 1-based child index and Y is the count of children.
  143. container_->GetViewAccessibility().OverrideRole(ax::mojom::Role::kList);
  144. // Placeholder.
  145. auto placeholder = CreatePlaceholder();
  146. if (placeholder) {
  147. placeholder_ = AddChildView(std::move(placeholder));
  148. InitLayerForAnimations(placeholder_);
  149. placeholder_->SetVisible(true);
  150. header_->layer()->SetOpacity(1.f);
  151. header_->SetVisible(true);
  152. }
  153. // Views.
  154. HoldingSpaceModel* model = HoldingSpaceController::Get()->model();
  155. if (model && !model->items().empty()) {
  156. std::vector<const HoldingSpaceItem*> item_ptrs;
  157. for (const auto& item : model->items())
  158. item_ptrs.push_back(item.get());
  159. // Sections are not animated during initialization as their respective
  160. // bubbles will be animated in instead.
  161. base::AutoReset<bool> scoped_disable_animations(&disable_animations_, true);
  162. OnHoldingSpaceItemsAdded(item_ptrs);
  163. }
  164. // Re-enable propagation of `PreferredSizeChanged()` after initializing.
  165. disable_preferred_size_changed_ = false;
  166. PreferredSizeChanged();
  167. }
  168. void HoldingSpaceItemViewsSection::Reset() {
  169. // The holding space item views `delegate_` will be destroyed before this view
  170. // when asynchronously closing the holding space bubble. To prevent accessing
  171. // `delegate_` after deletion, prevent animation callbacks from being run.
  172. weak_factory_.InvalidateWeakPtrs();
  173. // Propagate `Reset()` to children.
  174. for (views::View* view : container_->children()) {
  175. DCHECK(HoldingSpaceItemView::IsInstance(view));
  176. HoldingSpaceItemView::Cast(view)->Reset();
  177. }
  178. }
  179. std::vector<HoldingSpaceItemView*>
  180. HoldingSpaceItemViewsSection::GetHoldingSpaceItemViews() {
  181. std::vector<HoldingSpaceItemView*> views;
  182. for (views::View* view : container_->children()) {
  183. DCHECK(HoldingSpaceItemView::IsInstance(view));
  184. views.push_back(HoldingSpaceItemView::Cast(view));
  185. }
  186. return views;
  187. }
  188. void HoldingSpaceItemViewsSection::ChildPreferredSizeChanged(
  189. views::View* child) {
  190. PreferredSizeChanged();
  191. }
  192. void HoldingSpaceItemViewsSection::ChildVisibilityChanged(views::View* child) {
  193. // This section should be visible iff it has visible children.
  194. bool visible = false;
  195. for (const views::View* c : children()) {
  196. if (c->GetVisible()) {
  197. visible = true;
  198. break;
  199. }
  200. }
  201. if (visible != GetVisible())
  202. SetVisible(visible);
  203. PreferredSizeChanged();
  204. }
  205. void HoldingSpaceItemViewsSection::PreferredSizeChanged() {
  206. if (!disable_preferred_size_changed_)
  207. views::View::PreferredSizeChanged();
  208. }
  209. void HoldingSpaceItemViewsSection::ViewHierarchyChanged(
  210. const views::ViewHierarchyChangedDetails& details) {
  211. if (details.parent != container_)
  212. return;
  213. // Update visibility when becoming empty or non-empty. Note that in the case
  214. // of a view being added, `ViewHierarchyChanged()` is called *after* the view
  215. // has been parented but in the case of a view being removed, it is called
  216. // *before* the view is un-parented.
  217. if (container_->children().size() != 1u)
  218. return;
  219. // Disable propagation of `PreferredSizeChanged()` while modifying child
  220. // view visibility to reduce the number of layout events bubbling up.
  221. disable_preferred_size_changed_ = true;
  222. header_->SetVisible(placeholder_ || details.is_add);
  223. container_->SetVisible(details.is_add);
  224. if (placeholder_)
  225. placeholder_->SetVisible(!details.is_add);
  226. // Re-enable propagation of `PreferredSizeChanged()` after modifying child
  227. // view visibility.
  228. disable_preferred_size_changed_ = false;
  229. PreferredSizeChanged();
  230. }
  231. void HoldingSpaceItemViewsSection::OnHoldingSpaceItemsAdded(
  232. const std::vector<const HoldingSpaceItem*>& items) {
  233. const bool needs_update = std::any_of(
  234. items.begin(), items.end(), [this](const HoldingSpaceItem* item) {
  235. return item->IsInitialized() &&
  236. base::Contains(supported_types_, item->type());
  237. });
  238. if (needs_update)
  239. MaybeAnimateOut();
  240. }
  241. void HoldingSpaceItemViewsSection::OnHoldingSpaceItemsRemoved(
  242. const std::vector<const HoldingSpaceItem*>& items) {
  243. const bool needs_update = std::any_of(
  244. items.begin(), items.end(), [this](const HoldingSpaceItem* item) {
  245. return base::Contains(views_by_item_id_, item->id());
  246. });
  247. if (needs_update)
  248. MaybeAnimateOut();
  249. }
  250. void HoldingSpaceItemViewsSection::OnHoldingSpaceItemInitialized(
  251. const HoldingSpaceItem* item) {
  252. if (base::Contains(supported_types_, item->type()))
  253. MaybeAnimateOut();
  254. }
  255. void HoldingSpaceItemViewsSection::RemoveAllHoldingSpaceItemViews() {
  256. // Holding space item views should only be removed when the `container_` is
  257. // not visible to the user.
  258. DCHECK(!IsDrawn() || !container_->IsDrawn() ||
  259. container_->layer()->opacity() == 0.f);
  260. container_->RemoveAllChildViews();
  261. views_by_item_id_.clear();
  262. }
  263. std::unique_ptr<views::View> HoldingSpaceItemViewsSection::CreatePlaceholder() {
  264. return nullptr;
  265. }
  266. void HoldingSpaceItemViewsSection::DestroyPlaceholder() {
  267. if (!placeholder_)
  268. return;
  269. RemoveChildViewT(placeholder_);
  270. placeholder_ = nullptr;
  271. // In the absence of `placeholder_`, the `header_` should only be visible
  272. // when `container_` is non-empty.
  273. if (header_->GetVisible() && container_->children().empty())
  274. header_->SetVisible(false);
  275. }
  276. void HoldingSpaceItemViewsSection::MaybeAnimateIn() {
  277. if (animation_state_ & AnimationState::kAnimatingIn)
  278. return;
  279. animation_state_ |= AnimationState::kAnimatingIn;
  280. // NOTE: `animate_in_observer` is deleted after `OnAnimateInCompleted()`.
  281. ui::CallbackLayerAnimationObserver* animate_in_observer =
  282. new ui::CallbackLayerAnimationObserver(DeleteObserverAfterRunning(
  283. base::BindOnce(&HoldingSpaceItemViewsSection::OnAnimateInCompleted,
  284. weak_factory_.GetWeakPtr())));
  285. AnimateIn(animate_in_observer);
  286. animate_in_observer->SetActive();
  287. }
  288. void HoldingSpaceItemViewsSection::MaybeAnimateOut() {
  289. if (animation_state_ & AnimationState::kAnimatingOut)
  290. return;
  291. animation_state_ |= AnimationState::kAnimatingOut;
  292. // Don't allow event processing while animating out. The views being animated
  293. // out may be associated with holding space items that no longer exist and
  294. // so should not be acted upon by the user during this time.
  295. SetCanProcessEventsWithinSubtree(false);
  296. // Hide the vertical scroll bar when swapping out section contents to prevent
  297. // it from showing as views are being added/removed and while the holding
  298. // space bubble is animating bounds.
  299. if (scroll_view_)
  300. scroll_view_->SetVerticalScrollBarMode(ScrollBarMode::kHiddenButEnabled);
  301. // NOTE: `animate_out_observer` is deleted after `OnAnimateOutCompleted()`.
  302. ui::CallbackLayerAnimationObserver* animate_out_observer =
  303. new ui::CallbackLayerAnimationObserver(DeleteObserverAfterRunning(
  304. base::BindOnce(&HoldingSpaceItemViewsSection::OnAnimateOutCompleted,
  305. weak_factory_.GetWeakPtr())));
  306. AnimateOut(animate_out_observer);
  307. animate_out_observer->SetActive();
  308. }
  309. void HoldingSpaceItemViewsSection::AnimateIn(
  310. ui::LayerAnimationObserver* observer) {
  311. const base::TimeDelta animation_duration =
  312. disable_animations_ ? base::TimeDelta() : kAnimationDuration;
  313. // Delay animations slightly to allow time for bubble layout animations to
  314. // complete which animate size changes for this view when needed.
  315. const base::TimeDelta animation_delay =
  316. disable_animations_ ? base::TimeDelta() : kAnimationDuration;
  317. // If the `header_` is not opaque, this section was not previously visible
  318. // to the user so the `header_` needs to be animated in alongside any content.
  319. const bool animate_in_header = header_->layer()->GetTargetOpacity() != 1.f;
  320. if (animate_in_header) {
  321. holding_space_util::AnimateIn(header_, animation_duration, animation_delay,
  322. observer);
  323. }
  324. if (views_by_item_id_.empty() && placeholder_) {
  325. holding_space_util::AnimateIn(placeholder_, animation_duration,
  326. animation_delay, observer);
  327. return;
  328. }
  329. holding_space_util::AnimateIn(container_, animation_duration, animation_delay,
  330. observer);
  331. }
  332. void HoldingSpaceItemViewsSection::AnimateOut(
  333. ui::LayerAnimationObserver* observer) {
  334. // If this view is not drawn, animating will only cause latency to the user.
  335. const bool disable_animations = disable_animations_ || !IsDrawn();
  336. const base::TimeDelta animation_duration =
  337. disable_animations ? base::TimeDelta() : kAnimationDuration;
  338. // If this section does not have a `placeholder_` and the model does not
  339. // contain any associated and initialized items, then this section is becoming
  340. // invisible to the user and the `header_` needs to be animated out alongside
  341. // any content.
  342. bool animate_out_header = !placeholder_;
  343. if (animate_out_header) {
  344. HoldingSpaceModel* model = HoldingSpaceController::Get()->model();
  345. if (model) {
  346. animate_out_header = std::none_of(
  347. supported_types_.begin(), supported_types_.end(),
  348. [&model](HoldingSpaceItem::Type supported_type) {
  349. return model->ContainsInitializedItemOfType(supported_type);
  350. });
  351. }
  352. }
  353. if (animate_out_header)
  354. holding_space_util::AnimateOut(header_, animation_duration, observer);
  355. if (placeholder_ && placeholder_->GetVisible()) {
  356. DCHECK(views_by_item_id_.empty());
  357. holding_space_util::AnimateOut(placeholder_, animation_duration, observer);
  358. return;
  359. }
  360. holding_space_util::AnimateOut(container_, animation_duration, observer);
  361. }
  362. void HoldingSpaceItemViewsSection::OnAnimateInCompleted(
  363. const ui::CallbackLayerAnimationObserver& observer) {
  364. DCHECK(animation_state_ & AnimationState::kAnimatingIn);
  365. animation_state_ &= ~AnimationState::kAnimatingIn;
  366. if (observer.aborted_count())
  367. return;
  368. DCHECK_EQ(animation_state_, AnimationState::kNotAnimating);
  369. // Restore event processing that was disabled while animating out. The views
  370. // that have been animated in should all be associated with holding space
  371. // items that exist in the model.
  372. SetCanProcessEventsWithinSubtree(true);
  373. // Once contents have animated in the holding space bubble should have reached
  374. // its target bounds and the vertical scroll bar can be re-enabled.
  375. if (scroll_view_)
  376. scroll_view_->SetVerticalScrollBarMode(ScrollBarMode::kEnabled);
  377. }
  378. void HoldingSpaceItemViewsSection::OnAnimateOutCompleted(
  379. const ui::CallbackLayerAnimationObserver& observer) {
  380. DCHECK(animation_state_ & AnimationState::kAnimatingOut);
  381. animation_state_ &= ~AnimationState::kAnimatingOut;
  382. if (observer.aborted_count())
  383. return;
  384. DCHECK_EQ(animation_state_, AnimationState::kNotAnimating);
  385. // All holding space item views are going to be removed after which views will
  386. // be re-added for those items which still exist. A `ScopedSelectionRestore`
  387. // will serve to persist the current selection during this modification.
  388. HoldingSpaceViewDelegate::ScopedSelectionRestore scoped_selection_restore(
  389. delegate_);
  390. // Disable propagation of `PreferredSizeChanged()` while performing batch
  391. // child additions/removals to reduce the number of layout events bubbling up.
  392. disable_preferred_size_changed_ = true;
  393. base::ScopedClosureRunner scoped_preferred_size_changed(base::BindOnce(
  394. [](HoldingSpaceItemViewsSection* section) {
  395. section->disable_preferred_size_changed_ = false;
  396. section->PreferredSizeChanged();
  397. },
  398. base::Unretained(this)));
  399. RemoveAllHoldingSpaceItemViews();
  400. DCHECK(views_by_item_id_.empty());
  401. HoldingSpaceModel* model = HoldingSpaceController::Get()->model();
  402. if (!model)
  403. return;
  404. for (const auto& item : model->items()) {
  405. if (item->IsInitialized() &&
  406. base::Contains(supported_types_, item->type())) {
  407. DCHECK(!base::Contains(views_by_item_id_, item->id()));
  408. // Remove the last holding space item view if already at max capacity.
  409. if (max_count_ && container_->children().size() == max_count_.value()) {
  410. auto view = container_->RemoveChildViewT(container_->children().back());
  411. views_by_item_id_.erase(
  412. HoldingSpaceItemView::Cast(view.get())->item()->id());
  413. }
  414. // Add holding space item view to the front in order to sort by recency.
  415. views_by_item_id_[item->id()] =
  416. container_->AddChildViewAt(CreateView(item.get()), 0);
  417. }
  418. }
  419. // Only animate this section in if it has content to show.
  420. if (placeholder_ || !container_->children().empty())
  421. MaybeAnimateIn();
  422. }
  423. } // namespace ash