Add recycler-table module
@ -1,4 +1,4 @@
|
|||||||
|
|
||||||
// this is a generated file, do not modify. To update it run 'collectArtifacts.js' script
|
// 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 };
|
export { artifacts };
|
||||||
|
@ -60,6 +60,7 @@ module.exports = {
|
|||||||
'/docs/v3/image/okhttp.md',
|
'/docs/v3/image/okhttp.md',
|
||||||
'/docs/v3/image/svg.md',
|
'/docs/v3/image/svg.md',
|
||||||
'/docs/v3/recycler/',
|
'/docs/v3/recycler/',
|
||||||
|
'/docs/v3/recycler-table/',
|
||||||
'/docs/v3/syntax-highlight/',
|
'/docs/v3/syntax-highlight/',
|
||||||
'/docs/v3/migration-2-3.md'
|
'/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);
|
myTableWidget.setTable(table);
|
||||||
```
|
```
|
||||||
|
|
||||||
Unfortunately Markwon does not provide a widget that can be used for tables. But it does
|
:::tip
|
||||||
provide API that can be used to achieve desired result.
|
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
|
## Theme
|
||||||
|
|
||||||
|
@ -7,4 +7,140 @@ Adds support for GFM (Github-flavored markdown) task-lists:
|
|||||||
```java
|
```java
|
||||||
Markwon.builder(context)
|
Markwon.builder(context)
|
||||||
.usePlugin(TaskListPlugin.create(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 />
|
<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
|
## Snapshot
|
||||||
|
|
||||||
In order to use latest `SNAPSHOT` version add snapshot repository
|
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.content.Context;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -101,6 +102,9 @@ public abstract class Markwon {
|
|||||||
*/
|
*/
|
||||||
public abstract boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> plugin);
|
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}.
|
* Builder for {@link Markwon}.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package ru.noties.markwon;
|
package ru.noties.markwon;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
@ -91,13 +92,19 @@ class MarkwonImpl extends Markwon {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> type) {
|
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) {
|
for (MarkwonPlugin plugin : plugins) {
|
||||||
if (type.isAssignableFrom(plugin.getClass())) {
|
if (type.isAssignableFrom(plugin.getClass())) {
|
||||||
result = true;
|
out = plugin;
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
//noinspection unchecked
|
||||||
|
return (P) out;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,10 @@ import java.util.List;
|
|||||||
*/
|
*/
|
||||||
public abstract class MarkwonReducer {
|
public abstract class MarkwonReducer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return direct children of supplied Node. In the most usual case
|
||||||
|
* will return all BlockNodes of a Document
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonReducer directChildren() {
|
public static MarkwonReducer directChildren() {
|
||||||
return new DirectChildren();
|
return new DirectChildren();
|
||||||
|
@ -34,6 +34,12 @@ public interface MarkwonSpansFactory {
|
|||||||
@NonNull
|
@NonNull
|
||||||
<N extends Node> Builder setFactory(@NonNull Class<N> node, @Nullable SpanFactory factory);
|
<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
|
@NonNull
|
||||||
MarkwonSpansFactory build();
|
MarkwonSpansFactory build();
|
||||||
}
|
}
|
||||||
|
@ -52,6 +52,12 @@ class MarkwonSpansFactoryImpl implements MarkwonSpansFactory {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public <N extends Node> SpanFactory getFactory(@NonNull Class<N> node) {
|
||||||
|
return factories.get(node);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public MarkwonSpansFactory build() {
|
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());
|
return new TablePlugin(builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private final TableTheme theme;
|
||||||
private final TableVisitor visitor;
|
private final TableVisitor visitor;
|
||||||
|
|
||||||
@SuppressWarnings("WeakerAccess")
|
@SuppressWarnings("WeakerAccess")
|
||||||
TablePlugin(@NonNull TableTheme tableTheme) {
|
TablePlugin(@NonNull TableTheme tableTheme) {
|
||||||
|
this.theme = tableTheme;
|
||||||
this.visitor = new TableVisitor(tableTheme);
|
this.visitor = new TableVisitor(tableTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public TableTheme theme() {
|
||||||
|
return theme;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configureParser(@NonNull Parser.Builder builder) {
|
public void configureParser(@NonNull Parser.Builder builder) {
|
||||||
builder.extensions(Collections.singleton(TablesExtension.create()));
|
builder.extensions(Collections.singleton(TablesExtension.create()));
|
||||||
|
@ -13,15 +13,19 @@ public class TableTheme {
|
|||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
public static TableTheme create(@NonNull Context context) {
|
public static TableTheme create(@NonNull Context context) {
|
||||||
final Dip dip = Dip.create(context);
|
return buildWithDefaults(context).build();
|
||||||
return builder()
|
|
||||||
.tableCellPadding(dip.toPx(4))
|
|
||||||
.tableBorderWidth(dip.toPx(1))
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@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();
|
return new Builder();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,6 +62,20 @@ public class TableTheme {
|
|||||||
this.tableHeaderRowBackgroundColor = builder.tableHeaderRowBackgroundColor;
|
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() {
|
public int tableCellPadding() {
|
||||||
return 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
|
* 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
|
* 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)}).
|
* 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 #builder(int, int)
|
||||||
* @see #create()
|
* @see #builder(Entry)
|
||||||
* @see #create(int)
|
* @see #create(int, int)
|
||||||
* @see #create(Entry)
|
* @see #create(Entry)
|
||||||
* @see #setMarkdown(Markwon, String)
|
* @see #setMarkdown(Markwon, String)
|
||||||
* @see #setParsedMarkdown(Markwon, Node)
|
* @see #setParsedMarkdown(Markwon, Node)
|
||||||
@ -37,14 +33,34 @@ import ru.noties.markwon.MarkwonReducer;
|
|||||||
*/
|
*/
|
||||||
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
|
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.
|
* Factory method to obtain {@link Builder} instance.
|
||||||
*
|
*
|
||||||
* @see Builder
|
* @see Builder
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Builder builder() {
|
public static Builder builder(
|
||||||
return new MarkwonAdapterImpl.BuilderImpl();
|
@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
|
* adapter will use default layout for all blocks. Default layout has no styling and should
|
||||||
* be specified explicitly.
|
* be specified explicitly.
|
||||||
*
|
*
|
||||||
* @see #create(int)
|
|
||||||
* @see #create(Entry)
|
* @see #create(Entry)
|
||||||
|
* @see #builder(int, int)
|
||||||
|
* @see SimpleEntry
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonAdapter create() {
|
public static MarkwonAdapter create(
|
||||||
return new MarkwonAdapterImpl.BuilderImpl().build();
|
@LayoutRes int defaultEntryLayoutResId,
|
||||||
|
@IdRes int defaultEntryTextViewResId
|
||||||
|
) {
|
||||||
|
return builder(defaultEntryLayoutResId, defaultEntryTextViewResId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -65,35 +85,19 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
|||||||
* nodes.
|
* nodes.
|
||||||
*
|
*
|
||||||
* @param defaultEntry {@link Entry} to be used for node rendering
|
* @param defaultEntry {@link Entry} to be used for node rendering
|
||||||
* @see SimpleEntry
|
* @see #builder(Entry)
|
||||||
*/
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonAdapter create(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
|
public static MarkwonAdapter create(@NonNull Entry<? extends Node, ? extends Holder> defaultEntry) {
|
||||||
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build();
|
return builder(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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builder to create an instance of {@link MarkwonAdapter}
|
* Builder to create an instance of {@link MarkwonAdapter}
|
||||||
*
|
*
|
||||||
* @see #include(Class, Entry)
|
* @see #include(Class, Entry)
|
||||||
* @see #defaultEntry(int)
|
|
||||||
* @see #defaultEntry(Entry)
|
|
||||||
* @see #reducer(MarkwonReducer)
|
* @see #reducer(MarkwonReducer)
|
||||||
|
* @see #build()
|
||||||
*/
|
*/
|
||||||
public interface Builder {
|
public interface Builder {
|
||||||
|
|
||||||
@ -112,29 +116,6 @@ public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter
|
|||||||
@NonNull Class<N> node,
|
@NonNull Class<N> node,
|
||||||
@NonNull Entry<? super N, ? extends Holder> entry);
|
@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
|
* 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
|
* {@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
|
* @see SimpleEntry
|
||||||
*/
|
*/
|
||||||
public interface Entry<N extends Node, H extends Holder> {
|
public static abstract class Entry<N extends Node, H extends Holder> {
|
||||||
|
|
||||||
@NonNull
|
@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);
|
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) {
|
protected <V extends View> V requireView(@IdRes int id) {
|
||||||
final V v = itemView.findViewById(id);
|
final V v = itemView.findViewById(id);
|
||||||
if (v == null) {
|
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;
|
return v;
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
|||||||
this.entries = entries;
|
this.entries = entries;
|
||||||
this.defaultEntry = defaultEntry;
|
this.defaultEntry = defaultEntry;
|
||||||
this.reducer = reducer;
|
this.reducer = reducer;
|
||||||
|
|
||||||
setHasStableIds(true);
|
setHasStableIds(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +91,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
|||||||
: 0;
|
: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewRecycled(@NonNull Holder holder) {
|
||||||
|
super.onViewRecycled(holder);
|
||||||
|
|
||||||
|
final Entry<Node, Holder> entry = getEntry(holder.getItemViewType());
|
||||||
|
entry.onViewRecycled(holder);
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unused")
|
@SuppressWarnings("unused")
|
||||||
@NonNull
|
@NonNull
|
||||||
public List<Node> getItems() {
|
public List<Node> getItems() {
|
||||||
@ -132,9 +141,14 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
|||||||
|
|
||||||
private final SparseArray<Entry<Node, Holder>> entries = new SparseArray<>(3);
|
private final SparseArray<Entry<Node, Holder>> entries = new SparseArray<>(3);
|
||||||
|
|
||||||
private Entry<Node, Holder> defaultEntry;
|
private final Entry<Node, Holder> defaultEntry;
|
||||||
|
|
||||||
private MarkwonReducer reducer;
|
private MarkwonReducer reducer;
|
||||||
|
|
||||||
|
BuilderImpl(@NonNull Entry<Node, Holder> defaultEntry) {
|
||||||
|
this.defaultEntry = defaultEntry;
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public <N extends Node> Builder include(
|
public <N extends Node> Builder include(
|
||||||
@ -145,22 +159,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
|||||||
return this;
|
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
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Builder reducer(@NonNull MarkwonReducer reducer) {
|
public Builder reducer(@NonNull MarkwonReducer reducer) {
|
||||||
@ -172,11 +170,6 @@ class MarkwonAdapterImpl extends MarkwonAdapter {
|
|||||||
@Override
|
@Override
|
||||||
public MarkwonAdapter build() {
|
public MarkwonAdapter build() {
|
||||||
|
|
||||||
if (defaultEntry == null) {
|
|
||||||
//noinspection unchecked
|
|
||||||
defaultEntry = (Entry<Node, Holder>) (Entry) new SimpleEntry();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reducer == null) {
|
if (reducer == null) {
|
||||||
reducer = MarkwonReducer.directChildren();
|
reducer = MarkwonReducer.directChildren();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
package ru.noties.markwon.recycler;
|
package ru.noties.markwon.recycler;
|
||||||
|
|
||||||
|
import android.support.annotation.IdRes;
|
||||||
import android.support.annotation.LayoutRes;
|
import android.support.annotation.LayoutRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -16,32 +15,43 @@ import java.util.HashMap;
|
|||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import ru.noties.markwon.Markwon;
|
import ru.noties.markwon.Markwon;
|
||||||
|
import ru.noties.markwon.utils.NoCopySpannableFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @since 3.0.0
|
* @since 3.0.0
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("WeakerAccess")
|
@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
|
// small cache for already rendered nodes
|
||||||
private final Map<Node, Spanned> cache = new HashMap<>();
|
private final Map<Node, Spanned> cache = new HashMap<>();
|
||||||
|
|
||||||
private final int layoutResId;
|
private final int layoutResId;
|
||||||
|
private final int textViewIdRes;
|
||||||
|
|
||||||
public SimpleEntry() {
|
public SimpleEntry(@LayoutRes int layoutResId, @IdRes int textViewIdRes) {
|
||||||
this(R.layout.markwon_adapter_simple_entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
public SimpleEntry(@LayoutRes int layoutResId) {
|
|
||||||
this.layoutResId = layoutResId;
|
this.layoutResId = layoutResId;
|
||||||
|
this.textViewIdRes = textViewIdRes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Holder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
|
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
|
@Override
|
||||||
@ -54,11 +64,6 @@ public class SimpleEntry implements MarkwonAdapter.Entry<Node, SimpleEntry.Holde
|
|||||||
markwon.setParsedMarkdown(holder.textView, spanned);
|
markwon.setParsedMarkdown(holder.textView, spanned);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public long id(@NonNull Node node) {
|
|
||||||
return node.hashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void clear() {
|
public void clear() {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
@ -68,21 +73,21 @@ public class SimpleEntry implements MarkwonAdapter.Entry<Node, SimpleEntry.Holde
|
|||||||
|
|
||||||
final TextView textView;
|
final TextView textView;
|
||||||
|
|
||||||
protected Holder(@NonNull View itemView) {
|
protected Holder(@IdRes int textViewIdRes, @NonNull View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
|
||||||
this.textView = requireView(R.id.text);
|
final TextView textView;
|
||||||
this.textView.setSpannableFactory(NO_COPY_SPANNABLE_FACTORY);
|
if (textViewIdRes == 0) {
|
||||||
}
|
if (!(itemView instanceof TextView)) {
|
||||||
}
|
throw new IllegalStateException("TextView is not root of layout " +
|
||||||
|
"(specify TextView ID explicitly): " + itemView);
|
||||||
private static class NoCopySpannableFactory extends Spannable.Factory {
|
}
|
||||||
|
textView = (TextView) itemView;
|
||||||
@Override
|
} else {
|
||||||
public Spannable newSpannable(CharSequence source) {
|
textView = requireView(textViewIdRes);
|
||||||
return source instanceof Spannable
|
}
|
||||||
? (Spannable) source
|
this.textView = textView;
|
||||||
: new SpannableString(source);
|
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
|
targetCompatibility JavaVersion.VERSION_1_8
|
||||||
sourceCompatibility 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 {
|
dependencies {
|
||||||
@ -51,6 +43,7 @@ dependencies {
|
|||||||
implementation project(':markwon-image-svg')
|
implementation project(':markwon-image-svg')
|
||||||
implementation project(':markwon-syntax-highlight')
|
implementation project(':markwon-syntax-highlight')
|
||||||
implementation project(':markwon-recycler')
|
implementation project(':markwon-recycler')
|
||||||
|
implementation project(':markwon-recycler-table')
|
||||||
|
|
||||||
deps.with {
|
deps.with {
|
||||||
implementation it['support-recycler-view']
|
implementation it['support-recycler-view']
|
||||||
|
@ -7,11 +7,9 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:roundIcon="@mipmap/ic_launcher"
|
|
||||||
android:theme="@style/AppTheme"
|
android:theme="@style/AppTheme"
|
||||||
tools:ignore="AllowBackup,GoogleAppIndexingWarning">
|
tools:ignore="AllowBackup,GoogleAppIndexingWarning,MissingApplicationIcon">
|
||||||
|
|
||||||
<activity android:name=".MainActivity">
|
<activity android:name=".MainActivity">
|
||||||
<intent-filter>
|
<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 android.text.TextUtils;
|
||||||
|
|
||||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||||
import org.commonmark.ext.gfm.tables.TablesExtension;
|
|
||||||
import org.commonmark.node.FencedCodeBlock;
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
import org.commonmark.parser.Parser;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.InputStreamReader;
|
import java.io.InputStreamReader;
|
||||||
import java.util.Collections;
|
|
||||||
|
|
||||||
import ru.noties.debug.AndroidLogDebugOutput;
|
import ru.noties.debug.AndroidLogDebugOutput;
|
||||||
import ru.noties.debug.Debug;
|
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.image.svg.SvgPlugin;
|
||||||
import ru.noties.markwon.recycler.MarkwonAdapter;
|
import ru.noties.markwon.recycler.MarkwonAdapter;
|
||||||
import ru.noties.markwon.recycler.SimpleEntry;
|
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.sample.R;
|
||||||
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
import ru.noties.markwon.urlprocessor.UrlProcessor;
|
||||||
import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute;
|
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
|
// create MarkwonAdapter and register two blocks that will be rendered differently
|
||||||
// * fenced code block (can also specify the same Entry for indended code block)
|
// * fenced code block (can also specify the same Entry for indended code block)
|
||||||
// * table block
|
// * table block
|
||||||
final MarkwonAdapter adapter = MarkwonAdapter.builder()
|
final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_default_entry, R.id.text)
|
||||||
// we can simply use bundled SimpleEntry, that will lookup a TextView
|
// we can simply use bundled SimpleEntry
|
||||||
// with `@+id/text` id
|
.include(FencedCodeBlock.class, SimpleEntry.create(R.layout.adapter_fenced_code_block, R.id.text))
|
||||||
.include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block))
|
.include(TableBlock.class, TableEntry.create(builder -> builder
|
||||||
// create own implementation of entry for different rendering
|
.tableLayout(R.layout.adapter_table_block, R.id.table_layout)
|
||||||
.include(TableBlock.class, new TableEntry2())
|
.textLayoutIsRoot(R.layout.view_table_entry_cell)))
|
||||||
// specify default entry (for all other blocks)
|
|
||||||
.defaultEntry(new SimpleEntry(R.layout.adapter_default_entry))
|
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
final RecyclerView recyclerView = findViewById(R.id.recycler_view);
|
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)
|
// please note that we should notify updates (adapter doesn't do it implicitly)
|
||||||
adapter.notifyDataSetChanged();
|
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
|
@NonNull
|
||||||
@ -83,14 +76,8 @@ public class RecyclerActivity extends Activity {
|
|||||||
.usePlugin(CorePlugin.create())
|
.usePlugin(CorePlugin.create())
|
||||||
.usePlugin(ImagesPlugin.createWithAssets(context))
|
.usePlugin(ImagesPlugin.createWithAssets(context))
|
||||||
.usePlugin(SvgPlugin.create(context.getResources()))
|
.usePlugin(SvgPlugin.create(context.getResources()))
|
||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
// important to use TableEntryPlugin instead of TablePlugin
|
||||||
@Override
|
.usePlugin(TableEntryPlugin.create(context))
|
||||||
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()));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.usePlugin(HtmlPlugin.create())
|
.usePlugin(HtmlPlugin.create())
|
||||||
// .usePlugin(SyntaxHighlightPlugin.create())
|
// .usePlugin(SyntaxHighlightPlugin.create())
|
||||||
.usePlugin(new AbstractMarkwonPlugin() {
|
.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
|
<TableLayout
|
||||||
android:id="@+id/table_layout"
|
android:id="@+id/table_layout"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content" />
|
android:layout_height="wrap_content"
|
||||||
|
android:stretchColumns="*" />
|
||||||
|
|
||||||
</HorizontalScrollView>
|
</HorizontalScrollView>
|
@ -2,9 +2,7 @@
|
|||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:padding="8dip"
|
|
||||||
android:textAppearance="?android:attr/textAppearanceMedium"
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
android:textColor="#000"
|
android:textColor="#000"
|
||||||
android:textSize="16sp"
|
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-okhttp',
|
||||||
':markwon-image-svg',
|
':markwon-image-svg',
|
||||||
':markwon-recycler',
|
':markwon-recycler',
|
||||||
|
':markwon-recycler-table',
|
||||||
':markwon-syntax-highlight',
|
':markwon-syntax-highlight',
|
||||||
':markwon-test-span'
|
':markwon-test-span'
|
||||||
|