android、鸿蒙开发--第十五章-->Android Compose 新闻天气App

前言:我们依旧使用上一章MVVM中的天气和新闻接口;使用Compose 布局方式实现

先看运行效果:

1、MainActivity代码

2. MainVertical()布局代码和逻辑代码

@OptIn(ExperimentalFoundationApi::class)
@SuppressLint("MutableCollectionMutableState")
@Composable
@Preview
fun MainVertical() {

    //状态变量,用于记录选择的第几页与更新tab现实
    var selectIndex by remember { mutableStateOf(0) }
    //状态变量, 用于记录HorizontalPager选中的页数
    val pagerState = remember { PagerState(0) }
    //协程,用于切换主线下切换HorizontalPager
    val coroutineScope = remember { CoroutineScope(Dispatchers.Main) }

    //这里准备tab的展示数据
    val tabList = mutableListOf<TabData>()
    tabList.add(TabData("天气", R.mipmap.icon_weather_no, R.mipmap.icon_weather_yes))
    tabList.add(TabData("新闻", R.mipmap.icon_news_no, R.mipmap.icon_news_yes))

    //设置记录第二个tab界面,选中的第几个的状态变量
    var selectIndex2 by remember { mutableStateOf(0) }

    // PagerState变化时触发
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect {
            //当页数发生变化,更新tab selectIndex的状态,对应的ui也会变更
            if (selectIndex!=it) selectIndex = it
        }
    }

   // 线性垂直布局
    Column(modifier = Modifier
        .fillMaxSize()
        .background(Color.White),
        verticalArrangement = Arrangement.Bottom) {
        //添加pager,这也一个页数,与View体系中ViewPager效果相似
        HorizontalPager(tabList.size, Modifier.fillMaxSize().weight(1f), pagerState, userScrollEnabled = false) { page ->
            when (page) { //页数切换的时候, 加载不同的布局
                0 -> WeatherContent() //0 加载第一页 即天气
                else -> NewSet(selectIndex2){//其他的加载新闻界面,传递选中的状态,默认是0
                    selectIndex2=it  //当在新闻界面选中了某个,则回调回来做记录,下回切换回去,状态还在
                }
            }
        }

        //这里是底部导航栏
        BottomNavigation {
//            repeat 表示创建多个相同的控件 ,当selectIndex变更时候, BottomNavigation也会变更
            repeat(tabList.size) { iteration ->
                val tabData = tabList[iteration]
                BottomNavigationItem(
                    modifier = Modifier.background(Color.White),
                    label = { Text(tabData.tabName, modifier = Modifier.padding(bottom = 10.dp), color = Color.Black) },
                    selected = selectIndex == iteration, //是否选中
                    onClick = { //点击事件
                        if (selectIndex!=iteration){
                            selectIndex = iteration
                            coroutineScope.launch { pagerState.scrollToPage(selectIndex) }
                        }
                    },
                    icon = { //设置图标
                        val chooseImageId =
                        if (selectIndex == iteration) tabData.iconImageYes else tabData.iconImageNo
                        Image(
                            painter = painterResource(chooseImageId),
                            contentDescription = null,
                            modifier = Modifier.size(30.dp))
                    })
            }
        }
    }
}

3.解析我们看看。天气模块代码:WeatherContent()

private val cityList = listOf("浦东新区","石阡县") //定义两个地区
private var chooseCity= cityList[0] //用于记录选择的地区
private val weatherHaMap=HashMap<String,List<ForecastsData>?>() //全局记录地区天气数据

@SuppressLint("MutableCollectionMutableState", "CoroutineCreationDuringComposition")
@Composable
fun WeatherContent(){
    //创建城市集合状态变量
    val cityList by remember { mutableStateOf(cityList) }
    //这里View体系下的自定义View,用于展示天气数据的
    val mNewWeatherView: WeatherView? = null
    //创建 有关展示 天气的View状态变量
    var mWeatherView by remember { mutableStateOf(mNewWeatherView) }
    //创建协程
    val coroutineScope = remember { CoroutineScope(MyExceptionCatch.exceptionMainCatch) }

    //线性垂直布局
    Column {
        Row { //水平线性布局 ,repeat 创建多个Text
            repeat(cityList.size) { iteration ->
                Text(cityList[iteration], Modifier.padding(all = 10.dp).clickable { //点击是事件
                    //点击的不是同一区域,开启协程刷新数据
                    if (chooseCity != cityList[iteration]) coroutineScope.launch(MyExceptionCatch.exceptionMainCatch) {
                        chooseCity = cityList[iteration]
                        val forecastsData = weatherHaMap[chooseCity]
                        //先判断是否有缓存数据,有则使用,无获取网络数据
                        val forecasts=if (forecastsData.isNullOrEmpty()) {
                            withContext(MyExceptionCatch.exceptionIoCatch){ RetrofitManager.apiServer.getWeather(cityList[iteration]).data?.forecasts }
                        } else forecastsData
                        
                        weatherHaMap[chooseCity]=forecasts
                        //这里更新状态变量,刷新展示内容
                        mWeatherView?.setWeatherList(forecasts)
                    }
                })
            }
        }
        //这里是挂载View 体系下的 WeatherView
        AndroidView({ context -> WeatherView(context).apply { mWeatherView=this } }, Modifier
            .fillMaxSize()
            .weight(1f),{
        })

        //先判断选中的城市是否有缓存数据,有则使用,无获取网络数据
        val forecastsData = weatherHaMap[chooseCity]
        if (forecastsData.isNullOrEmpty()) coroutineScope.launch(MyExceptionCatch.exceptionMainCatch) {
            val forecasts = withContext(MyExceptionCatch.exceptionIoCatch){
                RetrofitManager.apiServer.getWeather("浦东新区").data.forecasts
            }
            weatherHaMap[chooseCity]=forecasts
            mWeatherView?.setWeatherList(forecasts)
        } else {
            mWeatherView?.setWeatherList(forecastsData)
        }
    }
}

看看WeatherView代码:

class WeatherView : View {

    constructor(context: Context?) : super(context,null)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs,0)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) :
            super(context, attrs, defStyleAttr) {
        init()
    }

    private val mPaint=Paint()
    private val dataList= mutableListOf<ForecastsData>()

    private var width = 0
    private var height = 0


    private fun init() {
        mPaint.color = ContextCompat.getColor(context, R.color.black)
        mPaint.isAntiAlias=true
        mPaint.textSize=50f
        mPaint.typeface=Typeface.DEFAULT_BOLD
    }

    //设置数据源
    fun setWeatherList(dataList : List<ForecastsData>?){
        this.dataList.clear()
        if (!dataList.isNullOrEmpty()) {
            this.dataList.addAll(dataList)
        }
        invalidate()
    }

    @SuppressLint("DrawAllocation")
    override fun onDraw(canvas: Canvas?) {
        super.onDraw(canvas)
        if (canvas == null || dataList.size == 0) return
        if (width == 0 || height == 0) return
        init()
        var weatherMax=-1000
        var weatherMin=1000
        for (data in dataList) { //寻找最大最小温差
            data.dayTempInt = data.dayTemp.split("")[0].toInt()
            data.nightTempInt = data.nightTemp.split("")[0].toInt()
            if (data.dayTempInt > weatherMax) weatherMax = data.dayTempInt
            if (data.nightTempInt < weatherMin) weatherMin = data.nightTempInt
        }
        var temperatureLevel = 0  //温差值
        if (weatherMax > 0 && weatherMin >= 0) temperatureLevel = weatherMax - weatherMin
        if (weatherMax > 0 && weatherMin < 0) temperatureLevel = weatherMax + abs(weatherMin)
        if (weatherMax < 0 && weatherMin < 0) temperatureLevel = abs(weatherMax) - abs(weatherMin)

        val lineProportion =getDivide(100*1f,temperatureLevel*1f)//折线占比
        val itemWidth = width / dataList.size //获得每一个的平均宽度
        val itemHeight = height / 10 //获得每一个的平均宽度

        //记录需要连线的坐标
        val listDayX= mutableListOf<Float>()
        val listDayY= mutableListOf<Float>()
        val listNightX= mutableListOf<Float>()
        val listNightY= mutableListOf<Float>()


        for (i in dataList.indices) {  //回执每一个数据
            val data = dataList[i]
            val left=itemWidth * i+itemWidth/4f
            val date = data.date.substring(data.date.indexOf("-") + 1)
            canvas.drawText(date, left, itemHeight / 2f, mPaint) //绘制日期
            canvas.drawText(data.getWeek(), left,  itemHeight*1f, mPaint) //绘制礼拜
            val weatherBitmap = WeatherBitmapUtils.getWeatherBitmap(context,data.dayWeather) //绘制白天天气图标
            //这是图标的截取的位置
            val rect1 = Rect(0, 0, weatherBitmap.width, weatherBitmap.height)
            //这是在画布上的位置
            val rect2 = Rect(
                itemWidth * i + itemWidth / 4,
                (itemHeight * 1.5).toInt(),
                itemWidth * i + itemWidth / 4 + weatherBitmap.width,
                (itemHeight * 1.5).toInt() + weatherBitmap.height)
            canvas.drawBitmap(weatherBitmap,rect1,rect2,mPaint) //绘制白天天气图标
            canvas.drawText(data.dayWeather, left,  itemHeight*2.0f+weatherBitmap.height, mPaint) //绘制白天天气说明
            canvas.drawText(data.dayTemp, left,  itemHeight*2.5f+weatherBitmap.height, mPaint) //绘制白天温度

            mPaint.color = ContextCompat.getColor(context, R.color.yellow)//绘制原点,把画笔换成蓝色
            var height = itemHeight * 3f + weatherBitmap.height  //这是坐标开始出,这涉及到手机上的坐标表示
            //先把温度值,都减去最小的一个值,那么坐标从0开始计算, dayTemp展示的值市字符串,不会变动
            data.dayTempInt=data.dayTempInt-weatherMin
            data.nightTempInt=data.nightTempInt-weatherMin
            var heightNew =height+ (100 - data.dayTempInt * lineProportion).toInt()
            canvas.drawCircle(left+weatherBitmap.width/2f,heightNew,8f,mPaint) //绘制白天温度的圆坐标
            listDayX.add(left+weatherBitmap.width/2f)  //记录坐标
            listDayY.add(heightNew*1f)

            heightNew =height+ (100 - data.nightTempInt * lineProportion).toInt()
            mPaint.color = ContextCompat.getColor(context, R.color.blue)
            canvas.drawCircle(left+weatherBitmap.width/2f,heightNew,8f,mPaint) //绘制晚上温度的圆坐标
            listNightX.add(left+weatherBitmap.width/2f) //记录坐标
            listNightY.add(heightNew*1f)
            mPaint.color = ContextCompat.getColor(context, R.color.black) //绘制原点结束把画笔换成黑色

            height =itemHeight * 3f + weatherBitmap.height+ 150 //越过绘制图像区域,接着绘制晚上的信息
            canvas.drawText(data.nightTemp, left,  itemHeight*0.5f+height, mPaint) //绘制晚上温度
            canvas.drawText(data.nightWeather, left,  itemHeight*1f+height, mPaint) //绘制晚上天气表述

            val nightWeatherBitmap = WeatherBitmapUtils.getWeatherBitmap(context,data.nightWeather) //绘制晚上天气图标
            //这是图标的截取的位置
            val rect3 = Rect(0, 0, nightWeatherBitmap.width, nightWeatherBitmap.height)
            //这是在画布上的位置
            val top = (height + (itemHeight * 1.5)).toInt()
            val rect4 = Rect(left.toInt(), top, left.toInt() + nightWeatherBitmap.width, top + nightWeatherBitmap.height)
            canvas.drawBitmap(weatherBitmap,rect3,rect4,mPaint) //绘制晚上天气图标

            height += (itemHeight * 2.0f) + nightWeatherBitmap.height // /越过晚上绘制图像区域
            canvas.drawText(data.nightWindDirection+"",left,height,mPaint) //晚上风力描述

            height+=itemHeight*0.5f
            canvas.drawText(data.nightWindPower,left,height,mPaint)  //晚上风力等级
        }

        val mPaint=Paint() //绘制原点,把画笔换成蓝色
        mPaint.style =Paint.Style.STROKE
        mPaint.strokeWidth = 5f
        mPaint.isAntiAlias=true
        mPaint.color = ContextCompat.getColor(context, R.color.yellow)
        for (i in listDayX.indices) {  //绘制白天的折线
            if (i + 1 < listDayX.size) {
                canvas.drawLine(listDayX[i],listDayY[i],listDayX[i+1],listDayY[i+1],mPaint)
            }
        }
        mPaint.color = ContextCompat.getColor(context, R.color.blue)
        for (i in listNightX.indices) {  //绘制晚上的折线
            if (i + 1 < listNightX.size) {
                canvas.drawLine(listNightX[i],listNightY[i],listNightX[i+1],listNightY[i+1],mPaint)
            }
        }
     }

    private fun getDivide(int1 : Float,int2 : Float) : Float{
        val bd1 = BigDecimal(int1.toDouble())
        val bd2 = BigDecimal(int2.toDouble())  //折线占比
       return bd1.divide(bd2, 2, RoundingMode.HALF_UP).toFloat()
    }


    //设置View 的的大小
    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        // 获取测量模式和大小
        val widthMode = MeasureSpec.getMode(widthMeasureSpec)
        val widthSize = MeasureSpec.getSize(widthMeasureSpec)
        val heightMode = MeasureSpec.getMode(heightMeasureSpec)
        val heightSize = MeasureSpec.getSize(heightMeasureSpec)
        // 根据模式设置最终宽高
        val width = getDefaultSize(suggestedMinimumWidth, widthMeasureSpec)
        val height = getDefaultSize(suggestedMinimumHeight, heightMeasureSpec)
        setMeasuredDimension(width, height)
    }

    //设置获得View 的的大小
    override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
        super.onSizeChanged(w, h, oldw, oldh)
        width = w
        height = h
    }





}

由于WeatherBitmapUtils中图片资源过多:

WeatherBitmapUtils.getWeatherBitmap(context,data.dayWeather)请使用

BitmapFactory.decodeResource(context.resources, resourcesId)代替:

resourcesId是你的图片资源。

4.看看协程调度异常捕捉:MyExceptionCatch

object MyExceptionCatch {

    private val lineSeparator = System.lineSeparator() //换行
    val exceptionMainCatch = Dispatchers.Main + CoroutineExceptionHandler { _, throwable ->
        val stringBuffer = StringBuffer()
        stringBuffer.append("使用协程异常:")
        stringBuffer.append(lineSeparator)
        stringBuffer.append(throwable.message.toString())
        stringBuffer.append(lineSeparator)
        for (data in throwable.stackTrace) {
            stringBuffer.append(data.toString())
            stringBuffer.append(lineSeparator)
        }
        Log.d("TTTTTTTTTTTTTTTT", "协程异常:${stringBuffer.toString()}")
    }

    val exceptionIoCatch = Dispatchers.IO + CoroutineExceptionHandler { _, throwable ->
        val stringBuffer = StringBuffer()
        stringBuffer.append("使用协程异常:")
        stringBuffer.append(lineSeparator)
        stringBuffer.append(throwable.message.toString())
        stringBuffer.append(lineSeparator)
        for (data in throwable.stackTrace) {
            stringBuffer.append(data.toString())
            stringBuffer.append(lineSeparator)
        }
        Log.d("TTTTTTTTTTTTTTTT", "协程异常:${stringBuffer.toString()}")
    }

}

5.接下来我们看看新闻模块代码:NewSet()

//全局记录新闻数据
private val hashMapList=HashMap<String, List<NewsListData>>()

@SuppressLint("CoroutineCreationDuringComposition", "MutableCollectionMutableState")
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NewSet(selectIndexNew : Int,callBack : ((Int)->Unit)) {

    //这是Tab数据
    val dataList by remember { mutableStateOf(SnapshotStateList<TabNewsData>()) }
    //记录所在页数
    val pagerState = remember { PagerState(selectIndexNew) }
    //记录选中的Tab是第几个,
    var selectIndex by remember { mutableStateOf(selectIndexNew) }
    //定义协程
    val coroutineScope = remember { CoroutineScope(Dispatchers.Main) }
    //pagerState 变化 监听
    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }.collect {
            if (it!=selectIndex){
                selectIndex = it  //如果页数变更,那么tab也要变更,同时也要把选择中tab,回调给调用者做记录
                callBack.invoke(selectIndex)
            }
        }
    }

    if (dataList.isEmpty()) { //如果 没有Tab数据,就去获取Tab数据
        coroutineScope.launch(MyExceptionCatch.exceptionMainCatch) {
            val tabNews = withContext(MyExceptionCatch.exceptionIoCatch){
                RetrofitManager.apiServer.getTabNews() //接口同上一章MVVM的一样,这里不做讲解
            }
            if (tabNews.code == 1) dataList.addAll(tabNews.data) //拿到数据
        }
    }

    //线性垂直布局
    Column(modifier = Modifier
        .fillMaxSize()
        .background(Color.White)) {
        //线性水平多Item布局,根据数据大小创建不同的Item
        LazyRow(modifier = Modifier.fillMaxWidth()) {
            items(dataList){
                //每一个Item     //线性垂直布局 一个展示文本内容  ,一个用于表示是否选择的状态线
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text(it.typeName, modifier = Modifier
                        .padding(start = 10.dp, end = 10.dp, top = 20.dp)
                        .clickable {
                            for (i in dataList.indices) {
                                if (dataList[i].typeName == it.typeName && i!=selectIndex) {
                                    selectIndex = i
                                    callBack.invoke(selectIndex)
                                    break
                                }
                            }
                            coroutineScope.launch { pagerState.scrollToPage(selectIndex) }
                        }, color = Color.Black)
                    if (it.typeName==dataList[selectIndex].typeName){
                        Text("选中", Modifier
                                .background(Color.Red)
                                .fillMaxWidth()
                                .height(2.dp) //高度只有2dp,用户展示下划线效果
                                .padding(top = 10.dp))
                    }
                }

            }
        }
        //添加pager
        HorizontalPager(dataList.size,
            Modifier.fillMaxWidth().weight(1f), pagerState, userScrollEnabled = false) { page ->
            //选中某个Pager,根据TabTabNewsData中的id,获取展示的数据
                ItemPagerContent(dataList[selectIndex]) //当页数变化的时候,展示 ItemPagerContent内容
        }
    }
}

@OptIn(ExperimentalCoilApi::class)
@SuppressLint("MutableCollectionMutableState", "CoroutineCreationDuringComposition")
@Composable
fun ItemPagerContent(data: TabNewsData) {
  //定义一个协程
    val coroutineScope = remember { CoroutineScope(Dispatchers.Main) }
    //定义状态变化数据源
    val dataList by remember { mutableStateOf(SnapshotStateList<NewsListData>()) }
    //从缓存获取数据
    val newsList  = hashMapList[data.typeId.toString()]
    if (newsList.isNullOrEmpty()){ //缓存没有数据,则从接口获取数据
        coroutineScope.launch(MyExceptionCatch.exceptionIoCatch){
            Thread.sleep(1000) //延时1000的原因,是因为,接口不能短时调用多次(接口被做了限制);实际开发中,不会出现
            val newsList1 = withContext(MyExceptionCatch.exceptionIoCatch){
                RetrofitManager.apiServer.getNewsList(data.typeId.toString(), "1")
            }
            if (newsList1.code==1){
                hashMapList[data.typeId.toString()]=newsList1.data
                dataList.clear()
                dataList.addAll(newsList1.data)
            }
        }
    }else{
        dataList.clear()
        dataList.addAll(newsList)
    }
    //线性垂直多Item布局
    LazyColumn{
        //每一item,可以在点击事件中跳转到新的界面展示详情,我这边就不做详细展示了
        items(dataList){
            //每一Item 都是一个线性垂直布局,分别是TextImage TextText
            Column(modifier = Modifier.fillMaxWidth()) {
                Text(text = it.title,Modifier.padding(10.dp)) //展示文字
                BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
                    //这是加载网络布局,在compose ImageView中讲过
                   // implementation ("io.coil-kt:coil-compose:1.3.2") 需要加载依赖
                    val imageUrl =if (!it.imgList.isNullOrEmpty()) it.imgList[0]
                        else "http://cms-bucket.ws.126.net/2024/0801/9f68a330p00shivps000zc0009c0070c.png"
                    val image = rememberImagePainter(imageUrl)
                    Image(painter = image, contentDescription = null,
                        modifier = Modifier
                            .fillMaxWidth()
                            .height(120.dp))
                }
                //新闻描述
                Text(text = it.digest,Modifier.padding(10.dp))
                //信息来源+时间
                Text(text = it.source+" "+it.postTime,
                    Modifier
                        .padding(10.dp)
                        .fillMaxWidth(), textAlign = TextAlign.End)
            }
        }
    }

}

这个只是简单使用;,这里有很多地方都可以优化的,大家学习可以自行优化代码。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值