rotation edge case
This commit is contained in:
@@ -32,6 +32,16 @@ App.feed = App.feed || {};
|
|||||||
let scrollBound = false;
|
let scrollBound = false;
|
||||||
let scrollRaf = null;
|
let scrollRaf = null;
|
||||||
|
|
||||||
|
// The slide height under which the current scroll position / top-spacer were
|
||||||
|
// last laid out. On an orientation change the viewport (and therefore the
|
||||||
|
// slide height) changes, which makes the browser re-snap the scroll-snap
|
||||||
|
// container and fire scroll events with positions that no longer map to the
|
||||||
|
// active slide. We compare against this so onScroll can ignore those
|
||||||
|
// resize-induced scroll events and let onResize realign to the active video
|
||||||
|
// instead -- the active slide must never change just because the device was
|
||||||
|
// rotated. It's updated only when we (re)align the scroll position ourselves.
|
||||||
|
let lastSlideHeight = 0;
|
||||||
|
|
||||||
// HUD auto-hide: the reels HUD fades out after this much inactivity and
|
// HUD auto-hide: the reels HUD fades out after this much inactivity and
|
||||||
// reappears on any pointer movement / tap / scroll. Buttons keep their
|
// reappears on any pointer movement / tap / scroll. Buttons keep their
|
||||||
// pointer-events while hidden, so they stay clickable even when invisible.
|
// pointer-events while hidden, so they stay clickable even when invisible.
|
||||||
@@ -385,6 +395,8 @@ App.feed = App.feed || {};
|
|||||||
const clamped = clampIndex(index);
|
const clamped = clampIndex(index);
|
||||||
if (clamped < 0) return;
|
if (clamped < 0) return;
|
||||||
state.feedActiveIndex = clamped;
|
state.feedActiveIndex = clamped;
|
||||||
|
const activeVideo = (state.loadedVideos || [])[clamped];
|
||||||
|
state.feedActiveVideoId = activeVideo ? activeVideo.id : null;
|
||||||
|
|
||||||
syncWindow(clamped);
|
syncWindow(clamped);
|
||||||
|
|
||||||
@@ -421,7 +433,14 @@ App.feed = App.feed || {};
|
|||||||
scrollRaf = null;
|
scrollRaf = null;
|
||||||
const scroller = getScroller();
|
const scroller = getScroller();
|
||||||
if (!scroller) return;
|
if (!scroller) return;
|
||||||
const index = clampIndex(Math.round(scroller.scrollTop / slideHeight()));
|
const h = slideHeight();
|
||||||
|
// A scroll that fires while the slide height differs from what the
|
||||||
|
// current layout was aligned to is a side effect of a viewport
|
||||||
|
// change (e.g. an orientation switch re-snapping the container), not
|
||||||
|
// a real user swipe. Ignore it and let onResize re-anchor to the
|
||||||
|
// active video, so rotating the device never jumps to another slide.
|
||||||
|
if (h !== lastSlideHeight) return;
|
||||||
|
const index = clampIndex(Math.round(scroller.scrollTop / h));
|
||||||
if (index < 0) return;
|
if (index < 0) return;
|
||||||
if (index !== state.feedActiveIndex) {
|
if (index !== state.feedActiveIndex) {
|
||||||
setActive(index);
|
setActive(index);
|
||||||
@@ -429,18 +448,42 @@ App.feed = App.feed || {};
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResize = function() {
|
// Re-anchors the scroll position on the currently active video after the
|
||||||
if (!state.feedOpen || state.feedActiveIndex < 0) return;
|
// viewport changes. The active slide is resolved by id (not by a possibly
|
||||||
|
// stale scroll position) so an orientation change always keeps the same
|
||||||
|
// video playing/focused rather than snapping to a neighbour.
|
||||||
|
const realignToActive = function() {
|
||||||
|
const total = (state.loadedVideos || []).length;
|
||||||
|
if (total === 0) return;
|
||||||
|
let index = state.feedActiveIndex;
|
||||||
|
if (state.feedActiveVideoId != null) {
|
||||||
|
const found = (state.loadedVideos || [])
|
||||||
|
.findIndex((v) => String(v.id) === String(state.feedActiveVideoId));
|
||||||
|
if (found >= 0) index = found;
|
||||||
|
}
|
||||||
|
index = clampIndex(index);
|
||||||
|
if (index < 0) return;
|
||||||
|
state.feedActiveIndex = index;
|
||||||
const h = slideHeight();
|
const h = slideHeight();
|
||||||
const start = Math.max(0, state.feedActiveIndex - HISTORY_COUNT);
|
const start = Math.max(0, index - HISTORY_COUNT);
|
||||||
const spacer = getTopSpacer();
|
const spacer = getTopSpacer();
|
||||||
if (spacer) spacer.style.height = `${start * h}px`;
|
if (spacer) spacer.style.height = `${start * h}px`;
|
||||||
const scroller = getScroller();
|
const scroller = getScroller();
|
||||||
if (scroller) scroller.scrollTop = state.feedActiveIndex * h;
|
if (scroller) scroller.scrollTop = index * h;
|
||||||
const activeSlide = slidesByIndex.get(state.feedActiveIndex);
|
lastSlideHeight = h;
|
||||||
|
const activeSlide = slidesByIndex.get(index);
|
||||||
if (activeSlide) measureFeedTitle(activeSlide);
|
if (activeSlide) measureFeedTitle(activeSlide);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onResize = function() {
|
||||||
|
if (!state.feedOpen || state.feedActiveIndex < 0) return;
|
||||||
|
realignToActive();
|
||||||
|
// Orientation changes can settle over more than one frame (the visual
|
||||||
|
// viewport and the scroll-snap re-anchor in stages); realign again once
|
||||||
|
// layout has settled so the active video stays put either way.
|
||||||
|
requestAnimationFrame(realignToActive);
|
||||||
|
};
|
||||||
|
|
||||||
App.feed.isOpen = function() {
|
App.feed.isOpen = function() {
|
||||||
return !!state.feedOpen;
|
return !!state.feedOpen;
|
||||||
};
|
};
|
||||||
@@ -461,6 +504,8 @@ App.feed = App.feed || {};
|
|||||||
slidesByIndex.clear();
|
slidesByIndex.clear();
|
||||||
resumeTimes.clear();
|
resumeTimes.clear();
|
||||||
state.feedActiveIndex = -1;
|
state.feedActiveIndex = -1;
|
||||||
|
state.feedActiveVideoId = null;
|
||||||
|
lastSlideHeight = 0;
|
||||||
const spacer = getTopSpacer();
|
const spacer = getTopSpacer();
|
||||||
if (spacer) spacer.style.height = '0px';
|
if (spacer) spacer.style.height = '0px';
|
||||||
const scroller = getScroller();
|
const scroller = getScroller();
|
||||||
@@ -506,7 +551,8 @@ App.feed = App.feed || {};
|
|||||||
// Force a fresh activation even if the index happens to match.
|
// Force a fresh activation even if the index happens to match.
|
||||||
state.feedActiveIndex = -1;
|
state.feedActiveIndex = -1;
|
||||||
setActive(startIndex);
|
setActive(startIndex);
|
||||||
scroller.scrollTop = startIndex * slideHeight();
|
lastSlideHeight = slideHeight();
|
||||||
|
scroller.scrollTop = startIndex * lastSlideHeight;
|
||||||
|
|
||||||
App.feed.updateToggleButton();
|
App.feed.updateToggleButton();
|
||||||
App.feed.updateMuteButton();
|
App.feed.updateMuteButton();
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ App.state = {
|
|||||||
feedOpen: false,
|
feedOpen: false,
|
||||||
feedMuted: true,
|
feedMuted: true,
|
||||||
feedActiveIndex: -1,
|
feedActiveIndex: -1,
|
||||||
|
feedActiveVideoId: null,
|
||||||
groupCursors: null
|
groupCursors: null
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -749,15 +749,58 @@ App.videos = App.videos || {};
|
|||||||
update();
|
update();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Records the topmost card crossing (or just below) the viewport top,
|
||||||
|
// plus how far its top sits from the viewport top. A column re-pack (on
|
||||||
|
// resize / orientation change) reassigns every card's position, so the
|
||||||
|
// raw scrollTop would otherwise point at a different video afterwards.
|
||||||
|
// Anchoring on the topmost visible card (rather than the centred one)
|
||||||
|
// is independent of the viewport height, which has *already* changed by
|
||||||
|
// the time a resize/orientation event fires -- so it stays correct even
|
||||||
|
// as portrait<->landscape swaps the height out from under us.
|
||||||
|
const captureAnchor = function() {
|
||||||
|
let anchor = null;
|
||||||
|
let bestTop = Infinity;
|
||||||
|
mounted.forEach((card, i) => {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
if (rect.bottom <= 0) return; // fully scrolled past
|
||||||
|
if (rect.top < bestTop) {
|
||||||
|
bestTop = rect.top;
|
||||||
|
anchor = { index: i, offsetTop: rect.top };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return anchor;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Scrolls so the anchored card sits at the same viewport offset it had
|
||||||
|
// before the re-pack, keeping the user's place across the layout change.
|
||||||
|
const restoreAnchor = function(anchor) {
|
||||||
|
if (!anchor) return;
|
||||||
|
const el = grid();
|
||||||
|
const l = layout[anchor.index];
|
||||||
|
if (!el || !l) return;
|
||||||
|
const gridTopDoc = el.getBoundingClientRect().top + window.scrollY;
|
||||||
|
const target = gridTopDoc + l.top - anchor.offsetTop;
|
||||||
|
window.scrollTo(0, Math.max(0, target));
|
||||||
|
};
|
||||||
|
|
||||||
let resizeRaf = null;
|
let resizeRaf = null;
|
||||||
|
let pendingAnchor = null;
|
||||||
const ensureInit = function() {
|
const ensureInit = function() {
|
||||||
if (!cols) measureMetrics();
|
if (!cols) measureMetrics();
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
initialized = true;
|
initialized = true;
|
||||||
window.addEventListener('scroll', scheduleUpdate, { passive: true });
|
window.addEventListener('scroll', scheduleUpdate, { passive: true });
|
||||||
window.addEventListener('resize', () => {
|
window.addEventListener('resize', () => {
|
||||||
|
// Capture before the re-pack (positions are still the old ones)
|
||||||
|
// and keep the earliest anchor across a burst of resize events.
|
||||||
|
if (!pendingAnchor) pendingAnchor = captureAnchor();
|
||||||
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
if (resizeRaf) cancelAnimationFrame(resizeRaf);
|
||||||
resizeRaf = requestAnimationFrame(relayout);
|
resizeRaf = requestAnimationFrame(() => {
|
||||||
|
resizeRaf = null;
|
||||||
|
relayout();
|
||||||
|
restoreAnchor(pendingAnchor);
|
||||||
|
pendingAnchor = null;
|
||||||
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user