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
#### 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])<br>Thanks [@magnusvs]
[#284]: https://github.com/noties/Markwon/pull/284
[#289]: https://github.com/noties/Markwon/issues/289
[@magnusvs]: https://github.com/magnusvs

View File

@ -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",

View File

@ -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);

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
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

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;
}
@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;