Skip to content

Commit 17cb339

Browse files
committed
update docs
1 parent 4fe3000 commit 17cb339

File tree

9 files changed

+222
-10
lines changed

9 files changed

+222
-10
lines changed

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,11 @@ It is driven by a [StateMachine](https://github.com/Tinder/StateMachine)
2828

2929
<div>
3030

31-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/ins.gif" width="160" height="346" />
32-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/wechat.gif" width="160" height="346" />
33-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/moment.gif" width="160" height="346" />
34-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/tab.gif" width="160" height="346" />
35-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/nestedscroll.gif" width="160" height="346" />
31+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/ins.gif" width="160" height="346" />
32+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/wechat.gif" width="160" height="346" />
33+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/moment.gif" width="160" height="346" />
34+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/tab.gif" width="160" height="346" />
35+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/nestedscroll.gif" width="160" height="346" />
3636

3737
</div>
3838

README_CN.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@
2424

2525
<div>
2626

27-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/ins.gif" width="160" height="346" />
28-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/wechat.gif" width="160" height="346" />
29-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/moment.gif" width="160" height="346" />
30-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/tab.gif" width="160" height="346" />
31-
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/doc/nestedscroll.gif" width="160" height="346" />
27+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/ins.gif" width="160" height="346" />
28+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/wechat.gif" width="160" height="346" />
29+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/moment.gif" width="160" height="346" />
30+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/tab.gif" width="160" height="346" />
31+
<img src="https://github.com/s1rius/android-nest-scroll-ptr/blob/master/images/nestedscroll.gif" width="160" height="346" />
3232

3333
</div>
3434

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# 使用 Compose 实现下拉刷新控件
2+
3+
### 定义下拉刷新状态
4+
在 Compose 中,状态是基础,所有的 UI 都是对当前状态的一种展示,状态改变驱动 UI 改变。根据之前使用 View 实现下拉刷新的经验,给出状态的定义
5+
6+
```kotlin
7+
@Stable
8+
class NSPtrState(
9+
val contentInitPosition: Dp = 0.dp,// 初始位置
10+
val contentRefreshPosition: Dp = 54.dp,// 刷新位置
11+
val pullFriction: Float = 0.56f,// 拖动的摩擦力参数
12+
coroutineScope: CoroutineScope,
13+
onRefresh: (suspend (NSPtrState) -> Unit)? = null, // 触发刷新的回调
14+
) {
15+
...
16+
// 当前 content view 所处的位置
17+
var contentPositionPx: Float by mutableStateOf(0f)
18+
19+
// 最近一次状态转变的对象
20+
var lastTransition: StateMachine.Transition<State, Event, SideEffect>? = null
21+
22+
// 内部用来处理状态转变逻辑的状态机
23+
private val _stateMachine = createNSPtrFSM {}
24+
25+
// 当前的下拉刷新状态
26+
var state: State by mutableStateOf(_stateMachine.state)
27+
28+
// 触发状态转变事件
29+
fun dispatchPtrEvent(event: Event) {
30+
_stateMachine.transition(event)
31+
}
32+
33+
// content view 的位移方法
34+
private suspend fun animateContentTo(
35+
value: Float,
36+
animationSpec: AnimationSpec<Float> = SpringSpec()
37+
) {
38+
// 动画实现
39+
}
40+
...
41+
}
42+
43+
```
44+
45+
46+
### 了解 Compose 中实现自定义控件的流程
47+
一般自定义 ViewGroup 控件在 View 中的实现步骤分为以下的几个步骤
48+
49+
1. 继承 ViewGroup
50+
2. 重写 onMeasure 和 onLayout 方法
51+
52+
这两个步骤在 Compose 中都有如下的对应
53+
54+
1. 创建 Composable 方法,并在方法中调用 Layout() 方法
55+
56+
参照 Android codelabs 的[示例](https://developer.android.com/codelabs/jetpack-compose-layouts#6), 创建 NSPtrLayout 的 Composable 方法
57+
58+
```kotlin
59+
@Composable
60+
fun NSPtrLayout(
61+
modifier: Modifier,
62+
content: @Composable () -> Unit
63+
) {
64+
...
65+
Layout(
66+
modifier: Modifier = Modifier,
67+
measurePolicy: MeasurePolicy,
68+
content: @Composable () -> Unit,
69+
)
70+
...
71+
}
72+
```
73+
74+
追踪 Layout 代码块的具体实现
75+
76+
```kotlin
77+
@Composable inline fun Layout(
78+
content: @Composable () -> Unit, // 子控件代码块
79+
modifier: Modifier = Modifier, // 布局修饰符
80+
measurePolicy: MeasurePolicy // 测量和布局策略符
81+
) {
82+
val density = LocalDensity.current
83+
val layoutDirection = LocalLayoutDirection.current
84+
ReusableComposeNode<ComposeUiNode, Applier<Any>>(
85+
factory = ComposeUiNode.Constructor,
86+
update = {
87+
set(measurePolicy, ComposeUiNode.SetMeasurePolicy)
88+
set(density, ComposeUiNode.SetDensity)
89+
set(layoutDirection, ComposeUiNode.SetLayoutDirection)
90+
},
91+
skippableUpdate = materializerOf(modifier),
92+
content = content
93+
)
94+
}
95+
```
96+
获取了当前的像素密度,布局方向。最后用 LayoutNodes 去创建一个树形结构实现 UI。
97+
忽略掉底层的实现细节,实现控件的测量和布局的模版方法。
98+
99+
2. 实现测量和布局
100+
101+
在 View 的系统中,我们需要重写两个方法,onMeasure 和 onLayout 来实现这个过程。在 Compose 中,我们需要自定义 MeasurePolicy 。
102+
103+
```kotlin
104+
internal fun ptrMeasurePolicy() = MeasurePolicy { measurables, constraints ->
105+
if (measurables.isEmpty()) {
106+
return@MeasurePolicy layout(constraints.minWidth, constraints.minHeight) {}
107+
} else {
108+
val layoutWidth: Int = constraints.maxWidth
109+
val layoutHeight: Int = constraints.maxHeight
110+
val placeables = arrayOfNulls<Placeable>(measurables.size)
111+
112+
measurables.forEachIndexed { index, measurable ->
113+
placeables[index] = measurable.measure(constraints)
114+
// 测量逻辑
115+
}
116+
117+
layout(layoutWidth, layoutHeight) {
118+
placeables.forEachIndexed { index, placeable ->
119+
// layout 逻辑
120+
placeable.place(x, y)
121+
}
122+
}
123+
}
124+
}
125+
```
126+
127+
measurables 是所有子节点的约束合集,通过 measure 确定具体的宽高,返回 Placeable。对应 View 的 onMeasure() 流程就执行完了。
128+
MeasurePolicy 的 measure 方法需要一个 MeasureResult 的返回值,需要调用 layout 方法。
129+
Layout 方法确定子节点的布局位置,返回 MeasureResult
130+
131+
```kotlin
132+
fun layout(
133+
width: Int,
134+
height: Int,
135+
alignmentLines: Map<AlignmentLine, Int> = emptyMap(),
136+
placementBlock: Placeable.PlacementScope.() -> Unit
137+
) = object : MeasureResult {
138+
override val width = width
139+
override val height = height
140+
override val alignmentLines = alignmentLines
141+
override fun placeChildren() {
142+
Placeable.PlacementScope.executeWithRtlMirroringValues(
143+
width,
144+
layoutDirection,
145+
placementBlock
146+
)
147+
}
148+
}
149+
```
150+
151+
我们需要自定义下拉控件的位置,需要实现这个 placementBlock 函数,可以拿到之前在 measure 中存储的所有 Placeable 对象,遍历进行布局。
152+
153+
```kotlin
154+
layout(layoutWidth, layoutHeight) {
155+
placeables.forEachIndexed { index, placeable ->
156+
placeable.place(x, y)
157+
}
158+
}
159+
```
160+
161+
到这里,基本的 Compose 中的自定义 Layout 的流程就结束了。
162+
163+
### 手势事件的分发和处理
164+
165+
首先明确目的,在这里我们只关心 down 事件和 up/cancel 事件,不对手势事件做拦截处理。
166+
167+
Compose 中的手势事件的源头是 AndroidComposeView 的 dispatchTouchEvent 方法,通过 PointerInputEventProcessor 桥接,最终调用 Modifier 中定义的 PointerInputScope 的 suspend 扩展方法。先不考虑实现原理,实现模版代码。
168+
169+
```kotlin
170+
suspend fun PointerInputScope.detectDownAndUp(
171+
onDown: (Offset) -> Unit,
172+
onUpOrCancel: (Offset?) -> Unit
173+
) {
174+
forEachGesture {
175+
awaitPointerEventScope {
176+
// 首次 down 事件触发
177+
awaitFirstDown(false).also {
178+
onDown(it.position)
179+
}
180+
181+
val up = waitForUpOrCancel()
182+
// 所有的手指都离开屏幕,最后触发的 up 或者 cancel 事件
183+
onUpOrCancel.invoke(up?.position)
184+
}
185+
}
186+
}
187+
```
188+
189+
这样,我们下拉刷新所需要处理的手势事件就结束了
190+
191+
### 位移动画的实现
192+
Compose 中的 SuspendAnimation.kt 提供了类似 ValueAnimtor 的实现,下面实现了 NSPtrState 中对 content view 位置属性的动画
193+
194+
```kotlin
195+
private suspend fun animateContentTo(
196+
value: Float,
197+
animationSpec: AnimationSpec<Float> = SpringSpec()
198+
) {
199+
var prevValue = 0f // 存储上一个动画的值
200+
animate(
201+
0f,
202+
(value - contentPositionPx),
203+
animationSpec = animationSpec
204+
) { currentValue, _ ->
205+
// 获取差值,加给目标值
206+
contentPositionPx += currentValue - prevValue
207+
prevValue = currentValue
208+
}
209+
}
210+
```
211+
212+
本文简单的叙述了下拉刷新控件的实现步骤,后续还需要将这些步骤进行组合,在布局中响应状态中的字段。具体的实现细节可以移步[源码](https://github.com/s1rius/android-nest-scroll-ptr)
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)