Creating A Table/grid With A Frozen Column And Frozen Headers
Solution 1:
About a week ago I revisited this problem and came up with a solution. The solution requires me to do a lot of manual width setting for the columns in this grid, and I consider that to be extremely sub-par in this day and age. Unfortunately, I have also continued to look for a more well-rounded solution native to the Android platform, but I have not turned anything up.
The following is the code to create this same grid, should any one following me need it. I will explain some of the more pertinent details below!
The layout: grid.xml
:
<?xml version="1.0" encoding="utf-8"?><RelativeLayoutxmlns: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"><TableLayoutandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:orientation="vertical"android:layout_marginBottom="2dip"android:layout_weight="1"android:minHeight="100dip"><LinearLayoutandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TableLayoutandroid: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.ObservableHorizontalScrollViewandroid: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"><TableLayoutandroid:id="@+id/contentTableHeader"android:layout_width="fill_parent"android:layout_height="wrap_content"android:stretchColumns="1"/></qvtcapital.mobile.controls.ObservableHorizontalScrollView></LinearLayout><ScrollViewandroid:id="@+id/verticalScrollView"android:layout_width="fill_parent"android:layout_height="wrap_content"android:scrollbars="vertical"><LinearLayoutandroid:layout_width="fill_parent"android:layout_height="wrap_content"android:orientation="horizontal"><TableLayoutandroid: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.ObservableHorizontalScrollViewandroid: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"><TableLayoutandroid:id="@+id/contentTable"android:layout_width="fill_parent"android:layout_height="wrap_content"android:stretchColumns="1"/></qvtcapital.mobile.controls.ObservableHorizontalScrollView></LinearLayout></ScrollView></TableLayout>
The activity: Grid.java
:
publicclassResultGridextendsActivityimplementsHorizontalScrollViewListener {
private TableLayout frozenHeaderTable;
private TableLayout contentHeaderTable;
private TableLayout frozenTable;
private TableLayout contentTable;
Typeface font;
float fontSize;
int cellWidthFactor;
ObservableHorizontalScrollView headerScrollView;
ObservableHorizontalScrollView contentScrollView;
publicvoidonCreate(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 clarityfinalfloatscale= getBaseContext().getResources().getDisplayMetrics().density;
cellWidthFactor = (int) Math.ceil(fontSize * scale * (fontSize < 10 ? 0.9 : 0.7));
ButtonbackButton= (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();
}
protectedvoidInitializeInitialData() {
ArrayList<String[]> content;
BundlemyBundle= getIntent().getExtras();
try {
content = (ArrayList<String[]>) myBundle.get("gridData");
} catch (Exception e) {
content = newArrayList<String[]>();
content.add(newString[] {"Error", "There was an error parsing the result data, please try again"} );
e.printStackTrace();
}
PopulateMainTable(content);
}
protectedvoidPopulateMainTable(ArrayList<String[]> content) {
frozenTable.setBackgroundResource(R.color.tableBorder);
contentTable.setBackgroundResource(R.color.tableBorder);
TableLayout.LayoutParamsfrozenRowParams=newTableLayout.LayoutParams(
TableLayout.LayoutParams.WRAP_CONTENT,
TableLayout.LayoutParams.WRAP_CONTENT);
frozenRowParams.setMargins(1, 1, 1, 1);
frozenRowParams.weight=1;
TableLayout.LayoutParamstableRowParams=newTableLayout.LayoutParams(
TableLayout.LayoutParams.WRAP_CONTENT,
TableLayout.LayoutParams.WRAP_CONTENT);
tableRowParams.setMargins(0, 1, 1, 1);
tableRowParams.weight=1;
TableRow frozenTableHeaderRow=null;
TableRow contentTableHeaderRow=null;
intmaxFrozenChars=0;
int[] maxContentChars = newint[content.get(0).length-1];
for (inti=0; i < content.size(); i++){
TableRowfrozenRow=newTableRow(this);
frozenRow.setLayoutParams(frozenRowParams);
frozenRow.setBackgroundResource(R.color.tableRows);
TextViewfrozenCell=newTextView(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 themTableRowrow=newTableRow(this);
row.setLayoutParams(tableRowParams);
row.setBackgroundResource(R.color.tableRows);
for (intj=1; j < content.get(0).length; j++) {
TextViewrowCell=newTextView(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, newint[]{maxFrozenChars});
setChildTextViewWidths(contentTableHeaderRow, maxContentChars);
for (inti=0; i < contentTable.getChildCount(); i++) {
TableRowfrozenRow= (TableRow) frozenTable.getChildAt(i);
setChildTextViewWidths(frozenRow, newint[]{maxFrozenChars});
TableRowrow= (TableRow) contentTable.getChildAt(i);
setChildTextViewWidths(row, maxContentChars);
}
}
privatevoidsetChildTextViewWidths(TableRow row, int[] widths) {
if (null==row) {
return;
}
for (inti=0; i < row.getChildCount(); i++) {
TextViewcell= (TextView) row.getChildAt(i);
intreplacementWidth=
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);
}
}
publicvoidonScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY) {
if (scrollView==headerScrollView) {
contentScrollView.scrollTo(x, y);
} elseif (scrollView==contentScrollView) {
headerScrollView.scrollTo(x, y);
}
}
The scroll view listener (to hook the two up): HorizontalScrollViewListener.java
:
publicinterfaceHorizontalScrollViewListener {
voidonScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
}
The ScrollView class that implements this listener: ObservableHorizontalScrollView.java
:
publicclassObservableHorizontalScrollViewextendsHorizontalScrollView {
private HorizontalScrollViewListener scrollViewListener=null;
publicObservableHorizontalScrollView(Context context) {
super(context);
}
publicObservableHorizontalScrollView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
publicObservableHorizontalScrollView(Context context, AttributeSet attrs) {
super(context, attrs);
}
publicvoidsetScrollViewListener(HorizontalScrollViewListener scrollViewListener) {
this.scrollViewListener = scrollViewListener;
}
@OverrideprotectedvoidonScrollChanged(int x, int y, int oldX, int oldY) {
super.onScrollChanged(x, y, oldX, oldY);
if (null!=scrollViewListener) {
scrollViewListener.onScrollChanged(this, x, y, oldX, oldY);
}
}
}
The really important part of this is sort of three-fold:
- The ObservableHorizontalScrollView allows the header table and the content table to scroll in sync. Basically, this provides all of the horizontal motion for the grid.
- The way in which they stay aligned is by detecting the largest string that will be in a column. This is done at the end of
PopulateMainTable()
. While we're going through each of the TextViews and adding them to the rows, you'll notice that there are two arraysmaxFrozenChars
andmaxContentChars
that keep track of what the largest string value we've seen is. At the end ofPopulateMainTable()
we loop through each of the rows and for each of the cells we set its min and max width based on the largest string we saw in that column. This is handled bysetChildTextViewWidths
. - The last item that makes this work is to use a monospaced font. You'll notice that in
onCreate
I am loading a consola.ttf font, and later applying it to each of the grid's TextViews that act as the cells in the grid. This allows us to be reasonably sure that the text will not be rendered larger than we have set the minimum and maximum width to in the prior step. I am doing a little bit of fanciness here, what with the whole cellWidthFactor and the maximum size of that column. This is really so that smaller strings will fit for sure, while we can minimize the white space for larger strings that are (for my system) not going to be all capital letters. If you ran in to trouble using this and you got strings that did not fit in the column size you set, this is where you would want to edit things. You would want to change thereplacementWidth
variable with some other formula for determining the cell width, such as50 * widths[i]
which would be quite large! But would leave you with a good amount of whitespace in some columns. Basically, depending on what you plan on putting in your grid, this may need to be tweaked. Above is what worked for me.
I hope this helps someone else in the future!
Solution 2:
TableFixHeaders library might be useful for you in this case.
Solution 3:
Off the top of my head, this is how I would approach this:
1) Create an interface with one method that your Activity would implement to receive scroll coordinates and that your ScrollView can call back to when a scroll occurs:
publicinterfaceScrollCallback {
publicvoidscrollChanged(int newXPos, int newYPos);
}
2) Implement this in your activity to scroll the two constrained scrollviews to the position that the main scrollview just scrolled to:
@OverridepublicvoidscrollChanged(int newXPos, int newYPos) {
mVerticalScrollView.scrollTo(0, newYPos);
mHorizontalScrollView.scrollTo(newXPos, 0);
}
3) Subclass ScrollView to override the onScrollChanged() method, and add a method and member variable to call back to the activity:
private ScrollCallback mCallback;
//...@OverrideprotectedvoidonScrollChanged(int l, int t, int oldl, int oldt) {
mCallback.scrollChanged(l, t);
super.onScrollChanged(l, t, oldl, oldt);
}
publicvoidsetScrollCallback(ScrollCallback callback) {
mCallback = callback;
}
4) Replace the stock ScrollView in your XML with your new class and call setScrollCallback(this)
in onCreate()
.
Post a Comment for "Creating A Table/grid With A Frozen Column And Frozen Headers"