Android自定义view实现横向滚动弹幕

news2024/11/25 8:20:14

参考文章
此方案使用动画方式实现,只适合轻量级别的弹幕滚动效果实现,数据量过大时会出现内存激增的情况。

效果:

效果图

自定义view代码

public class TumbleLayout extends ViewGroup {

    private final String TAG = "TumbleLayout";

    private int parentWidth;
    private int parentHeight;
    private long currentHshCode = 0;
    // 弹幕数据缓存池
    private DataPool dataPool = new DataPool<ContentBeen>(100);
    private DataPool userDataPool = new DataPool<ContentBeen>(10);

    private boolean isDetached = false;

    public TumbleLayout(@NonNull Context context) {
        super(context);
        initView();
    }

    public TumbleLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public TumbleLayout(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initView();
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        if (getParent() instanceof ViewGroup) {
            parentWidth = ((ViewGroup) getParent()).getWidth();
            parentHeight = ((ViewGroup) getParent()).getHeight();
        }
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int viewWidth = getViewWidth(widthMeasureSpec);
        int viewHeight = getViewHeight(heightMeasureSpec);
        parentWidth = viewWidth;
        parentHeight = viewHeight;
        // 设置子view的宽高
        setMeasuredDimension(viewWidth, viewHeight);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        // 宽高计算完毕 开始显示弹幕
        if (changed) {
            showNextData();
        }
    }

    @Override
    protected void onFinishInflate() {
        super.onFinishInflate();
    }

    private void initView() {
    }

    public void addUserChildView(ContentBeen contentBeen) {
        if (userDataPool != null) {
            userDataPool.release(contentBeen);
            if (!dataPool.hasNext()) {
                showNextData();
            }
        }
    }

    public void addChildView(ContentBeen contentBeen) {
        if (dataPool != null) {
            // 将数据加入队列
            dataPool.release(contentBeen);
        }
    }

    private void startAnimator(View child) {
        ObjectAnimator animator = new ObjectAnimator();
        animator.setIntValues(0, parentWidth + child.getMeasuredWidth());
        animator.setDuration(3000);
        animator.setInterpolator(new LinearInterpolator());
        animator.addUpdateListener(animation -> {
            // view已经退出 则停止所有动画 防止内存泄露
            if (isDetached) {
                animation.cancel();
                return;
            }
            int x = (int) animation.getAnimatedValue();
            int left = parentWidth - x;
            int right = parentWidth + child.getMeasuredWidth() - x;
            // 控制弹幕密集度 当上一条数据离开屏幕右侧边框时 展示下一条弹幕数据
            if (currentHshCode == child.hashCode() && right + 50 <= parentWidth) {
                // 展示下一条弹幕
                showNextData();
            }
            child.layout(left, child.getTop(), right, child.getBottom());
            if (child.getRight() <= 0) {
                // 动画结束 移除view
                removeView(child);
            }
        });
        animator.start();
    }

    private void showNextData() {
        ContentBeen acquire = null;
        if (userDataPool == null && dataPool == null) {
            return;
        }
        // 用户本地弹幕优先级最高 若有本地用户弹幕 则先展示用户弹幕
        if (userDataPool.hasNext()) {
            acquire = (ContentBeen) userDataPool.acquire();
        } else if (dataPool.hasNext()) {
            acquire = (ContentBeen) dataPool.acquire();
        }

        // 执行一下条弹幕出现
        if (acquire != null) {
            // 小于最大数量时 添加新的子view
            currentHshCode = acquire.getChildView().hashCode();
            addView(acquire.getChildView());
            int childCount = getChildCount();
            if (childCount != 0) {
                int index = childCount - 1;
                View child = getChildAt(index);
                measureMyChild(child);
                int left = parentWidth + 30;
                int num = laneNum(child);
                int top = num * child.getMeasuredHeight();
                int right = parentWidth + child.getMeasuredWidth() + 30;
                int bottom = top + child.getMeasuredHeight();
                MLog.e(TAG, "measureMyChild  hashCode = " + child.hashCode()
                        + " top = " + top + " bottom = " + bottom + " parentHeight" + getHeight());
                child.layout(left, top, right, bottom);
                startAnimator(child);
            }
        }
    }

    private int getViewWidth(int measureSpec) {
        int size = 100;
        int specSize = MeasureSpec.getSize(measureSpec);
        int specMode = MeasureSpec.getMode(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            size = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            size = Math.max(size, specSize);
        }
        return size;
    }

    private int getViewHeight(int measureSpec) {
        int size = 100;
        int specSize = MeasureSpec.getSize(measureSpec);
        int specMode = MeasureSpec.getMode(measureSpec);
        if (specMode == MeasureSpec.EXACTLY) {
            size = specSize;
        } else if (specMode == MeasureSpec.AT_MOST) {
            size = Math.max(size, specSize);
        }
        return size;
    }

    /**
     * 测量某一个child的宽高
     */
    protected void measureMyChild(View child) {
        final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

    /**
     * 计算随机高度值 起到随机位置展示效果
     */
    private int laneNum(View child) {
        // 计算出大概有几条泳道
        int laneCount = getHeight() / child.getMeasuredHeight();
        // 给弹幕随机分配泳道
        Random random = new Random();
        // 返回泳道编号
        return random.nextInt(laneCount);
    }

    public void destroy() {
        // 回收资源 防止泄露
        userDataPool.clean();
        dataPool.clean();
        userDataPool = null;
        dataPool = null;
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        isDetached = true;
        MLog.e(TAG, "onDetachedFromWindow");
    }
}

存储数据队列的代码

public class DataPool<T> implements Pools.Pool<T> {
    private Object[] mPool;
    private int mPoolSize;

    private int l = 0;
    private int curIndex = 0;

    public DataPool(int maxPoolSize) {
        if (maxPoolSize <= 0) {
            throw new IllegalArgumentException("The max pool size must be > 0");
        }
        // 构造池对象容器
        mPool = new Object[maxPoolSize];
    }

    @Nullable
    @Override
    public T acquire() {
        // 从容器中取出对象
        if (l > 0) {
            T instance = (T) mPool[curIndex];
            mPool[curIndex] = null;
            l--;
            curIndex++;
            if(l <= 0){
                curIndex = 0;
            }
            return instance;
        }
        return null;
    }

    @Override
    public boolean release(@NonNull T instance) {
        if (isInPool(instance)) {
            throw new IllegalStateException("Already in the pool!");
        }
        // 存储对象
        if (l < mPool.length) {
            mPool[l] = instance;
            l++;
            return true;
        }
        return false;
    }

    // 判断对象是否在池中
    private boolean isInPool(@NonNull T instance) {
        // 遍历池对象
        for (int i = 0; i < l; i++) {
            if (mPool[i] == instance) {
                return true;
            }
        }
        return false;
    }

    public boolean hasNext(){
        return l > 0;
    }

    public void clean(){
        l = 0;
        curIndex = 0;
    }
}

数据格式

public class ContentBeen {
    private String content;
    private View childView;

    public ContentBeen(String content,View childView){
        this.content = content;
        this.childView = childView;
    }

    public void setChildView(View childView) {
        this.childView = childView;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }

    public View getChildView() {
        return childView;
    }
}

布局文件内容

	<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.activity.BarrageActivity">

    <com.example.app_view_model.view.TumbleLayout
        android:id="@+id/tumble_layout"
        android:layout_width="match_parent"
        android:layout_height="300dp"
        android:background="@color/black">

    </com.example.app_view_model.view.TumbleLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignParentBottom="true"
        android:layout_marginBottom="20dp"
        android:orientation="horizontal">

        <EditText
            android:id="@+id/txt_edit"
            android:layout_weight="1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <Button
            android:layout_weight="4"
            android:id="@+id/send_btn"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="发送" />
    </LinearLayout>

</RelativeLayout>

使用方法

	// 使用方法
	addUserChildView(); // 添加用户输入弹幕
	addChildView(); // 加载数据弹幕
	destroy(); // 资源回收 此方法一定要调用 防止大量动画无法回收导致oom

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/941453.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Camunda 7.x 系列【30】中间事件

有道无术,术尚可求,有术无道,止于术。 本系列Spring Boot 版本 2.7.9 本系列Camunda 版本 7.19.0 源码地址:https://gitee.com/pearl-organization/camunda-study-demo 文章目录 1. 概述2. 消息中间事件3. 定时器中间事件4. 信号中间事件5. 错误中间事件6. 条件中间事件7…

代码随想录算法训练营第五十天|LeetCode 739,496

目录 LeetCode 739.每日温度 LeetCode 496.下一个更大元素&#xff01; LeetCode 739.每日温度 文章讲解&#xff1a;代码随想录 力扣题目&#xff1a;力扣&#xff08;LeetCode&#xff09;官网 - 全球极客挚爱的技术成长平台 代码如下&#xff08;Java&#xff09;&#xf…

Python“牵手”义乌购商品列表数据,关键词搜索义乌购API接口数据,义乌购API接口申请指南

义乌购平台API接口是为开发电商类应用程序而设计的一套完整的、跨浏览器、跨平台的接口规范&#xff0c;义乌购API接口是指通过编程的方式&#xff0c;让开发者能够通过HTTP协议直接访问义乌购平台的数据&#xff0c;包括商品信息、店铺信息、物流信息等&#xff0c;从而实现义…

国标GB28181视频平台EasyGBS视频监控平台无法播放,抓包返回ICMP排查过程

国标GB28181视频平台EasyGBS是基于国标GB/T28181协议的行业内安防视频流媒体能力平台&#xff0c;可实现的视频功能包括&#xff1a;实时监控直播、录像、检索与回看、语音对讲、云存储、告警、平台级联等功能。国标GB28181视频监控平台部署简单、可拓展性强&#xff0c;支持将…

产品经理工作常见的4大误区

产品管理对项目来说非常重要&#xff0c;但在日常工作中&#xff0c;我们往往容易进入思维误区&#xff0c;如果我们没有及时发现错误并进行纠正&#xff0c;这会对产品需求工作以及项目进度产生较大影响。 因此我们需要重视产品工作中常见的思维误区并及时避免&#xff0c;常见…

2023Web自动化测试的技术框架和工具有哪些?

Web 自动化测试是一种自动化测试方式&#xff0c;旨在模拟人工操作对 Web 应用程序进行测试。这种测试方式可以提高测试效率和测试精度&#xff0c;减少人工测试的工作量和测试成本。在 Web 自动化测试中&#xff0c;技术框架和工具起着至关重要的作用。本文将介绍几种常见的 W…

如何让你的交易高效且安全?离不开这项技术

作者&#xff5c;Jason Jiang 在区块链技术演变过程中&#xff0c;有两个关键问题始终绕不过去&#xff1a;隐私与扩容。当我们探寻这两个问题的“标准解法”时&#xff0c;却发现它们都离不开一种技术&#xff0c;那就是&#xff1a;零知识证明。什么是零知识证明&#xff1f…

Dubbo源码环境搭建

背景 Dubbo 作为一款微服务框架&#xff0c;最重要的是向用户提供跨进程的 RPC 远程调用能力。如上图所示&#xff0c;Dubbo 的服务消费者&#xff08;Consumer&#xff09;通过一系列的工作将请求发送给服务提供者&#xff08;Provider&#xff09;。 为了实现这样一个目标&a…

Apipost: 程序员必备的API管理神器

作为一款专为程序员打造的API管理工具&#xff0c;Apipost也成为开发人员圈子里的一款热门工具。Apipost拥有强大的功能和便捷操作性&#xff0c;这也让许多开发者爱不释手。那么&#xff0c;Apipost到底有哪些吸引人的特点呢&#xff1f;本文将为您详细介绍。 统一API管理 A…

【具身智能】论文系列解读-RL-ViGen

1. RL-ViGen&#xff1a;视觉泛化的强化学习基准 RL-ViGen: A Reinforcement Learning Benchmark for Visual Generalization 0 摘要与总结 视觉强化学习&#xff08;Visual RL&#xff09;与高维观察相结合&#xff0c;一直面临着分布外泛化的长期挑战。尽管重点关注旨在解…

[QT]设置程序仅打开一个,再打开就唤醒已打开程序的窗口

需求&#xff1a;speedcrunch 这个软件是开源的计算器软件。配合launch类软件使用时&#xff0c;忘记关闭就经常很多窗口&#xff0c;强迫症&#xff0c;从网上搜索对版本进行了修改。 #include "gui/mainwindow.h"#include <QCoreApplication> #include <…

如何基于自己训练的Yolov5权重,结合DeepSort实现目标跟踪

网上有很多相关不错的操作demo&#xff0c;但自己在训练过程仍然遇到不少疑惑。因此&#xff0c;我这总结一下操作过程中所解决的问题。 1、deepsort的训练集是否必须基于逐帧视频&#xff1f; 我经过尝试&#xff0c;发现非连续性的图像仍可以作为训练集。一个实例&#xff0…

kubernetes搭建及基本使用

1. 前置要求准备 一台或多台机器&#xff0c;操作系统 CentOS7.x-86_x64硬件配置&#xff1a;2GB 或更多 RAM&#xff0c;2 个 CPU 或更多 CPU&#xff0c;硬盘 30GB 或更多集群中所有机器之间网络互通可以访问外网&#xff0c;需要拉取镜像禁止 swap 分区 此处我是白嫖的谷歌云…

No122.精选前端面试题,享受每天的挑战和学习

文章目录 1、vue中key的作用2、如何让vue页面重新渲染3、组件之间通信方式4、vue为什么要mutation、 action操作5、插槽、具名插槽、作用域插槽6、用set求两个数组的交集7、树用js如何实现&#xff1f; 1、vue中key的作用 在Vue中&#xff0c;key的作用是帮助Vue识别每个VNode…

性能评估之旅:软件测试的神秘工具与方法论

引言&#xff1a;性能评估的重要性 在当今的软件开发领域&#xff0c;性能评估已经成为了一个不可或缺的环节。随着用户对于软件响应速度和稳定性的要求越来越高&#xff0c;如何确保软件在各种环境下都能稳定运行&#xff0c;成为了每一个开发者和测试者必须面对的问题。性能…

【App端】uni-app使用echarts和百度地图api

目录 前言方案一&#xff1a;echarts百度地图获取百度地图AK安装echarts和引入百度地图api完整使用代码 方案二&#xff1a;echarts地图和柱状图变形动画完整使用代码 前言 近期的app项目中想加一个功能&#xff0c;展示全国各地的某一数据统计情况&#xff0c;想来想去&#…

搭建Android自动化python+appium环境

一. 需要软件 1. JDK:JAVA安装后配置JDK环境 2. SDK:SDK下载后配置adb环境 3. Python:pyhton语言 4. Pycharm:python脚本编译工具 5. Appium-python-client:pyhton中的库 6. Appium客户端 二. 搭建步骤 1. 配置JDK环境 ①. 下载安装java: https://www.oracle.com/jav…

windows下Mysql安装配置教程

Mysql下载 在官网下载mysql community Server https://dev.mysql.com/downloads/mysql/ 可以选择下载压缩包或者MSI安装程序 使用压缩包安装 MySQL 压缩包安装通常需要以下步骤&#xff1a; 1. 下载 MySQL 安装包 你可以从 MySQL 官网上下载适合你系统的 MySQL 安装包&am…

Android软键盘弹出,底部的控件随之弹出,但整体布局不会向上弹

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.co…

浅析Linux 物理内存外碎片化

本文出现的内核代码来自Linux4.19&#xff0c;如果有兴趣&#xff0c;读者可以配合代码阅读本文。 一、Linux物理内存外碎片化概述 什么是Linux物理内存碎片化&#xff1f;Linux物理内存碎片化包括两种&#xff1a; 1.物理内存内碎片&#xff1a;指分配给用户的内存空间中未…