项目页面的实现
- 前言
- 获取数据
- ScrollableTabRow实现顶部滑动菜单
- ScrollableTabRow属性解析
- 将ScrollableTabRow和HorizontalPager进行绑定
- 根据各种属性设置样式
- 循环添加tabs子项元素
- HorizontalPager 实现页面数据列表
- 列表样式
- 使用HorizontalPager加载页面
- Compose中Webview的使用
- AndroidView的属性
- 使用Webview
- 源码地址
前言
之前已经实现首页和分类页面,今天来实现项目页面,主要是一个顶部的滑动菜单,和下面滑动的pager,以及点击item跳转webview打开链接;先来看一下效果图,因为手机崩掉了,所以用一张小程序的效果图代替,大致一样。
获取数据
在viewmodel中获取数据,顶部的tab菜单栏直接获取,底部数据栏在选中的tab发生变化时要重新获取。
class ProjectViewModel : ViewModel() {
private var _treeList = MutableLiveData(listOf<TreeEntity>())
val treeList = _treeList
fun getProjectTreeList(){
NetWork.service.getProjectTreeList().enqueue(object : Callback<BaseResult<List<TreeEntity>>>{
override fun onResponse(
call: Call<BaseResult<List<TreeEntity>>>,
response: Response<BaseResult<List<TreeEntity>>>) {
response.body()?.let {
_treeList.value = it.data
}
}
override fun onFailure(call: Call<BaseResult<List<TreeEntity>>>, t: Throwable) {
}
})
}
private var _treeChild = MutableLiveData<TreeChildEntity>()
val treeChild:MutableLiveData<TreeChildEntity> = _treeChild
fun getTreeChildList(cid:Int){
NetWork.service.getProjectTreeChildList(cid).enqueue(object : Callback<BaseResult<TreeChildEntity>>{
override fun onResponse(
call: Call<BaseResult<TreeChildEntity>>,
response: Response<BaseResult<TreeChildEntity>>) {
response.body()?.let {
_treeChild.value = it.data
}
}
override fun onFailure(call: Call<BaseResult<TreeChildEntity>>, t: Throwable) {
}
})
}
val _isToWebview: MutableLiveData<Boolean> = MutableLiveData(false)
val webviewUrl: MutableLiveData<String> = MutableLiveData("")
init {
getProjectTreeList()
}
}
点击tab重新获取列表数据
val dataList by pVM.treeChild.observeAsState()
LaunchedEffect(key1 = pageIndex, block = {
pVM.getTreeChildList(cid = cid)
})
数据已经请求到了,接下来就是页面的绘制。
ScrollableTabRow实现顶部滑动菜单
使用TabRow可以实现一个菜单栏,但是不能滑动,这里官方推出了ScrollableTabRow组件来实现可滑动的菜单栏,在菜单栏有很多的情况下可以使用此组件来实现。
ScrollableTabRow属性解析
先来了解一下ScrollableTabRow的属性,方便在项目中使用:
@Composable
fun ScrollableTabRow(
selectedTabIndex: Int,
modifier: Modifier = Modifier,
backgroundColor: Color = MaterialTheme.colors.primarySurface,
contentColor: Color = contentColorFor(backgroundColor),
edgePadding: Dp = TabRowDefaults.ScrollableTabRowPadding,
indicator: @Composable (tabPositions: List<TabPosition>) -> Unit = @Composable { tabPositions ->
TabRowDefaults.Indicator(
Modifier.tabIndicatorOffset(tabPositions[selectedTabIndex])
)
},
divider: @Composable () -> Unit = @Composable {
TabRowDefaults.Divider()
},
tabs: @Composable () -> Unit
)
- selectedTabIndex 当前选中的tabitem下标
- modifier .
- backgroundColor 背景颜色
- contentColor 内容颜色
- edgePadding 左边边距,默认是有52.dp的边距,所以左边和右边看起来就会离屏幕两侧很远
- indicator 设置滑动条的属性,默认是白色
- divider 底部分割线
- tabs tab元素集合
将ScrollableTabRow和HorizontalPager进行绑定
在代码中使用ScrollableTabRow,在使用之前我们需要获取HorizontalPager的页面状态,毕竟菜单栏和页面是要进行绑定的,所以用HorizontalPager的页面状态来进行绑定:
val pagerState = rememberPagerState()//记录页面状态
selectedTabIndex = pagerState.currentPage,//将页面和tab进行绑定
pagerState.scrollToPage(index)//点击tab滑动到指定pager
根据各种属性设置样式
ScrollableTabRow(
selectedTabIndex = pagerState.currentPage,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
backgroundColor = Color.White,
indicator = { positions ->//设置滑动条的属性,默认是白色的
TabRowDefaults.Indicator(
color = Color(114,160,240),
modifier = Modifier
.tabIndicatorOffset(positions[pagerState.currentPage])
.width(10.dp)
)
},
divider = {//设置底部的分割线
Box(Modifier
.fillMaxWidth()
.height(2.dp)
.background(Color.White)) {
}
},edgePadding = 0.dp) {
//tasb
}
循环添加tabs子项元素
if (treeList !== null && treeList?.size !== 0){
treeList!!.forEachIndexed { index, treeEntity ->
Tab(
text = {
Text(
text = treeEntity.name,
color = if (index == pagerState.currentPage) Color(114,160,240) else Color.Black)
}, //标签名
selected = pagerState.currentPage == index, //是否选中
onClick = { //点击事件
CoroutineScope(Dispatchers.Main).launch {
pagerState.scrollToPage(index)
}
},
modifier=Modifier.alpha(0.9f), //透明度
enabled=true, //是否启用
selectedContentColor= Color(114,160,240), //选中的颜色
unselectedContentColor= Color.Black, //未选中的颜色
)
}
}
HorizontalPager 实现页面数据列表
HorizontalPager和ConstraintLayout已经在前面介绍过了,这里就不再介绍了,直接贴列表代码了。
列表样式
列表样式没什么特殊的,就用到了compose 的ConstraintLayout布局,在使用的时候多注意控件id就可以了。
@ExperimentalMaterialApi
@Composable
private fun TreeChildList(viewModel: BottomTabBarViewModel,pageIndex:Int,cid:Int,pVM: ProjectViewModel){
val dataList by pVM.treeChild.observeAsState()
LaunchedEffect(key1 = pageIndex, block = {
pVM.getTreeChildList(cid = cid)
})
if (dataList !== null && dataList?.size !== 0){
initTreeChild(viewModel,dataList!!.datas!!,pVM)
}
}
@ExperimentalMaterialApi
@Composable
private fun initTreeChild(viewModel: BottomTabBarViewModel,dataList:List<TreeChildDetailEntity>,pVM: ProjectViewModel){
LazyColumn(modifier = Modifier.fillMaxSize()){
itemsIndexed(dataList){ index: Int, item: TreeChildDetailEntity ->
TreeChildItem(viewModel,entity = item,pVM)
}
}
}
@SuppressLint("UnrememberedMutableState")
@ExperimentalMaterialApi
@Composable
private fun TreeChildItem(viewModel: BottomTabBarViewModel,entity: TreeChildDetailEntity,pVM: ProjectViewModel){
val openWebView by pVM._isToWebview.observeAsState()
val url by pVM.webviewUrl.observeAsState()
if (openWebView == true && !url.isNullOrBlank()){
pVM._isToWebview.value = false
viewModel.openWebViewPage = url
}
Card(
modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 0.dp)
.clickable {
pVM._isToWebview.value = true
pVM.webviewUrl.value = entity.link
},
elevation = 2.dp,
shape = RoundedCornerShape(10.dp)) {
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (img,title,desc,time,author) = createRefs()
Image(
painter = rememberImagePainter(entity.envelopePic),
modifier = Modifier
.width(120.dp)
.height(150.dp)
.padding(10.dp, 10.dp, 10.dp, 10.dp)
.constrainAs(img) {},
contentScale = ContentScale.Crop,
contentDescription = null
)
Text(modifier = Modifier
.padding(0.dp, 10.dp, 32.dp, 0.dp)
.constrainAs(title) {
top.linkTo(img.top)
start.linkTo(img.end)
},
text = entity.title,
fontSize = 14.sp,
color = Color.Black,
maxLines = 2,
overflow = TextOverflow.Ellipsis)
Text(modifier = Modifier
.padding(0.dp, 10.dp, 32.dp, 0.dp)
.constrainAs(desc) {
top.linkTo(title.bottom)
start.linkTo(img.end)
},
text = entity.desc,
fontSize = 12.sp,
color = Color.Gray,
maxLines = 2,
overflow = TextOverflow.Ellipsis)
Text(text = entity.niceShareDate,
fontSize = 12.sp,
color = Color.Gray,modifier = Modifier
.constrainAs(time) {
bottom.linkTo(img.bottom)
start.linkTo(img.end)
}
.padding(vertical = 10.dp))
Text(text = "${entity.author}",
modifier = Modifier
.padding(8.dp)
.border(
1.dp, color = Color(R.color.b_666),
RoundedCornerShape(8.dp)
)
.width(100.dp)
.padding(horizontal = 8.dp, vertical = 2.dp)
.constrainAs(author) {
bottom.linkTo(img.bottom)
end.linkTo(parent.end)
},
color = Color.Gray,
fontSize = 12.sp,
maxLines = 1,
textAlign = TextAlign.Center,
overflow = TextOverflow.Ellipsis
)
}
}
}
使用HorizontalPager加载页面
在使用HorizontalPager的时候要注意count和state参数就可以了。
HorizontalPager(
count = treeList!!.size,
state = pagerState, //用于控制或观察viewpage状态的状态对象。
modifier=Modifier.padding(top = 4.dp), //修饰符
reverseLayout=false, //反转滚动和布局的方向,为true时第一个页面在最后
itemSpacing=2.dp, //水平间距
verticalAlignment= Alignment.CenterVertically //垂直对齐
) { page -> //页面内容描述
TreeChildList(viewModel,pageIndex = pagerState.currentPage,cid = treeList!![pagerState.currentPage].id,pVM = pVM)
}
Compose中Webview的使用
在android中我们要记在一个网页链接都是使用webview进行加载,但是在compose里面没有对应的组件,那么还是要使用Webview进行加载;这里就需要使用到AndroidView这个组件了,让我们在compose中可以使用原生android的控件,这里就来使用一下Webview。
AndroidView的属性
先来看一下AndroidView的属性
@Composable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
)
- modifier
- factory 主要就是这个参数,实现我们使用android原生view的代码块
使用Webview
既然是在compose里面使用,所以方法还是要被@Composable标注起来,再传入一个String类型的url就可以了;然后webview的配置可以和在android中使用时的配置一样。
fun WebViewPage(modifier: Modifier = Modifier,linkUrl:String){
Column(modifier = Modifier.fillMaxSize()) {
AndroidView(modifier = Modifier.fillMaxSize(),
factory = {
val webView = WebView(it)
webView.settings.javaScriptEnabled = true
webView.settings.javaScriptCanOpenWindowsAutomatically = true
webView.settings.domStorageEnabled = true
webView.settings.loadsImagesAutomatically = true
webView.settings.mediaPlaybackRequiresUserGesture = false
webView.webViewClient = WebViewClient()
webView.loadUrl(linkUrl)
webView
})
}
}
配置完成了再点击item的时候打开当前pager就行了;但是有一个问题,知道compose是在当前页面打开,所以如果在点击item的时候加载这个webview,那么他就会直接在item那里进行加载,而不是跳转到新页面,这里就需要将Webview的pager放置到首页,然后通过viewmodel记录状态进行打开。
在viewmodel中记录打开状态:
var openModule: String? by mutableStateOf(null)
var openWebViewPage: String? by mutableStateOf(null)
如果符合条件viewModel.openWebViewPage !== null,则加载webview的页面:
val openOffset by animateFloatAsState(
if (viewModel.openModule == null) {
1f
} else {
0f
}
)
fun Modifier.percentOffsetX(percent: Float) = this.layout { measurable, constraints ->
val placeable = measurable.measure(constraints)
layout(placeable.width, placeable.height) {
placeable.placeRelative(IntOffset((placeable.width * percent).roundToInt(), 0))
}
}
if (viewModel.openWebViewPage !== null){
WebViewPage(Modifier.percentOffsetX(openOffset),linkUrl = viewModel.openWebViewPage!!)
}
在点击item的时候设置viewmodel中打开状态的值:
viewModel.openWebViewPage = url//这里是设置为要打开的链接,具体可以按照自己需求设置
以上就是在compose中使用android view的方法了。
源码地址
到这里今天的代码就结束了,源码戳~