/* * Copyright (c) 2018. Evren Coşkun * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package com.evrencoskun.tableview; import android.content.Context; import android.content.res.TypedArray; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Parcelable; import android.util.AttributeSet; import android.view.ViewGroup; import android.widget.FrameLayout; import com.evrencoskun.tableview.adapter.AbstractTableAdapter; import com.evrencoskun.tableview.adapter.recyclerview.CellRecyclerView; import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractViewHolder; import com.evrencoskun.tableview.filter.Filter; import com.evrencoskun.tableview.handler.ColumnSortHandler; import com.evrencoskun.tableview.handler.ColumnWidthHandler; import com.evrencoskun.tableview.handler.FilterHandler; import com.evrencoskun.tableview.handler.PreferencesHandler; import com.evrencoskun.tableview.handler.ScrollHandler; import com.evrencoskun.tableview.handler.SelectionHandler; import com.evrencoskun.tableview.handler.VisibilityHandler; import com.evrencoskun.tableview.layoutmanager.CellLayoutManager; import com.evrencoskun.tableview.layoutmanager.ColumnHeaderLayoutManager; import com.evrencoskun.tableview.listener.ITableViewListener; import com.evrencoskun.tableview.listener.TableViewLayoutChangeListener; import com.evrencoskun.tableview.listener.itemclick.ColumnHeaderRecyclerViewItemClickListener; import com.evrencoskun.tableview.listener.itemclick.RowHeaderRecyclerViewItemClickListener; import com.evrencoskun.tableview.listener.scroll.HorizontalRecyclerViewListener; import com.evrencoskun.tableview.listener.scroll.VerticalRecyclerViewListener; import com.evrencoskun.tableview.preference.SavedState; import com.evrencoskun.tableview.sort.SortState; import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; /** * Created by evrencoskun on 11/06/2017. */ public class TableView extends FrameLayout implements ITableView { @NonNull protected CellRecyclerView mCellRecyclerView; @NonNull protected CellRecyclerView mColumnHeaderRecyclerView; @NonNull protected CellRecyclerView mRowHeaderRecyclerView; @Nullable protected AbstractTableAdapter mTableAdapter; @Nullable private ITableViewListener mTableViewListener; @NonNull private VerticalRecyclerViewListener mVerticalRecyclerListener; @NonNull private HorizontalRecyclerViewListener mHorizontalRecyclerViewListener; @NonNull private ColumnHeaderLayoutManager mColumnHeaderLayoutManager; @NonNull private LinearLayoutManager mRowHeaderLayoutManager; @NonNull private CellLayoutManager mCellLayoutManager; @NonNull private DividerItemDecoration mVerticalItemDecoration; @NonNull private DividerItemDecoration mHorizontalItemDecoration; @NonNull private SelectionHandler mSelectionHandler; @Nullable private ColumnSortHandler mColumnSortHandler; @NonNull private VisibilityHandler mVisibilityHandler; @NonNull private ScrollHandler mScrollHandler; @Nullable private FilterHandler mFilterHandler; @NonNull private PreferencesHandler mPreferencesHandler; @NonNull private ColumnWidthHandler mColumnWidthHandler; private int mRowHeaderWidth; private int mColumnHeaderHeight; private int mSelectedColor; private int mUnSelectedColor; private int mShadowColor; private int mSeparatorColor = -1; private boolean mHasFixedWidth; private boolean mIgnoreSelectionColors; private boolean mShowHorizontalSeparators = true; private boolean mShowVerticalSeparators = true; private boolean mAllowClickInsideCell = false; private boolean mAllowClickInsideRowHeader = false; private boolean mAllowClickInsideColumnHeader = false; private boolean mIsSortable; public TableView(@NonNull Context context) { super(context); initialDefaultValues(null); initialize(); } public TableView(@NonNull Context context, @Nullable AttributeSet attrs) { super(context, attrs); initialDefaultValues(attrs); initialize(); } public TableView(@NonNull Context context, @Nullable AttributeSet attrs, @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); initialDefaultValues(null); initialize(); } private void initialDefaultValues(@Nullable AttributeSet attrs) { // Dimensions mRowHeaderWidth = (int) getResources().getDimension(R.dimen.default_row_header_width); mColumnHeaderHeight = (int) getResources().getDimension(R.dimen .default_column_header_height); // Colors mSelectedColor = ContextCompat.getColor(getContext(), R.color .table_view_default_selected_background_color); mUnSelectedColor = ContextCompat.getColor(getContext(), R.color .table_view_default_unselected_background_color); mShadowColor = ContextCompat.getColor(getContext(), R.color .table_view_default_shadow_background_color); if (attrs == null) { // That means TableView is created programmatically. return; } // Get values from xml attributes TypedArray a = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable .TableView, 0, 0); try { // Dimensions mRowHeaderWidth = (int) a.getDimension(R.styleable.TableView_row_header_width, mRowHeaderWidth); mColumnHeaderHeight = (int) a.getDimension(R.styleable .TableView_column_header_height, mColumnHeaderHeight); // Colors mSelectedColor = a.getColor(R.styleable.TableView_selected_color, mSelectedColor); mUnSelectedColor = a.getColor(R.styleable.TableView_unselected_color, mUnSelectedColor); mShadowColor = a.getColor(R.styleable.TableView_shadow_color, mShadowColor); mSeparatorColor = a.getColor(R.styleable.TableView_separator_color, ContextCompat .getColor(getContext(), R.color.table_view_default_separator_color)); // Booleans mShowVerticalSeparators = a.getBoolean(R.styleable.TableView_show_vertical_separator, mShowVerticalSeparators); mShowHorizontalSeparators = a.getBoolean(R.styleable .TableView_show_horizontal_separator, mShowHorizontalSeparators); mAllowClickInsideCell = a.getBoolean(R.styleable.TableView_allow_click_inside_cell, mAllowClickInsideCell); mAllowClickInsideRowHeader = a.getBoolean(R.styleable.TableView_allow_click_inside_row_header, mAllowClickInsideRowHeader); mAllowClickInsideColumnHeader = a.getBoolean(R.styleable.TableView_allow_click_inside_column_header, mAllowClickInsideColumnHeader); } finally { a.recycle(); } } private void initialize() { // Create Views mColumnHeaderRecyclerView = createColumnHeaderRecyclerView(); mRowHeaderRecyclerView = createRowHeaderRecyclerView(); mCellRecyclerView = createCellRecyclerView(); // Add Views addView(mColumnHeaderRecyclerView); addView(mRowHeaderRecyclerView); addView(mCellRecyclerView); // Create Handlers mSelectionHandler = new SelectionHandler(this); mVisibilityHandler = new VisibilityHandler(this); mScrollHandler = new ScrollHandler(this); mPreferencesHandler = new PreferencesHandler(this); mColumnWidthHandler = new ColumnWidthHandler(this); initializeListeners(); } protected void initializeListeners() { // --- Listeners to help Scroll synchronously --- // It handles Vertical scroll listener mVerticalRecyclerListener = new VerticalRecyclerViewListener(this); // Set this listener both of Cell RecyclerView and RowHeader RecyclerView mRowHeaderRecyclerView.addOnItemTouchListener(mVerticalRecyclerListener); mCellRecyclerView.addOnItemTouchListener(mVerticalRecyclerListener); // It handles Horizontal scroll listener mHorizontalRecyclerViewListener = new HorizontalRecyclerViewListener(this); // Set scroll listener to be able to scroll all rows synchrony. mColumnHeaderRecyclerView.addOnItemTouchListener(mHorizontalRecyclerViewListener); // --- Listeners to help item clicks --- // Create item click listeners // Add item click listener for column header recyclerView if (mAllowClickInsideColumnHeader) { ColumnHeaderRecyclerViewItemClickListener columnHeaderRecyclerViewItemClickListener = new ColumnHeaderRecyclerViewItemClickListener (mColumnHeaderRecyclerView, this); mColumnHeaderRecyclerView.addOnItemTouchListener(columnHeaderRecyclerViewItemClickListener); } // Add item click listener for row header recyclerView if (mAllowClickInsideRowHeader) { RowHeaderRecyclerViewItemClickListener rowHeaderRecyclerViewItemClickListener = new RowHeaderRecyclerViewItemClickListener (mRowHeaderRecyclerView, this); mRowHeaderRecyclerView.addOnItemTouchListener(rowHeaderRecyclerViewItemClickListener); } // Add Layout change listener both of Column Header & Cell recyclerView to detect // changing size // For some case, it is pretty necessary. TableViewLayoutChangeListener layoutChangeListener = new TableViewLayoutChangeListener (this); mColumnHeaderRecyclerView.addOnLayoutChangeListener(layoutChangeListener); mCellRecyclerView.addOnLayoutChangeListener(layoutChangeListener); } @NonNull protected CellRecyclerView createColumnHeaderRecyclerView() { CellRecyclerView recyclerView = new CellRecyclerView(getContext()); // Set layout manager recyclerView.setLayoutManager(getColumnHeaderLayoutManager()); // Set layout params LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, mColumnHeaderHeight); layoutParams.leftMargin = mRowHeaderWidth; recyclerView.setLayoutParams(layoutParams); if (isShowHorizontalSeparators()) { // Add vertical item decoration to display column line recyclerView.addItemDecoration(getHorizontalItemDecoration()); } return recyclerView; } @NonNull protected CellRecyclerView createRowHeaderRecyclerView() { CellRecyclerView recyclerView = new CellRecyclerView(getContext()); // Set layout manager recyclerView.setLayoutManager(getRowHeaderLayoutManager()); // Set layout params LayoutParams layoutParams = new LayoutParams(mRowHeaderWidth, LayoutParams.WRAP_CONTENT); layoutParams.topMargin = mColumnHeaderHeight; recyclerView.setLayoutParams(layoutParams); if (isShowVerticalSeparators()) { // Add vertical item decoration to display row line recyclerView.addItemDecoration(getVerticalItemDecoration()); } return recyclerView; } @NonNull protected CellRecyclerView createCellRecyclerView() { CellRecyclerView recyclerView = new CellRecyclerView(getContext()); // Disable multitouch recyclerView.setMotionEventSplittingEnabled(false); // Set layout manager recyclerView.setLayoutManager(getCellLayoutManager()); // Set layout params LayoutParams layoutParams = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams .WRAP_CONTENT); layoutParams.leftMargin = mRowHeaderWidth; layoutParams.topMargin = mColumnHeaderHeight; recyclerView.setLayoutParams(layoutParams); if (isShowVerticalSeparators()) { // Add vertical item decoration to display row line on center recycler view recyclerView.addItemDecoration(getVerticalItemDecoration()); } return recyclerView; } public <CH, RH, C> void setAdapter(@Nullable AbstractTableAdapter<CH, RH, C> tableAdapter) { if (tableAdapter != null) { this.mTableAdapter = tableAdapter; this.mTableAdapter.setRowHeaderWidth(mRowHeaderWidth); this.mTableAdapter.setColumnHeaderHeight(mColumnHeaderHeight); this.mTableAdapter.setTableView(this); // set adapters mColumnHeaderRecyclerView.setAdapter(mTableAdapter.getColumnHeaderRecyclerViewAdapter()); mRowHeaderRecyclerView.setAdapter(mTableAdapter.getRowHeaderRecyclerViewAdapter()); mCellRecyclerView.setAdapter(mTableAdapter.getCellRecyclerViewAdapter()); // Create Sort Handler mColumnSortHandler = new ColumnSortHandler(this); // Create Filter Handler mFilterHandler = new FilterHandler<>(this); } } @Override public boolean hasFixedWidth() { return mHasFixedWidth; } public void setHasFixedWidth(boolean hasFixedWidth) { this.mHasFixedWidth = hasFixedWidth; // RecyclerView has also the same control to provide better performance. mColumnHeaderRecyclerView.setHasFixedSize(hasFixedWidth); } @Override public boolean isIgnoreSelectionColors() { return mIgnoreSelectionColors; } public void setIgnoreSelectionColors(boolean ignoreSelectionColor) { this.mIgnoreSelectionColors = ignoreSelectionColor; } @Override public boolean isShowHorizontalSeparators() { return mShowHorizontalSeparators; } @Override public boolean isAllowClickInsideCell(){ return mAllowClickInsideCell; } @Override public boolean isSortable() { return mIsSortable; } public void setShowHorizontalSeparators(boolean showSeparators) { this.mShowHorizontalSeparators = showSeparators; } @Override public boolean isShowVerticalSeparators() { return mShowVerticalSeparators; } public void setShowVerticalSeparators(boolean showSeparators) { this.mShowVerticalSeparators = showSeparators; } @NonNull @Override public CellRecyclerView getCellRecyclerView() { return mCellRecyclerView; } @NonNull @Override public CellRecyclerView getColumnHeaderRecyclerView() { return mColumnHeaderRecyclerView; } @NonNull @Override public CellRecyclerView getRowHeaderRecyclerView() { return mRowHeaderRecyclerView; } @NonNull @Override public ColumnHeaderLayoutManager getColumnHeaderLayoutManager() { if (mColumnHeaderLayoutManager == null) { mColumnHeaderLayoutManager = new ColumnHeaderLayoutManager(getContext(), this); } return mColumnHeaderLayoutManager; } @NonNull @Override public CellLayoutManager getCellLayoutManager() { if (mCellLayoutManager == null) { mCellLayoutManager = new CellLayoutManager(getContext(), this); } return mCellLayoutManager; } @NonNull @Override public LinearLayoutManager getRowHeaderLayoutManager() { if (mRowHeaderLayoutManager == null) { mRowHeaderLayoutManager = new LinearLayoutManager(getContext(), LinearLayoutManager .VERTICAL, false); } return mRowHeaderLayoutManager; } @NonNull @Override public HorizontalRecyclerViewListener getHorizontalRecyclerViewListener() { return mHorizontalRecyclerViewListener; } @NonNull @Override public VerticalRecyclerViewListener getVerticalRecyclerViewListener() { return mVerticalRecyclerListener; } @Nullable @Override public ITableViewListener getTableViewListener() { return mTableViewListener; } public void setTableViewListener(@Nullable ITableViewListener tableViewListener) { this.mTableViewListener = tableViewListener; } @Override public void sortColumn(int columnPosition, @NonNull SortState sortState) { mIsSortable = true; mColumnSortHandler.sort(columnPosition, sortState); } @Override public void sortRowHeader(@NonNull SortState sortState) { mIsSortable = true; mColumnSortHandler.sortByRowHeader(sortState); } @Override public void remeasureColumnWidth(int column) { // Remove calculated width value to be ready for recalculation. getColumnHeaderLayoutManager().removeCachedWidth(column); // Recalculate of the width values of the columns getCellLayoutManager().fitWidthSize(column, false); } @Nullable @Override public AbstractTableAdapter getAdapter() { return mTableAdapter; } @Override public void filter(@NonNull Filter filter) { mFilterHandler.filter(filter); } @Nullable @Override public FilterHandler getFilterHandler() { return mFilterHandler; } @NonNull @Override public SortState getSortingStatus(int column) { return mColumnSortHandler.getSortingStatus(column); } @Nullable @Override public SortState getRowHeaderSortingStatus() { return mColumnSortHandler.getRowHeaderSortingStatus(); } @Override public void scrollToColumnPosition(int column) { mScrollHandler.scrollToColumnPosition(column); } @Override public void scrollToColumnPosition(int column, int offset) { mScrollHandler.scrollToColumnPosition(column, offset); } @Override public void scrollToRowPosition(int row) { mScrollHandler.scrollToRowPosition(row); } @Override public void scrollToRowPosition(int row, int offset) { mScrollHandler.scrollToRowPosition(row, offset); } @NonNull public ScrollHandler getScrollHandler() { return mScrollHandler; } @Override public void showRow(int row) { mVisibilityHandler.showRow(row); } @Override public void hideRow(int row) { mVisibilityHandler.hideRow(row); } @Override public void showAllHiddenRows() { mVisibilityHandler.showAllHiddenRows(); } @Override public void clearHiddenRowList() { mVisibilityHandler.clearHideRowList(); } @Override public void showColumn(int column) { mVisibilityHandler.showColumn(column); } @Override public void hideColumn(int column) { mVisibilityHandler.hideColumn(column); } @Override public boolean isColumnVisible(int column) { return mVisibilityHandler.isColumnVisible(column); } @Override public void showAllHiddenColumns() { mVisibilityHandler.showAllHiddenColumns(); } @Override public void clearHiddenColumnList() { mVisibilityHandler.clearHideColumnList(); } @Override public boolean isRowVisible(int row) { return mVisibilityHandler.isRowVisible(row); } /** * Returns the index of the selected row, -1 if no row is selected. */ public int getSelectedRow() { return mSelectionHandler.getSelectedRowPosition(); } public void setSelectedRow(int row) { // Find the row header view holder which is located on row position. AbstractViewHolder rowViewHolder = (AbstractViewHolder) getRowHeaderRecyclerView() .findViewHolderForAdapterPosition(row); mSelectionHandler.setSelectedRowPosition(rowViewHolder, row); } /** * Returns the index of the selected column, -1 if no column is selected. */ public int getSelectedColumn() { return mSelectionHandler.getSelectedColumnPosition(); } public void setSelectedColumn(int column) { // Find the column view holder which is located on column position . AbstractViewHolder columnViewHolder = (AbstractViewHolder) getColumnHeaderRecyclerView() .findViewHolderForAdapterPosition(column); mSelectionHandler.setSelectedColumnPosition(columnViewHolder, column); } public void setSelectedCell(int column, int row) { // Find the cell view holder which is located on x,y (column,row) position. AbstractViewHolder cellViewHolder = getCellLayoutManager().getCellViewHolder(column, row); mSelectionHandler.setSelectedCellPositions(cellViewHolder, column, row); } @NonNull @Override public SelectionHandler getSelectionHandler() { return mSelectionHandler; } @Nullable @Override public ColumnSortHandler getColumnSortHandler() { return mColumnSortHandler; } @NonNull @Override public VisibilityHandler getVisibilityHandler() { return mVisibilityHandler; } @NonNull @Override public DividerItemDecoration getHorizontalItemDecoration() { if (mHorizontalItemDecoration == null) { mHorizontalItemDecoration = createItemDecoration(DividerItemDecoration.HORIZONTAL); } return mHorizontalItemDecoration; } @NonNull @Override public DividerItemDecoration getVerticalItemDecoration() { if (mVerticalItemDecoration == null) { mVerticalItemDecoration = createItemDecoration(DividerItemDecoration.VERTICAL); } return mVerticalItemDecoration; } @NonNull protected DividerItemDecoration createItemDecoration(int orientation) { DividerItemDecoration itemDecoration = new DividerItemDecoration(getContext(), orientation); Drawable divider = ContextCompat.getDrawable(getContext(), R.drawable.cell_line_divider); if (divider == null) { return itemDecoration; } // That means; There is a custom separator color from user. if (mSeparatorColor != -1) { // Change its color divider.setColorFilter(mSeparatorColor, PorterDuff.Mode.SRC_ATOP); } itemDecoration.setDrawable(divider); return itemDecoration; } /** * This method helps to change default selected color programmatically. * * @param selectedColor It must be Color int. */ public void setSelectedColor(@ColorInt int selectedColor) { this.mSelectedColor = selectedColor; } @ColorInt @Override public int getSelectedColor() { return mSelectedColor; } public void setSeparatorColor(@ColorInt int mSeparatorColor) { this.mSeparatorColor = mSeparatorColor; } @ColorInt @Override public int getSeparatorColor() { return mSeparatorColor; } /** * This method helps to change default unselected color programmatically. * * @param unSelectedColor It must be Color int. */ public void setUnSelectedColor(@ColorInt int unSelectedColor) { this.mUnSelectedColor = unSelectedColor; } @ColorInt @Override public int getUnSelectedColor() { return mUnSelectedColor; } public void setShadowColor(@ColorInt int shadowColor) { this.mShadowColor = shadowColor; } @Override public @ColorInt int getShadowColor() { return mShadowColor; } /** * get row header width * * @return size in pixel */ @Override public int getRowHeaderWidth() { return mRowHeaderWidth; } /** * set RowHeaderWidth * * @param rowHeaderWidth in pixel */ @Override public void setRowHeaderWidth(int rowHeaderWidth) { this.mRowHeaderWidth = rowHeaderWidth; // Update RowHeader layout width ViewGroup.LayoutParams layoutParamsRow = mRowHeaderRecyclerView.getLayoutParams(); layoutParamsRow.width = rowHeaderWidth; mRowHeaderRecyclerView.setLayoutParams(layoutParamsRow); mRowHeaderRecyclerView.requestLayout(); // Update ColumnHeader left margin LayoutParams layoutParamsColumn = (LayoutParams) mColumnHeaderRecyclerView.getLayoutParams(); layoutParamsColumn.leftMargin = rowHeaderWidth; mColumnHeaderRecyclerView.setLayoutParams(layoutParamsColumn); mColumnHeaderRecyclerView.requestLayout(); // Update Cells left margin LayoutParams layoutParamsCell = (LayoutParams) mCellRecyclerView.getLayoutParams(); layoutParamsCell.leftMargin = rowHeaderWidth; mCellRecyclerView.setLayoutParams(layoutParamsCell); mCellRecyclerView.requestLayout(); if (getAdapter() != null) { // update CornerView size getAdapter().setRowHeaderWidth(rowHeaderWidth); } } public void setColumnWidth(int columnPosition, int width) { mColumnWidthHandler.setColumnWidth(columnPosition, width); } @Nullable @Override protected Parcelable onSaveInstanceState() { SavedState state = new SavedState(super.onSaveInstanceState()); // Save all preferences of The TableView before the configuration changed. state.preferences = mPreferencesHandler.savePreferences(); return state; } @Override protected void onRestoreInstanceState(Parcelable state) { if (!(state instanceof SavedState)) { super.onRestoreInstanceState(state); return; } SavedState savedState = (SavedState) state; super.onRestoreInstanceState(savedState.getSuperState()); // Reload the preferences mPreferencesHandler.loadPreferences(savedState.preferences); } }