OTOBANK Engineering Blog

オトバンクはコンテンツが大好きなエンジニアを募集しています!

React Native の ScrollView でスクロール時に child を上部に貼り付けたい

こんにちは、アプリ開発担当のエモトです。本業は稲作で、合間に横スクロールのダンジョン攻略や、アプリ開発を行っています。

React Native で画面を作る時、ScrollView を利用する方は多いと思います。その画面でスクロールするとき、ある child だけ上部に固定させたいというシーンが出てくるかもしれません。iOS で言うと UITableView におけるセクションヘッダーに近い動きです。さて、それを React Native で実装したい場合、どうすれば良いでしょうか?

f:id:mitsuharu_e:20201128141318g:plain

上部固定させたい

まず最初にネイティブ開発で考えると、iOS の UIScrollView にはそのような機能がないので、UITableView のセクションヘッダーをカスタマイズする方法が考えられます。

それに従うと React Native なら ScrollView から SectionList に置き換えと、手間のかかる改修が予想されました・・・が!、なんと、React Native の ScrollView 自体にその機能が既に実装してありました。stickyHeaderIndices に固定したい child の index を指定するだけです。簡単でした。

const Component: React.FC<Props> = () => {
  const stickyHeaderIndices = [1]
  return (
    <ScrollView stickyHeaderIndices={stickyHeaderIndices} >
      <Header />
      <Sticky />
      <Contents />
    </ScrollView>
  )
}

なぜ上部固定できるのか?

すでに ScrollView で組んでるコードをバラすことなく、パラメーター1つで実現できました。ただ、ここで1つの疑問が生まれます。本来ネイティブの UIScrollView ではそのよう機能がないのに、なぜ React Native の ScrollView だとできるのか?。

ここで私は、「ScrollView はセクションヘッダーを利用するため、実は UITableView なのでは?」と考えましたが、ネイティブ側に対応する RCTScrollView.h を見る限り、UIView を継承しいて、その考えは誤りでした。

次に、stickyHeaderIndices を設定すると呼ばれる ScrollViewStickyHeader.js を見ると、その答えが分かりました。

style={[..., {transform: [{translateY: this._translateY}]}, ...]}

このコンポーネントは AnimatedView で作られて、style をアニメーション制御していました。あの React Native でも力技的な・地味なコードを書いているんだなーという印象を受けました。

まとめ

私のバックボーンは iOS アプリのネイティブ開発なので、何かを作る際はまずネイティブの知識や経験を持ってきて、それを React Native に当てはめることをやるので、今回のようなネイティブではできないことが React Native では簡単にできるのは驚かされます。日々勉強です。

また、毎日使っている、便利でスマートなフレームワークは、OSS活動される皆様方たちの努力の上でなりたっているんだなと、改めて実感しました。そのフレームワークを使って、よいアプリを作っていこうと思う、良い機会でした。

audiobook.jp作品のサンプルコンテンツを埋め込める様になりました

こんにちは、普段サーバーサイドやWeb開発をやりつつフロントエンド入門中の岩Dです。
コロナも怖いですが、そろそろインフルエンザも怖くなってくる時期ですね。皆様インフルエンザの予防接種はお済みですか?私は午前に予防接種を受けてきて左腕が重い今日この頃です。

さて、今回のネタは「Embed (埋め込みコンテンツ)を作ってみた」です。

Embed って?


鈴村健一・堀江由衣共演!『君の膵臓をたべたい』

この様な感じで、ブログやWebページ内にYoutubeなどのコンテンツを埋め込んで表示ができるもので、埋め込んだページ内で動画や音声の再生などが出来たり、ページが華やかになったりと様々な良いことがあります。

参考 oEmbed

実装した Embed

今回実装したのがこの Embed です。この様にオーディオブックの書籍画像とサンプル音源の再生画面が表示され、audiobook.jp の商品ページへ行かずともサンプル音源が再生できる様になっています。
※サンプル音源が備わっていない作品につきましては、Embedを埋め込んでも再生画面が表示されません。

Embed 埋め込みサンプル

<iframe
  width="100%"
  height="253"
  src="https://audiobook.jp/embed/product/234391"
  frameborder="0"
  scrolling="no">
</iframe>

実装要件

この Embed は以下の様な要件で実装されております。

  • PC/SP で縦幅が同じになる様にする
  • 様々な端末で表示ができる様レスポンシブ対応にする
  • 社内デザイナーがデザインした様な見た目のサンプル音源再生を実現する
  • サンプル音源がない場合はサンプル音源再生部分に商品概要を表示させる
  • タイトル、著者などの表示について、PCでは1行で、SPでは2行で表示させて Embed の横幅を超える場合は省略表示させる
  • 販売されていないオーディオブックが埋め込まれた場合、コンテンツが表示できない事を表示させる

気を遣った点

縦幅について

PC/SP で表示が若干異なるものの、縦幅を同じにしなければいけない要件がありました。著者の情報がないもの、サンプル音源がないものなど表示内容に欠損があったとしても縦幅が変わる事なく表示できる様にするため、各項目の表示領域に対して親要素の幅から計算し height を適切な値で設定する事で Embed の高さが変わらない様にしています。また、SP表示の時はサンプル音源再生の部分が書籍画像の下に配置されますが、書籍画像のサイズをPC表示より少し小さくし、PC表示と同じ高さになる様に色々計算をしています。

PC表示
f:id:siwadate:20201106154752p:plain
SP表示
f:id:siwadate:20201106154843p:plain

サンプル音源の再生について

サンプル音源の再生について、今回は <audio> タグと MediaElement.js を使い、css で見た目の最終調整をして表示させています。

before
f:id:siwadate:20201106144817p:plain
after
f:id:siwadate:20201106144954p:plain

これは、audiobook.jp の商品ページ(君の膵臓をたべたい by audiobook.jp)などで表示しているサンプル音源再生の部分でもほぼ同じ仕組みが使われていますが、 css で見た目を調整するだけで随分と変わるものですね。

終わりに

PC/SPで Embed の縦幅が同じになる様にし、様々な端末で表示できる様レスポンシブ対応させつつ、サンプル音源再生を表示させるなど様々な苦労点がありました。一番こだわったサンプル音源再生の部分については、再生・停止ボタンを変更した以外は css で見た目の調整をしたのみで、さほど手間をかけずに良いものができたと思います。

今回の様な見た目として反映されるものを作るのが好きなので、今後もこの様な見た目を良くする仕事をしていきたいです。

また、この Embed について社内 note にも記載があるのでこちらも併せてどうぞ。 note.com

末筆になりますが、水樹奈々さん妊娠おめでとうございます。お子様にはぜひ「おやすみ、ロジャー」をお読み聞かせください。

FlatList の onViewableItemsChanged で起きたエラーを解決する

こんにちは、アプリ開発担当のエモトです。遊んでいたソシャゲが12月にサービス終了ということで、溜まった石やアイテムを使って、後先考えずに遊んでいます。アニメ作品のゲーム化なのでいつまで続くのか不安でしたが、2年弱も続いたので満足しております。なお、弊社本社近くのラーメン屋でガチャを回すと引きが良いです(個人調べ)。ガチャ好きな皆さまの入社をお待ちしております。

FlatList で今表示されている item の情報を取得するため、onViewableItemsChanged を利用したのですが、問題が起こり手間が取られてしまいました。今回はその問題と解決方法を共有したいと思います。なお、以降に提示するコード例は簡易的にしています。

問題

FlatList で表示されている item を取得しようと onViewableItemsChanged を利用しました。

const onViewableItemsChanged = ({viewableItems}) => { ... }
<FlatList onViewableItemsChanged={onViewableItemsChanged} />

すると、エラーが出てきました。しかも、このエラーは常にではなく、ホットリロード時に起こるなど、特殊なエラーでした。

Changing onViewableItemsChanged on the fly is not supported

調べると、

At least one of the viewAreaCoveragePercentThreshold or itemVisiblePercentThreshold is required.

とのことなので、viewabilityConfig に値を設定しました。

const onViewableItemsChanged = ({viewableItems}) => { ... }
const viewabilityConfig = {
  itemVisiblePercentThreshold: 50,
}
<FlatList
  onViewableItemsChanged={onViewableItemsChanged}
  viewabilityConfig={viewabilityConfig}
/>

しかしながら、エラーは治りませんでした。

解決

viewabilityConfig の公式ドキュメントを読むと、気になる一文がありました。

This needs to be done in the constructor to avoid following error

リンクされていた issue を読むと、render のタイミングで生成して渡すとダメな仕様でした。解決方法は分かったものの、私は関数コンポーネントで作成していたので、クラスコンポーネントの constructor に対応するものは何?と悩むことに。いろいろ調べ試すと今回は useRef() で良いと分かりました。

const onViewableItemsChanged = useRef(({viewableItems}) => { ... })
const viewabilityConfig = useRef({
  itemVisiblePercentThreshold: 50,
})
<FlatList
  onViewableItemsChanged={onViewableItemsChanged}
  viewabilityConfig={viewabilityConfig}
/>

やっと無事に動きました。私の中に useRef() に関数を入れる発想はなかったので、時間がかかってしまいました。

まとめ

FlatList の仕様と、クラスコンポーネントのメソッドを関数コンポーネントで書くにはどうすればいいんだという2つの問題を踏み抜いて、久々に沼ってしまいました。改めて、こういうシーンでは、

  • ドキュメントを漁る
  • 関数コンポーネントとクラスコンポーネントの比較

に立ち返るのが良いですね。この FlatList の仕様は他の方でもハマりそうな問題だと思うので、少しでも参考になれば幸いです。