Android Compose——一个简单的新闻APP
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.LongTypedefaultValue = -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 = truerestoreState = 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
,判断是否显示蒙层,因为select
为mutableStateOf
修饰的变量,当它的值变化后,系统会进行重组
,然后在其引用出进行重绘;
网络图片通过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> = _stateprivate val _edit = mutableStateOf(SearchModel(hint = "input some words..."))val edit:State<SearchModel> = _editinit {_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.labelListval key = viewModel.edit.valueColumn(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
Android Compose——一个简单的新闻APP相关推荐
- Android Compose——一个简单的Bilibili APP
Bilibili移动端APP 简介 依赖 效果 登录 效果 WebView 自定义TobRow的Indicator大小 首页 推荐 LazyGridView使用Paging3 热门 排行榜 搜索 模糊 ...
- Android Jetpack Compose——一个简单的微信界面
一个简单的微信界面 简述 效果视频 底部导航栏 导航元素 导航栏 放入插槽 绘制地图 消息列表 效果图 实现 聊天 效果图 实现 气泡背景 联系人界面 效果图 实现 好友详情 效果图 实现 发现 效果 ...
- Android实现一个简易的新闻列表APP(TabLayout+ViewPager+Fragment)
Android实现一个简易的新闻列表APP(TabLayout+ViewPager+Fragment) 文章目录 Android实现一个简易的新闻列表APP(TabLayout+ViewPager+F ...
- Android————一个简单的新闻面板
Android----一个简单的新闻面板 效果图 第一步:建一个实体类News 第二步:新建一个活动NewsContent 布局文件 第四步:建立NewsContentFragment和NewsTit ...
- 用Android Studio设计的一个简单的闹钟APP
该闹钟是用Android Studio为安卓手机设计的一个简单的闹钟APP 一.介绍系统的设计界面 闹钟的布局文件代码如下 <?xml version="1.0" encod ...
- 一个简单的手电筒APP源码分享(支持Android O(8.0)及以下版本)
一个简单的手电筒APP(无闪光灯的设备开启屏幕照明模式) GitHub地址: https://github.com/djzhao627/SimpleTorch 打包下载 http://download ...
- Android——一个简单的天气APP
一个简单的天气APP 效果演示视频 简述 天气JSON数据 实况天气 逐24小时天气预报 未来七天天气预报 天气详情页 效果图 获取JSON数据 URL请求 实况天气URL 逐24小时天气预报URL ...
- 使用Android studio做一个简单的网站APP
1.首先创建一个空白Android项目 2.然后打开项目,切换为Android视图,这时候会看到三个文件夹,分别是manifests.java.res.首先修改res/layout下的activity ...
- android studio的GearVR应用开发(二)、一个简单的VR app(Oculus官方GearVR开发教程,翻译转载)
声明:本文是Oculus官方的GearVR开发教程,为本人翻译转载,供广大VR开发爱好者一同学习进步使用. 原文章 一个简单的VR app 概观 在搭建好GearVR框架后,让我们一起来创建第一个VR ...
最新文章
- 外卖市场的搅屎棍要来了?字节跳动内测“心动外卖”!
- Mac下安装mysql5.7 完整步骤(图文详解)
- Android:阻止输入法将图片压缩变形
- 【Linux驱动】linux内核模块简介
- pdf文档遇到了共享冲突_如何将链接共享为PDF格式的Google文档链接
- php双向链表+性能,PHP双向链表定义与用法示例
- libevent的线程优雅的退出方式
- eclipse的编辑器样式风格设置
- python简单程序实例-Python简单基础小程序的实例代码
- 在排序数组中,找出给定数字的出现次数
- 10个微型计算机应用的例子,微型计算机原理与接技术版简答题.doc
- android版git中国只有,GitHub - ynztlxdeai/android-app: 本项目已经迁移到 git.oschina.net ,此处不再更新!...
- JQuery----倒计时插件downCount
- ft232h引脚_K9K8G08U0B-PIB0--斗门--镁光MICRON内存收购
- 初见NVelocity模板引擎
- 你知道有哪些正规的兼职平台吗?
- 强生单剂新冠疫苗对“德尔塔”有效;赛诺菲巴斯德将每年投资4亿欧元建mRNA疫苗中心 | 美通社头条...
- mac上的pdf编辑器怎么才能直接修改PDF文档上的字体大小
- 浙江工商大学python考试试卷_浙江工商大学期末考试试卷
- CentOS7 系统基础优化