| <!-- |
| // Copyright 2015 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. |
| --> |
| <import src="sky-element.sky" /> |
| |
| <sky-element> |
| <template> |
| <style> |
| :host { |
| overflow: hidden; |
| position: relative; |
| will-change: transform; |
| } |
| #scrollable { |
| will-change: transform; |
| } |
| #vbar { |
| position: absolute; |
| right: 0; |
| background-color: lightgray; |
| pointer-events: none; |
| top: 0; |
| height: 0; |
| will-change: opacity; |
| opacity: 0; |
| transition-property: opacity; |
| transition-function: ease-in-out; |
| } |
| </style> |
| <div id="scrollable"> |
| <content /> |
| </div> |
| <div id="vbar" /> |
| </template> |
| <script> |
| import "dart:math" as math; |
| import "dart:sky"; |
| import "../animation/fling_curve.dart"; |
| import "../theme/view-configuration.dart" as config; |
| |
| @Tagname('sky-scrollable') |
| class SkyScrollable extends SkyElement { |
| Element _scrollable; |
| Element _vbar; |
| double _scrollOffset = 0.0; |
| FlingCurve _flingCurve; |
| int _flingAnimationId; |
| |
| SkyScrollable() { |
| addEventListener('gesturescrollstart', _handleScrollStart); |
| addEventListener('gesturescrollend', _handleScrollEnd); |
| addEventListener('gesturescrollupdate', _handleScrollUpdate); |
| addEventListener('gestureflingstart', _handleFlingStart); |
| addEventListener('gestureflingcancel', _handleFlingCancel); |
| addEventListener('wheel', _handleWheel); |
| } |
| |
| void shadowRootReady() { |
| _scrollable = shadowRoot.getElementById('scrollable'); |
| _vbar = shadowRoot.getElementById('vbar'); |
| // This is not documented anywhere, but the scrollbar appears to only paint |
| // 3px even though it's official width is 10px? |
| // Chrome appears 3px wide with a 3px outer spacing. |
| // Contacts appears 3px wide with a 5px runner and 5px outer spacing. |
| // Settings appears 4px wide with no outer spacing. |
| const double paintPercent = 0.3; |
| const double outerGapPercent = 0.3; |
| const double innerGapPercent = 0.4; |
| const double paintWidth = paintPercent * config.kScrollbarSize; |
| _vbar.style['width'] = "${paintWidth}px"; |
| _vbar.style['margin-right'] = "${outerGapPercent * config.kScrollbarSize}px"; |
| _vbar.style['margin-left'] ="${innerGapPercent * config.kScrollbarSize}px"; |
| // The scroll thumb never quite makes it to the top or bottom in gmail |
| // or chrome (in chrome more from the bottom than the top). |
| _vbar.style['margin-top'] = "${config.kScrollbarSize}px"; |
| _vbar.style['margin-bottom'] = "${config.kScrollbarSize}px"; |
| |
| // Some android apps round their scrollbars, some don't, not rounding for now. |
| |
| const double msToSeconds = 1.0 / 1000.0; |
| _vbar.style['transition-duration'] = "${msToSeconds * config.kScrollbarFadeDuration}s"; |
| _vbar.style['transition-delay'] = "${msToSeconds * config.kScrollbarFadeDelay}s"; |
| } |
| |
| double get scrollOffset => _scrollOffset; |
| |
| set scrollOffset(double value) { |
| // TODO(abarth): Can we get these values without forcing a synchronous layout? |
| double outerHeight = clientHeight.toDouble(); |
| double innerHeight = _scrollable.clientHeight.toDouble(); |
| double scrollRange = innerHeight - outerHeight; |
| double newScrollOffset = math.max(0.0, math.min(scrollRange, value)); |
| if (newScrollOffset == _scrollOffset) |
| return; |
| // TODO(eseidel): We should scroll in device pixels instead of logical |
| // pixels, but to do that correctly we need to use a device pixel unit. |
| _scrollOffset = newScrollOffset; |
| String transform = 'translateY(${(-_scrollOffset).toInt()}px)'; |
| _scrollable.style['transform'] = transform; |
| |
| double topPercent = newScrollOffset / innerHeight * 100.0; |
| double heightPercent = outerHeight / innerHeight * 100.0; |
| _vbar.style['top'] = '${topPercent}%'; |
| _vbar.style['height'] = '${heightPercent}%'; |
| } |
| |
| bool scrollBy(double scrollDelta) { |
| double oldScrollOffset = _scrollOffset; |
| scrollOffset += scrollDelta; |
| return _scrollOffset != oldScrollOffset; |
| } |
| |
| void _scheduleFlingUpdate() { |
| _flingAnimationId = window.requestAnimationFrame(_updateFling); |
| } |
| |
| void _stopFling() { |
| window.cancelAnimationFrame(_flingAnimationId); |
| _flingCurve = null; |
| _flingAnimationId = null; |
| _vbar.style['opacity'] = '0'; |
| } |
| |
| void _updateFling(double timeStamp) { |
| double scrollDelta = _flingCurve.update(timeStamp); |
| if (scrollDelta == 0.0 || !scrollBy(scrollDelta)) |
| _stopFling(); |
| else |
| _scheduleFlingUpdate(); |
| } |
| |
| void _handleScrollStart(_) { |
| _vbar.style['opacity'] = '1'; |
| } |
| |
| void _handleScrollEnd(_) { |
| _vbar.style['opacity'] = '0'; |
| } |
| |
| void _handleScrollUpdate(GestureEvent event) { |
| scrollBy(-event.dy); |
| } |
| |
| void _handleFlingStart(GestureEvent event) { |
| _flingCurve = new FlingCurve(-event.velocityY, event.timeStamp); |
| _scheduleFlingUpdate(); |
| } |
| |
| void _handleFlingCancel(_) { |
| _stopFling(); |
| } |
| |
| void _handleWheel(WheelEvent event) { |
| scrollBy(-event.offsetY); |
| } |
| } |
| |
| _init(script) => register(script, SkyScrollable); |
| </script> |
| </sky-element> |