사용자가 어떠한 요청을 했는데 화면에 아무런 변화가 없다면 어떤 느낌을 받을까요? 요청이 제대로 들어갔는지, 앱이 고장 난 건지 사용자는 알 수가 없습니다.
따라서 데이터를 로딩 중에는 사용자가 알기 쉽게 로딩 애니메이션을 보여주는 것이 좋습니다. 로딩 애니메이션에는 스켈레톤 UI, 루프 애니메이션, 프로그래스 바 등의 다양한 방법이 있습니다.
이번 포스팅에서는 위의 화면과 같이 스켈레톤 로딩 화면을 구현해보겠습니다. 스켈레톤 로딩 화면은 표시될 정보의 대략적인 형태를 미리 보여줘서 다음 화면까지 부드럽게 연결해주는 역할을 합니다.
스켈레톤 로딩 화면을 구현하는 데는 여러 방법이 있지만, 가장 유명한 Facebook에서 제공하는 shimmer-android 라이브러리를 사용해보겠습니다.
라이브러리 추가
먼저 shimmer-android 라이브러리를 추가해줍니다.
implementation "com.facebook.shimmer:shimmer:0.5.0"
layout_item.xml
리스트에서 각각의 item을 그려줄 레이아웃을 작성합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout 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">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<ImageView
android:id="@+id/iv_sample"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:background="@color/cardview_shadow_start_color"/>
<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:textStyle="bold"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/tv_email"
app:layout_constraintStart_toEndOf="@id/iv_sample"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Name" />
<TextView
android:id="@+id/tv_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toBottomOf="@id/tv_name"
app:layout_constraintStart_toStartOf="@id/tv_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Email"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
layout_item_shimmer.xml
스켈레톤 로딩 화면에서 보여줄 UI를 작성해줍니다. 리스트 item의 기본 구조와 유사하게 작성합니다.
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<ImageView
android:id="@+id/iv_sample"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@color/cardview_shadow_start_color"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
android:id="@+id/tv_name"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:background="@color/cardview_shadow_start_color"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@id/tv_email"
app:layout_constraintStart_toEndOf="@id/iv_sample"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<TextView
android:id="@+id/tv_email"
android:layout_width="150dp"
android:layout_height="wrap_content"
android:background="@color/cardview_shadow_start_color"
android:layout_marginTop="4dp"
app:layout_constrainedWidth="true"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintTop_toBottomOf="@id/tv_name"
app:layout_constraintStart_toStartOf="@id/tv_name"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
위의 xml에서 중요한 점은 배경색을 흰색이 아닌 유색으로 지정해줘야 한다는 것입니다. 배경이 흰색이면 반짝이는 효과가 보이지 않습니다.
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_sample"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/layout_item"/>
<com.facebook.shimmer.ShimmerFrameLayout
android:id="@+id/sfl_sample"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
<include layout="@layout/layout_item_shimmer" />
</LinearLayout>
</com.facebook.shimmer.ShimmerFrameLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
ShimmerFrameLayout을 사용해 스켈레톤 로딩 화면을 만들어 줍니다. ShimmerFrameLayout으로 뷰를 감싸게 되면 감싸진 뷰에 Shimmer 이펙트를 적용할 수 있습니다.
리사이클러뷰의 item과 유사한 빈 레이아웃을 표현해주기 위해 layout_item_shimmer를 여러 개 추가해줍니다.
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy { DataBindingUtil.setContentView(this, R.layout.activity_main) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding.rvSample.adapter = SampleAdapter()
loadSampleData()
}
private fun loadSampleData() {
lifecycleScope.launch {
showSampleData(isLoading = true)
delay(3000)
val samples = getSampleList()
(binding.rvSample.adapter as SampleAdapter).replaceAll(samples)
showSampleData(isLoading = false)
}
}
private fun showSampleData(isLoading: Boolean) {
if (isLoading) {
binding.sflSample.startShimmer()
binding.sflSample.visibility = View.VISIBLE
binding.rvSample.visibility = View.GONE
} else {
binding.sflSample.stopShimmer()
binding.sflSample.visibility = View.GONE
binding.rvSample.visibility = View.VISIBLE
}
}
}
리사이클러뷰의 adapter를 설정한 뒤, loadSampleData() 메서드를 통해 데이터를 요청합니다. 서버에 데이터를 요청했다고 가정하고 3초의 딜레이를 적용해줬습니다.
ShimmerFrameLayout의 startShimmer() 메서드를 호출하면 Shimmer 이펙트가 적용됩니다. 데이터가 전부 로딩돼서 이펙트를 해제해주고 싶다면 stopShimmer() 메서드를 호출해주면 됩니다.
예제 소스
https://github.com/tkdgusl94/blog-source/tree/master/Shimmer-Effect
참고
https://blog.mindorks.com/using-shimmer-effect-placeholder-in-android
'Android' 카테고리의 다른 글
[Android] MVVM 패턴과 AAC에서의 ViewModel (8) | 2021.10.06 |
---|---|
[Android] Gson을 이용한 Room에 다양한 타입의 데이터 저장하기 (0) | 2021.09.24 |
[Android] viewModelScope.launch() 간단하게 바꿔보기 (0) | 2021.09.11 |
[Android] API key값 local.properties에 안전하게 보관하기 (2) | 2021.08.25 |
[Android] BottomNavigationView에서 Fragment 전환 (3) | 2021.08.09 |