ru.adios.budgeter.widgets.DataTableLayout.java Source code

Java tutorial

Introduction

Here is the source code for ru.adios.budgeter.widgets.DataTableLayout.java

Source

/*
 *
 *  *
 *  *  * Copyright 2015 Michael Kulikov
 *  *  *
 *  *  * 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 ru.adios.budgeter.widgets;

import android.content.Context;
import android.graphics.Canvas;
import android.os.AsyncTask;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.ColorInt;
import android.support.annotation.UiThread;
import android.support.annotation.WorkerThread;
import android.support.v4.content.ContextCompat;
import android.util.AttributeSet;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.Spinner;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import java8.util.Optional;
import java8.util.function.Consumer;
import ru.adios.budgeter.ElementsIdProvider;
import ru.adios.budgeter.R;
import ru.adios.budgeter.api.OptLimit;
import ru.adios.budgeter.api.Order;
import ru.adios.budgeter.api.OrderBy;
import ru.adios.budgeter.api.OrderedField;
import ru.adios.budgeter.api.RepoOption;
import ru.adios.budgeter.util.UiUtils;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;

/**
 * Created by Michail Kulikov
 * 11/5/15
 */
@UiThread
public class DataTableLayout extends TableLayout {

    @ColorInt
    private static final int BORDERS_COLOR = 0xFF4CAC45;
    private static final int MAX_ROW_CAPACITY = 8;
    private static final int DEFAULT_PAGE_SIZE = 10;
    private static final int MIN_PAGE_SIZE = 5;
    private static final int MAX_PAGE_SIZE = 100;

    private DataStore dataStore;
    private int rowsPerSet;
    private int count;

    private Optional<Consumer<DataTableLayout>> listener = Optional.empty();
    private Optional<String> tableName = Optional.empty();
    private Optional<OrderResolver> orderResolver = Optional.empty();
    private boolean tablePopulated = false;
    private Optional<TableRow[]> columnsRow;
    private Optional<LinearLayout> titleView = Optional.empty();
    private LinearLayout footer;
    private Button pressedButton;
    private int itemsPerInnerRow;
    private int itemsInFirstRow;
    private int headerOffset;
    private int knownWidth; // in pixels
    private int knownButtonWidth = 120; // in pixels
    private TextView selectedColumn;
    private boolean insidePageSize = false;
    private Optional<List<Integer>> spinnerContents = Optional.empty();
    private Spinner pageSizeSpinner;
    private final OnClickListener buttonListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            final int w = v.getWidth();
            if (w > 0 && w != knownButtonWidth) {
                knownButtonWidth = w;
            }
            turnToPage(Integer.valueOf(((TextView) v).getText().toString()), false);
            if (tablePopulated) {
                footer.removeAllViews();
                populateFooter();
            }
            doTableReload();
        }
    };

    private final OnClickListener rowOrderListener = new OnClickListener() {
        @Override
        public void onClick(View v) {
            if (orderResolver.isPresent()) {
                final TextView tv = (TextView) v;
                if (tv.isSelected()) {
                    if (orderBy.isPresent()) {
                        orderBy = Optional.of(orderBy.get().flipOrder());
                    }
                } else {
                    final Optional<OrderBy> possibleOrder = orderResolver.get()
                            .byColumnName(tv.getText().toString(), Order.DESC);
                    if (!possibleOrder.isPresent()) {
                        return;
                    }
                    orderBy = Optional.of(possibleOrder.get());
                    selectOrderByColumn(tv);
                }
                clearContents();
                loadTableProcedures();
            }
        }
    };

    // -------------TRANSIENT STATE-----------------------
    private int pageSize = DEFAULT_PAGE_SIZE;
    private int currentPage;
    private OptLimit pageLimit;
    private Optional<OrderBy> orderBy = Optional.empty();
    // ---------------------------------------------------

    private int dp1;
    private int dp2;
    private int dp3;
    private int dp4;
    private int dp5;

    public DataTableLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public DataTableLayout(Context context, int rowsPerSet, DataStore dataStore) {
        super(context);
        checkArgument(rowsPerSet >= 1, "rowsPerSet must be positive");
        checkArgument(dataStore != null, "dataStore is null");
        this.rowsPerSet = rowsPerSet;
        this.dataStore = dataStore;
        init();
    }

    public DataTableLayout(Context context, DataStore dataStore) {
        this(context, 1, dataStore);
    }

    public void start() {
        if (!tablePopulated) {
            if (getChildCount() == 0) {
                populateFraming(dataStore.getDataHeaders());
            }
            loadDataAndPopulateTable();
        }
    }

    public void repopulate() {
        if (tablePopulated) {
            clearContents();
        } else {
            removeAllViews();
            populateFraming(dataStore.getDataHeaders());
        }
        loadDataAndPopulateTable();
    }

    public void setTableName(String tableName) {
        this.tableName = Optional.of(tableName);
    }

    public void enableUserOrdering(OrderResolver orderResolver) {
        checkArgument(orderResolver != null, "orderResolver is null");
        this.orderResolver = Optional.of(orderResolver);
        if (getChildCount() > 0) {
            final TableRow columnsRow = (TableRow) getChildAt(2);
            for (int i = 0; i < columnsRow.getChildCount(); i++) {
                final TextView col = (TextView) columnsRow.getChildAt(i);
                final String text = col.getText().toString();

                if (!orderBy.isPresent()) {
                    final Optional<OrderBy> possibleOrder = orderResolver.byColumnName(text, Order.DESC);
                    if (!possibleOrder.isPresent()) {
                        continue;
                    }
                    orderBy = Optional.of(possibleOrder.get());
                    selectOrderByColumn(col);
                    if (tablePopulated) {
                        clearContents();
                        loadTableProcedures();
                    }
                    return;
                }

                if (text.equals(orderResolver.byField(orderBy.get().field).orElse(null))) {
                    if (!col.isSelected()) {
                        selectOrderByColumn(col);
                        col.invalidate();
                    }
                    break;
                }
            }
        }
    }

    public void disableUserOrdering() {
        orderResolver = Optional.empty();
        if (getChildCount() > 0) {
            final TableRow columnsRow = (TableRow) getChildAt(2);
            for (int i = 0; i < columnsRow.getChildCount(); i++) {
                final View childAt = columnsRow.getChildAt(i);
                if (childAt.isSelected()) {
                    childAt.setSelected(false);
                    childAt.invalidate();
                    break;
                }
            }
        }
    }

    public void setOnDataLoadedListener(Consumer<DataTableLayout> listener) {
        this.listener = Optional.ofNullable(listener);
    }

    public void setPageSize(int pageSize) {
        setPageSizeInternal(pageSize, true);
    }

    private void setPageSizeInternal(int pageSize, boolean turnThePage) {
        if (insidePageSize) {
            return;
        }
        checkArgument(pageSize > 0, "Page size must be positive");

        insidePageSize = true;
        try {
            if (pageSize == this.pageSize) {
                return;
            }

            this.pageSize = pageSize;

            updateSpinnerContents(pageSize);

            if (turnThePage) {
                int page = 1;
                if (pageLimit != null && pageLimit.offset > 0) {
                    page = pageLimit.offset / pageSize + 1;
                }
                turnToPage(page, true);
            }

            if (footer != null) {
                footer.removeAllViews();
                populateFooter();
            }
        } finally {
            insidePageSize = false;
        }
    }

    private void updateSpinnerContents(int newPageSize) {
        if (spinnerContents.isPresent()) {
            final List<Integer> contents = spinnerContents.get();
            if (!contents.contains(newPageSize)) {
                contents.add(newPageSize);
                Collections.sort(contents);
                pageSizeSpinner.setSelection(contents.indexOf(newPageSize));
            }
        }
    }

    public void setOrderBy(OrderBy orderBy) {
        setOrderByInternal(orderBy, true);
    }

    private void setOrderByInternal(OrderBy orderBy, boolean doReloadIfNeeded) {
        checkState(!orderResolver.isPresent() || orderBy != null,
                "User ordering enabled, cannot set no ordering at all");

        if ((this.orderBy.isPresent() && !this.orderBy.get().equals(orderBy))
                || (!this.orderBy.isPresent() && orderBy != null)) {
            if (orderResolver.isPresent() && columnsRow.isPresent()) {
                final TableRow[] colsRow = columnsRow.get();

                outer: for (final TableRow r : colsRow) {
                    for (int j = 0; j < r.getChildCount(); j++) {
                        final TextView col = (TextView) r.getChildAt(j);

                        if (col.getText().toString()
                                .equals(orderResolver.get().byField(orderBy.field).orElse(null))) {
                            if (!col.isSelected()) {
                                selectOrderByColumn(col);
                            }
                            break outer;
                        }
                    }
                }
            }

            this.orderBy = Optional.ofNullable(orderBy);

            if (tablePopulated && doReloadIfNeeded) {
                clearContents();
                loadTableProcedures();
            }
        }
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        Parcelable superState = super.onSaveInstanceState();
        return new SavedState(superState, pageSize, currentPage, orderBy);
    }

    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        SavedState savedState = (SavedState) state;
        super.onRestoreInstanceState(savedState.getSuperState());

        setOrderByInternal(savedState.orderBy.orElse(null), false);
        setPageSizeInternal(savedState.pageSize, false);
        turnToPage(savedState.currentPage, true);
    }

    @Override
    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
        super.dispatchFreezeSelfOnly(container);
    }

    @Override
    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
        super.dispatchThawSelfOnly(container);
    }

    private void setNewPressedPageButton(Button thisBtn) {
        if (pressedButton != null) {
            pressedButton.setPressed(false);
            pressedButton.setClickable(true);
        }
        thisBtn.setClickable(false);
        thisBtn.setPressed(true);
        pressedButton = thisBtn;
    }

    private void init() {
        setWillNotDraw(false);

        if (!isInEditMode()) {
            final int setSize = dataStore.getDataHeaders().size();
            checkArgument(setSize >= rowsPerSet,
                    "Data store returned set size less than rowsPerSet provided in constructor");
            itemsPerInnerRow = setSize / rowsPerSet;
            checkArgument(itemsPerInnerRow < MAX_ROW_CAPACITY, "Row appears to be too large: %s", itemsPerInnerRow);
            itemsInFirstRow = setSize % rowsPerSet + itemsPerInnerRow;
            checkArgument(itemsInFirstRow < MAX_ROW_CAPACITY,
                    "First row appears to be too large: %s, while others are %s", itemsInFirstRow,
                    itemsPerInnerRow);
        }
    }

    private void loadDataAndPopulateTable() {
        new AsyncTask<DataStore, Void, Integer>() {
            @Override
            protected Integer doInBackground(DataStore... params) {
                return params[0].count();
            }

            @Override
            protected void onPostExecute(Integer c) {
                count = c;
                if (c == 0) {
                    insertNoDataRow();
                    tablePopulated = true;
                    if (listener.isPresent()) {
                        listener.get().accept(DataTableLayout.this);
                    }
                    invalidate();
                    return;
                }

                if (currentPage == 0) {
                    turnToPage(1, false);
                }
                if (c > pageSize) {
                    final Context context = getContext();

                    if (tableName.isPresent()) {
                        pageSizeSpinner = new Spinner(context, Spinner.MODE_DROPDOWN);
                        pageSizeSpinner.setMinimumWidth(UiUtils.dpAsPixels(context, 70));
                        pageSizeSpinner.setId(ElementsIdProvider.getNextId());
                        final List<Integer> contents = getPageSpinnerContents();
                        spinnerContents = Optional.of(contents);
                        pageSizeSpinner.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_spinner_item,
                                android.R.id.text1, contents));
                        pageSizeSpinner.setSelection(contents.indexOf(pageSize));
                        pageSizeSpinner.setLayoutParams(new LinearLayout.LayoutParams(
                                TableRow.LayoutParams.WRAP_CONTENT, TableRow.LayoutParams.WRAP_CONTENT, 2f));
                        pageSizeSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
                            @Override
                            public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
                                setPageSize((Integer) parent.getAdapter().getItem(position));
                            }

                            @Override
                            public void onNothingSelected(AdapterView<?> parent) {
                            }
                        });
                        pageSizeSpinner.setVisibility(VISIBLE);

                        if (!titleView.isPresent()) {
                            addTitleRowWithSeparator(context, 10f, 8f);
                        } else {
                            titleView.get().setWeightSum(10f);
                            titleView.get().getChildAt(0).setLayoutParams(
                                    new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                                            LinearLayout.LayoutParams.WRAP_CONTENT, 8f));
                        }
                        titleView.get().addView(pageSizeSpinner, 0);
                    }
                    final TableRow.LayoutParams fp = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
                            TableRow.LayoutParams.WRAP_CONTENT, 1f);
                    fp.span = itemsPerInnerRow;
                    footer.setLayoutParams(fp);
                    footer.setVisibility(VISIBLE);
                    populateFooter();
                }
                loadTableProcedures();
            }
        }.execute(dataStore);
    }

    private List<Integer> getPageSpinnerContents() {
        final ArrayList<Integer> list = new ArrayList<>(11);
        int curValue = 0, circle = 0, additive = MIN_PAGE_SIZE, min = Math.min(MAX_PAGE_SIZE, count);
        boolean foundCurPSize = false;

        while (curValue < min) {
            if (circle++ == 3) {
                additive *= 2;
                circle = 0;
            }
            final int c = curValue += additive;
            if (c == pageSize) {
                foundCurPSize = true;
            }
            list.add(c);
        }

        if (!foundCurPSize) {
            list.add(pageSize);
            Collections.sort(list);
        }

        return list;
    }

    private void turnToPage(int numPage, boolean reloadTable) {
        checkArgument(numPage > 0, "page number must be positive");
        currentPage = numPage;
        pageLimit = numPage == 1 ? OptLimit.createLimit(pageSize)
                : OptLimit.create(pageSize, pageSize * (numPage - 1));
        if (reloadTable) {
            doTableReload();
        }
    }

    private void doTableReload() {
        if (tablePopulated) {
            clearContents();
            loadTableProcedures();
        }
    }

    private void clearContents() {
        for (int i = getChildCount() - 4; i >= headerOffset; i--) {
            removeViewAt(i);
        }
        tablePopulated = false;
    }

    private void loadTableProcedures() {
        new AsyncTask<DataStore, Void, List<Iterable<String>>>() {
            @Override
            protected List<Iterable<String>> doInBackground(DataStore... params) {
                return orderBy.isPresent() ? params[0].loadData(pageLimit, orderBy.get())
                        : params[0].loadData(pageLimit);
            }

            @Override
            protected void onPostExecute(List<Iterable<String>> iterables) {
                int rowId = headerOffset;
                for (final Iterable<String> dataSet : iterables) {
                    rowId = addDataRow(dataSet, rowId, Optional.<Consumer<TextView>>empty());
                }
                tablePopulated = true;
                if (listener.isPresent()) {
                    listener.get().accept(DataTableLayout.this);
                }
                invalidate();
            }
        }.execute(dataStore);
    }

    private void populateFraming(List<String> headers) {
        final Context context = getContext();

        addView(constructRowSeparator(1));
        headerOffset++;

        if (tableName.isPresent()) {
            addTitleRowWithSeparator(context, 1f, 1f);
        }

        if (orderResolver.isPresent() && !orderBy.isPresent()) {
            for (final String h : headers) {
                final Optional<OrderBy> possibleOrder = orderResolver.get().byColumnName(h, Order.DESC);
                if (possibleOrder.isPresent()) {
                    orderBy = Optional.of(possibleOrder.get());
                    break;
                }
            }
        }

        final int curHdrOff = rowsPerSet > 1 ? headerOffset + 1 : headerOffset;
        headerOffset = addDataRow(headers, headerOffset, Optional.<Consumer<TextView>>of(new Consumer<TextView>() {
            @Override
            public void accept(TextView textView) {
                if (orderResolver.isPresent() && orderBy.isPresent() && textView.getText().toString()
                        .equals(orderResolver.get().byField(orderBy.get().field).orElse(null))) {
                    selectOrderByColumn(textView);
                }
                textView.setOnClickListener(rowOrderListener);
            }
        }));
        final int colArrLength = headerOffset - curHdrOff;
        final TableRow[] rArr = new TableRow[colArrLength];
        for (int i = 0; i < colArrLength; i++) {
            rArr[i] = (TableRow) getChildAt(curHdrOff + i);
        }
        columnsRow = Optional.of(rArr);

        addView(constructRowSeparator(1));
        final TableRow footerRow = constructRow(context, 1f);
        footer = new LinearLayout(context);
        final TableRow.LayoutParams fp = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT, 0, 1f);
        fp.span = itemsPerInnerRow;
        footer.setLayoutParams(fp);
        footer.setBackgroundResource(R.drawable.cell_shape);
        footer.setGravity(Gravity.CENTER);
        footer.setOrientation(HORIZONTAL);
        footer.setVisibility(GONE);
        footerRow.addView(footer);
        addView(footerRow);
        addView(constructRowSeparator(1));
    }

    /**
     * Requires tableName to be checked, i.e. isPresent() == true .
     */
    private void addTitleRowWithSeparator(Context context, float layoutWeightSum, float titleViewWeight) {
        final TableRow tableNameRow = new TableRow(context);
        tableNameRow.setId(ElementsIdProvider.getNextId());
        tableNameRow.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        tableNameRow.setWeightSum(1f);

        titleView = Optional
                .of(getCenteredRowText(context, tableName.get(), layoutWeightSum, true, titleViewWeight));
        tableNameRow.addView(titleView.get());
        addView(tableNameRow, 1);
        addView(constructRowSeparator(1), 2);
        headerOffset += 2;
    }

    private void selectOrderByColumn(TextView col) {
        if (selectedColumn != null) {
            selectedColumn.setSelected(false);
            selectedColumn.invalidate();
        }
        col.setSelected(true);
        col.invalidate();
        selectedColumn = col;
    }

    private LinearLayout getCenteredRowText(Context context, String text, float layoutWeightSum, boolean largeText,
            float textViewWeight) {
        final LinearLayout inner = new LinearLayout(context);
        final TableRow.LayoutParams innerParams = new TableRow.LayoutParams(TableRow.LayoutParams.MATCH_PARENT,
                TableRow.LayoutParams.WRAP_CONTENT, 1f);
        innerParams.span = itemsPerInnerRow;
        inner.setLayoutParams(innerParams);
        inner.setWeightSum(layoutWeightSum);
        inner.setBackgroundResource(R.drawable.cell_shape);
        final TextView nameTextView = new TextView(context);
        nameTextView.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,
                LinearLayout.LayoutParams.WRAP_CONTENT, textViewWeight));
        nameTextView.setTextAppearance(context,
                largeText ? android.R.style.TextAppearance_Large : android.R.style.TextAppearance_Medium);
        nameTextView.setText(text);
        nameTextView.setGravity(Gravity.CENTER);
        inner.addView(nameTextView);
        return inner;
    }

    private void insertNoDataRow() {
        final Context context = getContext();
        final TableRow noDataRow = constructRow(context, 1f);
        noDataRow.addView(
                getCenteredRowText(context, getResources().getString(R.string.data_table_no_rows), 1f, false, 1f));
        addView(noDataRow, headerOffset);
    }

    private int addDataRow(Iterable<String> dataSet, int rowId, Optional<Consumer<TextView>> optional) {
        final Context context = getContext();

        if (rowsPerSet > 1) {
            addView(constructRowSeparator(rowsPerSet), rowId++);
        }

        int i = 0;
        boolean firstInner = true, fistRow = true;
        TableRow currentRow = constructRow(context, itemsInFirstRow * 2f);
        for (final String str : dataSet) {
            if (i > 0 && (fistRow ? i % itemsInFirstRow == 0 : i % itemsPerInnerRow == 0)) {
                i = 0;
                fistRow = false;
                addView(currentRow, rowId++);
                currentRow = constructRow(context, itemsPerInnerRow * 2f);
                firstInner = true;
            }

            final TextView textView;
            if (firstInner) {
                textView = createSpyingColumnForTableRow(str, rowId, 2f, context);
                firstInner = false;
            } else {
                textView = createColumnForTableRow(str, 2f, context);
            }
            final Optional<Integer> maxWidth = dataStore.getMaxWidthForData(i);
            if (maxWidth.isPresent()) {
                textView.setMaxWidth(UiUtils.dpAsPixels(context, maxWidth.get()));
            }

            if (optional.isPresent()) {
                optional.get().accept(textView);
            }

            currentRow.addView(textView);

            i++;
        }

        addView(currentRow, rowId++);

        return rowId;
    }

    private TextView createColumnForTableRow(String text, float weight, Context context) {
        final TextView view = new TextView(context);
        populateColumn(view, context, weight);
        view.setText(text);
        return view;
    }

    private TextView createSpyingColumnForTableRow(String text, final int rowId, final float weight,
            Context context) {
        final SpyingTextView view = new SpyingTextView(context);
        populateColumn(view, context, weight);
        view.heightCatch = true;
        view.setHeightRunnable(new Runnable() {
            @Override
            public void run() {
                final TableRow row = (TableRow) getChildAt(rowId);
                final int childCount = row.getChildCount();

                int maxHeight = 0;
                for (int i = 0; i < childCount; i++) {
                    final TextView childAt = (TextView) row.getChildAt(i);
                    maxHeight = Math.max(maxHeight, childAt.getHeight());
                }

                for (int i = 0; i < childCount; i++) {
                    final TextView childAt = (TextView) row.getChildAt(i);
                    childAt.setLayoutParams(
                            new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT, maxHeight, weight));
                }

                invalidate();
            }
        });
        view.setText(text);
        return view;
    }

    private void populateColumn(TextView view, Context context, float weight) {
        view.setLayoutParams(new TableRow.LayoutParams(TableRow.LayoutParams.WRAP_CONTENT,
                TableRow.LayoutParams.WRAP_CONTENT, weight));
        view.setId(ElementsIdProvider.getNextId());
        view.setBackground(ContextCompat.getDrawable(context, R.drawable.cell_shape));
        final int fiveDp = getDpAsPixels(5, context);
        view.setPadding(fiveDp, fiveDp, fiveDp, fiveDp);
        view.setTextAppearance(context, android.R.style.TextAppearance_Small);
    }

    private TableRow constructRow(Context context, float weightSum) {
        final TableRow row = new TableRow(context);
        row.setId(ElementsIdProvider.getNextId());
        row.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT));
        row.setWeightSum(weightSum);
        return row;
    }

    private View constructRowSeparator(int heightDp) {
        final Context context = getContext();
        final View view = new View(context);
        view.setBackgroundColor(BORDERS_COLOR);
        view.setLayoutParams(new TableLayout.LayoutParams(TableLayout.LayoutParams.MATCH_PARENT,
                getDpAsPixels(heightDp, context)));
        return view;
    }

    private void populateFooter() {
        if (knownWidth > 0 && footer != null && footer.getVisibility() == VISIBLE && footer.getChildCount() == 0) {
            final int maxButtons = knownWidth / knownButtonWidth;
            final int numPages = count / pageSize + 1;
            final int numButtons = Math.min(maxButtons, numPages);

            if (numButtons > 1) {
                int from = currentPage - numButtons / 2;
                int till = currentPage + numButtons / 2;
                if (from < 1) {
                    till += (1 - from);
                    from = 1;
                } else if (till > numPages) {
                    from -= (till - numPages);
                    till = numPages;
                }
                int i = 0;
                for (int page = from; page <= till; page++) {
                    if (page < 1) {
                        continue;
                    }
                    if (page > numPages) {
                        break;
                    }
                    footer.addView(createButtonForFooter(page, page == currentPage));
                    if (++i >= numButtons) {
                        break;
                    }
                }
            } else {
                footer.addView(createButtonForFooter(1, true));
            }
            footer.invalidate();
        }
    }

    private Button createButtonForFooter(int pageNum, boolean pressed) {
        final Button button = new Button(getContext());
        button.setText(String.valueOf(pageNum));
        if (pressed) {
            setNewPressedPageButton(button);
        }
        button.setOnClickListener(buttonListener);
        return button;
    }

    private int getDpAsPixels(int dps, Context context) {
        switch (dps) {
        case 1:
            if (dp1 == 0) {
                dp1 = UiUtils.dpAsPixels(context, dps);
            }
            return dp1;
        case 2:
            if (dp2 == 0) {
                dp2 = UiUtils.dpAsPixels(context, dps);
            }
            return dp2;
        case 3:
            if (dp3 == 0) {
                dp3 = UiUtils.dpAsPixels(context, dps);
            }
            return dp3;
        case 4:
            if (dp4 == 0) {
                dp4 = UiUtils.dpAsPixels(context, dps);
            }
            return dp4;
        case 5:
            if (dp5 == 0) {
                dp5 = UiUtils.dpAsPixels(context, dps);
            }
            return dp5;
        default:
            return UiUtils.dpAsPixels(context, dps);
        }
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        dp1 = 0;
        dp2 = 0;
        dp3 = 0;
        dp4 = 0;
        dp5 = 0;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        knownWidth = getWidth();
        if (footer != null && footer.getChildCount() > 0) {
            final int w = footer.getChildAt(0).getWidth();
            if (w > 0 && w != knownButtonWidth) {
                knownButtonWidth = w;
            }
        }
        populateFooter();
        super.onDraw(canvas);
    }

    public interface DataStore {

        /**
         * For execution in a separate thread.
         * @param options optional pagination for table to request.
         * @return data in string form.
         */
        @WorkerThread
        List<Iterable<String>> loadData(RepoOption... options);

        /**
         * For execution in a separate thread.
         * @return count of total data records.
         */
        @WorkerThread
        int count();

        @UiThread
        List<String> getDataHeaders();

        @UiThread
        Optional<Integer> getMaxWidthForData(int index);

    }

    @UiThread
    public interface OrderResolver {

        Optional<OrderBy> byColumnName(String columnName, Order order);

        Optional<String> byField(OrderedField field);

    }

    protected static class SavedState extends BaseSavedState {

        protected final int pageSize;
        protected final int currentPage;
        protected final Optional<OrderBy> orderBy;

        private SavedState(Parcelable superState, int pageSize, int currentPage, Optional<OrderBy> orderBy) {
            super(superState);
            this.pageSize = pageSize;
            this.currentPage = currentPage;
            this.orderBy = orderBy;
        }

        private SavedState(Parcel in) {
            super(in);
            pageSize = in.readInt();
            currentPage = in.readInt();
            final Serializable obAct = in.readSerializable();
            orderBy = obAct != null ? Optional.of((OrderBy) obAct) : Optional.<OrderBy>empty();
        }

        @Override
        public void writeToParcel(Parcel destination, int flags) {
            super.writeToParcel(destination, flags);
            destination.writeInt(pageSize);
            destination.writeInt(currentPage);
            destination.writeSerializable(orderBy.orElse(null));
        }

        public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {

            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }

        };

    }

}