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
相关文章
|
1月前
|
XML Java 数据库
安卓项目:app注册/登录界面设计
本文介绍了如何设计一个Android应用的注册/登录界面,包括布局文件的创建、登录和注册逻辑的实现,以及运行效果的展示。
139 0
安卓项目:app注册/登录界面设计
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android多线程编程的重要性及其实现方法,涵盖了基本概念、常见线程类型(如主线程、工作线程)以及多种多线程实现方式(如`Thread`、`HandlerThread`、`Executors`、Kotlin协程等)。通过合理的多线程管理,可大幅提升应用性能和用户体验。
120 15
一个Android App最少有几个线程?实现多线程的方式有哪些?
|
2月前
|
存储 开发工具 Android开发
使用.NET MAUI开发第一个安卓APP
【9月更文挑战第24天】使用.NET MAUI开发首个安卓APP需完成以下步骤:首先,安装Visual Studio 2022并勾选“.NET Multi-platform App UI development”工作负载;接着,安装Android SDK。然后,创建新项目时选择“.NET Multi-platform App (MAUI)”模板,并仅针对Android平台进行配置。了解项目结构,包括`.csproj`配置文件、`Properties`配置文件夹、平台特定代码及共享代码等。
156 2
|
2月前
|
XML Android开发 数据格式
🌐Android国际化与本地化全攻略!让你的App走遍全球无障碍!🌍
在全球化背景下,实现Android应用的国际化与本地化至关重要。本文以一款旅游指南App为例,详细介绍如何通过资源文件拆分与命名、适配布局与方向、处理日期时间及货币格式、考虑文化习俗等步骤,完成多语言支持和本地化调整。通过邀请用户测试并收集反馈,确保应用能无缝融入不同市场,提升用户体验与满意度。
102 3
|
2月前
|
Java 数据库 Android开发
一个Android App最少有几个线程?实现多线程的方式有哪些?
本文介绍了Android应用开发中的多线程编程,涵盖基本概念、常见实现方式及最佳实践。主要内容包括主线程与工作线程的作用、多线程的多种实现方法(如 `Thread`、`HandlerThread`、`Executors` 和 Kotlin 协程),以及如何避免内存泄漏和合理使用线程池。通过有效的多线程管理,可以显著提升应用性能和用户体验。
69 10
|
1月前
|
安全 网络安全 Android开发
深度解析:利用Universal Links与Android App Links实现无缝网页至应用跳转的安全考量
【10月更文挑战第2天】在移动互联网时代,用户经常需要从网页无缝跳转到移动应用中。这种跳转不仅需要提供流畅的用户体验,还要确保安全性。本文将深入探讨如何利用Universal Links(仅限于iOS)和Android App Links技术实现这一目标,并分析其安全性。
216 0
|
2月前
|
安全 Java Android开发
探索安卓应用开发的新趋势:Kotlin和Jetpack Compose
在安卓应用开发领域,随着技术的不断进步,新的编程语言和框架层出不穷。Kotlin作为一种现代的编程语言,因其简洁性和高效性正逐渐取代Java成为安卓开发的首选语言。同时,Jetpack Compose作为一个新的UI工具包,提供了一种声明式的UI设计方法,使得界面编写更加直观和灵活。本文将深入探讨Kotlin和Jetpack Compose的特点、优势以及如何结合使用它们来构建现代化的安卓应用。
61 4
|
2月前
|
XML 数据库 Android开发
10分钟手把手教你用Android手撸一个简易的个人记账App
该文章提供了使用Android Studio从零开始创建一个简单的个人记账应用的详细步骤,包括项目搭建、界面设计、数据库处理及各功能模块的实现方法。
|
3月前
|
API Android开发
Android P 性能优化:创建APP进程白名单,杀死白名单之外的进程
本文介绍了在Android P系统中通过创建应用进程白名单并杀死白名单之外的进程来优化性能的方法,包括设置权限、获取运行中的APP列表、配置白名单以及在应用启动时杀死非白名单进程的代码实现。
62 1
|
3月前
|
Android开发 iOS开发 C#
Xamarin:用C#打造跨平台移动应用的终极利器——从零开始构建你的第一个iOS与Android通用App,体验前所未有的高效与便捷开发之旅
【8月更文挑战第31天】Xamarin 是一个强大的框架,允许开发者使用单一的 C# 代码库构建高性能的原生移动应用,支持 iOS、Android 和 Windows 平台。作为微软的一部分,Xamarin 充分利用了 .NET 框架的强大功能,提供了丰富的 API 和工具集,简化了跨平台移动应用开发。本文通过一个简单的示例应用介绍了如何使用 Xamarin.Forms 快速创建跨平台应用,包括设置开发环境、定义用户界面和实现按钮点击事件处理逻辑。这个示例展示了 Xamarin.Forms 的基本功能,帮助开发者提高开发效率并实现一致的用户体验。
151 0