Android Table Scroll with Fixed Header and Column


Here’s a sample code I used when I was tasked to make an Android scroll-able table with static header and column. Please note that this code is not yet fully optimized but it can help you have an idea on how this functionality was coded.

Video Demo

The final output of our code for today!

Code Basis

In order to be successful at something, you have to first visualize it. So here’s a simple visualization of what views will be included in our code.

android-fixed-header-and-column

Code

MainActivity.java – executes the table layout

package com.example.tablefreezepane;

import com.Table.TableMainLayout;

import android.os.Bundle;
import android.app.Activity;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new TableMainLayout(this));
    }
}

SampleObject.java – used for populating some table data, like the rows and table headers.

package com.Table;

public class SampleObject {
    
    String header1;
    String header2;
    String header3;
    String header4;
    String header5;
    String header6;
    String header7;
    String header8;
    String header9;
    
    public SampleObject(String header1, String header2, String header3,
            String header4, String header5, String header6,
            String header7, String header8, String header9){
        
        this.header1 = header1;
        this.header2 = header2;
        this.header3 = header3;
        this.header4 = header4;
        this.header5 = header5;
        this.header6 = header6;
        this.header7 = header7;
        this.header8 = header8;
        this.header9 = header9;
        
    }
}

TableMainLayout.java – where everything were constructed.

*Custom horizontal and vertical scroll views were used to sync the movement of table scrolling effect.

package com.Table;


import java.util.ArrayList;
import java.util.List;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.HorizontalScrollView;
import android.widget.RelativeLayout;
import android.widget.ScrollView;
import android.widget.TableLayout;
import android.widget.TableRow;
import android.widget.TextView;

public class TableMainLayout extends RelativeLayout {

    public final String TAG = "TableMainLayout.java";
    
    // set the header titles
    String headers[] = {
        "Header 1 n multi-lines",
        "Header 2",
        "Header 3",
        "Header 4",
        "Header 5",
        "Header 6",
        "Header 7",
        "Header 8",
        "Header 9"
    };
    
    TableLayout tableA;
    TableLayout tableB;
    TableLayout tableC;
    TableLayout tableD;
    
    HorizontalScrollView horizontalScrollViewB;
    HorizontalScrollView horizontalScrollViewD;

    ScrollView scrollViewC;
    ScrollView scrollViewD;
    
    Context context;
    
    List<SampleObject> sampleObjects = this.sampleObjects();
    
    int headerCellsWidth[] = new int[headers.length];
    
    public TableMainLayout(Context context) {
        
        super(context);
        
        this.context = context;
        
        // initialize the main components (TableLayouts, HorizontalScrollView, ScrollView)
        this.initComponents();
        this.setComponentsId();
        this.setScrollViewAndHorizontalScrollViewTag();
        
        
        // no need to assemble component A, since it is just a table
        this.horizontalScrollViewB.addView(this.tableB);
        
        this.scrollViewC.addView(this.tableC);
        
        this.scrollViewD.addView(this.horizontalScrollViewD);
        this.horizontalScrollViewD.addView(this.tableD);
        
        // add the components to be part of the main layout
        this.addComponentToMainLayout();
        this.setBackgroundColor(Color.RED);
        
        
        // add some table rows
        this.addTableRowToTableA();
        this. addTableRowToTableB();
        
        this.resizeHeaderHeight();
        
        this.getTableRowHeaderCellWidth();
        
        this.generateTableC_AndTable_B();
        
        this.resizeBodyTableRowHeight();
    }

    // this is just the sample data
    List<SampleObject> sampleObjects(){
        
        List<SampleObject> sampleObjects = new ArrayList<SampleObject>();
        
        for(int x=1; x<=20; x++){
            
            SampleObject sampleObject = new SampleObject(
                "Col 1, Row " + x, 
                "Col 2, Row " + x + " - multi-lines",
                "Col 3, Row " + x,
                "Col 4, Row " + x,
                "Col 5, Row " + x,
                "Col 6, Row " + x,
                "Col 7, Row " + x,
                "Col 8, Row " + x,
                "Col 9, Row " + x
            );
            
            sampleObjects.add(sampleObject);
        }
        
        return sampleObjects;
    
    }
    
    // initalized components 
    private void initComponents(){
        
        this.tableA = new TableLayout(this.context); 
        this.tableB = new TableLayout(this.context); 
        this.tableC = new TableLayout(this.context); 
        this.tableD = new TableLayout(this.context);
        
        this.horizontalScrollViewB = new MyHorizontalScrollView(this.context);
        this.horizontalScrollViewD = new MyHorizontalScrollView(this.context);
        
        this.scrollViewC = new MyScrollView(this.context);
        this.scrollViewD = new MyScrollView(this.context);
        
        this.tableA.setBackgroundColor(Color.GREEN);
        this.horizontalScrollViewB.setBackgroundColor(Color.LTGRAY);
        
    }
    
    // set essential component IDs
    private void setComponentsId(){
        this.tableA.setId(1);
        this.horizontalScrollViewB.setId(2);
        this.scrollViewC.setId(3);
        this.scrollViewD.setId(4);
    }
    
    // set tags for some horizontal and vertical scroll view
    private void setScrollViewAndHorizontalScrollViewTag(){
        
        this.horizontalScrollViewB.setTag("horizontal scroll view b");
        this.horizontalScrollViewD.setTag("horizontal scroll view d");
        
        this.scrollViewC.setTag("scroll view c");
        this.scrollViewD.setTag("scroll view d");
    }
    
    // we add the components here in our TableMainLayout
    private void addComponentToMainLayout(){
        
        // RelativeLayout params were very useful here
        // the addRule method is the key to arrange the components properly
        RelativeLayout.LayoutParams componentB_Params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
        componentB_Params.addRule(RelativeLayout.RIGHT_OF, this.tableA.getId());
        
        RelativeLayout.LayoutParams componentC_Params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
        componentC_Params.addRule(RelativeLayout.BELOW, this.tableA.getId());
        
        RelativeLayout.LayoutParams componentD_Params = new RelativeLayout.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.WRAP_CONTENT);
        componentD_Params.addRule(RelativeLayout.RIGHT_OF, this.scrollViewC.getId());
        componentD_Params.addRule(RelativeLayout.BELOW, this.horizontalScrollViewB.getId());
        
        // 'this' is a relative layout, 
        // we extend this table layout as relative layout as seen during the creation of this class
        this.addView(this.tableA);
        this.addView(this.horizontalScrollViewB, componentB_Params);
        this.addView(this.scrollViewC, componentC_Params);
        this.addView(this.scrollViewD, componentD_Params);
            
    }
    

    
    private void addTableRowToTableA(){
        this.tableA.addView(this.componentATableRow());
    }
    
    private void addTableRowToTableB(){
        this.tableB.addView(this.componentBTableRow());
    }
    
    // generate table row of table A
    TableRow componentATableRow(){
        
        TableRow componentATableRow = new TableRow(this.context);
        TextView textView = this.headerTextView(this.headers[0]);
        componentATableRow.addView(textView);
        
        return componentATableRow;
    }
    
    // generate table row of table B
    TableRow componentBTableRow(){
        
        TableRow componentBTableRow = new TableRow(this.context);
        int headerFieldCount = this.headers.length;
        
        TableRow.LayoutParams params = new TableRow.LayoutParams(LayoutParams.WRAP_CONTENT,LayoutParams.MATCH_PARENT);
        params.setMargins(2, 0, 0, 0);
        
        for(int x=0; x<(headerFieldCount-1); x++){
            TextView textView = this.headerTextView(this.headers[x+1]);
            textView.setLayoutParams(params);
            componentBTableRow.addView(textView);
        }
        
        return componentBTableRow;
    }
    
    // generate table row of table C and table D
    private void generateTableC_AndTable_B(){
        
        // just seeing some header cell width
        for(int x=0; x<this.headerCellsWidth.length; x++){
            Log.v("TableMainLayout.java", this.headerCellsWidth[x]+"");
        }
        
        for(SampleObject sampleObject : this.sampleObjects){
            
            TableRow tableRowForTableC = this.tableRowForTableC(sampleObject);
            TableRow taleRowForTableD = this.taleRowForTableD(sampleObject);
            
            tableRowForTableC.setBackgroundColor(Color.LTGRAY);
            taleRowForTableD.setBackgroundColor(Color.LTGRAY);
            
            this.tableC.addView(tableRowForTableC);
            this.tableD.addView(taleRowForTableD);
            
        }
    }
    
    // a TableRow for table C
    TableRow tableRowForTableC(SampleObject sampleObject){
        
        TableRow.LayoutParams params = new TableRow.LayoutParams( this.headerCellsWidth[0],LayoutParams.MATCH_PARENT);
        params.setMargins(0, 2, 0, 0);
        
        TableRow tableRowForTableC = new TableRow(this.context);
        TextView textView = this.bodyTextView(sampleObject.header1);
        tableRowForTableC.addView(textView,params);
        
        return tableRowForTableC;
    }
    
    TableRow taleRowForTableD(SampleObject sampleObject){

        TableRow taleRowForTableD = new TableRow(this.context);
        
        int loopCount = ((TableRow)this.tableB.getChildAt(0)).getChildCount();
        String info[] = {
            sampleObject.header2,
            sampleObject.header3,
            sampleObject.header4,
            sampleObject.header5,
            sampleObject.header6,
            sampleObject.header7,
            sampleObject.header8,
            sampleObject.header9
        };
        
        for(int x=0 ; x<loopCount; x++){
            TableRow.LayoutParams params = new TableRow.LayoutParams( headerCellsWidth[x+1],LayoutParams.MATCH_PARENT);
            params.setMargins(2, 2, 0, 0);
            
            TextView textViewB = this.bodyTextView(info[x]);
            taleRowForTableD.addView(textViewB,params);
        }
        
        return taleRowForTableD;
        
    }
    
    // table cell standard TextView
    TextView bodyTextView(String label){
        
        TextView bodyTextView = new TextView(this.context);
        bodyTextView.setBackgroundColor(Color.WHITE);
        bodyTextView.setText(label);
        bodyTextView.setGravity(Gravity.CENTER);
        bodyTextView.setPadding(5, 5, 5, 5);
        
        return bodyTextView;
    }
    
    // header standard TextView
    TextView headerTextView(String label){
        
        TextView headerTextView = new TextView(this.context);
        headerTextView.setBackgroundColor(Color.WHITE);
        headerTextView.setText(label);
        headerTextView.setGravity(Gravity.CENTER);
        headerTextView.setPadding(5, 5, 5, 5);
        
        return headerTextView;
    }
    
    // resizing TableRow height starts here
    void resizeHeaderHeight() {
        
        TableRow productNameHeaderTableRow = (TableRow) this.tableA.getChildAt(0);
        TableRow productInfoTableRow = (TableRow)  this.tableB.getChildAt(0);

        int rowAHeight = this.viewHeight(productNameHeaderTableRow);
        int rowBHeight = this.viewHeight(productInfoTableRow);

        TableRow tableRow = rowAHeight < rowBHeight ? productNameHeaderTableRow : productInfoTableRow;
        int finalHeight = rowAHeight > rowBHeight ? rowAHeight : rowBHeight;

        this.matchLayoutHeight(tableRow, finalHeight);
    }
    
    void getTableRowHeaderCellWidth(){
        
        int tableAChildCount = ((TableRow)this.tableA.getChildAt(0)).getChildCount();
        int tableBChildCount = ((TableRow)this.tableB.getChildAt(0)).getChildCount();;
        
        for(int x=0; x<(tableAChildCount+tableBChildCount); x++){
            
            if(x==0){
                this.headerCellsWidth[x] = this.viewWidth(((TableRow)this.tableA.getChildAt(0)).getChildAt(x));
            }else{
                this.headerCellsWidth[x] = this.viewWidth(((TableRow)this.tableB.getChildAt(0)).getChildAt(x-1));
            }
            
        }
    }
    
    // resize body table row height
    void resizeBodyTableRowHeight(){
        
        int tableC_ChildCount = this.tableC.getChildCount();
        
        for(int x=0; x<tableC_ChildCount; x++){
        
            TableRow productNameHeaderTableRow = (TableRow) this.tableC.getChildAt(x);
            TableRow productInfoTableRow = (TableRow)  this.tableD.getChildAt(x);
    
            int rowAHeight = this.viewHeight(productNameHeaderTableRow);
            int rowBHeight = this.viewHeight(productInfoTableRow);
    
            TableRow tableRow = rowAHeight < rowBHeight ? productNameHeaderTableRow : productInfoTableRow;
            int finalHeight = rowAHeight > rowBHeight ? rowAHeight : rowBHeight;

            this.matchLayoutHeight(tableRow, finalHeight);      
        }
        
    }
    
    // match all height in a table row
    // to make a standard TableRow height
    private void matchLayoutHeight(TableRow tableRow, int height) {
        
        int tableRowChildCount = tableRow.getChildCount();
        
        // if a TableRow has only 1 child
        if(tableRow.getChildCount()==1){
            
            View view = tableRow.getChildAt(0);
            TableRow.LayoutParams params = (TableRow.LayoutParams) view.getLayoutParams();
            params.height = height - (params.bottomMargin + params.topMargin);
            
            return ;
        }
        
        // if a TableRow has more than 1 child
        for (int x = 0; x < tableRowChildCount; x++) {
            
            View view = tableRow.getChildAt(x);
            
            TableRow.LayoutParams params = (TableRow.LayoutParams) view.getLayoutParams();

            if (!isTheHeighestLayout(tableRow, x)) {
                params.height = height - (params.bottomMargin + params.topMargin);
                return;
            }
        }

    }

    // check if the view has the highest height in a TableRow
    private boolean isTheHeighestLayout(TableRow tableRow, int layoutPosition) {
        
        int tableRowChildCount = tableRow.getChildCount();
        int heighestViewPosition = -1;
        int viewHeight = 0;

        for (int x = 0; x < tableRowChildCount; x++) {
            View view = tableRow.getChildAt(x);
            int height = this.viewHeight(view);

            if (viewHeight < height) {
                heighestViewPosition = x;
                viewHeight = height;
            }
        }

        return heighestViewPosition == layoutPosition;
    }

    // read a view's height
    private int viewHeight(View view) {
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        return view.getMeasuredHeight();
    }

    // read a view's width
    private int viewWidth(View view) {
        view.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
        return view.getMeasuredWidth();
    }

    // horizontal scroll view custom class
    class MyHorizontalScrollView extends HorizontalScrollView{

        public MyHorizontalScrollView(Context context) {
            super(context);
        }
        
        @Override
        protected void onScrollChanged(int l, int t, int oldl, int oldt) {
            String tag = (String) this.getTag();
            
            if(tag.equalsIgnoreCase("horizontal scroll view b")){
                horizontalScrollViewD.scrollTo(l, 0);
            }else{
                horizontalScrollViewB.scrollTo(l, 0);
            }
        }
        
    }

    // scroll view custom class
    class MyScrollView extends ScrollView{

        public MyScrollView(Context context) {
            super(context);
        }
        
        @Override
        protected void onScrollChanged(int l, int t, int oldl, int oldt) {
            
            String tag = (String) this.getTag();
            
            if(tag.equalsIgnoreCase("scroll view c")){
                scrollViewD.scrollTo(0, t);
            }else{
                scrollViewC.scrollTo(0,t);
            }
        }
    }

    
}

You can share your thoughts (in the comments section below) about this scroll-able android TableLayout with fixed header and column code!
Thanks for reading this Android Table Scroll with Fixed Header and Column!

,

56 responses to “Android Table Scroll with Fixed Header and Column”

  1. Hello

    It’s a great tutorial but it has a limitation. It only works with n by n matrix. I think it is because the function

    void resizeBodyTableRowHeight() function.

  2. Hello, Liked congratulations on your component, but I wonder if it would be possible to demonstrate in what TextView (or line) is the focus (selected) as well as nro. line to know which object is selected. Grateful.

    • Hi @972e59aa0a96c8307249f6fc072d921b:disqus,, I think you have to keep track of each cell (like setting a tag on a view) during the loop and assign a click listener so it can return what table cell was selected.

  3. Nice job!
    I new in Android, just a question about the app, is it possible to scroll in diagonal direction? Now it scrolls horizontally or vertically.

  4. Hi, thanks for this wonderful work. I am quite new in Android. I found all the works you have done in class file. Do you have a code where layout was designed in XML instead of java code?

  5. Hey, great job, great example! Helped me a lot… One question. I need first column to be fixed (only left side), whithout headers at the top. How can I change the code…?

  6. Changed – if(viewHeight < height) to if(viewHeight = 2, width is changed other than header 3. Great Tut ! Great Site !

  7. Hi Mike!

    I’m trying to download the project, but site has a problem..

    Connection error: SQLSTATE[28000] [1045] Access denied for user ‘codeofaninja’@’xxx.xx.xxx.xxx’ (using password: YES)
    Fatal error: Call to a member function prepare() on a non-object in /home/content/73/11827973/html/000WEBSITES/download-coan/objects/blog_post.php on line 91

  8. How can move two different horizontal layouts (that i make in xml) with one touch input in android studio plzz reply

  9. Hey Mike, I came across your tutorial (awesome tutorial by the way) and as a beginner I have no idea how to follow the layout design to get the tables to display. When I run the code in the tutorial it displays “Hello World” not the table output. I’m doing something wrong but I have no idea what. Can you point me in the right direction? Thanks!

      • I forgot to change,
        setContentView(R.layout.activity_main);
        to
        setContentView(new TableMainLayout(this));
        So I it was showing the view of the activity_main.xml and not the table layout that you created. I think that was what my issue was after I changed that it worked!

  10. Hi, I am new of android. In your code You used setId(1) in setComponentId() but for me it is showing error like this “expected resource of type id”. Can You tell me please what is the problem

  11. hi well this is good Demo.
    But Can you clear me how to fill data from api in each shell and row is fix according to Api response

  12. How can I implement this in a fragment with AsynTask OnCreate I want to call the AsynTask to fetch data from the server and the create these table View please help

  13. How do I adjust the width of columns and rows according to the longest component?
    If the column width is larger, the row width will follow the column.
    If the row width is larger, the column width will follow the row.

  14. Hi,

    I read you code and found it to be helpful. I was just wondering where I can find the activity_main.xml file since that where I would find the how to set up the layout. Can this be downloaded somewhere?

    Thank You

  15. you use for Activity
    setContentView(new TableMainLayout(this));
    What if i use fragment then? any solutions

  16. Can you please share the xml file (which you have written by code) so that it would be clear to understand how it is actually structured. This file I am referring to is “https://www.androidcode.ninja/android-scroll-table-fixed-header-column/”. Thank you for sharing such a beautiful code. Loved a lot!

Leave a Reply

Your email address will not be published. Required fields are marked *