Sample handling of details HTML tag
This commit is contained in:
parent
17756a1137
commit
2e7d0aa46b
@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 4.2.1-SNAPSHOT
|
||||||
|
* Fix SpannableBuilder `subSequence` method
|
||||||
|
* Introduce Nougat check in `BulletListItemSpan` to position bullet (for bullets to be
|
||||||
|
positioned correctly when nested inside other `LeadingMarginSpan`s)
|
||||||
|
|
||||||
# 4.2.0
|
# 4.2.0
|
||||||
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
||||||
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
||||||
|
@ -187,7 +187,7 @@ public class SpannableBuilder implements Appendable, CharSequence {
|
|||||||
// if a span was fully including resulting subSequence it's start and
|
// if a span was fully including resulting subSequence it's start and
|
||||||
// end must be within 0..length bounds
|
// end must be within 0..length bounds
|
||||||
s = Math.max(0, span.start - start);
|
s = Math.max(0, span.start - start);
|
||||||
e = Math.max(length, s + (span.end - span.start));
|
e = Math.min(length, s + (span.end - span.start));
|
||||||
|
|
||||||
builder.setSpan(
|
builder.setSpan(
|
||||||
span.what,
|
span.what,
|
||||||
|
@ -4,6 +4,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.style.LeadingMarginSpan;
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
|
||||||
@ -15,6 +16,13 @@ import io.noties.markwon.utils.LeadingMarginUtils;
|
|||||||
|
|
||||||
public class BulletListItemSpan implements LeadingMarginSpan {
|
public class BulletListItemSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private static final boolean IS_NOUGAT;
|
||||||
|
|
||||||
|
static {
|
||||||
|
final int sdk = Build.VERSION.SDK_INT;
|
||||||
|
IS_NOUGAT = Build.VERSION_CODES.N == sdk || Build.VERSION_CODES.N_MR1 == sdk;
|
||||||
|
}
|
||||||
|
|
||||||
private MarkwonTheme theme;
|
private MarkwonTheme theme;
|
||||||
|
|
||||||
private final Paint paint = ObjectsPool.paint();
|
private final Paint paint = ObjectsPool.paint();
|
||||||
@ -62,28 +70,41 @@ public class BulletListItemSpan implements LeadingMarginSpan {
|
|||||||
|
|
||||||
final int marginLeft = (width - side) / 2;
|
final int marginLeft = (width - side) / 2;
|
||||||
|
|
||||||
// @since 2.0.2
|
|
||||||
// There is a bug in Android Nougat, when this span receives an `x` that
|
|
||||||
// doesn't correspond to what it should be (text is placed correctly though).
|
|
||||||
// Let's make this a general rule -> manually calculate difference between expected/actual
|
|
||||||
// and add this difference to resulting left/right values. If everything goes well
|
|
||||||
// we do not encounter a bug -> this `diff` value will be 0
|
|
||||||
final int diff;
|
|
||||||
if (dir < 0) {
|
|
||||||
// rtl
|
|
||||||
diff = x - (layout.getWidth() - (width * level));
|
|
||||||
} else {
|
|
||||||
diff = (width * level) - x;
|
|
||||||
}
|
|
||||||
|
|
||||||
// in order to support RTL
|
// in order to support RTL
|
||||||
final int l;
|
final int l;
|
||||||
final int r;
|
final int r;
|
||||||
{
|
{
|
||||||
final int left = x + (dir * marginLeft);
|
// @since 4.2.1-SNAPSHOT to correctly position bullet
|
||||||
final int right = left + (dir * side);
|
// when nested inside other LeadingMarginSpans (sorry, Nougat)
|
||||||
l = Math.min(left, right) + (dir * diff);
|
if (IS_NOUGAT) {
|
||||||
r = Math.max(left, right) + (dir * diff);
|
|
||||||
|
// @since 2.0.2
|
||||||
|
// There is a bug in Android Nougat, when this span receives an `x` that
|
||||||
|
// doesn't correspond to what it should be (text is placed correctly though).
|
||||||
|
// Let's make this a general rule -> manually calculate difference between expected/actual
|
||||||
|
// and add this difference to resulting left/right values. If everything goes well
|
||||||
|
// we do not encounter a bug -> this `diff` value will be 0
|
||||||
|
final int diff;
|
||||||
|
if (dir < 0) {
|
||||||
|
// rtl
|
||||||
|
diff = x - (layout.getWidth() - (width * level));
|
||||||
|
} else {
|
||||||
|
diff = (width * level) - x;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int left = x + (dir * marginLeft);
|
||||||
|
final int right = left + (dir * side);
|
||||||
|
l = Math.min(left, right) + (dir * diff);
|
||||||
|
r = Math.max(left, right) + (dir * diff);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (dir > 0) {
|
||||||
|
l = x + marginLeft;
|
||||||
|
} else {
|
||||||
|
l = x - width + marginLeft;
|
||||||
|
}
|
||||||
|
r = l + side;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2);
|
final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2);
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity android:name=".inlineparser.InlineParserActivity" />
|
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||||
|
<activity android:name=".htmldetails.HtmlDetailsActivity" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
|||||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||||
import io.noties.markwon.sample.editor.EditorActivity;
|
import io.noties.markwon.sample.editor.EditorActivity;
|
||||||
import io.noties.markwon.sample.html.HtmlActivity;
|
import io.noties.markwon.sample.html.HtmlActivity;
|
||||||
|
import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity;
|
||||||
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
||||||
import io.noties.markwon.sample.latex.LatexActivity;
|
import io.noties.markwon.sample.latex.LatexActivity;
|
||||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||||
@ -127,6 +128,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = InlineParserActivity.class;
|
activity = InlineParserActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case HTML_DETAILS:
|
||||||
|
activity = HtmlDetailsActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,9 @@ public enum Sample {
|
|||||||
|
|
||||||
EDITOR(R.string.sample_editor),
|
EDITOR(R.string.sample_editor),
|
||||||
|
|
||||||
INLINE_PARSER(R.string.sample_inline_parser);
|
INLINE_PARSER(R.string.sample_inline_parser),
|
||||||
|
|
||||||
|
HTML_DETAILS(R.string.sample_html_details);
|
||||||
|
|
||||||
private final int textResId;
|
private final int textResId;
|
||||||
|
|
||||||
|
@ -0,0 +1,410 @@
|
|||||||
|
package io.noties.markwon.sample.htmldetails;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
|
import io.noties.markwon.html.HtmlTag;
|
||||||
|
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
|
import io.noties.markwon.html.TagHandler;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.sample.R;
|
||||||
|
import io.noties.markwon.utils.LeadingMarginUtils;
|
||||||
|
import io.noties.markwon.utils.NoCopySpannableFactory;
|
||||||
|
|
||||||
|
public class HtmlDetailsActivity extends Activity {
|
||||||
|
|
||||||
|
private ViewGroup content;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_html_details);
|
||||||
|
|
||||||
|
content = findViewById(R.id.content);
|
||||||
|
|
||||||
|
sample_details();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sample_details() {
|
||||||
|
|
||||||
|
final String md = "# Hello\n\n<details>\n" +
|
||||||
|
" <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" +
|
||||||
|
" <p>\n\n" +
|
||||||
|
"<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" +
|
||||||
|
"## *formatted* **heading** with [a](link)\n" +
|
||||||
|
"```java\n" +
|
||||||
|
"code block\n" +
|
||||||
|
"```\n" +
|
||||||
|
"\n" +
|
||||||
|
" <details>\n" +
|
||||||
|
" <summary><small>nested</small> stuff</summary><p>\n" +
|
||||||
|
"<!-- alternative placement of p shown above -->\n" +
|
||||||
|
"\n" +
|
||||||
|
"* list\n" +
|
||||||
|
"* with\n" +
|
||||||
|
"\n\n" +
|
||||||
|
"\n\n" +
|
||||||
|
" 1. nested\n" +
|
||||||
|
" 1. items\n" +
|
||||||
|
"\n" +
|
||||||
|
" ```java\n" +
|
||||||
|
" // including code\n" +
|
||||||
|
" ```\n" +
|
||||||
|
" 1. blocks\n" +
|
||||||
|
"\n" +
|
||||||
|
"<details><summary>The 3rd!</summary>\n\n" +
|
||||||
|
"**bold** _em_\n</details>" +
|
||||||
|
" </p></details>\n" +
|
||||||
|
"</p></details>\n\n" +
|
||||||
|
"and **this** *is* how...";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(HtmlPlugin.create(plugin ->
|
||||||
|
plugin.addHandler(new DetailsTagHandler())))
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Spanned spanned = markwon.toMarkdown(md);
|
||||||
|
final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class);
|
||||||
|
|
||||||
|
// if we have no details, proceed as usual (single text-view)
|
||||||
|
if (spans == null || spans.length == 0) {
|
||||||
|
// no details
|
||||||
|
final TextView textView = appendTextView();
|
||||||
|
markwon.setParsedMarkdown(textView, spanned);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<DetailsElement> list = new ArrayList<>();
|
||||||
|
|
||||||
|
for (DetailsParsingSpan span : spans) {
|
||||||
|
final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list);
|
||||||
|
if (e != null) {
|
||||||
|
list.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DetailsElement element : list) {
|
||||||
|
initDetails(element, spanned);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(list);
|
||||||
|
|
||||||
|
|
||||||
|
TextView textView;
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
for (DetailsElement element : list) {
|
||||||
|
|
||||||
|
if (element.start != start) {
|
||||||
|
// subSequence and add new TextView
|
||||||
|
textView = appendTextView();
|
||||||
|
textView.setText(subSequenceTrimmed(spanned, start, element.start));
|
||||||
|
}
|
||||||
|
|
||||||
|
// now add details TextView
|
||||||
|
textView = appendTextView();
|
||||||
|
initDetailsTextView(markwon, textView, element);
|
||||||
|
|
||||||
|
start = element.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start != spanned.length()) {
|
||||||
|
// another textView with rest content
|
||||||
|
textView = appendTextView();
|
||||||
|
textView.setText(subSequenceTrimmed(spanned, start, spanned.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private TextView appendTextView() {
|
||||||
|
final View view = getLayoutInflater().inflate(R.layout.view_html_details_text_view, content, false);
|
||||||
|
final TextView textView = view.findViewById(R.id.text);
|
||||||
|
content.addView(view);
|
||||||
|
return textView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDetailsTextView(
|
||||||
|
@NonNull Markwon markwon,
|
||||||
|
@NonNull TextView textView,
|
||||||
|
@NonNull DetailsElement element) {
|
||||||
|
|
||||||
|
// minor optimization
|
||||||
|
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
|
||||||
|
|
||||||
|
// so, each element with children is a details tag
|
||||||
|
// there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans
|
||||||
|
// final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
final SpannableBuilder builder = new SpannableBuilder();
|
||||||
|
append(builder, markwon, textView, element, element);
|
||||||
|
markwon.setParsedMarkdown(textView, builder.spannableStringBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append(
|
||||||
|
@NonNull SpannableBuilder builder,
|
||||||
|
@NonNull Markwon markwon,
|
||||||
|
@NonNull TextView textView,
|
||||||
|
@NonNull DetailsElement root,
|
||||||
|
@NonNull DetailsElement element) {
|
||||||
|
if (!element.children.isEmpty()) {
|
||||||
|
|
||||||
|
final int start = builder.length();
|
||||||
|
|
||||||
|
// builder.append(element.content);
|
||||||
|
builder.append(subSequenceTrimmed(element.content, 0, element.content.length()));
|
||||||
|
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
element.expanded = !element.expanded;
|
||||||
|
|
||||||
|
initDetailsTextView(markwon, textView, root);
|
||||||
|
}
|
||||||
|
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
if (element.expanded) {
|
||||||
|
for (DetailsElement child : element.children) {
|
||||||
|
append(builder, markwon, textView, root, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
builder.append(element.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if null -> remove from where it was processed,
|
||||||
|
// else replace from where it was processed with a new one (can become expandable)
|
||||||
|
@Nullable
|
||||||
|
private static DetailsElement settle(
|
||||||
|
@NonNull DetailsElement element,
|
||||||
|
@NonNull List<? extends DetailsElement> elements) {
|
||||||
|
for (DetailsElement e : elements) {
|
||||||
|
if (element.start > e.start && element.end <= e.end) {
|
||||||
|
final DetailsElement settled = settle(element, e.children);
|
||||||
|
if (settled != null) {
|
||||||
|
|
||||||
|
// the thing is we must balance children if done like this
|
||||||
|
// let's just create a tree actually, so we are easier to modify
|
||||||
|
final Iterator<DetailsElement> iterator = e.children.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element));
|
||||||
|
if (balanced == null) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to our children
|
||||||
|
e.children.add(element);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) {
|
||||||
|
int end = element.end;
|
||||||
|
for (int i = element.children.size() - 1; i >= 0; i--) {
|
||||||
|
final DetailsElement child = element.children.get(i);
|
||||||
|
if (child.end < end) {
|
||||||
|
element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end)));
|
||||||
|
}
|
||||||
|
initDetails(child, spanned);
|
||||||
|
end = child.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int start = (element.start + element.content.length());
|
||||||
|
if (end != start) {
|
||||||
|
element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sort(@NonNull List<DetailsElement> elements) {
|
||||||
|
Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start));
|
||||||
|
for (DetailsElement element : elements) {
|
||||||
|
sort(element.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) {
|
||||||
|
|
||||||
|
while (start < end) {
|
||||||
|
|
||||||
|
final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start));
|
||||||
|
final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1));
|
||||||
|
|
||||||
|
if (!isStartEmpty && !isEndEmpty) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStartEmpty) {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
if (isEndEmpty) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs.subSequence(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsElement {
|
||||||
|
|
||||||
|
final int start;
|
||||||
|
final int end;
|
||||||
|
final CharSequence content;
|
||||||
|
final List<DetailsElement> children = new ArrayList<>(0);
|
||||||
|
|
||||||
|
boolean expanded;
|
||||||
|
|
||||||
|
DetailsElement(int start, int end, @NonNull CharSequence content) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DetailsElement{" +
|
||||||
|
"start=" + start +
|
||||||
|
", end=" + end +
|
||||||
|
", content=" + toStringContent(content) +
|
||||||
|
", children=" + children +
|
||||||
|
", expanded=" + expanded +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String toStringContent(@NonNull CharSequence cs) {
|
||||||
|
return cs.toString().replaceAll("\n", "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsTagHandler extends TagHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(
|
||||||
|
@NonNull MarkwonVisitor visitor,
|
||||||
|
@NonNull MarkwonHtmlRenderer renderer,
|
||||||
|
@NonNull HtmlTag tag) {
|
||||||
|
|
||||||
|
int summaryEnd = -1;
|
||||||
|
|
||||||
|
for (HtmlTag child : tag.getAsBlock().children()) {
|
||||||
|
|
||||||
|
if (!child.isClosed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("summary".equals(child.name())) {
|
||||||
|
summaryEnd = child.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
final TagHandler tagHandler = renderer.tagHandler(child.name());
|
||||||
|
if (tagHandler != null) {
|
||||||
|
tagHandler.handle(visitor, renderer, child);
|
||||||
|
} else if (child.isBlock()) {
|
||||||
|
visitChildren(visitor, renderer, child.getAsBlock());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryEnd > -1) {
|
||||||
|
visitor.builder().setSpan(new DetailsParsingSpan(
|
||||||
|
subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd)
|
||||||
|
), tag.start(), tag.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTags() {
|
||||||
|
return Collections.singleton("details");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsParsingSpan {
|
||||||
|
|
||||||
|
final CharSequence summary;
|
||||||
|
|
||||||
|
DetailsParsingSpan(@NonNull CharSequence summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private final DetailsElement element;
|
||||||
|
private final int blockMargin;
|
||||||
|
private final int blockQuoteWidth;
|
||||||
|
|
||||||
|
private final Rect rect = new Rect();
|
||||||
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) {
|
||||||
|
this.element = element;
|
||||||
|
this.blockMargin = theme.getBlockMargin();
|
||||||
|
this.blockQuoteWidth = theme.getBlockQuoteWidth();
|
||||||
|
this.paint.setStyle(Paint.Style.FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLeadingMargin(boolean first) {
|
||||||
|
return blockMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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) {
|
||||||
|
|
||||||
|
if (LeadingMarginUtils.selfStart(start, text, this)) {
|
||||||
|
rect.set(x, top, x + blockMargin, bottom);
|
||||||
|
if (element.expanded) {
|
||||||
|
paint.setColor(Color.GREEN);
|
||||||
|
} else {
|
||||||
|
paint.setColor(Color.RED);
|
||||||
|
}
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (element.expanded) {
|
||||||
|
final int l = (blockMargin - blockQuoteWidth) / 2;
|
||||||
|
rect.set(x + l, top, x + l + blockQuoteWidth, bottom);
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
paint.setColor(Color.GRAY);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
sample/src/main/res/layout/activity_html_details.xml
Normal file
12
sample/src/main/res/layout/activity_html_details.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</ScrollView>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000" />
|
@ -29,4 +29,6 @@
|
|||||||
|
|
||||||
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||||
|
|
||||||
|
<string name="sample_html_details"># \# HTML <details> tag\n\n<details> tag parsed and rendered</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user