Android TextView 中的 Markdown 支持

25

有没有一种方法可以使 TextView 能够检测 Markdown 标签并相应地呈现文本?更具体地说,我的应用程序中包含一个 TextView ,用户可以在其中提供说明,并经常使用 markdown 来格式化其说明。不幸的是,这些文本未能呈现,而是我们在 textview 中看到了所有标记的写法。


请您能否添加一些您的代码。这将有助于我们检测问题,也更有可能得到答案。 - t.h3ads
http://uncodin.github.io/bypass/,目前恐怕还不支持gradle构建,因为它是一个“apklib”。 - Ferran Maylinch
7个回答

12

Android SDK没有内置Markdown支持。您需要使用类似 markdown4jCommonMark 这样的库。


9
我了解您想将包含Markdown标记的字符串转换为格式化的CharSequence,以便在TextView中使用。我知道的两个选项是: 我都使用过,我认为第二个更好:无需处理本机架构,APK更小,并且性能相当不错(我的情况下大约慢2倍,足够好了)。
更新:找到另一个选项(这是我现在使用的):
  • Markwon:纯Java,也使用commonmark-java作为解析器,可选择支持图像和表格

这些都可以提供自定义吗?比如所有属性的字体颜色等? - ShahrozKhan91
Markwon允许相当多的自定义 - bwt
嗨@bwt,我正在尝试在我的应用程序中使用Markwon库,但我卡在了链接处理部分。我想知道如何检索链接文本以进行进一步的格式化。是否有一个地方可以获取有关使用Markwon库的更多信息?非常感谢任何帮助。 - Sandeep Yohans
@bwt 我不太明白如何使用Markwon,你能否在你的回答中解释一下? - tycoon

8
在TextView中没有原生的markdown支持,但是如果你只需要通过简单的“regexp”匹配实现简单的markdown-lite功能,可以参考这个链接:https://github.com/mofosyne/instantReadmeApp 中的“从项目根目录加载自述文件”的代码段。
请注意,这不会删除文本中的标记,只是以不同的样式呈现行。这可能是好事或坏事,具体取决于您的应用程序。
而且,好处在于它使用本地textview进行样式设置,因此文本仍然可像普通文本一样选择。
特别是这一行:https://github.com/mofosyne/instantReadmeApp/blob/master/app/src/main/java/com/github/mofosyne/instantreadme/ReadMe.java#L137 略作修改如下:private void style_psudomarkdown_TextView(String text, TextView textview_input),这样您就可以为不同的textview使用相同的函数。
/*
    Text Styler
    A crappy psudo markdown styler. Could do with a total revamp.
 */

/*
* Styling the textview for easier readability
* */
private void style_psudomarkdown_TextView(String text, TextView textview_input) {
    //TextView mTextView = (TextView) findViewById(R.id.readme_info);
    TextView mTextView = textview_input;

    // Let's update the main display
    // Needs to set as spannable otherwise https://dev59.com/0WQo5IYBdhLWcg3wE7-2
    mTextView.setText(text, TextView.BufferType.SPANNABLE);
    // Let's prettify it!
    changeLineinView_TITLESTYLE(mTextView, "# ", 0xfff4585d, 2f); // Primary Header
    changeLineinView(mTextView, "\n# ", 0xFFF4A158, 1.5f); // Secondary Header
    changeLineinView(mTextView, "\n## ", 0xFFF4A158, 1.2f); // Secondary Header
    changeLineinView(mTextView, "\n---", 0xFFF4A158, 1.2f); // Horizontal Rule
    changeLineinView(mTextView, "\n>",   0xFF89e24d, 0.9f); // Block Quotes
    changeLineinView(mTextView, "\n - ", 0xFFA74DE3, 1f);   // Classic Markdown List
    changeLineinView(mTextView, "\n- ", 0xFFA74DE3, 1f);   // NonStandard List

    //spanSetterInView(String startTarget, String endTarget, int typefaceStyle, String fontFamily,TextView tv, int colour, float size)
    // Limitation of spanSetterInView. Well its not a regular expression... so can't exactly have * list, and *bold* at the same time.
    spanSetterInView(mTextView, "\n```\n", "\n```\n",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.8f, false); // fenced code Blocks ( endAtLineBreak=false since this is a multiline block operator)
    spanSetterInView(mTextView,   " **"  ,     "** ",   Typeface.BOLD,        "",  0xFF89e24d,  1f, true); // Bolding
    spanSetterInView(mTextView,    " *"  ,      "* ",   Typeface.ITALIC,      "",  0xFF4dd8e2,  1f, true); // Italic
    spanSetterInView(mTextView,  " ***"  ,    "*** ",   Typeface.BOLD_ITALIC, "",  0xFF4de25c,  1f, true); // Bold and Italic
    spanSetterInView(mTextView,    " `"  ,      "` ",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.8f, true); // inline code
    spanSetterInView(mTextView, "\n    " ,      "\n",   Typeface.BOLD,        "monospace",  0xFF45c152,  0.7f, true); // classic indented code
}

private void changeLineinView(TextView tv, String target, int colour, float size) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    while (true) {
        startSpan = vString.indexOf(target, endSpan-1);     // (!@#$%) I want to check a character behind in case it is a newline
        endSpan = vString.indexOf("\n", startSpan+1);       // But at the same time, I do not want to read the point found by startSpan. This is since startSpan may point to a initial newline.
        ForegroundColorSpan foreColour = new ForegroundColorSpan(colour);
        // Need a NEW span object every loop, else it just moves the span
        // Fix: -1 in startSpan or endSpan, indicates that the indexOf has already searched the entire string with not valid match (Lack of endspan check, occoured because of the inclusion of endTarget, which added extra complications)
        if ( (startSpan < 0) || ( endSpan < 0 ) ) break;// Need a NEW span object every loop, else it just moves the span
        // Need to make sure that start range is always smaller than end range. (Solved! Refer to few lines above with (!@#$%) )
        if (endSpan > startSpan) {
            //endSpan = startSpan + target.length();
            spanRange.setSpan(foreColour, startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Also wannna bold the span too
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(Typeface.BOLD), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    tv.setText(spanRange);
}

private void changeLineinView_TITLESTYLE(TextView tv, String target, int colour, float size) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    /*
    * Had to do this, since there is something wrong with this overlapping the "##" detection routine
    * Plus you only really need one title.
     */
    //while (true) {
    startSpan = vString.substring(0,target.length()).indexOf(target, endSpan-1); //substring(target.length()) since we only want the first line
    endSpan = vString.indexOf("\n", startSpan+1);
    ForegroundColorSpan foreColour = new ForegroundColorSpan(colour);
    // Need a NEW span object every loop, else it just moves the span
        /*
        if (startSpan < 0)
            break;
            */
    if ( !(startSpan < 0) ) { // hacky I know, but its to cater to the case where there is no header text
        // Need to make sure that start range is always smaller than end range.
        if (endSpan > startSpan) {
            //endSpan = startSpan + target.length();
            spanRange.setSpan(foreColour, startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Also wannna bold the span too
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(Typeface.BOLD_ITALIC), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    //}
    tv.setText(spanRange);
}


private void spanSetterInView(TextView tv, String startTarget, String endTarget, int typefaceStyle, String fontFamily, int colour, float size, boolean endAtLineBreak) {
    String vString = (String) tv.getText().toString();
    int startSpan = 0, endSpan = 0;
    //Spannable spanRange = new SpannableString(vString);
    Spannable spanRange = (Spannable) tv.getText();
    while (true) {
        startSpan = vString.indexOf(startTarget, endSpan-1);     // (!@#$%) I want to check a character behind in case it is a newline
        endSpan = vString.indexOf(endTarget, startSpan+1+startTarget.length());     // But at the same time, I do not want to read the point found by startSpan. This is since startSpan may point to a initial newline. We also need to avoid the first patten matching a token from the second pattern.
        // Since this is pretty powerful, we really want to avoid overmatching it, and limit any problems to a single line. Especially if people forget to type in the closing symbol (e.g. * in bold)
        if (endAtLineBreak){
            int endSpan_linebreak = vString.indexOf("\n", startSpan+1+startTarget.length());
            if ( endSpan_linebreak < endSpan ) { endSpan = endSpan_linebreak; }
        }
        // Fix: -1 in startSpan or endSpan, indicates that the indexOf has already searched the entire string with not valid match (Lack of endspan check, occoured because of the inclusion of endTarget, which added extra complications)
        if ( (startSpan < 0) || ( endSpan < 0 ) ) break;// Need a NEW span object every loop, else it just moves the span
        // We want to also include the end "** " characters
        endSpan += endTarget.length();
        // If all is well, we shall set the styles and etc...
        if (endSpan > startSpan) {// Need to make sure that start range is always smaller than end range. (Solved! Refer to few lines above with (!@#$%) )
            spanRange.setSpan(new ForegroundColorSpan(colour), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new RelativeSizeSpan(size), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            spanRange.setSpan(new StyleSpan(typefaceStyle), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
            // Default to normal font family if settings is empty
            if( !fontFamily.equals("") )  spanRange.setSpan(new TypefaceSpan(fontFamily), startSpan, endSpan, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        }
    }
    tv.setText(spanRange);
}

上述实现仅支持最多2个标题(但您可以轻松修改正则表达式以支持超过2级标题)。

它是一系列基于正则表达式的文本视图,由两个函数组成,用于匹配始终为一行的正则表达式changeLineinView()changeLineinView_TITLESTYLE()

对于跨越多行的情况,spanSetterInView()函数处理此类情况。

因此,只要您具有不与任何其他语法冲突的正则表达式,就可以将其扩展到适合您的目的。

类似Markdown的语法:

这是受支持的语法。无法支持完整的markdown,因为这只是一个简单的hacky实现。但对于易于在移动电话键盘上输入的无花样显示非常方便。

# H1 only in first line (Due to technical hacks used)

## H2 headers as usual

## Styling
Like: *italic* **bold** ***bold_italic***

## Classic List
 - list item 1
 - list item 2

## Nonstandard List Syntax
- list item 1
- list item 2

## Block Quotes
> Quoted stuff

## codes
here is inline `literal` codes. Must have space around it.

    ```
    codeblocks
    Good for ascii art
    ```

    Or 4 space code indent like classic markdown.

1
你能添加一个 *.md 文件加载器吗? - binrebin

4

我可以推荐MarkdownView。我使用它来从资产文件夹中加载markdown文件。

如果有帮助的话,这是我的实现方法...

在我的布局中:

<us.feras.mdv.MarkdownView
    android:id="@+id/descriptionMarkdownView"
    android:layout_width="0dp"
    android:layout_height="wrap_content"
    android:layout_marginTop="10dp"
    app:layout_constraintTop_toBottomOf="@id/thumbnailImageView"
    app:layout_constraintStart_toEndOf="@id/guidelineStart"
    app:layout_constraintEnd_toEndOf="@id/guidelineEnd"
    app:layout_constraintBottom_toTopOf="@id/parent"/>

在我的Activity中:
val cssPath = "file:///android_asset/markdown.css"
val markdownPath = "file:///android_asset/markdown/filename.md"
descriptionMarkdownView.loadMarkdownFile(markdownPath, cssPath)

感谢您的评论,我在这个周末接触到了这个库,它非常容易处理。在我的情况下,我使用它来查看带有Markdown格式的笔记,并且它足够好以完成任务。 - JorgeAmVF

4

自上周以来,我一直关注这篇文章,并测试了这里建议的许多Markdown库 - 这个问题和这些答案基本上是我在网上能找到的最好的关于这个主题的来源。

其中有两个引起了我最大的注意,MarkdownViewMarkwon,但前者比后者更容易处理,因此我使用它来通过Markdown格式化增强一个笔记应用程序(这是我的主要个人目标)。

如果你想要一个Markdown实时预览,你可以直接使用库本身提供的这个示例活动,以及其他选项。如果你需要将自己的活动适应它,我建议你将以下代码片段添加到你的项目中:

build.gradle

implementation 'us.feras.mdv:markdownview:1.1.0'

private MarkdownView markdownView;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    this.markdownView = findViewById(R.id.markdownView);
    this.udateMarkdownView();
}

private void updateMarkdownView() {
    markdownView.loadMarkdown(note_content.getText().toString());
}

这里你可以找到我在GitHub上提供的示例,除了库本身提供的示例之外,你还可以看到一个工作项目。


2

看一下commonmark-java库。 虽然我没有亲自尝试过,但我认为您可以在您的情况下将其应用。


-3
如果您想呈现HTML,可以使用Html.fromHtml("your string"),有关Android中字符串的更多资源,请查看link

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接