package com.cleveroad.adaptivetablelayout; import android.annotation.SuppressLint; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Point; import android.graphics.Rect; import android.os.Build; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.v4.util.SparseArrayCompat; import android.support.v4.view.ViewCompat; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; public class AdaptiveTableLayout extends ViewGroup implements ScrollHelper.ScrollHelperListener, AdaptiveTableDataSetObserver { private static final String EXTRA_STATE_SUPER = "EXTRA_STATE_SUPER"; private static final String EXTRA_STATE_VIEW_GROUP = "EXTRA_STATE_VIEW_GROUP"; private static final int SHADOW_THICK = 20; private static final int SHADOW_HEADERS_THICK = 10; /** * Matrix with item view holders */ private SparseMatrix<ViewHolder> mViewHolders; /** * Map with column's headers view holders */ private SparseArrayCompat<ViewHolder> mHeaderColumnViewHolders; /** * Map with row's headers view holders */ private SparseArrayCompat<ViewHolder> mHeaderRowViewHolders; /** * Contained with drag and drop points */ private DragAndDropPoints mDragAndDropPoints; /** * Container with layout state */ private AdaptiveTableState mState; /** * Item's widths and heights manager. */ private AdaptiveTableManager mManager; /** * Need to fix columns bounce when dragging header. * Saved absolute point when header switched in drag and drop mode. */ private Point mLastSwitchHeaderPoint; /** * Contains visible area rect. Left top point and right bottom */ private Rect mVisibleArea; /** * View holder in the left top corner. */ @Nullable private ViewHolder mLeftTopViewHolder; /** * Table layout adapter */ private DataAdaptiveTableLayoutAdapter<ViewHolder> mAdapter; /** * Recycle ViewHolders */ private Recycler mRecycler; /** * Keep layout settings */ private AdaptiveTableLayoutSettings mSettings; /** * Detect all gestures on layout. */ private ScrollHelper mScrollHelper; /** * Runnable helps with fling events */ private SmoothScrollRunnable mScrollerRunnable; /** * Runnable helps with scroll in drag and drop mode */ private DragAndDropScrollRunnable mScrollerDragAndDropRunnable; /** * Helps work with row' or column' shadows. */ private ShadowHelper mShadowHelper; private int mLayoutDirection = ViewCompat.getLayoutDirection(this); private LayoutDirectionHelper mLayoutDirectionHelper; /** * Instant state */ @Nullable private TableInstanceSaver mSaver; public AdaptiveTableLayout(Context context) { super(context); init(context); } public AdaptiveTableLayout(Context context, AttributeSet attrs) { super(context, attrs); init(context); initAttrs(context, attrs); } public AdaptiveTableLayout(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context); initAttrs(context, attrs); } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public AdaptiveTableLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); init(context); initAttrs(context, attrs); } /** * @return true if layout direction is RightToLeft */ public boolean isRTL() { return mLayoutDirectionHelper.isRTL(); } @Override public void setLayoutDirection(int layoutDirection) { super.setLayoutDirection(layoutDirection); mLayoutDirection = layoutDirection; mLayoutDirectionHelper.setLayoutDirection(mLayoutDirection); mShadowHelper.onLayoutDirectionChanged(); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { if (changed) { // calculate layout width and height mSettings.setLayoutWidth(r - l); mSettings.setLayoutHeight(b - t); // init data initItems(); } } private void initAttrs(Context context, AttributeSet attrs) { TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.AdaptiveTableLayout, 0, 0); try { mSettings.setHeaderFixed(a.getBoolean(R.styleable.AdaptiveTableLayout_fixedHeaders, true)); mSettings.setCellMargin(a.getDimensionPixelSize(R.styleable.AdaptiveTableLayout_cellMargin, 0)); mSettings.setSolidRowHeader(a.getBoolean(R.styleable.AdaptiveTableLayout_solidRowHeaders, true)); mSettings.setDragAndDropEnabled(a.getBoolean(R.styleable.AdaptiveTableLayout_dragAndDropEnabled, true)); } finally { a.recycle(); } } private void init(Context context) { mViewHolders = new SparseMatrix<>(); mLayoutDirectionHelper = new LayoutDirectionHelper(mLayoutDirection); mHeaderColumnViewHolders = new SparseArrayCompat<>(); mHeaderRowViewHolders = new SparseArrayCompat<>(); mDragAndDropPoints = new DragAndDropPoints(); mState = new AdaptiveTableState(); mManager = new AdaptiveTableManagerRTL(mLayoutDirectionHelper); mLastSwitchHeaderPoint = new Point(); mVisibleArea = new Rect(); // init scroll and fling helpers mScrollerRunnable = new SmoothScrollRunnable(this); mScrollerDragAndDropRunnable = new DragAndDropScrollRunnable(this); mRecycler = new Recycler(); mSettings = new AdaptiveTableLayoutSettings(); mScrollHelper = new ScrollHelper(context); mScrollHelper.setListener(this); mShadowHelper = new ShadowHelper(mLayoutDirectionHelper); } @Override protected Parcelable onSaveInstanceState() { Bundle bundle = new Bundle(); bundle.putParcelable(EXTRA_STATE_SUPER, super.onSaveInstanceState()); mSaver = new TableInstanceSaver(); mSaver.mScrollX = mState.getScrollX(); mSaver.mScrollY = mState.getScrollY(); mSaver.mLayoutDirection = mLayoutDirection; mSaver.mFixedHeaders = mSettings.isHeaderFixed(); if (mAdapter != null) { mAdapter.onSaveInstanceState(bundle); } bundle.putParcelable(EXTRA_STATE_VIEW_GROUP, mSaver); return bundle; } @Override protected void onRestoreInstanceState(Parcelable state) { Parcelable result = state; if (state instanceof Bundle) { Bundle bundle = (Bundle) state; Parcelable parcelable = bundle.getParcelable(EXTRA_STATE_VIEW_GROUP); if (parcelable != null && parcelable instanceof TableInstanceSaver) { mSaver = (TableInstanceSaver) parcelable; mLayoutDirection = mSaver.mLayoutDirection; setLayoutDirection(mSaver.mLayoutDirection); mSettings.setHeaderFixed(mSaver.mFixedHeaders); } if (mAdapter != null) { mAdapter.onRestoreInstanceState(bundle); } result = bundle.getParcelable(EXTRA_STATE_SUPER); } super.onRestoreInstanceState(result); } private void initItems() { if (mAdapter == null) { // clear mManager.clear(); recycleViewHolders(true); return; } // init manager. Not include headers mManager.init(mAdapter.getRowCount() - 1, mAdapter.getColumnCount() - 1); // calculate widths for (int count = mManager.getColumnCount(), i = 0; i < count; i++) { int item = mAdapter.getColumnWidth(i); mManager.putColumnWidth(i, item); } // calculate heights for (int count = mManager.getRowCount(), i = 0; i < count; i++) { int item = mAdapter.getRowHeight(i); mManager.putRowHeight(i, item); } // set header's width and height. Set 0 in case < 0 mManager.setHeaderColumnHeight(Math.max(0, mAdapter.getHeaderColumnHeight())); mManager.setHeaderRowWidth(Math.max(0, mAdapter.getHeaderRowWidth())); // start calculating full width and full height mManager.invalidate(); // show items in this area mVisibleArea.set(mState.getScrollX(), mState.getScrollY(), mState.getScrollX() + mSettings.getLayoutWidth(), mState.getScrollY() + mSettings.getLayoutHeight()); addViewHolders(mVisibleArea); if (mSaver != null) { scrollTo(mSaver.mScrollX, mSaver.mScrollY); mSaver = null; } else if (isRTL()) { scrollTo(mSettings.getLayoutWidth(), 0); } } /** * Set adapter with IMMUTABLE data. * Create wrapper with links between layout rows, columns and data rows, columns. * On drag and drop event just change links but not change data in adapter. * * @param adapter AdaptiveTableLayout adapter */ @SuppressWarnings("unchecked") public void setAdapter(@Nullable AdaptiveTableAdapter adapter) { if (mAdapter != null) { // remove observers from old adapter mAdapter.unregisterDataSetObserver(this); } if (adapter != null) { // wrap adapter mAdapter = new LinkedAdaptiveTableAdapterImpl<>(adapter, mSettings.isSolidRowHeader()); // register notify callbacks mAdapter.registerDataSetObserver(this); adapter.registerDataSetObserver(new DataSetObserverProxy(mAdapter)); } else { // remove adapter mAdapter = null; } initItems(); } /** * Set adapter with MUTABLE data. * You need to implement switch rows and columns methods. * On drag and drop event calls {@link DataAdaptiveTableLayoutAdapter#changeColumns(int, int)} and * {@link DataAdaptiveTableLayoutAdapter#changeRows(int, int, boolean)} * <p> * DO NOT USE WITH BIG DATA!! * * @param adapter DataAdaptiveTableLayoutAdapter adapter */ @SuppressWarnings("unchecked") public void setAdapter(@Nullable DataAdaptiveTableLayoutAdapter adapter) { if (mAdapter != null) { mAdapter.unregisterDataSetObserver(this); } mAdapter = adapter; if (mAdapter != null) { mAdapter.registerDataSetObserver(this); } if (mSettings.getLayoutHeight() != 0 && mSettings.getLayoutWidth() != 0) { initItems(); } } /** * When used adapter with IMMUTABLE data, returns rows position modifications * (old position -> new position) * * @return row position modification map. Includes only modified row numbers */ @SuppressWarnings("unchecked") public Map<Integer, Integer> getLinkedAdapterRowsModifications() { return mAdapter instanceof LinkedAdaptiveTableAdapterImpl ? ((LinkedAdaptiveTableAdapterImpl) mAdapter).getRowsModifications() : Collections.<Integer, Integer>emptyMap(); } /** * When used adapter with IMMUTABLE data, returns columns position modifications * (old position -> new position) * * @return row position modification map. Includes only modified column numbers */ @SuppressWarnings("unchecked") public Map<Integer, Integer> getLinkedAdapterColumnsModifications() { return mAdapter instanceof LinkedAdaptiveTableAdapterImpl ? ((LinkedAdaptiveTableAdapterImpl) mAdapter).getColumnsModifications() : Collections.<Integer, Integer>emptyMap(); } @Override public void scrollTo(int x, int y) { scrollBy(x, y); } @Override public void scrollBy(int x, int y) { // block scroll one axle int tempX = mState.isRowDragging() ? 0 : x; int tempY = mState.isColumnDragging() ? 0 : y; int diffX = tempX; int diffY = tempY; int shadowShiftX = mManager.getColumnCount() * mSettings.getCellMargin(); int shadowShiftY = mManager.getRowCount() * mSettings.getCellMargin(); long maxX = mManager.getFullWidth() + shadowShiftX; long maxY = mManager.getFullHeight() + shadowShiftY; if (mState.getScrollX() + tempX <= 0) { // scroll over view to the left diffX = mState.getScrollX(); mState.setScrollX(0); } else if (mSettings.getLayoutWidth() > maxX) { // few items and we have free space. diffX = 0; mState.setScrollX(0); } else if (mState.getScrollX() + mSettings.getLayoutWidth() + tempX > maxX) { // scroll over view to the right diffX = (int) (maxX - mState.getScrollX() - mSettings.getLayoutWidth()); mState.setScrollX(mState.getScrollX() + diffX); } else { mState.setScrollX(mState.getScrollX() + tempX); } if (mState.getScrollY() + tempY <= 0) { // scroll over view to the top diffY = mState.getScrollY(); mState.setScrollY(0); } else if (mSettings.getLayoutHeight() > maxY) { // few items and we have free space. diffY = 0; mState.setScrollY(0); } else if (mState.getScrollY() + mSettings.getLayoutHeight() + tempY > maxY) { // scroll over view to the bottom diffY = (int) (maxY - mState.getScrollY() - mSettings.getLayoutHeight()); mState.setScrollY(mState.getScrollY() + diffY); } else { mState.setScrollY(mState.getScrollY() + tempY); } if (diffX == 0 && diffY == 0) { return; } if (mAdapter != null) { // refresh views recycleViewHolders(); mVisibleArea.set(mState.getScrollX(), mState.getScrollY(), mState.getScrollX() + mSettings.getLayoutWidth(), mState.getScrollY() + mSettings.getLayoutHeight()); addViewHolders(mVisibleArea); refreshViewHolders(); } } /** * Refresh all view holders */ private void refreshViewHolders() { if (mAdapter != null) { for (ViewHolder holder : mViewHolders.getAll()) { if (holder != null) { // cell item refreshItemViewHolder(holder, mState.isRowDragging(), mState.isColumnDragging()); } } if (mState.isColumnDragging()) { refreshAllColumnHeadersHolders(); refreshAllRowHeadersHolders(); } else { refreshAllRowHeadersHolders(); refreshAllColumnHeadersHolders(); } if (mLeftTopViewHolder != null) { refreshLeftTopHeaderViewHolder(mLeftTopViewHolder); mLeftTopViewHolder.getItemView().bringToFront(); } } } private void refreshAllColumnHeadersHolders() { for (int count = mHeaderColumnViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderColumnViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderColumnViewHolders.get(key); if (holder != null) { // column header refreshHeaderColumnViewHolder(holder); } } } private void refreshAllRowHeadersHolders() { for (int count = mHeaderRowViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderRowViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderRowViewHolders.get(key); if (holder != null) { // column header refreshHeaderRowViewHolder(holder); } } } /** * Refresh current item view holder. * * @param holder current view holder * @param isRowDragging row dragging state * @param isColumnDragging column dragging state */ private void refreshItemViewHolder(@NonNull ViewHolder holder, boolean isRowDragging, boolean isColumnDragging) { int left = getEmptySpace() + mManager.getColumnsWidth(0, Math.max(0, holder.getColumnIndex())); int top = mManager.getRowsHeight(0, Math.max(0, holder.getRowIndex())); View view = holder.getItemView(); if (isColumnDragging && holder.isDragging() && mDragAndDropPoints.getOffset().x > 0) { // visible dragging column. Calculate left offset using drag and drop points. left = mState.getScrollX() + mDragAndDropPoints.getOffset().x - view.getWidth() / 2; if (!isRTL()) { left -= mManager.getHeaderRowWidth(); } view.bringToFront(); } else if (isRowDragging && holder.isDragging() && mDragAndDropPoints.getOffset().y > 0) { // visible dragging row. Calculate top offset using drag and drop points. top = mState.getScrollY() + mDragAndDropPoints.getOffset().y - view.getHeight() / 2 - mManager.getHeaderColumnHeight(); view.bringToFront(); } int leftMargin = holder.getColumnIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); int topMargin = holder.getRowIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); if (!isRTL()) { left += mManager.getHeaderRowWidth(); } // calculate view layout positions int viewPosLeft = left - mState.getScrollX() + leftMargin; int viewPosRight = viewPosLeft + mManager.getColumnWidth(holder.getColumnIndex()); int viewPosTop = top - mState.getScrollY() + mManager.getHeaderColumnHeight() + topMargin; int viewPosBottom = viewPosTop + mManager.getRowHeight(holder.getRowIndex()); // update layout position view.layout(viewPosLeft, viewPosTop, viewPosRight, viewPosBottom); } /** * Refresh current item view holder with default parameters. * * @param holder current view holder */ private void refreshItemViewHolder(ViewHolder holder) { refreshItemViewHolder(holder, false, false); } /** * Refresh current column header view holder. * * @param holder current view holder */ private void refreshHeaderColumnViewHolder(ViewHolder holder) { int left = getEmptySpace() + mManager.getColumnsWidth(0, Math.max(0, holder.getColumnIndex())); if (!isRTL()) { left += mManager.getHeaderRowWidth(); } int top = mSettings.isHeaderFixed() ? 0 : -mState.getScrollY(); View view = holder.getItemView(); int leftMargin = holder.getColumnIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); int topMargin = holder.getRowIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); if (holder.isDragging() && mDragAndDropPoints.getOffset().x > 0) { left = mState.getScrollX() + mDragAndDropPoints.getOffset().x - view.getWidth() / 2; view.bringToFront(); } if (holder.isDragging()) { View leftShadow = mShadowHelper.getLeftShadow(); View rightShadow = mShadowHelper.getRightShadow(); if (leftShadow != null) { int shadowLeft = left - mState.getScrollX(); leftShadow.layout( Math.max(mManager.getHeaderRowWidth() - mState.getScrollX(), shadowLeft - SHADOW_THICK) + leftMargin, 0, shadowLeft + leftMargin, mSettings.getLayoutHeight()); leftShadow.bringToFront(); } if (rightShadow != null) { int shadowLeft = left + mManager.getColumnWidth(holder.getColumnIndex()) - mState.getScrollX(); rightShadow.layout( Math.max(mManager.getHeaderRowWidth() - mState.getScrollX(), shadowLeft) + leftMargin, 0, shadowLeft + SHADOW_THICK + leftMargin, mSettings.getLayoutHeight()); rightShadow.bringToFront(); } } int viewPosLeft = left - mState.getScrollX() + leftMargin; int viewPosRight = viewPosLeft + mManager.getColumnWidth(holder.getColumnIndex()); int viewPosTop = top + topMargin; int viewPosBottom = viewPosTop + mManager.getHeaderColumnHeight(); //noinspection ResourceType view.layout(viewPosLeft, viewPosTop, viewPosRight, viewPosBottom); if (mState.isRowDragging()) { view.bringToFront(); } if (!mState.isColumnDragging()) { View shadow = mShadowHelper.getColumnsHeadersShadow(); if (shadow == null) { shadow = mShadowHelper.addColumnsHeadersShadow(this); } //noinspection ResourceType shadow.layout(mState.isRowDragging() ? 0 : mSettings.isHeaderFixed() ? 0 : -mState.getScrollX(), top + mManager.getHeaderColumnHeight(), mSettings.getLayoutWidth(), top + mManager.getHeaderColumnHeight() + SHADOW_HEADERS_THICK); shadow.bringToFront(); } } /** * Refresh current row header view holder. * * @param holder current view holder */ private void refreshHeaderRowViewHolder(ViewHolder holder) { int top = mManager.getRowsHeight(0, Math.max(0, holder.getRowIndex())) + mManager.getHeaderColumnHeight(); int left = calculateRowHeadersLeft(); if (isRTL()) { left += mSettings.getCellMargin(); } View view = holder.getItemView(); int leftMargin = holder.getColumnIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); int topMargin = holder.getRowIndex() * mSettings.getCellMargin() + mSettings.getCellMargin(); if (holder.isDragging() && mDragAndDropPoints.getOffset().y > 0) { top = mState.getScrollY() + mDragAndDropPoints.getOffset().y - view.getHeight() / 2; view.bringToFront(); } if (holder.isDragging()) { View topShadow = mShadowHelper.getTopShadow(); View bottomShadow = mShadowHelper.getBottomShadow(); if (topShadow != null) { int shadowTop = top - mState.getScrollY(); topShadow.layout(0, Math.max(mManager.getHeaderColumnHeight() - mState.getScrollY(), shadowTop - SHADOW_THICK) + topMargin, mSettings.getLayoutWidth(), shadowTop + topMargin); topShadow.bringToFront(); } if (bottomShadow != null) { int shadowBottom = top - mState.getScrollY() + mManager.getRowHeight(holder.getRowIndex()); bottomShadow.layout( 0, Math.max(mManager.getHeaderColumnHeight() - mState.getScrollY(), shadowBottom) + topMargin, mSettings.getLayoutWidth(), shadowBottom + SHADOW_THICK + topMargin); bottomShadow.bringToFront(); } } //noinspection ResourceType view.layout(left + leftMargin * (isRTL() ? 0 : 1), top - mState.getScrollY() + topMargin, left + mManager.getHeaderRowWidth() + leftMargin * (isRTL() ? 1 : 0), top + mManager.getRowHeight(holder.getRowIndex()) - mState.getScrollY() + topMargin); if (mState.isColumnDragging()) { view.bringToFront(); } if (!mState.isRowDragging()) { View shadow = mShadowHelper.getRowsHeadersShadow(); if (shadow == null) { shadow = mShadowHelper.addRowsHeadersShadow(this); } int shadowStart, shadowEnd; shadowStart = !isRTL() ? view.getRight() : view.getLeft() - SHADOW_HEADERS_THICK; shadowEnd = shadowStart + SHADOW_HEADERS_THICK; shadow.layout(shadowStart, mState.isColumnDragging() ? 0 : mSettings.isHeaderFixed() ? 0 : -mState.getScrollY(), shadowEnd, mSettings.getLayoutHeight()); shadow.bringToFront(); } } /** * Refresh current row header view holder. * * @param holder current view holder */ private void refreshLeftTopHeaderViewHolder(ViewHolder holder) { int left = calculateRowHeadersLeft(); if (isRTL()) left += mSettings.getCellMargin(); int top = mSettings.isHeaderFixed() ? 0 : -mState.getScrollY(); View view = holder.getItemView(); int leftMargin = isRTL() ? 0 : mSettings.getCellMargin(); int topMargin = mSettings.getCellMargin(); view.layout(left + leftMargin, top + topMargin, left + mManager.getHeaderRowWidth() + leftMargin, top + mManager.getHeaderColumnHeight() + topMargin); } private int calculateRowHeadersLeft() { int left; if (isHeaderFixed()) { if (!isRTL()) { left = 0; } else { left = getRowHeaderStartX(); } } else { if (!isRTL()) { left = -mState.getScrollX(); } else { if (mManager.getFullWidth() <= mSettings.getLayoutWidth()) { left = -mState.getScrollX() + getRowHeaderStartX(); } else { left = -mState.getScrollX() + (int) (mManager.getFullWidth() - mManager.getHeaderRowWidth()) + mManager.getColumnCount() * mSettings.getCellMargin(); } } } return left; } /** * Recycle all views */ private void recycleViewHolders() { recycleViewHolders(false); } /** * Recycle view holders outside screen * * @param isRecycleAll recycle all view holders if true */ private void recycleViewHolders(boolean isRecycleAll) { if (mAdapter == null) { return; } final List<Integer> headerKeysToRemove = new ArrayList<>(); // item view holders for (ViewHolder holder : mViewHolders.getAll()) { if (holder != null && !holder.isDragging()) { View view = holder.getItemView(); // recycle view holder if (isRecycleAll || (view.getRight() < 0 || view.getLeft() > mSettings.getLayoutWidth() || view.getBottom() < 0 || view.getTop() > mSettings.getLayoutHeight())) { // recycle view holder mViewHolders.remove(holder.getRowIndex(), holder.getColumnIndex()); recycleViewHolder(holder); } } } if (!headerKeysToRemove.isEmpty()) { headerKeysToRemove.clear(); } // column header view holders for (int count = mHeaderColumnViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderColumnViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderColumnViewHolders.get(key); if (holder != null) { View view = holder.getItemView(); // recycle view holder if (isRecycleAll || view.getRight() < 0 || view.getLeft() > mSettings.getLayoutWidth()) { headerKeysToRemove.add(key); recycleViewHolder(holder); } } } removeKeys(headerKeysToRemove, mHeaderColumnViewHolders); if (!headerKeysToRemove.isEmpty()) { headerKeysToRemove.clear(); } // row header view holders for (int count = mHeaderRowViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderRowViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderRowViewHolders.get(key); if (holder != null && !holder.isDragging()) { View view = holder.getItemView(); // recycle view holder if (isRecycleAll || view.getBottom() < 0 || view.getTop() > mSettings.getLayoutHeight()) { headerKeysToRemove.add(key); recycleViewHolder(holder); } } } removeKeys(headerKeysToRemove, mHeaderRowViewHolders); } /** * Remove recycled viewholders from sparseArray * * @param keysToRemove List of ViewHolders keys that we need to remove * @param headers SparseArray of viewHolders from where we need to remove */ private void removeKeys(List<Integer> keysToRemove, SparseArrayCompat<ViewHolder> headers) { for (Integer key : keysToRemove) { headers.remove(key); } } /** * Recycle view holder and remove view from layout. * * @param holder view holder to recycle */ private void recycleViewHolder(ViewHolder holder) { mRecycler.pushRecycledView(holder); removeView(holder.getItemView()); mAdapter.onViewHolderRecycled(holder); } private int getEmptySpace() { if (isRTL() && mSettings.getLayoutWidth() > mManager.getFullWidth()) { return mSettings.getLayoutWidth() - (int) mManager.getFullWidth() - mManager.getColumnCount() * mSettings.getCellMargin(); } else { return 0; } } /** * Create and add view holders with views to the layout. * * @param filledArea visible rect */ private void addViewHolders(Rect filledArea) { //search indexes for columns and rows which NEED TO BE showed in this area int leftColumn = mManager.getColumnByXWithShift(filledArea.left, mSettings.getCellMargin()); int rightColumn = mManager.getColumnByXWithShift(filledArea.right, mSettings.getCellMargin()); int topRow = mManager.getRowByYWithShift(filledArea.top, mSettings.getCellMargin()); int bottomRow = mManager.getRowByYWithShift(filledArea.bottom, mSettings.getCellMargin()); for (int i = topRow; i <= bottomRow; i++) { for (int j = leftColumn; j <= rightColumn; j++) { // item view holders ViewHolder viewHolder = mViewHolders.get(i, j); if (viewHolder == null && mAdapter != null) { addViewHolder(i, j, ViewHolderType.ITEM); } } // row view headers holders ViewHolder viewHolder = mHeaderRowViewHolders.get(i); if (viewHolder == null && mAdapter != null) { addViewHolder(i, isRTL() ? mManager.getColumnCount() : 0, ViewHolderType.ROW_HEADER); } else if (viewHolder != null && mAdapter != null) { refreshHeaderRowViewHolder(viewHolder); } } for (int i = leftColumn; i <= rightColumn; i++) { // column view header holders ViewHolder viewHolder = mHeaderColumnViewHolders.get(i); if (viewHolder == null && mAdapter != null) { addViewHolder(0, i, ViewHolderType.COLUMN_HEADER); } else if (viewHolder != null && mAdapter != null) { refreshHeaderColumnViewHolder(viewHolder); } } // add view left top view. if (mLeftTopViewHolder == null && mAdapter != null) { mLeftTopViewHolder = mAdapter.onCreateLeftTopHeaderViewHolder(AdaptiveTableLayout.this); mLeftTopViewHolder.setItemType(ViewHolderType.FIRST_HEADER); View view = mLeftTopViewHolder.getItemView(); view.setTag(R.id.tag_view_holder, mLeftTopViewHolder); addView(view, 0); mAdapter.onBindLeftTopHeaderViewHolder(mLeftTopViewHolder); view.measure( MeasureSpec.makeMeasureSpec(mManager.getHeaderRowWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mManager.getHeaderColumnHeight(), MeasureSpec.EXACTLY)); int viewPosLeft = mSettings.getCellMargin(); if (isRTL()) { viewPosLeft += getRowHeaderStartX(); } int viewPosRight = viewPosLeft + mManager.getHeaderRowWidth(); int viewPosTop = mSettings.getCellMargin(); int viewPosBottom = viewPosTop + mManager.getHeaderColumnHeight(); view.layout(viewPosLeft, viewPosTop, viewPosRight, viewPosBottom); } else if (mLeftTopViewHolder != null && mAdapter != null) { refreshLeftTopHeaderViewHolder(mLeftTopViewHolder); } } private int getBindColumn(int column) { return !isRTL() ? column : mManager.getColumnCount() - 1 - column; } @SuppressWarnings("unused") private void addViewHolder(int row, int column, int itemType) { boolean createdNewView; // need to add new one ViewHolder viewHolder = mRecycler.popRecycledViewHolder(itemType); createdNewView = viewHolder == null; if (createdNewView) { viewHolder = createViewHolder(itemType); } if (viewHolder == null) { return; } // prepare view holder viewHolder.setRowIndex(row); viewHolder.setColumnIndex(column); viewHolder.setItemType(itemType); View view = viewHolder.getItemView(); view.setTag(R.id.tag_view_holder, viewHolder); // add view to the layout addView(view, 0); // save and measure view holder if (itemType == ViewHolderType.ITEM) { mViewHolders.put(row, column, viewHolder); if (createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindViewHolder(viewHolder, row, getBindColumn(column)); } view.measure( MeasureSpec.makeMeasureSpec(mManager.getColumnWidth(column), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mManager.getRowHeight(row), MeasureSpec.EXACTLY)); refreshItemViewHolder(viewHolder); if (!createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindViewHolder(viewHolder, row, getBindColumn(column)); } } else if (itemType == ViewHolderType.ROW_HEADER) { mHeaderRowViewHolders.put(row, viewHolder); if (createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindHeaderRowViewHolder(viewHolder, row); } view.measure( MeasureSpec.makeMeasureSpec(mManager.getHeaderRowWidth(), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mManager.getRowHeight(row), MeasureSpec.EXACTLY)); refreshHeaderRowViewHolder(viewHolder); if (!createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindHeaderRowViewHolder(viewHolder, row); } } else if (itemType == ViewHolderType.COLUMN_HEADER) { mHeaderColumnViewHolders.put(column, viewHolder); if (createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindHeaderColumnViewHolder(viewHolder, getBindColumn(column)); } view.measure( MeasureSpec.makeMeasureSpec(mManager.getColumnWidth(column), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(mManager.getHeaderColumnHeight(), MeasureSpec.EXACTLY)); refreshHeaderColumnViewHolder(viewHolder); if (!createdNewView) { // DO NOT REMOVE THIS!! Fix bug with request layout "requestLayout() improperly called" mAdapter.onBindHeaderColumnViewHolder(viewHolder, getBindColumn(column)); } } } /** * Create view holder by type * * @param itemType view holder type * @return Created view holder */ @Nullable private ViewHolder createViewHolder(int itemType) { if (itemType == ViewHolderType.ITEM) { return mAdapter.onCreateItemViewHolder(AdaptiveTableLayout.this); } else if (itemType == ViewHolderType.ROW_HEADER) { return mAdapter.onCreateRowHeaderViewHolder(AdaptiveTableLayout.this); } else if (itemType == ViewHolderType.COLUMN_HEADER) { return mAdapter.onCreateColumnHeaderViewHolder(AdaptiveTableLayout.this); } return null; } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // intercept event before OnClickListener on item view. mScrollHelper.onTouch(ev); return true; } @SuppressLint("ClickableViewAccessibility") @Override public boolean onTouchEvent(MotionEvent event) { if (mState.isDragging()) { // Drag and drop logic if (event.getAction() == MotionEvent.ACTION_UP) { // end drag and drop event mDragAndDropPoints.setEnd((int) (mState.getScrollX() + event.getX()), (int) (mState.getScrollY() + event.getY())); mLastSwitchHeaderPoint.set(0, 0); return mScrollHelper.onTouch(event); } // calculate absolute x, y int absoluteX = (int) (mState.getScrollX() + event.getX()) - getEmptySpace(); int absoluteY = (int) (mState.getScrollY() + event.getY()); // if column drag and drop mode and column offset > SHIFT_VIEWS_THRESHOLD if (mState.isColumnDragging()) { ViewHolder dragAndDropHolder = mHeaderColumnViewHolders.get(mState.getColumnDraggingIndex()); if (dragAndDropHolder != null) { int fromColumn = dragAndDropHolder.getColumnIndex(); int toColumn = mManager.getColumnByXWithShift(absoluteX, mSettings.getCellMargin()); if (fromColumn != toColumn) { int columnWidth = mManager.getColumnWidth(toColumn); int absoluteColumnX = mManager.getColumnsWidth(0, toColumn); if (!isRTL()) { absoluteColumnX += mManager.getHeaderRowWidth(); } if (fromColumn < toColumn) { // left column is dragging one int deltaX = (int) (absoluteColumnX + columnWidth * 0.6f); if (absoluteX > deltaX) { // move column from left to right for (int i = fromColumn; i < toColumn; i++) { shiftColumnsViews(i, i + 1); } mState.setColumnDragging(true, toColumn); } } else { // right column is dragging one int deltaX = (int) (absoluteColumnX + columnWidth * 0.4f); if (absoluteX < deltaX) { // move column from right to left for (int i = fromColumn; i > toColumn; i--) { shiftColumnsViews(i - 1, i); } mState.setColumnDragging(true, toColumn); } } } } } else if (mState.isRowDragging()) { ViewHolder dragAndDropHolder = mHeaderRowViewHolders.get(mState.getRowDraggingIndex()); if (dragAndDropHolder != null) { int fromRow = dragAndDropHolder.getRowIndex(); int toRow = mManager.getRowByYWithShift(absoluteY, mSettings.getCellMargin()); if (fromRow != toRow) { int rowHeight = mManager.getRowHeight(toRow); int absoluteColumnY = mManager.getRowsHeight(0, toRow) + mManager.getHeaderColumnHeight(); if (fromRow < toRow) { // left column is dragging one int deltaY = (int) (absoluteColumnY + rowHeight * 0.6f); if (absoluteY > deltaY) { // move column from left to right for (int i = fromRow; i < toRow; i++) { shiftRowsViews(i, i + 1); } mState.setRowDragging(true, toRow); } } else { // right column is dragging one int deltaY = (int) (absoluteColumnY + rowHeight * 0.4f); if (absoluteY < deltaY) { // move column from right to left for (int i = fromRow; i > toRow; i--) { shiftRowsViews(i - 1, i); } mState.setRowDragging(true, toRow); } } } } } // set drag and drop offset mDragAndDropPoints.setOffset((int) (event.getX()), (int) (event.getY())); // intercept touch for scroll in drag and drop mode mScrollerDragAndDropRunnable.touch((int) event.getX(), (int) event.getY(), mState.isColumnDragging() ? ScrollType.SCROLL_HORIZONTAL : ScrollType.SCROLL_VERTICAL); // update positions refreshViewHolders(); return true; } return mScrollHelper.onTouch(event); } /** * Method change columns. Change view holders indexes, kay in map, init changing items in adapter. * * @param fromColumn from column index which need to shift * @param toColumn to column index which need to shift */ private void shiftColumnsViews(final int fromColumn, final int toColumn) { if (mAdapter != null) { // change data mAdapter.changeColumns(getBindColumn(fromColumn), getBindColumn(toColumn)); // change view holders switchHeaders(mHeaderColumnViewHolders, fromColumn, toColumn, ViewHolderType.COLUMN_HEADER); // change indexes in array with widths mManager.switchTwoColumns(fromColumn, toColumn); Collection<ViewHolder> fromHolders = mViewHolders.getColumnItems(fromColumn); Collection<ViewHolder> toHolders = mViewHolders.getColumnItems(toColumn); removeViewHolders(fromHolders); removeViewHolders(toHolders); for (ViewHolder holder : fromHolders) { holder.setColumnIndex(toColumn); mViewHolders.put(holder.getRowIndex(), holder.getColumnIndex(), holder); } for (ViewHolder holder : toHolders) { holder.setColumnIndex(fromColumn); mViewHolders.put(holder.getRowIndex(), holder.getColumnIndex(), holder); } } } /** * Method change rows. Change view holders indexes, kay in map, init changing items in adapter. * * @param fromRow from row index which need to shift * @param toRow to row index which need to shift */ private void shiftRowsViews(final int fromRow, final int toRow) { if (mAdapter != null) { // change data mAdapter.changeRows(fromRow, toRow, mSettings.isSolidRowHeader()); // change view holders switchHeaders(mHeaderRowViewHolders, fromRow, toRow, ViewHolderType.ROW_HEADER); // change indexes in array with heights mManager.switchTwoRows(fromRow, toRow); Collection<ViewHolder> fromHolders = mViewHolders.getRowItems(fromRow); Collection<ViewHolder> toHolders = mViewHolders.getRowItems(toRow); removeViewHolders(fromHolders); removeViewHolders(toHolders); for (ViewHolder holder : fromHolders) { holder.setRowIndex(toRow); mViewHolders.put(holder.getRowIndex(), holder.getColumnIndex(), holder); } for (ViewHolder holder : toHolders) { holder.setRowIndex(fromRow); mViewHolders.put(holder.getRowIndex(), holder.getColumnIndex(), holder); } // update row headers if (!mSettings.isSolidRowHeader()) { ViewHolder fromViewHolder = mHeaderRowViewHolders.get(fromRow); ViewHolder toViewHolder = mHeaderRowViewHolders.get(toRow); if (fromViewHolder != null) { mAdapter.onBindHeaderRowViewHolder(fromViewHolder, fromRow); } if (toViewHolder != null) { mAdapter.onBindHeaderRowViewHolder(toViewHolder, toRow); } } } } /** * Method switch view holders in map (map with headers view holders). * * @param map header view holder's map * @param fromIndex index from view holder * @param toIndex index to view holder * @param type type of items (column header or row header) */ @SuppressWarnings("unused") private void switchHeaders(HashMap<Integer, ViewHolder> map, int fromIndex, int toIndex, int type) { ViewHolder fromVh = map.get(fromIndex); if (fromVh != null) { map.remove(fromIndex); if (type == ViewHolderType.COLUMN_HEADER) { fromVh.setColumnIndex(toIndex); } else if (type == ViewHolderType.ROW_HEADER) { fromVh.setRowIndex(toIndex); } } ViewHolder toVh = map.get(toIndex); if (toVh != null) { map.remove(toIndex); if (type == ViewHolderType.COLUMN_HEADER) { toVh.setColumnIndex(fromIndex); } else if (type == ViewHolderType.ROW_HEADER) { toVh.setRowIndex(fromIndex); } } if (fromVh != null) { map.put(toIndex, fromVh); } if (toVh != null) { map.put(fromIndex, toVh); } } /** * Method switch view holders in map (map with headers view holders). * * @param map header view holder's map * @param fromIndex index from view holder * @param toIndex index to view holder * @param type type of items (column header or row header) */ @SuppressWarnings("unused") private void switchHeaders(SparseArrayCompat<ViewHolder> map, int fromIndex, int toIndex, int type) { ViewHolder fromVh = map.get(fromIndex); if (fromVh != null) { map.remove(fromIndex); if (type == ViewHolderType.COLUMN_HEADER) { fromVh.setColumnIndex(toIndex); } else if (type == ViewHolderType.ROW_HEADER) { fromVh.setRowIndex(toIndex); } } ViewHolder toVh = map.get(toIndex); if (toVh != null) { map.remove(toIndex); if (type == ViewHolderType.COLUMN_HEADER) { toVh.setColumnIndex(fromIndex); } else if (type == ViewHolderType.ROW_HEADER) { toVh.setRowIndex(fromIndex); } } if (fromVh != null) { map.put(toIndex, fromVh); } if (toVh != null) { map.put(fromIndex, toVh); } } /** * Remove item view holders from base collection * * @param toRemove Collection with view holders which need to remove */ private void removeViewHolders(@Nullable Collection<ViewHolder> toRemove) { if (toRemove != null) { for (ViewHolder holder : toRemove) { mViewHolders.remove(holder.getRowIndex(), holder.getColumnIndex()); } } } private int getRowHeaderStartX() { return isRTL() ? getRight() - mManager.getHeaderRowWidth() : 0; } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final boolean result; final ViewHolder viewHolder = (ViewHolder) child.getTag(R.id.tag_view_holder); canvas.save(); int headerFixedX = mSettings.isHeaderFixed() ? getRowHeaderStartX() : mState.getScrollX(); int headerFixedY = mSettings.isHeaderFixed() ? 0 : mState.getScrollY(); int itemsAndColumnsLeft = !isRTL() ? Math.max(0, mManager.getHeaderRowWidth() - headerFixedX) : 0; int itemsAndColumnsRight = mSettings.getLayoutWidth(); if (isRTL()) { itemsAndColumnsRight += mSettings.getCellMargin() - mManager.getHeaderRowWidth() * (isHeaderFixed() ? 1 : 0); } //noinspection StatementWithEmptyBody if (viewHolder == null) { //ignore } else if (viewHolder.getItemType() == ViewHolderType.ITEM) { // prepare canvas rect area for draw item (cell in table) canvas.clipRect( itemsAndColumnsLeft, Math.max(0, mManager.getHeaderColumnHeight() - headerFixedY), itemsAndColumnsRight, mSettings.getLayoutHeight()); } else if (viewHolder.getItemType() == ViewHolderType.ROW_HEADER) { // prepare canvas rect area for draw row header canvas.clipRect( getRowHeaderStartX() - mSettings.getCellMargin() * (isRTL() ? 0 : 1), Math.max(0, mManager.getHeaderColumnHeight() - headerFixedY), Math.max(0, getRowHeaderStartX() + mManager.getHeaderRowWidth() + mSettings.getCellMargin()), mSettings.getLayoutHeight()); } else if (viewHolder.getItemType() == ViewHolderType.COLUMN_HEADER) { // prepare canvas rect area for draw column header canvas.clipRect( itemsAndColumnsLeft, 0, itemsAndColumnsRight, Math.max(0, mManager.getHeaderColumnHeight() - headerFixedY)); } else if (viewHolder.getItemType() == ViewHolderType.FIRST_HEADER) { // prepare canvas rect area for draw item (cell in table) canvas.clipRect( !isRTL() ? 0 : getRowHeaderStartX(), 0, !isRTL() ? Math.max(0, mManager.getHeaderRowWidth() - headerFixedX) : Math.max(0, getRowHeaderStartX() + mManager.getHeaderRowWidth()), Math.max(0, mManager.getHeaderColumnHeight() - headerFixedY)); } result = super.drawChild(canvas, child, drawingTime); canvas.restore(); // need to restore here. return result; } @Override public boolean onDown(MotionEvent e) { // stop smooth scrolling if (!mScrollerRunnable.isFinished()) { mScrollerRunnable.forceFinished(); } return true; } @Override public boolean onSingleTapUp(MotionEvent e) { // simple click event ViewHolder viewHolder = getViewHolderByPosition((int) e.getX(), (int) e.getY()); if (viewHolder != null) { OnItemClickListener onItemClickListener = mAdapter.getOnItemClickListener(); if (onItemClickListener != null) { if (viewHolder.getItemType() == ViewHolderType.ITEM) { onItemClickListener.onItemClick(viewHolder.getRowIndex(), getBindColumn(viewHolder.getColumnIndex())); } else if (viewHolder.getItemType() == ViewHolderType.ROW_HEADER) { onItemClickListener.onRowHeaderClick(viewHolder.getRowIndex()); } else if (viewHolder.getItemType() == ViewHolderType.COLUMN_HEADER) { onItemClickListener.onColumnHeaderClick(getBindColumn(viewHolder.getColumnIndex())); } else { onItemClickListener.onLeftTopHeaderClick(); } } } return true; } @Override public void onLongPress(MotionEvent e) { // prepare drag and drop // search view holder by x, y ViewHolder viewHolder = getViewHolderByPosition((int) e.getX(), (int) e.getY()); if (viewHolder != null) { if (!mSettings.isDragAndDropEnabled()) { checkLongPressForItemAndFirstHeader(viewHolder); return; } // save start dragging touch position mDragAndDropPoints.setStart((int) (mState.getScrollX() + e.getX()), (int) (mState.getScrollY() + e.getY())); if (viewHolder.getItemType() == ViewHolderType.COLUMN_HEADER) { // dragging column header mState.setRowDragging(false, viewHolder.getRowIndex()); mState.setColumnDragging(true, viewHolder.getColumnIndex()); // set dragging flags to column's view holder setDraggingToColumn(viewHolder.getColumnIndex(), true); mShadowHelper.removeColumnsHeadersShadow(this); mShadowHelper.addLeftShadow(this); mShadowHelper.addRightShadow(this); // update view refreshViewHolders(); } else if (viewHolder.getItemType() == ViewHolderType.ROW_HEADER) { // dragging column header mState.setRowDragging(true, viewHolder.getRowIndex()); mState.setColumnDragging(false, viewHolder.getColumnIndex()); // set dragging flags to row's view holder setDraggingToRow(viewHolder.getRowIndex(), true); mShadowHelper.removeRowsHeadersShadow(this); mShadowHelper.addTopShadow(this); mShadowHelper.addBottomShadow(this); // update view refreshViewHolders(); } else { checkLongPressForItemAndFirstHeader(viewHolder); } } } private void checkLongPressForItemAndFirstHeader(ViewHolder viewHolder) { OnItemLongClickListener onItemClickListener = mAdapter.getOnItemLongClickListener(); if (onItemClickListener != null) { if (viewHolder.getItemType() == ViewHolderType.ITEM) { onItemClickListener.onItemLongClick(viewHolder.getRowIndex(), viewHolder.getColumnIndex()); } else if (viewHolder.getItemType() == ViewHolderType.FIRST_HEADER) { onItemClickListener.onLeftTopHeaderLongClick(); } } } /** * Method set dragging flag to all view holders in the specific column * * @param column specific column * @param isDragging flag to set */ @SuppressWarnings("unused") private void setDraggingToColumn(int column, boolean isDragging) { Collection<ViewHolder> holders = mViewHolders.getColumnItems(column); for (ViewHolder holder : holders) { holder.setIsDragging(isDragging); } ViewHolder holder = mHeaderColumnViewHolders.get(column); if (holder != null) { holder.setIsDragging(isDragging); } } /** * Method set dragging flag to all view holders in the specific row * * @param row specific row * @param isDragging flag to set */ @SuppressWarnings("unused") private void setDraggingToRow(int row, boolean isDragging) { Collection<ViewHolder> holders = mViewHolders.getRowItems(row); for (ViewHolder holder : holders) { holder.setIsDragging(isDragging); } ViewHolder holder = mHeaderRowViewHolders.get(row); if (holder != null) { holder.setIsDragging(isDragging); } } @Override public boolean onActionUp(MotionEvent e) { if (mState.isDragging()) { // remove shadows from dragging views mShadowHelper.removeAllDragAndDropShadows(this); // stop smooth scrolling if (!mScrollerDragAndDropRunnable.isFinished()) { mScrollerDragAndDropRunnable.stop(); } // remove dragging flag from all item view holders Collection<ViewHolder> holders = mViewHolders.getAll(); for (ViewHolder holder : holders) { holder.setIsDragging(false); } // remove dragging flag from all column header view holders for (int count = mHeaderColumnViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderColumnViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderColumnViewHolders.get(key); if (holder != null) { holder.setIsDragging(false); } } // remove dragging flag from all row header view holders for (int count = mHeaderRowViewHolders.size(), i = 0; i < count; i++) { int key = mHeaderRowViewHolders.keyAt(i); // get the object by the key. ViewHolder holder = mHeaderRowViewHolders.get(key); if (holder != null) { holder.setIsDragging(false); } } // remove dragging flags from state mState.setRowDragging(false, AdaptiveTableState.NO_DRAGGING_POSITION); mState.setColumnDragging(false, AdaptiveTableState.NO_DRAGGING_POSITION); // clear dragging point positions mDragAndDropPoints.setStart(0, 0); mDragAndDropPoints.setOffset(0, 0); mDragAndDropPoints.setEnd(0, 0); // update main layout refreshViewHolders(); } return true; } @Nullable private ViewHolder getViewHolderByPosition(int x, int y) { int tempX = x; int tempY = y; ViewHolder viewHolder; int absX = tempX + mState.getScrollX() - getEmptySpace(); int absY = tempY + mState.getScrollY(); if (!mSettings.isHeaderFixed() && isRTL() && getEmptySpace() == 0) { tempX = absX - mState.getScrollX(); tempY = absY - mState.getScrollY(); } else if (!mSettings.isHeaderFixed() && !isRTL()) { tempX = absX; tempY = absY; } if (tempY < mManager.getHeaderColumnHeight() && tempX < mManager.getHeaderRowWidth() && !isRTL() || tempY < mManager.getHeaderColumnHeight() && tempX > calculateRowHeadersLeft() && isRTL()) { // left top view was clicked viewHolder = mLeftTopViewHolder; } else if (mSettings.isHeaderFixed()) { if (tempY < mManager.getHeaderColumnHeight()) { // coordinate x, y in the column header's area int column = mManager.getColumnByXWithShift(absX, mSettings.getCellMargin()); viewHolder = mHeaderColumnViewHolders.get(column); } else if (tempX < mManager.getHeaderRowWidth() && !isRTL() || tempX > calculateRowHeadersLeft() && isRTL()) { // coordinate x, y in the row header's area int row = mManager.getRowByYWithShift(absY, mSettings.getCellMargin()); viewHolder = mHeaderRowViewHolders.get(row); } else { // coordinate x, y in the items area int column = mManager.getColumnByXWithShift(absX, mSettings.getCellMargin()); int row = mManager.getRowByYWithShift(absY, mSettings.getCellMargin()); viewHolder = mViewHolders.get(row, column); } } else { if (absY < mManager.getHeaderColumnHeight()) { // coordinate x, y in the column header's area int column = mManager.getColumnByXWithShift(absX, mSettings.getCellMargin()); viewHolder = mHeaderColumnViewHolders.get(column); } else if (absX < mManager.getHeaderRowWidth() && !isRTL() || absX - mState.getScrollX() > calculateRowHeadersLeft() - getEmptySpace() && isRTL()) { // coordinate x, y in the row header's area int row = mManager.getRowByYWithShift(absY, mSettings.getCellMargin()); viewHolder = mHeaderRowViewHolders.get(row); } else { // coordinate x, y in the items area int column = mManager.getColumnByXWithShift(absX, mSettings.getCellMargin()); int row = mManager.getRowByYWithShift(absY, mSettings.getCellMargin()); viewHolder = mViewHolders.get(row, column); } } return viewHolder; } @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (!mState.isDragging()) { // simple scroll.... if (!mScrollerRunnable.isFinished()) { mScrollerRunnable.forceFinished(); } scrollBy((int) distanceX, (int) distanceY); } return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { if (!mState.isDragging()) { // simple fling mScrollerRunnable.start( mState.getScrollX(), mState.getScrollY(), (int) velocityX / 2, (int) velocityY / 2, (int) (mManager.getFullWidth() - mSettings.getLayoutWidth() + mManager.getColumnCount() * mSettings.getCellMargin()), (int) (mManager.getFullHeight() - mSettings.getLayoutHeight() + mManager.getRowCount() * mSettings.getCellMargin()) ); } return true; } @Override public void notifyDataSetChanged() { recycleViewHolders(true); mVisibleArea.set(mState.getScrollX(), mState.getScrollY(), mState.getScrollX() + mSettings.getLayoutWidth(), mState.getScrollY() + mSettings.getLayoutHeight()); addViewHolders(mVisibleArea); } @Override public void notifyLayoutChanged() { recycleViewHolders(true); invalidate(); mVisibleArea.set(mState.getScrollX(), mState.getScrollY(), mState.getScrollX() + mSettings.getLayoutWidth(), mState.getScrollY() + mSettings.getLayoutHeight()); addViewHolders(mVisibleArea); } @Override public void notifyItemChanged(int rowIndex, int columnIndex) { ViewHolder holder; if (rowIndex == 0 && columnIndex == 0) { holder = mLeftTopViewHolder; } else if (rowIndex == 0) { holder = mHeaderColumnViewHolders.get(columnIndex - 1); } else if (columnIndex == 0) { holder = mHeaderRowViewHolders.get(rowIndex - 1); } else { holder = mViewHolders.get(rowIndex - 1, columnIndex - 1); } if (holder != null) { viewHolderChanged(holder); } } @Override public void notifyRowChanged(int rowIndex) { Collection<ViewHolder> rowHolders = mViewHolders.getRowItems(rowIndex); for (ViewHolder holder : rowHolders) { viewHolderChanged(holder); } } @Override public void notifyColumnChanged(int columnIndex) { Collection<ViewHolder> columnHolders = mViewHolders.getColumnItems(columnIndex); for (ViewHolder holder : columnHolders) { viewHolderChanged(holder); } } private void viewHolderChanged(@NonNull ViewHolder holder) { if (holder.getItemType() == ViewHolderType.FIRST_HEADER) { mLeftTopViewHolder = holder; mAdapter.onBindLeftTopHeaderViewHolder(mLeftTopViewHolder); } else if (holder.getItemType() == ViewHolderType.COLUMN_HEADER) { mHeaderColumnViewHolders.remove(holder.getColumnIndex()); recycleViewHolder(holder); addViewHolder(holder.getRowIndex(), holder.getColumnIndex(), holder.getItemType()); } else if (holder.getItemType() == ViewHolderType.ROW_HEADER) { mHeaderRowViewHolders.remove(holder.getRowIndex()); recycleViewHolder(holder); addViewHolder(holder.getRowIndex(), holder.getColumnIndex(), holder.getItemType()); } else { mViewHolders.remove(holder.getRowIndex(), holder.getColumnIndex()); recycleViewHolder(holder); addViewHolder(holder.getRowIndex(), holder.getColumnIndex(), holder.getItemType()); } } public boolean isHeaderFixed() { return mSettings.isHeaderFixed(); } public void setHeaderFixed(boolean headerFixed) { mSettings.setHeaderFixed(headerFixed); } public boolean isSolidRowHeader() { return mSettings.isSolidRowHeader(); } public void setSolidRowHeader(boolean solidRowHeader) { mSettings.setSolidRowHeader(solidRowHeader); } public boolean isDragAndDropEnabled() { return mSettings.isDragAndDropEnabled(); } public void setDragAndDropEnabled(boolean enabled) { mSettings.setDragAndDropEnabled(enabled); } private static class TableInstanceSaver implements Parcelable { public static final Creator<TableInstanceSaver> CREATOR = new Creator<TableInstanceSaver>() { @Override public TableInstanceSaver createFromParcel(Parcel source) { return new TableInstanceSaver(source); } @Override public TableInstanceSaver[] newArray(int size) { return new TableInstanceSaver[size]; } }; private int mScrollX; private int mScrollY; private int mLayoutDirection; private boolean mFixedHeaders; public TableInstanceSaver() { // no default data } protected TableInstanceSaver(Parcel in) { mScrollX = in.readInt(); mScrollY = in.readInt(); mLayoutDirection = in.readInt(); mFixedHeaders = in.readByte() != 0; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.mScrollX); dest.writeInt(this.mScrollY); dest.writeInt(this.mLayoutDirection); dest.writeByte((byte) (mFixedHeaders ? 1 : 0)); } } }