Android Compose——一个简单的新闻APP

简介: 此Demo是参考Google Github其中一个Demo而完成,涉及的内容并不复杂,主要是为了熟悉Compose编码习惯,其次参考官方的代码,可以有利于培养编程思维,仅此而已

简述

此Demo是参考Google Github其中一个Demo而完成,涉及的内容并不复杂,主要是为了熟悉Compose编码习惯,其次参考官方的代码,可以有利于培养编程思维,仅此而已
Google Github Demo地址

效果视频

65736de256a94e198aacd7b59c9c8179.gif#pic_center

导航

总体分为A,B,C三个路由结点,A跳转B,B跳转C;其中B界面拥有底部导航栏,总共有三个子结点,其中B的子结点可以跳转C结点,也可以返回A结点,底部导航蓝栏中各元素也可以相互路由

导航结点

前三个是主体页面导航结点,后面三个是HomePage界面底部导航栏三个子结点,其中HomePage页面并不存在实际功能,只是作为一个入口,然后它的源点设置为子结点之一;这样当LabelPage跳转到HomePage界面,实际是导航到HomePage的源点

/**
 * 所有页面路由结点*/
sealed class Screen(val route:String){
    object LabelPage:Screen("LabelPage")//标签兴趣页
    object HomePage:Screen("HomePage")//首页,底部导航栏包含三个子页面
    object DetailPage:Screen("DetailPage")//内容详情页


    object CoursePage:Screen("HomePage/CoursePage")//底部导航栏-课程内容页
    object FeaturePage:Screen("HomePage/FeaturePage")//底部导航栏-推荐内容页
    object SearchPage:Screen("HomePage/SearchPage")//底部导航栏-搜索页
}

路线图

以下构建了三个结点之间的导航路线,由于其中HomePage结点是拥有底部导航栏界面,并没有实际作用,然后通过navigation在它的内部又构建了三个子结点,使用的都是同一个navHostController,同样都在同一个NavHost

@Composable
fun NavigationGraph(
    navHostController: NavHostController,
    startDestination: String = Screen.LabelPage.route,
    modifier: Modifier = Modifier,
    finishActivity:()->Unit
){
    val actions = MainAction()
    NavHost(navController = navHostController, startDestination = startDestination){
        /**
         * 标签兴趣选择页面*/
        composable(Screen.LabelPage.route){
            BackHandler {
                finishActivity()
            }
            LabelPage(){
                actions.toHomePage(navHostController)
            }
        }

        /**
         * route:代表外面一层导航结点
         * startDestination:代表底部导航栏中结点起始页*/
        navigation(route = Screen.HomePage.route,startDestination = Screen.FeaturePage.route){
            navigationSubPage(navHostController = navHostController, modifier = modifier,actions)
        }

        /**
         * 内容详情页面*/
        composable(
            Screen.DetailPage.route+"?id={id}",
            arguments = listOf(
                navArgument(name = "id")
                {
                    type = NavType.LongType
                    defaultValue = -1L
                }
            )
        ){
            DetailPage(
                onBack = {
                actions.back(navHostController)
            },
                onNavigation = {
                    actions.toDetail(navHostController,it)
                }
            )
        }
    }
}

下面三个结点为底部导航栏包含的子结点,也就是HomePage页面的子结点,构建与上述一致

/**
 * 底部导航栏子页面路由结点*/
fun NavGraphBuilder.navigationSubPage(navHostController: NavHostController,modifier: Modifier,action: MainAction){
    composable(Screen.CoursePage.route){
        CoursePage(modifier){
            action.toDetail(navHostController,it)
        }
    }

    composable(Screen.FeaturePage.route){
        FeaturePage(modifier){
            action.toDetail(navHostController,it)
        }
    }

    composable(Screen.SearchPage.route){
        SearchPage(modifier)
    }
}

底部导航栏

其中最重要的代码如下,通过判断当前节点是否属于底部导航栏结点之一,如果属于就构建底部导航栏,否则不构建;在一开始接触compose navigtion时,就出现过糗事,当时想要从拥有底部导航栏的界面跳转的一个新的界面,然后跳转的新页面也存在底部导航栏(不想它显示),然后当时我的办法是构建两个NavHostController,绑定两个不同NavHost,虽然这样能够解决问题,但是两个NavHostController之间导航切换,实在过于繁琐;

val route = tabs.map { it.route }
    if (currentRoute in route){
        BottomNavigation(...)
        }

将底部导航栏的元素结点通过遍历进行一一构建BottomNavigationItem,然后从外部传入NavHostController,完成内部结点导航

@Composable
fun bottomNavBar(navHostController: NavHostController,tabs: Array<NavElement> = NavElement.values()){
    val navBackStackEntry by navHostController.currentBackStackEntryAsState()
    val currentRoute = navBackStackEntry?.destination?.route
    /**
     * 关键部分
     * 只有当前路由结点属于底部导航栏列表元素中其中一个才显示底部导航栏*/
    val route = tabs.map { it.route }
    if (currentRoute in route){
        BottomNavigation(
            Modifier.windowInsetsBottomHeight(WindowInsets.navigationBars.add(WindowInsets(bottom = 56.dp))),
            backgroundColor = OWLTheme.colors.bottomBar
        ) {
            tabs.forEach {
                BottomNavigationItem(
                    icon = {Icon(painter = painterResource(id = it.icon), contentDescription = it.route)},
                    label = { Text(text = stringResource(id = it.title))},
                    selected = currentRoute == it.route,
                    alwaysShowLabel = false,
                    selectedContentColor = OWLTheme.colors.selectIcon,
                    unselectedContentColor = OWLTheme.colors.unselectIcon,
                    modifier = Modifier.navigationBarsPadding(),
                    onClick = {
                        navHostController.navigate(it.route){
                            navHostController.graph.startDestinationRoute?.let { route->
                                popUpTo(route){saveState = true}
                            }
                            launchSingleTop = true
                            restoreState = true
                        }
                    }
                )
            }
        }
    }
}

/**
 * 底部导航栏元素*/
enum class NavElement(
    @StringRes val title:Int,
    val route:String,
    @DrawableRes val icon:Int
){
    Course(R.string.my_courses,Screen.CoursePage.route,R.drawable.ic_grain),
    Feature(R.string.featured,Screen.FeaturePage.route,R.drawable.ic_featured),
    Search(R.string.search,Screen.SearchPage.route,R.drawable.ic_search)
}

使用

最后直接在最外层页面,也就是Activity起点通过插槽Scaffold添加bottomBar,因为在bottomBar构建时,已经通过判断,页面是否构建底部导航栏了,所以可以直接在初始页面进行构建

val navHostController = rememberNavController()
   Scaffold(
                bottomBar = {bottomNavBar(navHostController = navHostController)},
                modifier = Modifier.fillMaxSize()
            ) { paddingValues ->
                NavigationGraph(
                    navHostController = navHostController,
                    modifier = Modifier.padding(paddingValues),
                    finishActivity = finishActivity
                )
            }

标签页

0d9b52cbd04945bd9e6560e7c5b402e9.png#pic_center

状态切换

所有标签通过LazyHorizontalGrid构建而成,分为3行,每一个Item拥有两种状态,被选中和为未选中,其中被选中的Item会在图片上层覆盖一层蒙层

两个Boolean状态变量用于监听toggleable值变化,labelStyle通过select的值获取两套不一样的参数,也就是点击和未点击的变化量

 val (select,onSelect) = remember { mutableStateOf(false) }
 val labelStyle = labelChangeStyle(select)

两套不同的参数内容,

  • 第一个参数:圆角角度
  • 第二个参数:透明度
  • 第三个参数:比例(可无)
/**
 * label选中和为选中样式数值*/
fun labelChangeStyle(flag: Boolean):LabelStyle{
    return  when(flag){
        false ->{
            LabelStyle(0.dp,0f,0.6f)
        }
        true -> {
            LabelStyle(20.dp,0.8f,1f)
        }
    }
}

单个Item的代码如下,Surface绑定参数内容的radius,并只设置成左上角,然后将Row添加toggleable点击事件,并绑定上述两个Boolean状态值,然后通过状态值是否为true,判断是否显示蒙层,因为selectmutableStateOf修饰的变量,当它的值变化后,系统会进行重组,然后在其引用出进行重绘;
网络图片通过Coil库的AsyncImage组件实现,

@Composable
fun LabelGridItem(bean:LabelModel){
    val (select,onSelect) = remember { mutableStateOf(false) }
    val labelStyle = labelChangeStyle(select)

    Surface(
        modifier = Modifier.padding(4.dp),
        shape = RoundedCornerShape(topStart = labelStyle.radius)
    ) {
        Row(
            modifier = Modifier.toggleable(value = select, onValueChange = onSelect)
        ) {
            Box {
                AsyncImage(
                    model = bean.imageUrl,
                    contentDescription = bean.name,
                    placeholder = ColorPainter(Color.Gray.copy(alpha = 0.6f)),
                    contentScale = ContentScale.Crop,
                    modifier = Modifier
                        .size(72.dp)
                        .aspectRatio(1f)
                )
                /**
                 * 是否被选中*/
                if (select) {
                    Surface(
                        color = pink500.copy(alpha = labelStyle.alpha),
                        modifier = Modifier.matchParentSize()
                    ) {
                        Icon(
                            imageVector = Icons.Filled.Done,
                            contentDescription = null,
                            tint = OWLTheme.colors.selectIcon.copy(
                                alpha = labelStyle.alpha
                            ),
                            modifier = Modifier
                                .wrapContentSize()
                                .scale(labelStyle.scale)
                        )
                    }
                }
            }
            Column {
                Text(
                    text = bean.name,
                    style = MaterialTheme.typography.body1,
                    modifier = Modifier.padding(
                        start = 16.dp,
                        top = 16.dp,
                        end = 16.dp,
                        bottom = 8.dp
                    )
                )
                Row(
                    verticalAlignment = Alignment.CenterVertically
                ) {
                        Icon(
                            painter = painterResource(R.drawable.ic_grain),
                            contentDescription = null,
                            modifier = Modifier
                                .padding(start = 16.dp)
                                .size(12.dp)
                        )
                        Text(
                            text = "${bean.number}",
                            style = MaterialTheme.typography.caption,
                            modifier = Modifier.padding(start = 8.dp)
                        )
                }
            }
        }
    }
}

FeaturePage

831a4ca028ce483295cb7c7aa46d5dd4.png#pic_center

构建

所有Model数据通过LazyVerticalGrid列表构建,单个Item通过ConstraintLayout进行组合

@Composable
private fun featureGridItem(
    bean: FeatureBean,
    onNavigation: (Long) -> Unit
){
    ConstraintLayout(
        modifier = Modifier
            .background(OWLTheme.colors.detailBackground)
            .clickable {
                onNavigation(bean.id)
            }
    ) {
        val (imageRef,iconRef,titleRef,contentRef,numIconRef,numTextRef) = createRefs()
        AsyncImage(
            model = bean.thumbUrl,
            contentDescription = bean.name,
            contentScale = ContentScale.Crop,
            placeholder =  ColorPainter(Color.Gray.copy(alpha = 0.6f)),
            modifier = Modifier
                .aspectRatio(4f / 3f)
                .constrainAs(imageRef) {
                    centerHorizontallyTo(parent)
                    top.linkTo(parent.top)
                }
        )

        Box(
            modifier = Modifier
                .size(38.dp)
                .background(white, shape = CircleShape)
                .padding(2.dp)
                .border(1.dp, OWLTheme.colors.homeBackground, CircleShape)
                .constrainAs(iconRef) {
                    centerHorizontallyTo(parent)
                    top.linkTo(imageRef.bottom, (-19).dp)
                }
        ) {
            AsyncImage(
                model = bean.instructor,
                contentDescription = bean.name,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .fillMaxSize()
                    .clip(CircleShape)
            )
        }

        Text(
            text = bean.subject.uppercase(),
            style = MaterialTheme.typography.overline,
            color = OWLTheme.colors.homeBackground,
            modifier = Modifier
                .padding(top = 16.dp, bottom = 16.dp)
                .constrainAs(titleRef) {
                    centerHorizontallyTo(parent)
                    top.linkTo(iconRef.bottom)
                }
        )

        Text(
            text = bean.name,
            style = MaterialTheme.typography.subtitle1,
            color = OWLTheme.colors.primaryTitle,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .constrainAs(contentRef){
                    centerHorizontallyTo(parent)
                    top.linkTo(titleRef.bottom)
                }
        )

        val center = createGuidelineFromStart(0.5f)
        Icon(
            imageVector = Icons.Default.OndemandVideo,
            contentDescription = "watch",
            tint = OWLTheme.colors.homeBackground,
            modifier = Modifier
                .size(16.dp)
                .constrainAs(numIconRef) {
                    end.linkTo(center)
                    centerVerticallyTo(numTextRef)
                }
        )

        Text(
            text = "${bean.steps}",
            style = MaterialTheme.typography.subtitle2,
            color = OWLTheme.colors.homeBackground,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .padding(top = 16.dp, bottom = 16.dp, start = 4.dp)
                .constrainAs(numTextRef) {
                    start.linkTo(center)
                    top.linkTo(contentRef.bottom)
                }
        )
    }
}

CoursePage

9dbfa5da74cb4b9485d4d5991077ca71.png#pic_center

实现

整个列表由LazyColumn构建,单个Item由ConstraintLayout组合,其中每个Item的对于左侧空出的宽度,奇数与偶数分别为两个常量,然后单个Item通过padding进行空出;其中modifier每个扩展函数的先后顺序也会有不同的变化,如果padding放在前方,则如上图所示,被当作magin使用,因为在宽度为声明之前,先声明padding,此时之后声明的宽度或高度是被padding影响之后的大小;反之,如果宽度和高度定义在前,padding定义在后,此时padding发挥本职作用,偏移定义的内边距

modifier = Modifier
            .padding(start = spacerWidth)
            .height(100.dp)
            .fillMaxWidth()
            .background(OWLTheme.colors.detailBackground, shape = RoundedCornerShape(topStart = 20.dp))

ConstraintLayout中,通过建立一条基准线val center = createGuidelineFromTop(0.5f),基准线有上下左右四个方位和绝对位置等,用于切割某一大小,例如传入0.5f,则代表引用的两个组件各占一半,以此类推

@Composable
private fun courseItem(
    spacerWidth: Dp,
    bean: FeatureBean,
    onNavigation:(Long)-> Unit
){
    ConstraintLayout(
        modifier = Modifier
            .padding(start = spacerWidth)
            .height(100.dp)
            .fillMaxWidth()
            .background(OWLTheme.colors.detailBackground, shape = RoundedCornerShape(topStart = 20.dp))
            .clickable { onNavigation(bean.id) }
    ) {
        val (imgRef,nameRef,iconRef,epiRef) = createRefs()
        AsyncImage(
            model = bean.thumbUrl,
            contentDescription = bean.name,
            placeholder = ColorPainter(Color.Gray.copy(alpha = 0.6f)),
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .aspectRatio(1f)
                .clip(RoundedCornerShape(topStart = 20.dp))
                .constrainAs(imgRef) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                }
        )

        val center = createGuidelineFromTop(0.5f)
        Text(
            text = bean.name,
            color = OWLTheme.colors.primaryTitle,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            style = MaterialTheme.typography.subtitle1,
            modifier = Modifier
                .constrainAs(nameRef) {
                    bottom.linkTo(center,5.dp)
                    start.linkTo(imgRef.end,16.dp)
                    end.linkTo(parent.end)
                    width = Dimension.fillToConstraints
                }
        )

        Icon(
            imageVector =  Icons.Default.OndemandVideo,
            contentDescription = "Watch",
            tint = OWLTheme.colors.homeBackground,
            modifier = Modifier
                .size(16.dp)
                .constrainAs(iconRef) {
                    top.linkTo(center,5.dp)
                    start.linkTo(imgRef.end,16.dp)
                }
        )

        Text(
            text = stringResource(
                id = com.franz.owl.R.string.course_step_steps,
                bean.step,
                bean.steps
            ),
            color = OWLTheme.colors.homeBackground,
            maxLines = 1,
            overflow = TextOverflow.Ellipsis,
            style = MaterialTheme.typography.subtitle2,
            modifier = Modifier
                .constrainAs(epiRef) {
                    top.linkTo(iconRef.top)
                    bottom.linkTo(iconRef.bottom)
                    start.linkTo(iconRef.end,4.dp)
                }
        )
    }
}

搜索

cdf37571ab904c7c8c40460bc2dcbb4f.png#pic_center

ViewModel

其中_state监听的是列表数据源,_edit监听的是输入框的内容,在初始化处对_state进行赋值,然后onEvent方法用于监听View部分的输入框的变化,然后通过输入框传过来的值通过filter进行过滤,然后将符合条件的数据通过浅拷贝重新给_state赋值,外部绑定_state的组件,因为_state发生变化,外面组件也会相应进行重组

class SearchViewModel: ViewModel() {
    private val _state = mutableStateOf(LabelBean())
    val state:State<LabelBean> = _state

    private val _edit = mutableStateOf(SearchModel(
        hint = "input some words..."
    ))
    val edit:State<SearchModel> = _edit

    init {
        _state.value = state.value.copy(
            labelList = labels
        )
    }

    fun onEvent(key: String){
        _edit.value = edit.value.copy(
            text = key
        )
        _state.value = state.value.copy(
            labelList = labels.filter {
                it.name.contains(key,true)
            }
        )
    }
}

View

SearchBar的输入框中不断返回当前内容,然后执行onEvent,不断改变其值;在列表处绑定viewModel中的列表状态变量,随它的变化而重组

@Composable
fun SearchPage(
    modifier: Modifier = Modifier,
    viewModel: SearchViewModel = viewModel()
){
    val keys = viewModel.state.value.labelList
    val key = viewModel.edit.value
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(OWLTheme.colors.homeBackground)
            .padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 66.dp)
            .navigationBarsPadding()
    ) {
        SearchBar(key.text,key.hint){
            viewModel.onEvent(it)
        }
        Spacer(modifier = Modifier.height(15.dp))
        SearchList(keys)
    }
}

@Composable
private fun SearchBar(
    text: String,
    hint: String,
    onValueChange: (String)->Unit
){
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .statusBarsPadding(),
        verticalAlignment = Alignment.CenterVertically
    ) {
        Icon(
            imageVector = Icons.Default.Search, 
            contentDescription = "search",
            tint = white,
        )
        
        Spacer(modifier = Modifier.width(10.dp))
        
        BasicTextField(
            value = text,
            textStyle = MaterialTheme.typography.subtitle1.copy(
                color = white
            ),
            onValueChange = {onValueChange(it)},
            singleLine = true,
            cursorBrush = SolidColor(white)
        )
    }
} 

@OptIn(ExperimentalFoundationApi::class)
@Composable
private fun SearchList(keys: List<LabelModel>){
    LazyColumn(
        verticalArrangement = Arrangement.spacedBy(15.dp)
    ){
        items(keys.size){
            Text(
                text = keys[it].name,
                color = white,
                style = MaterialTheme.typography.h5,
                fontWeight = FontWeight.Bold,
                modifier = Modifier
                    .fillMaxWidth()
                   .animateItemPlacement()
            )
        }
    }
}

详情页

ce83cc80b48d4f81a67cee4fca1eb4ed.png
02596d67e8cf40679eeefcbd3e6fe66f.png

Detail

此页面分为两个界面,由Box组件进行组合,通过底部FAB按钮进行后面那个页面是否显示,使用AnimatedVisibility组件包裹Lesson页,并为其设置了入场和退出动画;其中BackHandler用于拦截系统导航栏返回按钮点击事件,当位于Lesson页面时,点击系统导航栏返回按钮,则返回Describe页

@Composable
fun DetailPage(
    viewModel: DetailViewModel = viewModel(),
    onBack: ()->Unit,
    onNavigation:(Long)->Unit

){
    val lessonState = remember { mutableStateOf(false) }
    val bean = viewModel.state.value//获取详情页数据
    val scope = rememberCoroutineScope()//协程

    /**
     * 拦截底部导航栏退出按钮点击事件
     * 如果LessonPage页为展开状态,则关闭,LessonPage
     * 否则退出详情页*/
    BackHandler(
        enabled = lessonState.value) {
        scope.launch { lessonState.value = false }
    }
        Box() {
            /**详情页*/
            DescribePage(bean, onBack = onBack, onNavigation = onNavigation)

            /**SheetButton,用于控制LessonPage的显示与隐藏*/
            sheetBtnView(modifier = Modifier.align(Alignment.BottomEnd)){
                scope.launch {
                    lessonState.value = it
                }
            }

            AnimatedVisibility(
                visible = lessonState.value,
                enter = fadeIn() + slideInVertically(),
                exit = fadeOut() + slideOutVertically()
            ) {
                /**Lesson页*/
                LessonPage(bean){
                    lessonState.value = it
                }
            }
    }
}

Describe

组件通过ConstraintLayout进行组合,顶部图片和顶部导航栏重合,中间为详细内容,底部为推荐相关数据列表;
如果Text需要显示string.xml文件的内容可以通过stringResource进行引用,如果文件的内容字符串需要传入数字或者字符,可以通过下列方式进行使用,具体参数由vararg可多变数量参数修饰

 text = stringResource(
                id = R.string.course_step_steps,
                bean.step,
                bean.steps
            )

由于底部整个布局高度超过一个屏幕最大高度,导致底部横向列表数据无法显示,故而通过 verticalScroll(rememberScrollState())进行竖向滑动

/**
 * 内容详情页
 * 用于展示相关内容*/
@Composable
fun DescribePage(
    bean: FeatureBean,
    onNavigation:(Long)->Unit,
    onBack: () -> Unit
){
    ConstraintLayout(
        modifier = Modifier
            .fillMaxSize()
            .background(OWLTheme.colors.detailBackground)
            .verticalScroll(rememberScrollState())
    ) {
        val (appBarRef,imgRef,iconRef,nameRef,titleRef, contentRef,dividerRef,
            tipOneRef,tipTwoRef,contentListRef) = createRefs()

        AsyncImage(
            model = bean.thumbUrl,
            contentDescription = bean.name,
            placeholder = ColorPainter(Color.Gray.copy(alpha = 0.6f)),
            contentScale = ContentScale.Crop,
            modifier = Modifier
                .fillMaxWidth()
                .aspectRatio(4f / 3f)
                .constrainAs(imgRef) {
                    top.linkTo(parent.top)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
        )

        AppBar(
            modifier = Modifier.constrainAs(appBarRef){
                top.linkTo(parent.top,20.dp)
                start.linkTo(parent.start,20.dp)
            }) { onBack() }

        Box(
            modifier = Modifier
                .size(38.dp)
                .background(white, shape = CircleShape)
                .padding(2.dp)
                .border(1.dp, OWLTheme.colors.homeBackground, CircleShape)
                .constrainAs(iconRef) {
                    centerHorizontallyTo(parent)
                    top.linkTo(imgRef.bottom, (-19).dp)
                }
        ) {
            AsyncImage(
                model = bean.instructor,
                contentDescription = bean.subject,
                contentScale = ContentScale.Crop,
                modifier = Modifier
                    .fillMaxSize()
                    .clip(CircleShape)
            )
        }

        Text(
            text = bean.subject,
            color = Color.Red,
            style = MaterialTheme.typography.body2,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(nameRef) {
                    top.linkTo(iconRef.bottom, 16.dp)
                    centerHorizontallyTo(parent)
                }
        )

        Text(
            text = bean.name,
            color = OWLTheme.colors.primaryTitle,
            style = MaterialTheme.typography.h4,
            textAlign = TextAlign.Center,
            fontWeight = FontWeight.Bold,
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 16.dp)
                .constrainAs(titleRef) {
                    top.linkTo(nameRef.bottom, 16.dp)
                    centerHorizontallyTo(parent)
                }
        )

        Text(
            text = stringResource(id = R.string.course_desc),
            color = OWLTheme.colors.primaryContent,
            style = MaterialTheme.typography.body1,
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 16.dp, end = 16.dp)
                .constrainAs(contentRef) {
                    top.linkTo(titleRef.bottom, 20.dp)
                    start.linkTo(parent.start)
                }
        )

        Divider(
            color = OWLTheme.colors.primaryContent.copy(alpha = 0.6f),
            thickness = 1.dp,
            modifier = Modifier
                .fillMaxWidth()
                .padding(vertical = 20.dp)
                .constrainAs(dividerRef) {
                    top.linkTo(contentRef.bottom)
                    start.linkTo(parent.start)
                }
        )

        Text(
            text = stringResource(id = R.string.what_you_ll_need),
            color = OWLTheme.colors.primaryTitle,
            style = MaterialTheme.typography.h6,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(tipOneRef) {
                    top.linkTo(dividerRef.bottom)
                    start.linkTo(parent.start)
                }
        )

        Text(
            text = stringResource(id = R.string.needs),
            color = OWLTheme.colors.primaryContent,
            style = MaterialTheme.typography.body1,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .fillMaxWidth()
                .constrainAs(tipTwoRef) {
                    top.linkTo(tipOneRef.bottom, 20.dp)
                    start.linkTo(parent.start)
                    end.linkTo(parent.end)
                }
        )

        recommendContentList(
            onNavigation = onNavigation,
            modifier = Modifier.constrainAs(contentListRef){
                top.linkTo(tipTwoRef.bottom,20.dp)
                start.linkTo(parent.start)
            }
        )

    }
}

Lesson

此页面用于展示Decribe页面相关内容,数据为静态数据,仅作为展示,通过顶部标题栏返回按钮的点击事件,改变上述AnimatedVisibility所绑定的状态变量值,然后进行重组,使其隐藏

@Composable
fun LessonPage(
    bean: FeatureBean,
    onClick: (Boolean) -> Unit)
{
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(pink500)
            .statusBarsPadding()
            .navigationBarsPadding()
            .padding(start = 10.dp, end = 10.dp, bottom = 20.dp)
    ) {
        LessonAppBar(bean.name, onClick = onClick)
        Spacer(modifier = Modifier.height(20.dp))
        LessonList()
    }
}

此Demo还增加了沉浸式标题栏、SplashScreen界面、主题切换等功能,由于篇幅问题,在此不予贴出,有意者,可点击下述项目链接进行访问

Gitte

Gitte链接

https://gitee.com/FranzLiszt1847/owl
相关文章
|
3月前
|
IDE API 开发工具
Google I/O :Android Jetpack 最新变化(四)Compose
Google I/O :Android Jetpack 最新变化(四)Compose
100 0
|
3月前
|
Android开发 开发者 iOS开发
APP开发后如何上架,上架Android应用市场前要准备什么
移动应用程序(APP)的开发已经成为现代企业和开发者的常见实践。然而,开发一个成功的APP只是第一步,将其上架到应用商店让用户下载和使用是实现其潜力的关键一步。
|
1月前
|
设计模式 测试技术 数据库
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
基于Android的食堂点餐APP的设计与实现(论文+源码)_kaic
|
1月前
|
XML API Android开发
【Android 从入门到出门】第三章:使用Hilt处理Jetpack Compose UI状态
【Android 从入门到出门】第三章:使用Hilt处理Jetpack Compose UI状态
26 4
|
2月前
|
安全 Java 数据挖掘
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace
当 App 有了系统权限,真的可以为所欲为? Android Performance Systrace 转载自: https://androidperformance.com/2023/05/14/bad-android-app-with-system-permissions/#/0-Dex-%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF
30 0
|
3月前
|
Android开发
闲暇时间收集和整理的Android的一些常用的App
闲暇时间收集和整理的Android的一些常用的App
14 0
|
3月前
|
Android开发 UED 开发者
解释Android App Bundle是什么,它的优势是什么?
解释Android App Bundle是什么,它的优势是什么?
55 0
|
3月前
|
Android开发 Kotlin 索引
Android Compose——ScrollableTabRow和LazyColumn同步滑动
Android Compose——ScrollableTabRow和LazyColumn同步滑动
|
3月前
|
Android开发
解决在Android Compose中点击空白处收回软键盘
解决在Android Compose中点击空白处收回软键盘