できたもの
好きなアーティストの曲をSpotifyからとってきて、BPM(曲の速さ)順に表示し1、試聴させてくれます。
スマホ前提。
https://spotify-bpm.now.sh/
https://github.com/IYA-UFO/spotify-bpm
背景
本業はいわゆるマークアップエンジニア。
完全に静的なランディングページを1文字1文字ゴリゴリ コーディングしたりしている。
しかしなぜかReactで作っている。
Reactを使うからには、色々操作できて、データとか取得して、ヌルヌル動く、SPAっぽい...
..いや、ネイティブアプリっぽい美ページを作ってみたい!!
できたこと
横スクロール
アーティスト選択と曲選択の行き来は横スクロール!
100%原始的ハンドメイド。
const Index = () => (
<ThemeProvider theme={{ isSongView }}>
<Row>
<LeftCol>アーティスト選択</LeftCol>
<RightCol>曲選択</RightCol>
</Row>
</ThemeProvider>
);
const Row = styled.div`
width: 200%;
transform: ${({ theme }) =>
theme.isSongView ? 'translateX(-50%)' : 'translateX(0)'};
transition: transform 0.5s;
display: flex;
flex-direction: row;
`;
const LeftCol = styled.div`
width: 50%;
height: ${({ theme }) => (theme.isSongView ? '100%' : 'auto')};
`;
const RightCol = styled.div`
width: 50%;
height: ${({ theme }) => (theme.isSongView ? 'auto' : '100%')};
`;
非表示画面の高さを100%固定に直さないと、表示されていない画面の高さに応じてスクロールできてしまうのがポイント。
試聴コントローラーを下に固定
position:fixed
で一発!と思ったら気のせいだった。
画面高さを100vhにしているのだが、Safariではここにメニューバー?の高さが含まれている。
そのため、
.player {
position:fixed;
bottom:0;
}
ではコントローラーはメニューバーの下に隠れてしまう。。。
なので、マウント後にwindow.innerHeight
を測って表示領域の高さを直した。
viewportとは(哲学)
const SongPicker = () => {
const [innerHeight, setInnerHeight] = useState(0);
useEffect(() => {
setInnerHeight(window.innerHeight);
}, []);
return (
<Wrap innerHeight={innerHeight}>
<p>コンテンツ</p>
</Wrap>
);
};
const Wrap = styled.div`
height: ${({ innerHeight }) => `${innerHeight}px`};
`;
export default SongPicker;
試聴の開始・終了でフワッとアニメーションを消す
この波アニメーションは重く、再生中以外の曲では透過なので隠すのではなく要素を削除する必要がある。
オーディオが再生されたらフワッと出すのは簡単だ。
停止したらフワッと消すのは...?
例えば↓のようにすると、再生が終わった瞬間ブツ!っと消えてしまう。
const Wave = ({ isPlaying }) => (
<Wrap>{isPlaying && <p>波のアニメーション色々</p>}</Wrap>
);
↓のように、onAnimationEndというイベントでレンダリングを止めるようにした波要素なら、自身のアニメーションが終わってから人知れず静かに消えてくれる。
参考: https://czaplinski.io/blog/super-easy-animation-with-react-hooks/
const Wave = ({ isPlaying }) => {
const [shouldRender, setRender] = useState(isPlaying);
useEffect(() => {
if (isPlaying) setRender(true);
}, [isPlaying]);
const onAnimationEnd = () => {
if (!isPlaying) {
setRender(false);
}
};
return (
<Wrap isPlaying={isPlaying} onAnimationEnd={onAnimationEnd} >
{shouldRender && <p>波のアニメーション色々</p>}
</Wrap>
);
};
曲一覧のロード中にかっこいいプレースホルダー
上から順に ふわ~っと 点滅する表示枠みたいなアレである。
各行に1-15の番号を渡してやり、それぞれ自分の番号×0.1秒遅れて点滅アニメーションを始める。
const LoadingAnimation = () => {
const loadingArray = [];
for (let i = 0; i < 15; i++) {
loadingArray.push('');
}
return (
<>
{loadingArray.map((el, index) => (
<Row key={index} index={index} />
))}
</>
);
};
const blink = keyframes`
0% { opacity: 0; }
50% { opacity: 1;}
100% {opacity: 0;}
`;
const Row = styled.div`
&:nth-of-type(2n) {
opacity: 0;
animation: ${blink} 2s linear ${({ index }) => `0.${index}s`} infinite;
}
`;
15なのはスマホの画面を埋め尽くして余裕があるから。
動けばよかろうだ。
できなかったけど やりたいこと
Spotifyアクセストークンのキャッシュ
SpotifyのAPIに曲やアーティストの情報を教えてもらうには、
- Spotify for Developer でIDとパスを発行する
- ID,パスを添えて「アクセストークン」をリクエストする
- アクセストークンを添えて本当に欲しかった情報をリクエスト
という2段階になっている!
サーバー側の知識が≒0な自分にはわからない深い事情があるに違いない。
IDとパスワードをクライアントに配るのもどうかと思い、Spotifyからの情報取得はNext.jsを経由してもらうことにした。
すると、アーティストの一覧を検索するだけで
- Webページを表示
- クライアント⇨アーティスト情報を要求⇨Next.js
- Next.js⇨トークンを要求⇨Spotify
- Spotify⇨トークンを返却⇨Next.js
- Next.js⇨アーティスト情報を要求⇨Spotify
- Spotify⇨アーティスト情報を返却⇨Next.js
- Next.js⇨アーティスト情報を返却⇨クライアント
と長い長い道のり。
タイムラグがすごい。
「Next.jsがアクセストークンをしばらく記憶」すれば無駄↓が減るが、
自分にとってサーバーとは、URLに応じて何度でも同じものを返す記憶喪失マシンだったので、サーバーが記憶?!を持つなんて と脳が停止して挫折した2。
が、速さを求めるなら避けられない
宣言的なアニメーション
アニメーションを手探りで頑張った結果、気づくと「あれしたら、次にこれして...」みたいな手続き的?なコードだらけになっていた。
//該当部分例
const handleBackClick = () => {
goBackToAirtistPicker();
audioRef.current.src = '';
coverRef.current.scrollTo({
top: 0,
behavior: 'smooth',
});
};
Reactらしく宣言的なアニメーションの書き方を先人たちが開拓しているはずである。学ぼう。
説明ページ
ほぼ自習用とは言え、使い方を説明すべきである。ひょっとすると検索エンジンにかかるようになるかもしれない。
横スクロール時をエレガントに
横スクロールでアプリっぽい!と喜ぶには以下の2つがほしい
- 曲選択からアーティスト選択に戻るとき、アニメーション終了まで曲選択画面の内容を消さない
- 3ページ目以降もスイスイ足せる、自然で拡張性のある書き方