ext-tables, table aware movement method

This commit is contained in:
Dimitry Ivanov 2020-08-31 23:03:51 +03:00
parent 4c3fba8929
commit f8eaac6197
6 changed files with 205 additions and 14 deletions

View File

@ -2,12 +2,16 @@
# SNAPSHOT # SNAPSHOT
#### Added
* `ext-tables` - `TableAwareMovementMethod` a special movement method to handle clicks inside tables ([#289])
#### Changed #### Changed
* `image-glide` - update to `4.11.0` version * `image-glide` - update to `4.11.0` version
* `inline-parser` - revert parsing index when `InlineProcessor` returns `null` as result * `inline-parser` - revert parsing index when `InlineProcessor` returns `null` as result
* `image-coil` - update `Coil` to `0.12.0` ([Coil changelog](https://coil-kt.github.io/coil/changelog/)) ([#284])<br>Thanks [@magnusvs] * `image-coil` - update `Coil` to `0.12.0` ([Coil changelog](https://coil-kt.github.io/coil/changelog/)) ([#284])<br>Thanks [@magnusvs]
[#284]: https://github.com/noties/Markwon/pull/284 [#284]: https://github.com/noties/Markwon/pull/284
[#289]: https://github.com/noties/Markwon/issues/289
[@magnusvs]: https://github.com/magnusvs [@magnusvs]: https://github.com/magnusvs

View File

@ -216,7 +216,7 @@
"javaClassName": "io.noties.markwon.app.samples.table.TableLinkifySample", "javaClassName": "io.noties.markwon.app.samples.table.TableLinkifySample",
"id": "20200702135739", "id": "20200702135739",
"title": "Linkify table", "title": "Linkify table",
"description": "Automatically linkify markdown content including content inside tables", "description": "Automatically linkify markdown content including content inside tables, handle clicks inside tables",
"artifacts": [ "artifacts": [
"EXT_TABLES", "EXT_TABLES",
"LINKIFY" "LINKIFY"
@ -894,12 +894,13 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample",
"id": "20200629162024", "id": "20200629162024",
"title": "User mention and issue (via text)", "title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [ "artifacts": [
"CORE" "CORE",
"INLINE_PARSER"
], ],
"tags": [ "tags": [
"parsing", "parsing",
@ -908,13 +909,12 @@
] ]
}, },
{ {
"javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample",
"id": "20200629162024", "id": "20200629162024",
"title": "User mention and issue (via text)", "title": "User mention and issue (via text)",
"description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`",
"artifacts": [ "artifacts": [
"CORE", "CORE"
"INLINE_PARSER"
], ],
"tags": [ "tags": [
"parsing", "parsing",

View File

@ -3,8 +3,10 @@ package io.noties.markwon.app.samples.table;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.app.sample.Tags; import io.noties.markwon.app.sample.Tags;
import io.noties.markwon.app.sample.ui.MarkwonTextViewSample; import io.noties.markwon.app.sample.ui.MarkwonTextViewSample;
import io.noties.markwon.ext.tables.TableAwareMovementMethod;
import io.noties.markwon.ext.tables.TablePlugin; import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.linkify.LinkifyPlugin; import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.movement.MovementMethodPlugin;
import io.noties.markwon.sample.annotations.MarkwonArtifact; import io.noties.markwon.sample.annotations.MarkwonArtifact;
import io.noties.markwon.sample.annotations.MarkwonSampleInfo; import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
@ -12,7 +14,7 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo;
id = "20200702135739", id = "20200702135739",
title = "Linkify table", title = "Linkify table",
description = "Automatically linkify markdown content " + description = "Automatically linkify markdown content " +
"including content inside tables", "including content inside tables, handle clicks inside tables",
artifacts = {MarkwonArtifact.EXT_TABLES, MarkwonArtifact.LINKIFY}, artifacts = {MarkwonArtifact.EXT_TABLES, MarkwonArtifact.LINKIFY},
tags = {Tags.links} tags = {Tags.links}
) )
@ -23,10 +25,11 @@ public class TableLinkifySample extends MarkwonTextViewSample {
"| HEADER | HEADER | HEADER |\n" + "| HEADER | HEADER | HEADER |\n" +
"|:----:|:----:|:----:|\n" + "|:----:|:----:|:----:|\n" +
"| 测试 | 测试 | 测试 |\n" + "| 测试 | 测试 | 测试 |\n" +
"| 测试 | 测试 | 测测测12345试测试测试 |\n" + "| 测试 | 测试 | 测测测12345试测试测试 |\n" +
"| 测试 | 测试 | 123445 |\n" + "| 测试 | 测试 | 123445 |\n" +
"| 测试 | 测试 | (650) 555-1212 |\n" + "| 测试 | 测试 | (650) 555-1212 |\n" +
"| 测试 | 测试 | [link](#) |\n" + "| 测试 | 测试 | mail@ma.il |\n" +
"| 测试 | 测试 | some text that goes here is very very very very important [link](https://noties.io/Markwon) |\n" +
"\n" + "\n" +
"测试\n" + "测试\n" +
"\n" + "\n" +
@ -35,6 +38,8 @@ public class TableLinkifySample extends MarkwonTextViewSample {
final Markwon markwon = Markwon.builder(context) final Markwon markwon = Markwon.builder(context)
.usePlugin(LinkifyPlugin.create()) .usePlugin(LinkifyPlugin.create())
.usePlugin(TablePlugin.create(context)) .usePlugin(TablePlugin.create(context))
// use TableAwareLinkMovementMethod to handle clicks inside tables
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.build(); .build();
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, md);

View File

@ -40,13 +40,26 @@ final Markwon markwon = Markwon.builder(context)
)) ))
``` ```
Please note, that _by default_ tables have limitations. For example, there is no support Please note, that _by default_ tables have limitations. For example, table contents won't be copied to clipboard if a TextView
for images inside table cells. And table contents won't be copied to clipboard if a TextView
has such functionality. Table will always take full width of a TextView in which it is displayed. has such functionality. Table will always take full width of a TextView in which it is displayed.
All columns will always be of the same width. So, _default_ implementation provides basic All columns will always be of the same width. So, _default_ implementation provides basic
functionality which can answer some needs. These all come from the limited nature of the TextView functionality which can answer some needs. These all come from the limited nature of the TextView
to display such content. to display such content.
:::warning
If table contains links a special `MovementMethod` must be used on a `TextView` widget - `TableAwareMovementMethod`,
for example with `MovementMethodPlugin`:
```java
final Markwon markwon = Markwon.builder(context)
.usePlugin(LinkifyPlugin.create())
.usePlugin(TablePlugin.create(context))
// use TableAwareLinkMovementMethod to handle clicks inside tables,
// wraps LinkMovementMethod internally
.usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create()))
.build();
```
:::
In order to provide full-fledged experience, tables must be displayed in a special widget. In order to provide full-fledged experience, tables must be displayed in a special widget.
Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows Since version `3.0.0` Markwon provides a special artifact `markwon-recycler` that allows
to render markdown in a set of widgets in a RecyclerView. It also gives ability to change to render markdown in a set of widgets in a RecyclerView. It also gives ability to change

View File

@ -0,0 +1,131 @@
package io.noties.markwon.ext.tables;
import android.text.Layout;
import android.text.Spannable;
import android.text.Spanned;
import android.text.method.LinkMovementMethod;
import android.text.method.MovementMethod;
import android.text.style.ClickableSpan;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.widget.TextView;
import androidx.annotation.NonNull;
/**
* @since $SNAPSHOT;
*/
public class TableAwareMovementMethod implements MovementMethod {
@NonNull
public static TableAwareMovementMethod wrap(@NonNull MovementMethod movementMethod) {
return new TableAwareMovementMethod(movementMethod);
}
/**
* Wraps LinkMovementMethod
*/
@NonNull
public static TableAwareMovementMethod create() {
return new TableAwareMovementMethod(LinkMovementMethod.getInstance());
}
public static boolean handleTableRowTouchEvent(
@NonNull TextView widget,
@NonNull Spannable buffer,
@NonNull MotionEvent event) {
// handle only action up (originally action down is used in order to handle selection,
// which tables do no have)
if (event.getAction() != MotionEvent.ACTION_UP) {
return false;
}
int x = (int) event.getX();
int y = (int) event.getY();
x -= widget.getTotalPaddingLeft();
y -= widget.getTotalPaddingTop();
x += widget.getScrollX();
y += widget.getScrollY();
final Layout layout = widget.getLayout();
final int line = layout.getLineForVertical(y);
final int off = layout.getOffsetForHorizontal(line, x);
final TableRowSpan[] spans = buffer.getSpans(off, off, TableRowSpan.class);
if (spans.length == 0) {
return false;
}
final TableRowSpan span = spans[0];
// okay, we can calculate the x to obtain span, but what about y?
final Layout rowLayout = span.findLayoutForHorizontalOffset(x);
if (rowLayout != null) {
// line top as basis
final int rowY = layout.getLineTop(line);
final int rowLine = rowLayout.getLineForVertical(y - rowY);
final int rowOffset = rowLayout.getOffsetForHorizontal(rowLine, x % span.cellWidth());
final ClickableSpan[] rowClickableSpans = ((Spanned) rowLayout.getText())
.getSpans(rowOffset, rowOffset, ClickableSpan.class);
if (rowClickableSpans.length > 0) {
rowClickableSpans[0].onClick(widget);
return true;
}
}
return false;
}
private final MovementMethod wrapped;
public TableAwareMovementMethod(@NonNull MovementMethod wrapped) {
this.wrapped = wrapped;
}
@Override
public void initialize(TextView widget, Spannable text) {
wrapped.initialize(widget, text);
}
@Override
public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) {
return wrapped.onKeyDown(widget, text, keyCode, event);
}
@Override
public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) {
return wrapped.onKeyUp(widget, text, keyCode, event);
}
@Override
public boolean onKeyOther(TextView view, Spannable text, KeyEvent event) {
return wrapped.onKeyOther(view, text, event);
}
@Override
public void onTakeFocus(TextView widget, Spannable text, int direction) {
wrapped.onTakeFocus(widget, text, direction);
}
@Override
public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) {
return wrapped.onTrackballEvent(widget, text, event);
}
@Override
public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) {
// let wrapped handle first, then if super handles nothing, search for table row spans
return wrapped.onTouchEvent(widget, buffer, event)
|| handleTableRowTouchEvent(widget, buffer, event);
}
@Override
public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) {
return wrapped.onGenericMotionEvent(widget, text, event);
}
@Override
public boolean canSelectArbitrarily() {
return wrapped.canSelectArbitrarily();
}
}

View File

@ -63,6 +63,7 @@ public class TableRowSpan extends ReplacementSpan {
return text; return text;
} }
@NonNull
@Override @Override
public String toString() { public String toString() {
return "Cell{" + return "Cell{" +
@ -170,7 +171,10 @@ public class TableRowSpan extends ReplacementSpan {
final int size = layouts.size(); final int size = layouts.size();
final int w = (int) (1F * width / size + 0.5F); final int w = cellWidth(size);
// @since $SNAPSHOT; roundingDiff to offset last vertical border
final int roundingDiff = w - (width / size);
// @since 1.1.1 // @since 1.1.1
// draw backgrounds // draw backgrounds
@ -264,7 +268,13 @@ public class TableRowSpan extends ReplacementSpan {
canvas.drawRect(rect, paint); canvas.drawRect(rect, paint);
if (i == (size - 1)) { if (i == (size - 1)) {
rect.set(w - borderWidth, borderTop, w, borderBottom); // @since $SNAPSHOT; subtract rounding offset for the last vertical divider
rect.set(
w - borderWidth - roundingDiff,
borderTop,
w - roundingDiff,
borderBottom
);
canvas.drawRect(rect, paint); canvas.drawRect(rect, paint);
} }
} }
@ -298,7 +308,7 @@ public class TableRowSpan extends ReplacementSpan {
final int columns = cells.size(); final int columns = cells.size();
final int padding = theme.tableCellPadding() * 2; final int padding = theme.tableCellPadding() * 2;
final int w = (width / columns) - padding; final int w = cellWidth(columns) - padding;
this.layouts.clear(); this.layouts.clear();
@ -374,6 +384,34 @@ public class TableRowSpan extends ReplacementSpan {
} }
} }
/**
* Obtain Layout given horizontal offset. Primary usage target - MovementMethod
*
* @since $SNAPSHOT;
*/
@Nullable
public Layout findLayoutForHorizontalOffset(int x) {
final int size = layouts.size();
final int w = cellWidth(size);
final int i = x / w;
if (i >= size) {
return null;
}
return layouts.get(i);
}
/**
* @since $SNAPSHOT;
*/
public int cellWidth() {
return cellWidth(layouts.size());
}
// @since $SNAPSHOT;
protected int cellWidth(int size) {
return (int) (1F * width / size + 0.5F);
}
@SuppressLint("SwitchIntDef") @SuppressLint("SwitchIntDef")
private static Layout.Alignment alignment(@Alignment int alignment) { private static Layout.Alignment alignment(@Alignment int alignment) {
final Layout.Alignment out; final Layout.Alignment out;