IDEA-95774 Optimise UI for touch

Touchscreen scrolling support with inertia

GitOrigin-RevId: 665ad282eebc78da500b45154af306ff278c3539
This commit is contained in:
Denis Konoplev
2019-11-20 14:11:39 +00:00
committed by intellij-monorepo-bot
parent d46973e188
commit b715da4f79
4 changed files with 184 additions and 36 deletions
@@ -6,6 +6,7 @@ import com.intellij.openapi.util.SystemInfo;
import com.intellij.openapi.wm.IdeGlassPane.TopComponent;
import com.intellij.ui.scale.JBUIScale;
import com.intellij.util.ui.RegionPainter;
import com.intellij.util.ui.TouchScrollUtil;
import com.intellij.util.ui.UIUtil;
import org.intellij.lang.annotations.JdkConstants;
import org.jetbrains.annotations.NotNull;
@@ -202,7 +203,7 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl
* @return {@code true} if the specified event is handled and consumed, {@code false} otherwise
*/
public boolean handleMouseWheelEvent(MouseWheelEvent event) {
if (MouseWheelEvent.WHEEL_UNIT_SCROLL != event.getScrollType()) return false;
if (!isSupportedScrollType(event)) return false;
if (event.isShiftDown() == (orientation == VERTICAL)) return false;
if (!ScrollSettings.isEligibleFor(this)) return false;
@@ -235,6 +236,10 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl
return true;
}
private static boolean isSupportedScrollType(MouseWheelEvent e) {
return e.getScrollType() == MouseWheelEvent.WHEEL_UNIT_SCROLL || TouchScrollUtil.isUpdate(e);
}
private JViewport getViewport() {
Component parent = getParent();
if (parent instanceof JScrollPane) {
@@ -284,6 +289,9 @@ public class JBScrollBar extends JScrollBar implements TopComponent, Interpolabl
* @return a scrolling delta for this scrollbar
*/
private double getPreciseDelta(MouseWheelEvent event) {
if (TouchScrollUtil.isTouchScroll(event)) {
return TouchScrollUtil.getDelta(event);
}
double rotation = event.getPreciseWheelRotation();
if (ScrollSettings.isPixelPerfectEnabled()) {
// calculate an absolute delta if possible
@@ -25,6 +25,8 @@ import static java.lang.Math.*;
public class MouseWheelSmoothScroll {
private final InertialAnimator horizontal = new InertialAnimator(), vertical = new InertialAnimator();
private final FlingDetector horizontalFling = new FlingDetector(), verticalFling = new FlingDetector();
private final EventCounter touchpadRecognizer = new EventCounter(100);
private final @NotNull Supplier<Boolean> myScrollEnabled;
public static MouseWheelSmoothScroll create() {
@@ -51,23 +53,63 @@ public class MouseWheelSmoothScroll {
return;
}
InertialAnimator animator = isHorizontalScroll(e) ? horizontal : vertical;
int delta = (int)round(getDelta(bar, e));
int value = bar.getValue();
int minimum = bar.getMinimum();
int maximum = bar.getMaximum();
if (abs(delta) > 0.01) { // ignore small delta event
animator.start(value, value + delta, bar::setValue, (v) -> {
return v - bar.getValue() != 0 || minimum != bar.getMinimum() || maximum != bar.getMaximum() || !bar.isShowing();
});
e.consume();
int delta = (int)round(getDelta(bar, e));
int targetValue = value + delta;
if (TouchScrollUtil.isUpdate(e)) {
bar.setValue(targetValue);
FlingDetector fling = isHorizontalScroll(e) ? horizontalFling : verticalFling;
fling.updateDelta(delta);
} else if (TouchScrollUtil.isEnd(e)) {
startFling(getEventVerticalScrollBar(e), verticalFling, vertical);
startFling(getEventHorizontalScrollBar(e), horizontalFling, horizontal);
} else if (abs(delta) != 0) { // ignore small delta event
InertialAnimator animator = isHorizontalScroll(e) ? horizontal : vertical;
touchpadRecognizer.addTime(System.currentTimeMillis());
if (touchpadRecognizer.getSize() >= getTouchpadThreshold()) {
bar.setValue(targetValue);
animator.stop();
} else {
animator.start(value, targetValue, new ScrollAnimationSettings(), bar::setValue, shouldStop(bar));
}
}
e.consume();
}
public void startFling(JScrollBar bar, FlingDetector fling, InertialAnimator animator) {
if (bar != null && fling.shouldStart()) {
animator.stop();
int value = bar.getValue();
int targetValue = fling.getTargetValue(value);
animator.start(value, targetValue, new FlingAnimationSettings(), bar::setValue, shouldStop(bar));
}
}
public static @Nullable JScrollBar getEventScrollBar(@NotNull MouseWheelEvent e) {
JScrollPane scroller = (JScrollPane) e.getComponent();
if (scroller == null) return null;
return isHorizontalScroll(e) ? scroller.getHorizontalScrollBar() : scroller.getVerticalScrollBar();
public static @Nullable
JScrollBar getEventScrollBar(@NotNull MouseWheelEvent e) {
return isHorizontalScroll(e) ? getEventHorizontalScrollBar(e) : getEventVerticalScrollBar(e);
}
public static @Nullable
JScrollBar getEventHorizontalScrollBar(@NotNull MouseWheelEvent e) {
JScrollPane scroller = (JScrollPane)e.getComponent();
return scroller == null ? null : scroller.getHorizontalScrollBar();
}
public static @Nullable
JScrollBar getEventVerticalScrollBar(@NotNull MouseWheelEvent e) {
JScrollPane scroller = (JScrollPane)e.getComponent();
return scroller == null ? null : scroller.getVerticalScrollBar();
}
private static Predicate<Integer> shouldStop(JScrollBar bar) {
int minimum = bar.getMinimum();
int maximum = bar.getMaximum();
return (v) -> {
return v - bar.getValue() != 0 || minimum != bar.getMinimum() || maximum != bar.getMaximum() || !bar.isShowing();
};
}
public static boolean isHorizontalScroll(@NotNull MouseWheelEvent e) {
@@ -78,7 +120,14 @@ public class MouseWheelSmoothScroll {
return Registry.intValue("idea.inertial.smooth.scrolling.unit.increment");
}
public int getTouchpadThreshold() {
return Registry.intValue("idea.inertial.smooth.scrolling.touchpad.threshold");
}
public static double getDelta(@NotNull JScrollBar bar, @NotNull MouseWheelEvent event) {
if (TouchScrollUtil.isTouchScroll(event)) {
return TouchScrollUtil.getDelta(event);
}
double rotation = event.getPreciseWheelRotation();
int direction = rotation < 0 ? -1 : 1;
// bar.getUnitIncrement can return -1 for top bound value. Fix it
@@ -87,7 +136,7 @@ public class MouseWheelSmoothScroll {
return unitIncrement * rotation * event.getScrollAmount();
}
private final static class InertialAnimator implements ActionListener {
private static class InertialAnimator implements ActionListener {
private final static int REFRESH_TIME = 1000 / 60; // 60 Hz
private final static double VELOCITY_THRESHOLD = 0.001;
@@ -100,31 +149,20 @@ public class MouseWheelSmoothScroll {
private @NotNull Consumer<Integer> myConsumer = BLACK_HOLE;
private final Predicate<Integer> FALSE_PREDICATE = (value) -> false;
private @NotNull Predicate<Integer> myShouldStop = FALSE_PREDICATE;
private AnimationSettings myAnimationSettings = null;
private final Timer myTimer = TimerUtil.createNamedTimer("Inertial Animation Timer", REFRESH_TIME, this);
private final EventCounter myTouchpadRecognizer = new EventCounter(100);
private InertialAnimator() {
myTimer.setInitialDelay(0);
}
public double getDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.duration")), 0);
}
public double getDecayDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.decay.duration")), 0);
}
public int getTouchpadThreshold() {
return Registry.intValue("idea.inertial.smooth.scrolling.touchpad.threshold");
}
public final void start(int initValue, int targetValue, @NotNull Consumer<Integer> consumer, @Nullable Predicate<Integer> shouldStop) {
long currentEventTime = System.currentTimeMillis();
myTouchpadRecognizer.addTime(currentEventTime);
double duration = getDuration();
if (duration == 0 || myTouchpadRecognizer.getSize() >= getTouchpadThreshold()) {
public final void start(int initValue, int targetValue,
@NotNull AnimationSettings animationSettings,
@NotNull Consumer<Integer> consumer,
@Nullable Predicate<Integer> shouldStop) {
double duration = animationSettings.getDuration();
if (duration == 0) {
consumer.accept(targetValue);
stop();
return;
@@ -137,11 +175,12 @@ public class MouseWheelSmoothScroll {
myTargetValue = targetValue;
}
myLastEventTime = currentEventTime - myTimer.getDelay();
myLastEventTime = System.currentTimeMillis() - myTimer.getDelay();
myVelocity = (myTargetValue - initValue) / duration;
myLambda = 1.0;
myConsumer = Objects.requireNonNull(consumer);
myShouldStop = shouldStop == null ? FALSE_PREDICATE : shouldStop;
myAnimationSettings = animationSettings;
myCurrentValue = initValue;
myTimer.start();
}
@@ -163,7 +202,7 @@ public class MouseWheelSmoothScroll {
// slowdown the animation
double animationTimeLeft = (myTargetValue - myCurrentValue) / myVelocity;
if (myLambda == 1.0 && animationTimeLeft > REFRESH_TIME && animationTimeLeft < getDecayDuration()) {
if (myLambda == 1.0 && animationTimeLeft > REFRESH_TIME && animationTimeLeft < myAnimationSettings.getDecayDuration()) {
// find q of geometric progression using n-th member formulae
myLambda = pow(abs(VELOCITY_THRESHOLD / myVelocity), 1.0 / (animationTimeLeft / REFRESH_TIME));
}
@@ -189,11 +228,63 @@ public class MouseWheelSmoothScroll {
}
}
private static class FlingDetector {
private int lastDelta = 0;
public void updateDelta(int delta) {
lastDelta = delta;
}
private static int getPixelThreshold() {
return Registry.intValue("idea.inertial.smooth.scrolling.touch.pixel.threshold");
}
private static int getFlingMultiplier() {
return Registry.intValue("idea.inertial.smooth.scrolling.touch.multiplier");
}
public boolean shouldStart() {
return abs(lastDelta) >= getPixelThreshold();
}
public int getTargetValue(int initValue) {
return initValue + lastDelta * getFlingMultiplier();
}
}
private interface AnimationSettings {
double getDuration();
double getDecayDuration();
}
private static class ScrollAnimationSettings implements AnimationSettings {
@Override
public double getDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.duration")), 0);
}
@Override
public double getDecayDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.decay.duration")), 0);
}
}
private static class FlingAnimationSettings implements AnimationSettings {
@Override
public double getDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.touch.duration")), 0);
}
@Override
public double getDecayDuration() {
return max(abs(Registry.doubleValue("idea.inertial.smooth.scrolling.touch.decay")), 0);
}
}
private final static class EventCounter {
private final List<Long> myValues = new LinkedList<>();
private final long myDuration;
private EventCounter(long duration) {
myDuration = max(duration, 1);
}
@@ -0,0 +1,34 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.util.ui;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.awt.event.MouseWheelEvent;
@ApiStatus.Experimental
public class TouchScrollUtil {
private static final int TOUCH_BEGIN = 2;
private static final int TOUCH_UPDATE = 3;
private static final int TOUCH_END = 4;
public static boolean isTouchScroll(@NotNull MouseWheelEvent e) {
return e.getScrollType() >= TOUCH_BEGIN && e.getScrollType() <= TOUCH_END;
}
public static double getDelta(@NotNull MouseWheelEvent e) {
return e.getPreciseWheelRotation() * e.getScrollAmount();
}
public static boolean isBegin(@NotNull MouseWheelEvent e) {
return e.getScrollType() == TOUCH_BEGIN;
}
public static boolean isUpdate(@NotNull MouseWheelEvent e) {
return e.getScrollType() == TOUCH_UPDATE;
}
public static boolean isEnd(@NotNull MouseWheelEvent e) {
return e.getScrollType() == TOUCH_END;
}
}
@@ -1066,6 +1066,21 @@ idea.inertial.smooth.scrolling.touchpad.threshold=10
idea.inertial.smooth.scrolling.touchpad.threshold.description=If number of events during last 100 ms is greater than this count, \
then no animation is used
idea.inertial.smooth.scrolling.touch.duration=1500
idea.inertial.smooth.scrolling.touch.duration.description=Base duration (ms) of touch inertial scroll animation. \
See full description in idea.inertial.smooth.scrolling.duration
idea.inertial.smooth.scrolling.touch.decay=1200
idea.inertial.smooth.scrolling.touch.decay.description=Works as decay in smooth scrolling, \
See full description in idea.inertial.smooth.scrolling.decay.duration
idea.inertial.smooth.scrolling.touch.pixel.threshold=3
idea.inertial.smooth.scrolling.touch.pixel.threshold.description=Threshold to decide, should we start \
inertial scroll animation
idea.inertial.smooth.scrolling.touch.multiplier=100
idea.inertial.smooth.scrolling.touch.multiplier.description=Multiplier to increase fling scroll amount
decompiler.use.line.mapping=true
decompiler.use.line.mapping.description=Maps original to decompiled line numbers when stepping in debugger.
decompiler.dump.original.lines=false