用Compose代码写出:全网火爆的排名视频,排名动态可视化
互联网智慧大数据分析,图表,和可视化趋势是一种趋势,分析的利器
可视化图表系列如下:
(一)Compose曲线图表库WXChart,你只需要提供数据配置就行了
(二)Compose折线图,贝赛尔曲线图,柱状图,圆饼图,圆环图。带动画和点击效果
(三)全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
(四)全网最火可视化趋势视频实现深度解析,同时新增条形图表
(五)庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化
(六)Android六边形战士能力图绘制,Compose实现
(七)Android之中美PK,赛事PK对比图Compose实现
(八)Android之等级金字塔之Compose智能实现
上面GIF效果图:是Android Studio模拟器上App中
用Compose写出动画,然后录屏成视频转化后的GIF效果,本身是带音乐背景的。
项目工程内截图:
一、前言
在互联网发展到今天,数据可视化分析是非常流行的一种形式,特别是趋势对比图,在我前面文章已经写过:
全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
全网最火可视化趋势视频实现深度解析,同时新增条形图表
今天:
我来分享下如何实现排名趋动画视频的
这块在智慧大屏,城市展览厅等地方用的特别频繁。
今天示例项目数据源自掘金稀土 优质作者榜 · 2024年第11期月榜数据
很可惜!!我自己只排在第二
第一是国内大名鼎鼎的 郭神
二、Compose代码实现上面效果
1. 准备数据:
手动敲下来的:
private fun getSourceList(): MutableList<TopDataSource> {
val list = mutableListOf<TopDataSource>()
list.add(TopDataSource("恋猫de小郭", "https://p3-passport.byteacctimg.com/img/user-avatar/4f40c8c1bc9e95d86779e105c922ca2f~300x300.image", 1158f))
list.add(TopDataSource("Wgllss", "https://p6-passport.byteacctimg.com/img/user-avatar/cdf8ff3d0fc94ae7f1c5cd90b5a0ae35~300x300.image", 840f))
list.add(TopDataSource("张风捷特烈", "https://p9-passport.byteacctimg.com/img/user-avatar/5b2b7b85d1c818fa71d9e2e8ba944a44~300x300.image", 713f))
list.add(TopDataSource("小墙程序员", "https://p9-passport.byteacctimg.com/img/user-avatar/c20e903aa0eecae09a2bd5593ef58db7~300x300.image", 582f))
list.add(TopDataSource("法的空间", "https://p3-passport.byteacctimg.com/img/user-avatar/41fe666b46afd29d2da26222a6241e94~300x300.image", 535f))
list.add(TopDataSource("稀有猿诉", "https://p3-passport.byteacctimg.com/img/user-avatar/2cf639585a7793ada0c7957d5b3a5735~300x300.image", 342f))
list.add(TopDataSource("陆业聪", "https://p6-passport.byteacctimg.com/img/user-avatar/74a60ecfd93fad88c8fbe59e3b2cc2d4~300x300.image", 294f))
list.add(TopDataSource("半山居士", "https://p26-passport.byteacctimg.com/img/user-avatar/3060d9b49a1c858c4f9f3b43d2fa7fd4~300x300.image", 266f))
list.add(TopDataSource("奇风FantasyWind", "https://p6-passport.byteacctimg.com/img/user-avatar/bbb88e1ebef2a8a4ae1d3b41933bd684~300x300.image", 238f))
list.add(TopDataSource("一杯凉白开", "https://p6-passport.byteacctimg.com/img/user-avatar/b02d71c859387330709540f93c4cfe3c~300x300.image", 231f))
list.add(TopDataSource("青衫", "https://p26-passport.byteacctimg.com/img/user-avatar/fdcc3658bfd48a576d113d145aa25305~300x300.image", 221f))
list.add(TopDataSource("_小马快跑_", "https://p6-passport.byteacctimg.com/img/user-avatar/1678f071bd16e3016f45068b9ac865dc~300x300.image", 211f))
list.add(TopDataSource("程序员DHL", "https://p3-passport.byteacctimg.com/img/user-avatar/5166799b3da871fdc15f8c0275741284~300x300.image", 186f))
list.add(TopDataSource("朱涛的自习室", "https://p3-passport.byteacctimg.com/img/user-avatar/833b93a0841a26548e37f128d4b01aaf~300x300.image", 181f))
list.add(TopDataSource("芦半山", "https://p26-passport.byteacctimg.com/img/user-avatar/ae2d7f8f77b2bd99d9c32a587b1bec97~300x300.image", 165f))
list.add(TopDataSource("IAM四十二", "https://p6-passport.byteacctimg.com/img/user-avatar/98b3934295f138b26c259cb2a9be0fec~300x300.image", 162f))
list.add(TopDataSource("iOS阿玮", "https://p9-passport.byteacctimg.com/img/user-avatar/f9bc81aa8a0c2a42f27fb87689873b30~300x300.image", 148f))
list.add(TopDataSource("vivo高启强", "https://p6-passport.byteacctimg.com/img/user-avatar/7fba93d8babd024219d3bc2abfb6dff8~300x300.image", 133f))
list.add(TopDataSource("IT小码哥", "https://p3-passport.byteacctimg.com/img/user-avatar/308fcfe3062ce610355c3a5cd4387b61~300x300.image", 126f))
list.add(TopDataSource("天枢破军", "https://p6-passport.byteacctimg.com/img/user-avatar/3ec2b49f3956a7402720672798e10cf7~300x300.image", 124f))
list.sortedByDescending { it.value }//从大到小 降序
return list
}
2. 将数据源处理成我们想要的模型数据内容:
val list = getSourceList()
val defaultBitmap = createImageBitmap()
val sortList = mutableListOf<DynamicTopBarBean>()//处理成我们模型里面想要的排序结果 list 对象
val size = list.size
val arrayKey = Array(size) { DynamicKey("", 0f, 0f) }
val mapImage by lazy { mutableMapOf<Int, DynamicImage>() }
val mapPreValueCache by lazy { mutableMapOf<Int, Float>() }
list.forEachIndexed { index, it ->
sortList.add(DynamicTopBarBean(it.value, listColor[index % 10], it.title, index + 1, it.img))
mapImage[index + 1] = DynamicImage(it.img, defaultBitmap)
val maxF = it.value / 10
var xMaxValue = Math.floor(maxF.toDouble()).toInt() * 10F
arrayKey[size - 1 - index] = DynamicKey((size - 1 - index).toString(), it.value, xMaxValue)
mapPreValueCache[size - 1 - index] = it.value
}
val mapScource by lazy { mutableMapOf<Int, MutableList<DynamicTopBarBean>>() }
sortList.sortBy { it.value }//从小到大 升序
var key = size
val maxNum = 10
while (key > 0) {
if (!mapScource.contains(key - 1)) {
val keyList = mutableListOf<DynamicTopBarBean>()
var lePos = key - maxNum
if (lePos < 0) lePos = 0
for (i in lePos until key) {
//i 从 20——29, 19——28,18——27, 17-26, 。。。
//i 从 10——19. 9——18 ... 3——12, 1——10
//i 从 0——9 ,0——8, 0-1, 0
sortList[i].run {
keyList.add(DynamicTopBarBean(value, color, title, sortNo, imgUrl))
}
}
mapScource[key - 1] = keyList
}
key--
}
var keyNo = 0
while (keyNo < size) {
if (mapScource.contains(keyNo)) {
mapScource[keyNo]?.forEachIndexed { index, it ->
if (keyNo + 1 == size) {
it.prePos = index
it.diffPos = 0
} else if (keyNo < maxNum) {
it.prePos = index + maxNum - keyNo
it.diffPos = -1
} else {
it.prePos = index + 1
it.diffPos = -1
}
}
}
keyNo++
}
3. 准备数据配置成排名数据模型:
这里面的配置和前面文章写过的全网最火视频,Compose代码写出来,动态可视化趋势视频,帅到爆
里面内容大同小异,可以参考前面文章。
val dynamicTopModel = DynamicTopModel(arrayKey, mapScource, mapImage).apply {
topSize = sortList.size
barWidth = toDp(25f) //条形宽度 isAutoBarWidth为false 才可用
yCount = maxNum //y轴横线刻度数 ,包含 0,比如0到5 设置为 6
xCount = 4 //X轴横线刻度数 ,包含 0,比如0到5 设置为 6
keyTextOffsetX = toDp(110f) //年份相对右下角X便宜
keyTextOffextY = toDp(90f) //年份相对右下角Y便宜
xLableStep = 1 //X刻度上面文案显示步长
isAutoBarWidth = true //是否自动计算条形宽度
offsetx = toDp(90f) //UI上原点左下角 x偏移
offsetxLable = toDp(30f)//原点 y轴上面刻度文字x偏移 相对控件最左边偏移
offsety = toDp(3f) //UI上原点左下角 y 偏移
offsetyLable = toDp(8f)//原点 y上面刻度文字文字 y偏移 相对控件左下角点,调整Y值文字在竖直中间位置 与横线对齐
durationMillis = 3000 //每个年份间隔执行动画
isTop = true//X轴是否位于上面
offextBarTopY = toDp(18f) //条形距离顶部偏移 ,这里不是视图,是相对Y轴线最上端
offextBarBottomY = toDp(5f) //条形距离底部偏移 ,这里不是视图,是相对Y轴线最下端
formatString = "%.2f" //数字格式化设置
multiplier = 1f //数据显示格式所用的乘数
musicUrl = "asset:///vv.mp3" //背景音乐,可配置网络链接
titleColor = Color.White //每个条形名称颜色
dynamicChartName = "优质作者榜(客户端):2024年第11期月榜" //动态表名称
colorLable = Color.White//X轴上面刻度变化文案颜色
maxValueOffsetRight = toDp(150f)
// bgUrl = "https://copyright.bdstatic.com/vcg/creative/820617f247e3a7f89686800e43c62ab2.jpg"//整个动画背景图
bgUrl = "https://lmg.jj20.com/up/allimg/1114/0P421133347/210P4133347-1-1200.jpg"//整个动画背景图
}
4. Compose主要界面实现写法:
@Composable
fun dynamicTopBarChart(innerPadding: PaddingValues = PaddingValues(0.dp), viewModel: DynamicViewModel = DynamicViewModel().apply { setData() }) {
val textMeasurer = rememberTextMeasurer()
val context = LocalContext.current
val chatModel by viewModel.dynamicTopModel.observeAsState()
var bitmapBg by remember { mutableStateOf(createImageBitmap()) }
var width by remember { mutableStateOf(0) }
var height by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
chatModel?.bgUrl?.takeIf {
it.isNotEmpty()
}.let {
Glide.with(context).asBitmap().load(it).skipMemoryCache(false).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
bitmapBg = resource.asImageBitmap()
}
})
}
}
chatModel?.let {
Column(modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.onSizeChanged {
width = it.width
height = it.height
}
.drawBehind {
drawImage(
bitmapBg, srcOffset = IntOffset.Zero, srcSize = IntSize(bitmapBg.width, bitmapBg.height), //绘制的图片大小
dstOffset = IntOffset.Zero, dstSize = IntSize(
width, height
)
)
}) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(40.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center
) {
Text(text = it.dynamicChartName, fontSize = 22.sp, color = Color.White, style = TextStyle.Default)
}
val modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
drawDynamicTopBarChart(
modifier, textMeasurer, TextStyle(
fontSize = 60.sp, fontWeight = FontWeight.Bold, color = Color.Red
), it
)
}
}
}
5. 真正绘制动态排名趋势图方法(drawDynamicTopBarChart):
@Composable
fun drawDynamicTopBarChart(modifier: Modifier, textMeasurer: TextMeasurer, style: TextStyle, it: DynamicTopModel) {
var mSize by remember { mutableStateOf(Size(0f, 0f)) }
var start by remember { mutableStateOf(false) }
val lifecycleOwner = LocalLifecycleOwner.current
val context = LocalContext.current
val player = remember { ExoPlayer.Builder(context).build() }
var drawPosition by remember { mutableStateOf(0) }
val drawKeyNo = remember(drawPosition) { it.arrayKey[drawPosition].currentKey }
val width = mSize.width
val height = mSize.height
val barWidth = if (it.isAutoBarWidth) {
(height - 2 * it.offsety - it.offextBarTopY - it.offextBarBottomY - it.yCount * 2f) / it.yCount
} else it.barWidth
val heightDiv = (height - 2 * it.offsety - it.offextBarTopY - it.offextBarBottomY - barWidth) / (it.yCount - 1)
val maxUIBarStartY = height - it.offsety - it.offextBarBottomY - barWidth
val minUIBarStartY = it.offsety + it.offextBarTopY
val availableWidth = width - 1.5f * it.offsetx - it.maxValueOffsetRight
val dList = remember(drawKeyNo) { it.mapScource[drawPosition]!! }
val listSize = dList.size
val floatTweenSpec = remember { FloatTweenSpec(it.durationMillis, easing = LinearEasing) }
var currHeightAnimateValue by remember { mutableStateOf(0) }
val animatedHeightDiff by animateFloatAsState(targetValue = if (start && currHeightAnimateValue == 0) heightDiv else 0f, animationSpec = floatTweenSpec)
var currWidthAnimateValue by remember { mutableStateOf(0) }
val animateWidthDiff by animateFloatAsState(targetValue = if (start && currWidthAnimateValue == 0) 100f else 0f, animationSpec = floatTweenSpec)
var currFirstAnimateValue by remember { mutableStateOf(0) }
val firstAnimate by animateFloatAsState(if (start && currFirstAnimateValue == 0) 1f else 0f, animationSpec = floatTweenSpec)
val firstBarColor by animateColorAsState(targetValue = if (start) dList[listSize - 1].color else Color.Transparent, animationSpec = tween(durationMillis = it.durationMillis, easing = LinearEasing))
val colorXDivText by animateColorAsState(targetValue = if (start && currWidthAnimateValue == 0) it.colorLable else Color.Transparent, animationSpec = tween(durationMillis = it.durationMillis, easing = LinearEasing))
val animateHValue = if (currHeightAnimateValue == 0) animatedHeightDiff else (heightDiv - animatedHeightDiff)
val animateWValue = if (currHeightAnimateValue == 0) animateWidthDiff else (100f - animateWidthDiff)
val animateFirstPosition = if (currFirstAnimateValue == 0) firstAnimate else (1f - firstAnimate)
val maxX = it.preMaxValue(drawPosition) + (it.maxDiffValue(drawPosition) / 100f) * animateWValue
val xValue = it.xMaxValue(drawPosition) / (it.xCount)
val xAbs = availableWidth / maxX
val xMaxWidth = availableWidth * it.xMaxValue(drawPosition) / maxX
val xDiv = xMaxWidth / (it.xCount)
val mapImage = remember { it.mapImage }
LaunchedEffect(Unit) {
player.run {
setMediaItem(MediaItem.fromUri(Uri.parse(it.musicUrl)))
}
mapImage.forEach {
Glide.with(context).asBitmap().load(it.value.imgUrl).skipMemoryCache(false).diskCacheStrategy(DiskCacheStrategy.RESOURCE).into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
it.value.bitmap = resource.asImageBitmap()
}
})
}
delay(it.animateDelay)
start = true
player.run {
prepare()
play()
}
while (drawPosition < it.arrayKey.size - 1) {
delay(it.durationMillis.toLong())
drawPosition += 1
if (currHeightAnimateValue == 0) currHeightAnimateValue = 1
else currHeightAnimateValue = 0
if (currWidthAnimateValue == 0) currWidthAnimateValue = 1
else currWidthAnimateValue = 0
if (currFirstAnimateValue == 0) currFirstAnimateValue = 1
else currFirstAnimateValue = 0
}
if (drawPosition == it.arrayKey.size - 1) {
delay(it.durationMillis.toLong())
player.stop()
player.release()
it.isPlayComplete = true
}
}
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_STOP) {
player.stop()
player.release()
}
}
lifecycleOwner.lifecycle.addObserver(observer)
onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Canvas(modifier = modifier) {
mSize = size
if (it.isShowYLine)
//绘制 Y轴
drawLine(
start = Offset(it.offsetx, height - it.offsety), end = Offset(it.offsetx, it.offsety + if (it.isTop) it.offextBarTopY else 0f), color = it.colorYLine, strokeWidth = 2f
)
//绘制 X轴
drawLine(
start = Offset(it.offsetx, if (it.isTop) it.offsety + it.offextBarTopY else (height - it.offsety)), end = Offset(width - it.offsetx / 2 - it.maxValueOffsetRight, if (it.isTop) it.offsety + it.offextBarTopY else (height - it.offsety)), color = it.colorXLine, strokeWidth = 2f
)
for (i in 0..it.xCount step it.xLableStep) {
//绘制x轴上几条竖线
drawLine(
start = Offset(it.offsetx + i * xDiv, height - it.offsety), end = Offset(it.offsetx + i * xDiv, it.offsety + if (it.isTop) it.offextBarTopY else 0f), color = it.colorLine, strokeWidth = 1f
)
//绘制x轴上几条竖线对应的X值
if (start) drawText(
textMeasurer = textMeasurer, text = "${it.getTextValueFormat(i * xValue)}", topLeft = Offset(it.offsetx + i * xDiv - it.offsetxLable, if (it.isTop) it.offsety else (height - it.offsety)), style = TextStyle(color = colorXDivText)
)
}
for (m in 0 until listSize) {
val item = dList[m]
val newValueC = maxUIBarStartY - item.prePos * heightDiv - (animateHValue * item.diffPos)
val newAnimateValue = if (newValueC < minUIBarStartY) {
minUIBarStartY
} else if (newValueC > maxUIBarStartY) {
maxUIBarStartY
} else newValueC
//绘制Y上刻度文案
// if (start)
drawText(
textMeasurer = textMeasurer, text = "第${item.sortNo}名", topLeft = Offset(it.offsetxLable, newAnimateValue), style = TextStyle(color = it.titleColor ?: item.color)
)
val newWidth = if (m == dList.size - 1 || availableWidth < (item.value) * xAbs) {
item.value * xAbs * animateFirstPosition
} else item.value * xAbs
// } else (item.preValueAbs * availableWidth) + (((item.value / it.maxValue(drawPosition) * availableWidth - item.preValueAbs * availableWidth) / 100f) * animateWValue)
//绘制条型柱状
drawRect(
color = if (m == dList.size - 1) firstBarColor else item.color, topLeft = Offset(it.offsetx, newAnimateValue), size = Size(newWidth, barWidth)
)
drawImage(it.mapImage[item.sortNo]!!.bitmap, dstOffset = IntOffset((it.offsetx + newWidth).toInt(), newAnimateValue.toInt()), dstSize = IntSize(barWidth.toInt(), barWidth.toInt()))
// if (start)
drawText(
textMeasurer = textMeasurer, text = " ${item.title}:${it.getTextValueFormat(item.value)}", topLeft = Offset(it.offsetx + barWidth + newWidth, newAnimateValue), style = TextStyle(color = it.titleColor ?: item.color)
)
}
if (start) drawText(
textMeasurer = textMeasurer, text = "第${(it.topSize - drawPosition)}名", topLeft = Offset(
width - it.offsetx - it.keyTextOffsetX, height - it.offsety - it.keyTextOffextY,
), style = style
)
}
}
三、总结:
本文重点介绍了数据可视化排名视频
及重点介绍:用Compose是如何一步一步实现出,视频效果的
后续会进行整理项目,同时会
介绍Android下怎么代码实现录屏,
怎么通过代码将视频处理成GIf等等
统一前面动态趋势图视频等一系列整体输出