前言:我们依旧使用上一章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,根据Tab的TabNewsData中的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 都是一个线性垂直布局,分别是Text,Image Text,Text
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)
}
}
}
}
这个只是简单使用;,这里有很多地方都可以优化的,大家学习可以自行优化代码。