Wednesday, May 26, 2010

Wheel UI contol: Layouts

The NumericWheel widget has a simple design. It contains only text data and gradient backgrounds. In this post I'd like to write about text layouts.


Text layouts
Widget contains three text fields:
  • Current value - number on middle area  ("11" on image above)
  • Label - text on right area ("hour" on image above)
  • Items - all visible numbers excluding current value ("9", "10", "12", "13" on image above)
Next picture shows how all these items are "placed" on the widget.

About the Control area - it is just a space where defined above items are placed. It is the whole widget area excluding left and right padding. Note that there is also indent between Items and the Label areas.


To show text fields there is used the StaticLayout class that does all hard work. It renders text using a lot of settings, such as text paint, text alignment and so on.
Note that the Current value item is excluded from Items field because there is used another text settings for drawing it - it is drown with the same settings as the Label field.

As you can see, design is easy. It needs only to calculate their sizes and the whole widget dimensions.

Width
Before creating the StaticLayout object we should know its width (further it is possible only to make it more). So it needs to calculate it at first.

    int widthItems = (int) FloatMath.ceil(Layout.getDesiredWidth(getMaxText(), itemsPaint)); 
    widthItems += ADDITIONAL_ITEMS_SPACE; // make it some more

    int widthLabel = 0;
    if (label != null && label.length() > 0) {
         widthLabel = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));
    }

Then it needs to update width according to the widget layout width settings (android:layout_width) and, finally, calculate the whole widget width that is the sum of text fields width, left and top padding and indent between text fields. Next code does that.
/**
* Calculates control width and creates text layouts
*
@param widthSize the input layout width
*
@param mode the layout mode
*
@return the calculated control width
*/
private int calculateLayoutWidth(int widthSize, int mode) {
    initResourcesIfNecessary();

    int width = widthSize;

    int widthItems = (int) FloatMath.ceil(Layout.getDesiredWidth(getMaxText(), itemsPaint)); 
    widthItems += ADDITIONAL_ITEMS_SPACE; // make it some more

    int widthLabel = 0;
    if (label != null && label.length() > 0) {
        widthLabel = (int) FloatMath.ceil(Layout.getDesiredWidth(label, valuePaint));
    }

    boolean recalculate = false;
    if (mode == MeasureSpec.EXACTLY) {
        width = widthSize;
        recalculate = true;
    } else { 
        width = widthItems + widthLabel + 2 * PADDING;
        if (widthLabel > 0) {
            width += LABEL_OFFSET;
        }

        // Check against our minimum width
        width = Math.max(width, getSuggestedMinimumWidth());

        if (mode == MeasureSpec.AT_MOST && widthSize < width) {
            width = widthSize;
            recalculate = true; 
        }
    }

    if (recalculate) {
        // recalculate width
        int pureWidth = width - LABEL_OFFSET - 2 * PADDING;
        if (widthLabel > 0) {
            double newWidthItems = (double) widthItems * pureWidth / (widthItems + widthLabel);
            widthItems = (int) newWidthItems;
            widthLabel = pureWidth - widthItems;
        } else {
            widthItems = pureWidth + LABEL_OFFSET; // no label
        }
    }

    createLayouts(widthItems, widthLabel);

    return width;
}

/**
* Creates layouts
*
@param widthItems width of items layout
*
@param widthLabel width of label layout
*/
private void createLayouts(int widthItems, int widthLabel) {
    if (itemsLayout == null || itemsLayout.getWidth() > widthItems) {
        itemsLayout = new StaticLayout(buildText(), itemsPaint, widthItems,
            widthLabel > 0 ? Layout.Alignment.ALIGN_OPPOSITE :
                Layout.Alignment.ALIGN_CENTER,
            1, ADDITIONAL_ITEM_HEIGHT, false);
    } else {
        itemsLayout.increaseWidthTo(widthItems);
    }

    if (valueLayout == null || valueLayout.getWidth() > widthItems) {
        valueLayout = new StaticLayout(Integer.toString(currentValue),
            valuePaint, widthItems, widthLabel > 0 ?
            Layout.Alignment.ALIGN_OPPOSITE : Layout.Alignment.ALIGN_CENTER,
            1, ADDITIONAL_ITEM_HEIGHT, false);
    } else {
        valueLayout.increaseWidthTo(widthItems);
    }

    if (widthLabel > 0) {
        if (labelLayout == null || labelLayout.getWidth() > widthLabel) {
            labelLayout = new StaticLayout(label, valuePaint,
                widthLabel, Layout.Alignment.ALIGN_NORMAL, 1,
                    ADDITIONAL_ITEM_HEIGHT, false);
        } else {
            labelLayout.increaseWidthTo(widthLabel);
        }
    }
}

As you can see there is not calculated the Current item width. It is because it has the same width as the Items layout that is as long as the longest text that it can contain.
How to build a text for the Items layout? Just create a string that contains five lines (in general, the wheel can have any count of items): the first two are the previous numbers of current value, the last two are the next numbers, and the middle one is empty - at this position there will be shown current value.

Height
The StaticLayout class allows to get its height, so it is not necessary co calculate that. It needs only to calculate the whole control height.
As you can see the Wheel control height is equal to the Items layout height. Just correct it to show top and bottom items not fully.
/**
* Calculates desired height for layout
*
*
@param layout the source layout
*
@return the desired layout height
*/
private int getDesiredHeight(Layout layout) {
    
if (layout == null) {
          return 0;
     }

     int linecount = layout.getLineCount();
     int desired = layout.getLineTop(linecount) - ITEM_OFFSET * 2 - ADDITIONAL_ITEM_HEIGHT;

     // Check against our minimum height
     desired = Math.max(desired, getSuggestedMinimumHeight());

     return desired;
}

onMeasure
So, all code for necessary implementation of onMeasure() method is written. Just correct height according to the widget layout height settings
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
     int heightMode = MeasureSpec.getMode(heightMeasureSpec);
     int widthSize = MeasureSpec.getSize(widthMeasureSpec);
     int heightSize = MeasureSpec.getSize(heightMeasureSpec);

     int width = calculateLayoutWidth(widthSize, widthMode);

     int height;
     if (heightMode == MeasureSpec.EXACTLY) {
          height = heightSize;
     } else {
          height = getDesiredHeight(itemsLayout);

          if (heightMode == MeasureSpec.AT_MOST) {
               height = Math.min(height, heightSize);
          }
     }

     setMeasuredDimension(width, height);
}

In next post I'll describe how to create nice backgrounds for the Wheel widget.

See also
    Introduction to the Wheel control
    The Wheel control backgrounds
    Scrolling the wheel control
    Ideas for the future

5 comments:

  1. Hi kankan How can i increase the size of month value.Suppose You set month view is 15 dip.I want increase 25 dip.Can you please help me

    ReplyDelete
  2. I need numeric wheel in which i can pick the number like decimal eg.
    1
    2
    3

    9
    10
    10.01
    10.2
    anybody can help me its urgent.I shall be very thankful.

    ReplyDelete
  3. Anonymous,

    You should use a custom adapter in this case.

    ReplyDelete
  4. Can u provide the wheel in the horizontal orientation of the wheel. As soon as possible Please.

    ReplyDelete
  5. hi , but how to download the source.

    ReplyDelete