概要
这个需求其实是拿来优化的,因为之前设备不同,可能渲染复杂的WebView会导致字幕的轻微撕裂以及滚动效果的不加,所以后面决定使用原生Android来实现复杂的跑马灯效果,折腾了好久,请原谅我才刚入行的菜鸟。
整体架构流程
使用到的三方库
implementation 'org.jsoup:jsoup:1.14.3' // 使用最新版本替换此处的1.14.3
在build.gradle(app)中导入这个用来解析HTML三方库
实现细节
1.通过jsoup库的解析,将指定元素下的节点以及子节点全部解析出来
2.涉及深度优先搜索策略,对DOM树的解析
3.涉及结构化实体类的搭建
第一步 1.jsoup库的解析以及使用(目前涉及到的只是针对assets文件下资源文件的解析,如果要解析本地路径,把相对应的替换即可,因为assets下的HTML文件好调试。)
首先创建一个工具类
AcquireTextMessageUtils (给出核心代码部分)
该工具类主要是运用jsoup库来实现对HTML的文件解析,一共包含两个方法,一个是可以获取资源文件assets中的HTML文件,一个获取本地指定路径下的文件。
/**
* 解析`assets`目录下HTML文件中指定父元素及其所有后代元素的属性,并返回结构化的元素数据列表。
* <即将使用的主要方法>
* @param context 上下文对象
* @param assetHtmlFileName `assets`目录下HTML文件名
* @param parentSelector 父元素的CSS选择器
* @return 结构化的元素数据列表,每个元素数据为一个`ElementData`对象
*/
public static List<ElementData> parseChildElementsAndAttributes(Context context, String assetHtmlFileName, String parentSelector) {
try {
// 从assets目录读取HTML文件内容
StringBuilder htmlStringBuilder = new StringBuilder();
BufferedReader reader = new BufferedReader(new InputStreamReader(context.getAssets().open(assetHtmlFileName), StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
htmlStringBuilder.append(line).append("\n");
}
reader.close();
String htmlContent = htmlStringBuilder.toString();
// 解析HTML内容为Document对象
Document doc = Jsoup.parse(htmlContent);
// 选取指定父元素
Element parentElement = doc.getElementById(parentSelector);
if (parentElement != null) {
// 递归遍历父元素及其所有后代节点,获取结构化的元素数据列表
return parseElementAndAttributes(parentElement, 0);
} else {
Log.w("AcquireTextMessageUtils", "Parent element not found for selector: " + parentSelector);
return new ArrayList<>();
}
} catch (IOException e) {
Log.e("AcquireTextMessageUtils", "Error reading or parsing HTML file:", e);
return new ArrayList<>();
}
}
private static List<ElementData> parseElementAndAttributes(Element element, int depth) {
List<ElementData> elementDataList = new ArrayList<>();
StringBuilder indent = new StringBuilder();
for (int i = 0; i < depth; i++) {
indent.append("\t");
}
ElementData elementData = new ElementData();
elementData.setDepth(depth);
elementData.setIndent(indent.toString());
elementData.setTagName(element.tagName());
Attributes attributes = element.attributes();
for (Attribute attribute : attributes) {
elementData.getAttributes().put(attribute.getKey(), attribute.getValue());
}
for (Node child : element.childNodes()) {
if (child instanceof Element) {
elementData.getChildNodes().addAll(parseElementAndAttributes((Element) child, depth + 1));
} else if (child instanceof TextNode) {
// 处理 TextNode
ElementData otherNodeData = new ElementData();
otherNodeData.setDepth(depth + 1);
otherNodeData.setIndent(indent.toString());
otherNodeData.setTextContent(((TextNode) child).text());
elementData.getChildNodes().add(otherNodeData);
} else {
// 处理其他非 Element 节点(如 CommentNode 等)
ElementData otherNodeData = new ElementData();
otherNodeData.setDepth(depth + 1);
otherNodeData.setIndent(indent.toString());
// 修改此处,使用合适的文本表示方法而非 outerHtml()
otherNodeData.setTextContent(child.toString()); // 或者使用 child.nodeName()、child.baseUri() 等,取决于您希望存储的内容
elementData.getChildNodes().add(otherNodeData);
}
}
elementDataList.add(elementData);
return elementDataList;
}
/**
* 增加一个方法,从本地DOWNLOAD解析文件
* 类似于上述方法,由于不需要从本app的文件读取资源文件,所以剔除了context
* */
public static List<ElementData> parseChildElementsAndAttributes(String filePath, String parentSelector) {
try {
// 从本地路径读取HTML文件内容
StringBuilder htmlStringBuilder = new StringBuilder();
FileInputStream fis = new FileInputStream(filePath);
BufferedReader reader = new BufferedReader(new InputStreamReader(fis, StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
htmlStringBuilder.append(line).append("\n");
}
reader.close();
fis.close();
String htmlContent = htmlStringBuilder.toString();
// 解析HTML内容为Document对象
Document doc = Jsoup.parse(htmlContent);
// 选取指定父元素
Element parentElement = doc.getElementById(parentSelector);
//对指定的父元素再缓存或者源文件中插入display:none属性
if (parentElement != null) {
// 递归遍历父元素及其所有后代节点,获取结构化的元素数据列表
return parseElementAndAttributes(parentElement, 0);
} else {
Log.w("AcquireTextMessageUtils", "Parent element not found for selector: " + parentSelector);
return new ArrayList<>();
}
} catch (IOException e) {
Log.e("AcquireTextMessageUtils", "Error reading or parsing HTML file:", e);
return new ArrayList<>();
}
}
方法实现起来比较简单,没有做过多CSS解释器的处理,只是单纯的解析目标元素以及子节点 。
当然解析只是第一步,只是负责将目标元素从HTML文件中提取出来,你还需要对数据进行处理方便你后续对该数据的解析以及正则表达。
第二步:对数据的处理
创建
HTMLMessageExtractor类
可以看到如下调用,
public static ArrayList<String> getHtmlMessage(String filePath, String parentSelector, StyleTextUpdate styleTextUpdate, DisplayMetrics displayMetrics) {
List<ElementData> elementDataList = AcquireTextMessageUtils.parseChildElementsAndAttributes(filePath, parentSelector);
return collectElementDataRecursively(elementDataList, new ArrayList<>(), false, false, styleTextUpdate, displayMetrics);
}
private static ArrayList<String> collectElementDataRecursively(List<ElementData> elementDataList, List<String> accumulatedAttributes, boolean isParentInsideEm, boolean isParentStrong, StyleTextUpdate styleTextUpdate, DisplayMetrics displayMetrics) {
ArrayList<String> logMessages = new ArrayList<>();
for (ElementData elementData : elementDataList) {
// 合并当前元素的属性到累积属性中
List<String> combinedAttributes = new ArrayList<>(accumulatedAttributes);
combinedAttributes.add(elementData.getAttributes().get("style"));
// 获取文本内容,如果为空,则使用"无文本内容"代替
String textContent = elementData.getTextContent() != null ? elementData.getTextContent() : "";
//-----------------------这里根据需要,你可以对你需要的标签做进一步处理,比如对strong em标签做标记以便后续处理。
// 构建属性字符串
StringBuilder attrStrBuilder = new StringBuilder();
for (String entry : combinedAttributes) {
attrStrBuilder.append(entry).append("; ");
}
if (attrStrBuilder.length() > 0) {
attrStrBuilder.setLength(attrStrBuilder.length() - 2); // 移除最后一个分号和空格
}
String logMessagess = " 深度" + elementData.getDepth() + " TagName:[" + elementData.getTagName() + "] 文本内容: {" + textContent + "}; 属性: " + attrStrBuilder + (isInsideEm ? "<em>" : "") + " " + (isStrong ? "<strong>" : "");//+" <---> "+attrStrBuilder+(isStrong?"<strong>":"")
// Log.d("AcquireTextMessageUtils", "AcquireTextMessageUtils data:" + logMessagess);
int startIndex = styleTextUpdate.getLength();
// styleTextUpdate.appendContent(textContent);
// 收集逻辑,包括强调状态
if (!textContent.isEmpty()) {
String logMessage = " 深度" + elementData.getDepth() + " TagName:[" + elementData.getTagName() + "] 文本内容: {" + textContent + "}; 属性: " + attrStrBuilder + (isInsideEm ? "<em>" : "") + " " + (isStrong ? "<strong>" : "");
Log.d("AcquireTextMessageUtils", "AcquireTextMessageUtils data:" + logMessage);
styleTextUpdate.enqueueLogContent(textContent, combinedAttributes, isInsideEm, isStrong, displayMetrics);
logMessages.add(logMessage);
}
// 递归调用,传递累积的属性和当前的强调状态,并收集返回的日志消息
if (elementData.getChildNodes() != null && !elementData.getChildNodes().isEmpty()) {
ArrayList<String> childMessages = collectElementDataRecursively(elementData.getChildNodes(), combinedAttributes, isInsideEm, isStrong, styleTextUpdate, displayMetrics);
logMessages.addAll(childMessages);
}
int endIndex = styleTextUpdate.getLength();
}
return logMessages;
}
第三步对数据进行进一步处理,对样式进行修改
styleTextUpdate类
private SpanTextView textView2;
private SpannableStringBuilder accumulatedText = new SpannableStringBuilder();
private static final String TAG = "StyleTextUpdate";
private int as=0;
public StyleTextUpdate(SpanTextView textView) {
this.textView2 = textView;
}
public void enqueueLogContent(String content, List<String> styles, boolean isInsideEm, boolean isInsideStrong, DisplayMetrics displayMetrics) {
as++;
Log.d("StyleTextUpdate","循环次数"+as);
// Log.d("StyleTextUpdate","---》内容:"+content+" ---->样式"+styleInfo+" -----s"+content.length());
// 添加内容到累积文本
accumulatedText.append(content);
stylesFlag = new int[12];
// 应用样式到新添加的文本上
for (int i = styles.size() - 1; i >= 0; i--) {
String styleInfo = styles.get(i);
if (styleInfo == null) {
continue;
}
applyStyles(styleInfo, accumulatedText.length() - content.length(), accumulatedText.length(), displayMetrics);
int a = accumulatedText.length() - content.length();
Log.d("StyleTextUpdate", " index:" + a + " ---" + accumulatedText.length());
}
//这里做你需要的标签处理
// 设置到TextView
textView2.setText(accumulatedText);
}
public int getLength() {
return accumulatedText.length();
}
可以看到这里用
SpannableStringBuilder做字符串拼接
小结
说白了到这里,基础的框架已经搭建完了,具体实现的地方可以自己自定义,上述给出的是部分代码,如果需要源码,请移步到我github地址:GitHub - CzZmessages/ailiyunOSS: this is ailiyunDownload 这是一个阿里云的下载代码