Saturday 29 December 2012

Android - horizontal flow layout

If you use a LinearLayout which has horizontal orientation and limited width, you'll notice that the child views you add to this LinearLayout are placed one after another left-to-right up to and beyond the width of the LinearLayout. The child views will not wrap to a new line and there is no native layout to do this. So! I've wrote one, as below. It's an adaptation of a view I found on Nishant Nair's Blog here:
https://nishantvnair.wordpress.com/2010/09/28/flowlayout-in-android

UPDATE: For the latest version of the HorizontalFlowLayout class, see this repository:
https://bitbucket.org/adilson05uk/android-utils
 
 
/**
 * Custom view which extends {@link RelativeLayout}
 * and which places its children horizontally,
 * flowing over to a new line whenever it runs out of width.
 */
public class HorizontalFlowLayout extends RelativeLayout {

  /**
   * Constructor to use when creating View from code.
   */
  public HorizontalFlowLayout(Context context) {
    super(context);
  }

  /**
   * Constructor that is called when inflating View from XML.
   */
  public HorizontalFlowLayout(Context context, AttributeSet attrs) {
    super(context, attrs);
  }

  /**
   * Perform inflation from XML and apply a class-specific base style.
   */
  public HorizontalFlowLayout(Context context, AttributeSet attrs, int defStyle) {
    super(context, attrs, defStyle);
  }

  @Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // need to call super.onMeasure(...) otherwise get some funny behaviour
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);

    final int width = MeasureSpec.getSize(widthMeasureSpec);
    int height = MeasureSpec.getSize(heightMeasureSpec);

    // increment the x position as we progress through a line
    int xpos = getPaddingLeft();
    // increment the y position as we progress through the lines
    int ypos = getPaddingTop();
    // the height of the current line
    int line_height = 0;

    // go through children
    // to work out the height required for this view

    // call to measure size of children not needed I think?!
    // getting child's measured height/width seems to work okay without it
    //measureChildren(widthMeasureSpec, heightMeasureSpec);

    View child;
    MarginLayoutParams childMarginLayoutParams;
    int childWidth, childHeight, childMarginLeft, childMarginRight, childMarginTop, childMarginBottom;

    for (int i = 0; i < getChildCount(); i++) {
      child = getChildAt(i);

      if (child.getVisibility() != GONE) {
        childWidth = child.getMeasuredWidth();
        childHeight = child.getMeasuredHeight();

        if (child.getLayoutParams() != null
            && child.getLayoutParams() instanceof MarginLayoutParams) {
          childMarginLayoutParams = (MarginLayoutParams)child.getLayoutParams();

          childMarginLeft = childMarginLayoutParams.leftMargin;
          childMarginRight = childMarginLayoutParams.rightMargin;
          childMarginTop = childMarginLayoutParams.topMargin;
          childMarginBottom = childMarginLayoutParams.bottomMargin;
        }
        else {
          childMarginLeft = 0;
          childMarginRight = 0;
          childMarginTop = 0;
          childMarginBottom = 0;
        }

        if (xpos + childMarginLeft + childWidth + childMarginRight + getPaddingRight() > width) {
          // this child will need to go on a new line

          xpos = getPaddingLeft();
          ypos += line_height;

          line_height = childMarginTop + childHeight + childMarginBottom;
        }
        else {
          // enough space for this child on the current line
          line_height = Math.max(
              line_height,
              childMarginTop + childHeight + childMarginBottom);
        }

        xpos += childMarginLeft + childWidth + childMarginRight;
      }
    }

    ypos += line_height + getPaddingBottom();

    if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
      // set height as measured since there's no height restrictions
      height = ypos;
    }
    else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST
        && ypos < height) {
      // set height as measured since it's less than the maximum allowed
      height = ypos;
    }

    setMeasuredDimension(width, height);
  }

  @Override
  protected void onLayout(boolean changed, int l, int t, int r, int b) {
    // increment the x position as we progress through a line
    int xpos = getPaddingLeft();
    // increment the y position as we progress through the lines
    int ypos = getPaddingTop();
    // the height of the current line
    int line_height = 0;

    View child;
    MarginLayoutParams childMarginLayoutParams;
    int childWidth, childHeight, childMarginLeft, childMarginRight, childMarginTop, childMarginBottom;

    for (int i = 0; i < getChildCount(); i++) {
      child = getChildAt(i);

      if (child.getVisibility() != GONE) {
        childWidth = child.getMeasuredWidth();
        childHeight = child.getMeasuredHeight();

        if (child.getLayoutParams() != null
            && child.getLayoutParams() instanceof MarginLayoutParams) {
          childMarginLayoutParams = (MarginLayoutParams)child.getLayoutParams();

          childMarginLeft = childMarginLayoutParams.leftMargin;
          childMarginRight = childMarginLayoutParams.rightMargin;
          childMarginTop = childMarginLayoutParams.topMargin;
          childMarginBottom = childMarginLayoutParams.bottomMargin;
        }
        else {
          childMarginLeft = 0;
          childMarginRight = 0;
          childMarginTop = 0;
          childMarginBottom = 0;
        }

        if (xpos + childMarginLeft + childWidth + childMarginRight + getPaddingRight() > r - l) {
          // this child will need to go on a new line

          xpos = getPaddingLeft();
          ypos += line_height;

          line_height = childHeight + childMarginTop + childMarginBottom;
        }
        else {
          // enough space for this child on the current line
          line_height = Math.max(
              line_height,
              childMarginTop + childHeight + childMarginBottom);
        }

        child.layout(
            xpos + childMarginLeft,
            ypos + childMarginTop,
            xpos + childMarginLeft + childWidth,
            ypos + childMarginTop + childHeight);

        xpos += childMarginLeft + childWidth + childMarginRight;
      }
    }
  }
}
 

Friday 28 December 2012

Android: DialogFragment and screen rotation

If you're setting up your dialogs using the DialogFragment class, you've probably noticed that a dialog which is shown becomes hidden when the screen orientation is changed. Not great! Spending a fair bit of time looking into this - browsing online and Stack Overflow in particular - you'll find lots of discussion and suggestions but it becomes pretty clear very soon that nobody really quite knows what to do about it!! Below are a couple of things I tried:

(1) Included an empty (i.e. no arguments), public constructor in my DialogFragment class which simply calls the default, empty super constructor.

(2) Tried different combinations of setRetainInstance(true) and setRetainInstance(false) with getDialog().setDismissMessage(null) and getDialog().setOnDismissListener(null) in my DialogFragment.onCreate(Bundle savedInstanceState) and DialogFragment.onDestroyView() methods respectively.

In the end I gave up and am now allowing my dialogs to be dismissed on screen rotation by calling setRetainInstance(true) when my dialog is created (i.e. in my DialogFragment.onCreate(Bundle savedInstanceState) method). This causes the dialog to be dismissed proper on screen orientation change instead of simply showing as invisible whilst blocking the screen. Not ideal and it would have been great to have dialogs persist beyond screen rotations but better not to spend any more time on this shooting in the dark!

Friday 7 December 2012

Talk summary: So you have an app idea?, by Dave Addey

Here's a summary of a good talk I listened to last week by Dave Addey entitled "So you have an app idea?". It's the first talk I've heard in a long time which was worthy of note-taking. You'll find in the list below some points the speaker mentions for determining the potential of an app idea.
  1. Universal appeal: Is it niche, i.e. relevant only to a particular interest group and therefore not going to make much money?
  2. International appeal: Is it as relevant in China as it is in the UK?
  3. Lasting appeal: Will it still be relevant a year(+) from now for users to install and use?
  4. Ongoing use: Is it a use once or twice and throw away app or will it be used over a lengthy, potentially indefinite time-frame?
  5. Personal interest: Does it scratch my own itch? Do I want or need this myself?
  6. The hook: Is it something that has not been done before? Does it give people a reason to write about it, to be excited about it, and to show it off to their friends?
  7. Dead time principle: Can it be dived into and out of for short spells, i.e. whenever the user has a moment free to use it?
A video recording of the talk used to be up on Be Square here but it looks like that's been taken down since. Good news though: I managed to find an audio recording of the talk on Sound Cloud here.

Android: show a View as pressed without using a Selector resource

It's possible to show a View as pressed or unpressed (when in the pressed or default state respectively) by having two versions of the View's background image, putting the two images in a selector drawable, and then assigning the selector to be the background of your View. However, sometimes you might not have two versions of the image or you might not want to bloat the size of your apk by having two(+) versions of each image. In such a situation, here's an alternative way of showing a View as pressed (or not), i.e. show a black-ish, transparent overlay over the View.

The first step is to define a black, transparent colour in your res/values/colors.xml file as follows:

<?xml version="1.0" encoding="utf-8"?>
<resources>
  <color name="black_transparent">#70000000</color>
</resources>

The second step is to create a res/drawable/selector_foreground_black_transparent.xml file defined as follows:

<?xml version="1.0" encoding="utf-8"?>
<selector
  xmlns:android="http://schemas.android.com/apk/res/android" >
  <item
    android:state_pressed="true"
    android:drawable="@color/black_transparent" />
  <item
    android:drawable="@android:color/transparent" />
</selector>

The third and final step is two wrap your View - the one you want to be displayed as pressed or not - in a FrameLayout something as follows:

<FrameLayout
  android:id="@+id/layout_my_view"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:foreground="@drawable/selector_foreground_black_transparent"
  android:foregroundGravity="fill" >
  <View
    android:id="@+id/my_view"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:background="@drawable/my_view_bg" />
</FrameLayout>

That's it. The only other thing to keep in mind is to put a click listener on your FrameLayout instead of putting it on your View.