feat(gallery): smooth isolated wheel notches and widen mid-row date gap
Glide a lone mouse-wheel notch (one arriving more than 200 ms after the previous direct scroll) over a short decaying animation instead of jumping, and defer the edge-bounce check until that glide has settled. A mouse press or a new momentum scroll stops the glide. Triple DATE_SPACING (16 -> 48 px) so a date boundary inside a shared row reads as a clear seam; the date header columns derive their offsets from the same constant and stay aligned with the thumbnails below. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011xpLSeYKKHX6o16jzYLgZv
This commit is contained in:
@@ -58,16 +58,16 @@ public class GalleryTimelineBar extends Region {
|
||||
public record YearMark(double fraction, String label) {
|
||||
}
|
||||
|
||||
private static final double PAD = 12;
|
||||
private static final double VISUAL_WIDTH = 72;
|
||||
private static final double LINE_INSET = 10;
|
||||
private static final double DOT_RADIUS = 1.6;
|
||||
private static final double MARKER_WIDTH = 34;
|
||||
private static final double BADGE_PAD_X = 6;
|
||||
private static final double BADGE_PAD_Y = 3;
|
||||
private static final double BADGE_RADIUS = 14; // matches .gallery-timeline-pill's own -fx-background-radius
|
||||
private static final Duration FADE = Duration.millis(180);
|
||||
private static final Duration IDLE_DELAY = Duration.millis(1400);
|
||||
private static final double PAD = 12;
|
||||
private static final double VISUAL_WIDTH = 72;
|
||||
private static final double LINE_INSET = 10;
|
||||
private static final double DOT_RADIUS = 1.6;
|
||||
private static final double MARKER_WIDTH = 54;
|
||||
private static final double BADGE_PAD_X = 6;
|
||||
private static final double BADGE_PAD_Y = 3;
|
||||
private static final double BADGE_RADIUS = 14; // matches .gallery-timeline-pill's own -fx-background-radius
|
||||
private static final Duration FADE = Duration.millis(180);
|
||||
private static final Duration IDLE_DELAY = Duration.millis(1400);
|
||||
|
||||
private final Canvas canvas = new Canvas(VISUAL_WIDTH, 0);
|
||||
private final StackPane pill = new StackPane();
|
||||
@@ -84,32 +84,38 @@ public class GalleryTimelineBar extends Region {
|
||||
* {@code .module-selector .toggle-button:hover}, so the chip reads as "a subtle button", not a new colour
|
||||
* invented for this bar alone.
|
||||
*/
|
||||
private final Region accentProbe = new Region();
|
||||
private final Region yearBadgeProbe = new Region();
|
||||
private final Label yearTextProbe = new Label();
|
||||
private final Region accentProbe = new Region();
|
||||
private final Region yearBadgeProbe = new Region();
|
||||
private final Label yearTextProbe = new Label();
|
||||
|
||||
private final PauseTransition idleTimer = new PauseTransition(IDLE_DELAY);
|
||||
private final FadeTransition fadeIn = new FadeTransition(FADE);
|
||||
private final FadeTransition fadeOut = new FadeTransition(FADE);
|
||||
private final FadeTransition fadeIn = new FadeTransition(FADE);
|
||||
private final FadeTransition fadeOut = new FadeTransition(FADE);
|
||||
|
||||
private List<YearMark> yearMarks = List.of();
|
||||
private double[] density = new double[0];
|
||||
private boolean dragging;
|
||||
private boolean dragging;
|
||||
|
||||
/** The grid's actual scroll position — where the pill sits whenever {@link #hovering} is {@code false}. */
|
||||
private double currentFraction = 0;
|
||||
/**
|
||||
* The grid's actual scroll position — where the pill sits whenever {@link #hovering} is {@code false}.
|
||||
*/
|
||||
private double currentFraction = 0;
|
||||
private @Nullable String currentLabel;
|
||||
|
||||
/** Live preview while the pointer is over this bar — where the pill sits whenever {@link #hovering} is {@code true}. */
|
||||
private boolean hovering;
|
||||
private double previewFraction;
|
||||
private @Nullable String previewLabel;
|
||||
/**
|
||||
* Live preview while the pointer is over this bar — where the pill sits whenever {@link #hovering} is {@code true}.
|
||||
*/
|
||||
private boolean hovering;
|
||||
private double previewFraction;
|
||||
private @Nullable String previewLabel;
|
||||
|
||||
/** Snapshot of {@link #currentFraction} taken on {@link #beginHover}; {@code -1} while no tick is shown. */
|
||||
/**
|
||||
* Snapshot of {@link #currentFraction} taken on {@link #beginHover}; {@code -1} while no tick is shown.
|
||||
*/
|
||||
private double markerFraction = -1;
|
||||
|
||||
private @Nullable DoubleConsumer onSeek;
|
||||
private @Nullable DoubleFunction<String> onPreview;
|
||||
private @Nullable DoubleConsumer onSeek;
|
||||
private @Nullable DoubleFunction<String> onPreview;
|
||||
|
||||
public GalleryTimelineBar(double clickableWidth) {
|
||||
getStyleClass().add("gallery-timeline");
|
||||
@@ -193,7 +199,9 @@ public class GalleryTimelineBar extends Region {
|
||||
this.onSeek = onSeek;
|
||||
}
|
||||
|
||||
/** Resolves a hovered fraction to a preview label, without touching the grid's actual scroll position. */
|
||||
/**
|
||||
* Resolves a hovered fraction to a preview label, without touching the grid's actual scroll position.
|
||||
*/
|
||||
public void setOnPreview(DoubleFunction<String> onPreview) {
|
||||
this.onPreview = onPreview;
|
||||
}
|
||||
@@ -208,7 +216,9 @@ public class GalleryTimelineBar extends Region {
|
||||
redraw();
|
||||
}
|
||||
|
||||
/** Each value is one month-with-photos' fraction along the line, ascending. */
|
||||
/**
|
||||
* Each value is one month-with-photos' fraction along the line, ascending.
|
||||
*/
|
||||
public void setDensity(double[] fractions) {
|
||||
this.density = fractions;
|
||||
redraw();
|
||||
@@ -257,7 +267,9 @@ public class GalleryTimelineBar extends Region {
|
||||
idleTimer.playFromStart();
|
||||
}
|
||||
|
||||
/** Pointer entered (or a click/drag just released back into) the bar: snapshot home into the tick. */
|
||||
/**
|
||||
* Pointer entered (or a click/drag just released back into) the bar: snapshot home into the tick.
|
||||
*/
|
||||
private void beginHover() {
|
||||
activity();
|
||||
if (yearMarks.isEmpty()) {
|
||||
@@ -268,7 +280,9 @@ public class GalleryTimelineBar extends Region {
|
||||
redraw();
|
||||
}
|
||||
|
||||
/** Pointer left without ever pressing: the preview never happened, so the pill returns straight home. */
|
||||
/**
|
||||
* Pointer left without ever pressing: the preview never happened, so the pill returns straight home.
|
||||
*/
|
||||
private void endHoverReverting() {
|
||||
hovering = false;
|
||||
markerFraction = -1;
|
||||
@@ -276,7 +290,9 @@ public class GalleryTimelineBar extends Region {
|
||||
redraw();
|
||||
}
|
||||
|
||||
/** Moves the pill to wherever {@code event} is, previewing {@link #onPreview}'s label for that fraction. */
|
||||
/**
|
||||
* Moves the pill to wherever {@code event} is, previewing {@link #onPreview}'s label for that fraction.
|
||||
*/
|
||||
private void previewAt(MouseEvent event) {
|
||||
if (onPreview == null || yearMarks.isEmpty()) {
|
||||
return;
|
||||
@@ -332,9 +348,9 @@ public class GalleryTimelineBar extends Region {
|
||||
}
|
||||
|
||||
private void redraw() {
|
||||
double width = canvas.getWidth();
|
||||
double height = canvas.getHeight();
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
double width = canvas.getWidth();
|
||||
double height = canvas.getHeight();
|
||||
GraphicsContext gc = canvas.getGraphicsContext2D();
|
||||
gc.clearRect(0, 0, width, height);
|
||||
if (yearMarks.isEmpty() || width <= 0 || height <= 0) {
|
||||
return;
|
||||
@@ -374,7 +390,9 @@ public class GalleryTimelineBar extends Region {
|
||||
}
|
||||
}
|
||||
|
||||
/** Width/height of {@code text} rendered at the canvas's own default font, without adding it to any scene. */
|
||||
/**
|
||||
* Width/height of {@code text} rendered at the canvas's own default font, without adding it to any scene.
|
||||
*/
|
||||
private static Bounds measure(String text) {
|
||||
return new Text(text).getLayoutBounds();
|
||||
}
|
||||
@@ -390,13 +408,15 @@ public class GalleryTimelineBar extends Region {
|
||||
probe.applyCss();
|
||||
Background background = probe.getBackground();
|
||||
if (background != null && !background.getFills().isEmpty()
|
||||
&& background.getFills().getFirst().getFill() instanceof Color color) {
|
||||
&& background.getFills().getFirst().getFill() instanceof Color color) {
|
||||
return color;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/** Same idea as {@link #resolveBackgroundColor}, but for a {@link Label}'s text fill rather than a background. */
|
||||
/**
|
||||
* Same idea as {@link #resolveBackgroundColor}, but for a {@link Label}'s text fill rather than a background.
|
||||
*/
|
||||
private static Color resolveTextColor(Label probe, Color fallback) {
|
||||
probe.applyCss();
|
||||
return probe.getTextFill() instanceof Color color ? color : fallback;
|
||||
|
||||
@@ -116,7 +116,7 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
* bigger than {@link #SPACING} (currently zero) so a date boundary mid-row still reads as a seam rather
|
||||
* than just another card — and so the header item's own columns, above, stay aligned with it.
|
||||
*/
|
||||
static final double DATE_SPACING = 16;
|
||||
static final double DATE_SPACING = 48;
|
||||
|
||||
/**
|
||||
* Used only until {@code gallery.thumbnail-size} has ever been read; the preference itself defaults
|
||||
@@ -328,9 +328,12 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
*/
|
||||
private final ListChangeListener<MediaFile> onFilesChanged = change -> throttledRelayout();
|
||||
|
||||
public JustifiedGalleryPane(MediaFileService mediaFileService, TaskService taskService,
|
||||
MediaLibraryState state, AppPreferences preferences,
|
||||
I18nService i18n, ThumbnailImageCache imageCache) {
|
||||
public JustifiedGalleryPane(MediaFileService mediaFileService,
|
||||
TaskService taskService,
|
||||
MediaLibraryState state,
|
||||
AppPreferences preferences,
|
||||
I18nService i18n,
|
||||
ThumbnailImageCache imageCache) {
|
||||
this.mediaFileService = mediaFileService;
|
||||
this.taskService = taskService;
|
||||
this.state = state;
|
||||
@@ -392,7 +395,10 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
// A click or trackpad tap mid-glide is the same "stop it now" signal as touching the pad to scroll
|
||||
// again — a filter, not a handler, so it stops the glide even when the press lands on a thumbnail
|
||||
// card underneath rather than bare pane background.
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> stopMomentumScroll());
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, event -> {
|
||||
stopMomentumScroll();
|
||||
stopIsolatedNotchSmoothing();
|
||||
});
|
||||
// A trackpad pinch — its own gesture, distinct from a two-finger scroll (already covered by the
|
||||
// shortcut+scroll branch above regardless of whether it came from a wheel or a trackpad).
|
||||
addEventFilter(ZoomEvent.ZOOM_STARTED, event -> pinchGestureStartSize = currentThumbnailSize());
|
||||
@@ -810,6 +816,54 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
*/
|
||||
private boolean isSynthesizingMomentum;
|
||||
|
||||
/**
|
||||
* {@link #onScroll}'s own last genuine (non-synthetic) direct scroll event time, in nanoseconds — what
|
||||
* {@link #ISOLATED_SCROLL_GAP_NANOS} compares a new one's own gap against to tell a slow, deliberate
|
||||
* single wheel notch apart from a trackpad's own continuous stream (or a fast wheel spin), which arrive
|
||||
* far more frequently and are never touched here — see {@link #smoothIsolatedNotch}'s own javadoc for
|
||||
* why only the former needs smoothing at all. {@code -1} until the very first direct scroll this pane
|
||||
* ever sees, deliberately left un-smoothed regardless of how it's spaced from nothing.
|
||||
*/
|
||||
private long lastDirectScrollNanos = -1;
|
||||
|
||||
/**
|
||||
* How long a gap since {@link #lastDirectScrollNanos} must be before a new direct scroll event is
|
||||
* treated as an isolated, deliberately slow wheel notch rather than part of a continuous gesture — 200ms
|
||||
* comfortably separates the two: a trackpad actively scrolling, or a wheel spun at any real speed, sends
|
||||
* events far more often than that (well under 100ms apart), while the user's own "1-2 notches a second"
|
||||
* case this exists for lands at 500-1000ms apart, nowhere near the boundary either way.
|
||||
*/
|
||||
private static final long ISOLATED_SCROLL_GAP_NANOS = 200_000_000L;
|
||||
|
||||
/**
|
||||
* {@link #smoothIsolatedNotch}'s own in-flight glide, if any — the exact same "drive {@link #virtualFlow}
|
||||
* with a synthetic {@link ScrollEvent} every frame via {@link #fireMomentumTick}" shape
|
||||
* {@link #momentumTimer} already uses, just chasing one notch's own fixed delta down to zero over a
|
||||
* short, fixed span instead of a decaying release velocity with no fixed end. Replaced outright, never
|
||||
* merged into, by a later call — see that method's own javadoc for why overlap here is both rare (by
|
||||
* definition, an isolated notch's own gap already exceeds this animation's whole span) and harmless.
|
||||
*/
|
||||
private @Nullable AnimationTimer isolatedNotchTimer;
|
||||
|
||||
/**
|
||||
* {@link #isolatedNotchTimer}'s own decay rate per millisecond — chosen so a single notch's own glide
|
||||
* lasts almost as long as the gap between notches at the pace this whole mechanism exists for (the
|
||||
* user's own "1-2 notches a second", 500-1000ms apart): about 90% of a notch's delta is gone by 400ms.
|
||||
* A much shorter glide (180ms was tried first) left a real, visible dead stretch of zero movement
|
||||
* between one notch's glide finishing and the next one arriving — read as the scroll moving "step by
|
||||
* step" rather than smoothly, since nothing was actually moving in that gap. Stretching the glide to
|
||||
* nearly fill it instead is what a slow-scrolling trackpad gets "for free" from sending many small
|
||||
* deltas continuously for as long as the finger moves — a wheel only ever gives one isolated delta per
|
||||
* notch, so matching that same continuous feel means deliberately spreading each one out this far.
|
||||
*/
|
||||
private static final double ISOLATED_NOTCH_DECAY_PER_MILLIS = 0.9943;
|
||||
|
||||
/**
|
||||
* {@link #isolatedNotchTimer}'s own stop condition — once the notch's own remaining, not-yet-applied
|
||||
* delta decays below this many pixels, the rest is imperceptible.
|
||||
*/
|
||||
private static final double ISOLATED_NOTCH_MIN_DELTA = 0.05;
|
||||
|
||||
/**
|
||||
* A shortcut+scroll/pinch zoom's own live target, accumulated across ticks between commits — see
|
||||
* {@link #adjustThumbnailSize}. {@code -1} outside of an active gesture, so the next one starts fresh
|
||||
@@ -1570,8 +1624,13 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
* else is a direct, finger-driven scroll — {@link #recordScrollSample}d for {@link #currentFlingVelocity}
|
||||
* and, if a glide was already running, stopped ({@link #stopMomentumScroll}) since a new touch on the pad
|
||||
* is exactly the "stop it now" signal the user asked for. {@link #isSynthesizingMomentum} tells our own
|
||||
* synthetic ticks apart from a genuine {@code isInertia()} event from the device — both look the same to
|
||||
* {@link ScrollEvent}, but only the latter should ever be swallowed.
|
||||
* synthetic ticks (this glide's own, or {@link #isolatedNotchTimer}'s) apart from a genuine
|
||||
* {@code isInertia()} event from the device — both look the same to {@link ScrollEvent}, but only the
|
||||
* latter should ever be swallowed. A direct event spaced more than {@link #ISOLATED_SCROLL_GAP_NANOS}
|
||||
* from the last one — a single, deliberately slow wheel notch, never a trackpad's own continuous stream
|
||||
* or a wheel spun at any real speed, both of which arrive far more often than that — is additionally
|
||||
* consumed outright and handed to {@link #smoothIsolatedNotch} instead of {@link #virtualFlow}'s own
|
||||
* native handling, which would otherwise land it in one instant, jarring step.
|
||||
*/
|
||||
private void onScroll(ScrollEvent event) {
|
||||
// isShortcutDown(), not isControlDown(): Cmd on macOS, Ctrl everywhere else — the platform's own
|
||||
@@ -1585,12 +1644,74 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
event.consume();
|
||||
return;
|
||||
}
|
||||
boolean smoothed = false;
|
||||
if (!isSynthesizingMomentum) {
|
||||
stopMomentumScroll();
|
||||
long now = System.nanoTime();
|
||||
boolean isolatedNotch = lastDirectScrollNanos >= 0 && now - lastDirectScrollNanos > ISOLATED_SCROLL_GAP_NANOS;
|
||||
lastDirectScrollNanos = now;
|
||||
recordScrollSample(event.getDeltaY());
|
||||
if (isolatedNotch) {
|
||||
smoothIsolatedNotch(event.getDeltaY());
|
||||
event.consume();
|
||||
smoothed = true;
|
||||
}
|
||||
}
|
||||
onGridScrolled();
|
||||
maybeBounceOnScroll();
|
||||
maybeBounceOnScroll(smoothed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ticks {@code deltaY} down to zero over a short, fixed span — the exact same "drive {@link #virtualFlow}
|
||||
* with a synthetic {@link ScrollEvent} every frame via {@link #fireMomentumTick}" shape
|
||||
* {@link #startMomentumScroll} already uses, just decaying a single notch's own fixed delta on a short
|
||||
* timer rather than a released gesture's velocity with no fixed end.
|
||||
*/
|
||||
private void smoothIsolatedNotch(double deltaY) {
|
||||
if (isolatedNotchTimer != null) {
|
||||
isolatedNotchTimer.stop();
|
||||
}
|
||||
double[] remaining = { deltaY };
|
||||
long[] lastFrameNanos = { -1 };
|
||||
isolatedNotchTimer = new AnimationTimer() {
|
||||
@Override
|
||||
public void handle(long now) {
|
||||
if (lastFrameNanos[0] < 0) {
|
||||
lastFrameNanos[0] = now;
|
||||
return;
|
||||
}
|
||||
double dtMillis = (now - lastFrameNanos[0]) / 1_000_000.0;
|
||||
lastFrameNanos[0] = now;
|
||||
double before = remaining[0];
|
||||
remaining[0] *= Math.pow(ISOLATED_NOTCH_DECAY_PER_MILLIS, dtMillis);
|
||||
// No isAtScrollEdge() check here, unlike momentumTimer's own — that one only ever starts
|
||||
// once a gesture has already moved the scrollbar away from wherever it began, so bailing
|
||||
// early there is a pure performance saving, never a correctness requirement. This timer, by
|
||||
// contrast, can genuinely be the very first scroll input from sitting exactly at an edge —
|
||||
// bailing on isAtScrollEdge() here would then refuse to move even a notch trying to scroll
|
||||
// AWAY from that edge, since this check can't tell direction from position alone. Letting it
|
||||
// run its full short course regardless is harmless: fireMomentumTick's own native handling
|
||||
// already clamps at the edge on its own if the notch really has nowhere to go.
|
||||
if (Math.abs(remaining[0]) < ISOLATED_NOTCH_MIN_DELTA) {
|
||||
stop();
|
||||
isolatedNotchTimer = null;
|
||||
return;
|
||||
}
|
||||
fireMomentumTick(before - remaining[0]);
|
||||
}
|
||||
};
|
||||
isolatedNotchTimer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops {@link #isolatedNotchTimer} if one is running — a new touch on the pad/wheel, a real momentum
|
||||
* glide about to take over instead, or {@link #dispose}.
|
||||
*/
|
||||
private void stopIsolatedNotchSmoothing() {
|
||||
if (isolatedNotchTimer != null) {
|
||||
isolatedNotchTimer.stop();
|
||||
isolatedNotchTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1634,6 +1755,10 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
*/
|
||||
private void startMomentumScroll(double velocity) {
|
||||
stopMomentumScroll();
|
||||
// Whatever an isolated notch's own smoothing hasn't caught up with yet must not keep ticking
|
||||
// alongside the release glide about to take over — both fire fireMomentumTick every frame, and
|
||||
// running together would double up on movement.
|
||||
stopIsolatedNotchSmoothing();
|
||||
momentumVelocity = velocity;
|
||||
momentumLastFrameNanos = -1;
|
||||
momentumTimer = new AnimationTimer() {
|
||||
@@ -1707,15 +1832,40 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How long {@link #maybeBounceOnScroll} waits, when {@code smoothed}, before checking whether a scroll
|
||||
* attempt actually moved {@link #verticalScrollBar} — long enough for {@link #isolatedNotchTimer} to
|
||||
* have caught up with essentially all of its own notch (~99% gone by then, given
|
||||
* {@link #ISOLATED_NOTCH_DECAY_PER_MILLIS}'s own, deliberately long glide). A single
|
||||
* {@link Platform#runLater} pulse — still used as-is for a plain, un-smoothed scroll, see
|
||||
* {@code smoothed}'s own javadoc — would be nowhere near enough for that case: {@link #isolatedNotchTimer}'s
|
||||
* own first frame only primes its clock and moves nothing yet, so checking that soon would misread even
|
||||
* a notch nowhere near an edge as stuck.
|
||||
*/
|
||||
private static final Duration ISOLATED_NOTCH_BOUNCE_CHECK_DELAY = Duration.millis(800);
|
||||
|
||||
/**
|
||||
* {@link #maybeBounceOnScroll}'s own debounced "did it actually move" check for the {@code smoothed}
|
||||
* case — restarted, not stacked, on every call, so a burst of isolated notches sitting at an edge only
|
||||
* ever schedules the last one's check rather than piling up several redundant ones.
|
||||
*/
|
||||
private final PauseTransition isolatedNotchBounceCheck = new PauseTransition(ISOLATED_NOTCH_BOUNCE_CHECK_DELAY);
|
||||
|
||||
/**
|
||||
* A wheel scroll that lands exactly on {@link #verticalScrollBar}'s own min or max — nowhere further
|
||||
* to go — {@link #bounceGrid}s toward whichever end that is. Checked by comparing the value just
|
||||
* before the event to the value a pulse later rather than trusting {@link ScrollEvent#getDeltaY()}'s
|
||||
* before the event to the value some time later rather than trusting {@link ScrollEvent#getDeltaY()}'s
|
||||
* sign (a scroll that's still free to move away from an edge must never bounce, and the direction a
|
||||
* positive/negative delta actually scrolls is a convention this reads back from the scrollbar itself
|
||||
* instead of assuming).
|
||||
*
|
||||
* @param smoothed whether {@link #onScroll} just handed this same event to {@link #smoothIsolatedNotch}
|
||||
* instead of letting {@link #virtualFlow} apply it natively — that delays the actual
|
||||
* movement across several frames, so the "did it move" check below needs
|
||||
* {@link #ISOLATED_NOTCH_BOUNCE_CHECK_DELAY} instead of a single pulse to reliably tell
|
||||
* a genuine edge apart from a notch that just hasn't caught up yet.
|
||||
*/
|
||||
private void maybeBounceOnScroll() {
|
||||
private void maybeBounceOnScroll(boolean smoothed) {
|
||||
if (verticalScrollBar == null) {
|
||||
return;
|
||||
}
|
||||
@@ -1725,11 +1875,19 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
if (!atTop && !atBottom) {
|
||||
return;
|
||||
}
|
||||
Platform.runLater(() -> {
|
||||
Runnable check = () -> {
|
||||
if (verticalScrollBar != null && Math.abs(verticalScrollBar.getValue() - before) < SCROLLBAR_EDGE_EPSILON) {
|
||||
bounceGrid(atTop);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (smoothed) {
|
||||
isolatedNotchBounceCheck.setOnFinished(event -> check.run());
|
||||
isolatedNotchBounceCheck.stop();
|
||||
isolatedNotchBounceCheck.playFromStart();
|
||||
}
|
||||
else {
|
||||
Platform.runLater(check);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1866,6 +2024,8 @@ public class JustifiedGalleryPane extends StackPane implements Disposable {
|
||||
thumbnailZoomSettle.stop();
|
||||
scrollGestureIdle.stop();
|
||||
stopMomentumScroll();
|
||||
stopIsolatedNotchSmoothing();
|
||||
isolatedNotchBounceCheck.stop();
|
||||
if (gridBounceTransition != null) {
|
||||
gridBounceTransition.stop();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user