Android之中美PK,赛事PK对比图Compose实现

1,463 阅读8分钟

可视化中PK图表,赛事PK图,可以很直观的反应出输赢

可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现

一、前言

数据对比分析具有重要的意义,做成可视化大屏可以很直接的反映出双方的差距

如下:中国和美国各项数据动态对比,这个是可以配置音乐做成视频的 2222.gif

在比如:以下是在NBA中国官方网站找的数据:

  • 球队对比

3333.gif

  • 球员对比

55555.gif

  • 动态两个球队数据对比 ,这种动态也是可以配置音乐做成视频的

ezgif-5-7efe535427.gif

二、数据模型设计

1. ChartPKBaseModel,PK的基本数据模:
包含了表格PK左边和右边的PK方名字及相关图片
包含左边赢了条形颜色,右边赢了条形颜色,输了的条形颜色,输了之后剩余部分条形颜色,还有两个相等时候的条形颜色
包含了最基础的,上下左右偏移,条形间隔,动画时长,第一次动画执行延迟时间,
包含了设置条形宽度,如果不设置默认为0,会自动根据对比的条数按照UI高度自动平分
如果是动态动画,可配置背景音乐,背景图片

open class ChartPKBaseModel {
    var pkLeftName: String = "" //pk名字1
    var pkLeftImgUrl: String = ""//pk名字1图片
    var pkRightName: String = ""//pk名字2
    var pkRightImgUrl: String = ""//pk名字2图片

    @Stable
    var win1Color: Color = Color.Magenta  //选手1赢了条形颜色

    @Stable
    var win2Color: Color = Color.Red //选手2赢了条形颜色

    @Stable
    var loseColor: Color = Color.Gray  //输了条形颜色

    @Stable
    var otherBgColor: Color = Color.LightGray//剩余条形颜色

    @Stable
    var eqColor: Color = Color.Green //相等时条形颜色
    var offsetHeight: Float = 0f//最上最下间隔
    var offsetLeft: Float = 30f //左边间隔
    var offsetRight: Float = 30f //右边间隔
    var marginDiv: Float = 0f//对比条间隔颜色
    var durationMillis: Int = 1000 // 动画时长
    var animateDelay: Long = 1000 //动画延迟执行时间
    var barSize: Float = 0f //如果设置了,就不自动根据控件最大高度自动计算

    var isPlayComplete = false
    var musicUrl = ""//背景音乐,可配置网络链接
    var bgUrl = ""
}

2. DynamicPKModel:普通对比数据模型: 即是上面图中:球队对比的模型数据
之包含了总共pk多少项的数据list
pkItemNum:UI界面显示对比项个数
还有中间PK单项名称所占的宽度

class DynamicPKModel(
   val list: MutableList<DynamicPKBarBean>,//总共pk多少项
   val pkItemNum: Int = list.size, //UI界面显示对比项个数
   var centerWidth: Float = 160f//中间宽度
) : ChartPKBaseModel()

3. DynamicPKRoleModel:带角色图片的PK模型
即为上面 球员对比的数据模型:
只是多了左右的两个图片的配置

class DynamicPKRoleModel(
    val list: MutableList<DynamicPKBarBean>,//总共pk多少项
    val pkItemNum: Int = list.size, //UI界面显示对比项个数
    val mapLeftImage: MutableMap<Int, DynamicImage>,
    val mapRightImage: MutableMap<Int, DynamicImage>
) : ChartPKBaseModel()

4. DynamicPKBarBean:PK单项的数据内容
包含:PK项名称
左边 选手1Pk项目得分
右边 选手2Pk项目得分
左边 选手1角色名称
左边 选手1角色图片 右边 选手2角色名称
右边 选手2角色图片
还有可配置的数字格式化设置,因为有些显示是数字值,有些还带比如%的,有些显示有小数等

class DynamicPKBarBean(
    val pkName: String, //PK项名称
    val value1: Float,  //选手1Pk项目得分
    val value2: Float,  //选手2Pk项目得分
    val role1Name: String? = null, //选手1角色名称
    val role1ImgUrl: String? = null,//选手1角色图片
    val role2Name: String? = null, //选手2角色名称
    val role2ImgUrl: String? = null //选手2角色图片
) {
    var formatString: String = ""//数字格式化设置
    var multiplier: Float = 1f//数据显示格式所用的乘数
    var enfBuff: String = ""

    fun getTextValueFormat(value: Float): String {
        return "${formatString?.format(value * multiplier) ?: value.toString()}$enfBuff"
    }
}

5. DynamicImage:角色图片配置项
包含:图标地址,需要绘制的bitmap

data class DynamicImage(
    val imgUrl: String,  //每个条形图可配置的图标地址
    var bitmap: ImageBitmap //需要绘制的bitmap
)

三、真正的调用

1、repositories中添加如下maven

    repositories {
        maven { url 'https://repo1.maven.org/maven2/' }
        maven { url 'https://s01.oss.sonatype.org/content/repositories/releases/' }
    }
}

2、 dependencies中添加依赖

implementation("io.github.wgllss:Wgllss-WXChart:1.0.16")

3. Android的ViewModel中数据准备:
这里可以是网络数据返回,转化秤准备的模型数据即可。


private val _datas3 = MutableLiveData<DynamicPKModel>()
val dynamicPKModel: LiveData<DynamicPKModel> = _datas3

fun setData3() {
        val dynamicPKModel = DynamicPKModel(
            list = mutableListOf(DynamicPKBarBean(
                "得分", 107f, 128f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "篮板", 44f, 49f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "助攻", 28f, 35f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "抢断", 4f, 8f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "盖帽", 5f, 6f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "失误", 14f, 11f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }, DynamicPKBarBean(
                "投篮命中率", 0.451f, 0.526f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "三分命中率", 0.375f, 0.545f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "罚球命中率", 0.625f, 1f
            ).apply {
                formatString = "%.1f" //数字格式化设置
                multiplier = 100f //数据显示格式所用的乘数
                enfBuff = "%"
            }, DynamicPKBarBean(
                "时间", 48f, 48f
            ).apply {
                formatString = "%.0f" //数字格式化设置
                multiplier = 1f //数据显示格式所用的乘数
            }), centerWidth = toDp(110f)
        ).apply {
            pkLeftName = "马刺"
            pkLeftImgUrl = "https://search-operate.cdn.bcebos.com/5305d1a7b721b5bef418041eff53ba82.png"
            pkRightName = "热火"
            pkRightImgUrl = "https://search-operate.cdn.bcebos.com/ff7ccef6a6b79c6417ee8367946b0aec.png"
            win1Color = Color.Magenta
            win2Color = Color.Red
            loseColor = Color.Gray
            otherBgColor = Color.LightGray
            eqColor = Color.Green
            offsetLeft = toDp(10f)
            offsetRight = toDp(10f)
            marginDiv = toDp(10f)
//            barSize = toDp(20f)
            musicUrl = "asset:///vv.mp3" //背景音乐,可配置网络链接
        }
        _datas3.value = dynamicPKModel
    }

4. Compose中使用方调用:
直接调用:

球队对比绘制: fun vSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel)
stylePkName:中间pk项配置文字的样式
style:条形数字配置文字样式

球员对比绘制调用:
@Composable fun vSWithRoleChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKRoleModel)

动态对比调用:
@Composable fun dynamicVSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel, onPlayComplete: (() -> Unit)? = null) {

球队对比调用绘制全部代码如下:

@Composable
fun pkChart(viewModel: DynamicViewModel = DynamicViewModel().apply { setData3() }) {
    val textMeasurer = rememberTextMeasurer()
    val context = LocalContext.current
    val chatModel by viewModel.dynamicPKModel.observeAsState()
    chatModel?.let {
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .height(600.dp)
//                .fillMaxHeight()
        ) {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(80.dp), verticalAlignment = Alignment.CenterVertically,   //整体垂直居中
                horizontalArrangement = Arrangement.Center                 //整体水平居中
            ) {
                Text(text = it.pkLeftName, fontSize = 30.sp)
                AsyncImage(
                    modifier = Modifier
                        .size(60.dp)
                        .padding(10.dp, 0.dp, 0.dp, 0.dp), model = it.pkLeftImgUrl, contentDescription = "", contentScale = ContentScale.Crop
                )
                Text(text = "VS", fontSize = 36.sp, modifier = Modifier.width(110.dp), textAlign = TextAlign.Center)
                AsyncImage(
                    modifier = Modifier.size(60.dp), model = it.pkRightImgUrl, contentDescription = "", contentScale = ContentScale.Crop
                )
                Text(text = it.pkRightName, fontSize = 30.sp, modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp))
            }
            val modifier = Modifier
                .fillMaxWidth()
                .fillMaxHeight()
            vSChart(
                modifier, textMeasurer, TextStyle(
                    fontSize = 16.sp, fontWeight = FontWeight.Normal, color = Color.Black
                ), TextStyle(
                    fontSize = 16.sp, fontWeight = FontWeight.Normal, color = Color.White
                ), it
            )
        }
    }
}

四、绘制原来解析:

真正的绘制:(以球队对比绘制如下全部代码为例)

  1. 通过UI高度计算出每一项所占的高度 heightDiv
  2. 通过左右边距及中间所占宽度,计算剩余总共可用宽度 availableWidth
  3. 计算出左右两个中的最大值,就是左边最大宽度,和右边最大宽度,每个数字值所占的UI宽度比例为: val widthAbs = availableWidth / (2 * Math.max(vs.value1, vs.value2))
  4. 需要判断出左右那个大,那个小,还是相等,便可以计算出:赢了的填满最大宽度 的一半:availableWidth/2,输了的,计算出输了的差值,得到其他颜色和失败的条形种的位置。
  5. 没有什么难度,基本就是小学数学计算逻辑

@Composable
fun vSChart(modifier: Modifier, textMeasurer: TextMeasurer, stylePkName: TextStyle, style: TextStyle, it: DynamicPKModel) {
    val context = LocalContext.current
    var mSize by remember { mutableStateOf(Size(0f, 0f)) }

    val width = mSize.width
    val height = mSize.height
    val availableWidth = width - it.offsetLeft - it.offsetRight - it.centerWidth
    val fontSizeDip = DisplayUtil.sp2dp(context, style.fontSize.value)
    val heightDiv = if (it.barSize > 0) it.barSize else (height - 2 * it.offsetHeight - (it.pkItemNum - 1) * it.marginDiv) / it.pkItemNum
    val fontHegitVcenterOffset = heightDiv / 2 - (fontSizeDip + 0.5f) / 2

    var start by remember { mutableStateOf(false) }
    val animatedBar by animateFloatAsState(targetValue = if (start) 1f else 0f, animationSpec = FloatTweenSpec(it.durationMillis))
    val leftanimate = 1f - animatedBar
    LaunchedEffect(Unit) {
        delay(it.animateDelay)
        start = true
    }

    Canvas(modifier = modifier) {
        mSize = size
        it.list.forEachIndexed { index, vs ->
            if (start) {
                val dl = vs.pkName.length * fontSizeDip
                drawText(textMeasurer = textMeasurer, topLeft = Offset(width / 2 - dl / 2, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.pkName, style = stylePkName)
                val widthAbs = availableWidth / (2 * Math.max(vs.value1, vs.value2))
                if (vs.value1 > vs.value2) {
                    drawRect(it.win1Color, topLeft = Offset(it.offsetLeft + (availableWidth / 2 * leftanimate), index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))
                    drawRect(it.loseColor, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(vs.value2 * widthAbs * animatedBar, heightDiv))
                    drawRect(it.otherBgColor, topLeft = Offset(width - it.offsetRight - (vs.value1 - vs.value2) * widthAbs * animatedBar, index * heightDiv + (index - 1) * it.marginDiv), size = Size((vs.value1 - vs.value2) * widthAbs * animatedBar, heightDiv))

                    drawText(textMeasurer = textMeasurer, topLeft = Offset(it.offsetLeft + fontSizeDip + (availableWidth / 2 - fontSizeDip) * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value1), style = style)
                    val dl2 = vs.getTextValueFormat(vs.value2).length * fontSizeDip / 2 + fontSizeDip
                    drawText(textMeasurer = textMeasurer, topLeft = Offset(width / 2 + it.centerWidth / 2 + (vs.value2 * widthAbs - dl2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value2 * animatedBar), style = style)
                } else if (vs.value1 < vs.value2) {
                    drawRect(it.otherBgColor, topLeft = Offset(it.offsetLeft, index * heightDiv + (index - 1) * it.marginDiv), size = Size((vs.value2 - vs.value1) * widthAbs * animatedBar, heightDiv))
                    drawRect(it.loseColor, topLeft = Offset(it.offsetLeft + (vs.value2 - vs.value1) * widthAbs + vs.value1 * widthAbs * leftanimate, index * heightDiv + (index - 1) * it.marginDiv), size = Size(vs.value1 * widthAbs * animatedBar, heightDiv))
                    drawRect(it.win2Color, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))

                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(it.offsetLeft + fontSizeDip + (vs.value2 - vs.value1) * widthAbs + vs.value1 * widthAbs * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value1),
                        style = style
                    )
                    val dl2 = vs.getTextValueFormat(vs.value1).length * fontSizeDip / 2 + fontSizeDip
                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(width / 2 + it.centerWidth / 2 + (width - it.offsetRight - dl2 - width / 2 - it.centerWidth / 2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value2 * animatedBar),
                        style = style
                    )
                } else {
                    drawRect(it.eqColor, topLeft = Offset(it.offsetLeft + (availableWidth / 2 * leftanimate), index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))
                    drawRect(it.eqColor, topLeft = Offset(width / 2 + it.centerWidth / 2, index * heightDiv + (index - 1) * it.marginDiv), size = Size(availableWidth / 2 * animatedBar, heightDiv))

                    drawText(textMeasurer = textMeasurer, topLeft = Offset(it.offsetLeft + fontSizeDip + (availableWidth / 2 - fontSizeDip) * leftanimate, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset), text = vs.getTextValueFormat(vs.value1), style = style)
                    val dl2 = vs.getTextValueFormat(vs.value1).length * fontSizeDip / 2 + fontSizeDip
                    drawText(
                        textMeasurer = textMeasurer,
                        topLeft = Offset(width / 2 + it.centerWidth / 2 + (width - it.offsetRight - dl2 - width / 2 - it.centerWidth / 2) * animatedBar, index * heightDiv + (index - 1) * it.marginDiv + fontHegitVcenterOffset),
                        text = vs.getTextValueFormat(vs.value2),
                        style = style
                    )
                }
            }
        }
    }
}

五、总结

本文全重点介绍了 PK图的绘制,包括三种样式:

  1. 球队比赛PK绘制
  2. 球队比赛中球员各项最大值对比
  3. 动态对比各项数据

已经封装成库,你只需要准备好数据就可以了。

github地址
gitee地址

感谢阅读:

欢迎 关注,点赞、收藏

这里你会学到不一样的东西