Wednesday, October 19, 2011

Creating a custom view, part 1 - Graphics

It's not often I write about user interface and applications, but since I have now received a few question on the subject, I thought I would describe one rather common task - how to create a custom view.

An activity containing a horizontal and vertical slider.
You will make your own customised slider, with the following features:
  • Scale and stretch it depending on screen size, UI design and orientation.
  • Change position from the activity.
  • React to touches.
  • Set min and max values from xml or application code.
There are many other features that could be added, but I will only add them if requested enough times in the comments.

The howto is divided into a few distinct steps, each describing a feature of the customised view.
Depending on the purpose of your own custom view, not all of the steps may be needed. Furthermore I have divided the howto into three parts, to make it a bit less overwhelming:



The source code can be downloaded from github, and all steps are tagged in the git database. Each step will be preceded with the git command you need in order to look at the code step by step. If you prefer, you can just look at the latest commit in git, since that includes the complete example.


Step 0. Download the source.

This is really optional, but it's probably a good idea to have the code nearby so you can compare my code with yours.
All these git commands are written for Linux command line. It should be very easy to from MS Windows or Mac OS as well, but unfortunately I have no experience in using git on any of those so you will have to find out yourself.

Create a working directory and cd to it. I have called mine "enea":
mkdir enea
cd enea
Use git clone to get the project, and cd to it:
git clone git://github.com/androidenea/CustomView.git
cd CustomView
You may now import the CustomView project to Eclipse.

All further steps in this howto have been clearly tagged for easy access. Each step starts with one line showing the git command needed to get to the correct place.

Please note that checking out a tag in git will get you to the correct version of the source code, but you are not allowed to commit any changes (unless you know what you are doing.)
If you want to modify your code and store that in git, you will need to create your own branch first. This is not the time and place for a tutorial about git, but I highly recommend the book "Pro Git" if you want to know more.

Step 1. Create a project


git checkout step_1

The first step will just give you a playground and a test harness in the shape of an activity.
Grab the code from the git repository for an easy starting point, or do it yourself in Eclipse:
Open Eclipse and create an Android project and fill in the following parametres:
  • Project name: CustomView
  • Build target: 1.6 or newer
  • Application name: CustomView
  • Package name: com.enea.training.customview
  • Create Activity: CustomViewActivity
  • Min SDK version: 4

Click finish to create your project. If you don't really understand what all those things actually mean, you will have a hard time understanding the other steps, as this tutorial is rather advanced. Read through a few other beginner's tutorials first and then you are most welcome back here.

To get a good base layout for the tests, and to get an idea of what this whole tutorial is about, you should replace the LinearLayout in main.xml with a RelativeLayout containing a SeekBar. The contents of main.xml should look like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <SeekBar android:id="@+id/slider"
      android:layout_height="wrap_content"
      android:layout_width="fill_parent"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
/>
</RelativeLayout>
That's it for step 1. That wasn't too bad was it?
Step 1. Standard SeekBar.

Step 2. Add a custom view.


This is by far the biggest step in this howto. We will add bitmaps and source code and we will also replace the SeekBar in step 1 with the custom view. We will do this in small steps to keep things apart.

Step 2.1 Bitmaps


git checkout step_2_1

There are three bitmaps that will be needed. I suggest you grab the ones from the git repository, even if you are implementing the rest without looking at my sample. The reason is that designing stretchable bitmaps is not within the scope of this tutorial. There are very good guidelines on how to design stretchable, or NinePatch, bitmaps on the Android SDK web page, here: Draw 9-patch

Copy the whole of directory res/drawable to your project. I have not spent much time on the graphics, other than making sure to use the nice Enea-red colour, but the bitmaps do scale well and will not look too shabby on anything from my small 2.6" QVGA phone to my nice 10.1" WXGA tablet.

Step 2.2 Slider class


git checkout step_2_2

Ok, so let's try to make use of those bitmaps, shall we?
Create a new class in the same package as earlier, and let it extend android.widget.View. In Eclipse, right-click on the package and select New->Class.
Fill in the blanks as follows:
  • Name: CustomSlider
  • Superclass: android.view.View
  • Generate constructors from superclass
Everything else should be left as default. Click Finish to create the class.

The two simpler constructors can just call to the most flexible one, with 0 or null as parameters, like so (the line numbers should roughly match the source code in git, assuming that you have checked out the corresponding step):
public CustomSlider(final Context context) {
  this(context, null, 0);
}

public CustomSlider(final Context context, final AttributeSet attrs) {
  this(context, attrs, 0);
}

The last constructor will need to do a few things though. It will be extended as we go along, but for now all that is needed is to point out the bitmaps to be used. For that you will also need to add references to the bitmaps.
Add the following members to your class:
private final Drawable mIndicator;
private final Drawable mBackground;

To initialise them, you will need a handle to the resources, and then set them, using the resource reference id.
Your third constructor should look like this:
public CustomSlider(Context context, AttributeSet attrs, int defStyle) {
  super(context, attrs, defStyle);

  final Resources res = context.getResources();
  mIndicator = res.getDrawable(R.drawable.indicator_horizontal);
  mBackground = res.getDrawable(R.drawable.background_horizontal);
}

Let Eclipse add any missing imports that might be needed.

Step 2.3 onDraw


git checkout step_2_3

All the drawing is happening in onDraw(). This method is potentially called quite often, so make a good habit out of making it as fast as possible. In particular, avoid allocating and freeing memory in it.
In this example, onDraw() will calculate the position for the indicator and draw the background, followed by the indicator, positioned relative to the background's end points.
For this to work there are a few member fields needed. First of all the normalised position of the indicator:
private float mMin;
  private float mMax;
  private float mPosition;
These need to be initialised in the constructor. For now just assume that the slider goes from -1.0 to 1.0 and that the indicator is positioned in the middle.
Add the following lines to the bottom of the constructor:
mMin = -1.0f;
    mMax = 1.0f;
    mPosition = (mMax - mMin) / 2 + mMin;
Then a few member fields are needed to help out with the drawing. We need somewhere to store the view's absolute drawing area:
private Rect mViewRect;
The offset, min and max absolute positions for the indicator are also stored, so we don't need to recalculate them every time.
private int mIndicatorOffset;
  private int mIndicatorMaxPos;
  private int mIndicatorMinPos;
And finally there's the actual onDraw method:
@Override
  protected void onDraw(final Canvas canvas) {
First, check if the absolute drawing area is null, and if it is, fill in all the absolute values:
if (mViewRect == null) {
      mViewRect = new Rect();
      getDrawingRect(mViewRect);
      mIndicatorOffset = mIndicator.getIntrinsicWidth();
      mIndicatorMaxPos = mViewRect.right - mIndicatorOffset;
      mIndicatorMinPos = mViewRect.left + mIndicatorOffset;
      mBackground.setBounds(mViewRect.left, mViewRect.top, mViewRect.right,
          mViewRect.bottom);
    }
Now it's time to calculate the position of the indicator bar, based on min and max values, offset, etc. Get some local variables to hold the position and edges of the indicator, and calculate the position:
final float pos;
    final int left;
    final int right;
    final int top;
    final int bottom;

    pos = mIndicatorMinPos
        + ((mIndicatorMaxPos - mIndicatorMinPos) / (mMax - mMin))
        * (mPosition - mMin);
When setting the drawing bounds for the indicator, there are some calculations to be made, since pos above is the centerpoint of the indicator, and not one of the edges:
left = (int) pos - (mIndicator.getIntrinsicWidth() / 2);
    top = mViewRect.centerY() - (mIndicator.getIntrinsicHeight() / 2);
    right = left + mIndicator.getIntrinsicWidth();
    bottom = top + mIndicator.getIntrinsicHeight();
    mIndicator.setBounds(left, top, right, bottom);
And finally, it's time to draw (don't forget the closing bracket):
mBackground.draw(canvas);
    mIndicator.draw(canvas);
  }
As you have perhaps already figured out, the system will know where to draw the bitmaps through the call to setBounds() for each bitmap. The rest where just there to calculate the bounds.

Step 2.4 onMeasure


git checkout step_2_4

One more thing needs to be added to the CustomView class, before we can start using it in step 2.5. The layout engine still don't know how large our view is. To find out, it will need to measure the view. Whenever that happens, there will be a call to onMeasure().
The implementation of onMeasure() is far simpler than onDraw(). All that is needed is to specify the width and the height of the view.

First of all there needs to be a call to the super.onMeasure(). This will set the width and height to maximum possible values, based on your layout parameters, such as fill_parent (or match_parent) and wrap_contents, margins, etc.:
@Override
  protected void onMeasure(final int widthMeasureSpec,
     final int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
For horizontal rendering of the slider view, the width should be the maximum possible, but the height should be limited to the height of the indicator (if you use the bitmaps from my sample project.):
setMeasuredDimension(getMeasuredWidth(), mIndicator.getIntrinsicHeight());
  }
As usual, don't forget the closing bracket. (If you are wondering where the code for vertical rendering is, you will have to be patient, as it won't be added until step 5.)

Phew, that's it with the implementation for now. It will still not show up on screen, though. That is because it hasn't been added to the main.xml layout file. So let's do that.

Step 2.5 Use the view.


git checkout step_2_5

The final thing to do in order to see the work we have just done is to replace the SeekBar with the CustomSlider. There is only one thing to change to do that - In the xml file, replace SeekBar with com.enea.training.customview.CustomSlider. Note that the package name must be included, or the view will not be found by the layout engine. Here is what that edited line should look like:
<com.enea.training.customview.CustomSlider android:id="@+id/slider"

If you run your project now, you should see the new shiny slider stretched horizontally across the top of the screen. Tilt your Android device between landscape and portrait and you will see that the bitmaps are nicely stretched. You may also want to change the constructor to initialise mPosition to different values and see that the indiator is placed correctly. Usually the end points are the trickiest ones to get right, so try setting mPosition to mMin and mMax respectively and try the view in both portrait and landscape to make sure it looks good.
SeekBar replaced with CustomSlider
Custom
There are a few situations when this would be all you want to do with a view, but most likely you want to be able to change the slider indicator from your activity and also interact with it by touching it. Or maybe you want a vertical slider instead?

Finally, if you want simple ways of reusing it, you probably want to be able to set things like min and max directly in the xml file.

So let's do all that. Get some fresh coffee and head over to the second part of this tutorial.





Please use the comments field if you have any questions.

/Robert

Creating a custom view, part 3 - Xml Attributes

This is the final part in the series of articles describing how to create a custom view.
For the other parts, follow these links:


In this part, you will see how to make your slider vertical, and how to set attributes from within xml, just the way you do with all the built-in views.

Step 5 Vertical slider


Making the slider vertical is slightly more challenging than step 4. The view must be aware of its orientation, and all position calculations must detect orientation. The complexity of this step means that we are back to doing sub-steps.

Step 5.1 A Vertical member.


git checkout step_5_1

The orientation of the slider can only be vertical or horizontal (I leave it to you as an exercise to implement a diagonal slider), so storing orientation can be done in a boolean:
private boolean mIsVertical;

mIsVertical will need to be initialised in the constructor. It's just going to be hard-coded to true here, until things improve in step 6.
Near the top of the constructor, just after the call to super, this line should be added:
mIsVertical = true;

Obviously, the slider will not become vertical just because of this, that's why there are a few more steps.

Step 5.2 Pick your bitmaps.


git checkout step_5_2

Now that there is a boolean to check for orientation, it's time to act upon it. The first thing to do is to select the correct bitmaps. In the constructor, add an if statement that will initalise the mIndicator and mBackground drawables accordingly:
if (mIsVertical) {
      mIndicator = res.getDrawable(R.drawable.indicator_vertical);
      mBackground = res.getDrawable(R.drawable.background_vertical);
    } else {
      mIndicator = res.getDrawable(R.drawable.indicator_horizontal);
      mBackground = res.getDrawable(R.drawable.background_horizontal);
    }

Step 5.3 onTouchListener revisited.


git checkout step_5_3

The calculations in the touch listener should also be updated. This is where it becomes a bit tricky. For horizontal orientation the mMin corresponds to mIndicatorMinPos, that is pixels and values are growing in the same direction. Whereas for vertical orientation mMin corresponds to mIndicatorMaxPos, that is pixels and values grow in opposite directions.
In the touch listener that is located in the constructor, surround the calculations with the following if-statement:
if (mIsVertical) {
          pos = (mMax - ((mMax - mMin) / (mIndicatorMinPos - mIndicatorMaxPos))
              * event.getY());
        } else {
          pos = (mMin + ((mMax - mMin) / (mIndicatorMaxPos - mIndicatorMinPos))
              * event.getX());
        }
Note how the calculation of pos differs.

Step 5.4 Improve the drawings.


git checkout step_5_4

The onDraw() and onMeasure() will also need to be dependent on the orientation. Let's do onDraw() first.
The first part of onDraw() initialises indicator min, max and offset values, in relation to the view's drawing rectangle. Surround the initialisation with the following if statement:
if (mIsVertical) {
        mIndicatorOffset = mIndicator.getIntrinsicHeight();
        mIndicatorMaxPos = mViewRect.top + mIndicatorOffset;
        mIndicatorMinPos = mViewRect.bottom - mIndicatorOffset;
      } else {
        mIndicatorOffset = mIndicator.getIntrinsicWidth();
        mIndicatorMaxPos = mViewRect.right - mIndicatorOffset;
        mIndicatorMinPos = mViewRect.left + mIndicatorOffset;
      }
Note how max and min relates to the mViewRect coordinates.

The second part of onDraw() calculates the real position of the indicator, based on the values above. Only pos and top left corner will need to modified for the indicator, since the other corners are relative to this one. Surround the calculations with the following:
if (mIsVertical) {
      pos = mIndicatorMaxPos
          + ((mIndicatorMinPos - mIndicatorMaxPos) / (mMax - mMin))
          * (mMax - mPosition);
      left = mViewRect.centerX() - (mIndicator.getIntrinsicWidth() / 2);
      top = (int) pos - (mIndicator.getIntrinsicHeight() / 2);
    } else {
      pos = mIndicatorMinPos
          + ((mIndicatorMaxPos - mIndicatorMinPos) / (mMax - mMin))
          * (mPosition - mMin);
      left = (int) pos - (mIndicator.getIntrinsicWidth() / 2);
      top = mViewRect.centerY() - (mIndicator.getIntrinsicHeight() / 2);
    }

Finally, onMeasure() will need to be modified. This is much simpler, since no calculations are needed. Surround the setter with the following:
if (mIsVertical) {
      setMeasuredDimension(mIndicator.getIntrinsicWidth(), getMeasuredHeight());
    } else {
      setMeasuredDimension(getMeasuredWidth(), mIndicator.getIntrinsicHeight());
    }

Done. You are now finished with step 5. Run your project and make sure the slider works as expected. Change the initialisation of mIsVertical in the constructor to make sure that you haven't broken the horizontal layout.
Note that when rendering the slider vertically, it pushes the reset button off the screen. Correcting it would require a modification of your activity's layout, which will be done next.

Step 6 Add key-value attributes in xml.


Almost there, the only thing left in this tutorial is to add some attributes that can be set from within the xml layout. Let's start with specifying the attributes.
I'm not going to go too far, but at least it makes sense to be able to set orientation, min and max from the xml. Once you've seen how to do this, adding other attributes should be easy.

Step 6.1 xml modifications.


git checkout step_6_1

Right-click on your project and select New->Android xml file.
Specify the file name "attrs.xml" and select the Values radio button. Click Finish to generate the file.
Open up the xml view of attrs.xml. This is where the attributes should go.
The node to use is called declare-stylable, and is used to identify a group of attributes. The sub-nodes are all of type attr and contains name and type of the attribute. Min and max are floats, whereas orientation is vertical or horizontal.
Your attrs.xml file should look like this:
<?xml version="1.0" encoding="utf-8"?>
<resources>
  <declare-styleable name="CustomSlider">
    <attr name="orientation">
      <enum name="horizontal" value="0" />
      <enum name="vertical" value="1" />
    </attr>
    <attr name="max" format="float"/>
    <attr name="min" format="float"/>
  </declare-styleable>
</resources>
This provides three key-value pairs that can be set for the slider. It's worth noting that there are other ways of specifying attributes. For example, if you want to re-use the enum for orientation, you may declare it outside of the
tag and just refer to it. Just ask, and I will in the comments describe how to do that.

Step 6.2 Modified constructor.


git checkout step_6_2

There is one more thing to do to get this to work - modify the constructor to make use of the new attributes.
This is really quite easy. One of the input parameters to the constructor is an AttributeSet, containing all the attributes for your view.

From that, you need to extract the attributes you are interested in.

At the top of your constructor, right after the call to super(), you should add the following lines to get a TypedArray with the attributes:
final TypedArray a = context.obtainStyledAttributes(attrs,
        R.styleable.CustomSlider);
Now, instead of just hard-coding mIsVertical to true or false, you should grab the orientation from your attributes. But since the orientation attribute
translates horizontal/vertical to an integer, based on the enum, you will need your java code to turn it into true or false. This code should replace the "mIsVertical=true" statement:
final int vertical = a.getInt(R.styleable.CustomSlider_orientation, 0);
    mIsVertical = (vertical != 0);
Look at the help for getInt() and make sure that you understand what the parameters mean.

The min and max values are done in a similar way, but they are already floats, so no extra treatment is necessary. Replace the mMin and mMax initialisation statements with the following:
final int max = a.getInt(R.>final float max = a.getFloat(R.styleable.CustomSlider_max, 1.0f);
    final float min = a.getFloat(R.styleable.CustomSlider_min, -1.0f);
    setMinMax(min, max);

Ok, done with the slider. Let's put that latest piece of code to some use.

Step 6.3 Attributes in layout


git checkout step_6_3

Once the new attributes are declared, you can refer to them from your main.xml layout file.
You will need to add the namespace to the layout and then obviously set the attributes for the CustomSlider.

The namespace is an attribute for the RelativeLayout, so the first few lines in main.xml should now look like this:

Once that is added, it's just a matter of setting the attributes for the slider views. The updated slider tag should look like this:
<com.enea.training.customview.CustomSlider android:id="@+id/slider_horizontal"
      CustomView:orientation="horizontal"
      CustomView:min="0.0"
      CustomView:max="100.0"
      android:layout_height="wrap_content"
      android:layout_width="fill_parent"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
    />
To make things a bit more interesting, you could add another slider, and make it vertical:
<com.enea.training.customview.CustomSlider android:id="@+id/slider_vertical"
      CustomView:orientation="vertical"
      CustomView:min="-50.0"
      CustomView:max="50.0"
      android:layout_height="fill_parent"
      android:layout_width="wrap_content"
      android:layout_alignParentLeft="true"
      android:layout_below="@+id/slider_horizontal"
      android:layout_alignParentBottom="true"
    />

As you can see, the new attributes are being set exactly the same way as the standard attributes, the only difference is the namespace.

That new slider needs some code in the activity as well. You will need a new member field and a new position listener, and displayValues() will need to display the vertical value.
Furthermore, since the horizontal slider now gets the initial values from xml, there is no need to call the setters in onCreate().
The onReset() callback method for the button will also need to be updated to reset both sliders.
Here is the complete listing of the activity. As you can see, it's just a matter of duplicating the code of the horizontal slider for the vertical.
public class CustomViewActivity extends Activity {
  private TextView     mValues;
  private CustomSlider mSliderHorizontal;
  private CustomSlider mSliderVertical;

  @Override
  public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mValues = (TextView) findViewById(R.id.values);

    mSliderHorizontal = (CustomSlider) findViewById(R.id.slider_horizontal);
    mSliderHorizontal.setPositionListener(new CustomSliderPositionListener() {
      public void onPositionChange(final float newPosition) {
        displayValues();
      }
    });
    mSliderVertical = (CustomSlider) findViewById(R.id.slider_vertical);
    mSliderVertical.setPositionListener(new CustomSliderPositionListener() {
      public void onPositionChange(final float newPosition) {
        displayValues();
      }
    });

    displayValues();
  }

  void displayValues() {
    final String str = String.format("Horizontal: %3.2f\nVertical: %3.2f",
        mSliderHorizontal.getPosition(), mSliderVertical.getPosition());
    mValues.setText(str);
  }

  public void onReset(final View v) {
    float min = mSliderHorizontal.getMin();
    float max = mSliderHorizontal.getMax();
    float newPos = (max - min) / 2 + min;
    mSliderHorizontal.setPosition(newPos);

    min = mSliderVertical.getMin();
    max = mSliderVertical.getMax();
    newPos = (max - min) / 2 + min;
    mSliderVertical.setPosition(newPos);
  }
}


That's it! Try your application again, perhaps a few times while changing the attributes in main.xml.




Where to go from here

Even though this series of articles is a long read, the actual steps are not that difficult.

The view just implemented is far from complete, but I do believe it's a good starting point. Things you may want to add are:

  • Make the view retain values between orientation changes.
  • Different colour of the indicator when touched, like the standard views.
  • React to long clicks or double clicks.
  • Displaying the value within the view instead of a separate view.
  • Animations, 3D graphics.
Keep in mind though, that this example extends a standard View. If you only want to modify one of the already existing views, for example a new behaviour for a button, you can opt for the much simpler route of just extending that type of view and only modify the things you need.

Now go and create astonishing views for your applications, and feel free to use the comments field below for any questions.

If you don't want to use the back button to read the articles again, you can use these links:

Thanks for reading. Please use the comments field if you have any questions.

/Robert

Creating a custom view, part 2 - Interaction

This is the second part in my article about how to create a custom view in Android.
Please follow these links for the other parts:
In this part of the article, the slider will get some useful functionality, like being able to position the indicator from the activity, and react on touches.

Step 3 Change indicator position from the activity.


This step is far from the complexity in step 2, but still complex enough to warrant a few substeps.

Step 3.1 Setters and getters.


git checkout step_3_1

The requirement for this step was to be able to change the indicator position, but it makes sense to also make it possible to also alter the min and max values.

The getters are easy, but the setters need a few extras.
Let Eclipse do the easy part for you, by right-clicking on your class and selecting Source->Generate Getters and Setters. Tick mMin, mMax and mPosition and make sure to generate both getter and setter for mPosition and mMin. You don't need one for mMax, for reasons that will become clear in a moment.
After the code has been generated, you probably want to tidy up the method names slightly. Your getters should look something like this:
public float getMin() {
    return mMin;
  }

  public float getMax() {
    return mMax;
  }

  public float getPosition() {
    return mPosition;
  }
As I said, the setters require a few extra lines. Since calling a setter will possibly alter a visible view, the view should be invalidated. But since invalidating views trigger a redrawing of it and possibly neighbouring views you should not invalidate unless needed. Furthermore you probably don't want the position to be outside of min and max, so you should check for that. Your setter for mPosition should thus look like this:
public void setPosition(float position) {
    position = within(mMin, mMax);

    if(position != mPosition) {
      mPosition = position;
      invalidate();
    }
  }
You will also need to implement that helper method, within(). It should look something like this:
private float within(float position, final float min, final float max) {
    if (position < min) {
      position = min;
    }
    if (position > max) {
      position = max;
    }
    return position;
  }

Here is the reason for not generating a setter for mMax. For the slider to work properly, mMin must always be smaller than mMax, and the indicator must always be positioned somewhere in between min and max. That's why mMin and mMax must be set in the same method. The setter should check min and max, and throw an exception if they are wrong, and it should also adjust the position of the indicator. The adjustment can be done by reusing the setter for the position, like this (renaming setMin to setMinMax):
public void setMinMax(final float min, final float max) {
    if ((min != mMin) || (max != mMax)) {
      if (min > max) {
        throw new IllegalArgumentException(
            "setMinMax: min must be smaller than max.");
      }
      mMin = min;
      mMax = max;
      setPosition(mPosition);
      invalidate();
    }
  }

Step 3.2 onPositionChanged


git checkout step_3_2

Being able to set and get the position is handy, but you will also need some way for the view to tell the activity that values have changed.
For this you will need a listener interface for your view and your activity needs to implement it in a usual manner. (The activity will be modified in the next step.)
Adding an listener interface is just a matter of specifying what should go in the interface and which object that should be tied to it.
First the interface. Put this into your class:
interface CustomSliderPositionListener {
    void onPositionChange(float newPosition);
  }
A new member field is also needed:
private CustomSliderPositionListener mPositionListener;

The field will need a setter:
public void setPositionListener(final CustomSliderPositionListener listener) {
    mPositionListener = listener;
  }

And it should be initialised in the constructor, so you should add this line to the end of the constructor:
mPositionListener = null;

Finally, the listener should be called if the position does indeed changes, so you will need to add a few lines to setPosition, inside the if statement just after the call to invalidate:
if (mPositionListener != null) {
        mPositionListener.onPositionChange(mPosition);
      }

Ok, done.
(You should really add similar code for getting notified about changes to min and max as well. Feel free to do that, but I will not add it here, for the sake of the size of the tutorial.)

Step 3.3 Update the activity.


git checkout step_3_3

Again, the activity is there to show you that things are working, so let's do just that.
In you main.xml, add a text view and a button. The purpose of the text view is to show the current position of the indicator, and the purpose of the button is to reset it to the centre.
Your new main.xml should look something like this:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    >
    <com.enea.training.customview.CustomSlider android:id="@+id/slider_horizontal"
      android:layout_height="wrap_content"
      android:layout_width="fill_parent"
      android:layout_alignParentLeft="true"
      android:layout_alignParentTop="true"
    />
    <Button android:id="@+id/button_reset"
      android:text="@string/reset"
      android:onClick="onReset"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentRight="true"
      android:layout_alignParentBottom="true"
      />
    <TextView android:id="@+id/values"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_alignParentRight="true"
      android:layout_above="@id/button_reset"
      />
</RelativeLayout>

Notice that I have changed the id of the slider, this is a hint of what is to come...
You will also need to add a string for your button. Add this to res/values/strings.xml:
<string name="reset">Reset</string>

You will also need to add a fair bit of code to your CustomSliderActivity. First of all you will need two member fields, for the text view and the slider:
private TextView     mValueHorizontal;
  private CustomSlider mSliderHorizontal;

You will also need to initialise th fields in onCreate. That includes implementing the onPositionChanged listener added in step 3.2, and setting min, max and position by calling the setters in step 3.1.
The listener calls to a helper method, displayValues() that will show the position of the indicator properly formatted in a text view.
Your new onCreate(), and the helper method, should look like this:
@Override
  public void onCreate(final Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);

    mValues = (TextView) findViewById(R.id.values);

    mSliderHorizontal = (CustomSlider) findViewById(R.id.slider_horizontal);
    mSliderHorizontal.setPositionListener(new CustomSliderPositionListener() {
      public void onPositionChange(final float newPosition) {
        displayValues();
      }
    });

    mSliderHorizontal.setMinMax(-100.0f, 100.0f);
    mSliderHorizontal.setPosition(30.0f);
    displayValues();
  }

  void displayValues() {
    final String str = String.format("Horizontal: %3.2f",
        mSliderHorizontal.getPosition());
    mValues.setText(str);
  }

All that is left now is to implement the callback for the button. Upon clicking the button, the indicator should go to the center of the slider. The code should look like this:
public void onReset(final View v) {
    final float min = mSliderHorizontal.getMin();
    final float max = mSliderHorizontal.getMax();
    final float newPos = (max - min) / 2 + min;
    mSliderHorizontal.setPosition(newPos);
  }
Time for another test run of your project. Make sure the code works as expected, and don't forget to click the button.
Modify the hard-coded values in onCreate and verify that the slider reflects your changes.

Step 4 onTouchListener


git checkout step_4

About time to interact a bit more. This is probably the simplest step of them all. All you need to do is in your constructor tell your view that it should use a touch listener:
setOnTouchListener(new OnTouchListener() {
      public boolean onTouch(final View v, final MotionEvent event) {
        final float pos;
        pos = (mMin + ((mMax - mMin) / (mIndicatorMaxPos - mIndicatorMinPos))
              * event.getX());
        setPosition(pos);
        return true;
      }
    });

Done. Run your project and try to move the indicator with your finger. Hit the reset button to get it back to the center.

That's it for this part of the article. In the next part, you will see how to make a vertical slider, and how to set some attributes from xml.


Please use the comments field if you have any questions.

/Robert

Friday, October 14, 2011

Using NFC in Android

Near Field Communication (NFC) is a technology for data exchange between devices in close proximity. It traces its root back to RFID, where a reader drives a passive electronic tag through a magnetic field. This is a typical use case for NFC, but it can also be used by two active devices to communicate with each other.

APIs for NFC have now been added to the Android SDK, and a few phones with the NFC capability are available. I have looked into how we could use NFC to improve an existing Android application we develop. The application is targeted for cities, and one of its features is to provide information about various places of public interest, and in some cases allow reports of malfunction to be sent through the application. This feature could make use of NFC by having tags posted at the places of interest, which would allow users to get directly to the information about the place by just tapping the phone on the tag. Another possibility would be to use Peer-to-Peer data exchange to let a user running the application tap another phone. If the other phone has the application installed, it would start the application on the same activity, otherwise the other phone may be redirected to the application on Android Market.

I looked more closely at the first use case of having tags at places of interest. One initial consideration is that the number of phones with NFC is still very small. It would therefore be desirable to have an alternative way to achieve the same functionality for other phones. For this reason I also had a look at using QR codes, which can be read through the camera in the phone.

When the Android system detects an NFC device, it searches for the most suitable application to process the data. NFC only works over a very short range, so having the user select the application to use from a list is only used as a last resort, since this would typically cause the user to move the phone away from the device, and thereby break the NFC connection. Instead, the NFC API has defined several types of intents with different priorities, and with different options to filter on tag content or technology. The highest priority is given to the foreground application, which can register itself to intercept any NFC intent. This is not a good option in our application, since it would require the user to start the application before tapping the phone on the tag.

The next highest priority is applications that filter on tag content, which would work well in this case. The content could be set to anything we want, since the tags would be made specifically for our application. A standard type of content which has support for filtering in Android is URIs. To ensure that our application gets the NFC intent we could use a non-standard scheme. The host part of the URI could be used to select the activity to use within the application, and an identifier could be included as a URI parameter:

abc://point?id=123

The intent filter for these NFC tags would look like this:

<intent-filter>
  <action android:name="android.nfc.action.NDEF_DISCOVERED" />
  <data android:host="point" android:scheme="abc" />
  <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

The same URI could also be encoded in a QR code. I used a bar code reader called ZXing which can easily be integrated with other applications. It can also do the reverse, i.e. generate a bar code image from text data which is useful for testing. There are two ways to use ZXing to get the scanned QR code into our application:
  1. Our application can send a specific intent defined by ZXing for integration. This starts the ZXing application and a result is returned when the scan is complete (or the scan was cancelled).
  2. The ZXing application is started and a QR code is scanned by the user. When the content is a URI, the user gets an option to open it. Our application can get the intent generated by this option by declaring an intent filter matching the URI.

For maximum usability, both ways could be implemented, allowing the user to proceed regardless of which application was started first. The way described in 1 above is described on the ZXing web page. The second way, which is what I tried, is quite simple. Our application declares an intent filter, which is very similar to the NFC intent filter above:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <data android:host="point" android:scheme="abc" />
  <category android:name="android.intent.category.DEFAULT" />
</intent-filter>

The URI can then be retrieved through the getData method on the received intent, in order to extract the id parameter, and this is used to find and display the desired information.

Some of the NFC features were introduced in API level 9, and the other were introduced in API level 10. The Android SDK develper guide describes how to declare an application to require this API level as well as NFC permission and hardware. This is however not what we want for our application. We want to use NFC if it is available, but the application should install and work without NFC and on old devices at lower API levels. The NFC permission must be declared to get NFC to work when it is available. The platform will use the permission declaration to implicitly derive the NFC hardware requirement, but the application can override this by explicitly declaring that NFC hardware is not required. Finally, the required API level can be kept at a low level while still allowing the application to use the NFC features in API level 10, by using the targetSdkVersion attribute. The result is the following declarations:

<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.nfc" android:required="false" />
<uses-sdk android:minsdkversion="4" android:targetsdkversion="10" />

The application can then be compiled with the API level 10 SDK, but can still be installed on anything with API level 4 or higher. In order to make use of NFC, the application will have to use classes in the android.nfc package, which is not available before API level 9. This may cause a  VerificationError on the application if it is not handled correctly. The solution is to put all dependencies on NFC into separate application classes that are only loaded on devices that actually support NFC. This works because the Dalvik VM only loads and verifies the application classes the first time any method or member in them are accessed. So even though some application classes would not compile on the API level of the device, the application will still run without errors as long as the classes using a higher API level are not touched. The application can check the API level of the device at runtime through the constant ”Build.VERSION.SDK_INT”. The application can also check if the NFC feature is available through the ”PacketManager.hasSystemFeature” method. Note that this method is only available from API level 5, so the API level must be checked first, and the feature check needs to be in a separate application class that is not loaded on lower API level devices.

The QR code alternative does not have the same problem with API levels (as the ZXing barcode reader only requires API level 3), but it will obviously only work if the barcode reader is installed. If the integration is implemented by starting the scan from ZXing, which then sends a ”VIEW” intent, then the application is not required to check anything. However in the other integration option, where the application sends an intent to the barcoder reader to initiate the scan, the application must check at runtime if the barcode reader is available. This check can be done with the ”PacketManager.queryIntentActivities” method. When the result of this check is negative, the application could disable any ”scan” button or anything else in the user interface used to initiate a scan operation. The application could also provide a link to the barcode reader in Android market to make it easy for the user to enable the QR code scanning feature. The link could be made like this:

Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse("market://search?q=pname:com.google.zxing.client.android"));
startActivity(intent);

In summary, I found that the NFC capability could indeed be used to add a new feature to our application. It would make the application easier to use, and it could be implemented without limiting the availablility of the application to phones that have NFC hardware. If QR codes are used as a complement to NFC tags, then the same functionality could be implemented for phones without NFC.

/Mikael