Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(2259)

Unified Diff: chrome/browser/resources/settings/settings_page/main_page_behavior.js

Issue 1736733003: MD Settings: Custom expand/collapse section animations. (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: function/rebase Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/settings/settings_page/main_page_behavior.js
diff --git a/chrome/browser/resources/settings/settings_page/main_page_behavior.js b/chrome/browser/resources/settings/settings_page/main_page_behavior.js
new file mode 100644
index 0000000000000000000000000000000000000000..99dbc67c6181c689a6c3d63067f450ed323534c5
--- /dev/null
+++ b/chrome/browser/resources/settings/settings_page/main_page_behavior.js
@@ -0,0 +1,379 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// Fast out, slow in.
+var EASING_FUNCTION = 'cubic-bezier(0.4, 0, 0.2, 1)';
+var EXPAND_DURATION = 350;
+
+/**
+ * Provides animations to expand and collapse individual sections in a page.
+ * Expanded sections take up the full height of the container. At most one
+ * section should be expanded at any given time.
+ * @polymerBehavior Polymer.MainPageBehavior
+ */
+var MainPageBehaviorImpl = {
+ /**
+ * @type {string} Selector to get the sections. Derived elements
+ * must override.
+ */
+ sectionSelector: '',
+
+ /** @type {?Element} The scrolling container. Elements must set this. */
+ scroller: null,
+
+ /**
+ * Hides or unhides the sections not being expanded.
+ * @param {string} sectionName The section to keep visible.
+ * @param {boolean} hidden Whether the sections should be hidden.
+ * @private
+ */
+ toggleOtherSectionsHidden_: function(sectionName, hidden) {
+ var sections = Polymer.dom(this.root).querySelectorAll(
+ this.sectionSelector + ':not([section=' + sectionName + '])');
+ for (var section of sections)
+ section.hidden = hidden;
+ },
+
+ /**
+ * Animates the card in |section|, expanding it to fill the page.
+ * @param {!SettingsSectionElement} section
+ */
+ expandSection: function(section) {
+ // If another section's card is expanding, cancel that animation first.
+ var expanding = this.$$('.expanding');
+ if (expanding) {
+ if (expanding == section)
+ return;
+
+ if (this.animations['section']) {
+ // Cancel the animation, then call startExpandSection_.
+ this.cancelAnimation('section', function() {
+ this.startExpandSection_(section);
+ }.bind(this));
+ } else {
+ // The animation must have finished but its promise hasn't resolved yet.
+ // When it resolves, collapse that section's card before expanding
+ // this one.
+ setTimeout(function() {
+ this.collapseSection(
+ /** @type {!SettingsSectionElement} */(expanding));
+ this.finishAnimation('section', function() {
+ this.startExpandSection_(section);
+ }.bind(this));
+ }.bind(this));
+ }
+
+ return;
+ }
+
+ if (this.$$('.collapsing') && this.animations['section']) {
+ // Finish the collapse animation before expanding.
+ this.finishAnimation('section', function() {
+ this.startExpandSection_(section);
+ }.bind(this));
+ return;
+ }
+
+ this.startExpandSection_(section);
+ },
+
+ /**
+ * Helper function to set up and start the expand animation.
+ * @param {!SettingsSectionElement} section
+ */
+ startExpandSection_: function(section) {
+ if (section.classList.contains('expanded'))
+ return;
+
+ // Freeze the scroller and save its position.
+ this.listScrollTop_ = this.scroller.scrollTop;
+
+ var scrollerWidth = this.scroller.clientWidth;
+ this.scroller.style.overflow = 'hidden';
+ // Adjust width to compensate for scroller.
+ var scrollbarWidth = this.scroller.clientWidth - scrollerWidth;
+ this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)';
+
+ // Freezes the section's height so its card can be removed from the flow.
+ this.freezeSection_(section);
+
+ // Expand the section's card to fill the parent.
+ var animationPromise = this.playExpandSection_(section);
+
+ animationPromise.then(function() {
+ this.scroller.scrollTop = 0;
+ this.toggleOtherSectionsHidden_(section.section, true);
+ }.bind(this), function() {
+ // Animation was canceled; restore the section.
+ this.unfreezeSection_(section);
+ }.bind(this)).then(function() {
+ this.scroller.style.overflow = '';
+ this.scroller.style.width = '';
+ }.bind(this));
+ },
+
+ /**
+ * Animates the card in |section|, collapsing it back into its section.
+ * @param {!SettingsSectionElement} section
+ */
+ collapseSection: function(section) {
+ // If the section's card is still expanding, cancel the expand animation.
+ if (section.classList.contains('expanding')) {
+ if (this.animations['section']) {
+ this.cancelAnimation('section');
+ } else {
+ // The animation must have finished but its promise hasn't finished
+ // resolving; try again asynchronously.
+ this.async(function() {
+ this.collapseSection(section);
+ });
+ }
+ return;
+ }
+
+ if (!section.classList.contains('expanded'))
+ return;
+
+ this.toggleOtherSectionsHidden_(section.section, false);
+
+ var scrollerWidth = this.scroller.clientWidth;
+ this.scroller.style.overflow = 'hidden';
+ // Adjust width to compensate for scroller.
+ var scrollbarWidth = this.scroller.clientWidth - scrollerWidth;
+ this.scroller.style.width = 'calc(100% - ' + scrollbarWidth + 'px)';
+
+ this.playCollapseSection_(section).then(function() {
+ this.unfreezeSection_(section);
+ this.scroller.style.overflow = '';
+ this.scroller.style.width = '';
+ section.classList.remove('collapsing');
+ }.bind(this));
+ },
+
+ /**
+ * Freezes a section's height so its card can be removed from the flow without
+ * affecting the layout of the surrounding sections.
+ * @param {!SettingsSectionElement} section
+ * @private
+ */
+ freezeSection_: function(section) {
+ var card = section.$.card;
+ section.style.height = section.clientHeight + 'px';
+
+ var cardHeight = card.offsetHeight;
+ var cardWidth = card.offsetWidth;
+ // If the section is not displayed yet (e.g., navigated directly to a
+ // sub-page), cardHeight and cardWidth are 0, so do not set the height or
+ // width explicitly.
+ // TODO(michaelpg): Improve this logic when refactoring
+ // settings-animated-pages.
+ if (cardHeight && cardWidth) {
+ // TODO(michaelpg): Temporary hack to store the height the section should
+ // collapse to when it closes.
+ card.origHeight_ = cardHeight;
+
+ card.style.height = cardHeight + 'px';
+ card.style.width = cardWidth + 'px';
+ } else {
+ // Set an invalid value so we don't try to use it later.
+ card.origHeight_ = NaN;
+ }
+
+ // Place the section's card at its current position but removed from the
+ // flow.
+ card.style.top = card.getBoundingClientRect().top + 'px';
+ section.classList.add('frozen');
+ },
+
+ /**
+ * After freezeSection_, restores the section to its normal height.
+ * @param {!SettingsSectionElement} section
+ * @private
+ */
+ unfreezeSection_: function(section) {
+ if (!section.classList.contains('frozen'))
+ return;
+ var card = section.$.card;
+ section.classList.remove('frozen');
+ card.style.top = '';
+ card.style.height = '';
+ card.style.width = '';
+ section.style.height = '';
+ },
+
+ /**
+ * Expands the card in |section| to fill the page.
+ * @param {!SettingsSectionElement} section
+ * @return {!Promise}
+ * @private
+ */
+ playExpandSection_: function(section) {
+ var card = section.$.card;
+
+ // The card should start at the top of the page.
+ var targetTop = this.parentElement.getBoundingClientRect().top;
+
+ section.classList.add('expanding');
+
+ // Expand the card, using minHeight. (The card must span the container's
+ // client height, so it must be at least 100% in case the card is too short.
+ // If the card is already taller than the container's client height, we
+ // don't want to shrink the card to 100% or the content will overflow, so
+ // we can't use height, and animating height wouldn't look right anyway.)
+ var keyframes = [{
+ top: card.style.top,
+ minHeight: card.style.height,
+ easing: EASING_FUNCTION,
+ }, {
+ top: targetTop + 'px',
+ minHeight: 'calc(100% - ' + targetTop + 'px)',
+ }];
+ var options = /** @type {!KeyframeEffectOptions} */({
+ duration: EXPAND_DURATION
+ });
+ // TODO(michaelpg): Change elevation of sections.
+ var promise;
+ if (keyframes[0].top && keyframes[0].minHeight)
+ promise = this.animateElement('section', card, keyframes, options);
+ else
+ promise = Promise.resolve();
+
+ promise.then(function() {
+ section.classList.add('expanded');
+ card.style.top = '';
+ this.style.margin = 'auto';
+ section.$.header.hidden = true;
+ section.style.height = '';
+ }.bind(this), function() {
+ // The animation was canceled; catch the error and continue.
+ }).then(function() {
+ // Whether finished or canceled, clean up the animation.
+ section.classList.remove('expanding');
+ card.style.height = '';
+ });
+
+ return promise;
+ },
+
+ /**
+ * Collapses the card in |section| back to its normal position.
+ * @param {!SettingsSectionElement} section
+ * @return {!Promise}
+ * @private
+ */
+ playCollapseSection_: function(section) {
+ var card = section.$.card;
+ var cardStyle = getComputedStyle(card);
+
+ this.style.margin = '';
+ section.$.header.hidden = false;
+
+ var startingTop = this.parentElement.getBoundingClientRect().top;
+
+ var cardHeightStart = card.clientHeight;
+
+ section.classList.add('collapsing');
+ section.classList.remove('expanding', 'expanded');
+
+ // If we navigated here directly, we don't know the original height of the
+ // section, so we skip the animation.
+ // TODO(michaelpg): remove this condition once sliding is implemented.
+ if (isNaN(card.origHeight_))
+ return Promise.resolve();
+
+ // Restore the section to its proper height to make room for the card.
+ section.style.height = section.clientHeight + card.origHeight_ + 'px';
+
+ // TODO(michaelpg): this should be in collapseSection(), but we need to wait
+ // until the full page height is available (setting the section height).
+ this.scroller.scrollTop = this.listScrollTop_;
+
+ // The card is unpositioned, so use its position as the ending state,
+ // but account for scroll.
+ var targetTop = card.getBoundingClientRect().top - this.scroller.scrollTop;
+
+ var keyframes = [{
+ top: startingTop + 'px',
+ minHeight: cardHeightStart + 'px',
+ easing: EASING_FUNCTION,
+ }, {
+ top: targetTop + 'px',
+ minHeight: card.origHeight_ + 'px',
+ }];
+ var options = /** @type {!KeyframeEffectOptions} */({
+ duration: EXPAND_DURATION
+ });
+ var promise = this.animateElement('section', card, keyframes, options);
+ return promise;
+ },
+};
+
+/** @polymerBehavior */
+var MainPageBehavior = [
+ TransitionBehavior,
+ MainPageBehaviorImpl
+];
+
+/**
+ * TODO(michaelpg): integrate slide animations.
+ * @polymerBehavior RoutableBehavior
+ */
+var RoutableBehaviorImpl = {
+ properties: {
+ /** Contains the current route. */
+ currentRoute: {
+ type: Object,
+ notify: true,
+ observer: 'currentRouteChanged_',
+ },
+ },
+
+ /** @private */
+ currentRouteChanged_: function(newRoute, oldRoute) {
+ // route.section is only non-empty when the user is within a subpage.
+ // When the user is not in a subpage, but on the Basic page, route.section
+ // is an empty string.
+ var newRouteIsSubpage = newRoute && newRoute.section;
+ var oldRouteIsSubpage = oldRoute && oldRoute.section;
+
+ if (!oldRoute && newRouteIsSubpage) {
+ // Allow the page to load before expanding the section. TODO(michaelpg):
+ // Time this better when refactoring settings-animated-pages.
+ setTimeout(function() {
+ var section = this.getSection_(newRoute.section);
+ if (section)
+ this.expandSection(section);
+ }.bind(this));
+ return;
+ }
+
+ if (!newRouteIsSubpage && oldRouteIsSubpage) {
+ var section = this.getSection_(oldRoute.section);
+ if (section)
+ this.collapseSection(section);
+ } else if (newRouteIsSubpage &&
+ (!oldRouteIsSubpage || newRoute.section != oldRoute.section)) {
+ var section = this.getSection_(newRoute.section);
+ if (section)
+ this.expandSection(section);
+ }
+ },
+
+ /**
+ * Helper function to get a section from the local DOM.
+ * @param {string} section Section name of the element to get.
+ * @return {?SettingsSectionElement}
+ * @private
+ */
+ getSection_: function(section) {
+ return /** @type {?SettingsSectionElement} */(
+ this.$$('[section=' + section + ']'));
+ },
+};
+
+/** @polymerBehavior */
+var RoutableBehavior = [
+ MainPageBehavior,
+ RoutableBehaviorImpl
+];

Powered by Google App Engine
This is Rietveld 408576698