[Android] Edge-to-Edge対応の完全ガイド — 実装手順と便利な拡張関数まとめ

Android 15(API 35)からEdge-to-Edgeがデフォルトで強制適用されるようになりました。「なんかステータスバーやナビゲーションバーの裏にコンテンツが隠れちゃう……」と困っている方も多いのではないでしょうか。

この記事では、Edge-to-Edge対応の基本的な実装手順から、WindowInsetsの扱い方、そして現場で使い回せる便利な拡張関数まで、まとめて紹介していきます。

スポンサーリンク

Edge-to-Edgeとは

Edge-to-Edgeとは、アプリのコンテンツをステータスバーやナビゲーションバーの裏側まで描画するレイアウト方式のことです。画面全体を使えるので没入感のあるUIが作れます。

従来はシステムバーの領域を避けてコンテンツを描画していましたが、Edge-to-Edgeではアプリ側でシステムバーとの重なりを自分でハンドリングする必要があります。

Android 15以降は全アプリで強制適用されるので、対応は避けて通れなくなりました。

前提条件

  • Android Studio(最新安定版推奨)
  • androidx.activity:activity-ktx:1.8.0 以上
  • androidx.core:core-ktx:1.12.0 以上
  • minSdk 21 以上

基本の実装手順

ステップ1:enableEdgeToEdge() を呼ぶ

一番シンプルな方法は、ActivityonCreateenableEdgeToEdge()を呼ぶだけです。これだけでステータスバーとナビゲーションバーが透明になり、コンテンツがその裏まで描画されるようになります。

import androidx.activity.enableEdgeToEdge
 
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        enableEdgeToEdge() // super.onCreate() より前に呼ぶ
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

enableEdgeToEdge()androidx.activityライブラリに含まれている関数です。以前はWindowCompat.setDecorFitsSystemWindows(window, false)を使っていましたが、今はenableEdgeToEdge()を使うのが公式推奨です。

ステップ2:WindowInsetsでパディングを調整する

Edge-to-Edgeを有効にしただけだと、コンテンツがステータスバーやナビゲーションバーの裏に隠れてしまいます。これを防ぐために、WindowInsetsを使ってパディングを設定します。

import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
 
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { view, windowInsets ->
    val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
    view.updatePadding(
        left = insets.left,
        top = insets.top,
        right = insets.right,
        bottom = insets.bottom
    )
    WindowInsetsCompat.CONSUMED
}

WindowInsetsCompat.Type.systemBars()でステータスバーとナビゲーションバーの両方のInsetsを取得できます。これをビューのパディングに設定すれば、コンテンツがシステムバーの下に隠れなくなります。

ステップ3:Gradle の依存関係

念のため、必要な依存関係をまとめておきます。

// build.gradle.kts
dependencies {
    implementation("androidx.activity:activity-ktx:1.9.3")
    implementation("androidx.core:core-ktx:1.15.0")
}

WindowInsetsの種類を理解する

Insetsには色々な種類があって、用途によって使い分ける必要があります。よく使うものをまとめておきます。

  • WindowInsetsCompat.Type.systemBars() — ステータスバー + ナビゲーションバー。一番よく使う
  • WindowInsetsCompat.Type.statusBars() — ステータスバーだけ
  • WindowInsetsCompat.Type.navigationBars() — ナビゲーションバーだけ
  • WindowInsetsCompat.Type.ime() — ソフトキーボード。入力フォームがある画面で必須
  • WindowInsetsCompat.Type.displayCutout() — ノッチ(カメラの切り欠き)領域

たとえば、ToolBarにはステータスバーのInsetsだけ適用して、一番下のボタンにはナビゲーションバーのInsetsだけ適用する、といった使い分けができます。

便利な拡張関数

毎回ViewCompat.setOnApplyWindowInsetsListenerを書くのは面倒ですよね。ここからは、Edge-to-Edge対応で使い回せる便利な拡張関数を紹介していきます。

システムバーのパディングを一発で適用する

一番よく使うパターンを拡張関数にまとめたものです。

import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
 
/**
 * システムバー(ステータスバー + ナビゲーションバー)の
 * Insetsをパディングとして適用する
 */
fun View.applySystemBarsPadding() {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
        view.updatePadding(
            left = insets.left,
            top = insets.top,
            right = insets.right,
            bottom = insets.bottom
        )
        WindowInsetsCompat.CONSUMED
    }
}

使い方はこれだけ。

// Activityで
findViewById<View>(R.id.main).applySystemBarsPadding()

適用する方向を選べるバージョン

画面によっては「上だけ」「下だけ」パディングを付けたいこともあります。方向を指定できるバージョンも用意しておくと便利です。

/**
 * 指定した方向にだけシステムバーのInsetsをパディングとして適用する
 */
fun View.applyInsetsAsPadding(
    applyTop: Boolean = false,
    applyBottom: Boolean = false,
    applyLeft: Boolean = false,
    applyRight: Boolean = false
) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
        view.updatePadding(
            left = if (applyLeft) insets.left else view.paddingLeft,
            top = if (applyTop) insets.top else view.paddingTop,
            right = if (applyRight) insets.right else view.paddingRight,
            bottom = if (applyBottom) insets.bottom else view.paddingBottom
        )
        WindowInsetsCompat.CONSUMED
    }
}

こんなふうに使います。

// ToolBarにはステータスバー分だけパディング
toolbar.applyInsetsAsPadding(applyTop = true)
 
// 下部のボタンにはナビゲーションバー分だけパディング
bottomButton.applyInsetsAsPadding(applyBottom = true)

マージンで適用したい場合

パディングではなくマージンで調整したいケースもあります。FAB(Floating Action Button)のような要素は、パディングだと見た目が崩れるのでマージンのほうが自然です。

import androidx.core.view.updateLayoutParams
import android.view.ViewGroup
 
/**
 * システムバーのInsetsをマージンとして適用する
 */
fun View.applyInsetsAsMargin(
    applyTop: Boolean = false,
    applyBottom: Boolean = false,
    applyLeft: Boolean = false,
    applyRight: Boolean = false
) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
        view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
            if (applyLeft) leftMargin = insets.left
            if (applyTop) topMargin = insets.top
            if (applyRight) rightMargin = insets.right
            if (applyBottom) bottomMargin = insets.bottom
        }
        WindowInsetsCompat.CONSUMED
    }
}
// FABにナビゲーションバー分のマージンを追加
fab.applyInsetsAsMargin(applyBottom = true)

IME(ソフトキーボード)対応の拡張関数

入力フォームがある画面では、キーボードが表示されたときにコンテンツを押し上げたいことがあります。ime()systemBars()を組み合わせた拡張関数が便利です。

/**
 * システムバー + IME(ソフトキーボード)のInsetsを
 * パディングとして適用する。
 * キーボード表示時に自動でパディングが変わる。
 */
fun View.applySystemBarsAndImePadding() {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val systemBars = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
        val ime = windowInsets.getInsets(WindowInsetsCompat.Type.ime())
        view.updatePadding(
            left = systemBars.left,
            top = systemBars.top,
            right = systemBars.right,
            // IMEが表示されていればIMEの高さ、そうでなければナビバーの高さ
            bottom = maxOf(systemBars.bottom, ime.bottom)
        )
        WindowInsetsCompat.CONSUMED
    }
}
// 入力フォームがある画面のルートビューに適用
findViewById<View>(R.id.scrollView).applySystemBarsAndImePadding()

Jetpack Composeでの対応

Jetpack Composeを使っている場合は、もっとシンプルに対応できます。Composeには最初からInsets用のModifierが用意されています。

import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.union
 
@Composable
fun MyScreen() {
    Scaffold(
        modifier = Modifier
            .fillMaxSize()
            .windowInsetsPadding(WindowInsets.systemBars)
    ) { innerPadding ->
        // innerPadding を使ってコンテンツを配置
        Column(
            modifier = Modifier.padding(innerPadding)
        ) {
            Text("Edge-to-Edge対応済み!")
        }
    }
}

ComposeのScaffoldを使っている場合は、innerPaddingがすでにシステムバーを考慮した値になっているので、それをそのまま使えばOKです。

キーボード対応も簡単で、WindowInsets.imeを使うだけです。

Modifier.windowInsetsPadding(WindowInsets.systemBars.union(WindowInsets.ime))

よくあるトラブルと対処法

コンテンツがステータスバーの裏に隠れる

enableEdgeToEdge()を呼んだあと、ルートビューにWindowInsetsのパディングを設定し忘れていませんか?Edge-to-Edgeを有効にしたら、必ずInsetsの処理もセットで行いましょう。

RecyclerViewの最後のアイテムがナビゲーションバーの裏に隠れる

RecyclerViewの場合は、clipToPadding="false"を設定して、パディングを付けるのがポイントです。こうするとスクロール時はナビゲーションバーの裏まで描画されつつ、最後のアイテムはちゃんと見える位置まで来てくれます。

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false" />
recyclerView.applyInsetsAsPadding(applyBottom = true)

ナビゲーションバーの色がおかしい

enableEdgeToEdge()が自動で透明にしてくれますが、テーマ側でnavigationBarColorを指定していると競合することがあります。テーマから以下の指定を削除してみてください。

<!-- これらを削除 -->
<item name="android:navigationBarColor">...</item>
<item name="android:statusBarColor">...</item>

拡張関数をまとめたユーティリティファイル

最後に、この記事で紹介した拡張関数をまとめたファイルを載せておきます。プロジェクトにコピーしてそのまま使ってください。

// WindowInsetsExtensions.kt
 
import android.view.View
import android.view.ViewGroup
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
 
/** システムバーのInsetsをパディングとして全方向に適用 */
fun View.applySystemBarsPadding() {
    applyInsetsAsPadding(
        applyTop = true,
        applyBottom = true,
        applyLeft = true,
        applyRight = true
    )
}
 
/** 指定方向にシステムバーのInsetsをパディングとして適用 */
fun View.applyInsetsAsPadding(
    applyTop: Boolean = false,
    applyBottom: Boolean = false,
    applyLeft: Boolean = false,
    applyRight: Boolean = false
) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val insets = windowInsets.getInsets(
            WindowInsetsCompat.Type.systemBars()
        )
        view.updatePadding(
            left = if (applyLeft) insets.left else view.paddingLeft,
            top = if (applyTop) insets.top else view.paddingTop,
            right = if (applyRight) insets.right else view.paddingRight,
            bottom = if (applyBottom) insets.bottom else view.paddingBottom
        )
        WindowInsetsCompat.CONSUMED
    }
}
 
/** 指定方向にシステムバーのInsetsをマージンとして適用 */
fun View.applyInsetsAsMargin(
    applyTop: Boolean = false,
    applyBottom: Boolean = false,
    applyLeft: Boolean = false,
    applyRight: Boolean = false
) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val insets = windowInsets.getInsets(
            WindowInsetsCompat.Type.systemBars()
        )
        view.updateLayoutParams<ViewGroup.MarginLayoutParams> {
            if (applyLeft) leftMargin = insets.left
            if (applyTop) topMargin = insets.top
            if (applyRight) rightMargin = insets.right
            if (applyBottom) bottomMargin = insets.bottom
        }
        WindowInsetsCompat.CONSUMED
    }
}
 
/** システムバー + IMEのInsetsをパディングとして適用 */
fun View.applySystemBarsAndImePadding() {
    ViewCompat.setOnApplyWindowInsetsListener(this) { view, windowInsets ->
        val systemBars = windowInsets.getInsets(
            WindowInsetsCompat.Type.systemBars()
        )
        val ime = windowInsets.getInsets(
            WindowInsetsCompat.Type.ime()
        )
        view.updatePadding(
            left = systemBars.left,
            top = systemBars.top,
            right = systemBars.right,
            bottom = maxOf(systemBars.bottom, ime.bottom)
        )
        WindowInsetsCompat.CONSUMED
    }
}

まとめ

Edge-to-Edge対応のポイントをおさらいします。

  1. enableEdgeToEdge()onCreatesuperより前に呼ぶ
  2. ViewCompat.setOnApplyWindowInsetsListenerでInsetsを受け取り、パディングやマージンを調整する
  3. よく使うパターンは拡張関数にまとめて使い回す
  4. Android 15以降は強制適用なので、早めに対応しておくのが吉

この記事で紹介した拡張関数をプロジェクトに入れておけば、新しい画面を作るたびに1行で対応できるのでかなり楽になるはずです。ぜひ活用してみてくださいね。

コメント

タイトルとURLをコピーしました