Таблица с фиксированным заголовком и прокруткой тела

 V*D*V

Решение следующих задач:

Создание таблицы с фиксированным заголовком и прокруткой тела.

Динамическое добавление/удаление строк.

Изменение порядка строк.

Строки должны иметь состояние "фокус" и "выбор".

Добавление слушателей смены выбора, смена фокуса между ячейками.

Управление редактируемостью ячеек.

 

API Level 10+

 

 

Пример таблицы

Пример таблицы

 

/**

* http://blog.stylingandroid.com/scrolling-table-part-3/

* Таблица с прокруткой тела. Выбранной может быть только одна строка.

* Номер строки содержится в её ID.

*/

class ScrollingTable extends TableLayout implements OnCheckedChangeListener, OnFocusChangeListener {

 static int sTableBackgroundColor = Color.TRANSPARENT;

 static int sTableBorderColor = Color.GRAY;

 static int sTableRowPressedColor = Color.RED;

 static int sTableRowSelectedColor = Color.BLUE;

 static int sTableTextColor = 0xfff8f8f8;

 /**

  * Создать объект для подсветки строки в разных состояниях

  * @return Объект для подсветки строки в разных состояниях

  */

 private static final StateListDrawable getRowStateSelector() {

         final StateListDrawable states = new StateListDrawable();

         // порядок важен!

         // касание

         states.addState( new int[]{android.R.attr.state_pressed}, new ColorDrawable( sTableRowPressedColor ) );

         // выбор

         states.addState( new int[]{android.R.attr.state_selected}, new ColorDrawable( sTableRowSelectedColor ) );

         // фокус

         states.addState( new int[]{android.R.attr.state_focused}, getBorder( sTableRowSelectedColor ) );

         // все другие

         states.addState( StateSet.WILD_CARD, new ColorDrawable( sTableBackgroundColor) );

         return states;

 }

 /**

  * Создать и вернуть прямоугольный бордюр в 1 пиксель толщиной

  * @return бордюр

  */

 private static final Drawable getBorder(int color) {

         final ShapeDrawable shape = new ShapeDrawable( new RectShape() );

         shape.getPaint().setStyle( Style.STROKE );

         shape.getPaint().setStrokeWidth( 1 );

         shape.getPaint().setColor( color );

         return shape;

 }

 private boolean mIsRowSelectionEnabled = true;

 private OnClickListener mSelectionListener = null;

 private OnClickListener mChangeListener = null;

 /**

  * TableRow не имеет состояния selected, поэтому нужен вспомогательный класс

  */

 private final class STableRow extends TableRow implements OnClickListener {

         public STableRow(Context context) {

                 super( context );

                 setOnClickListener( this );

         }

         @Override

         public final void onClick(View v) {

                 if( mIsRowSelectionEnabled ) setRowSelection( v.getId() );

                 if( mSelectionListener != null ) mSelectionListener.onClick( v );

         }

 }

 //

 private final TableLayout mHeader;

 private final TableLayout mBody;

 //

 ScrollingTable(Context context, Object[] headerData) {

         super( context );

         setLayoutParams( new TableLayout.LayoutParams( TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.MATCH_PARENT ) );

         //

         final TableLayout.LayoutParams fill_width_params = new TableLayout.LayoutParams( TableLayout.LayoutParams.MATCH_PARENT, TableLayout.LayoutParams.WRAP_CONTENT );

         // заголовок

         final TableLayout header = new TableLayout( context );

         header.setLayoutParams( fill_width_params );// их также будут наследовать строки

         final TableRow row = new TableRow( context );

         row.setGravity( Gravity.CENTER );

         row.setFocusable( true );

         row.setFocusableInTouchMode( false );

         row.setClickable( true );

         for( Object o : headerData ) {

                 final TextView v = new TextView( context );

                 v.setText( o.toString() );

                 v.setTextAppearance( context, android.R.style.TextAppearance_Small );

                 //v.setTextSize( TypedValue.COMPLEX_UNIT_SP, 14 );// 14 для малого, 18 для среднего

                 v.setBackgroundDrawable( getBorder( sTableBorderColor ) );

                 v.setPadding( 1, 1, 1, 1 );

                 v.setGravity( Gravity.CENTER );

                 // doc:

                 // The children of a TableRow do not need to specify the layout_width and layout_height attributes in the XML file.

                 // TableRow always enforces those values to be respectively MATCH_PARENT and WRAP_CONTENT.

                 row.addView( v );

                 // если не установить MATCH_PARENT, при переносе текста бордюр будет разным по высоте

                 final TableRow.LayoutParams params = (TableRow.LayoutParams) v.getLayoutParams();

                 params.height = TableRow.LayoutParams.MATCH_PARENT;

                 v.setLayoutParams( params );

         }

         header.addView( row );

         header.setStretchAllColumns( true );

         header.setShrinkAllColumns( true );

         mHeader = header;

         addView( header );

         // тело таблицы

         final TableLayout body = new TableLayout( context );

         body.setLayoutParams( fill_width_params );// их также будут наследовать строки

         body.setStretchAllColumns( true );

         body.setShrinkAllColumns( true );

         final ScrollView scroll = new ScrollView( context );

         scroll.setLayoutParams( fill_width_params );

         scroll.addView( body );

         mBody = body;

         addView( scroll );

 }

 /**

  * Разрешить/запретить выбор строки

  * @param isEnable true - разрешить, false - запретить

  */

 final void setRowSelectionEnable(boolean isEnable) {

         mIsRowSelectionEnabled = isEnable;

         final TableLayout table = mBody;

         for( int r = table.getChildCount(); r-- > 0; ) {

                 final View row = table.getChildAt( r );

                 row.setFocusable( isEnable );

                 row.setFocusableInTouchMode( isEnable );

                 row.setClickable( isEnable );

         }

 }

 /**

  * Получить количество колонок

  * @return количество колонок

  */

 final int getColumnCount() {

         return ((TableRow)mHeader.getChildAt( 0 )).getChildCount();

 }

 /**

  * Запрос можно изменять содержимое ячейки или нет.

  * От этого зависит тип содержимого ячейки при создании.

  * Необходимо переопределить метод для создаваемой таблицы,

  * если нужны редактируемые ячейки

  * @param row номер строки

  * @param column номер столбца

  * @return true - ячейка редактируемая, false - не редактируемая

  */

 boolean isCellEditable(int row, int column) {

         return false;

 }

 /**

  * Добавить строку, заполненную данными объектами

  * @param rowData объекты для размещения в ячейках

  */

 final void addRow(final Object[] rowData) {

         final Context context = getContext();

         final int row_index = mBody.getChildCount();

         final TableRow row = new STableRow( context );

         row.setGravity( Gravity.CENTER );// иначе центровка в ячейках не срабатывает

         // doc:

         // The children of a TableRow do not need to specify the layout_width and layout_height attributes in the XML file.

         // TableRow always enforces those values to be respectively MATCH_PARENT and WRAP_CONTENT.

         for( int column_index = 0, count = rowData.length; column_index < count; column_index++ ) {

                 final Object o = rowData[column_index];

                 if( o instanceof Boolean ) {// FIXME уменьшить размер CheckBox

                         final CheckBox check = new CheckBox( context );

                         check.setChecked( ((Boolean) o).booleanValue() );

                         check.setEnabled( isCellEditable( row_index, column_index ) );

                         check.setPadding( 1, 1, 1, 1 );

                         check.setId( column_index );

                         row.addView( check );

                         final TableRow.LayoutParams params = (TableRow.LayoutParams) check.getLayoutParams();

                         params.gravity = Gravity.CENTER_HORIZONTAL;// только так выравнивается

                         check.setLayoutParams( params );

                 } else {

                         final TextView v;

                         if( isCellEditable( row_index, column_index ) ) {

                                 v = new EditText( context );

                         } else {

                                 v = new TextView( context );

                                 v.setTextColor( sTableTextColor );// иначе цвет текста меняется в зависимости от состояния

                         }

                         v.setTextSize( TypedValue.COMPLEX_UNIT_SP, 22 );// 14 для малого, 18 для среднего

                         v.setText( o.toString() );

                         v.setGravity( Gravity.CENTER_VERTICAL );

                         v.setPadding( 1, 1, 1, 1 );

                         v.setId( column_index );

                         row.addView( v );

                 }

         }

         row.setId( mBody.getChildCount() );// номер строки в ID

         row.setBackgroundDrawable( getRowStateSelector() );

         row.setFocusable( mIsRowSelectionEnabled );

         //row.setFocusableInTouchMode( mIsRowSelectionEnabled );// первое касание перемещает фокус, второе - щелчок

         row.setClickable( mIsRowSelectionEnabled );

         mBody.addView( row );

 }

 /**

  * Получить количество строк

  * @return количество строк

  */

 final int getRowCount() {

         return mBody.getChildCount();

 }

 /**

  * Удалить строку с указанным номером

  * @param rowIndex номер строки

  */

 final void removeRow(int rowIndex ) {

         final View v = mBody.getChildAt( rowIndex );

         mBody.removeViewAt( rowIndex );

         for( final int count = mBody.getChildCount(); rowIndex < count; rowIndex++ ) {

                 mBody.getChildAt( rowIndex ).setId( rowIndex );

         }

 

         if( v.isSelected() && mSelectionListener != null ) {

                 mSelectionListener.onClick( v );

         }

 }

 /**

  * Удалить все строки

  */

 final void removeAllRows() {

         mBody.removeAllViews();

         if( mSelectionListener != null ) {

                 mSelectionListener.onClick( null );

         }

 }

 /**

  * Перевести фокус на указанную строку

  * @param rowIndex номер строки

  */

 final void setRowSelection(int rowIndex) {

         for( int r = mBody.getChildCount(); r-- > 0; ) {

                 mBody.getChildAt( r ).setSelected( false );

         }

         mBody.getChildAt( rowIndex ).setSelected( true );

 }

 /**

  * Получить номер строки, на которой стоит фокус

  * @return номер строки, на которой стоит фокус или -1, если такой нет

  */

 final int getSelectedRow() {// возможно, имеет смысл просто хранить переменную

         for( int r = mBody.getChildCount(); r-- > 0; ) {

                 if( mBody.getChildAt( r ).isSelected() ) {

                         return r;

                 }

         }

         return -1;

 }

 /**

  * Получить объект из указанной ячейки

  * @param row номер строки

  * @param column номер столбца

  * @return объект из указанной ячейки

  */

 final Object getValueAt(int row, int column) {

         final Object o = ((TableRow)mBody.getChildAt( row )).getChildAt( column );

         if( o instanceof CheckBox ) {

                 return Boolean.valueOf( ((CheckBox) o).isChecked() );

         }

         return ((TextView)o).getText().toString();

 }

 /**

  * Установить цвет текста в ячейке

  * @param color цвет

  * @param row номер строки

  * @param column номер столбца

  */

 final void setCellTextColor(int color, int row, int column) {

         final Object o = ((TableRow)mBody.getChildAt( row )).getChildAt( column );

         if( o instanceof TextView ) {

                 ((TextView) o).setTextColor( color );

         }

 }

 /**

  * Перевести фокус на указанную ячейку

  * @param row номер строки

  * @param column номер столбца

  */

 final void changeSelection(final int row, final int column) {

         // прямой вызов запроса фокуса блокирует ввод в EditText

         post( new Runnable() {

                 public void run() {

                         ((TableRow)mBody.getChildAt( row )).getChildAt( column ).requestFocus();

                 }

         });

 }

 // См. JavaSE SDK, DefaultTableModel

 private static final int gcd(final int i, final int j) {

         return (j == 0) ? i : gcd( j, i % j );

 }

 private final void rotate(final int a, final int b, final int shift) {

         final int size = b - a;

         final int r = size - shift;

         final int g = gcd( size, r );

         final View[] rows = new TableRow[mBody.getChildCount()];

         for( int i = rows.length; i-- > 0; ) {

                 rows[i] = mBody.getChildAt( i );

         }

         mBody.removeAllViews();

         for( int i = 0; i < g; i++ ) {

                 int to = i;

                 final View tmp = rows[a + to];

                 for( int from = (to + r) % size; from != i; from = (to + r) % size) {

                         rows[a + to] = rows[a + from];

                         to = from;

                 }

                 rows[a + to] = tmp;

         }

         for( int i = 0; i < rows.length; i++ ) {

                 final View v = rows[i];

                 v.setId( i );

                 mBody.addView( v );

         }

 }

 /**

  * Передвигает одну или несколько строк из диапазона от start до end в позицию to

  * @param start начало, включительно

  * @param end конец, включительно

  * @param to позиция, куда надо передвинуть

  */

 final void moveRow(final int start, final int end, final int to) {

         final int shift = to - start;

         int first, last;

         if( shift < 0 ) {

                 first = to;

                 last = end;

         } else {

                 first = start;

                 last = to + end - start;

         }

         rotate( first, last + 1, shift );

 }

 /**

  * Установить слушателя на щелчок по любой строке

  * @param listener слушатель

  */

 final void setRowSelectionListener(final OnClickListener listener) {

         mSelectionListener = listener;

 }

 /**

  * Установить слушателя на изменение данных в любой ячейке

  * Получение данных: row = ((TabelRow)v.getParent()).getId(), col = v.getId();

  * @param listener слушатель

  */

 final void setChangeListener(final OnClickListener listener) {

         mChangeListener = listener;

 }

 /**

  * Установить слушателя на смену фокуса в любой ячейке

  * @param listener слушатель

  */

 final void setCellOnFocusChangeListener(final OnFocusChangeListener listener) {

         final TableLayout table = mBody;

         for( int rn = 0, rcount = table.getChildCount(); rn < rcount; rn++ ) {

                 final TableRow row = (TableRow) table.getChildAt( rn );

                 for( int cn = 0, ccount = row.getChildCount(); cn < ccount; cn++ ) {

                         row.getChildAt( cn ).setOnFocusChangeListener( listener );

                 }

         }

 }

 // OnCheckedChangeListener, изменение данных в CheckBox

 @Override

 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

         if( mChangeListener != null ) {

                 mChangeListener.onClick( buttonView );

         }

 }

 // OnFocusChangeListener, изменение данных в EditText

 @Override

 public void onFocusChange(View v, boolean hasFocus) {

         if( ! hasFocus ) {

                 if( mChangeListener != null ) {

                         mChangeListener.onClick( v );

                 }

         }

 }

 @Override

 protected final void onLayout(boolean changed, int l, int t, int r, int b) {

         super.onLayout( changed, l, t, r, b );

         TableLayout table = mBody;

         int rcount = table.getChildCount();

         if( rcount != 0 ) {

                 final LinkedList<Integer> col_widths = new LinkedList<Integer>();

                 /*for( int rn = 0; rn < rcount; rn++ ) */{

                         final TableRow row = (TableRow) table.getChildAt( 0 );

                         for( int cn = 0, ccount = row.getChildCount(); cn < ccount; cn++ ) {

                                 final View cell = row.getChildAt( cn );

                                 final TableRow.LayoutParams params = (TableRow.LayoutParams)cell.getLayoutParams();

                                 final Integer cell_width = Integer.valueOf( params.span == 1 ? cell.getWidth() : 0 );

                                 if( col_widths.size() <= cn ) {

                                         col_widths.add( cell_width );

                                 } else {

                                         final Integer current = col_widths.get( cn );

                                         if( cell_width.compareTo( current ) > 0 ) {

                                                 col_widths.remove( cn );

                                                 col_widths.add( cn, cell_width );

                                         }

                                 }

                         }

                 }

                 table = mHeader;

                 rcount = table.getChildCount();

                 for( int rn = 0; rn < rcount; rn++ ) {

                         final TableRow row = (TableRow) table.getChildAt( rn );

                         for( int cn = 0, ccount = row.getChildCount(); cn < ccount; cn++ ) {

                                 final View cell = row.getChildAt( cn );

                                 final TableRow.LayoutParams params = (TableRow.LayoutParams)cell.getLayoutParams();

                                 params.width = 0;//params.width = col_widths.get( cn ).intValue();

                                 for( int span = 0; span < params.span; span++ ) {

                                         params.width += col_widths.get( cn + span ).intValue();

                                 }

                         }

                 }

                 // чтобы перерисовать заголовок с новыми размерами, надо запустить отдельный поток

                 post( new Runnable() {

                         @Override

                         public void run() {

                                 mHeader.requestLayout();

                         }

                 });

         }// if( rcount != 0 )

 }

}