Android -- [SelfView] 自定义多行歌词滚动显示器

news2025/1/19 13:45:13

Android – [SelfView] 自定义多行歌词滚动显示器

流畅、丝滑的滚动歌词控件
 * 1. 背景透明;
 * 2. 外部可控制进度变化;
 * 3. 支持屏幕拖动调节进度(回调给外部);

效果
在这里插入图片描述

歌词文件(.lrc)
在这里插入图片描述

一. 使用

<com.nepalese.harinetest.player.lrc.VirgoLrcView
	android:id="@+id/lrcView"
	android:layout_width="match_parent"
	android:layout_height="match_parent"/>
private VirgoLrcView lrcView;

lrcView = findViewById(R.id.lrcView);
initLrc();

//==================================
private void initLrc(){
//设置歌词文件 .lrc
//lrcView.setLrc(FileUtils.readTxtResource(getApplicationContext(), R.raw.shaonian, "utf-8"));
	lrcView.setLrc(R.raw.shaonian);
	lrcView.seekTo(0);

	lrcView.setCallback(new VirgoLrcView.LrcCallback() {
		@Override
		public void onUpdateTime(long time) {
        	//拖动歌词返回的时间点
		}

		@Override
		public void onFinish() {
			stopTask();
		}
	});
}

public void onStartPlay(View view) {
	startTask();
}

public void onStopPlay(View view) {
	stopTask();
}

//使用计时器模拟歌曲播放时进度刷新
private long curTime = 0;
private final Runnable timeTisk = new Runnable() {
	@Override
	public void run() {
		curTime += INTERVAL_FLASH;
		lrcView.seekTo(curTime);
	}
};

private void startTask() {
    stopTask();
    handler.post(timeTisk);
}

private void stopTask() {
    handler.removeCallbacks(timeTisk);
}

private final long INTERVAL_FLASH = 400L;
private final Handler handler = new Handler(Looper.myLooper()) {
	@Override
	public void handleMessage(@NonNull Message msg) {
		super.handleMessage(msg);
	}
};

二. 码源

attr.xml

<declare-styleable name="VirgoLrcView">
	<attr name="vlTextColorM" format="color|reference" />
	<attr name="vlTextColorS" format="color|reference" />
	<attr name="vlTextSize" format="dimension|reference" />
	<attr name="vlLineSpace" format="dimension|reference" />
</declare-styleable>

VirgoLrcView.java

import android.animation.Animator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.LinearInterpolator;

import androidx.annotation.Nullable;
import androidx.annotation.RawRes;

import com.nepalese.harinetest.R;
import com.nepalese.harinetest.utils.CommonUtil;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Created by Administrator on 2024/11/26.
 * Usage:更流畅、丝滑的滚动歌词控件
 * 1. 背景透明;
 * 2. 外部可控制进度变化;
 * 3. 支持屏幕拖动调节进度(回调给外部);
 */

public class VirgoLrcView extends View {
    private static final String TAG = "VirgoLrcView";
    private static final float PADD_VALUE = 25f;//时间线两边缩进值
    private static final float TEXT_RATE = 1.25f;//当前行字体放大比例
    private static final long INTERVAL_ANIMATION = 400L;//动画时长
    private static final String DEFAULT_TEXT = "暂无歌词,快去下载吧!";

    private final Context context;
    private Paint paint;//画笔, 仅一个
    private ValueAnimator animator;//动画
    private List<LrcBean> lineList;//歌词行
    private LrcCallback callback;//手动滑动进度刷新回调

    //可设置变量
    private int textColorMain;//选中字体颜色
    private int textColorSec;//其他字体颜色
    private float textSize;//字体大小
    private float lineSpace;//行间距
    private float selectTextSize;//当前选中行字体大小

    private int width, height;//控件宽高
    private int curLine;//当前行数
    private int locateLine;//滑动时居中行数
    private int underRows;//中分下需显示行数
    private float itemHeight;//一行字+行间距
    private float centerY;//居中y
    private float startY;//首行y
    private float oldY;//划屏时起始按压点y
    private float offsetY;//动画已偏移量
    private float offsetY2;//每次手动滑动偏移量
    private long maxTime;//歌词显示最大时间
    private boolean isDown;//按压界面
    private boolean isReverse;//往回滚动?

    public VirgoLrcView(Context context) {
        this(context, null);
    }

    public VirgoLrcView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public VirgoLrcView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.context = context;
        init(attrs);
    }

    private void init(AttributeSet attrs) {
        TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.VirgoLrcView);
        textColorMain = ta.getColor(R.styleable.VirgoLrcView_vlTextColorM, Color.CYAN);
        textColorSec = ta.getColor(R.styleable.VirgoLrcView_vlTextColorS, Color.GRAY);
        textSize = ta.getDimension(R.styleable.VirgoLrcView_vlTextSize, 45f);
        lineSpace = ta.getDimension(R.styleable.VirgoLrcView_vlLineSpace, 28f);
        ta.recycle();

        selectTextSize = textSize * TEXT_RATE;
        curLine = 0;
        maxTime = 0;
        isDown = false;
        isReverse = false;
        lineList = new ArrayList<>();

        paint = new Paint();
        paint.setTextSize(textSize);
        paint.setAntiAlias(true);

        calculateItem();
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
        if (width == 0 || height == 0) {
            initLayout();
        }
    }

    //控件大小变化时需重置计算
    private void initLayout() {
        width = getWidth();
        height = getHeight();
        centerY = (height - itemHeight) / 2.0f;
        startY = centerY;
        underRows = (int) Math.ceil(height / itemHeight / 3);
        Log.d(TAG, "itemHeight: " + itemHeight + ", underRows: " + underRows);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        //提示无歌词
        if (lineList.isEmpty()) {
            paint.setColor(textColorMain);
            paint.setTextSize(selectTextSize);
            canvas.drawText(DEFAULT_TEXT, getStartX(DEFAULT_TEXT, paint), centerY, paint);
            return;
        }

        if (isDown) {
            paint.setTextSize(textSize);
            paint.setColor(textColorSec);
            //画时间
            if (locateLine >= 0) {
                canvas.drawText(lineList.get(locateLine).getStrTime(), PADD_VALUE, centerY, paint);
            }
            //画选择线
            canvas.drawLine(PADD_VALUE, centerY, width - PADD_VALUE, centerY, paint);

            //手动滑动
            drawTexts(canvas, startY - offsetY2);
        } else {
            //自动滚动
            if (isReverse) {
                drawTexts(canvas, startY + offsetY);
            } else {
                drawTexts(canvas, startY - offsetY);
            }
        }
    }

    private void drawTexts(Canvas canvas, float tempY) {
        for (int i = 0; i < lineList.size(); i++) {
            float y = tempY + i * itemHeight;

            if (y < 0 || y > height) {
                continue;
            }

            if (curLine == i) {
                paint.setTextSize(selectTextSize);
                paint.setColor(textColorMain);
            } else {
                paint.setTextSize(textSize);
                paint.setColor(textColorSec);
            }

            canvas.drawText(lineList.get(i).getLrc(), getStartX(lineList.get(i).getLrc(), paint), y, paint);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        super.onTouchEvent(event);
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                isDown = true;
                if (animator != null) {
                    if (animator.isRunning()) {
                        //停止动画
                        animator.end();
                    }
                }
                locateLine = -1;
                oldY = event.getY();
                break;
            case MotionEvent.ACTION_MOVE:
                offsetY2 = oldY - event.getY();
                calculateCurLine(oldY - event.getY());//定位时间啊
                invalidate();
                break;
            case MotionEvent.ACTION_UP:
                isDown = false;
                postNewLine();
                break;
        }

        return true;
    }

    //计算滑动后当前居中的行
    private void calculateCurLine(float y) {
        int offLine = (int) Math.floor(y / itemHeight);
        if (offLine == 0) {
            return;
        }

        locateLine = curLine + offLine;
        if (locateLine > lineList.size() - 1) {
            //最后一行
            locateLine = lineList.size() - 1;
        } else if (locateLine < 0) {
            //第一行
            locateLine = 0;
        }
    }

    //回调通知,自身不跳转进度
    private void postNewLine() {
        //返回当前行对应的时间线
        if (callback == null) {
            return;
        }
        if (locateLine >= 0) {
            callback.onUpdateTime(lineList.get(locateLine).getTime());
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        releaseBase();
        super.onDetachedFromWindow();
    }

    /**
     * 移除控件,注销资源
     */
    private void releaseBase() {
        cancelAnim();

        if (lineList != null) {
            lineList.clear();
            lineList = null;
        }

        if (callback != null) {
            callback = null;
        }
    }

    private void calculateItem() {
        itemHeight = getTextHeight() + lineSpace;
    }

    //计算使文字水平居中
    private float getStartX(String str, Paint paint) {
        return (width - paint.measureText(str)) / 2.0f;
    }

    //获取文字高度
    private float getTextHeight() {
        Paint.FontMetrics fm = paint.getFontMetrics();
        return fm.descent - fm.ascent;
    }

    //解析歌词
    private void parseLrc(InputStreamReader inputStreamReader) {
        BufferedReader reader = new BufferedReader(inputStreamReader);
        String line;
        try {
            while ((line = reader.readLine()) != null) {
                parseLine(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try {
            inputStreamReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        try {
            reader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        maxTime = lineList.get(lineList.size() - 1).getTime() + 1000;//多加一秒
    }

    private long parseTime(String time) {
        // 00:01.10
        String[] min = time.split(":");
        String[] sec = min[1].split("\\.");

        long minInt = Long.parseLong(min[0].replaceAll("\\D+", "")
                .replaceAll("\r", "").replaceAll("\n", "").trim());
        long secInt = Long.parseLong(sec[0].replaceAll("\\D+", "")
                .replaceAll("\r", "").replaceAll("\n", "").trim());
        long milInt = Long.parseLong(sec[1].replaceAll("\\D+", "")
                .replaceAll("\r", "").replaceAll("\n", "").trim());

        return minInt * 60 * 1000 + secInt * 1000 + milInt;// * 10;
    }

    private void parseLine(String line) {
        Matcher matcher = Pattern.compile("\\[\\d.+].+").matcher(line);
        // 如果形如:[xxx]后面啥也没有的,则return空
        if (!matcher.matches()) {
            long time;
            String str;
            String con = line.replace("\\[", "").replace("\\]", "");
            if (con.matches("^\\d.+")) {//time
                time = parseTime(con);
                str = " ";
            } else {
                return;
            }
            lineList.add(new LrcBean(time, str, con));
            return;
        }

        //[00:23.24]让自己变得快乐
        line = line.replaceAll("\\[", "");
        String[] result = line.split("]");
        lineList.add(new LrcBean(parseTime(result[0]), result[1], result[0]));
    }

    private void reset() {
        lineList.clear();
        curLine = 0;
        maxTime = 0;
        isReverse = false;
        cancelAnim();
    }

    ///动画/

    /**
     * 更新动画
     *
     * @param lineNum 需跳转行数
     */
    private void updateAnim(int lineNum) {
        if (lineNum == 0) {
            return;
        } else if (lineNum == 1) {
            //自然变化
            if (curLine >= lineList.size() - underRows) {
                //停止动画 仅变更颜色
                cancelAnim();
                invalidate();
                return;
            }
        }
        isReverse = lineNum < 0;
        cancelAnim();
        setAnimator(Math.abs(lineNum));
        doAnimation();
    }

    /**
     * 注销已有动画
     */
    protected void cancelAnim() {
        if (animator != null) {
            animator.removeAllListeners();
            animator.end();
            animator = null;
        }
    }


    /**
     * 动态创建动画
     *
     * @param lineNum 需跳转行数
     */
    private void setAnimator(int lineNum) {
        animator = ValueAnimator.ofFloat(0, itemHeight * lineNum);//一行
        animator.setDuration(INTERVAL_ANIMATION);
        animator.setInterpolator(new LinearInterpolator());//插值器设为线性
    }

    /**
     * 监听动画
     */
    private void doAnimation() {
        if (animator == null) {
            return;
        }

        animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                offsetY = 0;
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                if (isReverse) {
                    startY += offsetY;
                } else {
                    startY -= offsetY;
                }
                offsetY = 0;
                invalidate();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

        animator.addUpdateListener(animation -> {
            float av = (float) animation.getAnimatedValue();
            if (av == 0) {
                return;
            }
            offsetY = av;
            invalidate();
        });

        animator.start();
    }

    public interface LrcCallback {
        void onUpdateTime(long time);

        void onFinish();
    }

    

    /**
     * 滑动监听
     *
     * @param callback LrcCallback
     */
    public void setCallback(LrcCallback callback) {
        this.callback = callback;
    }

    public void setTextColorMain(int textColorMain) {
        this.textColorMain = textColorMain;
    }

    public void setTextColorSec(int textColorSec) {
        this.textColorSec = textColorSec;
    }

    public void setTextSize(float textSize) {
        this.textSize = textSize;
        this.selectTextSize = textSize * TEXT_RATE;
        paint.setTextSize(textSize);
        calculateItem();
    }

    public void setLineSpace(float lineSpace) {
        this.lineSpace = lineSpace;
        calculateItem();
    }

    /**
     * 设置歌词
     *
     * @param lrc 解析后的string
     */
    public void setLrc(String lrc) {
        if (TextUtils.isEmpty(lrc)) {
            return;
        }
        reset();
        parseLrc(new InputStreamReader(new ByteArrayInputStream(lrc.getBytes())));
    }

    /**
     * 设置歌词
     *
     * @param resId 资源文件id
     */
    public void setLrc(@RawRes int resId) {
        reset();
        parseLrc(new InputStreamReader(context.getResources().openRawResource(resId), StandardCharsets.UTF_8));
    }

    /**
     * 设置歌词
     *
     * @param path lrc文件路径
     */
    public void setLrcFile(String path) {
        File file = new File(path);
        if (file.exists()) {
            reset();
            String format;
            if (CommonUtil.isUtf8(file)) {
                format = "UTF-8";
            } else {
                format = "GBK";
            }

            FileInputStream inputStream = null;
            try {
                inputStream = new FileInputStream(file);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

            InputStreamReader inputStreamReader = null;//'utf-8' 'GBK'
            try {
                inputStreamReader = new InputStreamReader(inputStream, format);
            } catch (UnsupportedEncodingException e) {
                e.printStackTrace();
            }

            parseLrc(inputStreamReader);
        }
    }

    /**
     * 调整播放位置
     *
     * @param time ms
     */
    public void seekTo(long time) {
        if (isDown) {
            //拖动歌词时暂不处理
            return;
        }

        if (time == 0) {
            //刷新
            invalidate();
            return;
        } else if (time > maxTime) {
            //超最大时间:通知结束
            if (callback != null) {
                callback.onFinish();
            }
            return;
        }

        for (int i = 0; i < lineList.size(); i++) {
            if (i < lineList.size() - 1) {
                if (time >= lineList.get(i).getTime() && time < lineList.get(i + 1).getTime()) {
                    int temp = i - curLine;
                    curLine = i;

                    updateAnim(temp);
                    break;
                }
            } else {//last line
                int temp = i - curLine;
                curLine = i;

                updateAnim(temp);
                break;
            }
        }
    }
}

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

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

相关文章

DNS/域名

概述 每个应用层协议都是为了解决某一类应用问题&#xff0c;而问题的解决又往往是通过位于不同主机中的多个应用进程之间的通信和协同工作来完成的。应用层的具体内容就是规定应用进程在通信时所遵循的协议。 应用层的许多协议都是基于客户服务器方式。客户(client)和服务器…

淘宝直播间智能化升级:基于LLM的学习与分析

自营直播应用技术团队负责的业务中&#xff0c;淘宝买菜的直播业务起步较晚&#xff0c;业务发展压力较大&#xff0c;业务上也就有了期望能够对一些二方的标杆直播间进行学习&#xff0c;并将其优点应用到自己直播间的需求。 最初 - 人海战术&#xff0c;学习PK 业务侧最直接的…

数学拯救世界(一)———寻“数”记

一、 很久很久以前&#xff0c;在一个只认识整数和小数的国度&#xff0c;有一个很残暴的国王提了一个要求&#xff1a;要是不能表示出把一段1米的绳子三等分后的大小&#xff0c;就要把所有的大臣杀掉。 1➗3 0.333&#xff0c;怎么办呀&#xff1f;怎么办呀&#xff1f; 袁q…

夏普MX-4608N复印机维修模式进入方法及载体初始化方法

夏普MX-4608N复印机载体型号&#xff08;图&#xff09;&#xff1a; 型 号&#xff1a;载体&#xff08;黑色&#xff09;MX-561CV 净含量&#xff1a;395克 下面图片中分别有载体、刮板、鼓芯、上纸盒搓纸轮一套&#xff0c;均原装正品&#xff1b; 保养周期将至的时候建…

FPGA Xilinx维特比译码器实现卷积码译码

FPGA Xilinx维特比译码器实现卷积码译码 文章目录 FPGA Xilinx维特比译码器实现卷积码译码1 Xilinx维特比译码器实现2 完整代码3 仿真结果 MATLAB &#xff08;n,k,m&#xff09;卷积码原理及仿真代码&#xff08;你值得拥有&#xff09;_matlab仿真后代码-CSDN博客 MATLAB 仿真…

java+ssm+mysql水产品商城

项目介绍&#xff1a; 使用javassmmysql开发的水产品商城&#xff0c;系统包含管理员、用户角色&#xff0c;功能如下&#xff1a; 管理员&#xff1a;用户管理&#xff1b;种类管理&#xff1b;商品管理&#xff1b;订单管理&#xff1b;评论管理&#xff1b;新闻管理&#…

【传感器技术】第5章 电容式传感器,变极距式电容传感器,变面积式电容传感器,变介质式电容传感器

关注作者了解更多 我的其他CSDN专栏 过程控制系统 工程测试技术 虚拟仪器技术 可编程控制器 工业现场总线 数字图像处理 智能控制 传感器技术 嵌入式系统 复变函数与积分变换 单片机原理 线性代数 大学物理 热工与工程流体力学 数字信号处理 光电融合集成电路…

二叉树前序遍历

什么是前序遍历&#xff1f; 一个二叉树的前序遍历就是对于树中的每一个节点而言&#xff0c;都是先遍历自己&#xff0c;再遍历左右。 如&#xff1a; 递归实现前序遍历 题目链接&#xff1a;144. 二叉树的前序遍历 - 力扣&#xff08;LeetCode&#xff09; 实现步骤&…

RAG系统分类、评估方法与未来方向

分享一篇RAG综述&#xff1a;Retrieval-Augmented Generation for Large Language Models: A Survey&#xff0c;主要想了解一下RAG的评估方法&#xff0c;分享给大家。 文章目录 一、RAG分类二、评估方法三、未来方向 一、RAG分类 RAG分类&#xff1a;Navie RAG、Advanced RA…

【软件安全】软件安全设计规范,软件系统安全设计制度(Word原件)

1.1安全建设原则 1.2 安全管理体系 1.3 安全管理规范 1.4 数据安全保障措施 1.4.1 数据库安全保障 1.4.2 操作系统安全保障 1.4.3 病毒防治 1.5安全保障措施 1.5.1实名认证保障 1.5.2 接口安全保障 1.5.3 加密传输保障 1.5.4终端安全保障 软件全面文档清单涵盖以下核心内容&a…

约克约克VRF中央空调,清凉舒适从此不再是梦

生活总是少不了空调的陪伴。但是&#xff0c;你是否还在为传统空调的高能耗、低效率而烦恼&#xff1f;别担心&#xff0c;约克VRF中央空调来帮你解决这一切难题&#xff01;      节能省电&#xff0c;我懂你~      现代生活讲究的是高效和环保&#xff0c;而约克VRF中…

DMA简介

DMA是一个数据转运小助手, 它主要是用来协助CPU,完成数据转运的工作 第一个程序: 在这个程序里&#xff0c;我们将使用DMA&#xff0c;进行存储器到存储器的数据转运, 也就是把一个数组里面的数据&#xff0c; 复制到另一个数组里 DMA简介 DMA外设&#xff0c; 是可以直接访…

liunx docker 部署 nacos seata sentinel

部署nacos 1.按要求创建好数据库 2.创建docker 容器 docker run -d --name nacos-server -p 8848:8848 -p 9848:9848 -p 9849:9849 -e MODEstandalone -e SPRING_DATASOURCE_PLATFORMmysql -e MYSQL_SERVICE_HOST172.17.251.166 -e MYSQL_SERVICE_DB_NAMEry-config -e MYSQL…

计算机视觉与各个学科融合:探索新方向

目录 引言计算机视觉与其他学科的结合 与医学的结合与机械工程的结合与土木工程的结合与艺术与人文的结合发文的好处博雅知航的辅导服务 引言 计算机视觉作为人工智能领域的重要分支&#xff0c;正迅速发展并渗透到多个学科。通过与其他领域的结合&#xff0c;计算机视觉不仅…

电阻计RM3544、RM3545的使用

目录&#xff1a; 一、电阻计与PC通讯 1、硬件连接 2、RmLogger.exe的使用 二、RM3545测量35uΩ电阻 一、电阻计与PC通讯 1、硬件连接 可以设置USB或COM口(串口)连接PC&#xff0c;也可以设置为“打印”输出。 1&#xff09;使用USB连接PC 2&#xff09;使用串口连接PC …

【JVM】JVM基础教程(一)

目录 初识JVM JVM是什么&#xff1f; JVM的功能 解释、即时编译和运行 内存管理 常见的JVM JVM虚拟机规范 HotSpot的发展历程 JVM的组成 字节码文件详解 应用场景 以正确姿势打开字节码文件 ​编辑字节码文件的组成 基本信息 Magic魔数 主副版本号 常量池 接口…

Mybaits的优点缺点?

大家好&#xff0c;我是锋哥。今天分享关于【Mybaits的优点&缺点?】面试题。希望对大家有帮助&#xff1b; Mybaits的优点&缺点? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 MyBatis 是一个优秀的持久层框架&#xff0c;通常用于 Java 应用程序中&…

gulp应该怎么用,前端批量自动化替换文件

背景 最近公司准备把所有项目中用到的国际化相关的key规范化&#xff0c;原因是: 一直以来公司的app和web端 在针对相同的需求以及相同的国际化语言&#xff0c;需要设置不同的两份国际化文件&#xff0c;难以维护旧版的国际化文件中&#xff0c;存在的大量值重复&#xff0c…

POI遍历行所有单元格的两种方式,getPhysicalNumberOfCells方式有问题,勿用

今天看POI源码的时候&#xff0c;发现HSSFWorkbook类型的工作簿&#xff0c;行数据是用TreeMap<Integer, HSSFRow>存储的&#xff0c;列数据是用HSSFCell[]数组来存的&#xff1b;XSSFWorkbook类型的工作簿&#xff0c;行数据是用SortedMap<Integer, XSSFRow>存储的…

NUMA-非统一内存访问架构

NUMA&#xff08;Non-Uniform Memory Access&#xff09; 是一种计算机内存架构&#xff0c;主要用于多处理器系统。NUMA架构中的每个处理器都连接到自己的本地内存&#xff0c;并且可以访问其他处理器的内存&#xff0c;但访问其他处理器的内存速度较慢。 内核通过调度优化进…