commit
032ca76cff
@ -1,5 +1,12 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
# 4.2.1
|
||||||
|
* Fix SpannableBuilder `subSequence` method
|
||||||
|
* Introduce Nougat check in `BulletListItemSpan` to position bullet (for bullets to be
|
||||||
|
positioned correctly when nested inside other `LeadingMarginSpan`s)
|
||||||
|
* Reduced number of invalidations in AsyncDrawable when result is ready
|
||||||
|
* AsyncDrawable#hasKnownDimentions -> AsyncDrawable#hasKnownDimensions typo fix
|
||||||
|
|
||||||
# 4.2.0
|
# 4.2.0
|
||||||
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
* `MarkwonEditor` to highlight markdown input whilst editing (new module: `markwon-editor`)
|
||||||
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
* `CoilImagesPlugin` image loader based on [Coil] library (new module: `markwon-image-coil`) ([#166], [#174])
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
package="io.noties.markwon.app">
|
package="io.noties.markwon.app">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_ALL_DOWNLOADS" />
|
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
|
@ -25,6 +25,7 @@ import io.noties.markwon.ext.tasklist.TaskListPlugin;
|
|||||||
import io.noties.markwon.html.HtmlPlugin;
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
import io.noties.markwon.image.ImagesPlugin;
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
import io.noties.markwon.image.file.FileSchemeHandler;
|
import io.noties.markwon.image.file.FileSchemeHandler;
|
||||||
|
import io.noties.markwon.image.gif.GifMediaDecoder;
|
||||||
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
import io.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
|
||||||
import io.noties.markwon.syntax.Prism4jTheme;
|
import io.noties.markwon.syntax.Prism4jTheme;
|
||||||
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
import io.noties.markwon.syntax.Prism4jThemeDarkula;
|
||||||
@ -105,7 +106,8 @@ public class MarkdownRenderer {
|
|||||||
// default-media-decoder is also added automatically
|
// default-media-decoder is also added automatically
|
||||||
plugin
|
plugin
|
||||||
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
|
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
|
||||||
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()));
|
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()))
|
||||||
|
.addMediaDecoder(GifMediaDecoder.create(false));
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
.usePlugin(SyntaxHighlightPlugin.create(prism4j, prism4jTheme))
|
||||||
|
11
build.gradle
11
build.gradle
@ -4,8 +4,8 @@ buildscript {
|
|||||||
jcenter()
|
jcenter()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:3.5.2'
|
classpath 'com.android.tools.build:gradle:3.5.3'
|
||||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.21.0'
|
classpath 'com.github.ben-manes:gradle-versions-plugin:0.27.0'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,6 +55,11 @@ ext {
|
|||||||
final def commonMarkVersion = '0.13.0'
|
final def commonMarkVersion = '0.13.0'
|
||||||
final def daggerVersion = '2.10'
|
final def daggerVersion = '2.10'
|
||||||
|
|
||||||
|
// please note that `pl.droidsonroids.gif:android-gif-drawable:1.2.15` is used due to the minimum
|
||||||
|
// api level mismatch that Markwon supports (16) and later versions of AndroidGifDrawable (17).
|
||||||
|
// It should not be a problem as this dependency is used as `compileOnly` and users
|
||||||
|
// must specify version explicitly (until library's API changes...)
|
||||||
|
|
||||||
deps = [
|
deps = [
|
||||||
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
|
'x-annotations' : 'androidx.annotation:annotation:1.1.0',
|
||||||
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
|
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
|
||||||
@ -63,7 +68,7 @@ ext {
|
|||||||
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
|
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
|
||||||
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
|
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",
|
||||||
'android-svg' : 'com.caverock:androidsvg:1.4',
|
'android-svg' : 'com.caverock:androidsvg:1.4',
|
||||||
'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.14',
|
'android-gif' : 'pl.droidsonroids.gif:android-gif-drawable:1.2.15',
|
||||||
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0',
|
'jlatexmath-android' : 'ru.noties:jlatexmath-android:0.1.0',
|
||||||
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
|
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
|
||||||
'prism4j' : 'io.noties:prism4j:2.0.0',
|
'prism4j' : 'io.noties:prism4j:2.0.0',
|
||||||
|
@ -32,7 +32,7 @@ export default {
|
|||||||
margin: 0.25em;
|
margin: 0.25em;
|
||||||
border-radius: 0.25em;
|
border-radius: 0.25em;
|
||||||
box-shadow: 0 0 0.1em 0.1em #eee;
|
box-shadow: 0 0 0.1em 0.1em #eee;
|
||||||
max-width: 30%;
|
max-width: 25%;
|
||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
BIN
docs/.vuepress/public/assets/apps/cinopsys.png
Normal file
BIN
docs/.vuepress/public/assets/apps/cinopsys.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
BIN
docs/.vuepress/public/assets/apps/habitica.png
Normal file
BIN
docs/.vuepress/public/assets/apps/habitica.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.5 KiB |
BIN
docs/.vuepress/public/assets/apps/nextcloud.png
Normal file
BIN
docs/.vuepress/public/assets/apps/nextcloud.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.1 KiB |
@ -6,7 +6,7 @@ title: 'Introduction'
|
|||||||
|
|
||||||
<br><br>
|
<br><br>
|
||||||
[](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20)
|
[](http://search.maven.org/#search|ga|1|g%3A%22io.noties.markwon%22%20)
|
||||||
[](https://travis-ci.org/noties/Markwon)
|
[](https://github.com/noties/Markwon/actions)
|
||||||
|
|
||||||
**Markwon** is a markdown library for Android. It parses markdown following
|
**Markwon** is a markdown library for Android. It parses markdown following
|
||||||
<Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library
|
<Link name="commonmark-spec" /> with the help of amazing <Link name="commonmark-java" /> library
|
||||||
@ -79,7 +79,26 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht
|
|||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
## Awesome Markwon
|
<style>
|
||||||
|
.awesome-block {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
height: 7rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.awesome-block * {
|
||||||
|
border: 0
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="awesome-block">
|
||||||
|
|
||||||
|
## # Awesome Markwon
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
<u>Applications using Markwon</u>:
|
<u>Applications using Markwon</u>:
|
||||||
|
|
||||||
@ -88,9 +107,12 @@ and 2 themes included: Light & Dark. It can be downloaded from [releases](ht
|
|||||||
* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds.
|
* [Boxcryptor](https://www.boxcryptor.com) - A software that adds AES-256 and RSA encryption to Dropbox, Google Drive, OneDrive and many other clouds.
|
||||||
|
|
||||||
<AwesomeGroup :apps="[
|
<AwesomeGroup :apps="[
|
||||||
{name: 'Cinopsys: Movies and Shows', image: 'http://drive.google.com/uc?export=view&id=1rD0HLd8tDUCe8QcVEG_iGvsJbFyozRhC', link: 'https://play.google.com/store/apps/details?id=com.cinopsys.movieshows'}
|
{name: 'Nextcloud', image: $withBase(`/assets/apps/nextcloud.png`), link: 'https://github.com/nextcloud/android', description: 'A safe home for all your data. Access & share your files, calendars, contacts, mail & more from any device, on your terms.'},
|
||||||
|
{name: 'Habitica', image: $withBase(`/assets/apps/habitica.png`), link: 'https://play.google.com/store/apps/details?id=com.habitrpg.android.habitica', description: 'Treat your life like a game to stay motivated and organized! Habitica makes it simple to have fun while accomplishing goals.'},
|
||||||
|
{name: 'Cinopsys: Movies and Shows', image: $withBase(`/assets/apps/cinopsys.png`), link: 'https://play.google.com/store/apps/details?id=com.cinopsys.movieshows'}
|
||||||
]" />
|
]" />
|
||||||
|
|
||||||
|
|
||||||
<u>Extension/plugins</u>:
|
<u>Extension/plugins</u>:
|
||||||
|
|
||||||
* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background.
|
* [MarkwonCodeEx](https://github.com/kingideayou/MarkwonCodeEx) - Markwon extension support elegant code background.
|
||||||
|
@ -184,11 +184,12 @@ imagesPlugin.addSchemeHandler(new SchemeHandler() {
|
|||||||
:::warning
|
:::warning
|
||||||
If you wish to add support for **SVG** or **GIF** you must explicitly add these dependencies
|
If you wish to add support for **SVG** or **GIF** you must explicitly add these dependencies
|
||||||
to your project:
|
to your project:
|
||||||
* for `SVG`: `com.caverock:androidsvg:1.4`
|
* to support `SVG`: [com.caverock:androidsvg](https://github.com/BigBadaboom/androidsvg)
|
||||||
* for `GIF`: `pl.droidsonroids.gif:android-gif-drawable:1.2.14`
|
* to support `GIF`: [pl.droidsonroids.gif:android-gif-drawable](https://github.com/koral--/android-gif-drawable)
|
||||||
|
|
||||||
You can try more recent versions of these libraries, but make sure that they doesn't
|
For [security reasons](https://github.com/noties/Markwon/issues/186) it's advisable to use latest
|
||||||
introduce any unexpected behavior.
|
versions of these libraries. If you notice compilation and/or runtime issues when used with Markwon,
|
||||||
|
please [create an issue](https://github.com/noties/Markwon/issues/new) specifying library and library version used.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ android.enableJetifier=true
|
|||||||
android.enableBuildCache=true
|
android.enableBuildCache=true
|
||||||
android.buildCacheDir=build/pre-dex-cache
|
android.buildCacheDir=build/pre-dex-cache
|
||||||
|
|
||||||
VERSION_NAME=4.2.0
|
VERSION_NAME=4.2.1
|
||||||
|
|
||||||
GROUP=io.noties.markwon
|
GROUP=io.noties.markwon
|
||||||
POM_DESCRIPTION=Markwon markdown for Android
|
POM_DESCRIPTION=Markwon markdown for Android
|
||||||
|
@ -187,7 +187,7 @@ public class SpannableBuilder implements Appendable, CharSequence {
|
|||||||
// if a span was fully including resulting subSequence it's start and
|
// if a span was fully including resulting subSequence it's start and
|
||||||
// end must be within 0..length bounds
|
// end must be within 0..length bounds
|
||||||
s = Math.max(0, span.start - start);
|
s = Math.max(0, span.start - start);
|
||||||
e = Math.max(length, s + (span.end - span.start));
|
e = Math.min(length, s + (span.end - span.start));
|
||||||
|
|
||||||
builder.setSpan(
|
builder.setSpan(
|
||||||
span.what,
|
span.what,
|
||||||
|
@ -4,6 +4,7 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.os.Build;
|
||||||
import android.text.Layout;
|
import android.text.Layout;
|
||||||
import android.text.style.LeadingMarginSpan;
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
|
||||||
@ -15,6 +16,13 @@ import io.noties.markwon.utils.LeadingMarginUtils;
|
|||||||
|
|
||||||
public class BulletListItemSpan implements LeadingMarginSpan {
|
public class BulletListItemSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private static final boolean IS_NOUGAT;
|
||||||
|
|
||||||
|
static {
|
||||||
|
final int sdk = Build.VERSION.SDK_INT;
|
||||||
|
IS_NOUGAT = Build.VERSION_CODES.N == sdk || Build.VERSION_CODES.N_MR1 == sdk;
|
||||||
|
}
|
||||||
|
|
||||||
private MarkwonTheme theme;
|
private MarkwonTheme theme;
|
||||||
|
|
||||||
private final Paint paint = ObjectsPool.paint();
|
private final Paint paint = ObjectsPool.paint();
|
||||||
@ -62,6 +70,14 @@ public class BulletListItemSpan implements LeadingMarginSpan {
|
|||||||
|
|
||||||
final int marginLeft = (width - side) / 2;
|
final int marginLeft = (width - side) / 2;
|
||||||
|
|
||||||
|
// in order to support RTL
|
||||||
|
final int l;
|
||||||
|
final int r;
|
||||||
|
{
|
||||||
|
// @since 4.2.1 to correctly position bullet
|
||||||
|
// when nested inside other LeadingMarginSpans (sorry, Nougat)
|
||||||
|
if (IS_NOUGAT) {
|
||||||
|
|
||||||
// @since 2.0.2
|
// @since 2.0.2
|
||||||
// There is a bug in Android Nougat, when this span receives an `x` that
|
// There is a bug in Android Nougat, when this span receives an `x` that
|
||||||
// doesn't correspond to what it should be (text is placed correctly though).
|
// doesn't correspond to what it should be (text is placed correctly though).
|
||||||
@ -76,14 +92,19 @@ public class BulletListItemSpan implements LeadingMarginSpan {
|
|||||||
diff = (width * level) - x;
|
diff = (width * level) - x;
|
||||||
}
|
}
|
||||||
|
|
||||||
// in order to support RTL
|
|
||||||
final int l;
|
|
||||||
final int r;
|
|
||||||
{
|
|
||||||
final int left = x + (dir * marginLeft);
|
final int left = x + (dir * marginLeft);
|
||||||
final int right = left + (dir * side);
|
final int right = left + (dir * side);
|
||||||
l = Math.min(left, right) + (dir * diff);
|
l = Math.min(left, right) + (dir * diff);
|
||||||
r = Math.max(left, right) + (dir * diff);
|
r = Math.max(left, right) + (dir * diff);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (dir > 0) {
|
||||||
|
l = x + marginLeft;
|
||||||
|
} else {
|
||||||
|
l = x - width + marginLeft;
|
||||||
|
}
|
||||||
|
r = l + side;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2);
|
final int t = baseline + (int) ((paint.descent() + paint.ascent()) / 2.F + .5F) - (side / 2);
|
||||||
|
@ -55,6 +55,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
/**
|
/**
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
@Nullable
|
@Nullable
|
||||||
public ImageSize getImageSize() {
|
public ImageSize getImageSize() {
|
||||||
return imageSize;
|
return imageSize;
|
||||||
@ -63,20 +64,33 @@ public class AsyncDrawable extends Drawable {
|
|||||||
/**
|
/**
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
@NonNull
|
@NonNull
|
||||||
public ImageSizeResolver getImageSizeResolver() {
|
public ImageSizeResolver getImageSizeResolver() {
|
||||||
return imageSizeResolver;
|
return imageSizeResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @see #hasKnownDimensions()
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
|
* @deprecated 4.2.1
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||||
|
@Deprecated
|
||||||
public boolean hasKnownDimentions() {
|
public boolean hasKnownDimentions() {
|
||||||
return canvasWidth > 0;
|
return canvasWidth > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #hasKnownDimentions()
|
* @since 4.2.1
|
||||||
|
*/
|
||||||
|
@SuppressWarnings({"unused", "WeakerAccess"})
|
||||||
|
public boolean hasKnownDimensions() {
|
||||||
|
return canvasWidth > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see #hasKnownDimensions()
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
*/
|
*/
|
||||||
public int getLastKnownCanvasWidth() {
|
public int getLastKnownCanvasWidth() {
|
||||||
@ -84,9 +98,10 @@ public class AsyncDrawable extends Drawable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see #hasKnownDimentions()
|
* @see #hasKnownDimensions()
|
||||||
* @since 4.0.0
|
* @since 4.0.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public float getLastKnowTextSize() {
|
public float getLastKnowTextSize() {
|
||||||
return textSize;
|
return textSize;
|
||||||
}
|
}
|
||||||
@ -95,6 +110,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
public boolean hasResult() {
|
public boolean hasResult() {
|
||||||
return result != null;
|
return result != null;
|
||||||
}
|
}
|
||||||
@ -104,10 +120,17 @@ public class AsyncDrawable extends Drawable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// yeah
|
// yeah
|
||||||
public void setCallback2(@Nullable Callback callback) {
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
public void setCallback2(@Nullable Callback cb) {
|
||||||
|
|
||||||
this.callback = callback;
|
// @since 4.2.1
|
||||||
super.setCallback(callback);
|
// wrap callback so invalidation happens to this AsyncDrawable instance
|
||||||
|
// and not for wrapped result/placeholder
|
||||||
|
this.callback = cb == null
|
||||||
|
? null
|
||||||
|
: new WrappedCallback(cb);
|
||||||
|
|
||||||
|
super.setCallback(cb);
|
||||||
|
|
||||||
// if not null -> means we are attached
|
// if not null -> means we are attached
|
||||||
if (callback != null) {
|
if (callback != null) {
|
||||||
@ -138,6 +161,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
/**
|
/**
|
||||||
* @since 3.0.1
|
* @since 3.0.1
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
protected void setPlaceholderResult(@NonNull Drawable placeholder) {
|
protected void setPlaceholderResult(@NonNull Drawable placeholder) {
|
||||||
// okay, if placeholder has bounds -> use it, otherwise use original imageSize
|
// okay, if placeholder has bounds -> use it, otherwise use original imageSize
|
||||||
|
|
||||||
@ -175,7 +199,6 @@ public class AsyncDrawable extends Drawable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.result = result;
|
this.result = result;
|
||||||
this.result.setCallback(callback);
|
|
||||||
|
|
||||||
initBounds();
|
initBounds();
|
||||||
}
|
}
|
||||||
@ -210,6 +233,12 @@ public class AsyncDrawable extends Drawable {
|
|||||||
|
|
||||||
final Rect bounds = resolveBounds();
|
final Rect bounds = resolveBounds();
|
||||||
result.setBounds(bounds);
|
result.setBounds(bounds);
|
||||||
|
// @since 4.2.1, we set callback after bounds are resolved
|
||||||
|
// to reduce number of invalidations
|
||||||
|
result.setCallback(callback);
|
||||||
|
|
||||||
|
// so, this method will check if there is previous bounds and call invalidate _BEFORE_
|
||||||
|
// applying new bounds. This is why it is important to have initial bounds empty.
|
||||||
setBounds(bounds);
|
setBounds(bounds);
|
||||||
|
|
||||||
invalidateSelf();
|
invalidateSelf();
|
||||||
@ -291,6 +320,7 @@ public class AsyncDrawable extends Drawable {
|
|||||||
return imageSizeResolver.resolveImageSize(this);
|
return imageSizeResolver.resolveImageSize(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "AsyncDrawable{" +
|
return "AsyncDrawable{" +
|
||||||
@ -302,4 +332,30 @@ public class AsyncDrawable extends Drawable {
|
|||||||
", waitingForDimensions=" + waitingForDimensions +
|
", waitingForDimensions=" + waitingForDimensions +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @since 4.2.1
|
||||||
|
// Wrapped callback to trigger invalidation for this AsyncDrawable instance (and not result/placeholder)
|
||||||
|
private class WrappedCallback implements Callback {
|
||||||
|
|
||||||
|
private final Callback callback;
|
||||||
|
|
||||||
|
WrappedCallback(@NonNull Callback callback) {
|
||||||
|
this.callback = callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateDrawable(@NonNull Drawable who) {
|
||||||
|
callback.invalidateDrawable(AsyncDrawable.this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||||
|
callback.scheduleDrawable(AsyncDrawable.this, what, when);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||||
|
callback.unscheduleDrawable(AsyncDrawable.this, what);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -42,11 +42,9 @@ public class AsyncDrawableSpan extends ReplacementSpan {
|
|||||||
this.alignment = alignment;
|
this.alignment = alignment;
|
||||||
this.replacementTextIsLink = replacementTextIsLink;
|
this.replacementTextIsLink = replacementTextIsLink;
|
||||||
|
|
||||||
// additionally set intrinsic bounds if empty
|
// @since 4.2.1 we do not set intrinsic bounds
|
||||||
final Rect rect = drawable.getBounds();
|
// at this point they will always be 0,0-1,1, but this
|
||||||
if (rect.isEmpty()) {
|
// will trigger another invalidation when we will have bounds
|
||||||
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,6 +34,7 @@
|
|||||||
android:windowSoftInputMode="adjustResize" />
|
android:windowSoftInputMode="adjustResize" />
|
||||||
|
|
||||||
<activity android:name=".inlineparser.InlineParserActivity" />
|
<activity android:name=".inlineparser.InlineParserActivity" />
|
||||||
|
<activity android:name=".htmldetails.HtmlDetailsActivity" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension.CustomExtensionActivity;
|
|||||||
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
import io.noties.markwon.sample.customextension2.CustomExtensionActivity2;
|
||||||
import io.noties.markwon.sample.editor.EditorActivity;
|
import io.noties.markwon.sample.editor.EditorActivity;
|
||||||
import io.noties.markwon.sample.html.HtmlActivity;
|
import io.noties.markwon.sample.html.HtmlActivity;
|
||||||
|
import io.noties.markwon.sample.htmldetails.HtmlDetailsActivity;
|
||||||
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
import io.noties.markwon.sample.inlineparser.InlineParserActivity;
|
||||||
import io.noties.markwon.sample.latex.LatexActivity;
|
import io.noties.markwon.sample.latex.LatexActivity;
|
||||||
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
import io.noties.markwon.sample.precomputed.PrecomputedActivity;
|
||||||
@ -127,6 +128,10 @@ public class MainActivity extends Activity {
|
|||||||
activity = InlineParserActivity.class;
|
activity = InlineParserActivity.class;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case HTML_DETAILS:
|
||||||
|
activity = HtmlDetailsActivity.class;
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,9 @@ public enum Sample {
|
|||||||
|
|
||||||
EDITOR(R.string.sample_editor),
|
EDITOR(R.string.sample_editor),
|
||||||
|
|
||||||
INLINE_PARSER(R.string.sample_inline_parser);
|
INLINE_PARSER(R.string.sample_inline_parser),
|
||||||
|
|
||||||
|
HTML_DETAILS(R.string.sample_html_details);
|
||||||
|
|
||||||
private final int textResId;
|
private final int textResId;
|
||||||
|
|
||||||
|
@ -0,0 +1,410 @@
|
|||||||
|
package io.noties.markwon.sample.htmldetails;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.noties.markwon.Markwon;
|
||||||
|
import io.noties.markwon.MarkwonVisitor;
|
||||||
|
import io.noties.markwon.SpannableBuilder;
|
||||||
|
import io.noties.markwon.core.MarkwonTheme;
|
||||||
|
import io.noties.markwon.html.HtmlPlugin;
|
||||||
|
import io.noties.markwon.html.HtmlTag;
|
||||||
|
import io.noties.markwon.html.MarkwonHtmlRenderer;
|
||||||
|
import io.noties.markwon.html.TagHandler;
|
||||||
|
import io.noties.markwon.image.ImagesPlugin;
|
||||||
|
import io.noties.markwon.sample.R;
|
||||||
|
import io.noties.markwon.utils.LeadingMarginUtils;
|
||||||
|
import io.noties.markwon.utils.NoCopySpannableFactory;
|
||||||
|
|
||||||
|
public class HtmlDetailsActivity extends Activity {
|
||||||
|
|
||||||
|
private ViewGroup content;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_html_details);
|
||||||
|
|
||||||
|
content = findViewById(R.id.content);
|
||||||
|
|
||||||
|
sample_details();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sample_details() {
|
||||||
|
|
||||||
|
final String md = "# Hello\n\n<details>\n" +
|
||||||
|
" <summary>stuff with \n\n*mark* **down**\n\n</summary>\n" +
|
||||||
|
" <p>\n\n" +
|
||||||
|
"<!-- the above p cannot start right at the beginning of the line and is mandatory for everything else to work -->\n" +
|
||||||
|
"## *formatted* **heading** with [a](link)\n" +
|
||||||
|
"```java\n" +
|
||||||
|
"code block\n" +
|
||||||
|
"```\n" +
|
||||||
|
"\n" +
|
||||||
|
" <details>\n" +
|
||||||
|
" <summary><small>nested</small> stuff</summary><p>\n" +
|
||||||
|
"<!-- alternative placement of p shown above -->\n" +
|
||||||
|
"\n" +
|
||||||
|
"* list\n" +
|
||||||
|
"* with\n" +
|
||||||
|
"\n\n" +
|
||||||
|
"\n\n" +
|
||||||
|
" 1. nested\n" +
|
||||||
|
" 1. items\n" +
|
||||||
|
"\n" +
|
||||||
|
" ```java\n" +
|
||||||
|
" // including code\n" +
|
||||||
|
" ```\n" +
|
||||||
|
" 1. blocks\n" +
|
||||||
|
"\n" +
|
||||||
|
"<details><summary>The 3rd!</summary>\n\n" +
|
||||||
|
"**bold** _em_\n</details>" +
|
||||||
|
" </p></details>\n" +
|
||||||
|
"</p></details>\n\n" +
|
||||||
|
"and **this** *is* how...";
|
||||||
|
|
||||||
|
final Markwon markwon = Markwon.builder(this)
|
||||||
|
.usePlugin(HtmlPlugin.create(plugin ->
|
||||||
|
plugin.addHandler(new DetailsTagHandler())))
|
||||||
|
.usePlugin(ImagesPlugin.create())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
final Spanned spanned = markwon.toMarkdown(md);
|
||||||
|
final DetailsParsingSpan[] spans = spanned.getSpans(0, spanned.length(), DetailsParsingSpan.class);
|
||||||
|
|
||||||
|
// if we have no details, proceed as usual (single text-view)
|
||||||
|
if (spans == null || spans.length == 0) {
|
||||||
|
// no details
|
||||||
|
final TextView textView = appendTextView();
|
||||||
|
markwon.setParsedMarkdown(textView, spanned);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<DetailsElement> list = new ArrayList<>();
|
||||||
|
|
||||||
|
for (DetailsParsingSpan span : spans) {
|
||||||
|
final DetailsElement e = settle(new DetailsElement(spanned.getSpanStart(span), spanned.getSpanEnd(span), span.summary), list);
|
||||||
|
if (e != null) {
|
||||||
|
list.add(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (DetailsElement element : list) {
|
||||||
|
initDetails(element, spanned);
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(list);
|
||||||
|
|
||||||
|
|
||||||
|
TextView textView;
|
||||||
|
int start = 0;
|
||||||
|
|
||||||
|
for (DetailsElement element : list) {
|
||||||
|
|
||||||
|
if (element.start != start) {
|
||||||
|
// subSequence and add new TextView
|
||||||
|
textView = appendTextView();
|
||||||
|
textView.setText(subSequenceTrimmed(spanned, start, element.start));
|
||||||
|
}
|
||||||
|
|
||||||
|
// now add details TextView
|
||||||
|
textView = appendTextView();
|
||||||
|
initDetailsTextView(markwon, textView, element);
|
||||||
|
|
||||||
|
start = element.end;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (start != spanned.length()) {
|
||||||
|
// another textView with rest content
|
||||||
|
textView = appendTextView();
|
||||||
|
textView.setText(subSequenceTrimmed(spanned, start, spanned.length()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private TextView appendTextView() {
|
||||||
|
final View view = getLayoutInflater().inflate(R.layout.view_html_details_text_view, content, false);
|
||||||
|
final TextView textView = view.findViewById(R.id.text);
|
||||||
|
content.addView(view);
|
||||||
|
return textView;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDetailsTextView(
|
||||||
|
@NonNull Markwon markwon,
|
||||||
|
@NonNull TextView textView,
|
||||||
|
@NonNull DetailsElement element) {
|
||||||
|
|
||||||
|
// minor optimization
|
||||||
|
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
|
||||||
|
|
||||||
|
// so, each element with children is a details tag
|
||||||
|
// there is a reason why we needed the SpannableBuilder in the first place -> we must revert spans
|
||||||
|
// final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
final SpannableBuilder builder = new SpannableBuilder();
|
||||||
|
append(builder, markwon, textView, element, element);
|
||||||
|
markwon.setParsedMarkdown(textView, builder.spannableStringBuilder());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void append(
|
||||||
|
@NonNull SpannableBuilder builder,
|
||||||
|
@NonNull Markwon markwon,
|
||||||
|
@NonNull TextView textView,
|
||||||
|
@NonNull DetailsElement root,
|
||||||
|
@NonNull DetailsElement element) {
|
||||||
|
if (!element.children.isEmpty()) {
|
||||||
|
|
||||||
|
final int start = builder.length();
|
||||||
|
|
||||||
|
// builder.append(element.content);
|
||||||
|
builder.append(subSequenceTrimmed(element.content, 0, element.content.length()));
|
||||||
|
|
||||||
|
builder.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(@NonNull View widget) {
|
||||||
|
element.expanded = !element.expanded;
|
||||||
|
|
||||||
|
initDetailsTextView(markwon, textView, root);
|
||||||
|
}
|
||||||
|
}, start, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
|
||||||
|
if (element.expanded) {
|
||||||
|
for (DetailsElement child : element.children) {
|
||||||
|
append(builder, markwon, textView, root, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.setSpan(new DetailsSpan(markwon.configuration().theme(), element), start);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
builder.append(element.content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if null -> remove from where it was processed,
|
||||||
|
// else replace from where it was processed with a new one (can become expandable)
|
||||||
|
@Nullable
|
||||||
|
private static DetailsElement settle(
|
||||||
|
@NonNull DetailsElement element,
|
||||||
|
@NonNull List<? extends DetailsElement> elements) {
|
||||||
|
for (DetailsElement e : elements) {
|
||||||
|
if (element.start > e.start && element.end <= e.end) {
|
||||||
|
final DetailsElement settled = settle(element, e.children);
|
||||||
|
if (settled != null) {
|
||||||
|
|
||||||
|
// the thing is we must balance children if done like this
|
||||||
|
// let's just create a tree actually, so we are easier to modify
|
||||||
|
final Iterator<DetailsElement> iterator = e.children.iterator();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
final DetailsElement balanced = settle(iterator.next(), Collections.singletonList(element));
|
||||||
|
if (balanced == null) {
|
||||||
|
iterator.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// add to our children
|
||||||
|
e.children.add(element);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void initDetails(@NonNull DetailsElement element, @NonNull Spanned spanned) {
|
||||||
|
int end = element.end;
|
||||||
|
for (int i = element.children.size() - 1; i >= 0; i--) {
|
||||||
|
final DetailsElement child = element.children.get(i);
|
||||||
|
if (child.end < end) {
|
||||||
|
element.children.add(new DetailsElement(child.end, end, spanned.subSequence(child.end, end)));
|
||||||
|
}
|
||||||
|
initDetails(child, spanned);
|
||||||
|
end = child.start;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int start = (element.start + element.content.length());
|
||||||
|
if (end != start) {
|
||||||
|
element.children.add(new DetailsElement(start, end, spanned.subSequence(start, end)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void sort(@NonNull List<DetailsElement> elements) {
|
||||||
|
Collections.sort(elements, (o1, o2) -> Integer.compare(o1.start, o2.start));
|
||||||
|
for (DetailsElement element : elements) {
|
||||||
|
sort(element.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static CharSequence subSequenceTrimmed(@NonNull CharSequence cs, int start, int end) {
|
||||||
|
|
||||||
|
while (start < end) {
|
||||||
|
|
||||||
|
final boolean isStartEmpty = Character.isWhitespace(cs.charAt(start));
|
||||||
|
final boolean isEndEmpty = Character.isWhitespace(cs.charAt(end - 1));
|
||||||
|
|
||||||
|
if (!isStartEmpty && !isEndEmpty) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStartEmpty) {
|
||||||
|
start += 1;
|
||||||
|
}
|
||||||
|
if (isEndEmpty) {
|
||||||
|
end -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cs.subSequence(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsElement {
|
||||||
|
|
||||||
|
final int start;
|
||||||
|
final int end;
|
||||||
|
final CharSequence content;
|
||||||
|
final List<DetailsElement> children = new ArrayList<>(0);
|
||||||
|
|
||||||
|
boolean expanded;
|
||||||
|
|
||||||
|
DetailsElement(int start, int end, @NonNull CharSequence content) {
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "DetailsElement{" +
|
||||||
|
"start=" + start +
|
||||||
|
", end=" + end +
|
||||||
|
", content=" + toStringContent(content) +
|
||||||
|
", children=" + children +
|
||||||
|
", expanded=" + expanded +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static String toStringContent(@NonNull CharSequence cs) {
|
||||||
|
return cs.toString().replaceAll("\n", "\\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsTagHandler extends TagHandler {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handle(
|
||||||
|
@NonNull MarkwonVisitor visitor,
|
||||||
|
@NonNull MarkwonHtmlRenderer renderer,
|
||||||
|
@NonNull HtmlTag tag) {
|
||||||
|
|
||||||
|
int summaryEnd = -1;
|
||||||
|
|
||||||
|
for (HtmlTag child : tag.getAsBlock().children()) {
|
||||||
|
|
||||||
|
if (!child.isClosed()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("summary".equals(child.name())) {
|
||||||
|
summaryEnd = child.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
final TagHandler tagHandler = renderer.tagHandler(child.name());
|
||||||
|
if (tagHandler != null) {
|
||||||
|
tagHandler.handle(visitor, renderer, child);
|
||||||
|
} else if (child.isBlock()) {
|
||||||
|
visitChildren(visitor, renderer, child.getAsBlock());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (summaryEnd > -1) {
|
||||||
|
visitor.builder().setSpan(new DetailsParsingSpan(
|
||||||
|
subSequenceTrimmed(visitor.builder(), tag.start(), summaryEnd)
|
||||||
|
), tag.start(), tag.end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Collection<String> supportedTags() {
|
||||||
|
return Collections.singleton("details");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsParsingSpan {
|
||||||
|
|
||||||
|
final CharSequence summary;
|
||||||
|
|
||||||
|
DetailsParsingSpan(@NonNull CharSequence summary) {
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class DetailsSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private final DetailsElement element;
|
||||||
|
private final int blockMargin;
|
||||||
|
private final int blockQuoteWidth;
|
||||||
|
|
||||||
|
private final Rect rect = new Rect();
|
||||||
|
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
|
|
||||||
|
DetailsSpan(@NonNull MarkwonTheme theme, @NonNull DetailsElement element) {
|
||||||
|
this.element = element;
|
||||||
|
this.blockMargin = theme.getBlockMargin();
|
||||||
|
this.blockQuoteWidth = theme.getBlockQuoteWidth();
|
||||||
|
this.paint.setStyle(Paint.Style.FILL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLeadingMargin(boolean first) {
|
||||||
|
return blockMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||||
|
|
||||||
|
if (LeadingMarginUtils.selfStart(start, text, this)) {
|
||||||
|
rect.set(x, top, x + blockMargin, bottom);
|
||||||
|
if (element.expanded) {
|
||||||
|
paint.setColor(Color.GREEN);
|
||||||
|
} else {
|
||||||
|
paint.setColor(Color.RED);
|
||||||
|
}
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if (element.expanded) {
|
||||||
|
final int l = (blockMargin - blockQuoteWidth) / 2;
|
||||||
|
rect.set(x + l, top, x + l + blockQuoteWidth, bottom);
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
paint.setColor(Color.GRAY);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
sample/src/main/res/layout/activity_html_details.xml
Normal file
12
sample/src/main/res/layout/activity_html_details.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical" />
|
||||||
|
|
||||||
|
</ScrollView>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000" />
|
@ -29,4 +29,6 @@
|
|||||||
|
|
||||||
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
|
||||||
|
|
||||||
|
<string name="sample_html_details"># \# HTML <details> tag\n\n<details> tag parsed and rendered</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user