Extracting functionality into library
This commit is contained in:
		
							parent
							
								
									3e9ff80da1
								
							
						
					
					
						commit
						6f5fd08de4
					
				| @ -1,32 +1,20 @@ | ||||
| apply plugin: 'com.android.application' | ||||
| 
 | ||||
| android { | ||||
|     compileSdkVersion 25 | ||||
|     buildToolsVersion "25.0.2" | ||||
| 
 | ||||
|     compileSdkVersion TARGET_SDK | ||||
|     buildToolsVersion BUILD_TOOLS | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         applicationId "ru.noties.markwon" | ||||
|         minSdkVersion 15 | ||||
|         targetSdkVersion 25 | ||||
|         minSdkVersion MIN_SDK | ||||
|         targetSdkVersion TARGET_SDK | ||||
|         versionCode 1 | ||||
|         versionName "1.0" | ||||
|         testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | ||||
|     } | ||||
|     buildTypes { | ||||
|         release { | ||||
|             minifyEnabled false | ||||
|             proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | ||||
|         } | ||||
|         versionName version | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile fileTree(dir: 'libs', include: ['*.jar']) | ||||
|     androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { | ||||
|         exclude group: 'com.android.support', module: 'support-annotations' | ||||
|     }) | ||||
|     compile 'com.android.support:appcompat-v7:25.3.1' | ||||
|     compile 'com.atlassian.commonmark:commonmark:0.9.0' | ||||
|     compile 'com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.9.0' | ||||
|     compile project(':library-renderer') | ||||
|     compile 'ru.noties:debug:3.0.0@jar' | ||||
|     testCompile 'junit:junit:4.12' | ||||
| } | ||||
|  | ||||
| @ -1,20 +1,22 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|           package="ru.noties.markwon"> | ||||
| <manifest | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     package="ru.noties.markwon"> | ||||
| 
 | ||||
|     <application | ||||
|             android:allowBackup="true" | ||||
|             android:icon="@mipmap/ic_launcher" | ||||
|             android:label="@string/app_name" | ||||
|             android:supportsRtl="true" | ||||
|             android:theme="@style/AppTheme"> | ||||
|         android:allowBackup="true" | ||||
|         android:icon="@mipmap/ic_launcher" | ||||
|         android:label="@string/app_name" | ||||
|         android:supportsRtl="true" | ||||
|         android:theme="@style/AppTheme"> | ||||
| 
 | ||||
|         <activity android:name=".MainActivity"> | ||||
|             <intent-filter> | ||||
|                 <action android:name="android.intent.action.MAIN"/> | ||||
| 
 | ||||
|                 <category android:name="android.intent.category.LAUNCHER"/> | ||||
|                 <action android:name="android.intent.action.MAIN" /> | ||||
|                 <category android:name="android.intent.category.LAUNCHER" /> | ||||
|             </intent-filter> | ||||
|         </activity> | ||||
| 
 | ||||
|     </application> | ||||
| 
 | ||||
| </manifest> | ||||
| @ -8,7 +8,13 @@ | ||||
| >> Second Quote | ||||
| >>> Third one, yuhuu! | ||||
| 
 | ||||
| `can a code have **markdown?**` so go it doesn't | ||||
| `can a code have **markdown?**` so good it doesn't | ||||
| 
 | ||||
| <h1>Yo! | ||||
| Omg | ||||
| 
 | ||||
| ddffdg | ||||
| </h1> | ||||
| 
 | ||||
| 
 | ||||
| ## Unordered list | ||||
| @ -17,6 +23,9 @@ | ||||
| * second | ||||
| * * second first | ||||
| * * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o | ||||
| * * * hm, is it actually a thing? | ||||
| * * * * and this?! | ||||
| * * * * * omg | ||||
| * third `and some code` | ||||
| 
 | ||||
| 
 | ||||
| @ -59,5 +68,4 @@ To compare<sub>~~13~~</sub> | ||||
| 
 | ||||
| <font color="#FF0000">RED</font> | ||||
| 
 | ||||
| **PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe# Hello! | ||||
| 
 | ||||
| **PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe# | ||||
| @ -1,26 +1,26 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.app.Activity; | ||||
| import android.os.Bundle; | ||||
| import android.os.SystemClock; | ||||
| import android.support.v7.app.AppCompatActivity; | ||||
| import android.text.method.LinkMovementMethod; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| import ru.noties.debug.AndroidLogDebugOutput; | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.spans.DrawableSpanUtils; | ||||
| 
 | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.Arrays; | ||||
| import java.util.Scanner; | ||||
| 
 | ||||
| public class MainActivity extends AppCompatActivity { | ||||
| import ru.noties.debug.AndroidLogDebugOutput; | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.renderer.*; | ||||
| import ru.noties.markwon.spans.DrawableSpanUtils; | ||||
| 
 | ||||
| public class MainActivity extends Activity { | ||||
| 
 | ||||
|     static { | ||||
|         Debug.init(new AndroidLogDebugOutput(true)); | ||||
| @ -62,21 +62,28 @@ public class MainActivity extends AppCompatActivity { | ||||
|                             .extensions(Arrays.asList(StrikethroughExtension.create())) | ||||
|                             .build(); | ||||
|                     final Node node = parser.parse(md); | ||||
|                     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 CharSequence text = new ru.noties.markwon.renderer.SpannableRenderer().render( | ||||
|                             SpannableConfiguration.create(MainActivity.this), | ||||
|                             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.setMovementMethod(LinkMovementMethod.getInstance()); | ||||
|                             textView.setText(text); | ||||
|                             DrawableSpanUtils.scheduleDrawables(textView); | ||||
|                         } | ||||
|  | ||||
| @ -1,7 +1,7 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| public class Markwon { | ||||
| 
 | ||||
|     // todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it | ||||
|     // view for debugging (to view in preview) x3! | ||||
| } | ||||
| //package ru.noties.markwon; | ||||
| // | ||||
| //public class Markwon { | ||||
| // | ||||
| //    // todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it | ||||
| //    // view for debugging (to view in preview) x3! | ||||
| //} | ||||
|  | ||||
| @ -1,442 +1,444 @@ | ||||
| package ru.noties.markwon; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.ColorFilter; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.os.Handler; | ||||
| import android.os.Looper; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Html; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.AbsoluteSizeSpan; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| import android.text.style.URLSpan; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||
| import org.commonmark.node.AbstractVisitor; | ||||
| import org.commonmark.node.BlockQuote; | ||||
| import org.commonmark.node.BulletList; | ||||
| import org.commonmark.node.Code; | ||||
| import org.commonmark.node.CustomBlock; | ||||
| import org.commonmark.node.CustomNode; | ||||
| import org.commonmark.node.Document; | ||||
| import org.commonmark.node.Emphasis; | ||||
| import org.commonmark.node.FencedCodeBlock; | ||||
| import org.commonmark.node.HardLineBreak; | ||||
| import org.commonmark.node.Heading; | ||||
| import org.commonmark.node.HtmlBlock; | ||||
| import org.commonmark.node.HtmlInline; | ||||
| import org.commonmark.node.Image; | ||||
| import org.commonmark.node.IndentedCodeBlock; | ||||
| import org.commonmark.node.Link; | ||||
| import org.commonmark.node.ListItem; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.node.OrderedList; | ||||
| import org.commonmark.node.Paragraph; | ||||
| import org.commonmark.node.SoftLineBreak; | ||||
| import org.commonmark.node.StrongEmphasis; | ||||
| import org.commonmark.node.Text; | ||||
| import org.commonmark.node.ThematicBreak; | ||||
| import org.commonmark.renderer.Renderer; | ||||
| 
 | ||||
| import java.util.ArrayDeque; | ||||
| import java.util.Arrays; | ||||
| import java.util.Deque; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.spans.BlockQuoteSpan; | ||||
| import ru.noties.markwon.spans.CodeSpan; | ||||
| import ru.noties.markwon.spans.DrawableSpan; | ||||
| import ru.noties.markwon.spans.EmphasisSpan; | ||||
| import ru.noties.markwon.spans.ListItemSpan; | ||||
| import ru.noties.markwon.spans.StrongEmphasisSpan; | ||||
| import ru.noties.markwon.spans.SubSpan; | ||||
| import ru.noties.markwon.spans.SupSpan; | ||||
| import ru.noties.markwon.spans.ThematicBreakSpan; | ||||
| 
 | ||||
| public class SpannableRenderer implements Renderer { | ||||
| 
 | ||||
|     // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc) | ||||
| 
 | ||||
|     @Override | ||||
|     public void render(Node node, Appendable output) { | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String render(Node node) { | ||||
|         // hm.. doesn't make sense to render to string | ||||
|         throw null; | ||||
|     } | ||||
| 
 | ||||
|     public CharSequence _render(Node node) { | ||||
|         final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|         node.accept(new SpannableNodeRenderer(builder)); | ||||
|         return builder; | ||||
|     } | ||||
| 
 | ||||
|     private static class SpannableNodeRenderer extends AbstractVisitor { | ||||
| 
 | ||||
| //        private static final float[] HEADING_SIZES = { | ||||
| //                1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, | ||||
| //        }; | ||||
| 
 | ||||
|         private final SpannableStringBuilder builder; | ||||
| 
 | ||||
|         private int blockQuoteIndent; | ||||
|         private int listLevel; | ||||
| 
 | ||||
|         SpannableNodeRenderer(SpannableStringBuilder builder) { | ||||
|             this.builder = builder; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(HardLineBreak hardLineBreak) { | ||||
|             Debug.i(hardLineBreak); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Text text) { | ||||
|             Debug.i(text); | ||||
|             builder.append(text.getLiteral()); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(StrongEmphasis strongEmphasis) { | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(strongEmphasis); | ||||
|             builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Emphasis emphasis) { | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(emphasis); | ||||
|             builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(IndentedCodeBlock indentedCodeBlock) { | ||||
|             Debug.i(indentedCodeBlock); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(BlockQuote blockQuote) { | ||||
|             builder.append('\n'); | ||||
|             final int length = builder.length(); | ||||
|             blockQuoteIndent += 1; | ||||
|             visitChildren(blockQuote); | ||||
|             builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             blockQuoteIndent -= 1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Code code) { | ||||
|             final int length = builder.length(); | ||||
|             builder.append(code.getLiteral()); | ||||
| //            builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(BulletList bulletList) { | ||||
|             Debug.i(bulletList, bulletList.getBulletMarker()); | ||||
|             visitChildren(bulletList); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(ListItem listItem) { | ||||
|             Debug.i(listItem); | ||||
| //package ru.noties.markwon; | ||||
| // | ||||
| //import android.graphics.Canvas; | ||||
| //import android.graphics.ColorFilter; | ||||
| //import android.graphics.drawable.Drawable; | ||||
| //import android.os.Handler; | ||||
| //import android.os.Looper; | ||||
| //import android.support.annotation.IntRange; | ||||
| //import android.support.annotation.NonNull; | ||||
| //import android.support.annotation.Nullable; | ||||
| //import android.text.Html; | ||||
| //import android.text.SpannableStringBuilder; | ||||
| //import android.text.Spanned; | ||||
| //import android.text.style.AbsoluteSizeSpan; | ||||
| //import android.text.style.StrikethroughSpan; | ||||
| //import android.text.style.URLSpan; | ||||
| // | ||||
| //import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||
| //import org.commonmark.node.AbstractVisitor; | ||||
| //import org.commonmark.node.BlockQuote; | ||||
| //import org.commonmark.node.BulletList; | ||||
| //import org.commonmark.node.Code; | ||||
| //import org.commonmark.node.CustomBlock; | ||||
| //import org.commonmark.node.CustomNode; | ||||
| //import org.commonmark.node.Document; | ||||
| //import org.commonmark.node.Emphasis; | ||||
| //import org.commonmark.node.FencedCodeBlock; | ||||
| //import org.commonmark.node.HardLineBreak; | ||||
| //import org.commonmark.node.Heading; | ||||
| //import org.commonmark.node.HtmlBlock; | ||||
| //import org.commonmark.node.HtmlInline; | ||||
| //import org.commonmark.node.Image; | ||||
| //import org.commonmark.node.IndentedCodeBlock; | ||||
| //import org.commonmark.node.Link; | ||||
| //import org.commonmark.node.ListItem; | ||||
| //import org.commonmark.node.Node; | ||||
| //import org.commonmark.node.OrderedList; | ||||
| //import org.commonmark.node.Paragraph; | ||||
| //import org.commonmark.node.SoftLineBreak; | ||||
| //import org.commonmark.node.StrongEmphasis; | ||||
| //import org.commonmark.node.Text; | ||||
| //import org.commonmark.node.ThematicBreak; | ||||
| //import org.commonmark.renderer.Renderer; | ||||
| // | ||||
| //import java.util.ArrayDeque; | ||||
| //import java.util.Arrays; | ||||
| //import java.util.Deque; | ||||
| // | ||||
| //import ru.noties.debug.Debug; | ||||
| //import ru.noties.markwon.spans.BlockQuoteSpan; | ||||
| //import ru.noties.markwon.spans.CodeSpan; | ||||
| //import ru.noties.markwon.spans.DrawableSpan; | ||||
| //import ru.noties.markwon.spans.EmphasisSpan; | ||||
| //import ru.noties.markwon.spans.BulletListItemSpan; | ||||
| //import ru.noties.markwon.spans.StrongEmphasisSpan; | ||||
| //import ru.noties.markwon.spans.SubSpan; | ||||
| //import ru.noties.markwon.spans.SupSpan; | ||||
| //import ru.noties.markwon.spans.ThematicBreakSpan; | ||||
| // | ||||
| //public class SpannableRenderer implements Renderer { | ||||
| // | ||||
| //    // todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc) | ||||
| // | ||||
| //    @Override | ||||
| //    public void render(Node node, Appendable output) { | ||||
| // | ||||
| //    } | ||||
| // | ||||
| //    @Override | ||||
| //    public String render(Node node) { | ||||
| //        // hm.. doesn't make sense to render to string | ||||
| //        throw null; | ||||
| //    } | ||||
| // | ||||
| //    public CharSequence _render(Node node) { | ||||
| //        final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
| //        node.accept(new SpannableNodeRenderer(builder)); | ||||
| //        return builder; | ||||
| //    } | ||||
| // | ||||
| //    private static class SpannableNodeRenderer extends AbstractVisitor { | ||||
| // | ||||
| ////        private static final float[] HEADING_SIZES = { | ||||
| ////                1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f, | ||||
| ////        }; | ||||
| // | ||||
| //        private final SpannableStringBuilder builder; | ||||
| // | ||||
| //        private int blockQuoteIndent; | ||||
| //        private int listLevel; | ||||
| // | ||||
| //        SpannableNodeRenderer(SpannableStringBuilder builder) { | ||||
| //            this.builder = builder; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(HardLineBreak hardLineBreak) { | ||||
| //            // todo | ||||
| //            Debug.i(hardLineBreak); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Text text) { | ||||
| //            builder.append(text.getLiteral()); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(StrongEmphasis strongEmphasis) { | ||||
| //            final int length = builder.length(); | ||||
| //            visitChildren(strongEmphasis); | ||||
| //            builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Emphasis emphasis) { | ||||
| //            final int length = builder.length(); | ||||
| //            visitChildren(emphasis); | ||||
| //            builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(IndentedCodeBlock indentedCodeBlock) { | ||||
| //            // todo | ||||
| //            Debug.i(indentedCodeBlock); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(BlockQuote blockQuote) { | ||||
| //            builder.append('\n'); | ||||
|             if (builder.charAt(builder.length() - 1) != '\n') { | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
|             final int length = builder.length(); | ||||
|             blockQuoteIndent += 1; | ||||
|             listLevel += 1; | ||||
|             visitChildren(listItem); | ||||
| //            builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             builder.setSpan(new ListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             blockQuoteIndent -= 1; | ||||
|             listLevel -= 1; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(ThematicBreak thematicBreak) { | ||||
|             final int length = builder.length(); | ||||
|             builder.append('\n') | ||||
|                     .append(' '); // without space it won't render | ||||
|             builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             builder.append('\n'); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(OrderedList orderedList) { | ||||
|             Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber()); | ||||
|             // todo, ordering numbers | ||||
|             super.visit(orderedList); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(SoftLineBreak softLineBreak) { | ||||
|             Debug.i(softLineBreak); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Heading heading) { | ||||
|             Debug.i(heading); | ||||
|             if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') { | ||||
|                 builder.append('\n'); | ||||
|             } | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(heading); | ||||
|             final int max = 120; | ||||
|             final int one = 20; // total is 6 | ||||
|             final int size = max - ((heading.getLevel() - 1) * one); | ||||
|             builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             builder.append('\n'); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(FencedCodeBlock fencedCodeBlock) { | ||||
|             builder.append('\n'); | ||||
|             final int length = builder.length(); | ||||
|             builder.append(fencedCodeBlock.getLiteral()); | ||||
|             builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Paragraph paragraph) { | ||||
|             Debug.i(paragraph); | ||||
|             if (listLevel == 0 | ||||
|                     && blockQuoteIndent == 0) { | ||||
|                 builder.append('\n') | ||||
|                         .append('\n'); | ||||
|             } | ||||
|             visitChildren(paragraph); | ||||
| 
 | ||||
|             if (listLevel == 0 | ||||
|                     && blockQuoteIndent == 0) { | ||||
|                 builder.append('\n') | ||||
|                         .append('\n'); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
| //        private int htmlStart = -1; | ||||
|         private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>(); | ||||
| 
 | ||||
|         private static class HtmlInlineItem { | ||||
| 
 | ||||
|             final int start; | ||||
|             final String tag; | ||||
| 
 | ||||
|             private HtmlInlineItem(int start, String tag) { | ||||
|                 this.start = start; | ||||
|                 this.tag = tag; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(HtmlInline htmlInline) { | ||||
| 
 | ||||
| //            Debug.i(htmlInline, htmlStart); | ||||
| //            Debug.i(htmlInline.getLiteral(), htmlInline.toString()); | ||||
| 
 | ||||
|             // okay, it's seems that we desperately need to understand if it's opening tag or closing | ||||
| 
 | ||||
|             final HtmlTag tag = parseTag(htmlInline.getLiteral()); | ||||
| 
 | ||||
|             Debug.i(htmlInline.getLiteral(), tag); | ||||
| 
 | ||||
|             if (tag != null) { | ||||
|                 Debug.i("tag: %s, closing: %s", tag.tag, tag.closing); | ||||
|                 if (!tag.closing) { | ||||
|                     htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag)); | ||||
|                     visitChildren(htmlInline); | ||||
|                 } else { | ||||
|                     final HtmlInlineItem item = htmlStack.pop(); | ||||
|                     final int start = item.start; | ||||
|                     final int end = builder.length(); | ||||
|                     // here, additionally, we can render some tags ourselves (sup/sub) | ||||
|                     if ("sup".equals(item.tag)) { | ||||
|                          builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|                     } else if("sub".equals(item.tag)) { | ||||
|                         builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|                     } else if("del".equals(item.tag)) { | ||||
|                         // weird, but `Html` class does not return a spannable for `<del>o</del>` | ||||
|                         // seems like a bug | ||||
|                         builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|                     } else { | ||||
|                         final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">"; | ||||
|                         final Spanned spanned = Html.fromHtml(html); | ||||
|                         final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||
| 
 | ||||
|                         Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans)); | ||||
| 
 | ||||
|                         if (spans != null | ||||
|                                 && spans.length > 0) { | ||||
|                             for (Object span: spans) { | ||||
|                                 Debug.i(span); | ||||
|                                 builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|             } else { | ||||
|                 super.visit(htmlInline); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static class HtmlTag { | ||||
|             final String tag; | ||||
|             final boolean closing; | ||||
|             HtmlTag(String tag, boolean closing) { | ||||
|                 this.tag = tag; | ||||
|                 this.closing = closing; | ||||
|             } | ||||
|             @Override | ||||
|             public String toString() { | ||||
|                 return "HtmlTag{" + | ||||
|                         "tag='" + tag + '\'' + | ||||
|                         ", closing=" + closing + | ||||
|                         '}'; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static HtmlTag parseTag(String in) { | ||||
| 
 | ||||
|             final HtmlTag out; | ||||
| 
 | ||||
|             final int length = in != null | ||||
|                     ? in.length() | ||||
|                     : 0; | ||||
| 
 | ||||
|             Debug.i(in, length); | ||||
| 
 | ||||
|             if (length == 0 || length < 3) { | ||||
|                 out = null; | ||||
|             } else { | ||||
| 
 | ||||
|                 final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1); | ||||
|                 final String tag = closing | ||||
|                         ? in.substring(2, in.length() - 1) | ||||
|                         : in.substring(1, in.length() - 1); | ||||
|                 out = new HtmlTag(tag, closing); | ||||
|             } | ||||
| 
 | ||||
|             return out; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(HtmlBlock htmlBlock) { | ||||
|             // interestring thing... what is it also? | ||||
|             Debug.i(htmlBlock); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(CustomBlock customBlock) { | ||||
|             // not supported, what is it anyway? | ||||
|             Debug.i(customBlock); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Document document) { | ||||
|             // the whole document, no need to do anything | ||||
|             Debug.i(document); | ||||
|             super.visit(document); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Link link) { | ||||
|             Debug.i(link); | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(link); | ||||
|             builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(Image image) { | ||||
|             // not supported... maybe for now? | ||||
|             Debug.i(image); | ||||
|             super.visit(image); | ||||
| 
 | ||||
|             final int length = builder.length(); | ||||
|             final TestDrawable drawable = new TestDrawable(); | ||||
|             final DrawableSpan span = new DrawableSpan(drawable); | ||||
|             builder.append("  "); | ||||
|             builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void visit(CustomNode customNode) { | ||||
| 
 | ||||
|             Debug.i(customNode); | ||||
| 
 | ||||
|             if (customNode instanceof Strikethrough) { | ||||
|                 final int length = builder.length(); | ||||
|                 visitChildren(customNode); | ||||
|                 builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|             } else { | ||||
|                 super.visit(customNode); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     private static class TestDrawable extends Drawable { | ||||
| 
 | ||||
|         private final Handler handler = new Handler(Looper.getMainLooper()); | ||||
|         private boolean called; | ||||
| 
 | ||||
|         TestDrawable() { | ||||
|             setBounds(0, 0, 50, 50); | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void draw(@NonNull final Canvas canvas) { | ||||
|             canvas.clipRect(getBounds()); | ||||
|             if (!called) { | ||||
|                 canvas.drawColor(0xFF00ff00); | ||||
|                 handler.removeCallbacksAndMessages(null); | ||||
|                 handler.postDelayed(new Runnable() { | ||||
|                     @Override | ||||
|                     public void run() { | ||||
|                         called = true; | ||||
|                         setBounds(0, 0, 400, 400); | ||||
|                         invalidateSelf(); | ||||
|                     } | ||||
|                 }, 2000L); | ||||
|             } else { | ||||
|                 canvas.drawColor(0xFFff0000); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public void setColorFilter(@Nullable ColorFilter colorFilter) { | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getOpacity() { | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getIntrinsicWidth() { | ||||
|             return called ? 400 : 50; | ||||
|         } | ||||
| 
 | ||||
|         @Override | ||||
|         public int getIntrinsicHeight() { | ||||
|             return called ? 400 : 50; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| //            final int length = builder.length(); | ||||
| //            blockQuoteIndent += 1; | ||||
| //            visitChildren(blockQuote); | ||||
| //            builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            blockQuoteIndent -= 1; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Code code) { | ||||
| //            final int length = builder.length(); | ||||
| //            builder.append(code.getLiteral()); | ||||
| ////            builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(BulletList bulletList) { | ||||
| //            Debug.i(bulletList, bulletList.getBulletMarker()); | ||||
| //            visitChildren(bulletList); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(ListItem listItem) { | ||||
| //            Debug.i(listItem); | ||||
| ////            builder.append('\n'); | ||||
| //            if (builder.charAt(builder.length() - 1) != '\n') { | ||||
| //                builder.append('\n'); | ||||
| //            } | ||||
| //            final int length = builder.length(); | ||||
| //            blockQuoteIndent += 1; | ||||
| //            listLevel += 1; | ||||
| //            visitChildren(listItem); | ||||
| ////            builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            blockQuoteIndent -= 1; | ||||
| //            listLevel -= 1; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(ThematicBreak thematicBreak) { | ||||
| //            final int length = builder.length(); | ||||
| //            builder.append('\n') | ||||
| //                    .append(' '); // without space it won't render | ||||
| //            builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            builder.append('\n'); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(OrderedList orderedList) { | ||||
| //            Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber()); | ||||
| //            // todo, ordering numbers | ||||
| //            super.visit(orderedList); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(SoftLineBreak softLineBreak) { | ||||
| //            Debug.i(softLineBreak); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Heading heading) { | ||||
| //            Debug.i(heading); | ||||
| //            if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') { | ||||
| //                builder.append('\n'); | ||||
| //            } | ||||
| //            final int length = builder.length(); | ||||
| //            visitChildren(heading); | ||||
| //            final int max = 120; | ||||
| //            final int one = 20; // total is 6 | ||||
| //            final int size = max - ((heading.getLevel() - 1) * one); | ||||
| //            builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            builder.append('\n'); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(FencedCodeBlock fencedCodeBlock) { | ||||
| //            builder.append('\n'); | ||||
| //            final int length = builder.length(); | ||||
| //            builder.append(fencedCodeBlock.getLiteral()); | ||||
| //            builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Paragraph paragraph) { | ||||
| //            Debug.i(paragraph); | ||||
| //            if (listLevel == 0 | ||||
| //                    && blockQuoteIndent == 0) { | ||||
| //                builder.append('\n') | ||||
| //                        .append('\n'); | ||||
| //            } | ||||
| //            visitChildren(paragraph); | ||||
| // | ||||
| //            if (listLevel == 0 | ||||
| //                    && blockQuoteIndent == 0) { | ||||
| //                builder.append('\n') | ||||
| //                        .append('\n'); | ||||
| //            } | ||||
| //        } | ||||
| // | ||||
| ////        private int htmlStart = -1; | ||||
| //        private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>(); | ||||
| // | ||||
| //        private static class HtmlInlineItem { | ||||
| // | ||||
| //            final int start; | ||||
| //            final String tag; | ||||
| // | ||||
| //            private HtmlInlineItem(int start, String tag) { | ||||
| //                this.start = start; | ||||
| //                this.tag = tag; | ||||
| //            } | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(HtmlInline htmlInline) { | ||||
| // | ||||
| ////            Debug.i(htmlInline, htmlStart); | ||||
| ////            Debug.i(htmlInline.getLiteral(), htmlInline.toString()); | ||||
| // | ||||
| //            // okay, it's seems that we desperately need to understand if it's opening tag or closing | ||||
| // | ||||
| //            final HtmlTag tag = parseTag(htmlInline.getLiteral()); | ||||
| // | ||||
| //            Debug.i(htmlInline.getLiteral(), tag); | ||||
| // | ||||
| //            if (tag != null) { | ||||
| //                Debug.i("tag: %s, closing: %s", tag.tag, tag.closing); | ||||
| //                if (!tag.closing) { | ||||
| //                    htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag)); | ||||
| //                    visitChildren(htmlInline); | ||||
| //                } else { | ||||
| //                    final HtmlInlineItem item = htmlStack.pop(); | ||||
| //                    final int start = item.start; | ||||
| //                    final int end = builder.length(); | ||||
| //                    // here, additionally, we can render some tags ourselves (sup/sub) | ||||
| //                    if ("sup".equals(item.tag)) { | ||||
| //                         builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //                    } else if("sub".equals(item.tag)) { | ||||
| //                        builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //                    } else if("del".equals(item.tag)) { | ||||
| //                        // weird, but `Html` class does not return a spannable for `<del>o</del>` | ||||
| //                        // seems like a bug | ||||
| //                        builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //                    } else { | ||||
| //                        final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">"; | ||||
| //                        final Spanned spanned = Html.fromHtml(html); | ||||
| //                        final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class); | ||||
| // | ||||
| //                        Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans)); | ||||
| // | ||||
| //                        if (spans != null | ||||
| //                                && spans.length > 0) { | ||||
| //                            for (Object span: spans) { | ||||
| //                                Debug.i(span); | ||||
| //                                builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //                            } | ||||
| //                        } | ||||
| //                    } | ||||
| //                } | ||||
| //            } else { | ||||
| //                super.visit(htmlInline); | ||||
| //            } | ||||
| //        } | ||||
| // | ||||
| //        private static class HtmlTag { | ||||
| //            final String tag; | ||||
| //            final boolean closing; | ||||
| //            HtmlTag(String tag, boolean closing) { | ||||
| //                this.tag = tag; | ||||
| //                this.closing = closing; | ||||
| //            } | ||||
| //            @Override | ||||
| //            public String toString() { | ||||
| //                return "HtmlTag{" + | ||||
| //                        "tag='" + tag + '\'' + | ||||
| //                        ", closing=" + closing + | ||||
| //                        '}'; | ||||
| //            } | ||||
| //        } | ||||
| // | ||||
| //        private static HtmlTag parseTag(String in) { | ||||
| // | ||||
| //            final HtmlTag out; | ||||
| // | ||||
| //            final int length = in != null | ||||
| //                    ? in.length() | ||||
| //                    : 0; | ||||
| // | ||||
| //            Debug.i(in, length); | ||||
| // | ||||
| //            if (length == 0 || length < 3) { | ||||
| //                out = null; | ||||
| //            } else { | ||||
| // | ||||
| //                final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1); | ||||
| //                final String tag = closing | ||||
| //                        ? in.substring(2, in.length() - 1) | ||||
| //                        : in.substring(1, in.length() - 1); | ||||
| //                out = new HtmlTag(tag, closing); | ||||
| //            } | ||||
| // | ||||
| //            return out; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(HtmlBlock htmlBlock) { | ||||
| //            // interestring thing... what is it also? | ||||
| //            Debug.i(htmlBlock); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(CustomBlock customBlock) { | ||||
| //            // not supported, what is it anyway? | ||||
| //            Debug.i(customBlock); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Document document) { | ||||
| //            // the whole document, no need to do anything | ||||
| //            Debug.i(document); | ||||
| //            super.visit(document); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Link link) { | ||||
| //            Debug.i(link); | ||||
| //            final int length = builder.length(); | ||||
| //            visitChildren(link); | ||||
| //            builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(Image image) { | ||||
| //            // not supported... maybe for now? | ||||
| //            Debug.i(image); | ||||
| //            final int length = builder.length(); | ||||
| //            super.visit(image); | ||||
| // | ||||
| ////            final int length = builder.length(); | ||||
| //            final TestDrawable drawable = new TestDrawable(); | ||||
| //            final DrawableSpan span = new DrawableSpan(drawable); | ||||
| //            builder.append("  "); | ||||
| //            builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void visit(CustomNode customNode) { | ||||
| // | ||||
| //            Debug.i(customNode); | ||||
| // | ||||
| //            if (customNode instanceof Strikethrough) { | ||||
| //                final int length = builder.length(); | ||||
| //                visitChildren(customNode); | ||||
| //                builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| //            } else { | ||||
| //                super.visit(customNode); | ||||
| //            } | ||||
| //        } | ||||
| //    } | ||||
| // | ||||
| // | ||||
| //    private static class TestDrawable extends Drawable { | ||||
| // | ||||
| //        private final Handler handler = new Handler(Looper.getMainLooper()); | ||||
| //        private boolean called; | ||||
| // | ||||
| //        TestDrawable() { | ||||
| //            setBounds(0, 0, 50, 50); | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void draw(@NonNull final Canvas canvas) { | ||||
| //            canvas.clipRect(getBounds()); | ||||
| //            if (!called) { | ||||
| //                canvas.drawColor(0xFF00ff00); | ||||
| //                handler.removeCallbacksAndMessages(null); | ||||
| //                handler.postDelayed(new Runnable() { | ||||
| //                    @Override | ||||
| //                    public void run() { | ||||
| //                        called = true; | ||||
| //                        setBounds(0, 0, 400, 400); | ||||
| //                        invalidateSelf(); | ||||
| //                    } | ||||
| //                }, 2000L); | ||||
| //            } else { | ||||
| //                canvas.drawColor(0xFFff0000); | ||||
| //            } | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void setAlpha(@IntRange(from = 0, to = 255) int alpha) { | ||||
| // | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public void setColorFilter(@Nullable ColorFilter colorFilter) { | ||||
| // | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public int getOpacity() { | ||||
| //            return 0; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public int getIntrinsicWidth() { | ||||
| //            return called ? 400 : 50; | ||||
| //        } | ||||
| // | ||||
| //        @Override | ||||
| //        public int getIntrinsicHeight() { | ||||
| //            return called ? 400 : 50; | ||||
| //        } | ||||
| //    } | ||||
| //} | ||||
|  | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| @ -6,12 +6,6 @@ import android.graphics.Rect; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Layout; | ||||
| import android.text.StaticLayout; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| import android.text.style.LineHeightSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| import android.text.style.ReplacementSpan; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| @ -24,10 +18,16 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ { | ||||
|     private final int start; | ||||
|     private final int end; | ||||
| 
 | ||||
|     private final Rect rect = new Rect(); | ||||
|     private final Rect borderRect = new Rect(); | ||||
|     private final Paint paint = new Paint(); | ||||
| 
 | ||||
|     public CodeSpan(boolean multiline, int start, int end) { | ||||
|         this.multiline = multiline; | ||||
|         this.start = start; | ||||
|         this.end = end; | ||||
| 
 | ||||
|         paint.setStyle(Paint.Style.FILL); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
| @ -74,33 +74,104 @@ public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ { | ||||
|             @NonNull Paint paint | ||||
|     ) { | ||||
| 
 | ||||
|         Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom); | ||||
| 
 | ||||
|         final CharSequence cs = text.subSequence(start, end); | ||||
| 
 | ||||
|         final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F); | ||||
| 
 | ||||
|         final int left = (int) (x + .5F); | ||||
|         final int right = multiline | ||||
|                 ? canvas.getWidth() | ||||
|                 : left + width; | ||||
| 
 | ||||
|         final Rect rect = new Rect( | ||||
|                 left, | ||||
|                 top, | ||||
|                 right, | ||||
|                 bottom | ||||
|         ); | ||||
|         final int right; | ||||
|         if (multiline) { | ||||
|             right = canvas.getWidth(); | ||||
|         } else { | ||||
|             final int width = (16 * 2) + (int) (paint.measureText(text, start, end) + .5F); | ||||
|             right = left + width; | ||||
|         } | ||||
| 
 | ||||
|         final Paint p = new Paint(); | ||||
|         p.setStyle(Paint.Style.FILL); | ||||
|         p.setColor(0x80ff0000); | ||||
|         canvas.drawRect(rect, p); | ||||
|         rect.set(left, top, right, bottom); | ||||
| 
 | ||||
| 
 | ||||
|         // okay, draw background first | ||||
|         drawBackground(canvas); | ||||
| 
 | ||||
|         // then, if any, draw borders | ||||
|         drawBorders(canvas, this.start == start, this.end == end); | ||||
| 
 | ||||
|         // draw text | ||||
|         // y center position | ||||
|         final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2); | ||||
|         p.setColor(0xFF000000); | ||||
|         canvas.drawText(cs, 0, cs.length(), x + 16, b, paint); | ||||
| //        if (config.textColor != 0) { | ||||
| //            // we will use Paint object that is used to draw all the text (textSize, textColor, typeface, etc) | ||||
| //            paint.setColor(config.textColor); | ||||
| //        } | ||||
|         canvas.drawText(text, start, end, x + 16, b, paint); | ||||
| 
 | ||||
| 
 | ||||
| //        Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom); | ||||
| // | ||||
| //        final CharSequence cs = text.subSequence(start, end); | ||||
| // | ||||
| //        final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F); | ||||
| // | ||||
| //        final int left = (int) (x + .5F); | ||||
| //        final int right = multiline | ||||
| //                ? canvas.getWidth() | ||||
| //                : left + width; | ||||
| // | ||||
| //        final Rect rect = new Rect( | ||||
| //                left, | ||||
| //                top, | ||||
| //                right, | ||||
| //                bottom | ||||
| //        ); | ||||
| // | ||||
| //        final Paint p = new Paint(); | ||||
| //        p.setStyle(Paint.Style.FILL); | ||||
| //        p.setColor(0x80ff0000); | ||||
| //        canvas.drawRect(rect, p); | ||||
| // | ||||
| //        // y center position | ||||
| //        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2); | ||||
| //        p.setColor(0xFF000000); | ||||
| //        canvas.drawText(cs, 0, cs.length(), x + 16, b, paint); | ||||
|     } | ||||
| 
 | ||||
|     private void drawBackground(Canvas canvas) { | ||||
| //        final int color = config.backgroundColor; | ||||
| //        if (color != 0) { | ||||
|             paint.setColor(0x40ff0000); | ||||
|             canvas.drawRect(rect, paint); | ||||
| //        } | ||||
|     } | ||||
| 
 | ||||
|     private void drawBorders(Canvas canvas, boolean top, boolean bottom) { | ||||
| 
 | ||||
|         final int color = 0xFFff0000; | ||||
|         final int width = 4; | ||||
| //        if (color == 0 | ||||
| //                || width == 0) { | ||||
| //            return; | ||||
| //        } | ||||
| 
 | ||||
|         paint.setColor(color); | ||||
| 
 | ||||
|         // left and right are always drawn | ||||
| 
 | ||||
|         // LEFT | ||||
|         borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom); | ||||
|         canvas.drawRect(borderRect, paint); | ||||
| 
 | ||||
|         // RIGHT | ||||
|         borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom); | ||||
|         canvas.drawRect(borderRect, paint); | ||||
| 
 | ||||
|         // TOP | ||||
|         if (top) { | ||||
|             borderRect.set(rect.left, rect.top, rect.right, rect.top + width); | ||||
|             canvas.drawRect(borderRect, paint); | ||||
|         } | ||||
| 
 | ||||
|         // BOTTOM | ||||
|         if (bottom) { | ||||
|             borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom); | ||||
|             canvas.drawRect(borderRect, paint); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
							
								
								
									
										97
									
								
								app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								app/src/main/java/ru/noties/markwon/spans2/DrawableSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.drawable.Drawable; | ||||
| import android.support.annotation.IntDef; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.style.ReplacementSpan; | ||||
| 
 | ||||
| public class DrawableSpan extends ReplacementSpan { | ||||
| 
 | ||||
|     @IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER }) | ||||
|     @interface Alignment {} | ||||
| 
 | ||||
|     public static final int ALIGN_BOTTOM = 0; | ||||
|     public static final int ALIGN_BASELINE = 1; | ||||
|     public static final int ALIGN_CENTER = 2; | ||||
| 
 | ||||
|     private final Drawable drawable; | ||||
|     private final int alignment; | ||||
| 
 | ||||
|     public DrawableSpan(@NonNull Drawable drawable) { | ||||
|         this(drawable, ALIGN_BOTTOM); | ||||
|     } | ||||
| 
 | ||||
|     public DrawableSpan(@NonNull Drawable drawable, @Alignment int alignment) { | ||||
|         this.drawable = drawable; | ||||
|         this.alignment = alignment; | ||||
| 
 | ||||
|         // additionally set intrinsic bounds if empty | ||||
|         final Rect rect = drawable.getBounds(); | ||||
|         if (rect.isEmpty()) { | ||||
|             drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getSize( | ||||
|             @NonNull Paint paint, | ||||
|             CharSequence text, | ||||
|             @IntRange(from = 0) int start, | ||||
|             @IntRange(from = 0) int end, | ||||
|             @Nullable Paint.FontMetricsInt fm) { | ||||
| 
 | ||||
|         final Rect rect = drawable.getBounds(); | ||||
| 
 | ||||
|         if (fm != null) { | ||||
|             fm.ascent = -rect.bottom; | ||||
|             fm.descent = 0; | ||||
| 
 | ||||
|             fm.top = fm.ascent; | ||||
|             fm.bottom = 0; | ||||
|         } | ||||
| 
 | ||||
|         return rect.right; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void draw( | ||||
|             @NonNull Canvas canvas, | ||||
|             CharSequence text, | ||||
|             @IntRange(from = 0) int start, | ||||
|             @IntRange(from = 0) int end, | ||||
|             float x, | ||||
|             int top, | ||||
|             int y, | ||||
|             int bottom, | ||||
|             @NonNull Paint paint) { | ||||
| 
 | ||||
|         final Drawable drawable = this.drawable; | ||||
| 
 | ||||
|         final int b = bottom - drawable.getBounds().bottom; | ||||
| 
 | ||||
|         final int save = canvas.save(); | ||||
|         try { | ||||
|             final int translationY; | ||||
|             if (ALIGN_CENTER == alignment) { | ||||
|                 translationY = (int) (b / 2.F + .5F); | ||||
|             } else if (ALIGN_BASELINE == alignment) { | ||||
|                 translationY = b - paint.getFontMetricsInt().descent; | ||||
|             } else { | ||||
|                 translationY = b; | ||||
|             } | ||||
|             canvas.translate(x, translationY); | ||||
|             drawable.draw(canvas); | ||||
|         } finally { | ||||
|             canvas.restoreToCount(save); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public Drawable getDrawable() { | ||||
|         return drawable; | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.drawable.Drawable; | ||||
							
								
								
									
										17
									
								
								app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								app/src/main/java/ru/noties/markwon/spans2/EmphasisSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class EmphasisSpan extends MetricAffectingSpan { | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint p) { | ||||
|         p.setTextSkewX(-0.25f); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint tp) { | ||||
|         tp.setTextSkewX(-0.25f); | ||||
|     } | ||||
| } | ||||
| @ -1,4 +1,4 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| @ -0,0 +1,17 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class StrongEmphasisSpan extends MetricAffectingSpan { | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint p) { | ||||
|         p.setFakeBoldText(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint tp) { | ||||
|         tp.setFakeBoldText(true); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans2/SubSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class SubSpan extends MetricAffectingSpan { | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint tp) { | ||||
|         tp.setTextSize(tp.getTextSize() * .75F); | ||||
|         tp.baselineShift -= (int) (tp.ascent() / 2); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint tp) { | ||||
|         tp.setTextSize(tp.getTextSize() * .75F); | ||||
|         tp.baselineShift -= (int) (tp.ascent() / 2); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/src/main/java/ru/noties/markwon/spans2/SupSpan.java
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class SupSpan extends MetricAffectingSpan { | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint tp) { | ||||
|         tp.setTextSize(tp.getTextSize() * .75F); | ||||
|         tp.baselineShift += (int) (tp.ascent() / 2); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint tp) { | ||||
|         tp.setTextSize(tp.getTextSize() * .75F); | ||||
|         tp.baselineShift += (int) (tp.ascent() / 2); | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| package ru.noties.markwon.spans2; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.text.Layout; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| 
 | ||||
| public class ThematicBreakSpan implements LeadingMarginSpan { | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLeadingMargin(boolean first) { | ||||
|         return 1; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { | ||||
|         final int middle = (bottom - top) / 2; | ||||
|         final Rect rect = new Rect(0, top + middle - 2, c.getWidth(), top + middle + 2); | ||||
|         final Paint paint = new Paint(); | ||||
|         paint.setStyle(Paint.Style.FILL); | ||||
|         paint.setColor(0x80000000); | ||||
|         c.drawRect(rect, paint); | ||||
|     } | ||||
| } | ||||
| @ -1,17 +1,19 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <ScrollView | ||||
|         xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         xmlns:tools="http://schemas.android.com/tools" | ||||
| android:layout_width="match_parent" | ||||
| android:layout_height="match_parent"> | ||||
|     xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|     xmlns:tools="http://schemas.android.com/tools" | ||||
|     android:layout_width="match_parent" | ||||
|     android:layout_height="match_parent"> | ||||
| 
 | ||||
|     <TextView | ||||
|             xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|             android:id="@+id/activity_main" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content" | ||||
|             android:layout_margin="16dip" | ||||
|             tools:context="ru.noties.markwon.MainActivity" | ||||
|             tools:text="yo\nman"/> | ||||
|     <TextView xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:id="@+id/activity_main" | ||||
|         android:layout_width="match_parent" | ||||
|         android:layout_height="wrap_content" | ||||
|         android:layout_margin="16dip" | ||||
|         android:textSize="18sp" | ||||
|         android:lineSpacingExtra="2dip" | ||||
|         android:textColor="@color/colorPrimaryDark" | ||||
|         tools:context="ru.noties.markwon.MainActivity" | ||||
|         tools:text="yo\nman" /> | ||||
| 
 | ||||
| </ScrollView> | ||||
|  | ||||
							
								
								
									
										10
									
								
								app/src/main/res/values-v21/styles.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								app/src/main/res/values-v21/styles.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <resources> | ||||
| 
 | ||||
|     <style name="AppThemeBase" parent="android:Theme.Material.Light"> | ||||
|         <item name="android:colorAccent">@color/colorAccent</item> | ||||
|         <item name="android:colorPrimary">@color/colorPrimary</item> | ||||
|         <item name="android:colorPrimaryDark">@color/colorPrimaryDark</item> | ||||
|     </style> | ||||
| 
 | ||||
| </resources> | ||||
| @ -1,6 +0,0 @@ | ||||
| <resources> | ||||
|     <!-- Example customization of dimensions originally defined in res/values/dimens.xml | ||||
|          (such as screen margins) for screens with more than 820dp of available width. This | ||||
|          would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). --> | ||||
|     <dimen name="activity_horizontal_margin">64dp</dimen> | ||||
| </resources> | ||||
| @ -1,11 +1,8 @@ | ||||
| <resources> | ||||
| 
 | ||||
|     <style name="AppThemeBase" parent="android:Theme.Holo.Light"/> | ||||
| 
 | ||||
|     <!-- Base application theme. --> | ||||
|     <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> | ||||
|         <!-- Customize your theme here. --> | ||||
|         <item name="colorPrimary">@color/colorPrimary</item> | ||||
|         <item name="colorPrimaryDark">@color/colorPrimaryDark</item> | ||||
|         <item name="colorAccent">@color/colorAccent</item> | ||||
|     </style> | ||||
|     <style name="AppTheme" parent="AppThemeBase"/> | ||||
| 
 | ||||
| </resources> | ||||
|  | ||||
| @ -1,14 +1,9 @@ | ||||
| // Top-level build file where you can add configuration options common to all sub-projects/modules. | ||||
| 
 | ||||
| buildscript { | ||||
|     repositories { | ||||
|         jcenter() | ||||
|     } | ||||
|     dependencies { | ||||
|         classpath 'com.android.tools.build:gradle:2.3.1' | ||||
| 
 | ||||
|         // NOTE: Do not place your application dependencies here; they belong | ||||
|         // in the individual module build.gradle files | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @ -33,4 +28,8 @@ ext { | ||||
|     // Dependencies | ||||
|     final def supportVersion = '25.3.1' | ||||
|     SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion" | ||||
| 
 | ||||
|     final def commonMarkVersion = '0.9.0' | ||||
|     COMMON_MARK = "com.atlassian.commonmark:commonmark:$commonMarkVersion" | ||||
|     COMMON_MARK_STRIKETHROUGHT = "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion" | ||||
| } | ||||
|  | ||||
							
								
								
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								gradle/wrapper/gradle-wrapper.properties
									
									
									
									
										vendored
									
									
								
							| @ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME | ||||
| distributionPath=wrapper/dists | ||||
| zipStoreBase=GRADLE_USER_HOME | ||||
| zipStorePath=wrapper/dists | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip | ||||
| distributionUrl=https\://services.gradle.org/distributions/gradle-3.5-all.zip | ||||
|  | ||||
							
								
								
									
										26
									
								
								library-renderer/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								library-renderer/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| apply plugin: 'com.android.library' | ||||
| 
 | ||||
| android { | ||||
| 
 | ||||
|     compileSdkVersion TARGET_SDK | ||||
|     buildToolsVersion BUILD_TOOLS | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         minSdkVersion MIN_SDK | ||||
|         targetSdkVersion TARGET_SDK | ||||
|         versionCode 1 | ||||
|         versionName version | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
| 
 | ||||
|     compile project(':library-spans') | ||||
| 
 | ||||
|     compile SUPPORT_ANNOTATIONS | ||||
|     compile COMMON_MARK | ||||
|     compile COMMON_MARK_STRIKETHROUGHT | ||||
| 
 | ||||
|     // todo, debugging only | ||||
|     compile 'ru.noties:debug:3.0.0@jar' | ||||
| } | ||||
							
								
								
									
										1
									
								
								library-renderer/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								library-renderer/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <manifest package="ru.noties.markwon.renderer" /> | ||||
| @ -0,0 +1,109 @@ | ||||
| package ru.noties.markwon.renderer; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.annotation.NonNull; | ||||
| 
 | ||||
| import ru.noties.markwon.spans.BlockQuoteSpan; | ||||
| import ru.noties.markwon.spans.BulletListItemSpan; | ||||
| import ru.noties.markwon.spans.CodeSpan; | ||||
| import ru.noties.markwon.spans.HeadingSpan; | ||||
| 
 | ||||
| public class SpannableConfiguration { | ||||
| 
 | ||||
|     // creates default configuration | ||||
|     public static SpannableConfiguration create(@NonNull Context context) { | ||||
|         return new Builder(context).build(); | ||||
|     } | ||||
| 
 | ||||
|     public static Builder builder(@NonNull Context context) { | ||||
|         return new Builder(context); | ||||
|     } | ||||
| 
 | ||||
|     private final BlockQuoteSpan.Config blockQuoteConfig; | ||||
|     private final CodeSpan.Config codeConfig; | ||||
|     private final BulletListItemSpan.Config bulletListConfig; | ||||
|     private final HeadingSpan.Config headingConfig; | ||||
| 
 | ||||
|     private SpannableConfiguration(Builder builder) { | ||||
|         this.blockQuoteConfig = builder.blockQuoteConfig; | ||||
|         this.codeConfig = builder.codeConfig; | ||||
|         this.bulletListConfig = builder.bulletListConfig; | ||||
|         this.headingConfig = builder.headingConfig; | ||||
|     } | ||||
| 
 | ||||
|     public BlockQuoteSpan.Config getBlockQuoteConfig() { | ||||
|         return blockQuoteConfig; | ||||
|     } | ||||
| 
 | ||||
|     public CodeSpan.Config getCodeConfig() { | ||||
|         return codeConfig; | ||||
|     } | ||||
| 
 | ||||
|     public BulletListItemSpan.Config getBulletListConfig() { | ||||
|         return bulletListConfig; | ||||
|     } | ||||
| 
 | ||||
|     public HeadingSpan.Config getHeadingConfig() { | ||||
|         return headingConfig; | ||||
|     } | ||||
| 
 | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private final Context context; | ||||
|         private BlockQuoteSpan.Config blockQuoteConfig; | ||||
|         private CodeSpan.Config codeConfig; | ||||
|         private BulletListItemSpan.Config bulletListConfig; | ||||
|         private HeadingSpan.Config headingConfig; | ||||
| 
 | ||||
|         public Builder(Context context) { | ||||
|             this.context = context; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setBlockQuoteConfig(@NonNull BlockQuoteSpan.Config blockQuoteConfig) { | ||||
|             this.blockQuoteConfig = blockQuoteConfig; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setCodeConfig(@NonNull CodeSpan.Config codeConfig) { | ||||
|             this.codeConfig = codeConfig; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setBulletListConfig(@NonNull BulletListItemSpan.Config bulletListConfig) { | ||||
|             this.bulletListConfig = bulletListConfig; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setHeadingConfig(@NonNull HeadingSpan.Config headingConfig) { | ||||
|             this.headingConfig = headingConfig; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         // todo, change to something more reliable | ||||
|         public SpannableConfiguration build() { | ||||
|             if (blockQuoteConfig == null) { | ||||
|                 blockQuoteConfig = new BlockQuoteSpan.Config( | ||||
|                         px(16), | ||||
|                         px(4), | ||||
|                         0xFFcccccc | ||||
|                 ); | ||||
|             } | ||||
|             if (codeConfig == null) { | ||||
|                 codeConfig = CodeSpan.Config.builder() | ||||
|                         .setMultilineMargin(px(8)) | ||||
|                         .build(); | ||||
|             } | ||||
|             if (bulletListConfig == null) { | ||||
|                 bulletListConfig = new BulletListItemSpan.Config(0, px(16), px(1)); | ||||
|             } | ||||
|             if (headingConfig == null) { | ||||
|                 headingConfig = new HeadingSpan.Config(px(1), 0); | ||||
|             } | ||||
|             return new SpannableConfiguration(this); | ||||
|         } | ||||
| 
 | ||||
|         private int px(int dp) { | ||||
|             return (int) (context.getResources().getDisplayMetrics().density * dp + .5F); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,126 @@ | ||||
| package ru.noties.markwon.renderer; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.os.Build; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.Html; | ||||
| import android.text.Spanned; | ||||
| 
 | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class SpannableHtmlParser { | ||||
| 
 | ||||
|     // we need to handle images independently (in order to parse alt, width, height, etc) | ||||
| 
 | ||||
|     // for simple tags without arguments | ||||
|     // <b>, <i>, etc | ||||
|     public interface SpanProvider { | ||||
|         Object provide(); | ||||
|     } | ||||
| 
 | ||||
|     public interface HtmlParser { | ||||
|         Object[] getSpans(@NonNull String html); | ||||
|     } | ||||
| 
 | ||||
|     // creates default parser | ||||
|     public static SpannableHtmlParser create() { | ||||
|         return null; | ||||
|     } | ||||
| 
 | ||||
|     public static Builder builder() { | ||||
|         return new Builder(); | ||||
|     } | ||||
| 
 | ||||
|     private final Map<String, SpanProvider> customTags; | ||||
|     private final HtmlParser parser; | ||||
| 
 | ||||
|     private SpannableHtmlParser(Builder builder) { | ||||
|         this.customTags = builder.customTags; | ||||
|         this.parser = builder.parser; | ||||
|     } | ||||
| 
 | ||||
|     public static class Builder { | ||||
| 
 | ||||
|         private final Map<String, SpanProvider> customTags = new HashMap<>(3); | ||||
|         private HtmlParser parser; | ||||
| 
 | ||||
|         public Builder customTag(@NonNull String tag, @NonNull SpanProvider provider) { | ||||
|             customTags.put(tag, provider); | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public Builder setParser(@NonNull HtmlParser parser) { | ||||
|             this.parser = parser; | ||||
|             return this; | ||||
|         } | ||||
| 
 | ||||
|         public SpannableHtmlParser build() { | ||||
|             if (parser == null) { | ||||
|                 // todo, images.... | ||||
|                 parser = DefaultHtmlParser.create(null, null); | ||||
|             } | ||||
|             return new SpannableHtmlParser(this); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     public static abstract class DefaultHtmlParser implements HtmlParser { | ||||
| 
 | ||||
|         public static DefaultHtmlParser create(@Nullable Html.ImageGetter imageGetter, @Nullable Html.TagHandler tagHandler) { | ||||
|             final DefaultHtmlParser parser; | ||||
|             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { | ||||
|                 parser = new Parser24(imageGetter, tagHandler); | ||||
|             } else { | ||||
|                 parser = new ParserPre24(imageGetter, tagHandler); | ||||
|             } | ||||
|             return parser; | ||||
|         } | ||||
| 
 | ||||
|         final Html.ImageGetter imageGetter; | ||||
|         final Html.TagHandler tagHandler; | ||||
| 
 | ||||
|         DefaultHtmlParser(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) { | ||||
|             this.imageGetter = imageGetter; | ||||
|             this.tagHandler = tagHandler; | ||||
|         } | ||||
| 
 | ||||
|         Object[] getSpans(Spanned spanned) { | ||||
|             final Object[] spans; | ||||
|             final int length = spanned != null ? spanned.length() : 0; | ||||
|             if (length == 0) { | ||||
|                 spans = null; | ||||
|             } else { | ||||
|                 spans = spanned.getSpans(0, length, Object.class); | ||||
|             } | ||||
|             return spans; | ||||
|         } | ||||
| 
 | ||||
|         @SuppressWarnings("deprecation") | ||||
|         private static class ParserPre24 extends DefaultHtmlParser { | ||||
| 
 | ||||
|             ParserPre24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) { | ||||
|                 super(imageGetter, tagHandler); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public Object[] getSpans(@NonNull String html) { | ||||
|                 return getSpans(Html.fromHtml(html, imageGetter, tagHandler)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         @TargetApi(Build.VERSION_CODES.N) | ||||
|         private static class Parser24 extends DefaultHtmlParser { | ||||
| 
 | ||||
|             Parser24(Html.ImageGetter imageGetter, Html.TagHandler tagHandler) { | ||||
|                 super(imageGetter, tagHandler); | ||||
|             } | ||||
| 
 | ||||
|             @Override | ||||
|             public Object[] getSpans(@NonNull String html) { | ||||
|                 return getSpans(Html.fromHtml(html, Html.FROM_HTML_MODE_COMPACT, imageGetter, tagHandler)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,310 @@ | ||||
| package ru.noties.markwon.renderer; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.SpannableStringBuilder; | ||||
| import android.text.Spanned; | ||||
| import android.text.style.StrikethroughSpan; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.Strikethrough; | ||||
| import org.commonmark.node.AbstractVisitor; | ||||
| import org.commonmark.node.BlockQuote; | ||||
| import org.commonmark.node.BulletList; | ||||
| import org.commonmark.node.Code; | ||||
| import org.commonmark.node.CustomNode; | ||||
| import org.commonmark.node.Emphasis; | ||||
| import org.commonmark.node.FencedCodeBlock; | ||||
| import org.commonmark.node.HardLineBreak; | ||||
| import org.commonmark.node.Heading; | ||||
| import org.commonmark.node.HtmlBlock; | ||||
| import org.commonmark.node.Image; | ||||
| import org.commonmark.node.ListBlock; | ||||
| import org.commonmark.node.ListItem; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.node.OrderedList; | ||||
| import org.commonmark.node.Paragraph; | ||||
| import org.commonmark.node.SoftLineBreak; | ||||
| import org.commonmark.node.StrongEmphasis; | ||||
| import org.commonmark.node.Text; | ||||
| import org.commonmark.node.ThematicBreak; | ||||
| 
 | ||||
| import ru.noties.debug.Debug; | ||||
| import ru.noties.markwon.spans.BlockQuoteSpan; | ||||
| import ru.noties.markwon.spans.BulletListItemSpan; | ||||
| import ru.noties.markwon.spans.CodeSpan; | ||||
| import ru.noties.markwon.spans.EmphasisSpan; | ||||
| import ru.noties.markwon.spans.HeadingSpan; | ||||
| import ru.noties.markwon.spans.StrongEmphasisSpan; | ||||
| import ru.noties.markwon.spans.ThematicBreakSpan; | ||||
| 
 | ||||
| public class SpannableMarkdownVisitor extends AbstractVisitor { | ||||
| 
 | ||||
|     // http://spec.commonmark.org/0.18/#html-blocks | ||||
| 
 | ||||
|     private final SpannableConfiguration configuration; | ||||
|     private final SpannableStringBuilder builder; | ||||
| 
 | ||||
|     private int blockQuoteIndent; | ||||
|     private int listLevel; | ||||
| 
 | ||||
|     public SpannableMarkdownVisitor( | ||||
|             @NonNull SpannableConfiguration configuration, | ||||
|             @NonNull SpannableStringBuilder builder | ||||
|     ) { | ||||
|         this.configuration = configuration; | ||||
|         this.builder = builder; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Text text) { | ||||
|         Debug.i(text); | ||||
|         builder.append(text.getLiteral()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(StrongEmphasis strongEmphasis) { | ||||
|         Debug.i(strongEmphasis); | ||||
|         final int length = builder.length(); | ||||
|         visitChildren(strongEmphasis); | ||||
|         setSpan(length, new StrongEmphasisSpan()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Emphasis emphasis) { | ||||
|         Debug.i(emphasis); | ||||
|         final int length = builder.length(); | ||||
|         visitChildren(emphasis); | ||||
|         setSpan(length, new EmphasisSpan()); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(BlockQuote blockQuote) { | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
| 
 | ||||
|         blockQuoteIndent += 1; | ||||
| 
 | ||||
|         visitChildren(blockQuote); | ||||
| 
 | ||||
|         setSpan(length, new BlockQuoteSpan( | ||||
|                 configuration.getBlockQuoteConfig(), | ||||
|                 blockQuoteIndent | ||||
|         )); | ||||
| 
 | ||||
|         blockQuoteIndent -= 1; | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Code code) { | ||||
| 
 | ||||
|         Debug.i(code); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
| 
 | ||||
|         // NB, in order to provide a _padding_ feeling code is wrapped inside two unbreakable spaces | ||||
|         // unfortunately we cannot use this for multiline code as we cannot control there a new line break will be inserted | ||||
|         builder.append('\u00a0'); | ||||
|         builder.append(code.getLiteral()); | ||||
|         builder.append('\u00a0'); | ||||
| 
 | ||||
|         setSpan(length, new CodeSpan( | ||||
|                 configuration.getCodeConfig(), | ||||
|                 false | ||||
|         )); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(FencedCodeBlock fencedCodeBlock) { | ||||
| 
 | ||||
|         Debug.i(fencedCodeBlock); | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
| 
 | ||||
|         builder.append(fencedCodeBlock.getLiteral()); | ||||
|         setSpan(length, new CodeSpan( | ||||
|                 configuration.getCodeConfig(), | ||||
|                 true | ||||
|         )); | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Image image) { | ||||
| 
 | ||||
|         Debug.i(image); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
| 
 | ||||
|         visitChildren(image); | ||||
| 
 | ||||
|         if (length == builder.length()) { | ||||
|             // nothing is added, and we need at least one symbol | ||||
|             builder.append(' '); | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
| ////            final int length = builder.length(); | ||||
| //        final TestDrawable drawable = new TestDrawable(); | ||||
| //        final DrawableSpan span = new DrawableSpan(drawable); | ||||
| //        builder.append("  "); | ||||
| //        builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(BulletList bulletList) { | ||||
|         Debug.i(bulletList); | ||||
|         newLine(); | ||||
|         visitChildren(bulletList); | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ListItem listItem) { | ||||
| 
 | ||||
|         Debug.i(listItem); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
|         blockQuoteIndent += 1; | ||||
|         listLevel += 1; | ||||
|         visitChildren(listItem); | ||||
|         // todo, can be a bullet list & ordered list (with leading numbers... looks like we need to `draw` numbers... | ||||
|         setSpan(length, new BulletListItemSpan( | ||||
|                 configuration.getBulletListConfig(), | ||||
|                 blockQuoteIndent, | ||||
|                 listLevel - 1, | ||||
|                 length | ||||
|         )); | ||||
| //        builder.setSpan(new BulletListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|         blockQuoteIndent -= 1; | ||||
|         listLevel -= 1; | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(ThematicBreak thematicBreak) { | ||||
| 
 | ||||
|         Debug.i(thematicBreak); | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
|         // todo, new lines... | ||||
|         final int length = builder.length(); | ||||
|         builder.append(' '); // without space it won't render | ||||
|         builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(OrderedList orderedList) { | ||||
| 
 | ||||
|         Debug.i(orderedList); | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
| //        Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber()); | ||||
|         // todo, ordering numbers | ||||
|         super.visit(orderedList); | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Heading heading) { | ||||
| 
 | ||||
|         Debug.i(heading); | ||||
| 
 | ||||
|         newLine(); | ||||
| 
 | ||||
|         final int length = builder.length(); | ||||
|         visitChildren(heading); | ||||
|         setSpan(length, new HeadingSpan( | ||||
|                 configuration.getHeadingConfig(), | ||||
|                 heading.getLevel(), | ||||
|                 builder.length()) | ||||
|         ); | ||||
| 
 | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(SoftLineBreak softLineBreak) { | ||||
|         Debug.i(softLineBreak); | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(HardLineBreak hardLineBreak) { | ||||
|         Debug.i(hardLineBreak); | ||||
|         newLine(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(CustomNode customNode) { | ||||
| 
 | ||||
|         Debug.i(customNode); | ||||
| 
 | ||||
|         if (customNode instanceof Strikethrough) { | ||||
|             final int length = builder.length(); | ||||
|             visitChildren(customNode); | ||||
|             setSpan(length, new StrikethroughSpan()); | ||||
|         } else { | ||||
|             super.visit(customNode); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(Paragraph paragraph) { | ||||
| 
 | ||||
|         final boolean inTightList = isInTightList(paragraph); | ||||
| 
 | ||||
|         Debug.i(paragraph, inTightList); | ||||
| 
 | ||||
|         if (!inTightList) { | ||||
|             newLine(); | ||||
|         } | ||||
| 
 | ||||
|         visitChildren(paragraph); | ||||
| 
 | ||||
|         if (!inTightList) { | ||||
|             newLine(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void visit(HtmlBlock htmlBlock) { | ||||
|         // http://spec.commonmark.org/0.18/#html-blocks | ||||
|         Debug.i(htmlBlock, htmlBlock.getLiteral()); | ||||
|         super.visit(htmlBlock); | ||||
|     } | ||||
| 
 | ||||
|     private void setSpan(int start, @NonNull Object span) { | ||||
|         builder.setSpan(span, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); | ||||
|     } | ||||
| 
 | ||||
|     private void newLine() { | ||||
|         if (builder.length() > 0 | ||||
|                 && '\n' != builder.charAt(builder.length() - 1)) { | ||||
|             builder.append('\n'); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private boolean isInTightList(Paragraph paragraph) { | ||||
|         final Node parent = paragraph.getParent(); | ||||
|         if (parent != null) { | ||||
|             final Node gramps = parent.getParent(); | ||||
|             if (gramps != null && gramps instanceof ListBlock) { | ||||
|                 ListBlock list = (ListBlock) gramps; | ||||
|                 return list.isTight(); | ||||
|             } | ||||
|         } | ||||
|         return false; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,28 @@ | ||||
| package ru.noties.markwon.renderer; | ||||
| 
 | ||||
| import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.SpannableStringBuilder; | ||||
| 
 | ||||
| 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 { | ||||
| 
 | ||||
|     // todo | ||||
|     // * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...) | ||||
|     // * Common interface for images (in markdown & inline-html) | ||||
|     // * util method to properly copy markdown (with lists/links, etc) | ||||
| 
 | ||||
|     public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) { | ||||
|         final CharSequence out; | ||||
|         if (node == null) { | ||||
|             out = null; | ||||
|         } else { | ||||
|             final SpannableStringBuilder builder = new SpannableStringBuilder(); | ||||
|             node.accept(new SpannableMarkdownVisitor(configuration, builder)); | ||||
|             out = builder; | ||||
|         } | ||||
|         return out; | ||||
|     } | ||||
| } | ||||
| @ -1,26 +0,0 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.content.Context; | ||||
| import android.support.test.InstrumentationRegistry; | ||||
| import android.support.test.runner.AndroidJUnit4; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| import org.junit.runner.RunWith; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| /** | ||||
|  * Instrumentation test, which will execute on an Android device. | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| @RunWith(AndroidJUnit4.class) | ||||
| public class ExampleInstrumentedTest { | ||||
|     @Test | ||||
|     public void useAppContext() throws Exception { | ||||
|         // Context of the app under test. | ||||
|         Context appContext = InstrumentationRegistry.getTargetContext(); | ||||
| 
 | ||||
|         assertEquals("ru.noties.markwon.spans.test", appContext.getPackageName()); | ||||
|     } | ||||
| } | ||||
| @ -1,10 +1 @@ | ||||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||||
| 
 | ||||
|     package="ru.noties.markwon.spans"> | ||||
| 
 | ||||
|     <application android:allowBackup="true" android:label="@string/app_name" | ||||
|         android:supportsRtl="true"> | ||||
| 
 | ||||
|     </application> | ||||
| 
 | ||||
| </manifest> | ||||
| <manifest package="ru.noties.markwon.spans" /> | ||||
|  | ||||
| @ -0,0 +1,73 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Layout; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| 
 | ||||
| public class BlockQuoteSpan implements LeadingMarginSpan { | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public static class Config { | ||||
| 
 | ||||
|         final int totalWidth; | ||||
|         final int quoteWidth; | ||||
|         final int quoteColor; // by default textColor with 0.1 alpha | ||||
| 
 | ||||
|         public Config( | ||||
|                 @IntRange(from = 0) int totalWidth, | ||||
|                 @IntRange(from = 0) int quoteWidth, | ||||
|                 @ColorInt int quoteColor) { | ||||
|             this.totalWidth = totalWidth; | ||||
|             this.quoteWidth = quoteWidth; | ||||
|             this.quoteColor = quoteColor; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final Config config; | ||||
|     private final Rect rect = new Rect(); | ||||
|     private final Paint paint = new Paint(); | ||||
|     private final int indent; | ||||
| 
 | ||||
|     public BlockQuoteSpan(@NonNull Config config, int indent) { | ||||
|         this.config = config; | ||||
|         this.indent = indent; | ||||
| 
 | ||||
|         paint.setStyle(Paint.Style.FILL); | ||||
|         paint.setColor(config.quoteColor); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLeadingMargin(boolean first) { | ||||
|         return config.totalWidth; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void drawLeadingMargin( | ||||
|             Canvas c, | ||||
|             Paint p, | ||||
|             int x, | ||||
|             int dir, | ||||
|             int top, | ||||
|             int baseline, | ||||
|             int bottom, | ||||
|             CharSequence text, | ||||
|             int start, | ||||
|             int end, | ||||
|             boolean first, | ||||
|             Layout layout) { | ||||
| 
 | ||||
|         final int save = c.save(); | ||||
|         try { | ||||
|             final int left = config.totalWidth * (indent - 1); | ||||
|             rect.set(left, top, left + config.quoteWidth, bottom); | ||||
|             c.drawRect(rect, paint); | ||||
|         } finally { | ||||
|             c.restoreToCount(save); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,135 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.RectF; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Layout; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| 
 | ||||
| public class BulletListItemSpan implements LeadingMarginSpan { | ||||
| 
 | ||||
|     // todo, there are 3 types of bullets: filled circle, stroke circle & filled rectangle | ||||
|     // also, there are ordered lists | ||||
| 
 | ||||
|     public static class Config { | ||||
| 
 | ||||
|         final int bulletColor; // by default uses text color | ||||
|         final int marginWidth; | ||||
|         final int bulletStrokeWidth; | ||||
| 
 | ||||
|         // from 0 but it makes sense to provide something wider | ||||
|         public Config(@ColorInt int bulletColor, @IntRange(from = 0) int marginWidth, int bulletStrokeWidth) { | ||||
|             this.bulletColor = bulletColor; | ||||
|             this.marginWidth = marginWidth; | ||||
|             this.bulletStrokeWidth = bulletStrokeWidth; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final Config config; | ||||
|     private final Paint paint = new Paint(); | ||||
| 
 | ||||
|     private RectF circle; | ||||
|     private Rect rectangle; | ||||
| 
 | ||||
|     private final int blockIndent; | ||||
|     private final int level; | ||||
|     private final int start; | ||||
| 
 | ||||
|     public BulletListItemSpan( | ||||
|             @NonNull Config config, | ||||
|             @IntRange(from = 0) int blockIndent, | ||||
|             @IntRange(from = 0) int level, | ||||
|             @IntRange(from = 0) int start) { | ||||
|         this.config = config; | ||||
|         this.blockIndent = blockIndent; | ||||
|         this.level = level; | ||||
|         this.start = start; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLeadingMargin(boolean first) { | ||||
|         return config.marginWidth; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { | ||||
| 
 | ||||
|         // if there was a line break, we don't need to draw it | ||||
|         if (this.start != start) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         final int color; | ||||
|         final float stroke; | ||||
| 
 | ||||
|         if (config.bulletColor == 0) { | ||||
|             color = p.getColor(); | ||||
|         } else { | ||||
|             color = config.bulletColor; | ||||
|         } | ||||
| 
 | ||||
|         if (config.bulletStrokeWidth == 0) { | ||||
|             stroke = p.getStrokeWidth(); | ||||
|         } else { | ||||
|             stroke = config.bulletStrokeWidth; | ||||
|         } | ||||
| 
 | ||||
|         paint.setColor(color); | ||||
|         paint.setStrokeWidth(stroke); | ||||
| 
 | ||||
|         final int save = c.save(); | ||||
|         try { | ||||
| 
 | ||||
|             // by default we use half of margin width, but if height is less than width, we calculate from it | ||||
|             final int width = config.marginWidth; | ||||
|             final int height = bottom - top; | ||||
| 
 | ||||
|             final int side = Math.min(config.marginWidth, height) / 2; | ||||
| 
 | ||||
|             final int marginLeft = (width - side) / 2; | ||||
|             final int marginTop = (height - side) / 2; | ||||
| 
 | ||||
|             final int l = (width * (blockIndent - 1)) + marginLeft; | ||||
|             final int t = top + marginTop; | ||||
|             final int r = l + side; | ||||
|             final int b = t + side; | ||||
| 
 | ||||
|             if (level == 0 | ||||
|                     || level == 1) { | ||||
| 
 | ||||
|                 // ensure we have circle rectF | ||||
|                 if (circle == null) { | ||||
|                     circle = new RectF(); | ||||
|                 } | ||||
| 
 | ||||
|                 circle.set(l, t, r, b); | ||||
| 
 | ||||
|                 final Paint.Style style = level == 0 | ||||
|                         ? Paint.Style.FILL | ||||
|                         : Paint.Style.STROKE; | ||||
|                 paint.setStyle(style); | ||||
| 
 | ||||
|                 c.drawOval(circle, paint); | ||||
|             } else { | ||||
| 
 | ||||
|                 // ensure rectangle | ||||
|                 if (rectangle == null) { | ||||
|                     rectangle = new Rect(); | ||||
|                 } | ||||
| 
 | ||||
|                 rectangle.set(l, t, r, b); | ||||
| 
 | ||||
|                 paint.setStyle(Paint.Style.FILL); | ||||
| 
 | ||||
|                 c.drawRect(rectangle, paint); | ||||
|             } | ||||
| 
 | ||||
|         } finally { | ||||
|             c.restoreToCount(save); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,307 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.graphics.Typeface; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Layout; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class CodeSpan extends MetricAffectingSpan implements LeadingMarginSpan { | ||||
| 
 | ||||
|     // the thing is.. we cannot use replacementSpan, because it won't let us create multiline code.. | ||||
|     // and we want new lines when we do not fit the width | ||||
|     // plus it complicates the copying | ||||
| 
 | ||||
|     // replacement span is great because we can have additional paddings & can actually get a hold | ||||
|     // of Canvas to draw background, but it implies a lot of manual text handling | ||||
| 
 | ||||
|     // also, we can reuse Rect instance as long as we apply our dimensions in each draw call | ||||
| 
 | ||||
|     @SuppressWarnings("WeakerAccess") | ||||
|     public static class Config { | ||||
| 
 | ||||
|         public static Builder builder() { | ||||
|             return new Builder(); | ||||
|         } | ||||
| 
 | ||||
|         final int textColor; // by default the same as main text | ||||
|         final int backgroundColor; // by default textColor with 0.1 alpha | ||||
|         final int multilineMargin; // by default 0 | ||||
|         final int textSize; // by default the same as main text | ||||
|         final Typeface typeface; // by default Typeface.MONOSPACE | ||||
| 
 | ||||
|         private Config(Builder builder) { | ||||
|             this.textColor = builder.textColor; | ||||
|             this.backgroundColor = builder.backgroundColor; | ||||
|             this.multilineMargin = builder.multilineMargin; | ||||
|             this.textSize = builder.textSize; | ||||
|             this.typeface = builder.typeface; | ||||
|         } | ||||
| 
 | ||||
|         public static class Builder { | ||||
| 
 | ||||
|             int textColor; | ||||
|             int backgroundColor; | ||||
|             int multilineMargin; | ||||
|             int textSize; | ||||
|             Typeface typeface; | ||||
| 
 | ||||
|             public Builder setTextColor(@ColorInt int textColor) { | ||||
|                 this.textColor = textColor; | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             public Builder setBackgroundColor(@ColorInt int backgroundColor) { | ||||
|                 this.backgroundColor = backgroundColor; | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             public Builder setMultilineMargin(int multilineMargin) { | ||||
|                 this.multilineMargin = multilineMargin; | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             public Builder setTextSize(@IntRange(from = 0) int textSize) { | ||||
|                 this.textSize = textSize; | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             public Builder setTypeface(@NonNull Typeface typeface) { | ||||
|                 this.typeface = typeface; | ||||
|                 return this; | ||||
|             } | ||||
| 
 | ||||
|             public Config build() { | ||||
|                 if (typeface == null) { | ||||
|                     typeface = Typeface.MONOSPACE; | ||||
|                 } | ||||
|                 return new Config(this); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final Config config; | ||||
|     private final Rect rect = new Rect(); | ||||
|     private final Paint paint = new Paint(); | ||||
| 
 | ||||
|     private final boolean multiline; | ||||
| 
 | ||||
|     public CodeSpan(@NonNull Config config, boolean multiline) { | ||||
|         this.config = config; | ||||
|         this.multiline = multiline; | ||||
| 
 | ||||
|         paint.setStyle(Paint.Style.FILL); | ||||
|         paint.setTypeface(config.typeface); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint p) { | ||||
|         apply(p); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint ds) { | ||||
|         apply(ds); | ||||
|         if (!multiline) { | ||||
|             final int color; | ||||
|             if (config.backgroundColor == 0) { | ||||
|                 color = applyAlpha(ds.getColor(), 25); | ||||
|             } else { | ||||
|                 color = config.backgroundColor; | ||||
|             } | ||||
|             ds.bgColor = color; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private void apply(TextPaint p) { | ||||
|         p.setTypeface(config.typeface); | ||||
|         if (config.textSize > 0) { | ||||
|             p.setTextSize(config.textSize); | ||||
|         } | ||||
|         if (config.textColor != 0) { | ||||
|             p.setColor(config.textColor); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLeadingMargin(boolean first) { | ||||
|         return multiline ? config.multilineMargin : 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { | ||||
| 
 | ||||
|         if (multiline) { | ||||
| 
 | ||||
|             final int color; | ||||
|             if (config.backgroundColor == 0) { | ||||
|                 color = applyAlpha(p.getColor(), 25); | ||||
|             } else { | ||||
|                 color = config.backgroundColor; | ||||
|             } | ||||
|             paint.setColor(color); | ||||
| 
 | ||||
|             rect.set(x, top, c.getWidth(), bottom); | ||||
| 
 | ||||
|             c.drawRect(rect, paint); | ||||
|         } | ||||
| 
 | ||||
| //        paint.setTextSize(p.getTextSize()); | ||||
| // | ||||
| //        final int left = (int) (x + .5F); | ||||
| // | ||||
| //        final int right; | ||||
| //        if (multiline) { | ||||
| //            right = c.getWidth(); | ||||
| //        } else { | ||||
| //            final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F); | ||||
| //            right = left + width; | ||||
| //        } | ||||
| // | ||||
| //        rect.set(left, top, right, bottom); | ||||
| // | ||||
| //        // okay, draw background first | ||||
| //        drawBackground(c); | ||||
| 
 | ||||
|         // then, if any, draw borders | ||||
| //        drawBorders(c, this.start == start, this.end == end); | ||||
| 
 | ||||
| //        final int color; | ||||
| //        if (config.textColor == 0) { | ||||
| //            color = p.getColor(); | ||||
| //        } else { | ||||
| //            color = config.textColor; | ||||
| //        } | ||||
| //        paint.setColor(color); | ||||
| // | ||||
| //        // draw text | ||||
| //        // y center position | ||||
| //        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2); | ||||
| //        canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint); | ||||
|     } | ||||
| 
 | ||||
|     private static int applyAlpha(int color, int alpha) { | ||||
|         return (color & 0x00FFFFFF) | (alpha << 24); | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| //    @Override | ||||
| //    public int getSize( | ||||
| //            @NonNull Paint p, | ||||
| //            CharSequence text, | ||||
| //            @IntRange(from = 0) int start, | ||||
| //            @IntRange(from = 0) int end, | ||||
| //            @Nullable Paint.FontMetricsInt fm | ||||
| //    ) { | ||||
| // | ||||
| //        paint.setTextSize(p.getTextSize()); | ||||
| // | ||||
| //        final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F); | ||||
| // | ||||
| //        if (fm != null) { | ||||
| //            // we add a padding top & bottom | ||||
| //            final float ratio = .62F; // golden ratio, there is no much point of moving this to config... it seems a bit `specific`... | ||||
| //            fm.ascent = fm.ascent - (config.paddingVertical); | ||||
| //            fm.descent = (int) (-fm.ascent * ratio); | ||||
| //            fm.top = fm.ascent; | ||||
| //            fm.bottom = fm.descent; | ||||
| //        } | ||||
| // | ||||
| //        return width; | ||||
| //    } | ||||
| 
 | ||||
| //    @Override | ||||
| //    public void draw( | ||||
| //            @NonNull Canvas canvas, | ||||
| //            CharSequence text, | ||||
| //            @IntRange(from = 0) int start, | ||||
| //            @IntRange(from = 0) int end, | ||||
| //            float x, | ||||
| //            int top, | ||||
| //            int y, | ||||
| //            int bottom, | ||||
| //            @NonNull Paint p | ||||
| //    ) { | ||||
| // | ||||
| //        paint.setTextSize(p.getTextSize()); | ||||
| // | ||||
| //        final int left = (int) (x + .5F); | ||||
| // | ||||
| //        final int right; | ||||
| //        if (multiline) { | ||||
| //            right = canvas.getWidth(); | ||||
| //        } else { | ||||
| //            final int width = (config.paddingHorizontal * 2) + (int) (paint.measureText(text, start, end) + .5F); | ||||
| //            right = left + width; | ||||
| //        } | ||||
| // | ||||
| //        rect.set(left, top, right, bottom); | ||||
| // | ||||
| //        // okay, draw background first | ||||
| //        drawBackground(canvas); | ||||
| // | ||||
| //        // then, if any, draw borders | ||||
| //        drawBorders(canvas, this.start == start, this.end == end); | ||||
| // | ||||
| //        final int color; | ||||
| //        if (config.textColor == 0) { | ||||
| //            color = p.getColor(); | ||||
| //        } else { | ||||
| //            color = config.textColor; | ||||
| //        } | ||||
| //        paint.setColor(color); | ||||
| // | ||||
| //        // draw text | ||||
| //        // y center position | ||||
| //        final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2); | ||||
| //        canvas.drawText(text, start, end, x + config.paddingHorizontal, b, paint); | ||||
| //    } | ||||
| 
 | ||||
| //    private void drawBackground(Canvas canvas) { | ||||
| //        final int color = config.backgroundColor; | ||||
| //        if (color != 0) { | ||||
| //            paint.setColor(color); | ||||
| //            canvas.drawRect(rect, paint); | ||||
| //        } | ||||
| //    } | ||||
| // | ||||
| //    private void drawBorders(Canvas canvas, boolean top, boolean bottom) { | ||||
| // | ||||
| //        final int color = config.borderColor; | ||||
| //        final int width = config.borderWidth; | ||||
| //        if (color == 0 | ||||
| //                || width == 0) { | ||||
| //            return; | ||||
| //        } | ||||
| // | ||||
| //        paint.setColor(color); | ||||
| // | ||||
| //        // left and right are always drawn | ||||
| // | ||||
| //        // LEFT | ||||
| //        borderRect.set(rect.left, rect.top, rect.left + width, rect.bottom); | ||||
| //        canvas.drawRect(borderRect, paint); | ||||
| // | ||||
| //        // RIGHT | ||||
| //        borderRect.set(rect.right - width, rect.top, rect.right, rect.bottom); | ||||
| //        canvas.drawRect(borderRect, paint); | ||||
| // | ||||
| //        // TOP | ||||
| //        if (top) { | ||||
| //            borderRect.set(rect.left, rect.top, rect.right, rect.top + width); | ||||
| //            canvas.drawRect(borderRect, paint); | ||||
| //        } | ||||
| // | ||||
| //        // BOTTOM | ||||
| //        if (bottom) { | ||||
| //            borderRect.set(rect.left, rect.bottom - width, rect.right, rect.bottom); | ||||
| //            canvas.drawRect(borderRect, paint); | ||||
| //        } | ||||
| //    } | ||||
| } | ||||
| @ -10,6 +10,7 @@ import android.support.annotation.NonNull; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.text.style.ReplacementSpan; | ||||
| 
 | ||||
| @SuppressWarnings("WeakerAccess") | ||||
| public class DrawableSpan extends ReplacementSpan { | ||||
| 
 | ||||
|     @IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER }) | ||||
| @ -0,0 +1,117 @@ | ||||
| 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 DrawableSpanUtils { | ||||
| 
 | ||||
|     // 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<Drawable> list = new ArrayList<>(2); | ||||
| 
 | ||||
|             for (Object span: spans) { | ||||
|                 if (span instanceof DrawableSpan) { | ||||
|                     list.add(((DrawableSpan) 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 (Drawable drawable: list) { | ||||
|                             drawable.setCallback(null); | ||||
|                         } | ||||
|                     } | ||||
|                 }); | ||||
| 
 | ||||
|                 for (Drawable drawable: list) { | ||||
|                     drawable.setCallback(new DrawableCallbackImpl(textView, drawable.getBounds())); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private DrawableSpanUtils() {} | ||||
| 
 | ||||
|     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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,96 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import android.graphics.Canvas; | ||||
| import android.graphics.Paint; | ||||
| import android.graphics.Rect; | ||||
| import android.support.annotation.ColorInt; | ||||
| import android.support.annotation.IntRange; | ||||
| import android.support.annotation.NonNull; | ||||
| import android.text.Layout; | ||||
| import android.text.TextPaint; | ||||
| import android.text.style.LeadingMarginSpan; | ||||
| import android.text.style.MetricAffectingSpan; | ||||
| 
 | ||||
| public class HeadingSpan extends MetricAffectingSpan implements LeadingMarginSpan { | ||||
| 
 | ||||
|     // taken from html spec (most browsers render headings like that) | ||||
|     private static final float[] HEADING_SIZES = { | ||||
|             2.F, 1.5F, 1.17F, 1.F, .83F, .67F, | ||||
|     }; | ||||
| 
 | ||||
|     public static class Config { | ||||
| 
 | ||||
|         final int breakHeight; // by default stroke width | ||||
|         final int breakColor; // by default -> textColor | ||||
| 
 | ||||
|         public Config(@IntRange(from = 0) int breakHeight, @ColorInt int breakColor) { | ||||
|             this.breakHeight = breakHeight; | ||||
|             this.breakColor = breakColor; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private final Config config; | ||||
|     private final Rect rect = new Rect(); | ||||
|     private final Paint paint = new Paint(); | ||||
|     private final int level; | ||||
|     private final int end; | ||||
| 
 | ||||
|     public HeadingSpan(@NonNull Config config, @IntRange(from = 1, to = 6) int level, @IntRange(from = 0) int end) { | ||||
|         this.config = config; | ||||
|         this.level = level - 1; | ||||
|         this.end = end; | ||||
| 
 | ||||
|         paint.setStyle(Paint.Style.FILL); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateMeasureState(TextPaint p) { | ||||
|         apply(p); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void updateDrawState(TextPaint tp) { | ||||
|         apply(tp); | ||||
|     } | ||||
| 
 | ||||
|     private void apply(TextPaint paint) { | ||||
|         paint.setTextSize(paint.getTextSize() * HEADING_SIZES[level]); | ||||
|         paint.setFakeBoldText(true); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public int getLeadingMargin(boolean first) { | ||||
|         // no margin actually, but we need to access Canvas to draw break | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) { | ||||
| 
 | ||||
|         // if we are configured to draw underlines, draw them here | ||||
| 
 | ||||
|         if (level == 0 | ||||
|                 || level == 1) { | ||||
| 
 | ||||
|             if (this.end == end) { | ||||
| 
 | ||||
|                 final int color; | ||||
|                 final int breakHeight; | ||||
|                 if (config.breakColor == 0) { | ||||
|                     color = p.getColor(); | ||||
|                 } else { | ||||
|                     color = config.breakColor; | ||||
|                 } | ||||
|                 if (config.breakHeight == 0) { | ||||
|                     breakHeight = (int) (p.getStrokeWidth() + .5F); | ||||
|                 } else { | ||||
|                     breakHeight = config.breakHeight; | ||||
|                 } | ||||
|                 paint.setColor(color); | ||||
| 
 | ||||
|                 rect.set(x, bottom - breakHeight, c.getWidth(), bottom); | ||||
|                 c.drawRect(rect, paint); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -1,3 +0,0 @@ | ||||
| <resources> | ||||
|     <string name="app_name">Spans</string> | ||||
| </resources> | ||||
| @ -1,17 +0,0 @@ | ||||
| package ru.noties.markwon.spans; | ||||
| 
 | ||||
| import org.junit.Test; | ||||
| 
 | ||||
| import static org.junit.Assert.*; | ||||
| 
 | ||||
| /** | ||||
|  * Example local unit test, which will execute on the development machine (host). | ||||
|  * | ||||
|  * @see <a href="http://d.android.com/tools/testing">Testing documentation</a> | ||||
|  */ | ||||
| public class ExampleUnitTest { | ||||
|     @Test | ||||
|     public void addition_isCorrect() throws Exception { | ||||
|         assertEquals(4, 2 + 2); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										19
									
								
								library-view/build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								library-view/build.gradle
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | ||||
| apply plugin: 'com.android.library' | ||||
| 
 | ||||
| android { | ||||
| 
 | ||||
|     compileSdkVersion TARGET_SDK | ||||
|     buildToolsVersion BUILD_TOOLS | ||||
| 
 | ||||
|     defaultConfig { | ||||
|         minSdkVersion MIN_SDK | ||||
|         targetSdkVersion TARGET_SDK | ||||
|         versionCode 1 | ||||
|         versionName version | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile project(':library-renderer') | ||||
|     compile SUPPORT_ANNOTATIONS | ||||
| } | ||||
							
								
								
									
										1
									
								
								library-view/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								library-view/src/main/AndroidManifest.xml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <manifest package="ru.noties.markwon.view"/> | ||||
| @ -0,0 +1,59 @@ | ||||
| package ru.noties.markwon.view; | ||||
| 
 | ||||
| import android.annotation.TargetApi; | ||||
| import android.content.Context; | ||||
| import android.os.Build; | ||||
| import android.support.annotation.Nullable; | ||||
| import android.util.AttributeSet; | ||||
| import android.widget.TextView; | ||||
| 
 | ||||
| import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension; | ||||
| import org.commonmark.node.Node; | ||||
| import org.commonmark.parser.Parser; | ||||
| 
 | ||||
| import java.util.Collections; | ||||
| 
 | ||||
| import ru.noties.markwon.renderer.SpannableConfiguration; | ||||
| import ru.noties.markwon.renderer.SpannableRenderer; | ||||
| 
 | ||||
| public class MarkdownTextView extends TextView { | ||||
| 
 | ||||
|     private Parser parser; | ||||
|     private SpannableRenderer renderer; | ||||
|     private SpannableConfiguration configuration; | ||||
| 
 | ||||
|     public MarkdownTextView(Context context) { | ||||
|         super(context); | ||||
|     } | ||||
| 
 | ||||
|     public MarkdownTextView(Context context, @Nullable AttributeSet attrs) { | ||||
|         super(context, attrs); | ||||
|     } | ||||
| 
 | ||||
|     public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { | ||||
|         super(context, attrs, defStyleAttr); | ||||
|     } | ||||
| 
 | ||||
|     @TargetApi(Build.VERSION_CODES.LOLLIPOP) | ||||
|     public MarkdownTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { | ||||
|         super(context, attrs, defStyleAttr, defStyleRes); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setText(CharSequence text, BufferType type) { | ||||
|         if (parser == null) { | ||||
|             parser = Parser.builder() | ||||
|                     .extensions(Collections.singletonList(StrikethroughExtension.create())) | ||||
|                     .build(); | ||||
|         } | ||||
|         if (renderer == null) { | ||||
|             renderer = new SpannableRenderer(); | ||||
|         } | ||||
|         if (configuration == null) { | ||||
|             configuration = SpannableConfiguration.create(getContext()); | ||||
|         } | ||||
|         final Node node = parser.parse(text.toString()); | ||||
|         final CharSequence cs = renderer.render(configuration, node); | ||||
|         super.setText(cs, type); | ||||
|     } | ||||
| } | ||||
| @ -1 +1 @@ | ||||
| include ':app', ':library-spans' | ||||
| include ':app', ':library-spans', ':library-renderer', ':library-view' | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov