Создание таблицы / сетки с замороженным столбцом и замороженными заголовками - PullRequest
18 голосов
/ 28 сентября 2011

Я работаю над небольшим приложением для Android. Часть того, что мне нужно для этого приложения для Android, состоит в том, чтобы иметь сетку с возможностью горизонтальной и вертикальной прокрутки. Однако самый левый столбец должен быть заморожен (всегда на экране, а не в части горизонтальной прокрутки). Точно так же верхняя строка заголовка должна быть заморожена (не часть вертикальной прокрутки)

Надеемся, что эта картина будет четко описывать это, если приведенное выше не имеет особого смысла:

Example of what I would like to do

ключ

  1. Белый: вообще не прокручивать
  2. Синий: вертикальная прокрутка
  3. Красный: горизонтальная прокрутка
  4. Фиолетовый: прокрутка по вертикали и горизонтали

Сделать одно из этих измерений достаточно просто, и я сделал это. Тем не менее, я испытываю затруднения, заставляя оба эти измерения работать (то есть, я могу получить нижнюю часть полностью синим цветом, или я могу получить правильную часть, чтобы все было красным, но не полностью, как указано выше) Код, который у меня есть, приведен ниже и в основном даст следующее:

What I have!

result_grid.xml:

<RelativeLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"
        android:background="@color/lightGrey">

    <LinearLayout
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_below="@id/summaryTableLayout"
        android:layout_weight="0.1"
        android:layout_marginBottom="50dip"
        android:minHeight="100dip">
        <ScrollView
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:scrollbars="vertical">
            <LinearLayout
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:orientation="horizontal">
                <TableLayout
                    android:id="@+id/frozenTable"
                    android:layout_height="wrap_content"
                    android:layout_width="wrap_content"
                    android:layout_marginTop="2dip"
                    android:layout_marginLeft="1dip"
                    android:stretchColumns="1"
                    />

                <HorizontalScrollView
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:layout_toRightOf="@id/frozenTable"
                    android:layout_marginTop="2dip"
                    android:layout_marginLeft="4dip"
                    android:layout_marginRight="1dip">

                    <TableLayout
                        android:id="@+id/contentTable"
                        android:layout_width="fill_parent"
                        android:layout_height="wrap_content"
                        android:stretchColumns="1"/>
                </HorizontalScrollView>
            </LinearLayout>
        </ScrollView>
    </LinearLayout>

    <LinearLayout
        android:layout_height="wrap_content"
        android:layout_width="fill_parent"
        android:orientation="vertical"
        android:layout_weight="0.1"
        android:layout_alignParentBottom="true">
        <Button
            android:id="@+id/backButton"
            android:layout_height="wrap_content"
            android:layout_width="fill_parent"
            android:text="Return"/>
    </LinearLayout>
</RelativeLayout>

Java-код:

private boolean showSummaries;

private TableLayout summaryTable;
private TableLayout frozenTable;
private TableLayout contentTable;

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.result_grid);

    Button backButton = (Button)findViewById(R.id.backButton);
    frozenTable = (TableLayout)findViewById(R.id.frozenTable);
    contentTable = (TableLayout)findViewById(R.id.contentTable);

    ArrayList<String[]> content;

    // [Removed Code] Here I get some data from getIntent().getExtras() that will populate the content ArrayList
    PopulateMainTable(content);
}

private void PopulateMainTable(ArrayList<String[]> content) {
    // [Removed Code] There is some code here to style the table (so it has lines for the rows)

    for (int i = 0; i < content.size(); i++){
        TableRow frozenRow = new TableRow(this);
            // [Removed Code] Styling of the row
        TextView frozenCell = new TextView(this);
        frozenCell.setText(content.get(i)[0]);
        // [Removed Code] Styling of the cell
        frozenRow.addView(frozenCell);
        frozenTable.addView(frozenRow);

        // The rest of them
        TableRow row = new TableRow(this);
        // [Renoved Code] Styling of the row
        for (int j = 1; j < content.get(0).length; j++) {
            TextView rowCell = new TextView(this);
            rowCell.setText(content.get(i)[j]);
            // [Removed Code] Styling of the cell
            row.addView(rowCell);
        }

        contentTable.addView(row);
    }
}

Вот как это выглядит:

Look, headers!

Так вот как выглядит небольшая горизонтальная прокрутка

Bah, no headers!

Вот как это выглядит при вертикальной прокрутке, обратите внимание, что вы теряете заголовки! Это проблема!

Две последние вещи на заметку!

Прежде всего, я не могу поверить, что этого где-то уже нет. (У меня нет Android, поэтому я не смог найти приложения, которые могут это сделать). Тем не менее, я искал по крайней мере два дня в StackOverflow и в Интернете в целом в поисках решения для GridView или TableLayout, которое предоставило бы мне то, что я хотел бы сделать, и пока не нашло решения. Как бы мне ни было стыдно за то, что я пропустил это, если кто-то знает о ресурсе, который описывает, как это сделать, я был бы признателен!

Во-вторых, я попытался «навязать» решение этой проблемы, добавив два LinearLayouts: один захватил «заголовок» той части сетки, которую я хочу создать, а другой - нижнюю «контентную» часть Сетка, которую я хочу создать. Я могу опубликовать этот код, но это уже довольно долго, и я надеюсь, что то, что я имею в виду, очевидно. Этот частично работал , но проблема здесь в том, что заголовки и столбцы содержимого никогда не были выстроены Я хотел использовать getWidth () и setMinimumWidth () для TextViews в TableRows, но, как описано здесь , эти данные были недоступны во время onCreate (а также были недоступны в onPostCreate). Я не смог найти способ заставить это работать, и решение в этой области было бы также замечательно!

Если вы добрались до конца, слава вам!

Ответы [ 3 ]

9 голосов
/ 17 октября 2011

Около недели назад я вернулся к этой проблеме и нашел решение.Решение требует, чтобы я сделал большую ручную настройку ширины для столбцов в этой сетке, и я считаю, что это будет крайне неоправданно в наше время.К сожалению, я также продолжал искать более всестороннее решение, встроенное в платформу Android, но я ничего не нашел.

Ниже приведен код для создания этой же сетки, если какой-либо следующиймне это нужно.Ниже я объясню некоторые более важные детали!

Макет: grid.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:background="@color/lightGrey">

<TableLayout
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:layout_marginBottom="2dip"
    android:layout_weight="1"
    android:minHeight="100dip">
    <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
        <TableLayout
                android:id="@+id/frozenTableHeader"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:layout_marginTop="2dip"
                android:layout_marginLeft="1dip"
                android:stretchColumns="1"
                />

        <qvtcapital.mobile.controls.ObservableHorizontalScrollView
            android:id="@+id/contentTableHeaderHorizontalScrollView"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:layout_toRightOf="@id/frozenTableHeader"
            android:layout_marginTop="2dip"
            android:layout_marginLeft="4dip"
            android:layout_marginRight="1dip">

            <TableLayout
                android:id="@+id/contentTableHeader"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:stretchColumns="1"/>
        </qvtcapital.mobile.controls.ObservableHorizontalScrollView>
    </LinearLayout>
    <ScrollView
        android:id="@+id/verticalScrollView"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:scrollbars="vertical">
        <LinearLayout
            android:layout_width="fill_parent"
            android:layout_height="wrap_content"
            android:orientation="horizontal">
            <TableLayout
                android:id="@+id/frozenTable"
                android:layout_height="wrap_content"
                android:layout_width="wrap_content"
                android:layout_marginTop="2dip"
                android:layout_marginLeft="1dip"
                android:stretchColumns="1"
                />

            <qvtcapital.mobile.controls.ObservableHorizontalScrollView
                android:id="@+id/contentTableHorizontalScrollView"
                android:layout_width="fill_parent"
                android:layout_height="wrap_content"
                android:layout_toRightOf="@id/frozenTable"
                android:layout_marginTop="2dip"
                android:layout_marginLeft="4dip"
                android:layout_marginRight="1dip">

                <TableLayout
                    android:id="@+id/contentTable"
                    android:layout_width="fill_parent"
                    android:layout_height="wrap_content"
                    android:stretchColumns="1"/>
            </qvtcapital.mobile.controls.ObservableHorizontalScrollView>
        </LinearLayout>
    </ScrollView>
</TableLayout>

Активность: Grid.java:

public class ResultGrid extends Activity implements HorizontalScrollViewListener {

private TableLayout frozenHeaderTable;
private TableLayout contentHeaderTable;
private TableLayout frozenTable;
private TableLayout contentTable;

Typeface font;
float fontSize;
int cellWidthFactor;

ObservableHorizontalScrollView headerScrollView;
ObservableHorizontalScrollView contentScrollView;

public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.result_grid);

    font = Typeface.createFromAsset(getAssets(), "fonts/consola.ttf");
    fontSize = 11; // Actually this is dynamic in my application, but that code is removed for clarity
    final float scale = getBaseContext().getResources().getDisplayMetrics().density;
    cellWidthFactor = (int) Math.ceil(fontSize * scale * (fontSize < 10 ? 0.9 : 0.7));

    Button backButton = (Button)findViewById(R.id.backButton);
    frozenTable = (TableLayout)findViewById(R.id.frozenTable);
    contentTable = (TableLayout)findViewById(R.id.contentTable);
    frozenHeaderTable = (TableLayout)findViewById(R.id.frozenTableHeader);
    contentHeaderTable = (TableLayout)findViewById(R.id.contentTableHeader);
    headerScrollView = (ObservableHorizontalScrollView) findViewById(R.id.contentTableHeaderHorizontalScrollView);
    headerScrollView.setScrollViewListener(this);
    contentScrollView = (ObservableHorizontalScrollView) findViewById(R.id.contentTableHorizontalScrollView);
    contentScrollView.setScrollViewListener(this);
    contentScrollView.setHorizontalScrollBarEnabled(false); // Only show the scroll bar on the header table (so that there aren't two)

    backButton.setOnClickListener(backButtonClick);

    InitializeInitialData();
}

protected void InitializeInitialData() {
    ArrayList<String[]> content;

    Bundle myBundle = getIntent().getExtras();
    try {
        content = (ArrayList<String[]>) myBundle.get("gridData");
    } catch (Exception e) {
        content = new ArrayList<String[]>();
        content.add(new String[] {"Error", "There was an error parsing the result data, please try again"} );
        e.printStackTrace();
    }

    PopulateMainTable(content);
}

protected void PopulateMainTable(ArrayList<String[]> content) {
    frozenTable.setBackgroundResource(R.color.tableBorder);
    contentTable.setBackgroundResource(R.color.tableBorder);

    TableLayout.LayoutParams frozenRowParams = new TableLayout.LayoutParams(
            TableLayout.LayoutParams.WRAP_CONTENT,
            TableLayout.LayoutParams.WRAP_CONTENT);
    frozenRowParams.setMargins(1, 1, 1, 1);
    frozenRowParams.weight=1;
    TableLayout.LayoutParams tableRowParams = new TableLayout.LayoutParams(
            TableLayout.LayoutParams.WRAP_CONTENT,
            TableLayout.LayoutParams.WRAP_CONTENT);
    tableRowParams.setMargins(0, 1, 1, 1);
    tableRowParams.weight=1;

    TableRow frozenTableHeaderRow=null;
    TableRow contentTableHeaderRow=null;
    int maxFrozenChars = 0;
    int[] maxContentChars = new int[content.get(0).length-1];

    for (int i = 0; i < content.size(); i++){
        TableRow frozenRow = new TableRow(this);
        frozenRow.setLayoutParams(frozenRowParams);
        frozenRow.setBackgroundResource(R.color.tableRows);
        TextView frozenCell = new TextView(this);
        frozenCell.setText(content.get(i)[0]);
        frozenCell.setTextColor(Color.parseColor("#FF000000"));
        frozenCell.setPadding(5, 0, 5, 0);
        if (0 == i) { frozenCell.setTypeface(font, Typeface.BOLD);
        } else { frozenCell.setTypeface(font, Typeface.NORMAL); }
        frozenCell.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
        frozenRow.addView(frozenCell);
        if (content.get(i)[0].length() > maxFrozenChars) {
            maxFrozenChars = content.get(i)[0].length();
        }

        // The rest of them
        TableRow row = new TableRow(this);
        row.setLayoutParams(tableRowParams);
        row.setBackgroundResource(R.color.tableRows);
        for (int j = 1; j < content.get(0).length; j++) {
            TextView rowCell = new TextView(this);
            rowCell.setText(content.get(i)[j]);
            rowCell.setPadding(10, 0, 0, 0);
            rowCell.setGravity(Gravity.RIGHT);
            rowCell.setTextColor(Color.parseColor("#FF000000"));
            if ( 0 == i) { rowCell.setTypeface(font, Typeface.BOLD);
            } else { rowCell.setTypeface(font, Typeface.NORMAL); }
            rowCell.setTextSize(TypedValue.COMPLEX_UNIT_DIP, fontSize);
            row.addView(rowCell);
            if (content.get(i)[j].length() > maxContentChars[j-1]) {
                maxContentChars[j-1] = content.get(i)[j].length();
            }
        }

        if (i==0) {
            frozenTableHeaderRow=frozenRow;
            contentTableHeaderRow=row;
            frozenHeaderTable.addView(frozenRow);
            contentHeaderTable.addView(row);
        } else {
            frozenTable.addView(frozenRow);
            contentTable.addView(row);
        }
    }

    setChildTextViewWidths(frozenTableHeaderRow, new int[]{maxFrozenChars});
    setChildTextViewWidths(contentTableHeaderRow, maxContentChars);
    for (int i = 0; i < contentTable.getChildCount(); i++) {
        TableRow frozenRow = (TableRow) frozenTable.getChildAt(i);
        setChildTextViewWidths(frozenRow, new int[]{maxFrozenChars});
        TableRow row = (TableRow) contentTable.getChildAt(i);
        setChildTextViewWidths(row, maxContentChars);
    }
}

private void setChildTextViewWidths(TableRow row, int[] widths) {
    if (null==row) {
        return;
    }

    for (int i = 0; i < row.getChildCount(); i++) {
        TextView cell = (TextView) row.getChildAt(i);
        int replacementWidth =
                widths[i] == 1
                        ? (int) Math.ceil(widths[i] * cellWidthFactor * 2)
                        : widths[i] < 3
                            ? (int) Math.ceil(widths[i] * cellWidthFactor * 1.7)
                            : widths[i] < 5
                                ? (int) Math.ceil(widths[i] * cellWidthFactor * 1.2)
                                :widths[i] * cellWidthFactor;
        cell.setMinimumWidth(replacementWidth);
        cell.setMaxWidth(replacementWidth);
    }
}

public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY) {
    if (scrollView==headerScrollView) {
        contentScrollView.scrollTo(x, y);
    } else if (scrollView==contentScrollView) {
        headerScrollView.scrollTo(x, y);
    }
}

Прослушиватель представления прокрутки (чтобы соединить два): HorizontalScrollViewListener.java:

public interface HorizontalScrollViewListener {
    void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
}

Класс ScrollView, который реализует этот прослушиватель: ObservableHorizontalScrollView.java:

public class ObservableHorizontalScrollView extends HorizontalScrollView {
   private HorizontalScrollViewListener scrollViewListener=null;

   public ObservableHorizontalScrollView(Context context) {
       super(context);
   }

   public ObservableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
       super(context, attrs, defStyle);
   }

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

   public void setScrollViewListener(HorizontalScrollViewListener scrollViewListener) {
       this.scrollViewListener = scrollViewListener;
   }

   @Override
   protected void onScrollChanged(int x, int y, int oldX, int oldY) {
       super.onScrollChanged(x, y, oldX, oldY);
       if (null!=scrollViewListener) {
           scrollViewListener.onScrollChanged(this, x, y, oldX, oldY);
       }
   }
}

Действительно важной частью этого является три вида:

  1. ObservableHor HorizontalScrollView позволяет синхронизировать прокручивание таблицы заголовков и таблицы содержимого.По сути, это обеспечивает все горизонтальное движение для сетки.
  2. Способ, которым они остаются выровненными, заключается в обнаружении наибольшей строки, которая будет в столбце.Это делается в конце PopulateMainTable().Пока мы просматриваем каждый из TextView и добавляем их в строки, вы заметите, что есть два массива maxFrozenChars и maxContentChars, которые отслеживают, какое наибольшее значение строки мы видели.В конце PopulateMainTable() мы перебираем каждую строку и для каждой из ячеек мы устанавливаем ее минимальную и максимальную ширину на основе наибольшей строки, которую мы видели в этом столбце.Это обрабатывается setChildTextViewWidths.
  3. Последний элемент, который делает эту работу, - это использование моноширинного шрифта.Вы заметите, что в onCreate я загружаю шрифт consola.ttf, а затем применяю его к каждому из текстовых представлений сетки, которые действуют как ячейки в сетке.Это позволяет нам быть достаточно уверенными, что текст не будет отображаться больше, чем мы установили минимальную и максимальную ширину на предыдущем шаге.Я делаю немного причуды здесь, что со всем cellWidthFactor и максимальным размером этого столбца.Это действительно так, что меньшие строки точно подойдут, в то время как мы можем минимизировать пустое пространство для больших строк, которые (для моей системы) не будут состоять из заглавных букв.Если вы столкнулись с проблемами при использовании этого и получили строки, которые не вписывались в размер столбца, который вы установили, это то место, где вы захотите что-то редактировать.Вы бы хотели изменить переменную replacementWidth другой формулой для определения ширины ячейки, такой как 50 * widths[i], которая была бы довольно большой!Но оставил бы вас с хорошим количеством пробелов в некоторых столбцах.В основном, в зависимости от того, что вы планируете добавить в свою сетку, это может потребоваться изменить.Это то, что сработало для меня.

Надеюсь, это поможет кому-то еще в будущем!

3 голосов
/ 13 ноября 2014

TableFixHeaders библиотека может быть полезна для вас в этом случае.

1 голос
/ 17 октября 2011

Вверх моей головы, вот как я бы подошел к этому:

1) Создайте интерфейс с одним методом, который ваш Activity будет реализовывать для получения координат прокрутки и который ваш ScrollView может вызывать, когдапроисходит прокрутка:

public interface ScrollCallback {
    public void scrollChanged(int newXPos, int newYPos);
}

2) Реализуйте это в своем упражнении, чтобы прокрутить два ограниченных вида прокрутки до положения, к которому только что прокрутился основной вид прокрутки:

@Override
public void scrollChanged(int newXPos, int newYPos) {
    mVerticalScrollView.scrollTo(0, newYPos);
    mHorizontalScrollView.scrollTo(newXPos, 0);
}

3) Подкласс ScrollViewчтобы переопределить метод onScrollChanged () и добавить метод и переменную-член для обратного вызова операции:

private ScrollCallback mCallback;

//...

@Override
protected void onScrollChanged (int l, int t, int oldl, int oldt) {
    mCallback.scrollChanged(l, t);
    super.onScrollChanged(l, t, oldl, oldt);
}

public void setScrollCallback(ScrollCallback callback) {
    mCallback = callback;
}

4) Замените базовый ScrollView в вашем XML новым классом и вызовите setScrollCallback(this) вonCreate().

...