ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Plaid 项目定制版 Bypass:用四个 Spannable 扩展打造可点击链接、优雅引用块与异步图片的 Android Markdown 渲染器

Plaid 项目定制版 Bypass:用四个 Spannable 扩展打造可点击链接、优雅引用块与异步图片的 Android Markdown 渲染器 移动开发UI组件【免费下载链接】plaidAn Android app which provides design news inspiration as well as being an example of implementing material design.项目地址https://gitcode.com/gh_mirrors/pl/plaid点击查看免费下载本文以 third_party/bypass/README.md 为核心骨架深入讲解 Plaid 项目如何 fork 并改造开源 Markdown 处理器 Bypass通过新增TouchableUrlSpan、FancyQuoteSpan、ImageLoadingSpan与LoadImageCallback四类扩展让纯文本 Markdown 渲染具备按下可反馈的链接、带主题色竖线的引用块和占位后异步替换的图片三大能力。读完本文你将掌握这套扩展的源码级实现原理、配置参数含义以及它们在 Plaid 中的实际装配方式。一、背景为什么 Plaid 要 fork BypassBypass 是一个以 C/C 解析器通过 JNI 加载libbypass.so配合 AndroidSpannable体系输出富文本的 Markdown 处理器特点是解析快、不依赖 WebView。Plaid 在设计新闻Designer News模块中需要把大量 Markdown 格式的帖子正文渲染进TextView因此把它以第三方库的形式内嵌到仓库中。但原生 Bypass 的渲染效果并不完全满足 Plaid 的需求原生URLSpan在TextView里对触摸点击不敏感链接缺少按压反馈默认的QuoteSpan引用块视觉效果平淡与 Material Design 风格不协调原生实现无法在图片下载完成前留下占位、下载后原位替换也就无法和 Glide 等图片加载库协作。于是 Plaid 在 third_party/bypass/README.md 中明确记录了对上游项目Uncodin 的 Bypass所做的四项本地增强对应的版本、许可信息记录在同目录的 README.google 中版本 1.1遵循 Apache License v2.0完整条款见 LICENSE.txt并且本地修改点与 README 完全一致——Added TouchableUrlSpan, FancyQuoteSpan ImageLoadingSpan LoadImageCallback mechanism。下面逐一剖析这四项扩展的实现与用法。二、TouchableUrlSpan让链接拥有按下态反馈README 中的描述是An extension to URLSpan which changes its background foreground color when pressed——即按下链接时同时改变前景色与背景色。2.1 实现原理源码位于 TouchableUrlSpan.java它继承URLSpan在构造时从传入的ColorStateList中解析出常态与按下态两种文字颜色public TouchableUrlSpan(String url, ColorStateList textColor, int pressedBackgroundColor) { super(url); this.normalTextColor textColor.getDefaultColor(); this.pressedTextColor textColor.getColorForState(STATE_PRESSED, normalTextColor); this.pressedBackgroundColor pressedBackgroundColor; }核心在于重写updateDrawState(TextPaint)把是否按下映射为画笔状态Override public void updateDrawState(TextPaint drawState) { drawState.setColor(isPressed ? pressedTextColor : normalTextColor); drawState.bgColor isPressed ? pressedBackgroundColor : 0; drawState.setUnderlineText(!isPressed); }注意一个细节常态下链接显示下划线按下时下划线消失配合背景色与前景色的变化形成完整的按压反馈。外部通过setPressed(boolean)驱动状态切换。2.2 与触摸联动Plaid 的配套改造单有 Span 还不够TextView默认的触摸链路不会把按下事件转发给 Span。Plaid 在 HtmlUtils.java 中做了完整的配套public static void setTextWithNiceLinks(TextView textView, CharSequence input) { textView.setText(input); textView.setMovementMethod(LinkTouchMovementMethod.getInstance()); textView.setFocusable(false); textView.setClickable(false); textView.setLongClickable(false); }即必须给TextView设置自定义的LinkTouchMovementMethodLinkTouchMovementMethod.java把触摸按下/抬起事件转发给TouchableUrlSpan.setPressed(...)。此外HtmlUtils.java 中linkifyPlainLinks还会把Linkify生成的普通URLSpan统一替换为TouchableUrlSpan保证纯文本链接非 Markdown 语法书写的 URL也具备同样的按压反馈。三、FancyQuoteSpan绘制更优雅的引用块README 中的描述是A quote span with a nicer presentation。原生方案使用QuoteSpan的默认竖线样式Plaid 则自绘一条可定制颜色、宽度和缩进间距的竖线。3.1 实现原理源码位于 FancyQuoteSpan.java它实现LeadingMarginSpanpublic int getLeadingMargin(boolean first) { return lineWidth gapWidth; } public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { Paint.Style prevStyle p.getStyle(); int prevColor p.getColor(); p.setStyle(Paint.Style.FILL); p.setColor(lineColor); c.drawRect(x, top, x dir * lineWidth, bottom, p); p.setStyle(prevStyle); p.setColor(prevColor); }getLeadingMargin返回lineWidth gapWidth为引用文本腾出竖线 间隔的左边距drawLeadingMargin在行首绘制一条从top到bottom的实心矩形竖线绘制结束后恢复画笔原有的样式与颜色避免污染后续文本绘制。三个构造参数分别为竖线宽度quoteLineWidth、竖线与文本的间距quoteLineIndent、竖线颜色quoteLineColor。3.2 在 Bypass 中的装配在 Bypass.java 的BLOCK_QUOTE分支中原生QuoteSpan被注释掉替换为两层边距 FancyQuoteSpan 前景色 斜体case BLOCK_QUOTE: setBlockSpan(builder, new LeadingMarginSpan.Standard(mBlockQuoteIndent)); setBlockSpan(builder, new FancyQuoteSpan(mBlockQuoteLineWidth, mBlockQuoteLineIndent, mOptions.mBlockQuoteLineColor)); setBlockSpan(builder, new ForegroundColorSpan(mOptions.mBlockQuoteTextColor)); setBlockSpan(builder, new LeadingMarginSpan.Standard(mBlockQuoteIndent)); setBlockSpan(builder, new StyleSpan(Typeface.ITALIC)); break;代码注释说明了为什么加两层LeadingMarginSpan.StandardWe add two leading margin spans so that when the order is reversed, the QuoteSpan will always be in the same spot.——即保证 Span 顺序反转后竖线仍固定在正确位置。整个块引用还叠加了引用文本前景色与斜体形成层次分明的排版。四、ImageLoadingSpan 与 LoadImageCallback异步图片的占位与替换机制这是四项扩展中最能体现工程价值的一组。README 的描述是ImageLoadingSpan是一个用来标记文本中某处将被下载完成的图片替换的简单 SpanLoadImageCallback则是图片加载的回调机制。4.1 ImageLoadingSpan纯占位标记源码位于 ImageLoadingSpan.java它继承CharacterStyleupdateDrawState为空实现——它本身不绘制任何内容只作为图片应插入于此的锚点public class ImageLoadingSpan extends CharacterStyle { Override public void updateDrawState(TextPaint textPaint) { // no-op } }4.2 LoadImageCallback异步加载的接口接口定义在 LoadImageCallback.ktinterface LoadImageCallback { fun loadImage(src: String, loadingSpan: ImageLoadingSpan) }调用方如 Glide 封装层拿到图片 URL 与占位 Span 后自行发起异步加载加载完成后用真实图片替换loadingSpan所覆盖的文本区间。4.3 Bypass 中的完整渲染流程在 Bypass.java 的IMAGE分支中可以看到完整的占位逻辑case IMAGE: String url element.getAttribute(link); if (loadImageCallback ! null !TextUtils.isEmpty(url)) { setPrependedNewlineSpan(builder, mOptions.mPreImageLinebreakHeight); ImageLoadingSpan loadingSpan new ImageLoadingSpan(); setSpanWithPrependedNewline(builder, loadingSpan); // make the (eventually loaded) image span clickable to open in browser setSpanWithPrependedNewline(builder, new TouchableUrlSpan(url, linksColors, highlightColor)); loadImageCallback.loadImage(url, loadingSpan); } break;几个值得注意的实现细节图片独占一行文本构建阶段在图片前先插入换行builder.append(\n)保证图片总是另起一行展示无回调时输出替换字符如果未提供LoadImageCallback或图片 URL 为空则只插入\uFFFC对象替换字符图片无法加载时界面不会崩溃alt/title 兜底存在 alt 文本时优先输出[alt]作为占位文字否则输出\uFFFC图片同时可点击图片位置同时挂一个TouchableUrlSpan加载完成后点按图片可在浏览器中打开原图换行高度可配置setPrependedNewlineSpan用mPreImageLinebreakHeight控制图片前空白行的高度默认 4dp 级别见下文 Options。五、配套的 Options 配置体系这四个扩展的视觉效果大多由 Bypass.java 内嵌的Options类驱动。Options提供链式 setter默认值如下配置项setter 方法默认值说明标题字号倍数h1~h6setHeaderSizes(float[6]){1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1.0f}必须恰好 6 个元素否则抛IllegalArgumentException无序列表符号setUnorderedListItem(String)\u2022•渲染无序列表项的前缀字符列表项缩进setListItemIndentSize(unit, size)dp, 10单位可为TypedValue.COMPLEX_UNIT_DIP/SP/PX等引用竖线颜色setBlockQuoteLineColor(int)0xff0000ff蓝FancyQuoteSpan竖线颜色引用文本颜色setBlockQuoteTextColor(int)无默认黑色引用块前景色引用竖线宽度setBlockQuoteLineWidth(int)未设默认由调用方传入单位固定为 dp引用竖线间距setBlockQuoteLineIndent(int)未设默认由调用方传入竖线与文本间隔单位 dp引用整体缩进setBlockQuoteIndentSize(unit, size)dp, 10两层边距之一图片前换行高度setPreImageLinebreakHeight(int)4控制图片前空白行像素高度代码块缩进setCodeBlockIndentSize(unit, size)dp, 10BLOCK_CODE左缩进分割线颜色/尺寸setHruleColor / setHruleSizeColor.GRAYdp, 1供HorizontalLineSpan绘制---分割线构造函数Bypass(DisplayMetrics displayMetrics, Options options)会把这些配置统一通过TypedValue.applyDimension换算成像素换算结果缓存为成员变量供渲染时直接使用。除上述四个扩展外仓库还包含一个同样由 Plaid 定制的 HorizontalLineSpan.java继承ReplacementSpangetSize返回Integer.MAX_VALUE以横贯整行绘制时在行垂直中段画一条矩形分割线对应 Markdown 的---语法。六、在 Plaid 应用中的装配方式Bypass 在 Plaid 中通过 Dagger 注入为Markdown接口实例。装配点位于 MarkdownModule.ktModule class MarkdownModule constructor( private val displayMetrics: DisplayMetrics, private val options: Bypass.Options Bypass.Options() ) { Provides FeatureScope fun provideMarkdown(): Markdown Bypass(displayMetrics, options) }Markdown接口定义在 Markdown.kt它把渲染入口统一为fun markdownToSpannable( content: String, linksColor: ColorStateList, ColorInt highlightColor: Int, callback: LoadImageCallback? ): CharSequence而在 HtmlUtils.java 中parseMarkdownAndPlainLinks把 Markdown 渲染与纯文本链接识别结合起来public static CharSequence parseMarkdownAndPlainLinks( String input, Markdown markdown, ColorStateList linkTextColors, ColorInt int highlightColor, LoadImageCallback loadImageCallback) { CharSequence markedUp markdown.markdownToSpannable(input, linkTextColors, highlightColor, loadImageCallback); return linkifyPlainLinks(markedUp, linkTextColors, highlightColor); }其方法注释解释了这样组合的原因Markdown不会处理非 Markdown 语法的裸链接而Linkify处理裸链接时会抹掉已有 Span因此只能先用 Markdown 渲染再对输出副本做 Linkify最后把新发现的URLSpan换成TouchableUrlSpan合并回结果——两全其美。这也正是 README 中四项扩展被设计出来的真实工程动机。七、构建与原生库依赖Bypass 的 Markdown 解析由原生代码完成。在 Bypass.java 的静态块中static { System.loadLibrary(bypass); }对应地仓库在 third_party/bypass/src/main/jniLibs 下为arm64-v8a、armeabi、armeabi-v7a、mips、mips64、x86、x86_64七种 ABI 预编译了libbypass.so解析入口processMarkdown声明为private native Document processMarkdown(String markdown)Bypass.java将 Markdown 文本解析为 Document.java 与 Element.java 组成的元素树再由 Java 侧递归遍历recurseElement逐节点挂载 Span。解析树渲染的典型元素类型包括HEADER按 level 应用RelativeSizeSpan 粗体、LIST/LIST_ITEM有序号则从 1 递增无序则用mUnorderedListItem、EMPHASIS/DOUBLE_EMPHASIS/TRIPLE_EMPHASIS斜体/粗体/粗斜体、CODE_SPAN/BLOCK_CODEmonospace 字体 缩进、LINK/AUTOLINKTouchableUrlSpan邮箱自动加mailto:前缀、BLOCK_QUOTEFancyQuoteSpan组合、STRIKETHROUGHStrikethroughSpan、HRULEHorizontalLineSpan、IMAGE占位 回调。此外 ReverseSpannableStringBuilder.java 作为内部构建器配合setSpanWithPrependedNewline等工具方法保证嵌套元素的 Span 边界正确。八、小结Plaid 对 Bypass 的 fork 改动虽小却精准解决了 Android 富文本渲染的三个常见痛点链接交互TouchableUrlSpanLinkTouchMovementMethod补上了原生URLSpan缺失的按压反馈引用样式FancyQuoteSpan用可配置的竖线、间距与颜色替代默认QuoteSpan配合斜体与前景色形成 Material 风格引用块图片异步化ImageLoadingSpan占位 LoadImageCallback回调让 Markdown 渲染与图片加载库解耦协作且图片始终独占一行、加载后可点击。整套机制通过Options暴露了从标题字号、列表符号、引用配色到图片占位高度的完整定制面并由 MarkdownModule.kt 以 Dagger 单例注入到 Plaid 的各业务模块中。如果你要在自己的 Android 项目里接入 Bypass 并复刻这套增强只需要内嵌该 fork含jniLibs各 ABI 的libbypass.so、按需构造Bypass.Options、实现LoadImageCallback接入 Glide 等图片库并为承载富文本的TextView设置LinkTouchMovementMethod即可获得与 Plaid 一致的渲染效果。赞分享移动开发UI组件【免费下载链接】plaidAn Android app which provides design news inspiration as well as being an example of implementing material design.项目地址https://gitcode.com/gh_mirrors/pl/plaid点击查看免费下载相关推荐RailsCasts评论功能开发指南嵌套评论与实时交互的Ruby实现RailsCasts评论功能开发指南嵌套评论与实时交互的Ruby实现 RailsCasts是一个经典的Ruby on Rails教程项目其评论系统实现了嵌套MarkdownView:一个用于Android的可自定义的Markdown渲染器MarkdownView:一个用于Android的可自定义的Markdown渲染器 MarkdownView是一个开源的Android库它允许您在应用程序中显移动开发UI库/组件Mailing邮件列表管理如何创建、订阅和退订功能的完整实现Mailing邮件列表管理如何创建、订阅和退订功能的完整实现 Mailing是一个基于React的邮件构建与管理工具能够帮助开发者轻松实现邮件列表的创建、订上一篇猫抓Cat-Catch5分钟学会免费捕获网页视频资源的终极浏览器扩展下一篇Apache SkyWalking v10 Service Hierarchy服务层级机制深度解析跨层服务/实例关系自动识别与自定义配置创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表