庆元旦,出排名,手撸全网火爆的排名视频,排名动态可视化

2,437 阅读7分钟

用Compose代码写出:全网火爆的排名视频,排名动态可视化
互联网智慧大数据分析,图表,和可视化趋势是一种趋势,分析的利器

ezgif-7-1724096f80.gif

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

上面GIF效果图:是Android Studio模拟器上App中

用Compose写出动画,然后录屏成视频转化后的GIF效果,本身是带音乐背景的。

项目工程内截图:

img_v3_02i3_ec0e6d7b-c03b-4ce9-9ae8-cfd0f31ffc8g.jpg

一、前言

在互联网发展到今天,数据可视化分析是非常流行的一种形式,特别是趋势对比图,在我前面文章已经写过:
全网最火视频,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,比如05 设置为 6
                    xCount = 4 //X轴横线刻度数 ,包含 0,比如05 设置为 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等等
统一前面动态趋势图视频等一系列整体输出

感谢阅读:

欢迎用你发财的小手 关注,点赞、收藏

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