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,38 +331,48 @@ 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 (htmlInlineItems.size() > 0) { |                 if (!voidTag) { | ||||||
|                     final HtmlInlineItem item = htmlInlineItems.pop(); |                     if (htmlInlineItems.size() > 0) { | ||||||
|                     final int start = item.start; |                         final HtmlInlineItem item = htmlInlineItems.pop(); | ||||||
|                     final Object span = htmlParser.handleTag(item.tag); |                         final Object span = htmlParser.handleTag(item.tag); | ||||||
|                     if (span != null) { |                         final int start = item.start; | ||||||
|                         setSpan(start, span); |                         if (span != null) { | ||||||
|                     } else { |                             setSpan(item.start, span); | ||||||
|                         final String content = builder.subSequence(start, builder.length()).toString(); |                         } else { | ||||||
|                         final String html = String.format(HTML_CONTENT, item.tag, content); |                             final String content = builder.subSequence(start, builder.length()).toString(); | ||||||
|                         final Object[] spans = htmlParser.htmlSpans(html); |                             final String html = String.format(HTML_CONTENT, item.tag, content); | ||||||
|                         final int length = spans != null |                             final Object[] spans = htmlParser.htmlSpans(html); | ||||||
|                                 ? spans.length |                             final int length = spans != null | ||||||
|                                 : 0; |                                     ? spans.length | ||||||
|                         for (int i = 0; i < length; i++) { |                                     : 0; | ||||||
|                             setSpan(start, spans[i]); |                             for (int i = 0; i < length; 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
	 Dimitry Ivanov
						Dimitry Ivanov