Documenting recycler module (work in progress)
This commit is contained in:
parent
24b95e2ffb
commit
2593539b65
@ -16,7 +16,7 @@ import java.util.List;
|
|||||||
import ru.noties.markwon.Markwon;
|
import ru.noties.markwon.Markwon;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class 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 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)}).
|
||||||
@ -36,43 +36,119 @@ import ru.noties.markwon.Markwon;
|
|||||||
*/
|
*/
|
||||||
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
|
public abstract class MarkwonAdapter extends RecyclerView.Adapter<MarkwonAdapter.Holder> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to obtain {@link Builder} instance.
|
||||||
|
*
|
||||||
|
* @see Builder
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
return new MarkwonAdapterImpl.BuilderImpl();
|
return new MarkwonAdapterImpl.BuilderImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a {@link MarkwonAdapter} for evaluation purposes. Resulting
|
||||||
|
* adapter will use default layout for all blocks. Default layout has no styling and should
|
||||||
|
* be specified explicitly.
|
||||||
|
*
|
||||||
|
* @see #create(int)
|
||||||
|
* @see #create(Entry)
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonAdapter create() {
|
public static MarkwonAdapter create() {
|
||||||
return new MarkwonAdapterImpl.BuilderImpl().build();
|
return new MarkwonAdapterImpl.BuilderImpl().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
// for an adapter with only one entry (all blocks are rendered the same with this entry)
|
/**
|
||||||
|
* Factory method to create a {@link MarkwonAdapter} that uses supplied entry to render all
|
||||||
|
* nodes.
|
||||||
|
*
|
||||||
|
* @param defaultEntry {@link Entry} to be used for node rendering
|
||||||
|
* @see SimpleEntry
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonAdapter create(@NonNull Entry<? extends Holder, ? extends Node> defaultEntry) {
|
public static MarkwonAdapter create(@NonNull Entry<? extends Holder, ? extends Node> defaultEntry) {
|
||||||
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build();
|
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(defaultEntry).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Factory method to create a {@link MarkwonAdapter} that will use supplied layoutResId view
|
||||||
|
* to display all nodes.
|
||||||
|
*
|
||||||
|
* <strong>Please note</strong> that supplied layout must have a TextView inside
|
||||||
|
* with {@code android:id="@+id/text"}
|
||||||
|
*
|
||||||
|
* @param layoutResId layout to be used to display all nodes
|
||||||
|
* @see SimpleEntry
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
public static MarkwonAdapter create(@LayoutRes int layoutResId) {
|
public static MarkwonAdapter create(@LayoutRes int layoutResId) {
|
||||||
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build();
|
return new MarkwonAdapterImpl.BuilderImpl().defaultEntry(layoutResId).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder to create an instance of {@link MarkwonAdapter}
|
||||||
|
*
|
||||||
|
* @see #include(Class, Entry)
|
||||||
|
* @see #defaultEntry(int)
|
||||||
|
* @see #defaultEntry(Entry)
|
||||||
|
* @see #reducer(Reducer)
|
||||||
|
*/
|
||||||
public interface Builder {
|
public interface Builder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Include a custom {@link Entry} rendering for a Node. Please note that `node` argument
|
||||||
|
* must be <em>exact</em> type, as internally there is no validation for inheritance. if multiple
|
||||||
|
* nodes should be rendered with the same {@link Entry} they must specify so explicitly.
|
||||||
|
* By calling this method for each.
|
||||||
|
*
|
||||||
|
* @param node type of the node to register
|
||||||
|
* @param entry {@link Entry} to be used for `node` rendering
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
<N extends Node> Builder include(
|
<N extends Node> Builder include(
|
||||||
@NonNull Class<N> node,
|
@NonNull Class<N> node,
|
||||||
@NonNull Entry<? extends Holder, ? super N> entry);
|
@NonNull Entry<? extends Holder, ? super N> entry);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify which {@link Entry} to use for all non-explicitly registered nodes
|
||||||
|
*
|
||||||
|
* @param defaultEntry {@link Entry}
|
||||||
|
* @return self
|
||||||
|
* @see SimpleEntry
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Builder defaultEntry(@NonNull Entry<? extends Holder, ? extends Node> defaultEntry);
|
Builder defaultEntry(@NonNull Entry<? extends Holder, ? extends Node> 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
|
@NonNull
|
||||||
Builder defaultEntry(@LayoutRes int layoutResId);
|
Builder defaultEntry(@LayoutRes int layoutResId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Specify how root Node will be <em>reduced</em> to a list of nodes. There is a default
|
||||||
|
* {@link Reducer} that will be used if not provided explicitly (there is no need to
|
||||||
|
* register your own unless you require it).
|
||||||
|
*
|
||||||
|
* @param reducer {@link Reducer}
|
||||||
|
* @return self
|
||||||
|
* @see Reducer
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
Builder reducer(@NonNull Reducer reducer);
|
Builder reducer(@NonNull Reducer reducer);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {@link MarkwonAdapter}
|
||||||
|
*/
|
||||||
@NonNull
|
@NonNull
|
||||||
MarkwonAdapter build();
|
MarkwonAdapter build();
|
||||||
}
|
}
|
||||||
|
@ -49,7 +49,7 @@ public class MarkwonRecyclerActivity extends Activity {
|
|||||||
|
|
||||||
final MarkwonAdapter adapter = MarkwonAdapter.builder()
|
final MarkwonAdapter adapter = MarkwonAdapter.builder()
|
||||||
.include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block))
|
.include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block))
|
||||||
.include(TableBlock.class, new TableNodeEntry())
|
.include(TableBlock.class, new TableEntry())
|
||||||
.defaultEntry(new SimpleEntry(R.layout.adapter_default_entry))
|
.defaultEntry(new SimpleEntry(R.layout.adapter_default_entry))
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import android.support.annotation.NonNull;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TableLayout;
|
|
||||||
|
|
||||||
import org.commonmark.ext.gfm.tables.TableBlock;
|
import org.commonmark.ext.gfm.tables.TableBlock;
|
||||||
|
|
||||||
@ -17,7 +16,7 @@ import ru.noties.markwon.recycler.MarkwonAdapter;
|
|||||||
import ru.noties.markwon.sample.extension.R;
|
import ru.noties.markwon.sample.extension.R;
|
||||||
|
|
||||||
// do not use in real applications, this is just a showcase
|
// do not use in real applications, this is just a showcase
|
||||||
public class TableNodeEntry implements MarkwonAdapter.Entry<TableNodeEntry.TableNodeHolder, TableBlock> {
|
public class TableEntry implements MarkwonAdapter.Entry<TableEntry.TableNodeHolder, TableBlock> {
|
||||||
|
|
||||||
private final Map<TableBlock, Table> cache = new HashMap<>(2);
|
private final Map<TableBlock, Table> cache = new HashMap<>(2);
|
||||||
|
|
||||||
@ -37,18 +36,11 @@ public class TableNodeEntry implements MarkwonAdapter.Entry<TableNodeEntry.Table
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (table != null) {
|
if (table != null) {
|
||||||
|
holder.tableEntryView.setTable(table);
|
||||||
// render table
|
// render table
|
||||||
renderTable(markwon, holder, table);
|
|
||||||
} // we need to do something with null table...
|
} // we need to do something with null table...
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderTable(
|
|
||||||
@NonNull Markwon markwon,
|
|
||||||
@NonNull TableNodeHolder holder,
|
|
||||||
@NonNull Table table) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long id(@NonNull TableBlock node) {
|
public long id(@NonNull TableBlock node) {
|
||||||
return node.hashCode();
|
return node.hashCode();
|
||||||
@ -61,12 +53,12 @@ public class TableNodeEntry implements MarkwonAdapter.Entry<TableNodeEntry.Table
|
|||||||
|
|
||||||
static class TableNodeHolder extends MarkwonAdapter.Holder {
|
static class TableNodeHolder extends MarkwonAdapter.Holder {
|
||||||
|
|
||||||
final TableLayout layout;
|
final TableEntryView tableEntryView;
|
||||||
|
|
||||||
TableNodeHolder(@NonNull View itemView) {
|
TableNodeHolder(@NonNull View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
|
||||||
this.layout = requireView(R.id.table_layout);
|
this.tableEntryView = requireView(R.id.table_entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -0,0 +1,153 @@
|
|||||||
|
package ru.noties.markwon.sample.extension.recycler;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.SpannedString;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
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.extension.R;
|
||||||
|
|
||||||
|
public class TableEntryView extends LinearLayout {
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setTextAlignment(textAlignment(column.alignment()));
|
||||||
|
textView.setText(column.content());
|
||||||
|
textView.getPaint().setFakeBoldText(row.header());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int textAlignment(@NonNull Table.Alignment alignment) {
|
||||||
|
final int out;
|
||||||
|
switch (alignment) {
|
||||||
|
case LEFT:
|
||||||
|
out = TextView.TEXT_ALIGNMENT_TEXT_START;
|
||||||
|
break;
|
||||||
|
case CENTER:
|
||||||
|
out = TextView.TEXT_ALIGNMENT_CENTER;
|
||||||
|
break;
|
||||||
|
case RIGHT:
|
||||||
|
out = TextView.TEXT_ALIGNMENT_TEXT_END;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unexpected alignment: " + alignment);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
}
|
@ -1,16 +1,21 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
<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_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:fillViewport="true"
|
android:fillViewport="true"
|
||||||
android:paddingLeft="16dip"
|
android:paddingLeft="16dip"
|
||||||
android:paddingRight="16dip">
|
android:paddingTop="8dip"
|
||||||
|
android:paddingRight="16dip"
|
||||||
|
android:paddingBottom="8dip">
|
||||||
|
|
||||||
<TableLayout
|
<ru.noties.markwon.sample.extension.recycler.TableEntryView
|
||||||
android:id="@+id/table_layout"
|
android:id="@+id/table_entry"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="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>
|
</HorizontalScrollView>
|
@ -0,0 +1,11 @@
|
|||||||
|
<?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:layout_width="0px"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:padding="4dip"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="#000"
|
||||||
|
android:textSize="16sp"
|
||||||
|
tools:text="Table content" />
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal" />
|
8
sample-custom-extension/src/main/res/values/attrs.xml
Normal file
8
sample-custom-extension/src/main/res/values/attrs.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<declare-styleable name="TableEntryView">
|
||||||
|
<attr name="tev_rowEvenBackgroundColor" format="color" />
|
||||||
|
<attr name="tev_debugData" format="string" />
|
||||||
|
</declare-styleable>
|
||||||
|
</resources>
|
Loading…
x
Reference in New Issue
Block a user