可视化中PK图表,赛事PK图,可以很直观的反应出输赢
可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现
一、前言
数据对比分析具有重要的意义,做成可视化大屏可以很直接的反映出双方的差距
如下:中国和美国各项数据动态对比,这个是可以配置音乐做成视频的
在比如:以下是在NBA中国官方网站找的数据:
- 球队对比
- 球员对比
- 动态两个球队数据对比 ,这种动态也是可以配置音乐做成视频的
二、数据模型设计
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
)
}
}
}
四、绘制原来解析:
真正的绘制:(以球队对比绘制如下全部代码为例)
- 通过UI高度计算出每一项所占的高度
heightDiv
- 通过左右边距及中间所占宽度,计算剩余总共可用宽度
availableWidth
- 计算出左右两个中的最大值,就是左边最大宽度,和右边最大宽度,每个数字值所占的UI宽度比例为:
val widthAbs = availableWidth / (2 * Math.max(vs.value1, vs.value2))
- 需要判断出左右那个大,那个小,还是相等,便可以计算出:赢了的填满最大宽度 的一半:
availableWidth/2
,输了的,计算出输了的差值,得到其他颜色和失败的条形种的位置。 - 没有什么难度,基本就是小学数学计算逻辑
@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图的绘制,包括三种样式:
- 球队比赛PK绘制
- 球队比赛中球员各项最大值对比
- 动态对比各项数据
已经封装成库,你只需要准备好数据就可以了。