Android's missing TableView component.
This Kotlin version is based on the original TableView implementation by Evren Coşkun. This component uses RecyclerViews for displaying column headers, row headers and cells.
You can check the releases page for the latest version and changelog.
To use this library in your Android project, add this dependency line
implementation 'ph.ingenuity.tableview:tableview:0.1.0-alpha'
to your application's build.gradle
file.
dependencies {
implementation 'ph.ingenuity.tableview:tableview:0.1.0-alpha'
}
val tableView = TableView(context)
<ph.ingenuity.tableview.TableView
android:id="@+id/table_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<ph.ingenuity.tableview.TableView
android:id="@+id/table_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:table_column_header_height="@dimen/default_column_header_height"
app:table_row_header_width="@dimen/row_header_width"
app:table_selected_color="@color/colorPrimary"
app:table_separator_color="@color/colorAccent"
app:table_shadow_color="@color/table_view_default_shadow_background_color"
app:table_show_horizontal_separator="true"
app:table_show_vertical_separator="true"
app:table_unselected_color="@color/table_view_default_unselected_background_color" />
val tableView = findViewById(R.id.table_view)
Note: To use these attributes on XML layout, the xmlns: namespace line
xmlns:app="http://schemas.android.com/apk/res-auto"
should be added on the layout root view.
Otherwise, Android Studio gives you compile error.
table_column_header_height
- height of the column headertable_row_header_width
- width of the row headertable_selected_color
- color for selected cellstable_separator_color
- separator colortable_shadow_color
- shadow colortable_show_horizontal_separator
- visibility control for horizontal separatortable_show_vertical_separator
- visibility control for vertical separatortable_unselected_color
- color for not selected cellsA custom TableViewAdapter must be created to handle the ViewHolders for the column header, row
header and cells. Since this library uses RecyclerView
component, onCreateViewHolder
and
onBindViewHolder
methods are called for the column header, row header and cells. The custom
TableViewAdapter should extend the AbstractTableAdapter
class. Also create your ViewHolder
classes to extend AbstractViewHolder
class.
class RandomDataTableViewAdapter(
private val context: Context
) : AbstractTableAdapter(context) {
override fun getColumnHeaderItemViewType(column: Int): Int = 0
override fun getRowHeaderItemViewType(row: Int): Int = 0
override fun getCellItemViewType(column: Int): Int = 0
override fun onCreateCellViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
val cellView = LayoutInflater.from(context).inflate(
// Replace this with your cell view layout
R.layout.table_cell_text_data,
parent,
false
)
return RandomDataCellViewHolder(cellView)
}
override fun onBindCellViewHolder(
holder: AbstractViewHolder,
cellItem: Any,
column: Int,
row: Int
) {
val cell = cellItem as RandomDataCell
val cellViewHolder = holder as RandomDataCellViewHolder
cellViewHolder.cellTextView.text = cell.content.toString()
}
override fun onCreateColumnHeaderViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
val columnHeaderView = LayoutInflater.from(context).inflate(
// Replace this with your column header view layout
R.layout.table_column_header_text_data,
parent,
false
)
return RandomDataColumnHeaderViewHolder(columnHeaderView)
}
override fun onBindColumnHeaderViewHolder(
holder: AbstractViewHolder,
columnHeaderItem: Any,
column: Int
) {
val columnHeaderCell = columnHeaderItem as RandomDataCell
val columnHeaderViewHolder = holder as RandomDataColumnHeaderViewHolder
columnHeaderViewHolder.cellTextView.text = columnHeaderCell.content.toString()
}
override fun onCreateRowHeaderViewHolder(
parent: ViewGroup,
viewType: Int
): RecyclerView.ViewHolder {
val rowHeaderView = LayoutInflater.from(context).inflate(
// Replace this with your row header view layout
R.layout.table_row_header_text_data,
parent,
false
)
return RandomDataRowHeaderViewHolder(rowHeaderView)
}
override fun onBindRowHeaderViewHolder(
holder: AbstractViewHolder,
rowHeaderItem: Any,
row: Int
) {
val rowHeaderCell = rowHeaderItem as RandomDataCell
val rowHeaderViewHolder = holder as RandomDataRowHeaderViewHolder
rowHeaderViewHolder.cellTextView.text = rowHeaderCell.content.toString()
}
override fun onCreateCornerView(): View? {
// Replace this with your corner view layout
val cornerView = LayoutInflater.from(context).inflate(R.layout.table_corner_view, null)
cornerView.setOnClickListener {
Toast.makeText(context, "CornerView has been clicked.", Toast.LENGTH_SHORT).show()
}
return cornerView
}
class RandomDataCellViewHolder(itemView: View) : AbstractViewHolder(itemView) {
val cellTextView: TextView
get() = itemView.findViewById(R.id.random_data_cell_data)
}
class RandomDataColumnHeaderViewHolder(itemView: View) : AbstractViewHolder(itemView) {
val cellTextView: TextView
get() = itemView.findViewById(R.id.column_header_text)
}
class RandomDataRowHeaderViewHolder(itemView: View) : AbstractViewHolder(itemView) {
val cellTextView: TextView
get() = itemView.findViewById(R.id.row_header_text)
}
}
AbstractTableAdapter
class requires three lists for the column header, row header and cell items.
// Retrieve your data from local storage or API
val cellsList = randomDataFactory.randomCellsList as List<List<Any>>
val rowHeadersList = randomDataFactory.randomRowHeadersList as List<Any>
val columnHeadersList = randomDataFactory.randomColumnHeadersList as List<Any>
// Create an instance of our custom TableViewAdapter
val tableAdapter = RandomDataTableViewAdapter(mainView!!.context)
// Set the adapter to the created TableView
tableView.adapter = tableAdapter
// Set the data to the adapter
tableAdapter.setAllItems(cellsList, columnHeadersList, rowHeadersList)
A custom TableViewListener must be created to handle column header, row header and
cell click and long pressed actions. The custom TableViewListener must implement the
ITableViewListener
interface.
class TableViewListener(private val tableView: TableView) : ITableViewListener {
private var toast: Toast? = null
private val context: Context = tableView.context
override fun onCellClicked(cellView: RecyclerView.ViewHolder, column: Int, row: Int) {
showToast("Cell $column $row has been clicked.")
}
override fun onCellLongPressed(cellView: RecyclerView.ViewHolder, column: Int, row: Int) {
showToast("Cell $column, $row has been long pressed.")
}
override fun onColumnHeaderClicked(columnHeaderView: RecyclerView.ViewHolder, column: Int) {
showToast("Column header $column has been clicked.")
}
override fun onColumnHeaderLongPressed(
columnHeaderView: RecyclerView.ViewHolder,
column: Int
) {
if (columnHeaderView is RandomDataColumnHeaderViewHolder) {
val popup = ColumnHeaderLongPressPopup(columnHeaderView, tableView)
popup.show()
}
}
override fun onRowHeaderClicked(rowHeaderView: RecyclerView.ViewHolder, row: Int) {
showToast("Row header $row has been clicked.")
}
override fun onRowHeaderLongPressed(rowHeaderView: RecyclerView.ViewHolder, row: Int) {
if (rowHeaderView is RandomDataRowHeaderViewHolder) {
val popup = RowHeaderLongPressPopup(rowHeaderView, tableView)
popup.show()
}
}
private fun showToast(message: String) {
if (toast == null) {
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
}
toast!!.setText(message)
toast!!.show()
}
}
tableView.tableViewListener = TableViewListener(tableView)
By now, we should have a working TableView with displayed data and action listeners. The next sections are for some TableView features such as changing data sets, hiding/showing columns and rows and scrolling to position, as well as advanced features such as sorting, filtering and pagination.
The data set in the TableView can be updated by updating the TableViewAdapter.
tableView.adapter.addRow(position, rowHeaderItem, cellItems)
tableView.adapter.addRowRange(position, rowHeaderItems, cellItems)
tableView.adapter.removeRow(position)
tableView.adapter.removeRow(position, count)
tableView.adapter.changeRowHeaderItem(position, rowHeaderItem)
tableView.adapter.changeRowHeaderItemRange(position, rowHeaderItems)
tableView.adapter.changeColumnHeader(position, columnHeaderItem)
tableView.adapter.changeColumnHeaderRange(position, columnHeaderItems)
tableView.adapter.changeCellItem(column, row, cellItem)
The visibility of the the rows and columns in the TableView can be controlled using the visibility controls in the TableView instance.
tableView.showRow(row)
tableView.hideRow(row)
tableView.showAllHiddenRows()
tableView.clearHiddenRowList()
tableView.isRowVisible(row)
tableView.showColumn(column)
tableView.hideColumn(column)
tableView.showAllHiddenColumns()
tableView.clearHiddenColumnList()
tableView.isColumnVisible(column)
tableView.remeasureColumnWidth(column)
tableView.hasFixedWidth = false
tableView.ignoreSelectionColors = false
tableView.showHorizontalSeparators = Boolean
tableView.showVerticalSeparators = Boolean
tableView.scrollToColumn(column)
tableView.scrollToRow(row)
Sorting, by definition and usage in this context is the rearrangement of values in a data set according to a property in ascending or descending manner.
To use this feature in the TableView, the Sortable
interface must be implemented in the data
classes or the models. The Sortable
interface requires you to provide a unique id
and
content
value for your data. Sorting can be done on the following data types:
Sortable
interface class RandomDataCell(
_data: Any,
_id: String = _data.hashCode().toString()
) : Sortable {
override var id: String = _id
override var content: Any = _data
}
tableView.sortColumn(column, sortState)
tableView.getColumnSortState(column)
SortState.ASCENDING
SortState.DESCENDING
SortState.UNSORTED
Listening to sorting state changes can be done by implementing the AbstractSorterViewHolder
interface to your ColumnHeaderViewHolder
class.
onSortingStatusChanged(SortState sortState) {
// do something here...
}
sortState
object: columnHeaderViewHolder.sortState
Filtering, by definition and usage in this context, is displaying a subset of data into the TableView based on a given filter globally. on a specified column or combination.
To use this feature in the TableView, the Filterable
interface must be implemented in the data
classes or the models. The Filterable
interface requires you to provide a unique
filterableKeyword
string value for your data. This filterableKeyword
will be used for filtering
the cell data based on a filter query.
Filterable
interface class RandomDataCell(
_filter: String = _data.toString()
) : Filterable {
override var filterableKeyword: String = _filter
}
Filter
classAn instance of the Filter
class must be created and pass the TableView to be filtered.
private lateinit var filter: Filter
initialize() {
setUpTableView()
filter = Filter(tableView)
}
Filtering can be done by calling the set()
method of the Filter
instance which can be used to
filter the whole tabele data, a column or combination. Clearing a filter can be simply done by
passing an empty string as filterKeyword (""
and not null
).
// filtering whole table data
fun filterWholeTable(filterKeyword: String) = filter.set(filterKeyword)
// filtering a specific column
fun filterThisColumn(column: Int, filterKeyword: String) = filter.set(column, filterKeyword)
// clear filter for whole table
fun clearTableFilter() = filter.set("")
// clear filter for a specific column
fun clearFilterForThisColumn(column: Int) = filter.set(column, "")
FilterChangedListener
A FilterChangedListener
object can be added to the TableView for handling data changes during
filtering process.
private val filterChangedListener = object : FilterChangedListener {
fun onFilterChanged(
filteredCellItems: List<List<Any>>,
filteredRowHeaderItems: List<Any>
) {
// do something here...
}
fun onFilterCleared(
originalCellItems: List<List<Any>>,
originalRowHeaderItems: List<Any>
) {
// do something here...
}
}
initialize() {
setUpTableView()
filter = Filter(tableView)
tableView.filterHandler.addFilterChangedListener(filterChangedListener)
}
Pagination, by definition and usage in this context, is the division of the whole set of data into subsets called pages and loading the data into the TableView page-by-page and not the whole data directly. This is useful if you have a large amount of data to be displayed.
Depending on your preference, you may not follow the following and create your own implementation.
Pagination
classPagination
class has three possible constructors: (1) passing the TableView
instance only,
(2) TableView
and the initial ITEMS_PER_PAGE
and (3) TableView
, initial ITEMS_PER_PAGE
and
the OnTableViewPageTurnedListener
. By default, if no ITEMS_PER_PAGE specified, the TableView will
be paginated into 10 items per page. private lateinit var pagination: Pagination
initialize() {
setUpTableView()
pagination = Pagination(tableView)
}
loadNextPage()
method.
You can assign this to your implementation of nextPageButton onClick action: fun nextTablePage() = pagination.loadNextPage()
loadPreviousPage()
method.
You can assign this to your implementation of previousPageButton onClick action: fun previousTablePage() = pagination.loadPreviousPage()
loadPage(page: Int)
method. You can assign this to the EditText field TextChanged
action (using TextWatcher): fun loadTablePage(page: Int) = pagination.loadPage(page)
itemsPerPage
property of the pagination. You can assign this to your Spinner
with the number of items per page list: fun setTableItemsPerPage(numItems: Int) {
pagination.itemsPerPage = numItems
}
OnTableViewPageTurnedListener
private val onTableViewPageTurnedListener =
object : Pagination.OnTableViewPageTurnedListener {
override fun onPageTurned(numItems: Int, itemsStart: Int, itemsEnd: Int) {
// do something here...
}
}
initialize() {
setUpTableView()
pagination = Pagination(tableView)
pagination.onTableViewPageTurnedListener = onTableViewPageTurnedListener
}
Original author, idea, implementation and code by Evren Coşkun.
Contributions of any kind are welcome!
Copyright 2018 Jeremy Patrick Pacabis
Copyright 2017-2018 Evren Coşkun
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.