可视化中等级区分金字塔,很直观的反映出等级梯度
可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现
(九)Android Compose轻松绘制地图可视化图表,带点击事件,可扩展二次开发
一、前言
智慧数据研究对比,经常会把某项指标或者综合指标所得到的分数相近的划分为不同等级,或者说成不同档次,而这种根据数据划分一直以来常常是人们主观手动来来划分。研究数据到了极致需要智能划分,可以设置几个数据分界点,或者更加出来的数据集按照比例可以划分为好几个档次,比如:根据全国各大高铁站数据客流量,查询出数据后可以自动就可以分为好几个档次。固实现了一套可以自动生成等级金字塔的图表。
都是可以配置音乐做成视频的
比如:2024年GAWC研究得出世界城市等级金字塔划分如下:
你定居的城市在等级金字塔哪一次层呢?
gif太大了80多M,只截取了上面2层的gif
再比如:多家权威媒体研究得出中国城市等级金字塔划分如下:
你定居的城市在等级金字塔哪一次层呢?
再来比如,体育媒体多家公认的顶尖NBA球员能力金字塔划分如下:
你喜欢的球员在等级金字塔哪一次层呢?
二、数据模型设计
(一) 纯文字金字塔
ChartPyramidModel
,数据集只有文字的模型
list
:包含了金字塔等级个数:list.size
intervalSize
: 金字塔两个之间间隔
topMargin
:顶部偏移
buttomMargin
:底部偏移
firstMarginTop
:唯一的第一层三角形,文字上偏移
bgUrl
:背景图片地址
realSize
:如果下面几层设置成矩形,真正金字塔层数要减掉那几层
open class ChartPyramidModel(
open val list: MutableList<ChartPyramidBean>,//金字塔等级个数
open val intervalSize: Float,//金字塔两个之间间隔
) {
var topMargin = 0f
var buttomMargin = 0f
var firstMarginTop = 0f
var bgUrl: String = ""
val realSize: Int
get() {
var count = 0
list.forEach {
if (!it.isRectangle) {
count++
}
}
return count
}
}
ChartPyramidBean类
:
包含等级绘制的背景颜色,
单个金字塔等级内的数据集,
该等级里面字体样式,
金字塔最下面2~3层可能绘制成矩形而不是等腰梯形
data class ChartPyramidBean(
@Stable val colorBg: Color, //单个金字塔等级的背景颜色
val datas: MutableList<String>,//单个金字塔等级内的数据集
val textStyle: TextStyle,//该等级里面字体样式
val isRectangle: Boolean = false//金字塔最下面2~3层可能绘制成矩形而不是等腰梯形
)
(二) 类似NBA球员图片金字塔数据模型
ChartPyramidRoleModel
包含,金字塔等级个数,金字塔两个之间间隔
mapImage
:存放金字塔图片imageBitmap
,用于实时加载网络图片
open class ChartPyramidRoleModel(
open val list: MutableList<ChartPyramidRoleBean>,//金字塔等级个数
open val intervalSize: Float,//金字塔两个之间间隔
open val mapImage: MutableMap<String, ImageBitmap>
) {
var topMargin = 0f
var buttomMargin = 0f
var firstMarginTop = 0f
var bgUrl: String = ""
val realSize: Int
get() {
var count = 0
list.forEach {
if (!it.isRectangle) {
count++
}
}
return count
}
}
ChartPyramidRoleBean
相比于纯文字金字塔,多了一个等级标题title
,datas
内数据,item数据是ImageKey:它包含名称和图片
data class ChartPyramidRoleBean(
@Stable val colorBg: Color, //单个金字塔等级的背景颜色
val title: String,//等级名称
val datas: MutableList<ImageKey>,//单个金字塔等级内的数据集
val textStyle: TextStyle,//该等级里面字体样式
val isRectangle: Boolean = false//金字塔最下面两层可能绘制成矩形而不是等腰梯形
)
data class ImageKey(
val imgUrl: String, //每个条形图可配置的图标地址
val name: String = ""
)
(三) 多功能内容金字塔,世界城市金字塔数据模型
ChartPyramidMultiModel
:该类模型,承载了,等级标题,等级类数据集,可以展示一行,也可以展示二行,数据集内容是上面图标,下面名称,图标宽度高度固定,但是名称文字长度不固定,文字样式大小等可以单独设置,数据集所在行内位置会根据当前行中所有数据集文字所占总宽度,来平分当前所在等级位置图表宽度。然后计算出单个所在位置。如果是2行可以配置第一行显示多少个(注意:这里不是当前等级下数据集除以2来平分,因为可能数据集名称文字长度太多,导致平分后,宽度显示不下,固配置分配,可单独配置文字样式大小等)
open class ChartPyramidMultiModel(
open val list: MutableList<ChartPyramidMultiBean>,//金字塔等级个数
open val intervalSize: Float,//金字塔两个之间间隔
open val mapImage: MutableMap<String, ImageBitmap>
) {
var topMargin = 0f
var buttomMargin = 0f
var firstMarginTop = 0f
var bgUrl: String = ""
val realSize: Int
get() {
var count = 0
list.forEach {
if (!it.isRectangle) {
count++
}
}
return count
}
}
ChartPyramidMultiBean
:
showLineCount
: 数据集展示第一行个数,剩余的展示到第二行,最多2行,只配置一行这个值等于list.size
textTitleStyle
: 该等级里面字体样式
textStyle
:该等级里面字体样式
textCharCount
:等级数据集里面第一行名称字符总数个数(如果配置一行就是list下所有文字字符总个数)
textCharCount2
:如果配置了2行:该值为第二行所有字符总数
data class ChartPyramidMultiBean(
@Stable val colorBg: Color, //单个金字塔等级的背景颜色
val title: String,//等级名称
val datas: MutableList<ImageKey>,//单个金字塔等级内的数据集
val showLineCount: Int,//数据集展示第一行个数,剩余的展示到第二行,最多2行
val textTitleStyle: TextStyle,//该等级里面字体样式
val textStyle: TextStyle = TextStyle(
fontSize = 9.sp, fontWeight = FontWeight.Bold, color = Color.White
),//该等级里面字体样式
val isRectangle: Boolean = false//金字塔最下面两层可能绘制成矩形而不是等腰梯形
) {
val textCharCount: Int
get() {
val sb = StringBuilder()
datas.forEachIndexed { index, it ->
if (index >= showLineCount) {
return@forEachIndexed
}
sb.append(it.name)
}
return getStrPhysicsLength(sb.toString())
}
val textCharCount2: Int
get() {
val sb = StringBuilder()
datas.forEachIndexed { index, it ->
if (index >= showLineCount) {
sb.append(it.name)
}
}
return getStrPhysicsLength(sb.toString())
}
}
(四) 带动画模型
- 以2024年GAWC研究得出世界城市等级金字塔划分为例
DynamicPyramidMultiModel
:多功能金字塔带动画的模型,多包含了:
durationMillis
: 动画时长
animateDelay
: 动画延迟执行时间
musicUrl
= ""背景音乐,可配置网络链接
animateStyle
= 1, 1:动画从左到右执行,2:动画从上到下执行
isPlayComplete
= false,动画是否执行完成
open class DynamicPyramidMultiModel(
override val list: MutableList<ChartPyramidMultiBean>,//金字塔等级个数
override val intervalSize: Float,//金字塔两个之间间隔
override val mapImage: MutableMap<String, ImageBitmap>
) : ChartPyramidMultiModel(list, intervalSize, mapImage) {
var durationMillis: Int = 3000 // 动画时长
var animateDelay: Long = 1000 //动画延迟执行时间
var musicUrl = ""//背景音乐,可配置网络链接
var animateStyle = 1 //1:动画从左到右执行,2:动画从上到下执行
var isPlayComplete = false
}
三、真正调用端
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中数据准备:
这里可以是网络数据返回,转化秤准备的模型数据即可。
- 以2024年GAWC研究得出世界城市等级金字塔划分为例
class PyramidViewModel : ViewModel() {
private val _datas7 = MutableLiveData<DynamicPyramidMultiModel>()
val pyramidModel7: LiveData<DynamicPyramidMultiModel> = _datas7
fun setData7() {
val titleFontSize = 20.sp
val fontSize1 = 21.sp
val fontSize2 = 12.sp
val fontSize3 = 13.sp
val fontSize4 = 13.sp
val chartPyramidBean1 = ChartPyramidMultiBean(
Color(0x30FF0000), title = "超一线", mutableListOf(
ImageKey(gq("美国"), "纽约"),
ImageKey(gq("英国"), "伦敦"),
), 2, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.Red
), textStyle = TextStyle(
fontSize = fontSize1, fontWeight = FontWeight.Bold, color = Color.White
)
)
val chartPyramidBean2 = ChartPyramidMultiBean(
Color(0x30ABC000), title = "世界强一线城市", mutableListOf(
ImageKey(gq("中国"), "香港"),
ImageKey(gq("中国"), "北京"),
ImageKey(gq("中国"), "上海"),
ImageKey(gq("新加坡"), "新加坡"),
ImageKey(gq("法国"), "巴黎"),
ImageKey(gq("阿联酋"), "迪拜"),
ImageKey(gq("日本"), "东京"),
ImageKey(gq("澳大利亚"), "悉尼"),
), 8, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.Red
), textStyle = TextStyle(
fontSize = fontSize2, fontWeight = FontWeight.Bold, color = Color.White
)
)
val chartPyramidBean3 = ChartPyramidMultiBean(
Color(0x600000FF), title = "世界一线城市-16座", mutableListOf(
ImageKey(gq("韩国"), "首尔"),
ImageKey(gq("意大利"), "米兰"),
ImageKey(gq("中国"), "广州"),
ImageKey(gq("加拿大"), "多伦多"),
ImageKey(gq("荷兰"), "阿姆斯特丹"),
ImageKey(gq("美国"), "芝加哥"),
ImageKey(gq("印度尼西亚"), "雅加达"),
ImageKey(gq("巴西"), "圣保罗"),
ImageKey(gq("马来西亚"), "吉隆坡"),
ImageKey(gq("美国"), "洛杉矶"),
ImageKey(gq("泰国"), "曼谷"),
ImageKey(gq("西班牙"), "马德里"),
ImageKey(gq("土耳其"), "伊斯坦布尔"),
ImageKey(gq("德国"), "法兰克福"),
ImageKey(gq("墨西哥"), "墨西哥城"),
ImageKey(gq("印度"), "孟买"),
), 8, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.Red
), textStyle = TextStyle(
fontSize = fontSize3, fontWeight = FontWeight.Bold, color = Color.White
)
)
val chartPyramidBean4 = ChartPyramidMultiBean(
Color(0x90FFFF00), title = "世界弱一线城市-22座", mutableListOf(
ImageKey(gq("卢森堡"), "卢森堡"),
ImageKey(gq("中国"), "台北"),
ImageKey(gq("中国"), "深圳"),
ImageKey(gq("比利时"), "布鲁塞尔"),
ImageKey(gq("瑞士"), "苏黎世"),
ImageKey(gq("阿根廷"), "布宣诺斯艾利斯"),
ImageKey(gq("澳大利亚"), "墨尔本"),
ImageKey(gq("美国"), "旧金山"),
ImageKey(gq("沙特阿拉伯"), "利雅德"),
ImageKey(gq("智利"), "圣地亚哥"),
ImageKey(gq("印度"), "新德里"),
ImageKey(gq("德国"), "杜塞尔多夫"),
ImageKey(gq("瑞典"), "斯德哥尔摩"),
ImageKey(gq("美国"), "华盛顿"),
ImageKey(gq("德国"), "柏林"),
ImageKey(gq("奥地利"), "维也纳"),
ImageKey(gq("葡萄牙"), "里斯本"),
ImageKey(gq("德国"), "慕尼黑"),
ImageKey(gq("爱尔兰"), "都柏林"),
ImageKey(gq("美国"), "休斯顿"),
ImageKey(gq("南非"), "约翰内斯堡"),
ImageKey(gq("美国"), "波士顿"),
), 11, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.Red
), textStyle = TextStyle(
fontSize = fontSize2, fontWeight = FontWeight.Bold, color = Color.White
)
)
val chartPyramidBean5 = ChartPyramidMultiBean(
Color(0x90FF00FF), title = "世界强二线城市-20座", mutableListOf(
ImageKey(gq("哥伦比亚"), "波哥大"),
ImageKey(gq("越南"), "胡志明"),
ImageKey(gq("意大利"), "罗马"),
ImageKey(gq("印度"), "班加罗尔"),
ImageKey(gq("匈牙利"), "布达佩斯"),
ImageKey(gq("希腊"), "雅典"),
ImageKey(gq("德国"), "汉堡"),
ImageKey(gq("卡塔尔"), "多哈"),
ImageKey(gq("中国"), "成都"),
ImageKey(gq("美国"), "吗哈密"),
ImageKey(gq("中国"), "天津"),
ImageKey(gq("美国"), "达拉斯"),
ImageKey(gq("美国"), "亚特兰大"),
ImageKey(gq("新西兰"), "奥克兰"),
ImageKey(gq("西班牙"), "巴塞罗那"),
ImageKey(gq("中国"), "杭州"),
ImageKey(gq("罗马尼亚"), "布加勒斯特"),
ImageKey(gq("秘鲁"), "利马"),
ImageKey(gq("加拿大"), "蒙特利尔"),
ImageKey(gq("捷克"), "布拉格"),
), 10, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.White
), isRectangle = true, textStyle = TextStyle(
fontSize = fontSize4, fontWeight = FontWeight.Bold, color = Color.White
)
)
val chartPyramidBean6 = ChartPyramidMultiBean(
Color(0x6000FF00), title = "世界二线城市-23座", mutableListOf(
ImageKey(gq("中国"), "重庆"),
ImageKey(gq("以色列"), "特拉维夫"),
ImageKey(gq("澳大利亚"), "布里斯班"),
ImageKey(gq("埃及"), "开罗"),
ImageKey(gq("越南"), "河内"),
ImageKey(gq("中国"), "南京"),
ImageKey(gq("挪威"), "奥斯陆"),
ImageKey(gq("澳大利亚"), "珀斯"),
ImageKey(gq("阿联酋"), "阿布扎比"),
ImageKey(gq("丹麦"), "哥本哈根"),
ImageKey(gq("巴林"), "麦纳麦"),
ImageKey(gq("中国"), "武汉"),
ImageKey(gq("菲律宾"), "马尼拉"),
ImageKey(gq("中国"), "厦门"),
ImageKey(gq("肯尼亚"), "内罗毕"),
ImageKey(gq("乌克兰"), "基辅"),
ImageKey(gq("瑞士"), "日内瓦"),
ImageKey(gq("中国"), "济南"),
ImageKey(gq("加拿大"), "卡尔加里"),
ImageKey(gq("中国"), "郑州"),
ImageKey(gq("中国"), "沈阳"),
ImageKey(gq("中国"), "大连"),
ImageKey(gq("中国"), "苏州"),
), 12, TextStyle(
fontSize = titleFontSize, fontWeight = FontWeight.Bold, color = Color.White
), isRectangle = true, textStyle = TextStyle(
fontSize = fontSize4, fontWeight = FontWeight.Bold, color = Color.White
)
)
val list = mutableListOf(chartPyramidBean1, chartPyramidBean2, chartPyramidBean3, chartPyramidBean4, chartPyramidBean5, chartPyramidBean6)
val intervalSize = toDp(10f)
val mapImage = mutableMapOf<String, ImageBitmap>()
list.forEach {
it.datas.forEach {
mapImage[it.imgUrl] = createImageBitmap()
}
}
val pyramidModel = DynamicPyramidMultiModel(
list, intervalSize, mapImage
).apply {
firstMarginTop = toDp(25f)
bgUrl = "https://q5.itc.cn/q_70/images01/20240723/42bdcb602f35471eadddb09908b683f1.jpeg"
animateStyle = 2 //动画样式
musicUrl = "asset:///vv.mp3" //背景音乐,可配置网络链接
}
_datas7.value = pyramidModel
}
}
4. Compose中使用方调用:
直接调用:
-
多功能金字塔:
不带动画:@Composable fun drawPyramidMultiChart(modifier: Modifier, textMeasurer: TextMeasurer, it: ChartPyramidMultiModel)
带动画:@Composable fun dynamicPyramidMultiChart(modifier: Modifier, textMeasurer: TextMeasurer, it: DynamicPyramidMultiModel, onPlayComplete: (() -> Unit)? = null)
-
头像图片等级金字塔:
不带动画:@Composable fun drawPyramidRoleChart(modifier: Modifier, textMeasurer: TextMeasurer, it: ChartPyramidRoleModel)
带动画:@Composable fun dynamicPyramidRoleChart(modifier: Modifier, textMeasurer: TextMeasurer, it: DynamicPyramidRoleModel, onPlayComplete: (() -> Unit)? = null)
-
纯文字金字塔:
不带动画:@Composable fun drawPyramidChart(modifier: Modifier, textMeasurer: TextMeasurer, it: ChartPyramidModel)
带动画:@Composable fun dynamicPyramidChart(modifier: Modifier, textMeasurer: TextMeasurer, it: DynamicPyramidModel, onPlayComplete: (() -> Unit)? = null)
-
以2024年GAWC研究得出世界城市等级金字塔划分为例
@Composable
fun dynamicPyramidmutilChart(viewModel: PyramidViewModel = PyramidViewModel().apply { setData7() }) {
val chatModel by viewModel.pyramidModel7.observeAsState()
val textMeasurer = rememberTextMeasurer()
val context = LocalContext.current
chatModel?.let {
Box(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
) {
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(), model = it.bgUrl, contentDescription = "", contentScale = ContentScale.Crop
)
val modifier = Modifier
.padding(0.dp, 60.dp, 0.dp, 0.dp)
.fillMaxWidth()
.fillMaxHeight()
dynamicPyramidMultiChart(
modifier, textMeasurer, it
) {
//动画执行结束
}
}
}
}
四、绘制原理解析
真正的绘制(以纯文字等级金字塔为例)
1. 通过UI控件宽度计算出顶点三角形坐标, x:为mSize.center.x,y为0
2. 通过UI控件高度减去所有空隙高度除以等级list.size,计算出每个等级的高度: val divHeight = (height - it.topMargin - it.buttomMargin - (listSize - 1) * it.intervalSize) / listSize
3. 如果设置了下面几层为矩形,真正金字塔三角形高度计算: val pyramidHeight = if (listSize == it.realSize) height else it.realSize * divHeight + (it.realSize - 1) * it.intervalSize
4. 根据比例计算出当前层三角形宽度一半宽度:即:相似三角形: 每一层高度/最大高度 = 当前层三角形底部宽度一半的长度/最大三角形底边一半的长度
5. 没什么难度,小学生比例尺计算逻辑完全够用
@Composable
fun drawPyramidChart(modifier: Modifier, textMeasurer: TextMeasurer, it: ChartPyramidModel) {
val context = LocalContext.current
var mSize by remember { mutableStateOf(Size(0f, 0f)) }
val centerX = mSize.center.x
val height = mSize.height
val listSize = it.list.size
val divHeight = (height - it.topMargin - it.buttomMargin - (listSize - 1) * it.intervalSize) / listSize
//金字塔三角形高度(不含下面矩形)
val pyramidHeight = if (listSize == it.realSize) height else it.realSize * divHeight + (it.realSize - 1) * it.intervalSize
Canvas(modifier = modifier) {
mSize = size
val path = Path()
for (i in 1..listSize) {
val style = it.list[i - 1].textStyle
val fontSizeDip = DisplayUtil.sp2Dip(context, style.fontSize.value)
val fontHegitVcenterOffset = divHeight / 4 - (fontSizeDip + 0.5f)
val textCount = it.list[i - 1].datas.size
val halfCount = (textCount / 2)
val otherCount = textCount - halfCount
if (i == 1) {
val y = i * divHeight
val xd = centerX * (y / pyramidHeight)
val x1 = centerX - xd
val x2 = centerX + xd
path.moveTo(centerX, 0f)
path.lineTo(x1, y)
path.lineTo(x2, y)
path.lineTo(centerX, 0f)
drawPath(path = path, color = it.list[i - 1].colorBg, style = Fill)
it.list[i - 1].datas.forEachIndexed { index, item ->
if (index < halfCount) {
val dl = getStrPhysicsLength(item) * fontSizeDip
if (halfCount == 1) {
drawText(textMeasurer, item, style = style, topLeft = Offset(centerX - dl / 2, (i - 1) * divHeight + fontHegitVcenterOffset + it.firstMarginTop))
} else {
if (index == 0) {
drawText(textMeasurer, item, style = style, topLeft = Offset(centerX - dl - fontSizeDip, (i - 1) * divHeight + 2 * fontHegitVcenterOffset))
} else {
drawText(textMeasurer, item, style = style, topLeft = Offset(centerX + fontSizeDip, (i - 1) * divHeight + 2 * fontHegitVcenterOffset))
}
}
} else {
val dl = getStrPhysicsLength(item) * fontSizeDip
val offsetC = if (index - otherCount <= 0) index - halfCount else index - halfCount
val xDiv = (2 * xd - 2 * fontSizeDip) / (otherCount + 1)
drawText(textMeasurer, item, style = style, topLeft = Offset(x1 + fontSizeDip + xDiv * (offsetC + 1) - dl / 2, (i - 1) * divHeight + divHeight / 2 + fontHegitVcenterOffset))
}
}
} else {
val dev = (i - 1) * it.intervalSize
val y1 = (i - 1) * divHeight + dev
val y2 = i * divHeight + dev
val xd1 = if (!it.list[i - 1].isRectangle) centerX * (y1 / pyramidHeight) else centerX
val xd2 = if (!it.list[i - 1].isRectangle) centerX * (y2 / pyramidHeight) else centerX
val x11 = centerX - xd1
val x12 = centerX + xd1
val x21 = centerX - xd2
val x22 = centerX + xd2
path.moveTo(x11, y1)
path.lineTo(x12, y1)
path.lineTo(x22, y2)
path.lineTo(x21, y2)
path.lineTo(x11, y1)
drawPath(path = path, color = it.list[i - 1].colorBg, style = Fill)
it.list[i - 1].datas.forEachIndexed { index, item ->
if (index < halfCount) {
val dl = getStrPhysicsLength(item) * fontSizeDip
val xDiv = (2 * xd1) / (halfCount + 1)
drawText(textMeasurer, item, style = style, topLeft = Offset(x11 + xDiv * (index + 1) - dl / 2, y1 + fontHegitVcenterOffset))
} else {
val dl = getStrPhysicsLength(item) * fontSizeDip
val offsetC = if (index - otherCount <= 0) index - halfCount else index - halfCount
val xDiv = (2 * xd2 - 2 * fontSizeDip) / (otherCount + 1)
drawText(textMeasurer, item, style = style, topLeft = Offset(x21 + xDiv * (offsetC + 1) - dl / 2, y1 + divHeight / 2 + fontHegitVcenterOffset))
}
}
}
path.reset()
}
path.close()
}
}
五、总结
本文全重点介绍了 等级金字塔,包括三种样式:
- 纯文字等级金字塔
- NBA球员能力等级金字塔
- 多功能等级金字塔(
GAWC研究得出世界城市等级金字塔
)->(含文字,图片,1行,2行,文字长度不固定等都可以自动配置)
已经封装成库,你只需要准备好数据就可以了。