提交 7ec1ec3c 编写于 作者: N Nicola Fiori (JD342)

Implement smooth scrolling feature

 * Apply smooth scrolling in every scrollable element when mouse wheel scroll
   occurs

 * Apply smooth scrolling in editor view when large jumps occur
   (e.g. when the cursor is moved by pressing PageUp or PageDown)
上级 77dd29d9
......@@ -236,11 +236,11 @@ export abstract class AbstractScrollbar extends Widget {
return this._scrollbarState.validateScrollPosition(desiredScrollPosition);
}
public setDesiredScrollPosition(desiredScrollPosition: number): boolean {
public setDesiredScrollPosition(desiredScrollPosition: number, smoothScrollDuration?: number): boolean {
desiredScrollPosition = this.validateScrollPosition(desiredScrollPosition);
let oldScrollPosition = this._getScrollPosition();
this._setScrollPosition(desiredScrollPosition);
this._setScrollPosition(desiredScrollPosition, smoothScrollDuration);
let newScrollPosition = this._getScrollPosition();
if (oldScrollPosition !== newScrollPosition) {
......@@ -258,5 +258,5 @@ export abstract class AbstractScrollbar extends Widget {
protected abstract _sliderMousePosition(e: IMouseMoveEventData): number;
protected abstract _sliderOrthogonalMousePosition(e: IMouseMoveEventData): number;
protected abstract _getScrollPosition(): number;
protected abstract _setScrollPosition(elementScrollPosition: number): void;
protected abstract _setScrollPosition(elementScrollPosition: number, smoothScrollDuration?: number): void;
}
......@@ -101,9 +101,9 @@ export class HorizontalScrollbar extends AbstractScrollbar {
return scrollState.scrollLeft;
}
protected _setScrollPosition(scrollPosition: number) {
protected _setScrollPosition(scrollPosition: number, smoothScrollDuration?: number) {
this._scrollable.updateState({
scrollLeft: scrollPosition
});
}, smoothScrollDuration);
}
}
......@@ -23,6 +23,7 @@ import Event, { Emitter } from 'vs/base/common/event';
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
const SCROLL_WHEEL_SMOOTH_SCROLL_TRESHOLD = 1.7;
export interface IOverviewRulerLayoutInfo {
parent: HTMLElement;
......@@ -240,7 +241,7 @@ export class ScrollableElement extends Widget {
}
}
const scrollState = this._scrollable.getState();
const scrollState = this._scrollable.getSmoothScrollTargetState();
if (deltaY) {
let currentScrollTop = scrollState.scrollTop;
desiredScrollTop = this._verticalScrollbar.validateScrollPosition((desiredScrollTop !== -1 ? desiredScrollTop : currentScrollTop) - SCROLL_WHEEL_SENSITIVITY * deltaY);
......@@ -258,11 +259,17 @@ export class ScrollableElement extends Widget {
if (desiredScrollTop !== -1 || desiredScrollLeft !== -1) {
if (desiredScrollTop !== -1) {
this._shouldRender = this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop) || this._shouldRender;
// If |∆y| is too small then do not apply smooth scroll animation, because in that case the input source must be a touchpad or something similar.
const applySmoothScroll = this._options.mouseWheelSmoothScroll && Math.abs(deltaY) > SCROLL_WHEEL_SMOOTH_SCROLL_TRESHOLD;
const shouldRender = this._verticalScrollbar.setDesiredScrollPosition(desiredScrollTop, applySmoothScroll ? this._options.mouseWheelSmoothScrollDuration : undefined);
this._shouldRender = shouldRender || this._shouldRender;
desiredScrollTop = -1;
}
if (desiredScrollLeft !== -1) {
this._shouldRender = this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft) || this._shouldRender;
// If |∆x| is too small then do not apply smooth scroll animation, because in that case the input source must be a touchpad or something similar.
const applySmoothScroll = this._options.mouseWheelSmoothScroll && Math.abs(deltaX) > SCROLL_WHEEL_SMOOTH_SCROLL_TRESHOLD;
const shouldRender = this._horizontalScrollbar.setDesiredScrollPosition(desiredScrollLeft, applySmoothScroll ? this._options.mouseWheelSmoothScrollDuration : undefined);
this._shouldRender = shouldRender || this._shouldRender;
desiredScrollLeft = -1;
}
}
......@@ -407,6 +414,8 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
mouseWheelSmoothScrollDuration: (typeof opts.mouseWheelSmoothScrollDuration !== 'undefined' ? opts.mouseWheelSmoothScrollDuration : 100),
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
......
......@@ -30,6 +30,16 @@ export interface ScrollableElementCreationOptions {
* Defaults to true
*/
handleMouseWheel?: boolean;
/**
* If mouse wheel is handled, make mouse wheel scrolling smooth.
* Defaults to true.
*/
mouseWheelSmoothScroll?: boolean;
/**
* Duration in milliseconds for mouse wheel smooth scrolling animation.
* Defaults to 100.
*/
mouseWheelSmoothScrollDuration?: number;
/**
* Flip axes. Treat vertical scrolling like horizontal and vice-versa.
* Defaults to false.
......@@ -120,6 +130,8 @@ export interface ScrollableElementResolvedOptions {
scrollYToX: boolean;
alwaysConsumeMouseWheel: boolean;
mouseWheelScrollSensitivity: number;
mouseWheelSmoothScroll: boolean;
mouseWheelSmoothScrollDuration: number;
arrowSize: number;
listenOnDomNode: HTMLElement;
horizontal: ScrollbarVisibility;
......
......@@ -106,9 +106,9 @@ export class VerticalScrollbar extends AbstractScrollbar {
return scrollState.scrollTop;
}
protected _setScrollPosition(scrollPosition: number): void {
protected _setScrollPosition(scrollPosition: number, smoothScrollDuration?: number): void {
this._scrollable.updateState({
scrollTop: scrollPosition
});
}, smoothScrollDuration);
}
}
......@@ -95,6 +95,17 @@ export class ScrollState {
);
}
public createUpdated(update: INewScrollState): ScrollState {
return new ScrollState(
(typeof update.width !== 'undefined' ? update.width : this.width),
(typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : this.scrollWidth),
(typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : this.scrollLeft),
(typeof update.height !== 'undefined' ? update.height : this.height),
(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : this.scrollHeight),
(typeof update.scrollTop !== 'undefined' ? update.scrollTop : this.scrollTop)
);
}
public createScrollEvent(previous: ScrollState): ScrollEvent {
let widthChanged = (this.width !== previous.width);
let scrollWidthChanged = (this.scrollWidth !== previous.scrollWidth);
......@@ -140,6 +151,8 @@ export class Scrollable extends Disposable {
_scrollableBrand: void;
private _state: ScrollState;
private _smoothScrolling: boolean;
private _smoothScrollAnimationParams: ISmoothScrollAnimationParams;
private _onScroll = this._register(new Emitter<ScrollEvent>());
public onScroll: Event<ScrollEvent> = this._onScroll.event;
......@@ -148,29 +161,101 @@ export class Scrollable extends Disposable {
super();
this._state = new ScrollState(0, 0, 0, 0, 0, 0);
this._smoothScrolling = false;
this._smoothScrollAnimationParams = null;
}
public getState(): ScrollState {
return this._state;
}
public updateState(update: INewScrollState): void {
const oldState = this._state;
const newState = new ScrollState(
(typeof update.width !== 'undefined' ? update.width : oldState.width),
(typeof update.scrollWidth !== 'undefined' ? update.scrollWidth : oldState.scrollWidth),
(typeof update.scrollLeft !== 'undefined' ? update.scrollLeft : oldState.scrollLeft),
(typeof update.height !== 'undefined' ? update.height : oldState.height),
(typeof update.scrollHeight !== 'undefined' ? update.scrollHeight : oldState.scrollHeight),
(typeof update.scrollTop !== 'undefined' ? update.scrollTop : oldState.scrollTop)
);
/**
* Returns the final scroll state that the instance will have once the smooth scroll animation concludes.
* If no scroll animation is occurring, it will return the actual scroll state instead.
*/
public getSmoothScrollTargetState(): ScrollState {
return this._smoothScrolling ? this._smoothScrollAnimationParams.newState : this._state;
}
public updateState(update: INewScrollState, smoothScrollDuration?: number): void {
// If smooth scroll duration is not specified, then assume that the invoker intends to do an immediate update.
if (smoothScrollDuration === undefined) {
const newState = this._state.createUpdated(update);
if (oldState.equals(newState)) {
// no change
// If smooth scrolling is in progress, terminate it.
if (this._smoothScrolling) {
this._smoothScrolling = false;
this._smoothScrollAnimationParams = null;
}
// Update state immediately if it is different from the previous one.
if (!this._state.equals(newState)) {
this._updateState(newState);
}
}
// Otherwise update scroll state incrementally.
else {
const targetState = this.getSmoothScrollTargetState();
const newTargetState = targetState.createUpdated(update);
// Proceed only if the new target state differs from the current one.
if (!targetState.equals(newTargetState)) {
// Initialize/update smooth scroll parameters.
this._smoothScrollAnimationParams = {
oldState: this._state,
newState: newTargetState,
startTime: Date.now(),
duration: smoothScrollDuration,
};
// Invoke smooth scrolling functionality in the next frame if it is not already in progress.
if (!this._smoothScrolling) {
this._smoothScrolling = true;
requestAnimationFrame(() => { this._performSmoothScroll(); });
}
}
}
}
private _performSmoothScroll(): void {
if (!this._smoothScrolling) {
// Smooth scrolling has been terminated.
return;
}
const completion = (Date.now() - this._smoothScrollAnimationParams.startTime) / this._smoothScrollAnimationParams.duration;
const newState = this._smoothScrollAnimationParams.newState;
if (completion < 1) {
const oldState = this._smoothScrollAnimationParams.oldState;
this._updateState(new ScrollState(
newState.width,
newState.scrollWidth,
oldState.scrollLeft + (newState.scrollLeft - oldState.scrollLeft) * completion,
newState.height,
newState.scrollHeight,
oldState.scrollTop + (newState.scrollTop - oldState.scrollTop) * completion
));
requestAnimationFrame(() => { this._performSmoothScroll(); });
}
else {
this._smoothScrolling = false;
this._smoothScrollAnimationParams = null;
this._updateState(newState);
}
}
private _updateState(newState: ScrollState): void {
const oldState = this._state;
this._state = newState;
this._onScroll.fire(this._state.createScrollEvent(oldState));
}
}
interface ISmoothScrollAnimationParams {
oldState: ScrollState;
newState: ScrollState;
startTime: number;
duration: number;
}
\ No newline at end of file
......@@ -237,6 +237,20 @@ export class LayoutProvider extends Disposable implements IViewLayout {
}
public setScrollPosition(position: editorCommon.INewScrollPosition): void {
this._scrollable.updateState(position);
const state = this._scrollable.getSmoothScrollTargetState();
const xAbsChange = Math.abs(typeof position.scrollLeft !== 'undefined' ? position.scrollLeft - state.scrollLeft : 0);
const yAbsChange = Math.abs(typeof position.scrollTop !== 'undefined' ? position.scrollTop - state.scrollTop : 0);
if (xAbsChange || yAbsChange) {
// If position change is big enough, then appply smooth scrolling
if (xAbsChange > state.width / 10 ||
yAbsChange > state.height / 10) {
this._scrollable.updateState(position, 125);
}
// Otherwise update scroll position immediately
else {
this._scrollable.updateState(position);
}
}
}
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册