Skip to content

Commit 45a59e3

Browse files
committed
feat: add the focus requester support
1 parent 472b3e5 commit 45a59e3

29 files changed

+1060
-225
lines changed

README.md

+2-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<br/>
66
<br/>
77
<div align="center">
8-
<img src="https://img.shields.io/static/v1?label=version&message=1.0.4&color=success"/>
8+
<img src="https://img.shields.io/static/v1?label=version&message=1.0.5&color=success"/>
99
<img src="https://img.shields.io/static/v1?label=platform&message=Android&color=green"/> <img src="https://img.shields.io/static/v1?label=platform&message=Desktop&color=blue"/>
1010
</div>
1111
<br/>
@@ -77,11 +77,10 @@ https://github.com/succlz123/AcFun-Client-Multiplatform/releases
7777
- 首页展示,分区内容展示,视频详情展示,UP主投稿视频查看。
7878
- 视频播放,直播,弹幕 (简易弹幕-实验性质),播放功能增强,变速等。
7979
- 搜索,下载。
80-
- Android Phone, Android Pad,Desktop 适配。
80+
- Android Phone, Android Pad, Android TV, Desktop 适配。
8181

8282
## 开发中功能
8383

8484
- 支持播放选集功能。
85-
- Android TV 适配。
8685
- DLNA 投屏。
8786
- 番剧,文章区。

android/build.gradle.kts

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
}
66

77
group = "io.github.succlz123"
8-
version = "1.0.4"
8+
version = "1.0.5"
99

1010
repositories {
1111
mavenCentral()
@@ -27,8 +27,8 @@ android {
2727
applicationId = "org.succlz123.app.acfun"
2828
minSdk = 21
2929
targetSdk = 30
30-
versionCode = 4
31-
versionName = "1.0.4"
30+
versionCode = 5
31+
versionName = "1.0.5"
3232

3333
resourceConfigurations += mutableSetOf("en", "zh")
3434
}

build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
group = "io.github.succlz123"
2-
version = "1.0.4"
2+
version = "1.0.5"
33

44
allprojects {
55
repositories {

desktop/build.gradle.kts

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66
}
77

88
group = "io.github.succlz123"
9-
version = "1.0.4"
9+
version = "1.0.5"
1010

1111
kotlin {
1212
jvm {
@@ -42,7 +42,7 @@ compose.desktop {
4242
iconFile.set(project.file("ic_acfun.png"))
4343
}
4444
packageName = "AcFun"
45-
packageVersion = "1.0.4"
45+
packageVersion = "1.0.5"
4646
copyright = "Copyright © 2022"
4747

4848
modules("java.sql", "jdk.unsupported")

shared/build.gradle.kts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ plugins {
88
}
99

1010
group = "io.github.succlz123"
11-
version = "1.0.4"
11+
version = "1.0.5"
1212

1313
kotlin {
1414
android()

shared/src/commonMain/kotlin/org/succlz123/app/acfun/SharedApp.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fun SharedApp() {
3939
Box(modifier = Modifier.fillMaxSize()) {
4040
ScreenHost(screenNavigator = screenNavigator, rootScreenName = Manifest.MainScreen) {
4141
groupScreen(screenName = (Manifest.MainScreen)) {
42-
MainScreen(modifier = Modifier.fillMaxSize())
42+
MainScreen()
4343
}
4444
groupScreen(screenName = (Manifest.VideoDetailScreen)) {
4545
VideoDetailScreen()

shared/src/commonMain/kotlin/org/succlz123/app/acfun/theme/ColorResource.kt

+3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@ package org.succlz123.app.acfun.theme
33
import androidx.compose.ui.graphics.Color
44

55
object ColorResource {
6+
val white = Color(0xFFFFFFFF)
67
val background = Color(0xFFF6F7F8)
78
val acRed = Color(0xFFFF4444)
9+
val acRed30 = Color(0x4dFF4444)
810
val divider = Color(0xFFF1F2F3)
911
val text = Color(0xFF000000)
1012
val subText = Color(0xFF9499A0)
1113
val border = Color(0xFFF1F2F3)
1214

15+
val black = Color(0xFF000000)
1316
val black20 = Color(0x33000000)
1417
val black30 = Color(0x4d000000)
1518
val black60 = Color(0x99000000)

shared/src/commonMain/kotlin/org/succlz123/app/acfun/ui/area/AreaContentScreen.kt

+12
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import org.succlz123.app.acfun.base.AcBackButton
1919
import org.succlz123.app.acfun.base.LoadingFailView
2020
import org.succlz123.app.acfun.base.LoadingView
2121
import org.succlz123.app.acfun.theme.ColorResource
22+
import org.succlz123.app.acfun.ui.main.GlobalFocusViewModel
2223
import org.succlz123.app.acfun.ui.main.tab.item.MainHomeContentItem
2324
import org.succlz123.lib.click.noRippleClickable
2425
import org.succlz123.lib.screen.LocalScreenNavigator
@@ -47,6 +48,13 @@ fun AreaContentScreen() {
4748
viewModel.getData(id)
4849
}
4950

51+
val focusVm = viewModel(GlobalFocusViewModel::class) {
52+
GlobalFocusViewModel()
53+
}
54+
LaunchedEffect(Unit) {
55+
focusVm.curFocusRequesterParent.value = viewModel.contentFocusParent
56+
}
57+
5058
Box(modifier = Modifier.fillMaxSize().background(Color.White)) {
5159
val state = viewModel.areaVideosState.collectAsState().value
5260
when (state) {
@@ -109,6 +117,10 @@ fun AreaContentScreen() {
109117
} else {
110118
MainHomeContentItem(result = ScreenResult.Success(acContentList),
111119
isExpandedScreen = isExpandedScreen,
120+
121+
thisRequester = viewModel.contentFocusParent,
122+
otherRequester = null,
123+
112124
onRefresh = {
113125
viewModel.getData(
114126
id,

shared/src/commonMain/kotlin/org/succlz123/app/acfun/ui/area/AreaContentViewModel.kt

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.succlz123.app.acfun.ui.area
22

3+
import androidx.compose.ui.focus.FocusRequester
34
import kotlinx.collections.immutable.ImmutableList
45
import kotlinx.collections.immutable.toImmutableList
56
import kotlinx.coroutines.Dispatchers
@@ -40,6 +41,8 @@ class AreaContentViewModel : ScreenPageViewModel() {
4041

4142
val rankSelectIndex: MutableStateFlow<Int> = MutableStateFlow(0)
4243

44+
val contentFocusParent = FocusRequester()
45+
4346
init {
4447
page = 1
4548
}

shared/src/commonMain/kotlin/org/succlz123/app/acfun/ui/detail/VideoDetailScreen.kt

+126-27
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ import androidx.compose.runtime.*
1313
import androidx.compose.ui.Alignment
1414
import androidx.compose.ui.Modifier
1515
import androidx.compose.ui.draw.clip
16+
import androidx.compose.ui.focus.FocusRequester
17+
import androidx.compose.ui.focus.focusRequester
18+
import androidx.compose.ui.focus.focusTarget
19+
import androidx.compose.ui.focus.onFocusChanged
1620
import androidx.compose.ui.graphics.Color
1721
import androidx.compose.ui.unit.dp
1822
import androidx.compose.ui.unit.sp
@@ -23,11 +27,16 @@ import org.succlz123.app.acfun.base.AcBackButton
2327
import org.succlz123.app.acfun.base.LoadingFailView
2428
import org.succlz123.app.acfun.base.LoadingView
2529
import org.succlz123.app.acfun.theme.ColorResource
30+
import org.succlz123.app.acfun.ui.main.GlobalFocusViewModel
2631
import org.succlz123.lib.click.noRippleClickable
2732
import org.succlz123.lib.common.getPlatformName
2833
import org.succlz123.lib.filedownloader.core.DownloadRequest
2934
import org.succlz123.lib.filedownloader.core.DownloadStateType
3035
import org.succlz123.lib.filedownloader.core.FileDownLoader
36+
import org.succlz123.lib.focus.FocusNode
37+
import org.succlz123.lib.focus.notAllowMove
38+
import org.succlz123.lib.focus.onFocusKeyEventMove
39+
import org.succlz123.lib.focus.onFocusParent
3140
import org.succlz123.lib.image.AsyncImageUrlMultiPlatform
3241
import org.succlz123.lib.screen.LocalScreenNavigator
3342
import org.succlz123.lib.screen.LocalScreenRecord
@@ -54,6 +63,7 @@ fun VideoDetailScreen() {
5463
LaunchedEffect(Unit) {
5564
viewModel.getDetail(acContent)
5665
}
66+
5767
Box(modifier = Modifier.fillMaxSize().background(Color.White).noRippleClickable {
5868
screenNavigation.cancelPopupWindow()
5969
}) {
@@ -74,7 +84,9 @@ fun VideoDetailScreen() {
7484
is ScreenResult.Success -> {
7585
val vc = videoContent.invoke()
7686
Box(contentAlignment = Alignment.Center) {
77-
videoDetailContent(acContent, vc, viewModel)
87+
videoDetailContent(
88+
acContent, vc, viewModel, viewModel.userSpaceFocusParent, viewModel.episodeFocusParent
89+
)
7890

7991
val showPlayerLoading = remember { mutableStateOf(false) }
8092
if (showPlayerLoading.value) {
@@ -113,7 +125,13 @@ fun VideoDetailScreen() {
113125
}
114126

115127
@Composable
116-
fun videoDetailContent(acContent: AcContent, vContent: VideoContent, viewModel: VideoDetailViewModel) {
128+
fun videoDetailContent(
129+
acContent: AcContent,
130+
vContent: VideoContent,
131+
viewModel: VideoDetailViewModel,
132+
userSpaceFocusParent: FocusRequester,
133+
episodeFocusParent: FocusRequester
134+
) {
117135
val screenNavigation = LocalScreenNavigator.current
118136
Column(modifier = Modifier.fillMaxSize().padding(24.dp, 48.dp, 24.dp, 48.dp)) {
119137
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -131,18 +149,50 @@ fun videoDetailContent(acContent: AcContent, vContent: VideoContent, viewModel:
131149
)
132150
}
133151
Spacer(modifier = Modifier.width(16.dp))
134-
Text(
135-
modifier = Modifier.noRippleClickable {
136-
screenNavigation.push(
137-
Manifest.UserSpaceScreen,
138-
screenKey = vContent.user?.id.orEmpty(),
139-
arguments = ScreenArgs.putValue("KEY_USER_NAME", vContent.user?.name)
140-
.putValue("KEY_USER_ID", vContent.user?.id),
141-
pushOptions = PushOptions(
142-
removePredicate = PushOptions.RemoveAnyPredicate(Manifest.UserSpaceScreen)
143-
)
152+
val isFocused = remember { mutableStateOf(false) }
153+
val userSpaceFocusNode = remember { FocusNode(tag = "UserSpace") }
154+
SideEffect {
155+
if (viewModel.currentFocusNode.value == userSpaceFocusNode) {
156+
userSpaceFocusParent.requestFocus()
157+
}
158+
}
159+
Text(modifier = Modifier.onFocusParent(userSpaceFocusParent, "video detail - user space") {
160+
isFocused.value = (it.isFocused)
161+
if (it.isFocused) {
162+
viewModel.currentFocusNode.value = userSpaceFocusNode
163+
}
164+
}.onFocusKeyEventMove(leftCanMove = notAllowMove,
165+
rightCanMove = notAllowMove,
166+
upCanMove = notAllowMove,
167+
downCanMove = {
168+
viewModel.currentFocusNode.value = FocusNode(tag = "Episode")
169+
episodeFocusParent.requestFocus()
170+
false
171+
}).noRippleClickable {
172+
screenNavigation.push(
173+
Manifest.UserSpaceScreen,
174+
screenKey = vContent.user?.id.orEmpty(),
175+
arguments = ScreenArgs.putValue("KEY_USER_NAME", vContent.user?.name)
176+
.putValue("KEY_USER_ID", vContent.user?.id),
177+
pushOptions = PushOptions(
178+
removePredicate = PushOptions.RemoveAnyPredicate(Manifest.UserSpaceScreen)
144179
)
145-
}, text = vContent.user?.name.orEmpty(), fontSize = 20.sp, color = ColorResource.acRed
180+
)
181+
userSpaceFocusParent.requestFocus()
182+
}.background(
183+
if (isFocused.value) {
184+
ColorResource.acRed
185+
} else {
186+
Color.Transparent
187+
}, shape = RoundedCornerShape(6.dp)
188+
).padding(12.dp, 6.dp),
189+
text = vContent.user?.name.orEmpty(),
190+
fontSize = 20.sp,
191+
color = if (isFocused.value) {
192+
Color.White
193+
} else {
194+
ColorResource.acRed
195+
}
146196
)
147197
Spacer(modifier = Modifier.width(16.dp))
148198
Column {
@@ -216,28 +266,77 @@ fun videoDetailContent(acContent: AcContent, vContent: VideoContent, viewModel:
216266
}
217267
val isExpandedScreen = rememberIsWindowExpanded()
218268

269+
val focusVm = viewModel(GlobalFocusViewModel::class) {
270+
GlobalFocusViewModel()
271+
}
272+
LaunchedEffect(Unit) {
273+
if (focusVm.curFocusRequesterParent.value == null) {
274+
viewModel.episodeFocusParent.requestFocus()
275+
}
276+
}
277+
278+
val grid = remember {
279+
if (isExpandedScreen) {
280+
6
281+
} else {
282+
2
283+
}
284+
}
219285
LazyVerticalGrid(
220-
columns = GridCells.Fixed(
221-
if (isExpandedScreen) {
222-
6
223-
} else {
224-
2
225-
}
226-
),
227-
modifier = Modifier.fillMaxSize(),
286+
columns = GridCells.Fixed(grid),
287+
modifier = Modifier.fillMaxSize().onFocusParent(episodeFocusParent, "video detail - episode")
288+
.onFocusKeyEventMove(upCanMove = {
289+
if (viewModel.currentFocusNode.value.index < grid) {
290+
viewModel.currentFocusNode.value = FocusNode(tag = "UserSpace")
291+
userSpaceFocusParent.requestFocus()
292+
}
293+
true
294+
}),
228295
contentPadding = PaddingValues(top = 12.dp, bottom = 12.dp),
229296
verticalArrangement = Arrangement.spacedBy(12.dp),
230297
horizontalArrangement = Arrangement.spacedBy(12.dp)
231298
) {
232299
itemsIndexed(vContent.videoList.orEmpty()) { index, item ->
300+
301+
val focusRequester = remember { FocusRequester() }
302+
val episodeFocusNode = remember { FocusNode(tag = "Episode", index = index) }
303+
304+
val curParentFocused = focusVm.curFocusRequesterParent.collectAsState()
305+
if (curParentFocused.value == episodeFocusParent && (viewModel.currentFocusNode.value == episodeFocusNode)) {
306+
SideEffect {
307+
focusRequester.requestFocus()
308+
}
309+
}
310+
311+
val isFocused = viewModel.currentFocusNode.collectAsState().value == episodeFocusNode
312+
233313
Box(
234-
modifier = Modifier.weight(1f).height(52.dp).clip(MaterialTheme.shapes.medium)
235-
.background(ColorResource.background)
314+
modifier = Modifier.weight(1f).height(52.dp).clip(MaterialTheme.shapes.medium).background(
315+
if (isFocused) {
316+
ColorResource.acRed
317+
} else {
318+
ColorResource.background
319+
}
320+
)
236321
) {
237-
Box(modifier = Modifier.align(Alignment.Center).noRippleClickable {
238-
viewModel.play(acContent, index + 1)
239-
}, contentAlignment = Alignment.Center) {
240-
Text(text = (index + 1).toString(), style = MaterialTheme.typography.h3)
322+
Box(modifier = Modifier.align(Alignment.Center).fillMaxSize().padding(0.dp, 0.dp, 32.dp, 0.dp)
323+
.focusRequester(focusRequester).onFocusChanged {
324+
if (it.isFocused) {
325+
viewModel.currentFocusNode.value = FocusNode(tag = "Episode", index = index)
326+
}
327+
}.focusTarget().noRippleClickable {
328+
viewModel.play(acContent, index + 1)
329+
}, contentAlignment = Alignment.Center
330+
) {
331+
Text(
332+
text = (index + 1).toString(),
333+
style = MaterialTheme.typography.h3,
334+
color = if (isFocused) {
335+
ColorResource.white
336+
} else {
337+
ColorResource.black
338+
}
339+
)
241340
}
242341
PopupWindowLayout(modifier = Modifier.align(Alignment.CenterEnd), displayContent = {
243342
Card(

shared/src/commonMain/kotlin/org/succlz123/app/acfun/ui/detail/VideoDetailViewModel.kt

+8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package org.succlz123.app.acfun.ui.detail
22

3+
import androidx.compose.ui.focus.FocusRequester
34
import kotlinx.coroutines.Dispatchers
45
import kotlinx.coroutines.flow.MutableSharedFlow
56
import kotlinx.coroutines.flow.MutableStateFlow
@@ -12,6 +13,7 @@ import org.succlz123.app.acfun.api.bean.AcContent
1213
import org.succlz123.app.acfun.api.bean.PlayList
1314
import org.succlz123.app.acfun.api.bean.VideoContent
1415
import org.succlz123.app.acfun.danmaku.DanmakuBean
16+
import org.succlz123.lib.focus.FocusNode
1517
import org.succlz123.lib.network.HttpX
1618
import org.succlz123.lib.result.screenResultDataNone
1719
import org.succlz123.lib.screen.result.ScreenResult
@@ -31,6 +33,12 @@ class VideoDetailViewModel : ScreenViewModel() {
3133

3234
private var currentPos = 1
3335

36+
val userSpaceFocusParent = FocusRequester()
37+
38+
val episodeFocusParent = FocusRequester()
39+
40+
var currentFocusNode = MutableStateFlow(FocusNode(tag = "Episode"))
41+
3442
override fun onCleared() {
3543
super.onCleared()
3644
currentPos = 1

0 commit comments

Comments
 (0)