Implemented html inline
This commit is contained in:
parent
bf18b87420
commit
07bd7b7cd1
@ -1,36 +1,26 @@
|
|||||||
package ru.noties.markwon;
|
package ru.noties.markwon;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.squareup.picasso.Picasso;
|
|
||||||
import com.squareup.picasso.Target;
|
|
||||||
|
|
||||||
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
import org.commonmark.parser.Parser;
|
import org.commonmark.parser.Parser;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Scanner;
|
import java.util.Scanner;
|
||||||
|
|
||||||
import ru.noties.debug.AndroidLogDebugOutput;
|
import ru.noties.debug.AndroidLogDebugOutput;
|
||||||
import ru.noties.debug.Debug;
|
import ru.noties.debug.Debug;
|
||||||
import ru.noties.markwon.renderer.*;
|
import ru.noties.markwon.renderer.SpannableConfiguration;
|
||||||
|
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||||
import ru.noties.markwon.spans.AsyncDrawable;
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
import ru.noties.markwon.spans.CodeSpan;
|
|
||||||
import ru.noties.markwon.spans.AsyncDrawableSpanUtils;
|
|
||||||
|
|
||||||
public class MainActivity extends Activity {
|
public class MainActivity extends Activity {
|
||||||
|
|
||||||
@ -38,7 +28,7 @@ public class MainActivity extends Activity {
|
|||||||
Debug.init(new AndroidLogDebugOutput(true));
|
Debug.init(new AndroidLogDebugOutput(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Target> targets = new ArrayList<>();
|
// private List<Target> targets = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@ -47,15 +37,15 @@ public class MainActivity extends Activity {
|
|||||||
|
|
||||||
final TextView textView = (TextView) findViewById(R.id.activity_main);
|
final TextView textView = (TextView) findViewById(R.id.activity_main);
|
||||||
|
|
||||||
|
//
|
||||||
final Picasso picasso = new Picasso.Builder(this)
|
// final Picasso picasso = new Picasso.Builder(this)
|
||||||
.listener(new Picasso.Listener() {
|
// .listener(new Picasso.Listener() {
|
||||||
@Override
|
// @Override
|
||||||
public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
|
// public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) {
|
||||||
Debug.i(exception, uri);
|
// Debug.i(exception, uri);
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.build();
|
// .build();
|
||||||
|
|
||||||
new Thread(new Runnable() {
|
new Thread(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@ -64,8 +54,8 @@ public class MainActivity extends Activity {
|
|||||||
Scanner scanner = null;
|
Scanner scanner = null;
|
||||||
String md = null;
|
String md = null;
|
||||||
try {
|
try {
|
||||||
// stream = getAssets().open("scrollable.md");
|
stream = getAssets().open("scrollable.md");
|
||||||
stream = getAssets().open("test.md");
|
// stream = getAssets().open("test.md");
|
||||||
scanner = new Scanner(stream).useDelimiter("\\A");
|
scanner = new Scanner(stream).useDelimiter("\\A");
|
||||||
if (scanner.hasNext()) {
|
if (scanner.hasNext()) {
|
||||||
md = scanner.next();
|
md = scanner.next();
|
||||||
@ -74,7 +64,10 @@ public class MainActivity extends Activity {
|
|||||||
Debug.e(t);
|
Debug.e(t);
|
||||||
} finally {
|
} finally {
|
||||||
if (stream != null) {
|
if (stream != null) {
|
||||||
try { stream.close(); } catch (IOException e) {}
|
try {
|
||||||
|
stream.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (scanner != null) {
|
if (scanner != null) {
|
||||||
scanner.close();
|
scanner.close();
|
||||||
@ -88,76 +81,36 @@ public class MainActivity extends Activity {
|
|||||||
.build();
|
.build();
|
||||||
final Node node = parser.parse(md);
|
final Node node = parser.parse(md);
|
||||||
|
|
||||||
// final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
|
final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this)
|
||||||
// .setAsyncDrawableLoader(new AsyncDrawable.Loader() {
|
.asyncDrawableLoader(new AsyncDrawable.Loader() {
|
||||||
// @Override
|
@Override
|
||||||
// public void load(@NonNull String destination, @NonNull final AsyncDrawable drawable) {
|
public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) {
|
||||||
// Debug.i(destination);
|
Debug.i("destination: %s, drawable: %s", destination, drawable);
|
||||||
// final Target target = new Target() {
|
}
|
||||||
// @Override
|
|
||||||
// public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
|
|
||||||
// Debug.i();
|
|
||||||
// final Drawable d = new BitmapDrawable(getResources(), bitmap);
|
|
||||||
// d.setBounds(0, 0, d.getIntrinsicWidth(), d.getIntrinsicHeight());
|
|
||||||
// drawable.setResult(d);
|
|
||||||
//// textView.setText(textView.getText());
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void onBitmapFailed(Drawable errorDrawable) {
|
|
||||||
// Debug.i();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void onPrepareLoad(Drawable placeHolderDrawable) {
|
|
||||||
// Debug.i();
|
|
||||||
// }
|
|
||||||
// };
|
|
||||||
// targets.add(target);
|
|
||||||
//
|
|
||||||
// picasso.load(destination)
|
|
||||||
// .tag(destination)
|
|
||||||
// .into(target);
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void cancel(@NonNull String destination) {
|
|
||||||
// Debug.i(destination);
|
|
||||||
// picasso
|
|
||||||
// .cancelTag(destination);
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .setCodeConfig(CodeSpan.Config.builder().setTextSize(
|
|
||||||
// (int) (getResources().getDisplayMetrics().density * 14 + .5F)
|
|
||||||
// ).setMultilineMargin((int) (getResources().getDisplayMetrics().density * 8 + .5F)).build())
|
|
||||||
// .build();
|
|
||||||
|
|
||||||
final SpannableConfiguration configuration = SpannableConfiguration.create(MainActivity.this);
|
@Override
|
||||||
|
public void cancel(@NonNull String destination) {
|
||||||
|
Debug.i("destination: %s", destination);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build();
|
||||||
|
|
||||||
final CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render(
|
final CharSequence text = new SpannableRenderer().render(
|
||||||
configuration,
|
configuration,
|
||||||
node
|
node
|
||||||
);
|
);
|
||||||
|
|
||||||
// final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
|
|
||||||
// @Override
|
|
||||||
// public void run() {
|
|
||||||
// textView.setText(textView.getText());
|
|
||||||
// final Drawable drawable = null;
|
|
||||||
// drawable.setCallback(textView);
|
|
||||||
// }
|
|
||||||
// }*/);
|
|
||||||
final long end = SystemClock.uptimeMillis();
|
final long end = SystemClock.uptimeMillis();
|
||||||
Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
|
Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
|
||||||
// Debug.i(text);
|
|
||||||
textView.post(new Runnable() {
|
textView.post(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
// NB! LinkMovementMethod forces frequent updates...
|
// NB! LinkMovementMethod forces frequent updates...
|
||||||
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
textView.setText(text);
|
textView.setText(text);
|
||||||
AsyncDrawableSpanUtils.scheduleDrawables(textView);
|
SpannableRenderer.scheduleDrawables(textView);
|
||||||
|
// AsyncDrawableSpanUtils.scheduleDrawables(textView);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,142 @@
|
|||||||
|
package ru.noties.markwon.renderer;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.DynamicDrawableSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawableSpan;
|
||||||
|
|
||||||
|
abstract class DrawablesScheduler {
|
||||||
|
|
||||||
|
static void schedule(@NonNull final TextView textView) {
|
||||||
|
|
||||||
|
final List<AsyncDrawable> list = extract(textView);
|
||||||
|
if (list.size() > 0) {
|
||||||
|
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onViewAttachedToWindow(View v) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(View v) {
|
||||||
|
// we obtain a new list in case text was changed
|
||||||
|
unschedule(textView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (AsyncDrawable d : list) {
|
||||||
|
d.setCallback2(new DrawableCallbackImpl(textView, d.getBounds()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// must be called when text manually changed in TextView
|
||||||
|
static void unschedule(@NonNull TextView view) {
|
||||||
|
for (AsyncDrawable d : extract(view)) {
|
||||||
|
d.setCallback2(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<AsyncDrawable> extract(@NonNull TextView view) {
|
||||||
|
|
||||||
|
final List<AsyncDrawable> list;
|
||||||
|
|
||||||
|
final CharSequence cs = view.getText();
|
||||||
|
final int length = cs != null
|
||||||
|
? cs.length()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (length == 0 || !(cs instanceof Spanned)) {
|
||||||
|
//noinspection unchecked
|
||||||
|
list = Collections.EMPTY_LIST;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
final Object[] spans = ((Spanned) cs).getSpans(0, length, Object.class);
|
||||||
|
if (spans != null
|
||||||
|
&& spans.length > 0) {
|
||||||
|
|
||||||
|
list = new ArrayList<>(2);
|
||||||
|
|
||||||
|
for (Object span : spans) {
|
||||||
|
if (span instanceof AsyncDrawableSpan) {
|
||||||
|
list.add(((AsyncDrawableSpan) span).getDrawable());
|
||||||
|
} else if (span instanceof DynamicDrawableSpan) {
|
||||||
|
// it's really not optimal thing because it stores Drawable in WeakReference...
|
||||||
|
// which is why it will be most likely already de-referenced...
|
||||||
|
final Drawable d = ((DynamicDrawableSpan) span).getDrawable();
|
||||||
|
if (d != null
|
||||||
|
&& d instanceof AsyncDrawable) {
|
||||||
|
list.add((AsyncDrawable) d);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
//noinspection unchecked
|
||||||
|
list = Collections.EMPTY_LIST;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrawablesScheduler() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DrawableCallbackImpl implements Drawable.Callback {
|
||||||
|
|
||||||
|
private final TextView view;
|
||||||
|
private Rect previousBounds;
|
||||||
|
|
||||||
|
DrawableCallbackImpl(TextView view, Rect initialBounds) {
|
||||||
|
this.view = view;
|
||||||
|
this.previousBounds = new Rect(initialBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateDrawable(@NonNull Drawable who) {
|
||||||
|
|
||||||
|
// okay... teh thing is IF we do not change bounds size, normal invalidate would do
|
||||||
|
// but if the size has changed, then we need to update the whole layout...
|
||||||
|
|
||||||
|
final Rect rect = who.getBounds();
|
||||||
|
|
||||||
|
if (!previousBounds.equals(rect)) {
|
||||||
|
// the only method that seems to work when bounds have changed
|
||||||
|
view.setText(view.getText());
|
||||||
|
previousBounds = new Rect(rect);
|
||||||
|
} else {
|
||||||
|
// if bounds are the same then simple invalidate would do
|
||||||
|
final int scrollX = view.getScrollX();
|
||||||
|
final int scrollY = view.getScrollY();
|
||||||
|
view.postInvalidate(
|
||||||
|
scrollX + rect.left,
|
||||||
|
scrollY + rect.top,
|
||||||
|
scrollX + rect.right,
|
||||||
|
scrollY + rect.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||||
|
final long delay = when - SystemClock.uptimeMillis();
|
||||||
|
view.postDelayed(what, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||||
|
view.removeCallbacks(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -105,7 +105,7 @@ public class SpannableConfiguration {
|
|||||||
linkResolver = new LinkResolverDef();
|
linkResolver = new LinkResolverDef();
|
||||||
}
|
}
|
||||||
if (htmlParser == null) {
|
if (htmlParser == null) {
|
||||||
htmlParser = SpannableHtmlParser.create(theme);
|
htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader);
|
||||||
}
|
}
|
||||||
return new SpannableConfiguration(this);
|
return new SpannableConfiguration(this);
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ package ru.noties.markwon.renderer;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.text.style.StrikethroughSpan;
|
import android.text.style.StrikethroughSpan;
|
||||||
|
|
||||||
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
||||||
@ -330,19 +331,23 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
public void visit(HtmlInline htmlInline) {
|
public void visit(HtmlInline htmlInline) {
|
||||||
final SpannableHtmlParser htmlParser = configuration.htmlParser();
|
final SpannableHtmlParser htmlParser = configuration.htmlParser();
|
||||||
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
|
final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral());
|
||||||
|
Debug.i(tag);
|
||||||
if (tag != null) {
|
if (tag != null) {
|
||||||
if (tag.opening()) {
|
|
||||||
|
final boolean voidTag = tag.voidTag();
|
||||||
|
if (!voidTag && tag.opening()) {
|
||||||
// push in stack
|
// push in stack
|
||||||
htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
|
htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length()));
|
||||||
visitChildren(htmlInline);
|
visitChildren(htmlInline);
|
||||||
} else {
|
} else {
|
||||||
// pop last item
|
|
||||||
|
if (!voidTag) {
|
||||||
if (htmlInlineItems.size() > 0) {
|
if (htmlInlineItems.size() > 0) {
|
||||||
final HtmlInlineItem item = htmlInlineItems.pop();
|
final HtmlInlineItem item = htmlInlineItems.pop();
|
||||||
final int start = item.start;
|
|
||||||
final Object span = htmlParser.handleTag(item.tag);
|
final Object span = htmlParser.handleTag(item.tag);
|
||||||
|
final int start = item.start;
|
||||||
if (span != null) {
|
if (span != null) {
|
||||||
setSpan(start, span);
|
setSpan(item.start, span);
|
||||||
} else {
|
} else {
|
||||||
final String content = builder.subSequence(start, builder.length()).toString();
|
final String content = builder.subSequence(start, builder.length()).toString();
|
||||||
final String html = String.format(HTML_CONTENT, item.tag, content);
|
final String html = String.format(HTML_CONTENT, item.tag, content);
|
||||||
@ -354,14 +359,20 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
setSpan(start, spans[i]);
|
setSpan(start, spans[i]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new IllegalStateException("Unexpected closing html tag: " + tag.name()
|
final String content = htmlInline.getLiteral();
|
||||||
+ ", at position: " + builder.length());
|
if (!TextUtils.isEmpty(content)) {
|
||||||
|
final Spanned html = htmlParser.html(content);
|
||||||
|
if (!TextUtils.isEmpty(html)) {
|
||||||
|
builder.append(html);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// let's add what we have
|
// todo, should we append just literal?
|
||||||
builder.append(htmlInline.getLiteral());
|
// builder.append(htmlInline.getLiteral());
|
||||||
visitChildren(htmlInline);
|
visitChildren(htmlInline);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,28 +410,10 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
|||||||
private static class HtmlInlineItem {
|
private static class HtmlInlineItem {
|
||||||
final String tag;
|
final String tag;
|
||||||
final int start;
|
final int start;
|
||||||
|
|
||||||
HtmlInlineItem(String tag, int start) {
|
HtmlInlineItem(String tag, int start) {
|
||||||
this.tag = tag;
|
this.tag = tag;
|
||||||
this.start = start;
|
this.start = start;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private static String dump(Node node) {
|
|
||||||
// final StringBuilder builder = new StringBuilder();
|
|
||||||
// node.accept(new DumpVisitor(builder));
|
|
||||||
// return builder.toString();
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// private static class DumpVisitor extends AbstractVisitor {
|
|
||||||
// private final StringBuilder builder;
|
|
||||||
//
|
|
||||||
// DumpVisitor(StringBuilder builder) {
|
|
||||||
// this.builder = builder;
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// @Override
|
|
||||||
// public void visit(Text text) {
|
|
||||||
// builder.append(text.getLiteral());
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
@ -3,12 +3,21 @@ package ru.noties.markwon.renderer;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import org.commonmark.node.Node;
|
import org.commonmark.node.Node;
|
||||||
|
|
||||||
// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
|
// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
|
||||||
public class SpannableRenderer {
|
public class SpannableRenderer {
|
||||||
|
|
||||||
|
public static void scheduleDrawables(@NonNull TextView view) {
|
||||||
|
DrawablesScheduler.schedule(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unscheduleDrawables(@NonNull TextView view) {
|
||||||
|
DrawablesScheduler.unschedule(view);
|
||||||
|
}
|
||||||
|
|
||||||
// todo
|
// todo
|
||||||
// * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...)
|
// * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...)
|
||||||
// * Common interface for images (in markdown & inline-html)
|
// * Common interface for images (in markdown & inline-html)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package ru.noties.markwon.renderer.html;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.Html;
|
||||||
|
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
|
|
||||||
|
class HtmlImageGetter implements Html.ImageGetter {
|
||||||
|
|
||||||
|
private final AsyncDrawable.Loader loader;
|
||||||
|
|
||||||
|
HtmlImageGetter(@NonNull AsyncDrawable.Loader loader) {
|
||||||
|
this.loader = loader;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Drawable getDrawable(String source) {
|
||||||
|
return new AsyncDrawable(source, loader);
|
||||||
|
}
|
||||||
|
}
|
@ -7,9 +7,13 @@ import android.support.annotation.Nullable;
|
|||||||
import android.text.Html;
|
import android.text.Html;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import ru.noties.markwon.spans.AsyncDrawable;
|
||||||
import ru.noties.markwon.spans.SpannableTheme;
|
import ru.noties.markwon.spans.SpannableTheme;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
@ -18,8 +22,8 @@ public class SpannableHtmlParser {
|
|||||||
// we need to handle images independently (in order to parse alt, width, height, etc)
|
// we need to handle images independently (in order to parse alt, width, height, etc)
|
||||||
|
|
||||||
// creates default parser
|
// creates default parser
|
||||||
public static SpannableHtmlParser create(@NonNull SpannableTheme theme) {
|
public static SpannableHtmlParser create(@NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader) {
|
||||||
return builderWithDefaults(theme)
|
return builderWithDefaults(theme, loader)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,12 +31,22 @@ public class SpannableHtmlParser {
|
|||||||
return new Builder();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builderWithDefaults(@NonNull SpannableTheme theme) {
|
public static Builder builderWithDefaults(
|
||||||
|
@NonNull SpannableTheme theme,
|
||||||
|
@Nullable AsyncDrawable.Loader asyncDrawableLoader
|
||||||
|
) {
|
||||||
|
|
||||||
final BoldProvider boldProvider = new BoldProvider();
|
final BoldProvider boldProvider = new BoldProvider();
|
||||||
final ItalicsProvider italicsProvider = new ItalicsProvider();
|
final ItalicsProvider italicsProvider = new ItalicsProvider();
|
||||||
final StrikeProvider strikeProvider = new StrikeProvider();
|
final StrikeProvider strikeProvider = new StrikeProvider();
|
||||||
|
|
||||||
|
final HtmlParser parser;
|
||||||
|
if (asyncDrawableLoader != null) {
|
||||||
|
parser = DefaultHtmlParser.create(new HtmlImageGetter(asyncDrawableLoader), null);
|
||||||
|
} else {
|
||||||
|
parser = DefaultHtmlParser.create(null, null);
|
||||||
|
}
|
||||||
|
|
||||||
return new Builder()
|
return new Builder()
|
||||||
.customTag("b", boldProvider)
|
.customTag("b", boldProvider)
|
||||||
.customTag("strong", boldProvider)
|
.customTag("strong", boldProvider)
|
||||||
@ -45,7 +59,8 @@ public class SpannableHtmlParser {
|
|||||||
.customTag("u", new UnderlineProvider())
|
.customTag("u", new UnderlineProvider())
|
||||||
.customTag("del", strikeProvider)
|
.customTag("del", strikeProvider)
|
||||||
.customTag("s", strikeProvider)
|
.customTag("s", strikeProvider)
|
||||||
.customTag("strike", strikeProvider);
|
.customTag("strike", strikeProvider)
|
||||||
|
.parser(parser);
|
||||||
}
|
}
|
||||||
|
|
||||||
// for simple tags without arguments
|
// for simple tags without arguments
|
||||||
@ -56,13 +71,16 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
public interface HtmlParser {
|
public interface HtmlParser {
|
||||||
Object[] getSpans(@NonNull String html);
|
Object[] getSpans(@NonNull String html);
|
||||||
|
Spanned parse(@NonNull String html);
|
||||||
}
|
}
|
||||||
|
|
||||||
private final Map<String, SpanProvider> customTags;
|
private final Map<String, SpanProvider> customTags;
|
||||||
|
private final Set<String> voidTags;
|
||||||
private final HtmlParser parser;
|
private final HtmlParser parser;
|
||||||
|
|
||||||
private SpannableHtmlParser(Builder builder) {
|
private SpannableHtmlParser(Builder builder) {
|
||||||
this.customTags = builder.customTags;
|
this.customTags = builder.customTags;
|
||||||
|
this.voidTags = voidTags();
|
||||||
this.parser = builder.parser;
|
this.parser = builder.parser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,11 +97,33 @@ public class SpannableHtmlParser {
|
|||||||
if (length < 3) {
|
if (length < 3) {
|
||||||
tag = null;
|
tag = null;
|
||||||
} else {
|
} else {
|
||||||
|
// okay, we will consider a tag a void one if it's in our void list tag or if it ends with `/>`
|
||||||
final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
|
final boolean closing = '<' == html.charAt(0) && '/' == html.charAt(1);
|
||||||
|
final boolean voidTag;
|
||||||
|
if (closing) {
|
||||||
|
voidTag = false;
|
||||||
|
} else {
|
||||||
|
int firstNonChar = -1;
|
||||||
|
for (int i = 1; i < length; i++) {
|
||||||
|
if (!Character.isLetterOrDigit(html.charAt(i))) {
|
||||||
|
firstNonChar = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (firstNonChar > 1) {
|
||||||
|
final String name = html.substring(1, firstNonChar);
|
||||||
|
voidTag = voidTags.contains(name);
|
||||||
|
} else {
|
||||||
|
voidTag = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// todo, we do not strip to void tag name, so it can be possibly ended with `/`
|
||||||
final String name = closing
|
final String name = closing
|
||||||
? html.substring(2, length - 1)
|
? html.substring(2, length - 1)
|
||||||
: html.substring(1, length - 1);
|
: html.substring(1, length - 1);
|
||||||
tag = new Tag(name, !closing);
|
|
||||||
|
tag = new Tag(name, !closing, voidTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
return tag;
|
return tag;
|
||||||
@ -107,6 +147,20 @@ public class SpannableHtmlParser {
|
|||||||
return parser.getSpans(html);
|
return parser.getSpans(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Spanned html(String html) {
|
||||||
|
return parser.parse(html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Set<String> voidTags() {
|
||||||
|
final String[] tags = {
|
||||||
|
"area", "base", "br", "col", "embed", "hr", "img", "input",
|
||||||
|
"keygen", "link", "meta", "param", "source", "track", "wbr"
|
||||||
|
};
|
||||||
|
final Set<String> set = new HashSet<>(tags.length);
|
||||||
|
Collections.addAll(set, tags);
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
public static class Builder {
|
public static class Builder {
|
||||||
|
|
||||||
private final Map<String, SpanProvider> customTags = new HashMap<>(3);
|
private final Map<String, SpanProvider> customTags = new HashMap<>(3);
|
||||||
@ -134,10 +188,12 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
private final String name;
|
private final String name;
|
||||||
private final boolean opening;
|
private final boolean opening;
|
||||||
|
private final boolean voidTag;
|
||||||
|
|
||||||
public Tag(String name, boolean opening) {
|
public Tag(String name, boolean opening, boolean voidTag) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.opening = opening;
|
this.opening = opening;
|
||||||
|
this.voidTag = voidTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String name() {
|
public String name() {
|
||||||
@ -148,11 +204,16 @@ public class SpannableHtmlParser {
|
|||||||
return opening;
|
return opening;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean voidTag() {
|
||||||
|
return voidTag;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Tag{" +
|
return "Tag{" +
|
||||||
"name='" + name + '\'' +
|
"name='" + name + '\'' +
|
||||||
", opening=" + opening +
|
", opening=" + opening +
|
||||||
|
", voidTag=" + voidTag +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +258,12 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object[] getSpans(@NonNull String html) {
|
public Object[] getSpans(@NonNull String html) {
|
||||||
return getSpans(Html.fromHtml(html, imageGetter, tagHandler));
|
return getSpans(parse(html));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Spanned parse(@NonNull String html) {
|
||||||
|
return Html.fromHtml(html, imageGetter, tagHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +276,12 @@ public class SpannableHtmlParser {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object[] getSpans(@NonNull String html) {
|
public Object[] getSpans(@NonNull String html) {
|
||||||
return getSpans(Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler));
|
return getSpans(parse(html));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Spanned parse(@NonNull String html) {
|
||||||
|
return Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
package ru.noties.markwon.spans;
|
|
||||||
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.text.Spanned;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class AsyncDrawableSpanUtils {
|
|
||||||
|
|
||||||
// todo, add `unschedule` method (to be used when new text is set, so
|
|
||||||
// drawables are removed from callbacks)
|
|
||||||
|
|
||||||
// this method is not completely valid because DynamicDrawableSpan stores
|
|
||||||
// a drawable in weakReference & it could easily be freed, thus we might need
|
|
||||||
// to re-schedule a new one, but we have no means to do it
|
|
||||||
public static void scheduleDrawables(@NonNull final TextView textView) {
|
|
||||||
|
|
||||||
final CharSequence cs = textView.getText();
|
|
||||||
final int length = cs != null
|
|
||||||
? cs.length()
|
|
||||||
: 0;
|
|
||||||
if (length == 0 || !(cs instanceof Spanned)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Object[] spans = ((Spanned) cs).getSpans(0, length, Object.class);
|
|
||||||
if (spans != null
|
|
||||||
&& spans.length > 0) {
|
|
||||||
|
|
||||||
final List<AsyncDrawable> list = new ArrayList<>(2);
|
|
||||||
|
|
||||||
for (Object span: spans) {
|
|
||||||
if (span instanceof AsyncDrawableSpan) {
|
|
||||||
list.add(((AsyncDrawableSpan) span).getDrawable());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (list.size() > 0) {
|
|
||||||
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
|
||||||
@Override
|
|
||||||
public void onViewAttachedToWindow(View v) {
|
|
||||||
// can it happen that the same view first detached & them attached with all previous content? hm..
|
|
||||||
// no op for now
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewDetachedFromWindow(View v) {
|
|
||||||
// remove callbacks...
|
|
||||||
textView.removeOnAttachStateChangeListener(this);
|
|
||||||
for (AsyncDrawable drawable: list) {
|
|
||||||
drawable.setCallback2(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (AsyncDrawable drawable: list) {
|
|
||||||
drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private AsyncDrawableSpanUtils() {}
|
|
||||||
|
|
||||||
private static class DrawableCallbackImpl implements Drawable.Callback {
|
|
||||||
|
|
||||||
private final TextView view;
|
|
||||||
private Rect previousBounds;
|
|
||||||
|
|
||||||
DrawableCallbackImpl(TextView view, Rect initialBounds) {
|
|
||||||
this.view = view;
|
|
||||||
this.previousBounds = new Rect(initialBounds);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void invalidateDrawable(@NonNull Drawable who) {
|
|
||||||
|
|
||||||
// okay... teh thing is IF we do not change bounds size, normal invalidate would do
|
|
||||||
// but if the size has changed, then we need to update the whole layout...
|
|
||||||
|
|
||||||
final Rect rect = who.getBounds();
|
|
||||||
|
|
||||||
if (!previousBounds.equals(rect)) {
|
|
||||||
// the only method that seems to work when bounds have changed
|
|
||||||
view.setText(view.getText());
|
|
||||||
previousBounds = new Rect(rect);
|
|
||||||
} else {
|
|
||||||
// if bounds are the same then simple invalidate would do
|
|
||||||
final int scrollX = view.getScrollX();
|
|
||||||
final int scrollY = view.getScrollY();
|
|
||||||
view.postInvalidate(
|
|
||||||
scrollX + rect.left,
|
|
||||||
scrollY + rect.top,
|
|
||||||
scrollX + rect.right,
|
|
||||||
scrollY + rect.bottom
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
|
||||||
final long delay = when - SystemClock.uptimeMillis();
|
|
||||||
view.postDelayed(what, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
|
||||||
view.removeCallbacks(what);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user