0%

封装一个关注按钮

最近在写一个基于react-native的Mastodon客户端。里面有一个关注用户的按钮,这里来记录一下自己是如何一步步修改并完善这个按钮的功能的。

需求分析

这是一个和Twitter里类似的关注按钮,通过点击来关注取关对方。在发起点击动作后,需要根据当前与对方的关系,来确定到底是发起关注该用户的请求、还是取关该用户的请求。那么可以确定的需求如下:

  • 根据与对方用户的关系,按钮应该显示不一样的状态
  • 根据与对方用户的关系,点击按钮应该发起不一样的请求

FollowButton

通过分析可以知道,这个Button在不同状态下有不同的表现方式,那么首先先定义出Button的几个状态,再基于这几个状态分别处理。

1
2
3
4
5
enum FollowButtonStatus {
UnFollow, // 关注
Following, // 正在关注
BothFollow, // 互相关注
}

再根据这三个状态,实现一个Button,类似于下面这样:

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
const FollowButton: React.FC<FollowButtonProps> = (props) => {
return (
<TouchableOpacity style={[styles.main, styles.out_view]}>
<Text style={styles.text_style}>
关注
</Text>
</TouchableOpacity>
)
}

const styles = StyleSheet.create({
main: {
padding: 15,
justifyContent: 'center',
alignItems: 'center',
height: 40,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
width: 100,
},
out_view: {
backgroundColor: '#fff',
borderColor: '#2593FC'
},
text_style: {
color: '#2593FC',
fontSize: 18,
}
});

目前为止实现了一个基本的Button效果,先手动修改一下代码,体验一下不同状态下这个按钮的表现。通过修改Text以及外层TouchableOpacity的样式可以得到以下效果:

代码 效果

可以看到,三个状态下的按钮仅仅是文字以及样式的改变。那么来创建一个state来表示按钮当前的状态,根据这个状态的变化去显示不同的按钮

1
const [buttonStatus, setButtonStatus] = useState(FollowButtonStatus.UnFollow);

我们定义了一个buttonStatus,并给了一个初始值也就是当FollowButton渲染完成之后,让它显示关注字样

根据不同的状态,可以返回不同的样式去渲染不同的UI

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
const content = useMemo(() => {
switch(true) {
case buttonStatus === FollowButtonStatus.UnFollow: {
return {
buttonText: '关注',
buttonStyle: {
backgroundColor: '#fff',
borderColor: '#2593FC',
},
textStyle: {
color: '#2593FC',
fontSize: 18,
},
}
}
case buttonStatus === FollowButtonStatus.Following: {
return {
buttonText: '正在关注',
buttonStyle: {
backgroundColor: '#2593FC',
borderColor: '#2593FC',
},
textStyle: {
color: '#fff',
fontSize: 16,
},
}
}
case buttonStatus === FollowButtonStatus.BothFollow: {
return {
buttonText: '互相关注',
buttonStyle: {
backgroundColor: '#2593FC',
borderColor: '#2593FC',
},
textStyle: {
color: '#fff',
fontSize: 16,
},
}
}
default: {
return {
buttonText: '关注',
buttonStyle: {
backgroundColor: '#fff',
borderColor: '#2593FC',
},
textStyle: {
color: '#2593FC',
fontSize: 18,
},
}
};
}
}, [buttonStatus]);

相应地修改JSX,去对应地按照样式显示

1
2
3
4
5
<TouchableOpacity style={[styles.main, content.buttonStyle]}>
<Text style={content.textStyle}>
{content.buttonText}
</Text>
</TouchableOpacity>

处理点击事件

现在我们要根据点击按钮时候,按钮本身的状态决定它发起什么请求

  • 当按钮处于UnFollow时,发起关注请求,请求成功后将按钮修改为Following
  • 当按钮处于Following和BothFollow时,发起取关请求,请求成功后将按钮修改为UnFollow
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const followSomeone = () => {
setTimeout(() => {
setButtonStatus(FollowButtonStatus.Following);
}, 1000);
}

const unFollowSomeone = () => {
setTimeout(() => {
setButtonStatus(FollowButtonStatus.UnFollow);
}, 1000);
};

const handleOnPress = useCallback(() => {
if(buttonStatus === FollowButtonStatus.UnFollow) {
followSomeone();
}
if (buttonStatus === FollowButtonStatus.Following || buttonStatus === FollowButtonStatus.BothFollow) {
unFollowSomeone();
}
}, [buttonStatus]);

暂时用一个定时器来模拟接口调用的过程,在请求成功之后通过setButtonStatus修改FollowButton状态

完善请求过程

OK目前为止完成了大半的功能,剩下的都是一些有关用户体验上的修改了。比如在哪里实现关注取关、在发起请求的时候,如何显示Loading状态。

我是这样考虑的:

  1. 首先这个Button在整个应用中出现频率比较高,在各个有关于用户的页面中都有这个Button出现。而且它的功能非常一致,就是用来实现关注取关的,所以关注取关这两个请求,就应该从它的父组件中,移动到组件内部。由组件内部发起请求,由组件内部管理自身的状态,它的父组件仅给它一个初始状态。
  2. 可以看到在发起请求的时候,如果不做任何处理组件就会维持上一个状态直到接口返回了最新的数据。从上一个状态到下一个状态之间,明显缺少一个过渡地状态,即请求中这个状态。一般来说就是在页面中显示一个Loading,鉴于FollowButton的请求是否成功并不直接影响后续操作,那么在App中全屏显示一个Loading就没有必要而且影响用户的使用体验。所以在请求过程中让FollowButton中的文字部分显示一个等待视图。首先为FollowButton再添加一个Requesting状态,并在发起请求的时候将状态修改为Requesting。
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
enum FollowButtonStatus {
UnFollow, // 关注
Following, // 正在关注
BothFollow, // 互相关注
Requesting, // 正在请求
}

const handleOnPress = useCallback(() => {
if(buttonStatus === FollowButtonStatus.UnFollow) {
followSomeone();
}
if (buttonStatus === FollowButtonStatus.Following || buttonStatus === FollowButtonStatus.BothFollow) {
unFollowSomeone();
}

// 发起请求时,将状态修改为Requesting
setButtonStatus(FollowButtonStatus.Requesting);
}, [buttonStatus]);

return (
<TouchableOpacity onPress={handleOnPress} style={[styles.main, content.buttonStyle]}>
{
buttonStatus == FollowButtonStatus.Requesting ?
<ActivityIndicator /> :
<Text style={content.textStyle}>
{content.buttonText}
</Text>
}
</TouchableOpacity>
)

这和我们想要的效果完全一致,在状态与状态之间添加了一个中间状态。就是显得有一点点奇怪,ActivityIndicator它的颜色一直是灰色的,如果可以给它一个颜色就更好了。但究竟给它设置一个什么颜色呢?还有就是当从Following发起请求的时候,按钮突然变成白色显得有些突兀。所以我们需要在Requesting状态下,为FollowButton设置一个全新的样式。

  • 从UnFollow到Following时,Requesting背景颜色应该与UnFollow相同为白色,ActivityIndicator应该为蓝色

  • 从Following到UnFollow时,Requesting背景颜色应该与Following相同为蓝色,ActivityIndicator应该为白色

可以看到Requesting的状态取决于上一个状态,那么首先要通过useRef获取上一个状态

1
2
3
4
5
6
const prevContentRef: any = useRef();
useEffect(() => {
prevContentRef.current = content;
});

const prevCount = prevContentRef.current;

再为ActivityIndicator动态地设置一个颜色

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
const content = useMemo(() => {
// 当点击按钮发起请求的时候,保持Button当前状态,根据当前状态显示出不一样颜色的等待视图
// 如果是第一次渲染,则返回一个默认状态
if(buttonStatus === FollowButtonStatus.Requesting) {
return prevCount || {
buttonText: '请求中',
buttonStyle: {
backgroundColor: '#fff',
borderColor: '#2593FC',
},
textStyle: {
color: '#2593FC',
fontSize: 18,
},
indicatorColor: '#2593FC',
}
}

switch(true) {
case buttonStatus === FollowButtonStatus.UnFollow: {
return {
buttonText: '关注',
buttonStyle: {
backgroundColor: '#fff',
borderColor: '#2593FC',
},
textStyle: {
color: '#2593FC',
fontSize: 18,
},
indicatorColor: '#2593FC',
}
}
case buttonStatus === FollowButtonStatus.Following: {
return {
buttonText: '正在关注',
buttonStyle: {
backgroundColor: '#2593FC',
borderColor: '#2593FC',
},
textStyle: {
color: '#fff',
fontSize: 16,
},
indicatorColor: '#fff',
}
}
case buttonStatus === FollowButtonStatus.BothFollow: {
return {
buttonText: '互相关注',
buttonStyle: {
backgroundColor: '#2593FC',
borderColor: '#2593FC',
},
textStyle: {
color: '#fff',
fontSize: 16,
},
indicatorColor: '#fff',
}
}
default: {
return {
buttonText: '关注',
buttonStyle: {
backgroundColor: '#fff',
borderColor: '#2593FC',
},
textStyle: {
color: '#2593FC',
fontSize: 18,
},
indicatorColor: '#2593FC',
}
};
}
}, [buttonStatus]);

return (
<TouchableOpacity onPress={handleOnPress} style={[styles.main, content.buttonStyle]}>
{
buttonStatus == FollowButtonStatus.Requesting ?
<ActivityIndicator color={content.indicatorColor} /> :
<Text style={content.textStyle}>
{content.buttonText}
</Text>
}
</TouchableOpacity>
)

效果如下:

完成

最后添加上实际的业务场景,点击一个用户之后跳转到该页面,请求当前用户与该用户的关系,之后显示当前的用户关系。所以将初始状态修改为Requesting,最终的效果如下:

全部的代码放在了这里