chore(openspec): archive add-mobile-photo-zoom and sync main specs
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
См. `proposal.md` — Why. Текущее устройство просмотра:
|
||||
|
||||
- `ViewerScreen.kt`: `ViewerContent` держит `HorizontalPager`; `pagerScrollEnabled =
|
||||
current?.isPanorama != true`. `MediaPage` разветвляет видео / панораму / изображение.
|
||||
- Изображение рисуется `GaloAsyncImage(... ContentScale.Fit)` — статично, без жестов.
|
||||
- Панорама уже имеет pinch-зум через `PanoramaView` + `PanoramaCamera`.
|
||||
- Веб-просмотрщик (`frontend/.../Viewer.svelte`) задаёт семантику зума: scale —
|
||||
множитель к натуральному размеру, предел 200% (`MAX_ZOOM = 2`), минимум — fit
|
||||
(`fitScale`), при зуме догружается `original_url`, зум сбрасывается при смене фото.
|
||||
Тач-жестов в вебе нет — жесты проектируем заново.
|
||||
- Прецедент ручных жестов: `AvatarCropDialog.kt` (`pointerInput { detectDragGestures }`).
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- Дать зум обычных изображений в Android-просмотрщике с веб-паритетом по семантике
|
||||
(предел от натурального размера, догрузка оригинала).
|
||||
- Сохранить корректную навигацию по альбому (свайп) вне зума.
|
||||
|
||||
**Non-Goals:**
|
||||
- Зум видео (исключён требованием просмотра медиа).
|
||||
- Изменения панорамы — её зум остаётся как есть.
|
||||
- Изменения веб-клиента и сервера.
|
||||
|
||||
## Decisions
|
||||
|
||||
### Ручные жесты вместо библиотеки
|
||||
Зум реализуется composable-обёрткой над изображением с `Modifier.pointerInput` +
|
||||
`graphicsLayer` (scale/translation), без сторонних зависимостей. Альтернатива —
|
||||
библиотека зума; отклонено: в проекте уже принят ручной подход (`AvatarCropDialog`),
|
||||
а новых зависимостей избегаем.
|
||||
|
||||
### Масштаб как множитель к натуральному размеру (вариант A)
|
||||
Храним `scale` относительно натурального размера, как в вебе:
|
||||
`fitScale = min(viewW / natW, viewH / natH, 1)`, допустимый диапазон
|
||||
`[fitScale, 2.0]` (200% натурального). Натуральные размеры берём из `Photo.width/height`
|
||||
(уже есть в модели), при отсутствии — из измеренного `intrinsicSize` изображения.
|
||||
Альтернатива (множитель от fit) отклонена в пользу веб-паритета.
|
||||
|
||||
### Двойной тап -> 100% натурального с якорем в точку тапа
|
||||
Двойной тап переключает fit <-> 100% натурального размера, центрируя на точке нажатия.
|
||||
Это тач-замена веб-`Space`, но с якорем в палец вместо центра.
|
||||
|
||||
### Состояние зума живёт в `ViewerContent`
|
||||
`zoomed` поднимается в `ViewerContent`, а zoomable-обёртка сообщает его вверх через
|
||||
колбэк. `pagerScrollEnabled = current?.isPanorama != true && !zoomed` — по той же логике,
|
||||
что уже применяется к панораме. Альтернатива (держать состояние внутри `MediaPage`) не
|
||||
даёт управлять pager'ом.
|
||||
|
||||
### Догрузка оригинала через Coil по стабильному ключу
|
||||
При входе в зум источник изображения переключается с `previewUrl` на `originalUrl`;
|
||||
Coil кэширует по устойчивому ключу объекта (`ObjectCacheKey`), поэтому подмена не ломает
|
||||
кэш. Пока оригинал грузится, на экране остаётся превью: запрос оригинала получает
|
||||
`placeholderMemoryCacheKey` превью, чтобы вход в зум не мигал пустым (чёрным) фоном.
|
||||
Сбрасывается при смене фото.
|
||||
|
||||
### Сброс при смене фото
|
||||
Состояние зума живёт на страницу pager (ключ — id фото); при переходе на другой элемент
|
||||
оно пересоздаётся во вписанном виде, что соответствует требованию сброса.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [Свайп pager перехватывает горизонтальный жест увеличенного фото] -> `pagerScrollEnabled`
|
||||
выключается, пока `zoomed`.
|
||||
- [`Photo.width/height` может быть 0 до извлечения EXIF] -> fallback на `intrinsicSize`
|
||||
загруженного изображения; при 0/0 зум временно недоступен.
|
||||
- [Догрузка оригинала расходует трафик при каждом входе в зум] -> загрузка только по факту
|
||||
зума и с дисковым кэшем Coil.
|
||||
- [Точность якоря при двойном тапе и pinch] -> покрываем юнит-тестом на пересчёт
|
||||
scale/offset.
|
||||
Reference in New Issue
Block a user