diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a447b1a..96183500 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,16 @@ # SNAPSHOT +#### Added +* `ext-tables` - `TableAwareMovementMethod` a special movement method to handle clicks inside tables ([#289]) + #### Changed * `image-glide` - update to `4.11.0` version * `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])
Thanks [@magnusvs] [#284]: https://github.com/noties/Markwon/pull/284 +[#289]: https://github.com/noties/Markwon/issues/289 [@magnusvs]: https://github.com/magnusvs diff --git a/app-sample/samples.json b/app-sample/samples.json index cedd7150..f88dad8e 100644 --- a/app-sample/samples.json +++ b/app-sample/samples.json @@ -216,7 +216,7 @@ "javaClassName": "io.noties.markwon.app.samples.table.TableLinkifySample", "id": "20200702135739", "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": [ "EXT_TABLES", "LINKIFY" @@ -894,12 +894,13 @@ ] }, { - "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", + "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", "id": "20200629162024", "title": "User mention and issue (via text)", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "artifacts": [ - "CORE" + "CORE", + "INLINE_PARSER" ], "tags": [ "parsing", @@ -908,13 +909,12 @@ ] }, { - "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueInlineParsingSample", + "javaClassName": "io.noties.markwon.app.samples.GithubUserIssueOnTextAddedSample", "id": "20200629162024", "title": "User mention and issue (via text)", "description": "Github-like user mention and issue rendering via `CorePlugin.OnTextAddedListener`", "artifacts": [ - "CORE", - "INLINE_PARSER" + "CORE" ], "tags": [ "parsing", diff --git a/app-sample/src/main/java/io/noties/markwon/app/samples/table/TableLinkifySample.java b/app-sample/src/main/java/io/noties/markwon/app/samples/table/TableLinkifySample.java index ae210a68..f599a393 100644 --- a/app-sample/src/main/java/io/noties/markwon/app/samples/table/TableLinkifySample.java +++ b/app-sample/src/main/java/io/noties/markwon/app/samples/table/TableLinkifySample.java @@ -3,8 +3,10 @@ package io.noties.markwon.app.samples.table; import io.noties.markwon.Markwon; import io.noties.markwon.app.sample.Tags; 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.linkify.LinkifyPlugin; +import io.noties.markwon.movement.MovementMethodPlugin; import io.noties.markwon.sample.annotations.MarkwonArtifact; import io.noties.markwon.sample.annotations.MarkwonSampleInfo; @@ -12,7 +14,7 @@ import io.noties.markwon.sample.annotations.MarkwonSampleInfo; id = "20200702135739", title = "Linkify table", description = "Automatically linkify markdown content " + - "including content inside tables", + "including content inside tables, handle clicks inside tables", artifacts = {MarkwonArtifact.EXT_TABLES, MarkwonArtifact.LINKIFY}, tags = {Tags.links} ) @@ -23,10 +25,11 @@ public class TableLinkifySample extends MarkwonTextViewSample { "| HEADER | HEADER | HEADER |\n" + "|:----:|:----:|:----:|\n" + "| 测试 | 测试 | 测试 |\n" + - "| 测试 | 测试 | 测测测12345试测试测试 |\n" + + "| 测试 | 测试 | 测测测12345试测试测试 |\n" + "| 测试 | 测试 | 123445 |\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" + @@ -35,6 +38,8 @@ public class TableLinkifySample extends MarkwonTextViewSample { final Markwon markwon = Markwon.builder(context) .usePlugin(LinkifyPlugin.create()) .usePlugin(TablePlugin.create(context)) + // use TableAwareLinkMovementMethod to handle clicks inside tables + .usePlugin(MovementMethodPlugin.create(TableAwareMovementMethod.create())) .build(); markwon.setMarkdown(textView, md); diff --git a/docs/docs/v4/ext-tables/README.md b/docs/docs/v4/ext-tables/README.md index 210928b3..81f4c87c 100644 --- a/docs/docs/v4/ext-tables/README.md +++ b/docs/docs/v4/ext-tables/README.md @@ -40,13 +40,26 @@ final Markwon markwon = Markwon.builder(context) )) ``` -Please note, that _by default_ tables have limitations. For example, there is no support -for images inside table cells. And table contents won't be copied to clipboard if a TextView +Please note, that _by default_ tables have limitations. For example, 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. 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 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. 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 diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableAwareMovementMethod.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableAwareMovementMethod.java new file mode 100644 index 00000000..a178d719 --- /dev/null +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableAwareMovementMethod.java @@ -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(); + } +} diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java index 9b6d1876..52c07cbf 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowSpan.java @@ -63,6 +63,7 @@ public class TableRowSpan extends ReplacementSpan { return text; } + @NonNull @Override public String toString() { return "Cell{" + @@ -170,7 +171,10 @@ public class TableRowSpan extends ReplacementSpan { 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 // draw backgrounds @@ -264,7 +268,13 @@ public class TableRowSpan extends ReplacementSpan { canvas.drawRect(rect, paint); 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); } } @@ -298,7 +308,7 @@ public class TableRowSpan extends ReplacementSpan { final int columns = cells.size(); final int padding = theme.tableCellPadding() * 2; - final int w = (width / columns) - padding; + final int w = cellWidth(columns) - padding; 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") private static Layout.Alignment alignment(@Alignment int alignment) { final Layout.Alignment out;