Реалізація Live Cursors в мобільному додатку
Live cursors — один з тих елементів UX, які виглядають просто, а реалізація вимагає балансу між плавністю анімації, реальним трафіком та коректним масштабуванням при десятках одночасних користувачів.
На вебе це вирішується через CSS-анімацію та transform: translate(). На мобілі — нативні Animated API або react-native-reanimated, UIView animation або Flutter AnimatedWidget. Мережевий рівень — Y.js Awareness або кастомний WebSocket-протокол.
Протокол: Awareness vs кастомний канал
Y.js Awareness Protocol — правильний вибір, якщо у додатку вже є Y.js для синхронізації контенту. Awareness зберігає ephemeral-стейти: не персистуються, не входять у історію змін, автоматично видаляються при відключенні користувача.
// Оновлюємо позицію свого курсора
provider.awareness.setLocalStateField('cursor', {
x: normalizedX, // у координатах документа, не екрана
y: normalizedY,
timestamp: Date.now()
});
// Підписуємося на зміни чужих курсорів
provider.awareness.on('change', ({ updated }) => {
updated.forEach(clientId => {
if (clientId === provider.awareness.clientID) return;
const state = provider.awareness.getStates().get(clientId);
if (state?.cursor) {
updateRemoteCursor(clientId, state.cursor);
}
});
});
Якщо Y.js не використовується — кастомний WebSocket-канал з throttle 30ms (≈33fps) достатньо для плавного ощущення. Більш частих обновлень не дають помітного покращення UX, але збільшують трафік.
Нормалізація координат
Критичний момент: координати курсора потрібно передавати у системі координат документа, не екрана. У різних користувачів різні zoom-рівні, розміри екрану, положення scroll. Якщо передавати screen coordinates — курсори будуть прискакувати по екрану замість плавного слідування за реальною позицією.
Для scrollable документа: cursorX = (screenX + scrollX) / scale, cursorY = (screenY + scrollY) / scale. При отриманні назад: displayX = documentX * scale - scrollX. При смені zoom у отримувача — позиція курсора автоматично пересчитується.
Інтерполяція: плавне руху з затримкою мережі
Raw-позиції від сервера — ступенчасті, особливо при затримці 100–200ms. Потрібна інтерполяція.
У React Native з react-native-reanimated:
const remoteCursorX = useSharedValue(0);
const remoteCursorY = useSharedValue(0);
// При отриманні нової позиції від сервера
const updateCursor = (x, y) => {
remoteCursorX.value = withSpring(x, { damping: 20, stiffness: 300 });
remoteCursorY.value = withSpring(y, { damping: 20, stiffness: 300 });
};
const animStyle = useAnimatedStyle(() => ({
transform: [
{ translateX: remoteCursorX.value },
{ translateY: remoteCursorY.value },
]
}));
withSpring додає пружинну інтерполяцію — курсор «догоняє» реальну позицію плавно. Альтернатива: withTiming з duration: 80 — простіше, менш «живе».
На Flutter: AnimationController + Tween<Offset> з CurvedAnimation(curve: Curves.easeOut).
На нативному iOS: UIViewPropertyAnimator з .interruptible option — дозволяє перривати та перезапускати анімацію при нових позиціях без артефактів.
Масштабування: 50+ користувачів
При великій кількості користувачів кілька проблем:
Трафік. N користувачів × 33fps × ~50 байтів = при 50 користувачах ~82 KB/s тільки на cursor updates. Рішення: server-side throttling (сервер не пересилає updates частіше ніж раз у 50ms на клієнта) + вимкнення курсорів для користувачів за межами viewport.
Рендеринг. 50 анімованих вьюшок одночасно на мобілі — навантаження. Використовуємо Canvas-based рендеринг замість окремих View для кожного курсора. Рисуємо всі курсори в одному CustomPainter / SKCanvas / Canvas за один прохід.
Ідентифікація. При 50 користувачах імя під курсором нечитаємо. Показуємо імя тільки при hover/tap на курсор, решту часу — тільки кольорова точка з аватаром.
Відображення імені та аватара
Імя користувача рядом з курсором — класичний UX. Реалізація: floating label, яка слідує за курсором з невеликим offset. Проблема: при русі до краю екрана label виходить за bounds. Потрібен clamp — якщо курсор ближче ніж X px до правого краю, label відображається ліворуч від курсора.
Аватар замість стандартного указівника — часто краще, ніж кольорова стрілка. Круглое зображення 24px діаметром, кешировано в пам'яті.
Орієнтири за часом
Live cursors як окремий компонент (без повної collaborative системи) — 1–2 тижні на платформу. У контексті повноцінного collaborative додатка — одна з перших фіч, яку реалізуємо після базової синхронізації документа.







