0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【力技】スクロールスナップで、現在主に表示している要素を取得し、そのデザインを変える

Posted at

成果物

画面収録.gif

スクロールスナップを実装した時、主に表示されている要素(今回は左端に留められている要素)は普通に表示して、それ以外は薄く表示する。

実装

やっていることはかなり力技です。

// useCenterClosestElement.ts

import { Dispatch, MutableRefObject, SetStateAction } from "react";

type Ref = MutableRefObject<HTMLDivElement | null>;
type Setter = Dispatch<SetStateAction<HTMLDivElement | null>>;

export const useCenterClosestElement = (
	containerRef: Ref, // 横スライドさせる要素をラップしているdiv要素
	setter: Setter // set関数
): void => {
	const container = containerRef.current; // CardWrapper要素のこと
	if (!container) return;

	// container内の子要素を配列として取得
	const items = Array.from(container.children) as HTMLDivElement[];
	// 最終的に、最も中心に近い要素を入れる変数
	let closestItem: HTMLDivElement | null = null;
	// 最も中心に近いアイテムまでの距離を保存する
	let closestDistance = Infinity;

	items.forEach((item) => {
		// itemのビューポードに対する相対位置を取得
		const rect = item.getBoundingClientRect();
		// CardWrapperのビューポードに対する相対位置を取得
		const containerRect = container.getBoundingClientRect();

		// itemの中央がどこにあるのか
		const itemCenter = rect.left + rect.width / 2;
		// CardWrapperの中央がどこにあるのか
		const containerCenter = containerRect.left + containerRect.width / 2;

		// itemの中心 - CardWrapperの中心
		const distance = Math.abs(itemCenter - containerCenter);

		// distanceが一番小さいitemが、一番中央に存在する
		if (distance < closestDistance) {
			closestDistance = distance;
			closestItem = item;
		}
	});

	setter(closestItem);
};

// App.tsx
import { useRef, useState, useEffect, useCallback } from "react";
import { useCenterClosestElement } from "./hooks/useCenterClosestElement";
import styled from "styled-components";

const CardWrapper = styled.div`
	overflow: auto;
	scroll-snap-type: x mandatory;
	display: flex;
`;

const Card = styled.div<{ isFocused: boolean }>`
	scroll-snap-align: start;
	flex: none;
	width: 70vw;
	height: 50vh;

	// 以下は見やすくするためのcss
	background-color: red;
	border: 2px solid black;
	margin-right: 5px;
	${({ isFocused }) => !isFocused && "opacity: 0.2"};
`;

export const App = () => {
	const containerRef = useRef<HTMLDivElement | null>(null);
	const [focusedItem, setFocusedItem] = useState<HTMLDivElement | null>(null);

	const handleScroll = useCallback(() => {
		useCenterClosestElement(containerRef, setFocusedItem);
	}, []);

	useEffect(() => {
		const container = containerRef.current;
		if (!container) return;

		// 初期表示アイテム設定
		const initialItem = container.children[0] as HTMLDivElement;
		setFocusedItem(initialItem);
		container.addEventListener("scroll", handleScroll);

		// クリーンアップ
		return () => {
			container.removeEventListener("scroll", handleScroll);
		};
	}, []);

	return (
		<CardWrapper ref={containerRef}>
			<Card isFocused={focusedItem === containerRef.current?.children[0]}>
				カード1
			</Card>
			<Card isFocused={focusedItem === containerRef.current?.children[1]}>
				カード2
			</Card>
			<Card isFocused={focusedItem === containerRef.current?.children[2]}>
				カード3
			</Card>
		</CardWrapper>
	);
};

hooksの解説

useCenterClosestElementは、containerRefsetterという2つの引数を受け取ります。

containerRefにはスクロールスナップでスライドさせる要素をラップしているdiv要素を渡します。今回の例ではCardWrapperがこれに当たります。

setterには、現在主として表示している要素を入れるローカルstateを変更するset関数が入ります。今回の例ではsetFocusedItemがこれに当たります。

hook内でやっているのは、containerRef内にある全ての子要素に対して、画面の中央との距離を計測して、距離が最も小さいものをsetterの引数に渡しています。

改善の余地はとってもありそうです。

まとめ

今回は力技で実装しましたが、世の中には便利なライブラリがたくさんあるので、よほどの理由がない限りはそれらを使う方がいいでしょう。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?