들어가며
본 글에서는 중첩된 CoordinatorLayout
을 사용하는 경우 ChildCoordinator의 unconsumed scroll event를 ParentCoordinator로 전파하는 방법에대하여 정리
Nested ScrollEvent 전파
짧게 정리하자면 NestedScrollingChild
에서 발생한 scroll event NestedScrollingParent
로 전파되며 이때 전달되는 consumed, unconsumed x,y값을 활용하여 nested scroll을 구현한다. 이때 NestedScrollingParent
, Child
interface가 사용되며 compatibility를 위해 Parent, Parent2, Child1, Child2, Child3등의 interface가 존재한다.
예시
흔히 볼수있는 CoordinatorLayout
구조이며 위 구조에서 RecyclerView(NestedScrollingChild)
의 스크롤 이벤트 발생시 CoordinatorLayout(NestedScrollingParent)
으로 이벤트가 전파되며 CoordinatorLayout
에서는 포함된 Chlid View의 Behavior
에 해당 이벤트를 전파한다.
1
2
3
4
5
6
7
8
9
10
11
//in CoordinatorLayout.onStartNestedScroll
final LayoutParams lp = (LayoutParams) view.getLayoutParams();
final Behavior viewBehavior = lp.getBehavior();
if (viewBehavior != null) {
final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child,
target, axes, type);
handled |= accepted;
lp.setNestedScrollAccepted(type, accepted);
} else {
lp.setNestedScrollAccepted(type, false);
}
NestedCoordinatorLayout 문제
만약 위처럼 CoordinatorLayout이 중첩된 Layout을 구현한다면 inner Coordinator의 스크롤이 발생해도 outer Coordinator에는 스크롤이 전파되지 않는 문제. 위 예시에서는 이해를 돕기위해 AppBar
, BottomNavigation
으로 명시했으나 CoordinatorLayout.Behavior
을 구현한 많은 컴포넌트가 포함되는 경우가 일반적이다.
해결방법
앞서 정리해본 내용을 토대로 생각해보면, CoordinatorLayout은 NestedScrollingParent
이므로 내부의 Behaviour
에는 스크롤 이벤트가 정상적으로 전파되나 부모 CoordinatorLayout
에는 전파되지 않는다. 여기서 자식 CoordinatorLayout
에서 NestedScrollingChild
를 구현하면 부모 CoordinatorLayout
에게 이벤트를 전파 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
class NestedCoordinatorLayout @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : CoordinatorLayout(context, attrs), NestedScrollingChild3 {
private val helper = NestedScrollingChildHelper(this)
init {
isNestedScrollingEnabled = true
}
override fun isNestedScrollingEnabled(): Boolean = helper.isNestedScrollingEnabled
override fun setNestedScrollingEnabled(enabled: Boolean) {
helper.isNestedScrollingEnabled = enabled
}
override fun hasNestedScrollingParent(type: Int): Boolean =
helper.hasNestedScrollingParent(type)
override fun hasNestedScrollingParent(): Boolean = helper.hasNestedScrollingParent()
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int): Boolean {
val superResult = super.onStartNestedScroll(child, target, axes, type)
return startNestedScroll(axes, type) || superResult
}
override fun onStartNestedScroll(child: View, target: View, axes: Int): Boolean {
val superResult = super.onStartNestedScroll(child, target, axes)
return startNestedScroll(axes) || superResult
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
val superConsumed = intArrayOf(0, 0)
super.onNestedPreScroll(target, dx, dy, superConsumed, type)
val thisConsumed = intArrayOf(0, 0)
dispatchNestedPreScroll(dx, dy, consumed, null, type)
consumed[0] = superConsumed[0] + thisConsumed[0]
consumed[1] = superConsumed[1] + thisConsumed[1]
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
val superConsumed = intArrayOf(0, 0)
super.onNestedPreScroll(target, dx, dy, superConsumed)
val thisConsumed = intArrayOf(0, 0)
dispatchNestedPreScroll(dx, dy, consumed, null)
consumed[0] = superConsumed[0] + thisConsumed[0]
consumed[1] = superConsumed[1] + thisConsumed[1]
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type)
super.onNestedScroll(
target,
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
type,
consumed
)
}
override fun onNestedScroll(
target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int,
dyUnconsumed: Int, type: Int
) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null, type)
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, null)
}
override fun onStopNestedScroll(target: View, type: Int) {
super.onStopNestedScroll(target, type)
stopNestedScroll(type)
}
override fun onStopNestedScroll(target: View) {
super.onStopNestedScroll(target)
stopNestedScroll()
}
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
val superResult = super.onNestedPreFling(target, velocityX, velocityY)
return dispatchNestedPreFling(velocityX, velocityY) || superResult
}
override fun onNestedFling(
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean
): Boolean {
val superResult = super.onNestedFling(target, velocityX, velocityY, consumed)
return dispatchNestedFling(velocityX, velocityY, consumed) || superResult
}
override fun startNestedScroll(axes: Int, type: Int): Boolean =
helper.startNestedScroll(axes, type)
override fun startNestedScroll(axes: Int): Boolean = helper.startNestedScroll(axes)
override fun stopNestedScroll(type: Int) {
helper.stopNestedScroll(type)
}
override fun stopNestedScroll() {
helper.stopNestedScroll()
}
override fun dispatchNestedScroll(
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
offsetInWindow: IntArray?,
type: Int,
consumed: IntArray
) {
helper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type,
consumed
)
}
override fun dispatchNestedScroll(
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int,
offsetInWindow: IntArray?, type: Int
): Boolean = helper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow,
type
)
override fun dispatchNestedScroll(
dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int,
dyUnconsumed: Int, offsetInWindow: IntArray?
): Boolean = helper.dispatchNestedScroll(
dxConsumed,
dyConsumed,
dxUnconsumed,
dyUnconsumed,
offsetInWindow
)
override fun dispatchNestedPreScroll(
dx: Int, dy: Int, consumed: IntArray?,
offsetInWindow: IntArray?, type: Int
): Boolean = helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
override fun dispatchNestedPreScroll(
dx: Int,
dy: Int,
consumed: IntArray?,
offsetInWindow: IntArray?
): Boolean =
helper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float): Boolean =
helper.dispatchNestedPreFling(velocityX, velocityY)
override fun dispatchNestedFling(
velocityX: Float,
velocityY: Float,
consumed: Boolean
): Boolean =
helper.dispatchNestedFling(velocityX, velocityY, consumed)
}