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

news2024/11/17 7:45:58

Owl

  • 简述
  • 效果视频
  • 导航
    • 导航结点
    • 路线图
    • 底部导航栏
    • 使用
  • 标签页
    • 状态切换
  • FeaturePage
    • 构建
  • CoursePage
    • 实现
  • 搜索
    • ViewModel
    • View
  • 详情页
    • Detail
    • Describe
    • Lesson
  • Gitte

简述

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

效果视频

导航

总体分为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
                )
            }

标签页

状态切换

所有标签通过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

构建

所有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

实现

整个列表由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)
                }
        )
    }
}

搜索

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()
            )
        }
    }
}

详情页

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/160477.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022爱分析・出海数字化系列报告之“出海实时互动与通信”厂商全景报告 | 爱分析报告

报告编委 张扬 爱分析联合创始人&首席分析师 文鸿伟 爱分析高级分析师 王鹏 爱分析分析师 目录 研究范围定义厂商全景地图市场分析与厂商评估入选厂商列表研究范围定义 研究范围 改革开放四十多年来&#xff0c;中国企业经历了自商品出海到当前的品牌出海&#xff0c;出海…

Servlet的使用

作者&#xff1a;~小明学编程 文章专栏&#xff1a;JavaEE 格言&#xff1a;热爱编程的&#xff0c;终将被编程所厚爱。 目录 什么是Servlet&#xff1f; 创建一个Servlet程序 1.创建一个Maven项目 2.引入依赖 3.创建目录 4.编写代码 5.打包 6.部署程序 7.验证程序 …

Rust如何进行模块化开发?

类似es6的模块化&#xff0c;Rust通过package、create、module来实现代码的模块化管理 Rust如何进行模块化开发&#xff1f; Rust的代码组织包括&#xff1a;哪些细节可以暴露&#xff0c;哪些细节是私有的&#xff0c;作用域内哪些名称有效等等。 而这些功能被统称为模块系统…

晒成绩单了,百度智能云交出2022年终大考试卷!

晒成绩单了&#xff0c;百度智能云交出2022年终大考试卷&#xff01; 2023年伊始&#xff0c;工厂加快步伐复工复产、城市烟火气涌现、消费活力加速释放&#xff0c;企业对未来发展呈现乐观预期。有外媒称&#xff0c;“中国经济将实现比预期更快的复苏””。 站在更宏观的视…

java入门到废为止

目录基础数据变量类型数据类型基本类型上下转型引用类型类型对比装箱拆箱缓存池输入数据数组初始化元素访问内存分配数组异常二维数组运算参数形参实参可变参数方法方法概述定义调用注意事项方法重载重载介绍方法选取继承重载参数传递枚举Debug对象概述类定义构造器包封装thiss…

【React】二.JSX

目录 二.JSX JSX的基本使用 jsx使用步骤 JSX中使用JavaScript表达式 嵌入JS表达式 注意点 JSX的条件渲染 问题记录 JSX的列表渲染 JSX的样式处理 总结 二.JSX JSX的基本使用 createElement()的问题繁琐不简洁不能直观看出所描述的结构不优雅&#xff0c;用户体验不佳…

Java设计模式-代理模式Proxy

介绍 代理模式是一种比较好理解的设计模式。简单来说就是 我们使用代理对象来代替对真实对象(real object)的访问&#xff0c;这样就可以在不修改原目标对象的前提下&#xff0c;提供额外的功能操作&#xff0c;扩展目标对象的功能。 代理模式的主要作用是扩展目标对象的功能&a…

Linux编译器-gcc/g++的使用

&#x1f4df;作者主页&#xff1a;慢热的陕西人 &#x1f334;专栏链接&#xff1a;Linux &#x1f4e3;欢迎各位大佬&#x1f44d;点赞&#x1f525;关注&#x1f693;收藏&#xff0c;&#x1f349;留言 本博客主要内容主要介绍了Linux编译器g/gcc的相关使用方法&#xff0c…

Linux学习笔记——分布式内存计算Flink环境部署

5.13、分布式内存计算Flink环境部署 5.13.1、简介 Flink同Spark一样&#xff0c;是一款分布式内存计算引擎&#xff0c;可以支撑海量数据的分布式计算。 Flink在大数据体系同样是明星产品&#xff0c;作为最新一代的综合计算引擎&#xff0c;支持离线计算和实时计算。 在大…

libcurl库及curl API的简介

目录 一、libcurl简介 二、curl API简介 三.库安装编译方法 内容来源&#xff1a;Http协议之libcurl实现 - 谢呈勖 - 博客园 (cnblogs.com) 一、libcurl简介 libcurl是一个跨平台的网络协议库&#xff0c;支持http, https, ftp, gopher, telnet, dict, file, 和ldap 协议。…

当 Rainbond 遇上龙蜥!小龙带你玩转一站式云原生,点击开启

Rainbond 是一个云原生应用管理平台&#xff0c;使用简单&#xff0c;不需要懂容器、Kubernetes 和底层复杂技术&#xff0c;支持管理多个 Kubernetes 集群&#xff0c;和管理企业应用全生命周期。主要功能包括应用开发环境、应用市场、微服务架构、应用交付、应用运维、应用级…

Golang的Fork/Join实现

做过Java开发的同学肯定知道&#xff0c;JDK7加入的Fork/Join是一个非常优秀的设计&#xff0c;到了JDK8&#xff0c;又结合并行流中进行了优化和增强&#xff0c;是一个非常好的工具。1、Fork/Join是什么Fork/Join本质上是一种任务分解&#xff0c;即&#xff1a;将一个很大的…

FPGA图像处理HLS实现RGB转灰度,提供HLS工程和vivado工程源码

目录一、图像RGB转灰度原理二、HLS方案实现三、HLS在线仿真并导出IP四、Kintex7开发板vivado工程验证五、zynq7100开发板vivado工程验证六、板级调试验证七、福利&#xff1a;工程源码获取一、图像RGB转灰度原理 图像rgb转灰度图有固定的公式&#xff0c;具体公式csdn一大堆&a…

mirco:bit是什么?小学生拿着它就能召唤神龙?

mirco:bit是什么&#xff1f;micro:bit是一款由英国广播电视公司(BBC) 为青少年编程教育设计&#xff0c;并由微软&#xff0c;三星&#xff0c;ARM&#xff0c;英国兰卡斯特大学等合作伙伴共同完成开发的微型电脑。BBC希望通过micro:bit驱动青少年参与到创造性的硬件制作和软件…

MySQL基础——DCL语句

概述 DCL(Data Control Language)语句&#xff1a;数据控制语句&#xff0c;用于控制不同数据段直接的许可和访问级别的语句。这些语句定义了数据库、表、字段、用户的访问权限和安全级别。 管理用户 查询 查询用户代码如下&#xff1a; USE mysql; SELECT * FROM user; …

ASEMI桥式整流器KBU808的优缺点

编辑-Z 型号&#xff1a;KBU808 最大重复峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;800V 最大RMS电桥输入电压&#xff08;VRMS&#xff09;&#xff1a;560V 最大直流阻断电压&#xff08;VDC&#xff09;&#xff1a;800V 最大平均正向整流输出电流&#xf…

3D视觉技术登上火星?NASA也用上了NeRF技术做太空勘探

原文链接&#xff1a;https://www.techbeat.net/article-info?id4468 作者&#xff1a;seven_ 现阶段&#xff0c;人类探索宇宙的一个关键方向是如何高效的利用航天器返回的数据来了解和分析外太空的环境特点。其中最为常用的就是图像数据&#xff0c;但是这些数据非常宝贵&am…

C语言-自定义类型-结构体应用-通讯录(11.2)

目录 1.通讯录的设计思路 1.1主函数与通讯录框架 1.2菜单的实现 1.3通讯录的定义与初始化 2.通讯录具体功能的实现 2.1添加联系人 2.2删除联系人 2.3查找联系人 2.4修改联系人信息 2.5整理通讯录&#xff08;按年龄排序&#xff09; 2.6查看整个通讯录 3.通讯录源码…

Ubuntu 网络管理

一&#xff1a;NetPlan配置 1、安装netplan 如果/etc/netplan目录不存在请用以下命令安装&#xff1a; apt -y install netplan.io 2、配置文件 创建并编辑/etc/netplan/01-netplan.yaml文件&#xff1a; eth0&#xff1a;动态分配&#xff1b;eth1&#xff1a;静态分配 …

聊一聊nginx中KeepAlive的设置

文章目录问题分析为什么要有KeepAlive&#xff1f;TCP KeepAlive和HTTP的Keep-Alive是一样的吗&#xff1f;Nginx的TCP KeepAlive如何设置Apache中KeepAlive和KeepAliveTimeOut参考资料问题 之前工作中遇到一个KeepAlive的问题&#xff0c;现在把它记录下来&#xff0c;场景是…