Documenting recycler module (work in progress)

This commit is contained in:
Dimitry Ivanov 2018-12-26 00:06:15 +03:00
parent 24b95e2ffb
commit 2593539b65
8 changed files with 269 additions and 19 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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