この記事はAndroid Advent Calendar 2021の18日目の記事です。
概要
あまり使う機会は無いかもしれませんが、Snackbarのカスタマイズ方法を整理してみました。
また従来のAndroid Viewでの方法とJetpack Composeでの方法を比較してみたいと思います。
以下で紹介するコードについて、こちらにサンプルプロジェクトをアップしてあります。
https://github.com/androhi/CustomSnackbarSample
Android Viewでのカスタマイズ方法
Android Viewでは、以下のクラスを作成する必要があります。
- ContentViewCallbackクラスを継承したSnackbarのViewクラス
- BaseTransientBottomBarクラスを継承したカスタムSnackbarクラス
- 上記それぞれのレイアウトファイル
class CustomSnackbarView @JvmOverloads constructor(
context: Context,
attributeSet: AttributeSet,
defaultStyle: Int = 0,
): ConstraintLayout(context, attributeSet, defaultStyle), ContentViewCallback {
private val binding = ItemCustomSnackbarBinding.inflate(LayoutInflater.from(context), this, true)
override fun animateContentIn(delay: Int, duration: Int) {
binding.icon.run {
alpha = 0f
animate().alpha(1f).setDuration(duration.toLong()).setStartDelay(delay.toLong()).start()
}
binding.message.run {
alpha = 0f
animate().alpha(1f).setDuration(duration.toLong()).setStartDelay(delay.toLong()).start()
}
}
override fun animateContentOut(delay: Int, duration: Int) {
binding.icon.run {
alpha = 1f
animate().alpha(0f).setDuration(duration.toLong()).setStartDelay(delay.toLong()).start()
}
binding.message.run {
alpha = 1f
animate().alpha(0f).setDuration(duration.toLong()).setStartDelay(delay.toLong()).start()
}
}
}
class CustomSnackbar(
parent: ViewGroup,
content: CustomSnackbarView
) : BaseTransientBottomBar<CustomSnackbar>(parent, content, content) {
init {
getView().setBackgroundColor(ContextCompat.getColor(view.context, android.R.color.transparent))
getView().setPadding(0, 0, 0, 0)
}
companion object {
fun make(parent: ViewGroup): CustomSnackbar {
val content = LayoutInflater.from(parent.context).inflate(
R.layout.view_custom_snackbar,
parent,
false
) as CustomSnackbarView
return CustomSnackbar(parent, content).setDuration(Snackbar.LENGTH_SHORT)
}
}
}
<com.androhi.customsnackbarsample.CustomSnackbarView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp" />
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/darker_gray"
android:padding="16dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<ImageView
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_baseline_self_improvement_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/message"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="test message"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
Android Viewでの使用例
Android ViewでSnackbarを使用するときは紐づくView Groupが必要になりますので、例えば以下のような使い方となります。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.showButton.setOnClickListener {
(binding.root.rootView as? ViewGroup)?.let {
CustomSnackbar.make(it).show()
}
}
}
}
Jetpack Composeでのカスタマイズ方法
Jetpack ComposeではSnackbarHost Composableを使ってSnackbarとして表示する部分をカスタマイズしていきます。
@Composable
fun Screen() {
val scaffoldState = rememberScaffoldState()
Scaffold(
scaffoldState = scaffoldState,
) {
// something to do
}
SnackbarHost(
hostState = scaffoldState.snackbarHostState,
snackbar = { snackbarData: SnackbarData ->
Card(
shape = RoundedCornerShape(8.dp),
backgroundColor = Color.LightGray,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Icon(imageVector = Icons.Default.Face, contentDescription = "")
Spacer(modifier = Modifier.width(8.dp))
Text(text = snackbarData.message)
}
}
}
)
}
SnackbarHostにSnackbarHostStateを渡すことによって、Snackbarの状態を適切に管理してくれます。
Jetpack Composeでの使用例
Jetpack ComposeでSnackbarを呼び出すためのメソッドはsuspend関数になっているため、coroutineの中で実行する必要があります。
@Composable
fun Screen() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
) {
Button(
onClick = {
scope.launch {
scaffoldState.snackbarHostState.showSnackbar("message", duration = SnackbarDuration.Short)
}
}
) {
Text(text = "show snackbar")
}
}
SnackbarHost(
hostState = scaffoldState.snackbarHostState,
snackbar = { snackbarData: SnackbarData ->
Card(
shape = RoundedCornerShape(8.dp),
backgroundColor = Color.LightGray,
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(8.dp),
horizontalArrangement = Arrangement.Start,
verticalAlignment = Alignment.CenterVertically
) {
Icon(imageVector = Icons.Default.Face, contentDescription = "")
Spacer(modifier = Modifier.width(8.dp))
Text(text = snackbarData.message)
}
}
}
)
}
まとめ
Android Viewの方は継承クラスなども少し複雑ですし、アニメーションなども自分で実装しなければいけないので面倒な部分が多いように見えます。一方、Jetpack Composeの方は面倒な部分はSnackbarHost内でよしなにやってくれるため、表示部分のカスタマイズに集中出来ます。
もしComposeを導入しているアプリであれば、SnackbarのカスタマイズはComposeでやるべきだと感じました。