みずきち日記

ひらすらプログラミング

StatusBar と ToolBar のあれこれ

今回の目標はコレです。 グラデーションのかかった Toolbar が StatusBar の下に潜り込んで表示されています。 そして、キーボードが表示された際の Activity のリサイズにも対応するようにします。

StatusBar と ActionBar

まずは、何も設定していない時はどうなるか見ていきます。

分かりやすいように EditText に赤い枠線を付けてます。 キーボードが EditText の上に覆いかぶさってしまっています。

これは、AndroidManifest の方で、android:windowSoftInputMode="adjustResize" を設定すると、キーボードが出現した際に Activity がリサイズされるようになります。

さらに、ActionBar を非表示にします。 Theme.AppCompat.Light.NoActionBar を Activity の Theme に指定すると非表示にできます。

次に、StatusBar の見た目をカスタマイズしていきたいと思います。 まずは、背景を透明にしていきます。

window.statusBarColor = Color.TRANSPARENT をすることで実現できます。

これで背景を透明にできました。しかし、これでは StatusBar の背景が白で、アイコンの色と被ってしまい見にくいので、LIGHT_STATUS_BAR にして、アイコンを黒にします。

window.statusBarColor = Color.TRANSPARENT
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val option = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
    window.decorView.systemUiVisibility = option
}

 

StatusBar の下に Activity の View を食い込ませる

Activity の表示領域を StatusBar の範囲まで拡大させるためには、systemUiVisibility に対して View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE を設定します。

これは、以下のコードで実現できます。 こちらのサイトがとても参考になりました。

window.statusBarColor = Color.TRANSPARENT
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    val option = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
    window.decorView.systemUiVisibility = option
}

 

しかし、これだとキーボードが表示された時、 android:windowSoftInputMode="adjustResize" を設定しているのにも関わらず、 Activity のリサイズが効かなくなってしまいます。

そこで、Activity のルートタグに fitsSystemWindows="true" を指定することで、キーボードが出現した時に、Activity が必要に応じてリサイズされるようになります。

Toolbar を設置する

ここまで出来たら、次は Toolbar を設置していきます。

<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@drawable/gradation"
    app:title="Toolbar"
    app:titleTextColor="#FFF" />

これを Activity に追加しておきます。

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00F"
        android:fitsSystemWindows="true"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@drawable/gradation"
            app:title="Toolbar"
            app:titleTextColor="#FFF" />

        <androidx.appcompat.widget.AppCompatEditText
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="16dp"
            android:background="@drawable/frame"
            android:gravity="top"
            android:hint="sample strings sample strings sample strings sample strings"
            android:padding="8dp"
            tools:ignore="HardcodedText" />
    </LinearLayout>
</layout>

ちなみに、背景の @drawable/gradation はこんな感じです。 gradient タグを使ってグラデーションを描きます。

<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <shape android:shape="rectangle">
            <gradient
                android:angle="45"
                android:centerColor="#FF8C00"
                android:endColor="#FFEE00"
                android:startColor="#F60000" />
        </shape>
    </item>
</layer-list>

この状態で実行すると、Toolbar にグラデーションが描かれていると思います。 しかし、StatusBar にグラデーションがかかっていません。

 

これは、fitsSystemWindows="true" を設定した際に、StatusBar の高さ分だけ Padding が自動的に付与されるからです。

なので、以下の方法でステータスバーの領域までグラデーションをかけようと考えました。

  1. StatusBar の高さ分だけ Toolbar の高さを伸ばす
  2. StatusBar の高さ分だけ Toolbar に対してマイナスマージンをかける

StatusBar の高さは ViewCompat.setOnApplyWindowInsetsListener を使います。今回は汎用性をもたせて拡張関数として定義しました。

fun View.getStatusBarHeightPx(f: ((Int) -> Unit)) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
        v.setOnApplyWindowInsetsListener(null)
        f(insets.systemWindowInsetTop)
        ViewCompat.onApplyWindowInsets(v, insets)
    }
}
private fun resizeToolbar() = with(binding) {
        root.getStatusBarHeightPx { statusBarHeightPx ->
            toolbar.updatePadding(top = statusBarHeightPx)
            toolbar.layoutParams = (toolbar.layoutParams as ViewGroup.MarginLayoutParams).apply {
                height += statusBarHeightPx
                updateMargins(top = -statusBarHeightPx)
            }
        }
    }

また、android:clipToPadding="false" を Activity のレイアウトに追加するのを忘れないでください。マイナスマージンをかけているので、Toolbar を親の View からはみ出して描画されるようにします。これでうまく動くはずです。

最後に、最低限必要なソースコードを載せておきます。

class MainActivity : AppCompatActivity() {

    private val binding by lazy {
        DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initStatusBar()
        setContentView(R.layout.activity_main)
        resizeToolbar()
    }

    private fun initStatusBar() {
        window.statusBarColor = Color.TRANSPARENT
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            val uiOption = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
            window.decorView.systemUiVisibility = uiOption
        }
    }

    private fun resizeToolbar() = with(binding) {
        root.getStatusBarHeightPx { statusBarHeightPx ->
            toolbar.updatePadding(top = statusBarHeightPx)
            toolbar.layoutParams = (toolbar.layoutParams as ViewGroup.MarginLayoutParams).apply {
                height += statusBarHeightPx
                updateMargins(top = -statusBarHeightPx)
            }
        }
    }
}

fun View.getStatusBarHeightPx(f: ((Int) -> Unit)) {
    ViewCompat.setOnApplyWindowInsetsListener(this) { v, insets ->
        v.setOnApplyWindowInsetsListener(null)
        f(insets.systemWindowInsetTop)
        ViewCompat.onApplyWindowInsets(v, insets)
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="#00F"
        android:clipToPadding="false"
        android:fitsSystemWindows="true"
        android:orientation="vertical"
        tools:context=".MainActivity">

        <androidx.appcompat.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            android:background="@drawable/gradation"
            app:title="Toolbar"
            app:titleTextColor="#FFF" />

        <androidx.appcompat.widget.AppCompatEditText
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_margin="16dp"
            android:background="@drawable/frame"
            android:gravity="top"
            android:hint="sample strings sample strings sample strings sample strings"
            android:padding="8dp"
            tools:ignore="HardcodedText" />
    </LinearLayout>
</layout>

何か、気づいたことや、懸念点あればコメント頂ければと思います。 参考になれば幸いです。

おしまい