Implemented html inline
This commit is contained in:
		
							parent
							
								
									bf18b87420
								
							
						
					
					
						commit
						07bd7b7cd1
					
				| @ -1,36 +1,26 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| 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.SystemClock; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import com.squareup.picasso.Picasso; | ||||
| import com.squareup.picasso.Target; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.List; | ||||
| import java.util.Scanner; | ||||
| 
 | ||||
| import ru.noties.debug.AndroidLogDebugOutput; | ||||
| 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.CodeSpan; | ||||
| import ru.noties.markwon.spans.AsyncDrawableSpanUtils; | ||||
| 
 | ||||
| public class MainActivity extends Activity { | ||||
| 
 | ||||
| @ -38,7 +28,7 @@ public class MainActivity extends Activity { | ||||
|         Debug.init(new AndroidLogDebugOutput(true)); | ||||
|     } | ||||
| 
 | ||||
|     private List<Target> targets = new ArrayList<>(); | ||||
| //    private List<Target> targets = new ArrayList<>(); | ||||
| 
 | ||||
|     @Override | ||||
|     protected void onCreate(Bundle savedInstanceState) { | ||||
| @ -47,15 +37,15 @@ public class MainActivity extends Activity { | ||||
| 
 | ||||
|         final TextView textView = (TextView) findViewById(R.id.activity_main); | ||||
| 
 | ||||
| 
 | ||||
|         final Picasso picasso = new Picasso.Builder(this) | ||||
|                 .listener(new Picasso.Listener() { | ||||
|                     @Override | ||||
|                     public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) { | ||||
|                         Debug.i(exception, uri); | ||||
|                     } | ||||
|                 }) | ||||
|                 .build(); | ||||
| // | ||||
| //        final Picasso picasso = new Picasso.Builder(this) | ||||
| //                .listener(new Picasso.Listener() { | ||||
| //                    @Override | ||||
| //                    public void onImageLoadFailed(Picasso picasso, Uri uri, Exception exception) { | ||||
| //                        Debug.i(exception, uri); | ||||
| //                    } | ||||
| //                }) | ||||
| //                .build(); | ||||
| 
 | ||||
|         new Thread(new Runnable() { | ||||
|             @Override | ||||
| @ -64,8 +54,8 @@ public class MainActivity extends Activity { | ||||
|                 Scanner scanner = null; | ||||
|                 String md = null; | ||||
|                 try { | ||||
| //                    stream = getAssets().open("scrollable.md"); | ||||
|                     stream = getAssets().open("test.md"); | ||||
|                     stream = getAssets().open("scrollable.md"); | ||||
| //                    stream = getAssets().open("test.md"); | ||||
|                     scanner = new Scanner(stream).useDelimiter("\\A"); | ||||
|                     if (scanner.hasNext()) { | ||||
|                         md = scanner.next(); | ||||
| @ -74,7 +64,10 @@ public class MainActivity extends Activity { | ||||
|                     Debug.e(t); | ||||
|                 } finally { | ||||
|                     if (stream != null) { | ||||
|                         try { stream.close(); } catch (IOException e) {} | ||||
|                         try { | ||||
|                             stream.close(); | ||||
|                         } catch (IOException e) { | ||||
|                         } | ||||
|                     } | ||||
|                     if (scanner != null) { | ||||
|                         scanner.close(); | ||||
| @ -88,76 +81,36 @@ public class MainActivity extends Activity { | ||||
|                             .build(); | ||||
|                     final Node node = parser.parse(md); | ||||
| 
 | ||||
| //                    final SpannableConfiguration configuration = SpannableConfiguration.builder(MainActivity.this) | ||||
| //                            .setAsyncDrawableLoader(new AsyncDrawable.Loader() { | ||||
| //                                @Override | ||||
| //                                public void load(@NonNull String destination, @NonNull final AsyncDrawable drawable) { | ||||
| //                                    Debug.i(destination); | ||||
| //                                    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.builder(MainActivity.this) | ||||
|                             .asyncDrawableLoader(new AsyncDrawable.Loader() { | ||||
|                                 @Override | ||||
|                                 public void load(@NonNull String destination, @NonNull AsyncDrawable drawable) { | ||||
|                                     Debug.i("destination: %s, drawable: %s", destination, drawable); | ||||
|                                 } | ||||
| 
 | ||||
|                     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, | ||||
|                             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(); | ||||
|                     Debug.i("Rendered: %d ms, length: %d", end - start, text.length()); | ||||
| //                    Debug.i(text); | ||||
| 
 | ||||
|                     textView.post(new Runnable() { | ||||
|                         @Override | ||||
|                         public void run() { | ||||
|                             // NB! LinkMovementMethod forces frequent updates... | ||||
|                             textView.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
|                             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(); | ||||
|             } | ||||
|             if (htmlParser == null) { | ||||
|                 htmlParser = SpannableHtmlParser.create(theme); | ||||
|                 htmlParser = SpannableHtmlParser.create(theme, asyncDrawableLoader); | ||||
|             } | ||||
|             return new SpannableConfiguration(this); | ||||
|         } | ||||
|  | ||||
| @ -3,6 +3,7 @@ package ru.noties.markwon.renderer; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.TextUtils; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||
| @ -330,38 +331,48 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|     public void visit(HtmlInline htmlInline) { | ||||
|         final SpannableHtmlParser htmlParser = configuration.htmlParser(); | ||||
|         final SpannableHtmlParser.Tag tag = htmlParser.parseTag(htmlInline.getLiteral()); | ||||
|         Debug.i(tag); | ||||
|         if (tag != null) { | ||||
|             if (tag.opening()) { | ||||
| 
 | ||||
|             final boolean voidTag = tag.voidTag(); | ||||
|             if (!voidTag && tag.opening()) { | ||||
|                 // push in stack | ||||
|                 htmlInlineItems.push(new HtmlInlineItem(tag.name(), builder.length())); | ||||
|                 visitChildren(htmlInline); | ||||
|             } else { | ||||
|                 // pop last item | ||||
|                 if (htmlInlineItems.size() > 0) { | ||||
|                     final HtmlInlineItem item = htmlInlineItems.pop(); | ||||
|                     final int start = item.start; | ||||
|                     final Object span = htmlParser.handleTag(item.tag); | ||||
|                     if (span != null) { | ||||
|                         setSpan(start, span); | ||||
|                     } else { | ||||
|                         final String content = builder.subSequence(start, builder.length()).toString(); | ||||
|                         final String html = String.format(HTML_CONTENT, item.tag, content); | ||||
|                         final Object[] spans = htmlParser.htmlSpans(html); | ||||
|                         final int length = spans != null | ||||
|                                 ? spans.length | ||||
|                                 : 0; | ||||
|                         for (int i = 0; i < length; i++) { | ||||
|                             setSpan(start, spans[i]); | ||||
| 
 | ||||
|                 if (!voidTag) { | ||||
|                     if (htmlInlineItems.size() > 0) { | ||||
|                         final HtmlInlineItem item = htmlInlineItems.pop(); | ||||
|                         final Object span = htmlParser.handleTag(item.tag); | ||||
|                         final int start = item.start; | ||||
|                         if (span != null) { | ||||
|                             setSpan(item.start, span); | ||||
|                         } else { | ||||
|                             final String content = builder.subSequence(start, builder.length()).toString(); | ||||
|                             final String html = String.format(HTML_CONTENT, item.tag, content); | ||||
|                             final Object[] spans = htmlParser.htmlSpans(html); | ||||
|                             final int length = spans != null | ||||
|                                     ? spans.length | ||||
|                                     : 0; | ||||
|                             for (int i = 0; i < length; i++) { | ||||
|                                 setSpan(start, spans[i]); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } else { | ||||
|                     throw new IllegalStateException("Unexpected closing html tag: " + tag.name() | ||||
|                             + ", at position: " + builder.length()); | ||||
|                     final String content = htmlInline.getLiteral(); | ||||
|                     if (!TextUtils.isEmpty(content)) { | ||||
|                         final Spanned html = htmlParser.html(content); | ||||
|                         if (!TextUtils.isEmpty(html)) { | ||||
|                             builder.append(html); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } else { | ||||
|             // let's add what we have | ||||
|             builder.append(htmlInline.getLiteral()); | ||||
|             // todo, should we append just literal? | ||||
| //            builder.append(htmlInline.getLiteral()); | ||||
|             visitChildren(htmlInline); | ||||
|         } | ||||
|     } | ||||
| @ -399,28 +410,10 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
|     private static class HtmlInlineItem { | ||||
|         final String tag; | ||||
|         final int start; | ||||
| 
 | ||||
|         HtmlInlineItem(String tag, int start) { | ||||
|             this.tag = tag; | ||||
|             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.Nullable; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.node.Node; | ||||
| 
 | ||||
| // please note that this class does not implement Renderer in order to return CharSequence (instead of String) | ||||
| public class SpannableRenderer { | ||||
| 
 | ||||
|     public static void scheduleDrawables(@NonNull TextView view) { | ||||
|         DrawablesScheduler.schedule(view); | ||||
|     } | ||||
| 
 | ||||
|     public static void unscheduleDrawables(@NonNull TextView view) { | ||||
|         DrawablesScheduler.unschedule(view); | ||||
|     } | ||||
| 
 | ||||
|     // todo | ||||
|     // * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...) | ||||
|     // * 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.Spanned; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| import java.util.HashMap; | ||||
| import java.util.HashSet; | ||||
| import java.util.Map; | ||||
| import java.util.Set; | ||||
| 
 | ||||
| import ru.noties.markwon.spans.AsyncDrawable; | ||||
| import ru.noties.markwon.spans.SpannableTheme; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| @ -18,8 +22,8 @@ public class SpannableHtmlParser { | ||||
|     // we need to handle images independently (in order to parse alt, width, height, etc) | ||||
| 
 | ||||
|     // creates default parser | ||||
|     public static SpannableHtmlParser create(@NonNull SpannableTheme theme) { | ||||
|         return builderWithDefaults(theme) | ||||
|     public static SpannableHtmlParser create(@NonNull SpannableTheme theme, @NonNull AsyncDrawable.Loader loader) { | ||||
|         return builderWithDefaults(theme, loader) | ||||
|                 .build(); | ||||
|     } | ||||
| 
 | ||||
| @ -27,12 +31,22 @@ public class SpannableHtmlParser { | ||||
|         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 ItalicsProvider italicsProvider = new ItalicsProvider(); | ||||
|         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() | ||||
|                 .customTag("b", boldProvider) | ||||
|                 .customTag("strong", boldProvider) | ||||
| @ -45,7 +59,8 @@ public class SpannableHtmlParser { | ||||
|                 .customTag("u", new UnderlineProvider()) | ||||
|                 .customTag("del", strikeProvider) | ||||
|                 .customTag("s", strikeProvider) | ||||
|                 .customTag("strike", strikeProvider); | ||||
|                 .customTag("strike", strikeProvider) | ||||
|                 .parser(parser); | ||||
|     } | ||||
| 
 | ||||
|     // for simple tags without arguments | ||||
| @ -56,13 +71,16 @@ public class SpannableHtmlParser { | ||||
| 
 | ||||
|     public interface HtmlParser { | ||||
|         Object[] getSpans(@NonNull String html); | ||||
|         Spanned parse(@NonNull String html); | ||||
|     } | ||||
| 
 | ||||
|     private final Map<String, SpanProvider> customTags; | ||||
|     private final Set<String> voidTags; | ||||
|     private final HtmlParser parser; | ||||
| 
 | ||||
|     private SpannableHtmlParser(Builder builder) { | ||||
|         this.customTags = builder.customTags; | ||||
|         this.voidTags = voidTags(); | ||||
|         this.parser = builder.parser; | ||||
|     } | ||||
| 
 | ||||
| @ -79,11 +97,33 @@ public class SpannableHtmlParser { | ||||
|         if (length < 3) { | ||||
|             tag = null; | ||||
|         } 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 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 | ||||
|                     ? html.substring(2, length - 1) | ||||
|                     : html.substring(1, length - 1); | ||||
|             tag = new Tag(name, !closing); | ||||
| 
 | ||||
|             tag = new Tag(name, !closing, voidTag); | ||||
|         } | ||||
| 
 | ||||
|         return tag; | ||||
| @ -107,6 +147,20 @@ public class SpannableHtmlParser { | ||||
|         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 { | ||||
| 
 | ||||
|         private final Map<String, SpanProvider> customTags = new HashMap<>(3); | ||||
| @ -134,10 +188,12 @@ public class SpannableHtmlParser { | ||||
| 
 | ||||
|         private final String name; | ||||
|         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.opening = opening; | ||||
|             this.voidTag = voidTag; | ||||
|         } | ||||
| 
 | ||||
|         public String name() { | ||||
| @ -148,11 +204,16 @@ public class SpannableHtmlParser { | ||||
|             return opening; | ||||
|         } | ||||
| 
 | ||||
|         public boolean voidTag() { | ||||
|             return voidTag; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public String toString() { | ||||
|             return "Tag{" + | ||||
|                     "name='" + name + '\'' + | ||||
|                     ", opening=" + opening + | ||||
|                     ", voidTag=" + voidTag + | ||||
|                     '}'; | ||||
|         } | ||||
|     } | ||||
| @ -197,7 +258,12 @@ public class SpannableHtmlParser { | ||||
| 
 | ||||
|             @Override | ||||
|             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 | ||||
|             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
	 Dimitry Ivanov
						Dimitry Ivanov