Android Compose 新闻App(八)抽屉布局、动态权限、拍照返回

前言

  在上一篇文章中,我们构建了HomeItem中的内容,这里面目前是由一个Tab构成五个部分,社会、军事、科技、财经、娱乐五个新闻类型,那么在上一篇中做了社会的新闻显示。

正文

  在本篇文章中将完善这个新闻类型。

一、完善新闻数据

那么首先你需要去天行API中去请求相应的数据接口,请求之后将这些接口调试一下你就会发现,这五个接口返回的数据类型一致,就是我们在社会新闻中返回的数据结构,那就可以直接复用。

① ApiService

于是我们就可以在ApiService中写出如下所示的代码:

	/**
     * 获取军事新闻
     */
    @GET("/military/index?key=$API_KEY")
    fun getMilitaryNews(): Call<News>

    /**
     * 获取科技新闻
     */
    @GET("/keji/index?key=$API_KEY")
    fun getTechnologyNews(): Call<News>

    /**
     * 获取财经新闻
     */
    @GET("/caijing/index?key=$API_KEY")
    fun getFinanceNews(): Call<News>

    /**
     * 获取娱乐新闻
     */
    @GET("/huabian/index?key=$API_KEY")
    fun getAmusementNews(): Call<News>

② NetworkRequest

那么现在ApiService中就有6个函数了,下面在NetworkRequest中添加如下代码:

	//获取军事新闻
    suspend fun getMilitaryNews() = service.getMilitaryNews().await()

    //获取科技新闻
    suspend fun getTechnologyNews() = service.getTechnologyNews().await()

    //获取财经新闻
    suspend fun getFinanceNews() = service.getFinanceNews().await()

    //获取娱乐新闻
    suspend fun getAmusementNews() = service.getAmusementNews().await()

③ HomeRepository

然后我们在HomeRepository中新增函数,代码如下:

	/**
     * 获取军事新闻
     */
    fun getMilitaryNews() = fire(Dispatchers.IO) {
    
    
        val news = NetworkRequest.getMilitaryNews()
        if (news.code == CODE) Result.success(news)
        else Result.failure(RuntimeException("getNews response code is ${
      
      news.code} msg is ${
      
      news.msg}"))
    }

    /**
     * 科技新闻
     */
    fun getTechnologyNews() = fire(Dispatchers.IO) {
    
    
        val news = NetworkRequest.getTechnologyNews()
        if (news.code == CODE) Result.success(news)
        else Result.failure(RuntimeException("getNews response code is ${
      
      news.code} msg is ${
      
      news.msg}"))
    }

    /**
     * 财经新闻
     */
    fun getFinanceNews() = fire(Dispatchers.IO) {
    
    
        val news = NetworkRequest.getFinanceNews()
        if (news.code == CODE) Result.success(news)
        else Result.failure(RuntimeException("getNews response code is ${
      
      news.code} msg is ${
      
      news.msg}"))
    }

    /**
     * 娱乐新闻
     */
    fun getAmusementNews() = fire(Dispatchers.IO) {
    
    
        val news = NetworkRequest.getAmusementNews()
        if (news.code == CODE) Result.success(news)
        else Result.failure(RuntimeException("getNews response code is ${
      
      news.code} msg is ${
      
      news.msg}"))
    }

④ HomeViewModel

然后就是在HomeViewModel中新增如下代码:

	val resultMilitary = repository.getMilitaryNews()

    val resultTechnology = repository.getTechnologyNews()

    val resultFinance = repository.getFinanceNews()

    val resultAmusement = repository.getAmusementNews()

下面我们可以直接在HomeItem中使用即可,代码如下所示:

				0 -> viewModel.result.observeAsState().value?.let {
    
    
                    ShowNewsList(mNavController, it.getOrNull()!!.newslist)
                }
                1 -> viewModel.resultMilitary.observeAsState().value?.let {
    
    
                    ShowNewsList(mNavController, it.getOrNull()!!.newslist)
                }
                2 -> viewModel.resultTechnology.observeAsState().value?.let {
    
    
                    ShowNewsList(mNavController, it.getOrNull()!!.newslist)
                }
                3 -> viewModel.resultFinance.observeAsState().value?.let {
    
    
                    ShowNewsList(mNavController, it.getOrNull()!!.newslist)
                }
                4 -> viewModel.resultAmusement.observeAsState().value?.let {
    
    
                    ShowNewsList(mNavController, it.getOrNull()!!.newslist)
                }

添加的代码如下图所示:
在这里插入图片描述
下面我们运行一下:
在这里插入图片描述
你会发现了这里的军事数据的图片没有显示出来,我们通过地址看到picUrl的值是空字符串,那么是加载不了的,我们可以这样修改一下AsyncImage,代码如下:

				AsyncImage(
                    model = ImageRequest.Builder(LocalContext.current)
                        .data(new.picUrl)
                        .error(R.drawable.placeholder)
                        .crossfade(true)
                        .build(),
                    contentDescription = null,
                    placeholder = painterResource(R.drawable.placeholder),
                    modifier = Modifier
                        .width(120.dp)
                        .height(80.dp),
                    contentScale = ContentScale.FillBounds
                )

这里我们修改了一下model的值,通过ImageRequest去设置要加载的图片,并设置加载失败的时候的图片,这个图片去我的源码中获取,然后这里还有一个placeholder,这个图的意思就是预览图,当加载网络图片时一开始没加载出来就显示此图片。
在这里插入图片描述

二、抽屉布局

这个抽屉布局通过主页面的左上角的菜单点击进行打开,因此我们首先添加一个菜单按钮和一个疫情新闻按钮,在HomePage中,

① 添加菜单

增加如下代码:

				navigationIcon = {
    
    
                    IconButton(onClick = {
    
     "Menu".showToast() }) {
    
    
                        Icon(Icons.Default.Menu, contentDescription = "Menu")
                    }
                },
                actions = {
    
    
                    IconButton(onClick = {
    
     "疫情新闻".showToast() }) {
    
    
                        Icon(Icons.Default.Sick, contentDescription = "疫情")
                    }
                }

② 打开抽屉

在Scaffold要打开抽屉布局,需要使用ScaffoldState中的drawerState,可以通过更改drawerState来控制打开或关闭抽屉布局,而要更改drawerState需要通过协程或其他挂起函数。

定义两个变量:

	val scaffoldState = rememberScaffoldState()
    val scope = rememberCoroutineScope()

一个是状态一个是协程作用域,下面我们使用它,如下图所示:
在这里插入图片描述

抽屉布局同样是一个页面,因此我们可以写一个可组合函数,在pages下创建一个DrawerView.kt,里面的代码如下:

@Composable
fun DrawerView() {
    
    
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = colorResource(id = R.color.red))
    ) {
    
    
        Text(text = "测试", color = colorResource(id = R.color.white))
    }
}

这个很简单,就是设置背景颜色和文字颜色,然后我们需要在HomePage中调用,一行代码解决问题,在Scaffold中添加如下代码:

	drawerContent = {
    
     DrawerView() },

添加位置如下图所示:
在这里插入图片描述
下面我们运行一下:
在这里插入图片描述

这个红色稍微的有那么一些辣眼睛。下面我们改一下,我们其实可以把这个改成个人中心,下面我们构建布局。修改DrawerView.kt中的代码,代码如下所示:

@Composable
fun DrawerView() {
    
    
    val context = LocalContext.current
    Column(
        modifier = Modifier
            .fillMaxSize()
            .background(color = colorResource(id = R.color.white))
            .padding(0.dp, 36.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
    
    
        Image(
            painter = painterResource(id = R.mipmap.ic_logo), contentDescription = "头像",
            modifier = Modifier
                .size(100.dp)
                .clip(CircleShape),
            contentScale = ContentScale.FillBounds
        )
        Spacer(modifier = Modifier.height(24.dp))
        Text(
            text = "初学者-Study",
            color = colorResource(id = R.color.black),
            fontFamily = FontFamily.Monospace,
            fontSize = 18.sp
        )
        Spacer(modifier = Modifier.height(12.dp))
        Text(
            text = "Android | Kotlin | Compose",
            color = colorResource(id = R.color.black),
            fontFamily = FontFamily.SansSerif,
            fontSize = 14.sp
        )
        Spacer(modifier = Modifier.height(24.dp))
        Row(modifier = Modifier.fillMaxWidth()) {
    
    
            ItemView("文章", 188, Modifier.weight(1f))
            ItemView("点赞", 2109, Modifier.weight(1f))
            ItemView("评论", 2897, Modifier.weight(1f))
            ItemView("收藏", 6450, Modifier.weight(1f))
        }

        Spacer(modifier = Modifier.height(24.dp))
        Divider(
            color = colorResource(id = R.color.gray_black),
            thickness = 1.dp,//线的高度
        )
        ItemViewOnClick("CSDN主页", "https://llw-study.blog.csdn.net/", context)
        ItemViewOnClick("GitHub主页", "https://github.com/lilongweidev/", context)
    }
}

@Composable
fun ItemView(name: String, num: Int, modifier: Modifier) {
    
    
    Column(
        modifier,
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
    
    
        Text(text = num.toString(), modifier = Modifier.padding(0.dp, 6.dp))
        Text(text = name)

    }
}

@Composable
fun ItemViewOnClick(name: String, url: String, context: Context) {
    
    
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable {
    
    
                val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
                context.startActivity(intent)
            }
            .height(50.dp)
            .padding(12.dp, 0.dp),
        verticalAlignment = Alignment.CenterVertically
    ) {
    
    
        Icon(Icons.Default.Stars, contentDescription = name)
        Text(
            text = name, modifier = Modifier
                .padding(12.dp, 0.dp)
                .weight(1f)
        )
        Icon(Icons.Default.ChevronRight, contentDescription = "打开")
    }
}

下面再我们运行一下:
在这里插入图片描述

GitHub打开的速度比较慢,现在我们的抽屉布局就写好了,看上去也是比较舒服的。

三、导航疫情页面

  在前几篇文章中的疫情页面已经安静很久了,我们不能忘记它了,所以我们在主页面导航到疫情新闻页面。在HomePage中,TopBar的左边是菜单图标,右边是一个生病的图标,这个图标点击之后就导航到疫情新闻页面,代码如下:

	mNavController.navigate(PageConstant.EPIDEMIC_NEWS_LIST_PAGE)

添加位置如下图所示:
在这里插入图片描述
由于疫情新闻页面我并没有在TopBar中写返回按钮,因此我们可以通过疫情新闻页面的浮动按钮点击返回到当前的主页面,
代码如下:

	mNavController.popBackStack()

代码添加位置如下图所示:
在这里插入图片描述

四、动态权限请求

  在Compose中请求权限和之前有所不同,下面我们来看看要怎么做,就用一个相机权限来举例说明。

① 添加依赖

在app的build.gradle中的dependencies{}闭包中添加如下依赖:

	//权限库
    implementation "com.google.accompanist:accompanist-permissions:0.18.0"

然后Sync Now。

然后我们在AndroidManifest.xml中添加权限配置

	<uses-permission android:name="android.permission.CAMERA"/>

下面我们可以想一下权限请求的入口在哪里,一般来说作为动态权限,我们需要在使用的时候再请求,而不是一打开App就请求,而我们现在的App中有一个抽屉布局,里面有一个头像,我们可以点击这个头像的时候请求动态权限,通过权限后我们提示一下,再次点击时,如果有权限也提示一下。

② 权限请求

在DrawerView()函数中增加如下代码:

	// 定义 Permission State
    val permissionState = rememberPermissionState(Manifest.permission.CAMERA)

    PermissionRequired(
        permissionState = permissionState,
        permissionNotGrantedContent = {
    
     /*TODO*/ },
        permissionNotAvailableContent = {
    
     /*TODO*/ }) {
    
    
        //调用权限获取之后功能
        "打开相机".showToast()
    }

这里我们首先定义一个权限状态,状态对应的是相机权限,而在下面有一个PermissionRequired()函数,这是刚才的依赖库里面的,里面有三个参数,permissionState 就是我们刚才的状态,这个参数很重要,通过它我们可以知道当前的权限请求是怎么样的,然后permissionNotGrantedContent 是权限未请求时显示的内容,permissionNotAvailableContent 是权限不可用显示的内容,这两个在一些场景下会用到,下面我们看看PermissionState 的内容
在这里插入图片描述
标注的这两个等下会用到,那么怎么去使用呢?

我们给头像一个点击事件,代码如下:

				.clickable {
    
    
                    //请求权限
                    if (permissionState.hasPermission) {
    
    
                        "打开相机".showToast()
                    } else {
    
    
                        permissionState.launchPermissionRequest()
                    }
                }

注意添加的位置:
在这里插入图片描述
这个代码应该很好理解,下面我们运行一下:
在这里插入图片描述

五、拍照显示图片

  在上面我们获取了相机权限,那么在下面我就需要进行拍照并显示图片。还记得之前在Android中的ActivityResult API吗?

① ActivityResult API

这个ActivityResult API里面携带了很多常用的页面处理,包括了进入系统相机,下面我们将使用它,在使用之前,我们在DrawerView函数中创建两个变量:

	var mCameraUri: Uri? = null

    val imageUir = remember {
    
    
        mutableStateOf<Uri?>(null)
    }

mCameraUri用于保存拍照返回的图片,imageUir 用于显示在页面上,然后我们可以写出这样的代码:

	//TakePicture 调用相机,拍照后将图片保存到开发者指定的Uri,返回true
    val openCameraLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.TakePicture(),
        onResult = {
    
     if (it) imageUir.value = mCameraUri })

在相机返回时,判断是否有拍照,有的话就对imageUir 进行赋值,赋值后状态会改变,改变时显示在页面上。

② 拍照显示

之前的头像用的是一个Image函数,现在改成AsyncImage函数,代码如下:

		AsyncImage(
            model = ImageRequest.Builder(context)
                .data(imageUir.value)
                .error(R.mipmap.ic_logo)
                .crossfade(true)
                .build(),
            contentDescription = null,
            placeholder = painterResource(R.mipmap.ic_logo),
            modifier = Modifier
                .size(100.dp)
                .clip(CircleShape)
                .clickable {
    
    
                    if (permissionState.hasPermission) {
    
    
                        //构建Uir
                        mCameraUri = context.contentResolver.insert(
                            if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) 
                                MediaStore.Images.Media.EXTERNAL_CONTENT_URI else
                                MediaStore.Images.Media.INTERNAL_CONTENT_URI, ContentValues()
                        )
                        //启动拍照
                        openCameraLauncher.launch(mCameraUri)
                    } else {
    
    
                        //请求权限
                        permissionState.launchPermissionRequest()
                    }
                },
            contentScale = ContentScale.FillBounds
        )

model 用于构建图片,这里的data我们用的就是imageUir.value,第一次运行,因为它里面是null,所以不会显示出来,我们用了.error(R.mipmap.ic_logo)作为处理,这样就不会一片空白了,而当imageUir赋值之后就会触发这个data,然后就会加载图片的uri,就能显示出来了。在图片的点击事件中,当有权限时我们构建一个uri,赋值给mCameraUri ,然后通过openCameraLauncher.launch(mCameraUri)去打开相机,如果你拍照了,那么图片就会在这个mCameraUri 中,没有拍照就不会在。现在代码写完了,下面我们运行一下吧。
在这里插入图片描述

六、源码

如果你觉得代码对你有帮助的话,不妨Fork或者Star一下~
GitHub:GoodNews
CSDN:GoodNews_8.rar

猜你喜欢

转载自blog.csdn.net/qq_38436214/article/details/124568852