先週、Android 17 のベータを入れたリサイズ対応エミュレータで自分のアプリを開いたとき、見慣れない光景に手が止まりました。ポートレート固定で設計したはずの画面が、横長のウィンドウいっぱいに引き伸ばされて表示されていたのです。レターボックス(左右の黒帯)で守られていた従来の表示は、そこにはありませんでした。
個人開発で Android アプリを運用している方の多くは、「画面はポートレート固定にしておけばレイアウトは崩れない」という前提で作ってきたはずです。私自身もそうでした。その前提が、今夏提供見込みの Android 17 では大画面デバイスにおいて通用しなくなります。
ここでは、Rork で生成した Expo(React Native)アプリを対象に、影響の有無を判定するところから、固定をほどく実装、エミュレータだけで完結する検証までを、私が 6 本のアプリで実際に進めた順番でまとめます。
何が変わるのか — 「固定の無視」は段階的に進んできた
まず変更の中身を整理します。Google は Android 16 から、画面の短辺が 600dp 以上の大画面デバイス(タブレット・折りたたみの展開状態・デスクトップウィンドウ)において、アプリ側が宣言した次の制限を無視する方針を段階的に適用してきました。
screenOrientation によるポートレート / ランドスケープ固定
resizableActivity="false" によるリサイズ拒否
- アスペクト比の上限・下限の宣言
Android 16 の時点では互換プロパティによる一時的なオプトアウトが認められていましたが、Android 17 ではこの逃げ道が外れる、というのが今回の節目です。つまり「対応するかどうか」ではなく「いつ対応するか」だけが残された選択になります。
スマートフォン単体(短辺 600dp 未満)では従来どおり固定が尊重されます。影響範囲はあくまで大画面側ですが、折りたたみ端末は閉じればスマートフォン・開けばタブレットという二面性を持つため、「うちはタブレット向けに出していないから無関係」という判断は成り立ちにくくなりました。
影響を受けるかを 10 分で判定する
改修の前に、自分のアプリがどの程度影響を受けるかを見積もります。私は次の 3 点を順に確認しました。
- app.json の orientation 設定を見る: Rork が生成する Expo プロジェクトの既定は
"orientation": "portrait" です。この値が portrait か landscape なら、固定無視の対象に入ります
- 固定幅・固定比率のレイアウトを洗い出す:
Dimensions.get("window") をモジュール読み込み時に一度だけ評価しているコードは、リサイズ後も古い寸法を返し続けます。grep -rn "Dimensions.get" src/ で全箇所を確認してください
- Google Play Console の端末内訳を見る: 「統計情報 → 端末の種類」でタブレット・折りたたみの利用比率を確認します。私のアプリでは合計 4〜7% でしたが、この層は画面が大きいぶん広告の視認面積も大きく、AdMob の eCPM がスマートフォンより高めに出る傾向があり、切り捨てるには惜しい層です
3 点のうち 1 と 2 の両方に該当するなら、放置した場合に「引き伸ばされて崩れた画面」がそのままユーザーに見えることになります。逆に、すでに orientation: "default" で運用していてレイアウトが寸法ベースなら、本稿の作業はほぼ検証だけで済みます。
固定をほどく — app.json と画面単位ロックの再設計
改修の第一歩は、グローバルな固定をやめて「必要な画面だけロックする」方針への転換です。app.json はこう変えます。
{
"expo": {
"orientation": "default",
"android": {
"softwareKeyboardLayoutMode": "pan"
},
"plugins": [
["expo-screen-orientation", { "initialOrientation": "DEFAULT" }]
]
}
}
全画面の固定を外すと、カメラ撮影画面や動画プレイヤーなど「本当に向きを固定したい画面」が困ります。そこは expo-screen-orientation で画面単位にロックし、なおかつ大画面では固定しない、という条件を足します。
// useConditionalPortraitLock.ts — スマートフォンでのみポートレート固定する
import { useEffect } from "react";
import { useWindowDimensions } from "react-native";
import * as ScreenOrientation from "expo-screen-orientation";
export function useConditionalPortraitLock() {
const { width, height } = useWindowDimensions();
// 短辺 600dp 以上は大画面とみなし、固定しない(固定しても無視されるため)
const isLargeScreen = Math.min(width, height) >= 600;
useEffect(() => {
if (isLargeScreen) {
ScreenOrientation.unlockAsync();
return;
}
ScreenOrientation.lockAsync(
ScreenOrientation.OrientationLock.PORTRAIT_UP
);
return () => {
ScreenOrientation.unlockAsync();
};
}, [isLargeScreen]);
}
「大画面では固定しない」を明示的に書いておくのは、将来 OS 側の挙動が変わっても意図が壊れないようにするためです。無視される設定に依存したコードを残すより、無視される前提を自分のコードに織り込むほうが、私は安全だと考えています。
レイアウトを寸法クラスで組み直す
向きの固定を外すと、次はレイアウトです。ここで画面ごとに if (isTablet) を散らかすと保守が破綻します。私は寸法クラスを 1 つのフックに集約し、各画面はクラス名だけを見る構成にしました。
// useSizeClass.ts — 寸法クラスを 1 箇所で定義する
import { useWindowDimensions } from "react-native";
export type SizeClass = "compact" | "medium" | "expanded";
export function useSizeClass(): SizeClass {
const { width } = useWindowDimensions();
if (width < 600) return "compact"; // スマートフォン縦
if (width < 840) return "medium"; // 折りたたみ展開・小型タブレット縦
return "expanded"; // タブレット横・デスクトップウィンドウ
}
しきい値の 600 / 840 は Material Design のウィンドウサイズクラスに合わせています。独自値ではなく公開された区分に乗っておくと、デザイナーや他のライブラリと前提を共有しやすくなります。
画面側は、たとえば一覧画面ならこう書けます。
// WallpaperGrid.tsx — 寸法クラスで列数とナビ位置を切り替える
import { FlatList, View } from "react-native";
import { useSizeClass } from "./useSizeClass";
const COLUMNS: Record<string, number> = {
compact: 2,
medium: 3,
expanded: 5,
};
export function WallpaperGrid({ items }: { items: Item[] }) {
const sizeClass = useSizeClass();
const numColumns = COLUMNS[sizeClass];
return (
<View style={{ flexDirection: sizeClass === "expanded" ? "row" : "column", flex: 1 }}>
{sizeClass === "expanded" && <SideNavigation />}
<FlatList
key={numColumns} // 列数変更時に再レイアウトさせる
data={items}
numColumns={numColumns}
renderItem={renderThumbnail}
/>
</View>
);
}
ひとつ実装上の罠を共有しておきます。FlatList は numColumns を動的に変えるとエラーになるため、key に列数を渡して強制的に再マウントさせる必要があります。折りたたみの開閉ではこの再マウントが頻発し得るので、スクロール位置を保持したい画面では開閉をまたいで initialScrollIndex を復元する処理も足しました。
検証は実機なしで完結できる
折りたたみ端末を買わずに検証を済ませたい、というのが個人開発の本音だと思います。実際、次の手順でほぼ完結しました。
- リサイズ対応エミュレータを使う: Android Studio の Device Manager で「Resizable」デバイスを作成すると、実行中にスマートフォン・折りたたみ・タブレットの形状をツールバーから切り替えられます
- 折りたたみの開閉を再現する: 同エミュレータの fold/unfold ボタンで、アプリを起動したまま開閉をまたぐ挙動(寸法変更イベント・再レイアウト)を確認します
- デスクトップウィンドウで自由リサイズを試す: ウィンドウの端をドラッグして連続的に寸法を変え、しきい値の境界(600dp / 840dp 付近)でレイアウトが行き来しても破綻しないかを見ます
- 回転を全画面で一周する: 固定を外した直後は、想定していなかった画面の横向き表示が初めて露出します。全画面を一巡して確認してください
この手順で私のアプリから出てきた修正点は 4 つでした。スプラッシュ画像が横長で引き伸ばされる(contain 指定で対処)、モーダルが横画面で縦に間延びする(最大幅 560dp を設定)、キーボード表示時に入力欄が隠れる(softwareKeyboardLayoutMode の見直し)、そして前述の FlatList 列数エラーです。いずれも対処は半日で終わる規模ですが、検証なしでリリースしていたら確実にレビューで指摘されていたと思います。
進め方の提案 — 義務化を待たずに一巡しておく
Android 17 の適用は今夏の Pixel から段階的に始まる見込みですが、対応作業そのものは OS の提供を待つ必要がありません。リサイズ対応エミュレータでの検証も、寸法クラスの導入も、今日の環境でそのまま実行できます。
私の 6 本での実測では、影響判定に 10 分、app.json と画面単位ロックの再設計に半日、寸法クラス導入とレイアウト修正に 1〜2 日、検証に半日という配分でした。アプリ 1 本あたり最大 3 日の投資で、タブレット・折りたたみユーザーへの体験が「崩れた画面」から「広い画面を活かした表示」に変わるなら、十分に割に合うと感じています。
まずはエミュレータの Resizable デバイスで現状を一度見てみることを推奨します。崩れ方を自分の目で確認すると、どこから手を付けるべきかは自然と決まります。レイアウト崩れの個別パターンへの対処は Rorkアプリのレイアウト崩れ・レスポンシブ対応トラブルシューティング に、iPad 側の大画面設計の考え方は Rork 生成アプリを iPad で動かしてから直した、3週間ぶんの設計メモ にまとめてありますので、あわせて参考にしていただければ幸いです。