Создание таблицы / сетки с замороженным столбцом и замороженными заголовками

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

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

Пример того, что я хотел бы сделать

Ключ :

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

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

Что у меня есть!

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); } } 

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

Смотрите, заголовки!

Таким образом, это похоже на немного горизонтальную прокрутку

Ба, нет заголовков!

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

Два последних примечания!

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

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

Если вы сделали это так далеко до конца, то вам это повезло!

Solutions Collecting From Web of "Создание таблицы / сетки с замороженным столбцом и замороженными заголовками"

Около недели назад я снова просмотрел эту проблему и придумал решение. Решение требует, чтобы я делал много ручной настройки ширины для столбцов в этой сетке, и я считаю, что это очень субар в этот день и возраст. К сожалению, я также продолжал искать более всестороннее решение, привычное для платформы 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> - <?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. ObservableHorizontalScrollView позволяет таблице заголовков и таблице содержимого прокручиваться синхронно. В принципе, это обеспечивает все горизонтальное движение для сетки.
  2. Способ, которым они остаются выровненными, заключается в обнаружении самой большой строки, которая будет находиться в столбце. Это делается в конце PopulateMainTable() . Пока мы проходим через каждый из TextViews и добавляем их в строки, вы заметите, что есть два массива maxFrozenChars и maxContentChars которые отслеживают, какое наибольшее значение строки мы видели. В конце PopulateMainTable() мы прокручиваем каждую из строк, и для каждой из ячеек мы устанавливаем минимальную и максимальную ширину на основе самой большой строки, которую мы видели в этом столбце. Это обрабатывается setChildTextViewWidths .
  3. Последний элемент, который делает эту работу, – использовать моноширинный шрифт. Вы заметите, что в onCreate я загружаю шрифт consola.ttf, а затем применяю его к каждому из текстовых элементов сетки, которые действуют как ячейки в сетке. Это позволяет нам быть достаточно уверенным, что текст не будет отображаться больше, чем мы установили минимальную и максимальную ширину на предыдущем шаге. Здесь я немного причудливо, что с целым cellWidthFactor и максимальным размером этого столбца. Это действительно так, что маленькие строки будут соответствовать наверняка, в то время как мы можем минимизировать пустое пространство для больших строк, которые (для моей системы) не будут заглавными буквами. Если вы столкнулись с трудностями при использовании этого метода, и вы получили строки, которые не соответствовали размеру столбца, который вы установили, здесь вы хотите редактировать вещи. Вы бы захотели изменить переменную replacementWidth с помощью какой-либо другой формулы для определения ширины ячейки, такой как 50 * widths[i] которая была бы довольно большой! Но в некоторых столбцах вы останетесь с большим количеством пробелов. В принципе, в зависимости от того, что вы планируете вкладывать в свою сетку, это может потребоваться изменить. Выше это то, что сработало для меня.

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

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

С моей точки зрения, вот как я подхожу к этому:

1) Создайте интерфейс с помощью одного метода, который будет выполняться вашей Деятельностью для получения координат прокрутки и что ваш 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() .