Add recycler-table module
@ -1,4 +1,4 @@
|
||||
|
||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
||||
const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||
const artifacts = [{"id":"core","name":"Core","group":"ru.noties.markwon","description":"Core Markwon artifact that includes basic markdown parsing and rendering"},{"id":"ext-latex","name":"LaTeX","group":"ru.noties.markwon","description":"Extension to add LaTeX formulas to Markwon markdown"},{"id":"ext-strikethrough","name":"Strikethrough","group":"ru.noties.markwon","description":"Extension to add strikethrough markup to Markwon markdown"},{"id":"ext-tables","name":"Tables","group":"ru.noties.markwon","description":"Extension to add tables markup (GFM) to Markwon markdown"},{"id":"ext-tasklist","name":"Task List","group":"ru.noties.markwon","description":"Extension to add task lists (GFM) to Markwon markdown"},{"id":"html","name":"HTML","group":"ru.noties.markwon","description":"Provides HTML parsing functionality"},{"id":"image-gif","name":"Image GIF","group":"ru.noties.markwon","description":"Adds GIF media support to Markwon markdown"},{"id":"image-okhttp","name":"Image OkHttp","group":"ru.noties.markwon","description":"Adds OkHttp client to retrieve images data from network"},{"id":"image-svg","name":"Image SVG","group":"ru.noties.markwon","description":"Adds SVG media support to Markwon markdown"},{"id":"recycler","name":"Recycler","group":"ru.noties.markwon","description":"Provides RecyclerView.Adapter to display Markwon markdown"},{"id":"recycler-table","name":"Recycler Table","group":"ru.noties.markwon","description":"Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget"},{"id":"syntax-highlight","name":"Syntax Highlight","group":"ru.noties.markwon","description":"Add syntax highlight to Markwon markdown via Prism4j library"}];
|
||||
export { artifacts };
|
||||
|
@ -60,6 +60,7 @@ module.exports = {
|
||||
'/docs/v3/image/okhttp.md',
|
||||
'/docs/v3/image/svg.md',
|
||||
'/docs/v3/recycler/',
|
||||
'/docs/v3/recycler-table/',
|
||||
'/docs/v3/syntax-highlight/',
|
||||
'/docs/v3/migration-2-3.md'
|
||||
],
|
||||
|
BIN
docs/.vuepress/public/assets/recycler-table-screenshot.png
Normal file
After Width: | Height: | Size: 77 KiB |
@ -54,8 +54,11 @@ final Table table = Table.parse(Markwon, TableBlock);
|
||||
myTableWidget.setTable(table);
|
||||
```
|
||||
|
||||
Unfortunately Markwon does not provide a widget that can be used for tables. But it does
|
||||
provide API that can be used to achieve desired result.
|
||||
:::tip
|
||||
To take advantage of this functionality and render tables without limitations (including
|
||||
horizontally scrollable layout when its contents exceed screen width), refer to [recycler-table](/docs/v3/recycler-table)
|
||||
module documentation that adds support for rendering `TableBlock` markdown node inside Android-native `TableLayout` widget.
|
||||
:::
|
||||
|
||||
## Theme
|
||||
|
||||
|
@ -8,3 +8,139 @@ Adds support for GFM (Github-flavored markdown) task-lists:
|
||||
Markwon.builder(context)
|
||||
.usePlugin(TaskListPlugin.create(context));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Create a default instance of `TaskListPlugin` with `TaskListDrawable` initialized to use
|
||||
`android.R.attr.textColorLink` as primary color and `android.R.attr.colorBackground` as background
|
||||
```java
|
||||
TaskListPlugin.create(context);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Create an instance of `TaskListPlugin` with exact color values to use:
|
||||
```java
|
||||
// obtain color values
|
||||
final int checkedFillColor = /* */;
|
||||
final int normalOutlineColor = /* */;
|
||||
final int checkMarkColor = /* */;
|
||||
|
||||
TaskListPlugin.create(checkedFillColor, normalOutlineColor, checkMarkColor);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Specify own drawable for a task list item:
|
||||
|
||||
```java
|
||||
// obtain drawable
|
||||
final Drawable drawable = /* */;
|
||||
|
||||
TaskListPlugin.create(drawable);
|
||||
```
|
||||
|
||||
:::warning
|
||||
Please note that custom drawable for a task list item must correctly handle state
|
||||
in order to display done/not-done:
|
||||
|
||||
```java
|
||||
public class MyTaskListDrawable extends Drawable {
|
||||
|
||||
private boolean isChecked;
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
// draw accordingly to the isChecked value
|
||||
}
|
||||
|
||||
/* implementation omitted */
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] state) {
|
||||
final boolean isChecked = contains(state, android.R.attr.state_checked);
|
||||
final boolean result = this.isChecked != isChecked;
|
||||
if (result) {
|
||||
this.isChecked = isChecked;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean contains(@Nullable int[] states, int value) {
|
||||
if (states != null) {
|
||||
for (int state : states) {
|
||||
if (state == value) {
|
||||
// NB return here
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
:::
|
||||
|
||||
## Task list mutation
|
||||
|
||||
It is possible to mutate task list item state (toggle done/not-done). But note
|
||||
that `Markwon` won't handle state change internally by any means and this change
|
||||
is merely a visual one. If you need to persist state of a task list
|
||||
item change you have to implement it yourself. This should get your started:
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TaskListPlugin.create(context))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
|
||||
|
||||
// obtain original SpanFactory set by TaskListPlugin
|
||||
final SpanFactory origin = builder.getFactory(TaskListItem.class);
|
||||
if (origin == null) {
|
||||
// or throw, as it's a bit weird state and we expect
|
||||
// this factory to be present
|
||||
return;
|
||||
}
|
||||
|
||||
builder.setFactory(TaskListItem.class, new SpanFactory() {
|
||||
@Override
|
||||
public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps props) {
|
||||
// it's a bit non-secure behavior and we should validate
|
||||
// the type of returned span first, but for the sake of brevity
|
||||
// we skip this step
|
||||
final TaskListSpan span = (TaskListSpan) origin.getSpans(configuration, props);
|
||||
|
||||
if (span == null) {
|
||||
// or throw
|
||||
return null;
|
||||
}
|
||||
|
||||
// return an array of spans
|
||||
return new Object[]{
|
||||
span,
|
||||
new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(@NonNull View widget) {
|
||||
// toggle VISUAL state
|
||||
span.setDone(!span.isDone());
|
||||
|
||||
// do not forget to invalidate widget
|
||||
widget.invalidate();
|
||||
|
||||
// execute your persistence logic
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
// no-op, so appearance is not changed (otherwise
|
||||
// task list item will look like a link)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build();
|
||||
```
|
@ -10,62 +10,6 @@ next: /docs/v3/core/getting-started.md
|
||||
|
||||
<ArtifactPicker />
|
||||
|
||||
# Bundle <Badge text="3.0.0" />
|
||||
If you wish to include all Markwon artifacts or add specific artifacts
|
||||
in a different manner than explicit gradle dependency definition, you can
|
||||
use `markwon-bundle.gradle` gradle script:
|
||||
|
||||
*(in your `build.gradle`)*
|
||||
```groovy
|
||||
apply plugin: 'com.android.application'
|
||||
apply from: 'https://raw.githubusercontent.com/noties/Markwon/master/markwon-bundle.gradle'
|
||||
|
||||
android { /* */ }
|
||||
|
||||
ext.markwon = [
|
||||
'version': '3.0.0',
|
||||
'includeAll': true
|
||||
]
|
||||
|
||||
dependencies { /* */ }
|
||||
```
|
||||
|
||||
`markwon` object can have these properties:
|
||||
* `version` - (required) version of `Markwon`
|
||||
* `includeAll` - if _true_ will add all known Markwon artifacts. Can be used with `exclude`
|
||||
* * `exclude` - an array of artifacts to _exclude_ (cannot exclude `core`)
|
||||
* `artifacts` - an array of artifacts (can omit `core`, as it will be added implicitly anyway)
|
||||
|
||||
If `includeAll` property is present and is `true`, then `artifacts` property won't be used.
|
||||
If there is no `includeAll` property or if it is `false`, `exclude` property won't be used.
|
||||
|
||||
These 2 markwon objects are equal:
|
||||
|
||||
```groovy
|
||||
// #1
|
||||
ext.markwon = [
|
||||
'version': '3.0.0',
|
||||
'artifacts': [
|
||||
'ext-latex',
|
||||
'ext-strikethrough',
|
||||
'ext-tables',
|
||||
'ext-tasklist',
|
||||
'html',
|
||||
'image-gif',
|
||||
'image-okhttp',
|
||||
'image-svg',
|
||||
'recycler',
|
||||
'syntax-highlight'
|
||||
]
|
||||
]
|
||||
|
||||
// #2
|
||||
ext.markwon = [
|
||||
'version': '3.0.0',
|
||||
'includeAll': true
|
||||
]
|
||||
```
|
||||
|
||||
## Snapshot
|
||||
|
||||
In order to use latest `SNAPSHOT` version add snapshot repository
|
||||
|
90
docs/docs/v3/recycler-table/README.md
Normal file
@ -0,0 +1,90 @@
|
||||
# Recycler Table <Badge text="3.0.0" />
|
||||
|
||||
Artifact that provides [MarkwonAdapter.Entry](/docs/v3/recycler/) to render `TableBlock` inside
|
||||
Android-native `TableLayout` widget.
|
||||
|
||||
<img :src="$withBase('/assets/recycler-table-screenshot.png')" alt="screenshot" width="45%">
|
||||
<br>
|
||||
<small><em><sup>*</sup> It's possible to wrap `TableLayout` inside a `HorizontalScrollView` to include all table content</em></small>
|
||||
|
||||
---
|
||||
|
||||
Register instance of `TableEntry` with `MarkwonAdapter` to render TableBlocks:
|
||||
```java
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
|
||||
.include(TableBlock.class, TableEntry.create(builder -> builder
|
||||
.tableLayout(R.layout.adapter_table_block, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.view_table_entry_cell)))
|
||||
.build();
|
||||
```
|
||||
|
||||
`TableEntry` requires at least 2 arguments:
|
||||
* `tableLayout` - layout with `TableLayout` inside
|
||||
* `textLayout` - layout with `TextView` inside (represents independent table cell)
|
||||
|
||||
In case when required view is the root of layout specific builder methods can be used:
|
||||
* `tableLayoutIsRoot(int)`
|
||||
* `textLayoutIsRoot(int)`
|
||||
|
||||
If your layouts have different structure (for example wrap a `TableView` inside a `HorizontalScrollView`)
|
||||
then you should use methods that accept ID of required view inside layout:
|
||||
* `tableLayout(int, int)`
|
||||
* `textLayout(int, int)`
|
||||
|
||||
---
|
||||
|
||||
To display `TableBlock` as a `TableLayout` specific `MarkwonPlugin` must be used: `TableEntryPlugin`.
|
||||
|
||||
:::warning
|
||||
Do not use `TablePlugin` if you wish to display markdown tables via `TableEntry`. Use **TableEntryPlugin** instead
|
||||
:::
|
||||
|
||||
`TableEntryPlugin` can reuse existing `TablePlugin` to make appearance of tables the same in both contexts:
|
||||
when rendering _natively_ in a TextView and when rendering in RecyclerView with TableEntry.
|
||||
|
||||
* `TableEntryPlugin.create(Context)` - creates plugin with default `TableTheme`
|
||||
* `TableEntryPlugin.create(TableTheme)` - creates plugin with provided `TableTheme`
|
||||
* `TableEntryPlugin.create(TablePlugin.ThemeConfigure)` - creates plugin with theme configured by `ThemeConfigure`
|
||||
* `TableEntryPlugin.create(TablePlugin)` - creates plugin with `TableTheme` used in provided `TablePlugin`
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TableEntryPlugin.create(context))
|
||||
// other plugins
|
||||
.build();
|
||||
```
|
||||
|
||||
```java
|
||||
final Markwon markwon = Markwon.builder(context)
|
||||
.usePlugin(TableEntryPlugin.create(builder -> builder
|
||||
.tableBorderWidth(0)
|
||||
.tableHeaderRowBackgroundColor(Color.RED)))
|
||||
// other plugins
|
||||
.build();
|
||||
```
|
||||
|
||||
## Table with scrollable content
|
||||
|
||||
To stretch table columns to fit the width of screen or to make table scrollable when content exceeds screen width
|
||||
this layout can be used:
|
||||
|
||||
```xml
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<TableLayout
|
||||
android:id="@+id/table_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="*" />
|
||||
|
||||
</HorizontalScrollView>
|
||||
```
|
@ -1,59 +0,0 @@
|
||||
// await project initialization and check for markwon object then
|
||||
// (so we do not have to force users to put `apply from` block at the bottom
|
||||
// of a build.gradle file)
|
||||
project.afterEvaluate {
|
||||
|
||||
if (!project.hasProperty('markwon')) {
|
||||
throw new RuntimeException("No `markwon` property object is found. " +
|
||||
"Define it with `ext.markwon = [prop: value]`")
|
||||
}
|
||||
|
||||
final def markwon = project.markwon
|
||||
if (!(markwon instanceof Map)) {
|
||||
throw new RuntimeException("`markwon` object property must be of type Map. " +
|
||||
"Groovy short-hand to define: `[:]`.")
|
||||
}
|
||||
|
||||
final def version = markwon.version
|
||||
final def includeAll = markwon.computeIfAbsent('includeAll', { false })
|
||||
final def artifacts
|
||||
if (includeAll) {
|
||||
|
||||
// cannot exclude core
|
||||
final def exclude = markwon.computeIfAbsent('exclude', { [] }) \
|
||||
.unique() \
|
||||
.findAll { 'core' != it }
|
||||
|
||||
artifacts = [
|
||||
'core',
|
||||
'ext-latex',
|
||||
'ext-strikethrough',
|
||||
'ext-tables',
|
||||
'ext-tasklist',
|
||||
'html',
|
||||
'image-gif',
|
||||
'image-okhttp',
|
||||
'image-svg',
|
||||
'recycler',
|
||||
'syntax-highlight'
|
||||
].findAll { !exclude.contains(it) }
|
||||
|
||||
} else {
|
||||
artifacts = (markwon.containsKey('artifacts') ? markwon.artifacts : ['core']).with {
|
||||
// add implicit core artifact
|
||||
if (!it.contains('core')) {
|
||||
it.add('core')
|
||||
}
|
||||
return it
|
||||
}
|
||||
}
|
||||
|
||||
if (!version) {
|
||||
throw new RuntimeException("Please specify version of Markwon, for example: " +
|
||||
"`ext.markwon = [ 'version': '1.0.0']`")
|
||||
}
|
||||
|
||||
artifacts.forEach {
|
||||
project.dependencies.add('implementation', "ru.noties.markwon:$it:$version")
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@ package ru.noties.markwon;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -101,6 +102,9 @@ public abstract class Markwon {
|
||||
*/
|
||||
public abstract boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> plugin);
|
||||
|
||||
@Nullable
|
||||
public abstract <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type);
|
||||
|
||||
/**
|
||||
* Builder for {@link Markwon}.
|
||||
* <p>
|
||||
|
@ -1,6 +1,7 @@
|
||||
package ru.noties.markwon;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.Spanned;
|
||||
import android.widget.TextView;
|
||||
|
||||
@ -91,13 +92,19 @@ class MarkwonImpl extends Markwon {
|
||||
|
||||
@Override
|
||||
public boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> type) {
|
||||
boolean result = false;
|
||||
return getPlugin(type) != null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> type) {
|
||||
MarkwonPlugin out = null;
|
||||
for (MarkwonPlugin plugin : plugins) {
|
||||
if (type.isAssignableFrom(plugin.getClass())) {
|
||||
result = true;
|
||||
break;
|
||||
out = plugin;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
//noinspection unchecked
|
||||
return (P) out;
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,10 @@ import java.util.List;
|
||||
*/
|
||||
public abstract class MarkwonReducer {
|
||||
|
||||
/**
|
||||
* @return direct children of supplied Node. In the most usual case
|
||||
* will return all BlockNodes of a Document
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonReducer directChildren() {
|
||||
return new DirectChildren();
|
||||
|
@ -34,6 +34,12 @@ public interface MarkwonSpansFactory {
|
||||
@NonNull
|
||||
<N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory);
|
||||
|
||||
/**
|
||||
* Can be useful when <em>enhancing</em> an already defined SpanFactory with another one.
|
||||
*/
|
||||
@Nullable
|
||||
<N extends Node> SpanFactory getFactory(@NonNull Class<N> node);
|
||||
|
||||
@NonNull
|
||||
MarkwonSpansFactory build();
|
||||
}
|
||||
|
@ -52,6 +52,12 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public <N extends Node> SpanFactory getFactory(@NonNull Class<N> node) {
|
||||
return factories.get(node);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MarkwonSpansFactory build() {
|
||||
|
@ -0,0 +1,30 @@
|
||||
package ru.noties.markwon.utils;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
|
||||
/**
|
||||
* Utility SpannableFactory that re-uses Spannable instance between multiple
|
||||
* `TextView#setText` calls.
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class NoCopySpannableFactory extends Spannable.Factory {
|
||||
|
||||
@NonNull
|
||||
public static NoCopySpannableFactory getInstance() {
|
||||
return Holder.INSTANCE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Spannable newSpannable(CharSequence source) {
|
||||
return source instanceof Spannable
|
||||
? (Spannable) source
|
||||
: new SpannableString(source);
|
||||
}
|
||||
|
||||
static class Holder {
|
||||
private static final NoCopySpannableFactory INSTANCE = new NoCopySpannableFactory();
|
||||
}
|
||||
}
|
@ -54,13 +54,20 @@ public class TablePlugin extends AbstractMarkwonPlugin {
|
||||
return new TablePlugin(builder.build());
|
||||
}
|
||||
|
||||
private final TableTheme theme;
|
||||
private final TableVisitor visitor;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
TablePlugin(@NonNull TableTheme tableTheme) {
|
||||
this.theme = tableTheme;
|
||||
this.visitor = new TableVisitor(tableTheme);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TableTheme theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.extensions(Collections.singleton(TablesExtension.create()));
|
||||
|
@ -13,15 +13,19 @@ public class TableTheme {
|
||||
|
||||
@NonNull
|
||||
public static TableTheme create(@NonNull Context context) {
|
||||
final Dip dip = Dip.create(context);
|
||||
return builder()
|
||||
.tableCellPadding(dip.toPx(4))
|
||||
.tableBorderWidth(dip.toPx(1))
|
||||
.build();
|
||||
return buildWithDefaults(context).build();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
public static Builder buildWithDefaults(@NonNull Context context) {
|
||||
final Dip dip = Dip.create(context);
|
||||
return emptyBuilder()
|
||||
.tableCellPadding(dip.toPx(4))
|
||||
.tableBorderWidth(dip.toPx(1));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder emptyBuilder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
@ -58,6 +62,20 @@ public class TableTheme {
|
||||
this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public Builder asBuilder() {
|
||||
return new Builder()
|
||||
.tableCellPadding(tableCellPadding)
|
||||
.tableBorderColor(tableBorderColor)
|
||||
.tableBorderWidth(tableBorderWidth)
|
||||
.tableOddRowBackgroundColor(tableOddRowBackgroundColor)
|
||||
.tableEvenRowBackgroundColor(tableEvenRowBackgroundColor)
|
||||
.tableHeaderRowBackgroundColor(tableHeaderRowBackgroundColor);
|
||||
}
|
||||
|
||||
public int tableCellPadding() {
|
||||
return tableCellPadding;
|
||||
}
|
||||
|
29
markwon-recycler-table/build.gradle
Normal file
@ -0,0 +1,29 @@
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdkVersion config['compile-sdk']
|
||||
buildToolsVersion config['build-tools']
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion config['min-sdk']
|
||||
targetSdkVersion config['target-sdk']
|
||||
versionCode 1
|
||||
versionName version
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
api project(':markwon-core')
|
||||
api project(':markwon-recycler')
|
||||
api project(':markwon-ext-tables')
|
||||
|
||||
deps['test'].with {
|
||||
testImplementation it['junit']
|
||||
testImplementation it['robolectric']
|
||||
testImplementation it['mockito']
|
||||
}
|
||||
}
|
||||
|
||||
registerArtifact(this)
|
4
markwon-recycler-table/gradle.properties
Normal file
@ -0,0 +1,4 @@
|
||||
POM_NAME=Recycler Table
|
||||
POM_ARTIFACT_ID=recycler-table
|
||||
POM_DESCRIPTION=Provides MarkwonAdapter.Entry to render TableBlocks inside Android-native TableLayout widget
|
||||
POM_PACKAGING=aar
|
1
markwon-recycler-table/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1 @@
|
||||
<manifest package="ru.noties.markwon.recycler.table" />
|
@ -0,0 +1,48 @@
|
||||
package ru.noties.markwon.recycler.table;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.Px;
|
||||
|
||||
class TableBorderDrawable extends Drawable {
|
||||
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
|
||||
TableBorderDrawable() {
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
if (paint.getStrokeWidth() > 0) {
|
||||
canvas.drawRect(getBounds(), paint);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(int alpha) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
// no op
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
void update(@Px int borderWidth, @ColorInt int color) {
|
||||
paint.setStrokeWidth(borderWidth);
|
||||
paint.setColor(color);
|
||||
invalidateSelf();
|
||||
}
|
||||
}
|
@ -0,0 +1,530 @@
|
||||
package ru.noties.markwon.recycler.table;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Px;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.ext.tables.Table;
|
||||
import ru.noties.markwon.recycler.MarkwonAdapter;
|
||||
import ru.noties.markwon.utils.NoCopySpannableFactory;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class TableEntry extends MarkwonAdapter.Entry<TableBlock, TableEntry.Holder> {
|
||||
|
||||
public interface Builder {
|
||||
|
||||
/**
|
||||
* @param tableLayoutResId layout with TableLayout
|
||||
* @param tableIdRes id of the TableLayout inside specified layout
|
||||
* @see #tableLayoutIsRoot(int)
|
||||
*/
|
||||
@NonNull
|
||||
Builder tableLayout(@LayoutRes int tableLayoutResId, @IdRes int tableIdRes);
|
||||
|
||||
/**
|
||||
* @param tableLayoutResId layout with TableLayout as the root view
|
||||
* @see #tableLayout(int, int)
|
||||
*/
|
||||
@NonNull
|
||||
Builder tableLayoutIsRoot(@LayoutRes int tableLayoutResId);
|
||||
|
||||
/**
|
||||
* @param textLayoutResId layout with TextView
|
||||
* @param textIdRes id of the TextView inside specified layout
|
||||
* @see #textLayoutIsRoot(int)
|
||||
*/
|
||||
@NonNull
|
||||
Builder textLayout(@LayoutRes int textLayoutResId, @IdRes int textIdRes);
|
||||
|
||||
/**
|
||||
* @param textLayoutResId layout with TextView as the root view
|
||||
* @see #textLayout(int, int)
|
||||
*/
|
||||
@NonNull
|
||||
Builder textLayoutIsRoot(@LayoutRes int textLayoutResId);
|
||||
|
||||
/**
|
||||
* @param cellTextCenterVertical if text inside a table cell should centered
|
||||
* vertically (by default `true`)
|
||||
*/
|
||||
@NonNull
|
||||
Builder cellTextCenterVertical(boolean cellTextCenterVertical);
|
||||
|
||||
/**
|
||||
* @param isRecyclable flag to set on RecyclerView.ViewHolder (by default `true`)
|
||||
*/
|
||||
@NonNull
|
||||
Builder isRecyclable(boolean isRecyclable);
|
||||
|
||||
@NonNull
|
||||
TableEntry build();
|
||||
}
|
||||
|
||||
public interface BuilderConfigure {
|
||||
void configure(@NonNull Builder builder);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
return new BuilderImpl();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static TableEntry create(@NonNull BuilderConfigure configure) {
|
||||
final Builder builder = builder();
|
||||
configure.configure(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private final int tableLayoutResId;
|
||||
private final int tableIdRes;
|
||||
|
||||
private final int textLayoutResId;
|
||||
private final int textIdRes;
|
||||
|
||||
private final boolean isRecyclable;
|
||||
private final boolean cellTextCenterVertical; // by default true
|
||||
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private final Map<TableBlock, Table> map = new HashMap<>(3);
|
||||
|
||||
TableEntry(
|
||||
@LayoutRes int tableLayoutResId,
|
||||
@IdRes int tableIdRes,
|
||||
@LayoutRes int textLayoutResId,
|
||||
@IdRes int textIdRes,
|
||||
boolean isRecyclable,
|
||||
boolean cellTextCenterVertical) {
|
||||
this.tableLayoutResId = tableLayoutResId;
|
||||
this.tableIdRes = tableIdRes;
|
||||
this.textLayoutResId = textLayoutResId;
|
||||
this.textIdRes = textIdRes;
|
||||
this.isRecyclable = isRecyclable;
|
||||
this.cellTextCenterVertical = cellTextCenterVertical;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
||||
return new Holder(
|
||||
isRecyclable,
|
||||
tableIdRes,
|
||||
inflater.inflate(tableLayoutResId, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindHolder(@NonNull Markwon markwon, @NonNull Holder holder, @NonNull TableBlock node) {
|
||||
|
||||
Table table = map.get(node);
|
||||
if (table == null) {
|
||||
table = Table.parse(markwon, node);
|
||||
map.put(node, table);
|
||||
}
|
||||
|
||||
// check if this exact TableBlock was already applied
|
||||
// set tag of tableLayoutResId as it's 100% to be present (we still allow 0 as
|
||||
// tableIdRes if tableLayoutResId has TableLayout as root view)
|
||||
final TableLayout layout = holder.tableLayout;
|
||||
if (table == null
|
||||
|| table == layout.getTag(tableLayoutResId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set this flag to indicate what table instance we current display
|
||||
layout.setTag(tableLayoutResId, table);
|
||||
|
||||
final TableEntryPlugin plugin = markwon.getPlugin(TableEntryPlugin.class);
|
||||
if (plugin == null) {
|
||||
throw new IllegalStateException("No TableEntryPlugin is found. Make sure that it " +
|
||||
"is _used_ whilst configuring Markwon instance");
|
||||
}
|
||||
|
||||
// we must remove unwanted ones (rows and columns)
|
||||
|
||||
final TableEntryTheme theme = plugin.theme();
|
||||
final int borderWidth;
|
||||
final int borderColor;
|
||||
final int cellPadding;
|
||||
{
|
||||
final TextView textView = ensureTextView(layout, 0, 0);
|
||||
borderWidth = theme.tableBorderWidth(textView.getPaint());
|
||||
borderColor = theme.tableBorderColor(textView.getPaint());
|
||||
cellPadding = theme.tableCellPadding();
|
||||
}
|
||||
|
||||
ensureTableBorderBackground(layout, borderWidth, borderColor);
|
||||
|
||||
//noinspection SuspiciousNameCombination
|
||||
// layout.setPadding(borderWidth, borderWidth, borderWidth, borderWidth);
|
||||
// layout.setClipToPadding(borderWidth == 0);
|
||||
|
||||
final List<Table.Row> rows = table.rows();
|
||||
|
||||
final int rowsSize = rows.size();
|
||||
|
||||
// all rows should have equal number of columns
|
||||
final int columnsSize = rowsSize > 0
|
||||
? rows.get(0).columns().size()
|
||||
: 0;
|
||||
|
||||
Table.Row row;
|
||||
Table.Column column;
|
||||
|
||||
TableRow tableRow;
|
||||
|
||||
for (int y = 0; y < rowsSize; y++) {
|
||||
|
||||
row = rows.get(y);
|
||||
tableRow = ensureRow(layout, y);
|
||||
|
||||
for (int x = 0; x < columnsSize; x++) {
|
||||
|
||||
column = row.columns().get(x);
|
||||
|
||||
final TextView textView = ensureTextView(layout, y, x);
|
||||
textView.setGravity(textGravity(column.alignment(), cellTextCenterVertical));
|
||||
textView.getPaint().setFakeBoldText(row.header());
|
||||
|
||||
// apply padding only if not specified in theme (otherwise just use the value from layout)
|
||||
if (cellPadding > 0) {
|
||||
textView.setPadding(cellPadding, cellPadding, cellPadding, cellPadding);
|
||||
}
|
||||
|
||||
ensureTableBorderBackground(textView, borderWidth, borderColor);
|
||||
markwon.setParsedMarkdown(textView, column.content());
|
||||
}
|
||||
|
||||
// row appearance
|
||||
if (row.header()) {
|
||||
tableRow.setBackgroundColor(theme.tableHeaderRowBackgroundColor());
|
||||
} else {
|
||||
// as we currently have no support for tables without head
|
||||
// we shift even/odd calculation a bit (head should not be included in even/odd calculation)
|
||||
final boolean isEven = (y % 2) == 1;
|
||||
if (isEven) {
|
||||
tableRow.setBackgroundColor(theme.tableEvenRowBackgroundColor());
|
||||
} else {
|
||||
// just take first
|
||||
final TextView textView = ensureTextView(layout, y, 0);
|
||||
tableRow.setBackgroundColor(
|
||||
theme.tableOddRowBackgroundColor(textView.getPaint()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// clean up here of un-used rows and columns
|
||||
removeUnused(layout, rowsSize, columnsSize);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TableRow ensureRow(@NonNull TableLayout layout, int row) {
|
||||
|
||||
final int count = layout.getChildCount();
|
||||
|
||||
// fill the requested views until we have added the `row` one
|
||||
if (row >= count) {
|
||||
|
||||
final Context context = layout.getContext();
|
||||
|
||||
int diff = row - count + 1;
|
||||
while (diff > 0) {
|
||||
layout.addView(new TableRow(context));
|
||||
diff -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// return requested child (here it always should be the last one)
|
||||
return (TableRow) layout.getChildAt(row);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView ensureTextView(@NonNull TableLayout layout, int row, int column) {
|
||||
|
||||
final TableRow tableRow = ensureRow(layout, row);
|
||||
final int count = tableRow.getChildCount();
|
||||
|
||||
if (column >= count) {
|
||||
|
||||
final LayoutInflater inflater = ensureInflater(layout.getContext());
|
||||
|
||||
boolean textViewChecked = false;
|
||||
|
||||
View view;
|
||||
TextView textView;
|
||||
ViewGroup.LayoutParams layoutParams;
|
||||
|
||||
int diff = column - count + 1;
|
||||
|
||||
while (diff > 0) {
|
||||
|
||||
view = inflater.inflate(textLayoutResId, tableRow, false);
|
||||
|
||||
// we should have `match_parent` as height (important for borders and text-vertical-align)
|
||||
layoutParams = view.getLayoutParams();
|
||||
if (layoutParams.height != ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
}
|
||||
|
||||
// it will be enough to check only once
|
||||
if (!textViewChecked) {
|
||||
|
||||
if (textIdRes == 0) {
|
||||
if (!(view instanceof TextView)) {
|
||||
final String name = layout.getContext().getResources().getResourceName(textLayoutResId);
|
||||
throw new IllegalStateException(String.format("textLayoutResId(R.layout.%s) " +
|
||||
"has other than TextView root view. Specify TextView ID explicitly", name));
|
||||
}
|
||||
textView = (TextView) view;
|
||||
} else {
|
||||
textView = view.findViewById(textIdRes);
|
||||
if (textView == null) {
|
||||
final Resources r = layout.getContext().getResources();
|
||||
final String layoutName = r.getResourceName(textLayoutResId);
|
||||
final String idName = r.getResourceName(textIdRes);
|
||||
throw new NullPointerException(String.format("textLayoutResId(R.layout.%s) " +
|
||||
"has no TextView found by id(R.id.%s): %s", layoutName, idName, view));
|
||||
}
|
||||
}
|
||||
// mark as checked
|
||||
textViewChecked = true;
|
||||
} else {
|
||||
if (textIdRes == 0) {
|
||||
textView = (TextView) view;
|
||||
} else {
|
||||
textView = view.findViewById(textIdRes);
|
||||
}
|
||||
}
|
||||
|
||||
// we should set SpannableFactory during creation (to avoid another setText method)
|
||||
textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
|
||||
tableRow.addView(textView);
|
||||
|
||||
diff -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
// we can skip all the validation here as we have validated our views whilst inflating them
|
||||
final View last = tableRow.getChildAt(column);
|
||||
if (textIdRes == 0) {
|
||||
return (TextView) last;
|
||||
} else {
|
||||
return last.findViewById(textIdRes);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureTableBorderBackground(@NonNull View view, @Px int borderWidth, @ColorInt int borderColor) {
|
||||
if (borderWidth == 0) {
|
||||
view.setBackground(null);
|
||||
} else {
|
||||
final Drawable drawable = view.getBackground();
|
||||
if (!(drawable instanceof TableBorderDrawable)) {
|
||||
final TableBorderDrawable borderDrawable = new TableBorderDrawable();
|
||||
borderDrawable.update(borderWidth, borderColor);
|
||||
view.setBackground(borderDrawable);
|
||||
} else {
|
||||
((TableBorderDrawable) drawable).update(borderWidth, borderColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private LayoutInflater ensureInflater(@NonNull Context context) {
|
||||
if (inflater == null) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
}
|
||||
return inflater;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@VisibleForTesting
|
||||
static void removeUnused(@NonNull TableLayout layout, int usedRows, int usedColumns) {
|
||||
|
||||
// clean up rows
|
||||
final int rowsCount = layout.getChildCount();
|
||||
if (rowsCount > usedRows) {
|
||||
layout.removeViews(usedRows, (rowsCount - usedRows));
|
||||
}
|
||||
|
||||
// validate columns
|
||||
// here we can use usedRows as children count
|
||||
|
||||
TableRow tableRow;
|
||||
int columnCount;
|
||||
|
||||
for (int i = 0; i < usedRows; i++) {
|
||||
tableRow = (TableRow) layout.getChildAt(i);
|
||||
columnCount = tableRow.getChildCount();
|
||||
if (columnCount > usedColumns) {
|
||||
tableRow.removeViews(usedColumns, (columnCount - usedColumns));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
public static class Holder extends MarkwonAdapter.Holder {
|
||||
|
||||
final TableLayout tableLayout;
|
||||
|
||||
public Holder(boolean isRecyclable, @IdRes int tableLayoutIdRes, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
// we must call this method only once (it's somehow _paired_ inside, so
|
||||
// any call in `onCreateViewHolder` or `onBindViewHolder` will log an error
|
||||
// `isRecyclable decremented below 0` which make little sense here)
|
||||
setIsRecyclable(isRecyclable);
|
||||
|
||||
final TableLayout tableLayout;
|
||||
if (tableLayoutIdRes == 0) {
|
||||
// try to cast directly
|
||||
if (!(itemView instanceof TableLayout)) {
|
||||
throw new IllegalStateException("Root view is not TableLayout. Please provide " +
|
||||
"TableLayout ID explicitly");
|
||||
}
|
||||
tableLayout = (TableLayout) itemView;
|
||||
} else {
|
||||
tableLayout = requireView(tableLayoutIdRes);
|
||||
}
|
||||
this.tableLayout = tableLayout;
|
||||
}
|
||||
}
|
||||
|
||||
// we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@SuppressLint("RtlHardcoded")
|
||||
@VisibleForTesting
|
||||
static int textGravity(@NonNull Table.Alignment alignment, boolean cellTextCenterVertical) {
|
||||
|
||||
final int gravity;
|
||||
|
||||
switch (alignment) {
|
||||
|
||||
case LEFT:
|
||||
gravity = Gravity.LEFT;
|
||||
break;
|
||||
|
||||
case CENTER:
|
||||
gravity = Gravity.CENTER_HORIZONTAL;
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
gravity = Gravity.RIGHT;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unknown table alignment: " + alignment);
|
||||
}
|
||||
|
||||
if (cellTextCenterVertical) {
|
||||
return gravity | Gravity.CENTER_VERTICAL;
|
||||
}
|
||||
|
||||
// do not center vertically
|
||||
return gravity;
|
||||
}
|
||||
|
||||
static class BuilderImpl implements Builder {
|
||||
|
||||
private int tableLayoutResId;
|
||||
private int tableIdRes;
|
||||
|
||||
private int textLayoutResId;
|
||||
private int textIdRes;
|
||||
|
||||
private boolean cellTextCenterVertical = true;
|
||||
|
||||
private boolean isRecyclable = true;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder tableLayout(int tableLayoutResId, int tableIdRes) {
|
||||
this.tableLayoutResId = tableLayoutResId;
|
||||
this.tableIdRes = tableIdRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder tableLayoutIsRoot(int tableLayoutResId) {
|
||||
this.tableLayoutResId = tableLayoutResId;
|
||||
this.tableIdRes = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder textLayout(int textLayoutResId, int textIdRes) {
|
||||
this.textLayoutResId = textLayoutResId;
|
||||
this.textIdRes = textIdRes;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder textLayoutIsRoot(int textLayoutResId) {
|
||||
this.textLayoutResId = textLayoutResId;
|
||||
this.textIdRes = 0;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder cellTextCenterVertical(boolean cellTextCenterVertical) {
|
||||
this.cellTextCenterVertical = cellTextCenterVertical;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder isRecyclable(boolean isRecyclable) {
|
||||
this.isRecyclable = isRecyclable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TableEntry build() {
|
||||
|
||||
if (tableLayoutResId == 0) {
|
||||
throw new IllegalStateException("`tableLayoutResId` argument is required");
|
||||
}
|
||||
|
||||
if (textLayoutResId == 0) {
|
||||
throw new IllegalStateException("`textLayoutResId` argument is required");
|
||||
}
|
||||
|
||||
return new TableEntry(
|
||||
tableLayoutResId, tableIdRes,
|
||||
textLayoutResId, textIdRes,
|
||||
isRecyclable, cellTextCenterVertical
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,65 @@
|
||||
package ru.noties.markwon.recycler.table;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import ru.noties.markwon.AbstractMarkwonPlugin;
|
||||
import ru.noties.markwon.ext.tables.TablePlugin;
|
||||
import ru.noties.markwon.ext.tables.TableTheme;
|
||||
|
||||
/**
|
||||
* This plugin must be used instead of {@link ru.noties.markwon.ext.tables.TablePlugin} when a markdown
|
||||
* table is intended to be used in a RecyclerView via {@link TableEntry}. This is required
|
||||
* because TablePlugin additionally processes markdown tables to be displayed in <em>limited</em>
|
||||
* context of a TextView. If TablePlugin will be used, {@link TableEntry} will display table,
|
||||
* but no content will be present
|
||||
*
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class TableEntryPlugin extends AbstractMarkwonPlugin {
|
||||
|
||||
@NonNull
|
||||
public static TableEntryPlugin create(@NonNull Context context) {
|
||||
final TableTheme tableTheme = TableTheme.create(context);
|
||||
return create(tableTheme);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static TableEntryPlugin create(@NonNull TableTheme tableTheme) {
|
||||
return new TableEntryPlugin(TableEntryTheme.create(tableTheme));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static TableEntryPlugin create(@NonNull TablePlugin.ThemeConfigure themeConfigure) {
|
||||
final TableTheme.Builder builder = new TableTheme.Builder();
|
||||
themeConfigure.configureTheme(builder);
|
||||
return new TableEntryPlugin(new TableEntryTheme(builder));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static TableEntryPlugin create(@NonNull TablePlugin plugin) {
|
||||
return create(plugin.theme());
|
||||
}
|
||||
|
||||
private final TableEntryTheme theme;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
TableEntryPlugin(@NonNull TableEntryTheme tableTheme) {
|
||||
this.theme = tableTheme;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public TableEntryTheme theme() {
|
||||
return theme;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
builder.extensions(Collections.singleton(TablesExtension.create()));
|
||||
}
|
||||
}
|
@ -0,0 +1,67 @@
|
||||
package ru.noties.markwon.recycler.table;
|
||||
|
||||
import android.graphics.Paint;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Px;
|
||||
|
||||
import ru.noties.markwon.ext.tables.TableTheme;
|
||||
import ru.noties.markwon.utils.ColorUtils;
|
||||
|
||||
/**
|
||||
* Mimics TableTheme to allow uniform table customization
|
||||
*
|
||||
* @see #create(TableTheme)
|
||||
* @see TableEntryPlugin
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class TableEntryTheme extends TableTheme {
|
||||
|
||||
@NonNull
|
||||
public static TableEntryTheme create(@NonNull TableTheme tableTheme) {
|
||||
return new TableEntryTheme(tableTheme.asBuilder());
|
||||
}
|
||||
|
||||
protected TableEntryTheme(@NonNull Builder builder) {
|
||||
super(builder);
|
||||
}
|
||||
|
||||
@Px
|
||||
@Override
|
||||
public int tableCellPadding() {
|
||||
return tableCellPadding;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public int tableBorderColor(@NonNull Paint paint) {
|
||||
return tableBorderColor == 0
|
||||
? ColorUtils.applyAlpha(paint.getColor(), TABLE_BORDER_DEF_ALPHA)
|
||||
: tableBorderColor;
|
||||
}
|
||||
|
||||
@Px
|
||||
@Override
|
||||
public int tableBorderWidth(@NonNull Paint paint) {
|
||||
return tableBorderWidth < 0
|
||||
? (int) (paint.getStrokeWidth() + .5F)
|
||||
: tableBorderWidth;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public int tableOddRowBackgroundColor(@NonNull Paint paint) {
|
||||
return tableOddRowBackgroundColor == 0
|
||||
? ColorUtils.applyAlpha(paint.getColor(), TABLE_ODD_ROW_DEF_ALPHA)
|
||||
: tableOddRowBackgroundColor;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public int tableEvenRowBackgroundColor() {
|
||||
return tableEvenRowBackgroundColor;
|
||||
}
|
||||
|
||||
@ColorInt
|
||||
public int tableHeaderRowBackgroundColor() {
|
||||
return tableHeaderRowBackgroundColor;
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
package ru.noties.markwon.recycler.table;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.widget.TableLayout;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.ext.tables.Table;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(manifest = Config.NONE)
|
||||
public class TableEntryTest {
|
||||
|
||||
@Test
|
||||
public void gravity() {
|
||||
// test textGravity is calculated correctly
|
||||
|
||||
final List<Pair<Table.Alignment, Integer>> noVerticalAlign = Arrays.asList(
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.LEFT, Gravity.LEFT),
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL),
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.RIGHT, Gravity.RIGHT)
|
||||
);
|
||||
|
||||
final List<Pair<Table.Alignment, Integer>> withVerticalAlign = Arrays.asList(
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.LEFT, Gravity.LEFT | Gravity.CENTER_VERTICAL),
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.CENTER, Gravity.CENTER_HORIZONTAL | Gravity.CENTER_VERTICAL),
|
||||
new Pair<Table.Alignment, Integer>(Table.Alignment.RIGHT, Gravity.RIGHT | Gravity.CENTER_VERTICAL)
|
||||
);
|
||||
|
||||
for (Pair<Table.Alignment, Integer> pair : noVerticalAlign) {
|
||||
assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, false));
|
||||
}
|
||||
|
||||
for (Pair<Table.Alignment, Integer> pair : withVerticalAlign) {
|
||||
assertEquals(pair.first.name(), pair.second.intValue(), TableEntry.textGravity(pair.first, true));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void holder_no_table_layout_id() {
|
||||
// validate that holder correctly obtains TableLayout instance casting root view
|
||||
|
||||
// root is not TableLayout
|
||||
try {
|
||||
new TableEntry.Holder(false, 0, mock(View.class));
|
||||
fail();
|
||||
} catch (IllegalStateException e) {
|
||||
assertTrue(e.getMessage().contains("Root view is not TableLayout"));
|
||||
}
|
||||
|
||||
// root is TableLayout
|
||||
try {
|
||||
final TableLayout tableLayout = mock(TableLayout.class);
|
||||
final TableEntry.Holder h = new TableEntry.Holder(false, 0, tableLayout);
|
||||
assertEquals(tableLayout, h.tableLayout);
|
||||
} catch (IllegalStateException e) {
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void holder_with_table_layout_id() {
|
||||
|
||||
// not found
|
||||
try {
|
||||
|
||||
final View view = mock(View.class);
|
||||
// resources are used to obtain id name for proper error message
|
||||
when(view.getResources()).thenReturn(mock(Resources.class));
|
||||
new TableEntry.Holder(false, 1, view);
|
||||
fail();
|
||||
} catch (NullPointerException e) {
|
||||
assertTrue(e.getMessage(), e.getMessage().contains("No view with id"));
|
||||
}
|
||||
|
||||
// found
|
||||
try {
|
||||
final TableLayout tableLayout = mock(TableLayout.class);
|
||||
final View view = mock(View.class);
|
||||
when(view.findViewById(3)).thenReturn(tableLayout);
|
||||
final TableEntry.Holder holder = new TableEntry.Holder(false, 3, view);
|
||||
assertEquals(tableLayout, holder.tableLayout);
|
||||
} catch (NullPointerException e) {
|
||||
e.printStackTrace();
|
||||
fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -18,17 +18,13 @@ import ru.noties.markwon.MarkwonReducer;
|
||||
|
||||
/**
|
||||
* Adapter to display markdown in a RecyclerView. It is done by extracting root blocks from a
|
||||
* parsed markdown document and rendering each block in a standalone RecyclerView entry. Provides
|
||||
* parsed markdown document (via {@link MarkwonReducer} and rendering each block in a standalone RecyclerView entry. Provides
|
||||
* ability to customize rendering of blocks. For example display certain blocks in a horizontal
|
||||
* scrolling container or display tables in a specific widget designed for it ({@link Builder#include(Class, Entry)}).
|
||||
* <p>
|
||||
* By default each node will be rendered in a TextView provided by this artifact. It has no styling
|
||||
* information and thus must be replaced with your own layout ({@link Builder#defaultEntry(int)} or
|
||||
* {@link Builder#defaultEntry(Entry)}).
|
||||
*
|
||||
* @see #builder()
|
||||
* @see #create()
|
||||
* @see #create(int)
|
||||
* @see #builder(int, int)
|
||||
* @see #builder(Entry)
|
||||
* @see #create(int, int)
|
||||
* @see #create(Entry)
|
||||
* @see #setMarkdown(Markwon, String)
|
||||
* @see #setParsedMarkdown(Markwon, Node)
|
||||
@ -37,14 +33,34 @@ import ru.noties.markwon.MarkwonReducer;
|
||||
*/
|
||||
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
|
||||
|
||||
@NonNull
|
||||
public static Builder builderTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
|
||||
return builder(SimpleEntry.createTextViewIsRoot(defaultEntryLayoutResId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain {@link Builder} instance.
|
||||
*
|
||||
* @see Builder
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
return new MarkwonAdapterImpl.BuilderImpl();
|
||||
public static Builder builder(
|
||||
@LayoutRes int defaultEntryLayoutResId,
|
||||
@IdRes int defaultEntryTextViewResId
|
||||
) {
|
||||
return builder(SimpleEntry.create(defaultEntryLayoutResId, defaultEntryTextViewResId));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
|
||||
//noinspection unchecked
|
||||
return new MarkwonAdapterImpl.BuilderImpl((Entry<Node, Holder>) defaultEntry);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static MarkwonAdapter createTextViewIsRoot(@LayoutRes int defaultEntryLayoutResId) {
|
||||
return builderTextViewIsRoot(defaultEntryLayoutResId)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -52,12 +68,16 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
||||
* adapter will use default layout for all blocks. Default layout has no styling and should
|
||||
* be specified explicitly.
|
||||
*
|
||||
* @see #create(int)
|
||||
* @see #create(Entry)
|
||||
* @see #builder(int, int)
|
||||
* @see SimpleEntry
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonAdapter create() {
|
||||
return new MarkwonAdapterImpl.BuilderImpl().build();
|
||||
public static MarkwonAdapter create(
|
||||
@LayoutRes int defaultEntryLayoutResId,
|
||||
@IdRes int defaultEntryTextViewResId
|
||||
) {
|
||||
return builder(defaultEntryLayoutResId, defaultEntryTextViewResId).build();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,35 +85,19 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
||||
* nodes.
|
||||
*
|
||||
* @param defaultEntry {@link Entry} to be used for node rendering
|
||||
* @see SimpleEntry
|
||||
* @see #builder(Entry)
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonAdapter create(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
|
||||
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link MarkwonAdapter} that will use supplied layoutResId view
|
||||
* to display all nodes.
|
||||
*
|
||||
* <strong>Please note</strong> that supplied layout must have a TextView inside
|
||||
* with {@code android:id="@+id/text"}
|
||||
*
|
||||
* @param layoutResId layout to be used to display all nodes
|
||||
* @see SimpleEntry
|
||||
*/
|
||||
@NonNull
|
||||
public static MarkwonAdapter create(@LayoutRes int layoutResId) {
|
||||
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build();
|
||||
return builder(defaultEntry).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder to create an instance of {@link MarkwonAdapter}
|
||||
*
|
||||
* @see #include(Class, Entry)
|
||||
* @see #defaultEntry(int)
|
||||
* @see #defaultEntry(Entry)
|
||||
* @see #reducer(MarkwonReducer)
|
||||
* @see #build()
|
||||
*/
|
||||
public interface Builder {
|
||||
|
||||
@ -112,29 +116,6 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
||||
@NonNull Class<N> node,
|
||||
@NonNull Entry<? super N, ? extends Holder> entry);
|
||||
|
||||
/**
|
||||
* Specify which {@link Entry} to use for all non-explicitly registered nodes
|
||||
*
|
||||
* @param defaultEntry {@link Entry}
|
||||
* @return self
|
||||
* @see SimpleEntry
|
||||
*/
|
||||
@NonNull
|
||||
Builder defaultEntry(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry);
|
||||
|
||||
/**
|
||||
* Specify which layout {@link SimpleEntry} will use to render all non-explicitly
|
||||
* registered nodes.
|
||||
*
|
||||
* <strong>Please note</strong> that supplied layout must have a TextView inside
|
||||
* with {@code android:id="@+id/text"}
|
||||
*
|
||||
* @return self
|
||||
* @see SimpleEntry
|
||||
*/
|
||||
@NonNull
|
||||
Builder defaultEntry(@LayoutRes int layoutResId);
|
||||
|
||||
/**
|
||||
* Specify how root Node will be <em>reduced</em> to a list of nodes. There is a default
|
||||
* {@link MarkwonReducer} that will be used if not provided explicitly (there is no need to
|
||||
@ -157,17 +138,27 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
||||
/**
|
||||
* @see SimpleEntry
|
||||
*/
|
||||
public interface Entry<N extends Node, H extends Holder> {
|
||||
public static abstract class Entry<N extends Node, H extends Holder> {
|
||||
|
||||
@NonNull
|
||||
H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
|
||||
public abstract H createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent);
|
||||
|
||||
void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
|
||||
public abstract void bindHolder(@NonNull Markwon markwon, @NonNull H holder, @NonNull N node);
|
||||
|
||||
long id(@NonNull N node);
|
||||
/**
|
||||
* Will be called when new content is available (clear internal cache if any)
|
||||
*/
|
||||
public void clear() {
|
||||
|
||||
// will be called when new content is available (clear internal cache if any)
|
||||
void clear();
|
||||
}
|
||||
|
||||
public long id(@NonNull N node) {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
public void onViewRecycled(@NonNull H holder) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void setMarkdown(@NonNull Markwon markwon, @NonNull String markdown);
|
||||
@ -196,7 +187,15 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
||||
protected <V extends View> V requireView(@IdRes int id) {
|
||||
final V v = itemView.findViewById(id);
|
||||
if (v == null) {
|
||||
throw new NullPointerException();
|
||||
final String name;
|
||||
if (id == 0
|
||||
|| id == View.NO_ID) {
|
||||
name = String.valueOf(id);
|
||||
} else {
|
||||
name = "R.id." + itemView.getResources().getResourceName(id);
|
||||
}
|
||||
throw new NullPointerException(String.format("No view with id(R.id.%s) is found " +
|
||||
"in layout: %s", name, itemView));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
||||
this.entries = entries;
|
||||
this.defaultEntry = defaultEntry;
|
||||
this.reducer = reducer;
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@ -90,6 +91,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
||||
: 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull Holder holder) {
|
||||
super.onViewRecycled(holder);
|
||||
|
||||
final Entry<Node, Holder> entry = getEntry(holder.getItemViewType());
|
||||
entry.onViewRecycled(holder);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@NonNull
|
||||
public List<Node> getItems() {
|
||||
@ -132,9 +141,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
||||
|
||||
private final SparseArray<Entry<Node, Holder>> entries = new SparseArray<>(3);
|
||||
|
||||
private Entry<Node, Holder> defaultEntry;
|
||||
private final Entry<Node, Holder> defaultEntry;
|
||||
|
||||
private MarkwonReducer reducer;
|
||||
|
||||
BuilderImpl(@NonNull Entry<Node, Holder> defaultEntry) {
|
||||
this.defaultEntry = defaultEntry;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <N extends Node> Builder include(
|
||||
@ -145,22 +159,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder defaultEntry(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
|
||||
//noinspection unchecked
|
||||
this.defaultEntry = (Entry<Node, Holder>) defaultEntry;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder defaultEntry(int layoutResId) {
|
||||
//noinspection unchecked
|
||||
this.defaultEntry = (Entry<Node, Holder>) (Entry) new SimpleEntry(layoutResId);
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Builder reducer(@NonNull MarkwonReducer reducer) {
|
||||
@ -172,11 +170,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
||||
@Override
|
||||
public MarkwonAdapter build() {
|
||||
|
||||
if (defaultEntry == null) {
|
||||
//noinspection unchecked
|
||||
defaultEntry = (Entry<Node, Holder>) (Entry) new SimpleEntry();
|
||||
}
|
||||
|
||||
if (reducer == null) {
|
||||
reducer = MarkwonReducer.directChildren();
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
package ru.noties.markwon.recycler;
|
||||
|
||||
import android.support.annotation.IdRes;
|
||||
import android.support.annotation.LayoutRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@ -16,32 +15,43 @@ import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.utils.NoCopySpannableFactory;
|
||||
|
||||
/**
|
||||
* @since 3.0.0
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class SimpleEntry implements MarkwonAdapter.Entry<Node, SimpleEntry.Holder> {
|
||||
public class SimpleEntry extends MarkwonAdapter.Entry<Node, SimpleEntry.Holder> {
|
||||
|
||||
public static final Spannable.Factory NO_COPY_SPANNABLE_FACTORY = new NoCopySpannableFactory();
|
||||
/**
|
||||
* Create {@link SimpleEntry} that has TextView as the root view of
|
||||
* specified layout.
|
||||
*/
|
||||
@NonNull
|
||||
public static SimpleEntry createTextViewIsRoot(@LayoutRes int layoutResId) {
|
||||
return new SimpleEntry(layoutResId, 0);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static SimpleEntry create(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
|
||||
return new SimpleEntry(layoutResId, textViewIdRes);
|
||||
}
|
||||
|
||||
// small cache for already rendered nodes
|
||||
private final Map<Node, Spanned> cache = new HashMap<>();
|
||||
|
||||
private final int layoutResId;
|
||||
private final int textViewIdRes;
|
||||
|
||||
public SimpleEntry() {
|
||||
this(R.layout.markwon_adapter_simple_entry);
|
||||
}
|
||||
|
||||
public SimpleEntry(@LayoutRes int layoutResId) {
|
||||
public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
|
||||
this.layoutResId = layoutResId;
|
||||
this.textViewIdRes = textViewIdRes;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
||||
return new Holder(inflater.inflate(layoutResId, parent, false));
|
||||
return new Holder(textViewIdRes, inflater.inflate(layoutResId, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -54,11 +64,6 @@ public class SimpleEntry implements MarkwonAdapter.Entry<Node, SimpleEntry.Holde
|
||||
markwon.setParsedMarkdown(holder.textView, spanned);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long id(@NonNull Node node) {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
@ -68,21 +73,21 @@ public class SimpleEntry implements MarkwonAdapter.Entry<Node, SimpleEntry.Holde
|
||||
|
||||
final TextView textView;
|
||||
|
||||
protected Holder(@NonNull View itemView) {
|
||||
protected Holder(@IdRes int textViewIdRes, @NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.textView = requireView(R.id.text);
|
||||
this.textView.setSpannableFactory(NO_COPY_SPANNABLE_FACTORY);
|
||||
final TextView textView;
|
||||
if (textViewIdRes == 0) {
|
||||
if (!(itemView instanceof TextView)) {
|
||||
throw new IllegalStateException("TextView is not root of layout " +
|
||||
"(specify TextView ID explicitly): " + itemView);
|
||||
}
|
||||
textView = (TextView) itemView;
|
||||
} else {
|
||||
textView = requireView(textViewIdRes);
|
||||
}
|
||||
|
||||
private static class NoCopySpannableFactory extends Spannable.Factory {
|
||||
|
||||
@Override
|
||||
public Spannable newSpannable(CharSequence source) {
|
||||
return source instanceof Spannable
|
||||
? (Spannable) source
|
||||
: new SpannableString(source);
|
||||
this.textView = textView;
|
||||
this.textView.setSpannableFactory(NoCopySpannableFactory.getInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:lineSpacingExtra="2dip"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
tools:text="# Hello there!" />
|
@ -29,14 +29,6 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
// let's use different res directory so sample will have _isolated_ resources from others
|
||||
res.srcDirs += [ './src/main/recycler/res' ]
|
||||
assets.srcDirs += ['./src/main/recycler/assets']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@ -51,6 +43,7 @@ dependencies {
|
||||
implementation project(':markwon-image-svg')
|
||||
implementation project(':markwon-syntax-highlight')
|
||||
implementation project(':markwon-recycler')
|
||||
implementation project(':markwon-recycler-table')
|
||||
|
||||
deps.with {
|
||||
implementation it['support-recycler-view']
|
||||
|
@ -7,11 +7,9 @@
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
|
||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
|
||||
|
||||
<activity android:name=".MainActivity">
|
||||
<intent-filter>
|
||||
|
1
sample/src/main/assets/README.md
Symbolic link
@ -0,0 +1 @@
|
||||
../../../../README.md
|
@ -11,15 +11,12 @@ import android.support.v7.widget.RecyclerView;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.Collections;
|
||||
|
||||
import ru.noties.debug.AndroidLogDebugOutput;
|
||||
import ru.noties.debug.Debug;
|
||||
@ -33,6 +30,8 @@ import ru.noties.markwon.image.ImagesPlugin;
|
||||
import ru.noties.markwon.image.svg.SvgPlugin;
|
||||
import ru.noties.markwon.recycler.MarkwonAdapter;
|
||||
import ru.noties.markwon.recycler.SimpleEntry;
|
||||
import ru.noties.markwon.recycler.table.TableEntry;
|
||||
import ru.noties.markwon.recycler.table.TableEntryPlugin;
|
||||
import ru.noties.markwon.sample.R;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
||||
import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
||||
@ -51,14 +50,12 @@ public class RecyclerActivity extends Activity {
|
||||
// create MarkwonAdapter and register two blocks that will be rendered differently
|
||||
// * fenced code block (can also specify the same Entry for indended code block)
|
||||
// * table block
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builder()
|
||||
// we can simply use bundled SimpleEntry, that will lookup a TextView
|
||||
// with `@+id/text` id
|
||||
.include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block))
|
||||
// create own implementation of entry for different rendering
|
||||
.include(TableBlock.class, new TableEntry2())
|
||||
// specify default entry (for all other blocks)
|
||||
.defaultEntry(new SimpleEntry(R.layout.adapter_default_entry))
|
||||
final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
|
||||
// we can simply use bundled SimpleEntry
|
||||
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text))
|
||||
.include(TableBlock.class, TableEntry.create(builder -> builder
|
||||
.tableLayout(R.layout.adapter_table_block, R.id.table_layout)
|
||||
.textLayoutIsRoot(R.layout.view_table_entry_cell)))
|
||||
.build();
|
||||
|
||||
final RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
||||
@ -71,10 +68,6 @@ public class RecyclerActivity extends Activity {
|
||||
|
||||
// please note that we should notify updates (adapter doesn't do it implicitly)
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
// NB, there is no currently available widget to render tables gracefully
|
||||
// TableEntryView is here for demonstration purposes only (to show that rendering
|
||||
// tables
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -83,14 +76,8 @@ public class RecyclerActivity extends Activity {
|
||||
.usePlugin(CorePlugin.create())
|
||||
.usePlugin(ImagesPlugin.createWithAssets(context))
|
||||
.usePlugin(SvgPlugin.create(context.getResources()))
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
@Override
|
||||
public void configureParser(@NonNull Parser.Builder builder) {
|
||||
// it's important NOT to use TablePlugin
|
||||
// the only thing we want from it is commonmark-java parser extension
|
||||
builder.extensions(Collections.singleton(TablesExtension.create()));
|
||||
}
|
||||
})
|
||||
// important to use TableEntryPlugin instead of TablePlugin
|
||||
.usePlugin(TableEntryPlugin.create(context))
|
||||
.usePlugin(HtmlPlugin.create())
|
||||
// .usePlugin(SyntaxHighlightPlugin.create())
|
||||
.usePlugin(new AbstractMarkwonPlugin() {
|
||||
|
@ -1,67 +0,0 @@
|
||||
package ru.noties.markwon.sample.recycler;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.debug.Debug;
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.ext.tables.Table;
|
||||
import ru.noties.markwon.recycler.MarkwonAdapter;
|
||||
import ru.noties.markwon.sample.R;
|
||||
|
||||
// do not use in real applications, this is just a showcase
|
||||
public class TableEntry implements MarkwonAdapter.Entry<TableBlock, TableEntry.TableNodeHolder> {
|
||||
|
||||
private final Map<TableBlock, Table> cache = new HashMap<>(2);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
||||
return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) {
|
||||
|
||||
Table table = cache.get(node);
|
||||
if (table == null) {
|
||||
table = Table.parse(markwon, node);
|
||||
cache.put(node, table);
|
||||
}
|
||||
|
||||
Debug.i(table);
|
||||
|
||||
if (table != null) {
|
||||
holder.tableEntryView.setTable(table);
|
||||
// render table
|
||||
} // we need to do something with null table...
|
||||
}
|
||||
|
||||
@Override
|
||||
public long id(@NonNull TableBlock node) {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
cache.clear();
|
||||
}
|
||||
|
||||
static class TableNodeHolder extends MarkwonAdapter.Holder {
|
||||
|
||||
final TableEntryView tableEntryView;
|
||||
|
||||
TableNodeHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.tableEntryView = requireView(R.id.table_entry);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,121 +0,0 @@
|
||||
package ru.noties.markwon.sample.recycler;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TableLayout;
|
||||
import android.widget.TableRow;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import ru.noties.markwon.Markwon;
|
||||
import ru.noties.markwon.ext.tables.Table;
|
||||
import ru.noties.markwon.recycler.MarkwonAdapter;
|
||||
import ru.noties.markwon.sample.R;
|
||||
|
||||
public class TableEntry2 implements MarkwonAdapter.Entry<TableBlock, TableEntry2.TableHolder> {
|
||||
|
||||
private final Map<TableBlock, Table> map = new HashMap<>(3);
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TableHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
||||
return new TableHolder(inflater.inflate(R.layout.adapter_table_block_2, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindHolder(@NonNull Markwon markwon, @NonNull TableHolder holder, @NonNull TableBlock node) {
|
||||
|
||||
Table table = map.get(node);
|
||||
if (table == null) {
|
||||
table = Table.parse(markwon, node);
|
||||
map.put(node, table);
|
||||
}
|
||||
|
||||
// check if this exact TableBlock was already
|
||||
final TableLayout layout = holder.layout;
|
||||
if (table == null
|
||||
|| table == layout.getTag(R.id.table_layout)) {
|
||||
return;
|
||||
}
|
||||
|
||||
layout.setTag(R.id.table_layout, table);
|
||||
layout.removeAllViews();
|
||||
layout.setBackgroundResource(R.drawable.bg_table_cell);
|
||||
|
||||
final Context context = layout.getContext();
|
||||
final LayoutInflater inflater = LayoutInflater.from(context);
|
||||
|
||||
TableRow tableRow;
|
||||
TextView textView;
|
||||
|
||||
for (Table.Row row : table.rows()) {
|
||||
tableRow = new TableRow(context);
|
||||
for (Table.Column column : row.columns()) {
|
||||
textView = (TextView) inflater.inflate(R.layout.view_table_entry_cell, tableRow, false);
|
||||
textView.setGravity(textGravity(column.alignment()));
|
||||
markwon.setParsedMarkdown(textView, column.content());
|
||||
textView.getPaint().setFakeBoldText(row.header());
|
||||
textView.setBackgroundResource(R.drawable.bg_table_cell);
|
||||
tableRow.addView(textView);
|
||||
}
|
||||
layout.addView(tableRow);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long id(@NonNull TableBlock node) {
|
||||
return node.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
map.clear();
|
||||
}
|
||||
|
||||
static class TableHolder extends MarkwonAdapter.Holder {
|
||||
|
||||
final TableLayout layout;
|
||||
|
||||
TableHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.layout = requireView(R.id.table_layout);
|
||||
}
|
||||
}
|
||||
|
||||
// we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private static int textGravity(@NonNull Table.Alignment alignment) {
|
||||
|
||||
final int gravity;
|
||||
|
||||
switch (alignment) {
|
||||
|
||||
case LEFT:
|
||||
gravity = Gravity.LEFT;
|
||||
break;
|
||||
|
||||
case CENTER:
|
||||
gravity = Gravity.CENTER_HORIZONTAL;
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
gravity = Gravity.RIGHT;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unknown table alignment: " + alignment);
|
||||
}
|
||||
|
||||
return gravity | Gravity.CENTER_VERTICAL;
|
||||
}
|
||||
}
|
@ -1,219 +0,0 @@
|
||||
package ru.noties.markwon.sample.recycler;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.SpannedString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import ru.noties.markwon.ext.tables.Table;
|
||||
import ru.noties.markwon.sample.R;
|
||||
|
||||
public class TableEntryView extends LinearLayout {
|
||||
|
||||
// paint and rect to draw borders
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Rect rect = new Rect();
|
||||
|
||||
private LayoutInflater inflater;
|
||||
|
||||
private int rowEvenBackgroundColor;
|
||||
|
||||
public TableEntryView(Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public TableEntryView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
setOrientation(VERTICAL);
|
||||
|
||||
if (attrs != null) {
|
||||
final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TableEntryView);
|
||||
try {
|
||||
|
||||
rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0);
|
||||
|
||||
final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0);
|
||||
|
||||
// half of requested
|
||||
final float strokeWidth = stroke > 0
|
||||
? stroke / 2.F
|
||||
: context.getResources().getDisplayMetrics().density / 2.F;
|
||||
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeWidth(strokeWidth);
|
||||
paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK));
|
||||
|
||||
if (isInEditMode()) {
|
||||
final String data = array.getString(R.styleable.TableEntryView_tev_debugData);
|
||||
if (data != null) {
|
||||
|
||||
boolean first = true;
|
||||
|
||||
final List<Table.Row> rows = new ArrayList<>();
|
||||
for (String row : data.split("\\|")) {
|
||||
final List<Table.Column> columns = new ArrayList<>();
|
||||
for (String column : row.split(",")) {
|
||||
columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column)));
|
||||
}
|
||||
final boolean header = first;
|
||||
first = false;
|
||||
rows.add(new Table.Row(header, columns));
|
||||
}
|
||||
final Table table = new Table(rows);
|
||||
setTable(table);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
public void setTable(@NonNull Table table) {
|
||||
final List<Table.Row> rows = table.rows();
|
||||
for (int i = 0, size = rows.size(); i < size; i++) {
|
||||
addRow(i, rows.get(i));
|
||||
}
|
||||
requestLayout();
|
||||
}
|
||||
|
||||
private void addRow(int index, @NonNull Table.Row row) {
|
||||
|
||||
final ViewGroup group = ensureRow(index);
|
||||
|
||||
final int backgroundColor = !row.header() && (index % 2) == 0
|
||||
? rowEvenBackgroundColor
|
||||
: 0;
|
||||
|
||||
group.setBackgroundColor(backgroundColor);
|
||||
|
||||
final List<Table.Column> columns = row.columns();
|
||||
|
||||
TextView textView;
|
||||
Table.Column column;
|
||||
|
||||
for (int i = 0, size = columns.size(); i < size; i++) {
|
||||
textView = ensureCell(group, i);
|
||||
column = columns.get(i);
|
||||
textView.setGravity(textGravity(column.alignment()));
|
||||
textView.setText(column.content());
|
||||
textView.getPaint().setFakeBoldText(row.header());
|
||||
}
|
||||
|
||||
group.requestLayout();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ViewGroup ensureRow(int index) {
|
||||
|
||||
final int count = getChildCount();
|
||||
if (index >= count) {
|
||||
|
||||
// count=0,index=1, diff=2
|
||||
// count=0,index=5, diff=6
|
||||
// count=1,index=2, diff=2
|
||||
int diff = index - count + 1;
|
||||
while (diff > 0) {
|
||||
addView(inflater.inflate(R.layout.view_table_entry_row, this, false));
|
||||
diff -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return (ViewGroup) getChildAt(index);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private TextView ensureCell(@NonNull ViewGroup group, int index) {
|
||||
|
||||
final int count = group.getChildCount();
|
||||
if (index >= count) {
|
||||
int diff = index - count + 1;
|
||||
while (diff > 0) {
|
||||
group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false));
|
||||
diff -= 1;
|
||||
}
|
||||
}
|
||||
|
||||
return (TextView) group.getChildAt(index);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
final int rows = getChildCount();
|
||||
if (rows == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// first draw the whole border
|
||||
rect.set(0, 0, getWidth(), getHeight());
|
||||
canvas.drawRect(rect, paint);
|
||||
|
||||
ViewGroup group;
|
||||
View view;
|
||||
|
||||
int top;
|
||||
|
||||
for (int row = 0; row < rows; row++) {
|
||||
group = (ViewGroup) getChildAt(row);
|
||||
top = group.getTop();
|
||||
for (int col = 0, cols = group.getChildCount(); col < cols; col++) {
|
||||
view = group.getChildAt(col);
|
||||
rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom());
|
||||
canvas.drawRect(rect, paint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17)
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private static int textGravity(@NonNull Table.Alignment alignment) {
|
||||
|
||||
final int gravity;
|
||||
|
||||
switch (alignment) {
|
||||
|
||||
case LEFT:
|
||||
gravity = Gravity.LEFT;
|
||||
break;
|
||||
|
||||
case CENTER:
|
||||
gravity = Gravity.CENTER_HORIZONTAL;
|
||||
break;
|
||||
|
||||
case RIGHT:
|
||||
gravity = Gravity.RIGHT;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Unknown table alignment: " + alignment);
|
||||
}
|
||||
|
||||
return gravity;
|
||||
}
|
||||
}
|
@ -1 +0,0 @@
|
||||
../../../../../README.md
|
@ -1,21 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingLeft="16dip"
|
||||
android:paddingTop="8dip"
|
||||
android:paddingRight="16dip"
|
||||
android:paddingBottom="8dip"
|
||||
android:scrollbarStyle="outsideInset">
|
||||
|
||||
<ru.noties.markwon.sample.recycler.TableEntryView
|
||||
android:id="@+id/table_entry"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:tev_debugData="head1,head2,head3|col1,col2,col3|col1,col2,col3|col1,col2,col3"
|
||||
app:tev_rowEvenBackgroundColor="#40ff0000" />
|
||||
|
||||
</HorizontalScrollView>
|
@ -1,9 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="TableEntryView">
|
||||
<attr name="tev_rowEvenBackgroundColor" format="color" />
|
||||
<attr name="tev_borderColor" format="color" />
|
||||
<attr name="tev_borderWidth" format="dimension" />
|
||||
<attr name="tev_debugData" format="string" />
|
||||
</declare-styleable>
|
||||
</resources>
|
@ -1,24 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M0,0h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
<path
|
||||
android:pathData="M0,256h512v256h-512z"
|
||||
android:strokeAlpha="0.94117647"
|
||||
android:strokeWidth="0.40000001"
|
||||
android:fillColor="#333333"
|
||||
android:strokeColor="#00000000"
|
||||
android:fillType="nonZero"
|
||||
android:fillAlpha="1"
|
||||
android:strokeLineCap="butt"/>
|
||||
</vector>
|
@ -13,6 +13,7 @@
|
||||
<TableLayout
|
||||
android:id="@+id/table_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:layout_height="wrap_content"
|
||||
android:stretchColumns="*" />
|
||||
|
||||
</HorizontalScrollView>
|
@ -2,9 +2,7 @@
|
||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="8dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||
android:textColor="#000"
|
||||
android:textSize="16sp"
|
@ -1,5 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 6.6 KiB |
@ -10,5 +10,6 @@ include ':app', ':sample',
|
||||
':markwon-image-okhttp',
|
||||
':markwon-image-svg',
|
||||
':markwon-recycler',
|
||||
':markwon-recycler-table',
|
||||
':markwon-syntax-highlight',
|
||||
':markwon-test-span'
|
||||
|