Skip to content

Commit

Permalink
feat(android): implement native selection for ListView and TableView
Browse files Browse the repository at this point in the history
  • Loading branch information
garymathews authored and ewanharris committed Sep 10, 2021
1 parent f152206 commit 2aee71c
Show file tree
Hide file tree
Showing 18 changed files with 811 additions and 122 deletions.
9 changes: 9 additions & 0 deletions android/modules/ui/res/drawable/titanium_icon_checkcircle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@android:color/black"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>
9 changes: 9 additions & 0 deletions android/modules/ui/res/drawable/titanium_icon_circle.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="96.0"
android:viewportHeight="96.0">
<path
android:fillColor="@android:color/black"
android:pathData="M47.1,0a47.1,47.1,0,1,0,47,47.1A47.1,47.1,0,0,0,47.1,0Zm0,84.7A37.7,37.7,0,1,1,84.7,47.1,37.7,37.7,0,0,1,47.1,84.7z"/>
</vector>
14 changes: 13 additions & 1 deletion android/modules/ui/res/layout/titanium_ui_listview_holder.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,24 @@
android:id="@+id/titanium_ui_listview_holder_outer_content_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/titanium_ui_listview_holder_header_container">
android:layout_below="@id/titanium_ui_listview_holder_header_container"
android:addStatesFromChildren="true">

<ImageView
android:id="@+id/titanium_ui_listview_holder_left_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_centerVertical="true"
android:layout_marginLeft="6dp"
android:focusable="false"
android:focusableInTouchMode="false"/>

<org.appcelerator.titanium.view.TiCompositeLayout
android:id="@+id/titanium_ui_listview_holder_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/titanium_ui_listview_holder_left_image"
android:addStatesFromChildren="true"/>

<ImageView
Expand Down
107 changes: 95 additions & 12 deletions android/modules/ui/src/java/ti/modules/titanium/ui/TableViewProxy.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,21 @@
import org.appcelerator.kroll.KrollDict;
import org.appcelerator.kroll.annotations.Kroll;
import org.appcelerator.titanium.TiC;
import org.appcelerator.titanium.TiDimension;
import org.appcelerator.titanium.proxy.TiViewProxy;
import org.appcelerator.titanium.view.TiUIView;

import android.app.Activity;

import androidx.recyclerview.selection.SelectionTracker;
import androidx.recyclerview.widget.RecyclerView;

import ti.modules.titanium.ui.widget.TiUITableView;
import ti.modules.titanium.ui.widget.listview.RecyclerViewProxy;
import ti.modules.titanium.ui.widget.tableview.TableViewAdapter;
import ti.modules.titanium.ui.widget.tableview.TiTableView;

import static android.util.TypedValue.COMPLEX_UNIT_DIP;

@Kroll.proxy(
creatableInModule = UIModule.class,
propertyAccessors = {
Expand All @@ -50,6 +53,7 @@
TiC.PROPERTY_SEARCH,
TiC.PROPERTY_SEPARATOR_COLOR,
TiC.PROPERTY_SEPARATOR_STYLE,
TiC.PROPERTY_SHOW_SELECTION_CHECK,
TiC.PROPERTY_SHOW_VERTICAL_SCROLL_INDICATOR,
TiC.PROPERTY_TOUCH_FEEDBACK,
TiC.PROPERTY_TOUCH_FEEDBACK_COLOR
Expand All @@ -61,12 +65,15 @@ public class TableViewProxy extends RecyclerViewProxy

private final List<TableViewSectionProxy> sections = new ArrayList<>();

private KrollDict contentOffset = null;

public TableViewProxy()
{
super();

defaultValues.put(TiC.PROPERTY_OVER_SCROLL_MODE, 0);
defaultValues.put(TiC.PROPERTY_SCROLLABLE, true);
defaultValues.put(TiC.PROPERTY_SHOW_SELECTION_CHECK, true);
defaultValues.put(TiC.PROPERTY_TOUCH_FEEDBACK, true);
}

Expand Down Expand Up @@ -336,6 +343,69 @@ public String getApiName()
return "Ti.UI.TableView";
}

// NOTE: For internal use only.
public KrollDict getContentOffset()
{
final TiTableView tableView = getTableView();

if (tableView != null) {
final KrollDict contentOffset = new KrollDict();

final int x = (int) new TiDimension(tableView.getScrollOffsetX(),
TiDimension.TYPE_WIDTH, COMPLEX_UNIT_DIP).getAsDefault(tableView);
final int y = (int) new TiDimension(tableView.getScrollOffsetY(),
TiDimension.TYPE_HEIGHT, COMPLEX_UNIT_DIP).getAsDefault(tableView);

contentOffset.put(TiC.PROPERTY_X, x);
contentOffset.put(TiC.PROPERTY_Y, y);

// NOTE: Since obtaining the scroll offset from RecyclerView is unreliable
// when items are added/removed, also grab the current visible item instead.
final int currentIndex = tableView.getAdapterIndex(tableView.getFirstVisibleItem().index);
contentOffset.put(TiC.PROPERTY_INDEX, currentIndex);

this.contentOffset = contentOffset;
}

return this.contentOffset;
}

@Kroll.method
public void setContentOffset(KrollDict contentOffset, @Kroll.argument(optional = true) KrollDict options)
{
final TiTableView tableView = getTableView();

if (contentOffset != null) {
this.contentOffset = contentOffset;

if (tableView != null) {

if (contentOffset.containsKeyAndNotNull(TiC.PROPERTY_INDEX)) {

// If available, scroll to provided index provided by internal `getContentOffset()` method.
tableView.getRecyclerView().scrollToPosition(contentOffset.getInt(TiC.PROPERTY_INDEX));
return;
}

final int x = contentOffset.optInt(TiC.EVENT_PROPERTY_X, 0);
final int y = contentOffset.optInt(TiC.EVENT_PROPERTY_Y, 0);
final int pixelX = new TiDimension(x, TiDimension.TYPE_WIDTH).getAsPixels(tableView);
final int pixelY = new TiDimension(y, TiDimension.TYPE_HEIGHT).getAsPixels(tableView);

// NOTE: `scrollTo()` is not supported, this is a minor workaround.
tableView.getRecyclerView().scrollToPosition(0);
tableView.getRecyclerView().post(new Runnable()
{
@Override
public void run()
{
tableView.getRecyclerView().scrollBy(pixelX, pixelY);
}
});
}
}
}

/**
* Get current table data.
*
Expand Down Expand Up @@ -484,6 +554,12 @@ protected TiUIView handleGetView()
// Update table if being re-used.
if (view != null) {
update();

if (this.contentOffset != null) {

// Restore previous content position.
setContentOffset(this.contentOffset, null);
}
}

return view;
Expand Down Expand Up @@ -648,6 +724,8 @@ public void release()
@Override
public void releaseViews()
{
this.contentOffset = getContentOffset();

super.releaseViews();

for (TableViewSectionProxy section : this.sections) {
Expand Down Expand Up @@ -709,14 +787,10 @@ public void selectRow(int index)
final TiTableView tableView = getTableView();

if (tableView != null) {
final RecyclerView recyclerView = tableView.getRecyclerView();

if (recyclerView != null) {
final TableViewAdapter adapter = (TableViewAdapter) recyclerView.getAdapter();
final SelectionTracker tracker = tableView.getTracker();

if (adapter != null) {
adapter.getTracker().select(row);
}
if (tracker != null) {
tracker.select(row);
}
}
}
Expand Down Expand Up @@ -773,12 +847,21 @@ private void processProperty(String name, Object value)
{
if (name.equals(TiC.PROPERTY_DATA) || name.equals(TiC.PROPERTY_SECTIONS)) {
setData((Object[]) value);
}

if (name.equals(TiC.PROPERTY_EDITING)
|| name.equals(TiC.PROPERTY_MOVING)) {
} else if (name.equals(TiC.PROPERTY_EDITING)) {
final TiViewProxy parent = getParent();

if (parent != null) {

// Due to Android limitations, selection trackers cannot be removed.
// Re-create TableView with new selection tracker.
parent.recreateChild(this);
}

} else if (name.equals(TiC.PROPERTY_MOVING)
|| name.equals(TiC.PROPERTY_SHOW_SELECTION_CHECK)) {

// Update table to display drag-handles.
// Update and refresh table.
update();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class TableViewRowProxy extends TiViewProxy
private int filteredIndex = -1;
private TableViewHolder holder;
private boolean placeholder = false;
private boolean selected = false;

// FIXME: On iOS the same row can be added to a table multiple times.
// Due to constraints, we need to create a new proxy and track changes.
Expand Down Expand Up @@ -277,6 +278,24 @@ public TableViewProxy getTableViewProxy()
return (TableViewProxy) parent;
}

/**
* Determine if row is currently selected.
*
* @return Boolean of selection status.
*/
public boolean isSelected()
{
return selected;
}

/**
* Set row selection status.
*/
public void setSelected(boolean selected)
{
this.selected = selected;
}

/**
* Handle initial creation properties.
*
Expand Down Expand Up @@ -393,6 +412,22 @@ private void processProperty(String name, Object value)
footerDeprecationLog();
}

// Set selected color from selection style.
if (!hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)
&& name.equals(TiC.PROPERTY_SELECTION_STYLE)
&& value instanceof Integer) {
String selectionColor = null;

switch ((Integer) value) {
case UIModule.SELECTION_STYLE_NONE:
selectionColor = "transparent";
break;
}

setProperty(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR, selectionColor);
invalidate();
}

if (name.equals(TiC.PROPERTY_LEFT_IMAGE)
|| name.equals(TiC.PROPERTY_RIGHT_IMAGE)
|| name.equals(TiC.PROPERTY_HAS_CHECK)
Expand All @@ -401,7 +436,9 @@ private void processProperty(String name, Object value)
|| name.equals(TiC.PROPERTY_BACKGROUND_COLOR)
|| name.equals(TiC.PROPERTY_BACKGROUND_IMAGE)
|| name.equals(TiC.PROPERTY_SELECTED_BACKGROUND_COLOR)
|| name.equals(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)
|| name.equals(TiC.PROPERTY_SELECTED_BACKGROUND_IMAGE)
|| name.equals(TiC.PROPERTY_BACKGROUND_SELECTED_IMAGE)
|| name.equals(TiC.PROPERTY_TITLE)
|| name.equals(TiC.PROPERTY_COLOR)
|| name.equals(TiC.PROPERTY_FONT)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,11 @@ public class UIModule extends KrollModule
@Kroll.constant
public static final int MAP_VIEW_HYBRID = 3;

@Kroll.constant
public static final int SELECTION_STYLE_NONE = 0;
@Kroll.constant
public static final int SELECTION_STYLE_DEFAULT = 1;

@Kroll.constant
public static final int SWITCH_STYLE_CHECKBOX = 0;
@Kroll.constant
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class ListItemProxy extends TiViewProxy
private String templateId;
private boolean placeholder = false;
private boolean hasAddedItemEvents = false;
private boolean selected = false;

public ListItemProxy()
{
Expand Down Expand Up @@ -577,6 +578,24 @@ public String getTemplateId()
return this.templateId;
}

/**
* Determine if item is currently selected.
*
* @return Boolean of selection status.
*/
public boolean isSelected()
{
return selected;
}

/**
* Set item selection status.
*/
public void setSelected(boolean selected)
{
this.selected = selected;
}

/**
* Handle creation from ListDataItem object.
*
Expand Down Expand Up @@ -779,6 +798,21 @@ private void processProperty(String name, Object value)
setProperty(TiC.PROPERTY_BACKGROUND_SELECTED_IMAGE, value);
}

// Set selected color from selection style.
if (!hasPropertyAndNotNull(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR)
&& name.equals(TiC.PROPERTY_SELECTION_STYLE)
&& value instanceof Integer) {
String selectionColor = null;

switch ((Integer) value) {
case UIModule.SELECTION_STYLE_NONE:
selectionColor = "transparent";
break;
}

setProperty(TiC.PROPERTY_BACKGROUND_SELECTED_COLOR, selectionColor);
}

if (name.equals(TiC.PROPERTY_CAN_MOVE)) {
invalidate();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import android.widget.RelativeLayout;

import androidx.annotation.NonNull;
import androidx.recyclerview.selection.SelectionTracker;

public class ListViewAdapter extends TiRecyclerViewAdapter<ListViewHolder>
{
Expand All @@ -30,7 +29,6 @@ public class ListViewAdapter extends TiRecyclerViewAdapter<ListViewHolder>
private LayoutInflater inflater;
private List<ListItemProxy> models;
private final TreeMap<String, LinkedList<ListItemProxy>> recyclableItemsMap = new TreeMap<>();
private SelectionTracker tracker;

public ListViewAdapter(@NonNull Context context, @NonNull List<ListItemProxy> models)
{
Expand Down Expand Up @@ -117,7 +115,7 @@ public void onBindViewHolder(@NonNull ListViewHolder holder, int position)
{
// Fetch item proxy for given list position.
final ListItemProxy item = this.models.get(position);
final boolean selected = tracker != null ? tracker.isSelected(item) : false;
final boolean selected = this.tracker != null ? this.tracker.isSelected(item) : false;

// Check if we have any recyclable items for the current template.
LinkedList<ListItemProxy> recyclableItems = this.recyclableItemsMap.get(item.getTemplateId());
Expand All @@ -138,6 +136,10 @@ public void onBindViewHolder(@NonNull ListViewHolder holder, int position)
}
}

// Notify item of its selected status.
// This is necessary to maintain selection status on theme change.
item.setSelected(selected);

// Update ListViewHolder with new model data.
holder.bind(item, selected);
}
Expand Down
Loading

0 comments on commit 2aee71c

Please sign in to comment.