이번 포스트에서는 CollapsingToolbarLayout을 응용하여 구글 캘린더 앱의 아래와 같은 효과를 따라 해 볼 것이다.
구현하고자 하는 기능은 위와 같이 월간 달력이 접히거나 펼치는 기능이 있고, 일간 화면을 스크롤하여 월간 달력을 접거나 펼칠 수 있다. 또한 월간 달력이 펼쳐진 상태에서는 일간 화면의 스크롤이 동작하지 않아야 하며, 접힌 상태에서는 동작이 되어야 한다.
전체적인 구조는 다음과 같다.
AppBarLayout과 상호작용 하기 위한 NestedScrollView와 FrameLayout이 있다. FrameLayout 안의 Fragment에서 일간 화면을 그려준다. NestedScrollView와 FrameLayout 둘 다 AppBarLayout 접히거나 펼쳐지면 함께 움직여야 하기 때문에 layout_behavior를 설정해준다.
다음은 AppBarLayout의 상태 변경을 감지할 수 있는 AppBarStateChangeListener를 만든다. AppBarStateChangeListener는 AppBarLayout.OnOffsetChangedListener를 오버라이드 해서 구현한다. AppBarStateChangeListener를 통해 AppBarLayout이 접혔는지, 펼쳐져 있는지를 판단할 수 있다.
마지막으로 월간 달력이 접힌 상태에서는 NestedScrollView의 스크롤을 막아 Fragment 안에 들어 있는 ScrollView의 스크롤이 적용되어야 하기 때문에 NestedScrollView를 상속받아 CustomScrollView를 만들어 준다.
우선 예제를 만들기 위해 [File -> New -> Activity -> Scrolling Activity] 경로로 이동해 새로운 프로젝트를 만들어 준다. Scrolling Activity로 새 프로젝트를 만들게 되면 기본적인 구조를 자동으로 만들어줘서 테스트 하기 편리하다.
activity_scrolling.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:fitsSystemWindows="true"
tools:context=".ScrollingActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
android:layout_width="match_parent"
android:layout_height="@dimen/app_bar_height"
android:fitsSystemWindows="true"
android:theme="@style/AppTheme.AppBarOverlay">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
app:contentScrim="?attr/colorPrimary"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"
app:toolbarId="@+id/toolbar">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:id="@+id/fl_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.leveloper.scrollingactivity.CustomScrollView
android:id="@+id/custom_scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ScrollingActivity"
tools:showIn="@layout/activity_scrolling">
<View
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.leveloper.scrollingactivity.CustomScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
layout_scrollFlags는 scroll, exitUntilCollapsed, snap으로 설정한다.
AppBarLayout과 상호작용 하기 위한 FrameLayout과 CustomScrollView를 추가한다. 이때 주의할 점은 CustomScrollView의 스크롤이 우선적으로 동작해야 하기 때문에 FrameLayout보다 CustomScrollView를 아래에 작성한다.
CustomScrollView의 사이즈를 match_parent로 하기 위하여 자식 뷰로 View를 하나 추가한다. 이때 ScrollView의 크기를 자식 뷰와 같게 하려면 fillViewport 값을 true로 줘야 한다.
위에서 설명한 대로 FrameLayout과 CustomScrollView 둘 다 layout_behavior를 설정해서 AppBarLayout과 상호작용이 되도록 한다.
CustomScrollView
class CustomScrollView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
): NestedScrollView(context, attrs, defStyleAttr) {
/* 스크롤 가능한지 */
var scrollable = true
override fun onTouchEvent(ev: MotionEvent): Boolean {
return when (ev.action) {
MotionEvent.ACTION_DOWN -> {
super.onTouchEvent(ev) && scrollable
}
else -> super.onTouchEvent(ev)
}
}
override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
return super.onInterceptTouchEvent(ev) && scrollable
}
}
scrollable 변수를 하나 둬서 스크롤 가능 여부를 판단한다. onTouchEvent 메서드와 onInterceptTouchEvent 메서드를 오버라이드 해서 스크롤을 막아준다.
AppBarStateChangeListener
abstract class AppBarStateChangeListener: AppBarLayout.OnOffsetChangedListener {
enum class State {
EXPANDED,
COLLAPSED,
IDLE
}
private var currentState = State.IDLE
override fun onOffsetChanged(appBarLayout: AppBarLayout?, verticalOffset: Int) {
appBarLayout?.let {
when {
verticalOffset == 0 -> {
if (currentState != State.EXPANDED) {
onStateChanged(appBarLayout, State.EXPANDED)
currentState = State.EXPANDED
}
}
abs(verticalOffset) >= appBarLayout.totalScrollRange -> {
if (currentState != State.COLLAPSED) {
onStateChanged(appBarLayout, State.COLLAPSED)
currentState = State.COLLAPSED
}
}
else -> {
if (currentState != State.IDLE) {
onStateChanged(appBarLayout, State.IDLE)
currentState = State.IDLE
}
}
}
}
}
/**
* Notifies on state change
* @param appBarLayout Layout
* @param state Collapse state
*/
abstract fun onStateChanged(appBarLayout: AppBarLayout, state: State)
}
AppBarLayout의 상태를 판단할 수 있는 AppBarStateChangeListener이다. AppBarLayout.OnOffsetChangedListener 인터페이스의 onOffsetChanged() 메서드를 오버라이드 해준다.
AppBarLayout의 verticalOffset 값에 따라 EXPANDED, COLLAPSED, IDLE 세 가지 상태로 나뉜다.
변경된 상태는 onStateChanged 메서드를 통해 외부에서 변경을 감지할 수 있다.
ScrollingActivity
class ScrollingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
setToolbar()
supportFragmentManager.beginTransaction().replace(R.id.fl_container, BlankFragment(), "").commitNowAllowingStateLoss()
app_bar.addOnOffsetChangedListener(appBarStateChangeListener)
}
private fun setToolbar() {
setSupportActionBar(toolbar)
toolbar_layout.title = title
toolbar.setOnClickListener {
custom_scrollView.scrollable = true
app_bar.setExpanded(true, true)
}
}
private val appBarStateChangeListener: AppBarStateChangeListener =
object : AppBarStateChangeListener() {
override fun onStateChanged(appBarLayout: AppBarLayout, state: State) {
when (state) {
State.EXPANDED -> custom_scrollView.scrollable = true
State.COLLAPSED -> custom_scrollView.scrollable = false
State.IDLE -> {}
}
}
}
}
app_bar에 addOnOffsetChangedListener()로 앞서 만들어준 AppBarStateChangeListener를 추가해준다. AppBarLayout의 상태가 EXPANDED가 되면 CustomScrollView의 스크롤 상태를 true로 설정해주고, COLLAPSED가 되면 false로 설정해준다.
Fragment, fragment.xml
class BlankFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_blank, container, false)
}
}
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment">
<ScrollView
android:id="@+id/scrollView"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:text="@string/large_text" />
</ScrollView>
</FrameLayout>
FrameLayout에 들어갈 Fragment를 설정해준다. 레이아웃 안에는 ScrollView가 들어있다.
결과 화면
'Android' 카테고리의 다른 글
[Android] Custom Calendar 만들기 - Infinite ViewPager 구현 (2) | 2020.11.27 |
---|---|
[Android] FlexboxLayout를 활용한 Chip EditText 만들기 (0) | 2020.11.19 |
[Android] ViewTreeObserver란? - View가 그려지는 시점 알아내기 (0) | 2020.09.19 |
[Android] ImageView에 색상 넣기 (ColorFilter vs Tint) (2) | 2020.09.11 |
[Android] RxKotlin과 Retrofit2를 사용해 Github api 정보 가져오기 (1) | 2020.08.30 |