diff --git a/resources/views/docs/mobile/4/edge-components/pager.md b/resources/views/docs/mobile/4/edge-components/pager.md new file mode 100644 index 000000000..348093f5e --- /dev/null +++ b/resources/views/docs/mobile/4/edge-components/pager.md @@ -0,0 +1,260 @@ +--- +title: Pager +order: 312 +--- + +## Overview + +A full-screen snap pager, the kind TikTok, Reels and Shorts are built on. Each page is the size of the pager's own +frame and a swipe settles on exactly one of them. Paging is vertical by default. Pass `horizontal` for a +stories-style strip. + +`` is a paired tag and its children are the pages. For a fixed set of pages that is all you need: + +@verbatim +```blade + + @foreach ($slides as $slide) + + {{ $slide->title }} + + @endforeach + +``` +@endverbatim + +A feed is different. It has no total, and you do not want every page in the tree. Ship a window of pages and tell +the pager where that window sits. See [Windowed feeds](#windowed-feeds). + +## Props + +- `count` - Items loaded so far, which is how many logical pages native lays out (optional, int, default: the + number of inline children). This is not a total; a feed has none. Grow `count` as batches arrive and the pager + grows in place. +- `page` - Page the pager should show (optional, int, default: `0`). See [Pager position](#pager-position). +- `from` - Absolute index of the first page emitted as a child (optional, int, default: `0`). +- `to` - Absolute index of the last page emitted, inclusive (optional, int). Native lays the children out from `from` + upwards, so `from` is the value that has to agree with your loop. `to` is there so the pair reads as one range. +- `has-more` - Append a loading page after the last loaded item (optional, boolean, default: `false`). The user can + pull into it while the next batch is fetched, instead of hitting a wall at the end of the window. Settling on it + reports `index === count`. +- `placeholders` - One image URL per loaded page index, `''` for no image (optional, array). Shown for loaded pages + this render has not shipped. See [Placeholders](#placeholders). +- `horizontal` - Page sideways instead of vertically (optional, boolean, default: `false`). +- `on-page-change` - Component method called with `(int $index)` as the current page changes. +- `a11y-label` - Accessibility label (optional) +- `a11y-hint` - Accessibility hint (optional) [Android] + +## Every page needs a `native:key` + +The root element of every page must carry `native:key`, set to an id that does not depend on the page's slot in the +shipped window: + +@verbatim +```blade + +``` +@endverbatim + +The window slides by one on every swipe. Without a key, each page picks up a new positional node id on every render, +so native rebuilds the page's whole subtree as soon as the window shifts. A video on that page then restarts from +zero, one round trip after the swipe settled. See [Subtree Reuse](../architecture/subtree-reuse#helping-the-diff-keys) +for how keys give a subtree its identity. + + + +## Pager position + +Native owns the scroll position and keeps it across re-renders. A render that lands mid-swipe never drags the pager +back. The `page` prop only moves the pager when PHP sends an index native never reported. That is how you jump +programmatically, and an echo of native's own report is ignored. + +`on-page-change` fires as soon as a page becomes the nearest one during a swipe, not when the scroll comes to rest, +so PHP's round trip overlaps the swipe animation. The handler takes a single int, the absolute page index: + +```php +public function onPagerPage(int $index): void +{ + $this->setPagerPage($index); +} +``` + +## Placeholders + +`placeholders` is an image URL per loaded page index, for every loaded item and not just the shipped window. A page +PHP has not shipped yet shows its image, so flicking faster than the round trip lands on a still instead of a blank. +Twenty URLs is a trivial payload, and the images are fetched and cached natively. + +Pages past `count` (the `has-more` tail) show a loading indicator instead of a placeholder. A loaded page with no +placeholder URL is transparent, so the pager's own `bg-*` shows through. + +## Windowed feeds + +The `HasPagerWindow` trait holds the windowing state and leaves you the fetch. Native reports each page change, your +handler records it, and the next render emits the pages around it. + +```php +use App\Services\Feed; +use Illuminate\View\View; +use Native\Mobile\Edge\NativeComponent; +use Native\Mobile\UI\Concerns\HasPagerWindow; + +class ReelFeed extends NativeComponent +{ + use HasPagerWindow; + + /** Clips loaded so far, in feed order. */ + public array $items = []; + + /** Opaque cursor from your API. */ + public ?string $cursor = null; + + public function mount(): void + { + $this->loadMore(); + } + + public function onPagerPage(int $index): void + { + $this->setPagerPage($index); + + if ($this->pagerNeedsMore()) { + $this->loadMore(); + } + } + + private function loadMore(): void + { + $batch = Feed::clips(cursor: $this->cursor); + + $this->items = [...$this->items, ...$batch->clips]; + $this->cursor = $batch->nextCursor; + $this->extendPager(count($batch->clips), hasMore: $batch->nextCursor !== null); + } + + public function render(): View + { + return view('native.pager-feed', [ + 'items' => $this->items, + 'placeholders' => array_map(fn ($clip) => $clip['poster'] ?? '', $this->items), + 'loaded' => $this->pagerLoaded, + 'hasMore' => $this->pagerHasMore, + 'page' => $this->pagerPage, + 'from' => $this->pagerWindowFrom(), + 'to' => $this->pagerWindowTo(), + ]); + } +} +``` + +The view emits only the window, so the loop runs over indexes rather than the collection: + +@verbatim +```blade + + @for ($index = $from; $index <= $to; $index++) + @include('native.pager-feed-page', ['index' => $index]) + @endfor + +``` +@endverbatim + +And the page itself, keyed by its absolute index: + +@verbatim +```blade static +{{-- resources/views/native/pager-feed-page.blade.php --}} +@php $clip = $items[$index]; @endphp + + + + + + {{ $clip['handle'] }} + {{ $clip['caption'] }} + + +``` +@endverbatim + +### `HasPagerWindow` state + +- `$pagerPage` - Absolute index of the page currently on screen (int, default: `0`) +- `$pagerWindow` - Pages shipped either side of the current one (int, default: `2`). Two keeps a fast second swipe on + device: the page after next is already there while the round trip for the settle is still in flight. Each shipped + page is rendered Blade plus a buffering video, so keep it small. +- `$pagerLoaded` - Items fetched so far, which is the pager's `count` (int, default: `0`) +- `$pagerHasMore` - Whether another batch can be fetched, which drives the trailing loading page (bool, default: + `true`) + +### `HasPagerWindow` methods + +- `setPagerPage(int $index)` - Record the page native reported +- `extendPager(int $added, bool $hasMore = true)` - Record a fetched batch: grows the pager and updates the tail state +- `pagerNeedsMore(int $threshold = 3)` - True when the current page is within `$threshold` items of the end of what is + loaded, or on the loading page itself, and more can be fetched. Three is the usual feed default: the PHP round trip + plus the API call has to land before the user swipes there. +- `pagerWindowFrom()` - First page index to emit +- `pagerWindowTo()` - Last page index to emit, inclusive, clamped to what is loaded + +## Pairing with video + +A [``](../plugins/core/media-player) on a pager page needs no coordination with the pager. With +`autoplay` the surface plays while it is at least 45% on screen and pauses below that. Only the page you are on +plays; the neighbours PHP shipped sit loaded and silent. Nothing in the pager renderer knows about playback. + +Give each page `:controls="false"` for a bare surface you can overlay your own EDGE elements on, and a `poster` so a +page shows a still from its first frame instead of black. The playing surface is the one the `MediaPlayer` facade +drives, so `MediaPlayer::pause()` from a tap handler pauses the page on screen without passing a page id. + +## Element + +```php +use Native\Mobile\UI\Elements\Pager; + +Pager::make($page1, $page2, $page3) + ->count($loaded) + ->page($current) + ->hasMore() + ->placeholders($posters) + ->onPageChange('onPagerPage'); +``` + +- `make(Element ...$children)` - Create a pager whose children are the pages +- `count(int $count)` - Items loaded so far +- `page(int $index)` - Page the pager should show +- `horizontal(bool $value = true)` - Page sideways +- `hasMore(bool $value = true)` - Append the trailing loading page +- `placeholders(array $urls)` - One image URL per loaded page index +- `onPageChange(string $method)` - Page-change handler +- `a11yLabel(string $value)` - Accessibility label +- `a11yHint(string $value)` - Accessibility hint + + diff --git a/resources/views/docs/mobile/4/plugins/core/media-player.md b/resources/views/docs/mobile/4/plugins/core/media-player.md new file mode 100644 index 000000000..78797c080 --- /dev/null +++ b/resources/views/docs/mobile/4/plugins/core/media-player.md @@ -0,0 +1,4 @@ +--- +title: Media Player +order: 800 +---