Blog

Back to Blog

Set Individual Column Widths with shinobigrids for Android

Posted on 14 Dec 2016 Written by Kai Armer

Here at Shinobi HQ we’ve been really busy developing new and exciting features for shinobicharts and our Advanced Charting Kit. We have however found some time to update shinobigrids for Android with a much requested feature – the ability to set individual column widths.

Previously, you could control the width of all columns in several ways, so let’s briefly recap these settings:

  • By default, if a column width was not specified, all columns in the grid will be assigned the same width, to show all columns on the screen, without the need to scroll.
  • By setting the minimumColumnWidth all columns will adopt at least this width. Scrolling may or may not be possible.
  • By setting the defaultColumnWidth all columns will adopt this width. Scrolling may or may not be possible. If both default and minimum column width are set, defaultColumnWidth takes precedence.

To demonstrate how the ability to set widths individually can improve the look of your data, we need (as I’m sure you have guessed) an app!

Shop ’till You Drop

We will display a fictitious shopping cart, containing several items with a shinobicontrols theme, ready for checkout.

Our shopping cart looks something like this:

grid with auto sized widths

As you can see, whilst this is functional, it does not make the best use of the available space. The item details field is truncated whilst the currency columns have a lot of wasted space as they are too wide. This is because the ShinobiGridView simply tries to fit all of the data in to the available space, without the need for scrolling.

We could of course try setting either a minimum or default column width, which would affect all columns of our grid. Looking at our data, this approach really wouldn’t suit as the item details column for example typically takes up much more space than the quantity.

As there isn’t really a suitable ‘one size fits all’ column width for this particular use case, let’s set some individual column widths.

The Wider Picture

Those familiar with the shinobigrids API may remember that every Column has a ColumnSpec, which determines various aspects of how a column will behave. For the most common use case, where one wishes to display text inside a column, shinobigrids has a TextColumnSpec object. Setting the width of text-based columns is thus very simple – you simply need to call the new setWidth method on the TextColumnSpec of your Column, passing in the number of pixels which you wish to be the width of your Column. To set the width of an individual column you might use code something like:

final int titleColumnWidth = getResources().getDimensionPixelSize(R.dimen
        .title_column_width);
((TextColumnSpec) column.getColumnSpec()).setWidth(titleColumnWidth);

 


Using this simple API, as you can see, our ShinobiGridView now makes much better use of the available space:

grid with individually set widths

But What About Phones?

It is worth mentioning here that when I first wrote this app, I individually set the width of all of the columns, which looked great in landscape orientation. Once I rotated the device to portrait however, I was able to scroll horizontally, as the data could no-longer fit to the screen. The reason for this is that the columns will still adopt the widths that you have specified, but of course in portrait orientation, there is less available width. One approach to resolve this is to programmatically swap out the widths upon device rotation. You could have one set of widths for landscape, and one set for portrait.

I found it much simpler however to simply not set the width of the widest column – the item details. If not set, the ShinobiGridView will auto size a column’s width. In this case as I’ve set the widths of all of the other columns, the item details column will simply take up all remaining space, not occupied by the other columns who have their width set individually. The end result is that for each orientation, all of the columns barring the item details, will adopt the widths which I have set. The item details column grows or shrinks its width to use up any remaining space. This means that regardless of orientation, the grid will always fit to the screen, without the need for scrolling. Please note – this app has been designed with tablet devices in mind. For smaller devices such as phones, you would probably want to use an alternative layout. This is beyond the scope of this blog.

Beyond Text

Of course, we like flexibility here at shinobicontrols and with shinobigrids you can certainly display more than text! I think this shopping cart would look good with an extra column which displays a small thumbnail image of the product, so why don’t we add this next. Along the way we will see how we set its width!

What we are going to do here is create a new ColumnSpec implementation which can handle ImageView objects, which will in turn require a custom ItemViewHolder. Some time ago I wrote a blog which demonstrated this principle, in the context of creating columns with editable text fields. This would certainly make good background reading for this blog, as it uses the same principles. You can find this blog here.

To create our column to hold thumbnail images, let’s create an ImageViewColumnSpec. We will simply create a new class which implements ColumnSpec. The Android RecyclerView (on which the GridRecyclerView is based) relies on ViewHolder objects who are responsible for describing an item view, whilst helping to improve performance during object lookup. As we will be holding ImageView objects in our new column, we need a ViewHolder for this:

static class ImageViewHolder extends RecyclerView.ViewHolder {
    static class Creator implements ItemViewHolderCreator {
        @Override
        public int getItemViewType() {
            return R.id.image_view_column_item_view_type;
        }

        @Override
        public RecyclerView.ViewHolder createItemViewHolder(ViewGroup parent) {
            Context context = parent.getContext();
            ImageView imageView = new ImageView(context);
            return new ImageViewHolder(imageView);
        }
    }

    private final ImageView imageView;

    ImageViewHolder(ImageView imageView) {
        super(imageView);
        this.imageView = imageView;
    }
}

 


As you can see this is a fairly simple class which just contains an ImageView. For those familiar with RecyclerView this will be nothing new. You may notice however that it also defines a static nested ItemViewHolderCreator class. This class, unique to shinobigrids knows how to create each ViewHolder specifically for this column. As we are using a new view type in our grid we need to setup a new ID resource. In your ids.xml file (create one in res/values if one does not yet exist) add the following:

<item name="image_view_column_item_view_type" type="id" />

 


We will also need to register the Creator class of our new ImageViewHolder with our ShinobiGridView. Back in the onCreate method of our VariableColumnWidthActivity:

shinobiGridView.registerItemViewHolderCreator(new ImageViewColumnSpec.ImageViewHolder
.Creator());

 


Now we have our ImageViewHolder created, we can build on our ImageViewColumnSpec. First we will need a suitable constructor, along with some properties which we will use later:

private final PropertyBinder<Integer> propertyBinder;
private final TextColumnStyle defaultStyle;
private final TextColumnStyle alternateStyle;
private final int width;

ImageViewColumnSpec(PropertyBinder<Integer> propertyBinder,
                    int width) {
    this.propertyBinder = propertyBinder;
    defaultStyle = new TextColumnStyle();
    alternateStyle = new TextColumnStyle();
    this.width = width;
}

 


Part of the job of ColumnSpec is to tell the ShinobiGridView what type of View its associated Column will hold. Let’s inform the grid that our Column will hold ImageView objects, using the ID resource we created earlier:

@Override
public int getItemViewType(int rowIndex) {
    return R.id.image_view_column_item_view_type;
}

 


Similarly, whilst for this column we won’t be using a title, we still want a header row, to ensure things look uniform. Let’s configure this now by informing the ShinobiGridView that our column has a header. We will also tell it what type of view we want for our header. Note that we are reusing the header row view type that comes with the library, hence the long R value!

@Override
public int getHeaderItemViewType() {
    return com.shinobicontrols.grids.R.id.sg_header_text_view;
}

@Override
public boolean hasHeader() {
    return true;
}

 


Next we implement the onBindViewHolder method to add the thumbnail image to the ImageView for the given row. We also set the background colour depending on whether we’re on an odd or even row:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int rowIndex) {
    final ImageViewHolder imageViewHolder = (ImageViewHolder) viewHolder;
    imageViewHolder.imageView.setImageResource(propertyBinder.bind(rowIndex));
    final TextColumnStyle style = rowIndex % 2 == 0 ? defaultStyle : alternateStyle;
    imageViewHolder.imageView.setBackgroundColor(style.getBackgroundColor());
}

 


You may be wondering where these TextColumnStyle objects come from – good question. The answer can be found in our onColumnAdded implementation:

@Override
public void onColumnAdded(Context context) {
    final TypedArray styledAttributes = context.obtainStyledAttributes(com
            .shinobicontrols.grids.R.styleable.ShinobiGridTheme);
    defaultStyle.setBackgroundColor(styledAttributes.getColor(com.shinobicontrols.grids.R
            .styleable.ShinobiGridTheme_sg_itemBackgroundColor, Color.TRANSPARENT));
    alternateStyle.setBackgroundColor(styledAttributes.getColor(com.shinobicontrols.grids
            .R.styleable.ShinobiGridTheme_sg_itemAlternateBackgroundColor, Color
            .TRANSPARENT));
    styledAttributes.recycle();
}

 


Obtaining values from the theme can be quite expensive, so we do it once when the column is added, not forgetting to recycle our styledAttributes. Notice we’ve chosen to store retrieved style values in TextColumnStyle objects. Although our column will contain images rather than text, we only wish to set the background colour. TextColumnStyle contains properties common to several View types and as such will be suitable in this case.

Let’s not forget, this blog is all about setting column widths! How do we set the width of our own, custom column? Let’s take a look:

@Override
public Integer getWidth() {
    return width;
}

 


That’s all that we need! During the layout stage, the framework will query the width of each ColumnSpec via this method. If you wanted your column to be auto sized you simply would return null. Note there is no need to implement a setter method for width although you can if you wish. That depends on your own use case!

Now we have our ImageViewColumnSpec, let’s do something useful with it. Heading over to the createAndAddColumns method of our VariableColumnWidthActivity:

ImageViewColumnSpec imageViewColumnSpec = new ImageViewColumnSpec(
        new PropertyBinder<Integer>() {
    @Override
    public Integer bind(int rowIndex) {
        final ShoppingCartItem shoppingCartItem = shoppingCartItems.get(rowIndex);
        return shoppingCartItem.imageId;
    }
}, getResources().getDimensionPixelSize(R.dimen.image_column_width));
shinobiGridView.addColumn(Column.create(imageViewColumnSpec));

 


This code is fairly straightforward but note that in our PropertyBinder implementation we return the Integer ID of our product image from our ShoppingCartItem.

Our grid should now look something like this:

grid with individually set widths and image column

As you can see the space is still used wisely, with the item details column simply shrinking in width slightly, to make room for our new product image column.

Taking Things a Little Further

Some of you may have noticed that the currency related columns, such as price, have a little formatting. For example the data is displayed to 2 decimal places, right-justified and with a Dollar sign prefix (I chose Dollars – we all know the British Pound hasn’t been doing too well lately!). In addition, the ShoppingCartItem stores these values as type double (note, for monetary values you should consider BigDecimal, but double is fine for demos), but TextColumnSpec expects a PropertyBinder using the generic CharSequence type. Now of course, we could do this work in our PropertyBinder, but it feels wrong and there will surely be repeated code. I thought a better way would be to create a CurrencyColumnSpec, which as the name suggests is used for any column which holds currency values. The approach to this is very similar to the one taken for ImageViewColumnSpec, with a few small differences:

  • The PropertyBinder expected will use the Double generic type.
  • We will pass extra values to the constructor for our column header title and column width, along with a suitable formatter for the currency values.
  • As we will ultimately hold text in our column, we can re-use the view types provided in the framework, rather than implement our own ViewHolder. We inform as such:
@Override
public int getItemViewType(int rowIndex) {
    return com.shinobicontrols.grids.R.id.sg_text_view;
}

@Override
public int getHeaderItemViewType() {
    return com.shinobicontrols.grids.R.id.sg_header_text_view;
}

 


Let’s not forget to add the title of the column to the header, along with suitable padding:

@Override
public void onBindHeaderViewHolder(RecyclerView.ViewHolder viewHolder) {
    HeaderTextViewHolder headerTextViewHolder = (HeaderTextViewHolder) viewHolder;
    headerTextViewHolder.headerTextView.setText(columnTitle);
    headerTextViewHolder.headerTextView.setPadding(headerStyle.getPaddingLeft(),
            headerStyle.getPaddingTop(), headerStyle.getPaddingRight(),
            headerStyle.getPaddingBottom());
}

 


We obtain the currency value as a double, do any necessary formatting and place it into our TextView in the onBindViewHolder method:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder viewHolder, int rowIndex) {
    final double currencyValue = propertyBinder.bind(rowIndex);
    final TextView textView = (TextView) viewHolder.itemView;
    textView.setGravity(Gravity.CENTER_VERTICAL | Gravity.END);
    textView.setText(decimalFormat.format(currencyValue));
    final TextColumnStyle style = rowIndex % 2 == 0 ? defaultStyle : alternateStyle;
    textView.setBackgroundColor(style.getBackgroundColor());
    textView.setPadding(style.getPaddingLeft(), style.getPaddingTop(),
            style.getPaddingRight(), style.getPaddingBottom());
}

 


Once we have our CurrencyColumnSpec, using it is simple:

final CurrencyColumnSpec priceColumnSpec = new CurrencyColumnSpec(getString(R.string.price_column_title),
        new PropertyBinder<Double>() {
    @Override
    public Double bind(int rowIndex) {
        ShoppingCartItem shoppingCartItem = shoppingCartItems.get(rowIndex);
        return shoppingCartItem.price;
    }
}, getResources().getDimensionPixelSize(R.dimen.currency_column_width) , decimalFormat);
shinobiGridView.addColumn(Column.create(priceColumnSpec));

Wrapping Things Up

In this blog we briefly reviewed the previously existing options for setting column width, before creating an app and demonstrating how the ability to set individual column widths can make data much more readable. You can download the sample here if you would like to try it for yourself. If you haven’t yet downloaded a free trial of shinobigrids you can do so here.

In this blog we used a fictitious shopping cart to demonstrate the concepts, but some apps do indeed use real shopping carts! Android Pay is becoming an ever popular choice for in-app purchases. Some of you may find this blog interesting in which I show you how to quickly and easily integrate Android Pay into your app.

Sadly, shinobicontrols do not currently sell items such as cuddly toys. That said, we do base our future product development on customer demand. If enough customers request it, who knows :-)

Back to Blog