参考文章
此方案使用动画方式实现,只适合轻量级别的弹幕滚动效果实现,数据量过大时会出现内存激增的情况。
效果:
自定义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