Katashin .info

バーチャルキーボード張り付き UI と Visual Viewport API

以前は Web で実装するのが難しくてモバイルアプリで実装するしかない UI がありました。その一つがバーチャルキーボードの上に要素を張り付けて配置する UI です。LINE のような画面下に入力フォームがあるチャットアプリはそのような UI の代表例です。

2023年7月現在は主要なブラウザに Visual Viewport API が実装され、バーチャルキーボードの高さを計算し、それを利用した要素の配置が容易です。この記事では Visual Viewport API を使ってスクロールや拡大率によらずバーチャルキーボードの上に要素を張り付ける方法を解説し、この API の前提知識となる Visual Viewport と Layout Viewport について解説します。

Visual Viewport API #

Visual Viewport API を使うと以下のデモのように、バーチャルキーボードのすぐ上にボタンを並べて表示することができます。自分で試してみたい方はこちらの Visual Viewport API を使ったデモページを開いてください。

デモのソースコード(HTML、JavaScript、CSS)は以下のとおりです。HTML には画面いっぱいに広げた <textarea>(id: textarea)とその入力エリアにフォーカスされたらバーチャルキーボードの上に張り付く絵文字のボタン(id: buttons)があります。

<!-- テキスト入力エリア -->
<textarea id="textarea">
モバイルデバイスでキーボードを出現させた時、その上にボタンが付いてきます。
ズーム倍率を変更しても同じ大きさになります。
</textarea>

<!-- バーチャルキーボードの上に表示させる絵文字ボタン -->
<div id="buttons">
  <button type="button">👍</button>
  <button type="button">👌</button>
  <button type="button">👀</button>
  <button type="button">😄</button>
  <button type="button">😇</button>
  <button type="button">🍣</button>
</div>

JavaScript のコードでは、まず textarea に focus と blur のイベントハンドラーを付与し、フォーカス時に buttons を表示、フォーカスが外れた時に非表示にしています。

const buttons = document.getElementById('buttons')
const textarea = document.getElementById('textarea')

// 入力エリアにフォーカスされたら絵文字ボタンを表示
textarea.addEventListener('focus', () => {
  buttons.style.display = 'flex'
})

// 入力エリアのフォーカスが外れたら絵文字ボタンを非表示
textarea.addEventListener('blur', () => {
  buttons.style.display = 'none'
})

window.visualViewport から Visual Viewport API を使用することができます。addEventListener で scroll と resize に対するイベントハンドラーを登録し、Visual Viewport のスクロール、リサイズを検知します。ハンドラー内では絵文字ボタンをバーチャルキーボードの上に配置し、見た目の大きさをズーム率によらず一定にするように、座標と拡大率を計算、適用しています。

Visual Viewport API には Visual Viewport の座標を返す offsetTop、offsetLeft、高さを返す height、拡大率を返す scale があります。buttons の水平位置と拡大率はこれらの情報から計算できます(offsetLeft、scale)。それに加えて、documentElement.clientHeight が Layout Viewport の高さを返す挙動を利用して Visual Viewport の下端が Layout Viewport の下端とどれだけズレているかを計算しています(offsetTop)。

// Visual Viewport のスクロール、リサイズが起きたらリスナーを実行
window.visualViewport.addEventListener('scroll', viewportHandler)
window.visualViewport.addEventListener('resize', viewportHandler)

// Visual Viewport の下端がどれだけ Layout Viewport からズレているかを計算し、
// 絵文字ボタンをそれと同じだけずらす
function viewportHandler() {
  const visualViewport = window.visualViewport

  const offsetLeft = visualViewport.offsetLeft

  // documentElement.clientHeight は Layout Viewport の高さとなる
  const offsetTop =
    visualViewport.offsetTop +
    visualViewport.height -
    document.documentElement.clientHeight

  // 絵文字ボタンを Visual Viewport の下端に合わせる
  const translate = `translate(${offsetLeft}px, ${offsetTop}px)`

  // 絵文字ボタンの大きさを常に同じにするため、現在の拡大率で割る
  const scale = `scale(${1 / visualViewport.scale})`

  buttons.style.transform = `${translate} ${scale}`
}

CSS はデモの本題とはあまり関係がないので解説は省略しますが、以下からコードを読めます。

上記デモの CSS ファイル
/* テキスト入力エリア */
textarea {
  position: absolute;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  border: none;
  resize: none;
  outline: none;
}

/* 絵文字ボタンのラッパー */
#buttons {
  display: none;
  justify-content: center;
  position: fixed;
  left: 0;
  bottom: 0;
  width: 100%;
  background-color: #f0f0f0;
  transition: transform 0.1s ease-out;
  transform-origin: left bottom;
}

button {
  background: none;
  border: none;
  flex: none;
  padding: 0;
  height: 36px;
  width: 40px;
  font-size: 24px;
}

iOS Safari と Android Chrome では scroll と resize イベントの発火の頻度にかなり差があり、Android Chrome では高頻度で発火される一方、iOS Safari ではスクロールやピンチが終わったときにしか発火されません。なので、iOS Safari ではスクロール中などの buttons の挙動がぎこちなく見えてしまいます。実際に公開する Web ページではスクロール中に buttons を非表示にするなどの工夫は必要でしょう。

Visual Viewport と Layout Viewport #

Visual Viewport API をうまく活用できるように Visual Viewport と、それとともに解説されることの多い Layout Viewport が何なのかを理解することは重要です。

Visual Viewport は現在見えている範囲を表していて、ピンチイン・アウトで拡大率を変えたり、バーチャルキーボードを表示するとその座標やサイズが変わります。

Layout Viewport は Web ページの要素をレイアウトするために使われる Viewport で、position: fixed;inset: 0; を指定した要素と同じ範囲だとイメージするとよいでしょう。バーチャルキーボードが現れても下側に position: fixed で固定している要素が押し上げられてこないのは、Layout Viewport が変わらないからです。

これらの Viewport の違いを図で見てみましょう。デモにアクセスした直後には以下の図のように Visual Viewport と Layout Viewport は一致しています。

ページにアクセスした直後の Layout Viewport と Visual Viewport を表した図

その後、textarea にフォーカスすると、拡大が行われ、バーチャルキーボードが出現するので、Visual Viewport のサイズが変わります(オレンジ色の枠)。一方、Layout Viewport のサイズは変わりません(青色の枠)。

拡大とバーチャルキーボードの表示が行われた後の Layout Viewport と Visual Viewport を表した図

おわりに #

Visual Viewport と Layout Viewport はややこしい概念ですが、正しく理解できるとモバイルデバイスでのレイアウトの選択肢が増えます。特にバーチャルキーボードの出現や、ピンチによるズームに合わせた要素のレイアウトに役立つでしょう。

参考文献 #

A tale of two viewports — part one
https://www.quirksmode.org/mobile/viewports.html

A tale of two viewports — part two
https://www.quirksmode.org/mobile/viewports2.html