I am working on a small Android app. Part of what I need for this android app is to have a grid that is both horizontally and vertically scroll-able. However, the leftmost column needs to be frozen (always on screen, and not part of the horizontal scrolling). Similarly, the top header row needs to be frozen (not part of the vertical scrolling)
This picture will hopefully describe this clearly if the above doesn't make too much sense:
Key:
To do one of these dimensions is easy enough, and I have done so. However, I am having trouble getting both of these dimensions to work. (i.e., I can get the bottom portion to be all blue, or I can get the right portion to be all red, but not entirely as above) The code I have is below, and will basically produce the following:
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 code:
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);
}
}
This is what it looks like:
So this is what it looks like with a little bit of horizontal scrolling
This is what it looks like when scrolling vertically, note that you lose the headers! This is a problem!
Two last things to note!
First off, I cannot believe that this doesn't exist somewhere already. (I do not own an Android, so I have not been able to look around for apps that may do this). However, I have searched for at least two days within StackOverflow and in the Internet at large looking for a solution for either GridView or TableLayout that will provide me for what I'd like to do, and have yet to find a solution. As embarrassed as I would be for having missed it, if someone knows of a resource out there that describes how to do this, I would be grateful!
Secondly, I did try to "force" a solution to this, in that I added two LinearLayouts, one capturing the "Header" part of the grid I want to create, and another for the bottom "content" part of the grid I want to create. I can post this code, but this is already quite long and I'm hoping that what I mean is obvious. This partially worked but the problem here is that the headers and content columns were never lined up. I wanted to use getWidth() and setMinimumWidth() on the TextViews within the TableRows, but as described here this data was inaccessible during onCreate (and was also inaccessible within onPostCreate). I have been unable to find a way to get this to work, and a solution in this realm would be wonderful as well!
If you made it this far to the end, kudos to you!
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"?>
<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>
The activity: 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);
}
}
The scroll view listener (to hook the two up): HorizontalScrollViewListener.java
:
public interface HorizontalScrollViewListener {
void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldX, int oldY);
}
The ScrollView class that implements this listener: 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);
}
}
}
The really important part of this is sort of three-fold:
PopulateMainTable()
. While we're going through each of the TextViews and adding them to the rows, you'll notice that there are two arrays maxFrozenChars
and maxContentChars
that keep track of what the largest string value we've seen is. At the end of PopulateMainTable()
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 by setChildTextViewWidths
.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 the replacementWidth
variable with some other formula for determining the cell width, such as 50 * 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!
TableFixHeaders library might be useful for you in this case.
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:
public interface ScrollCallback {
public void scrollChanged(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:
@Override
public void scrollChanged(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;
//...
@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) Replace the stock ScrollView in your XML with your new class and call setScrollCallback(this)
in onCreate()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With