This is the final part in the series of articles describing how to create a custom view.
For the other parts, follow these links:
- Part I - Graphics
- Part II - Interaction
- Part III - Xml Attributes
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) {Note how the calculation of pos differs.
pos = (mMax - ((mMax - mMin) / (mIndicatorMinPos - mIndicatorMaxPos))
* event.getY());
} else {
pos = (mMin + ((mMax - mMin) / (mIndicatorMaxPos - mIndicatorMinPos))
* event.getX());
}
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) {Note how max and min relates to the mViewRect coordinates.
mIndicatorOffset = mIndicator.getIntrinsicHeight();
mIndicatorMaxPos = mViewRect.top + mIndicatorOffset;
mIndicatorMinPos = mViewRect.bottom - mIndicatorOffset;
} else {
mIndicatorOffset = mIndicator.getIntrinsicWidth();
mIndicatorMaxPos = mViewRect.right - mIndicatorOffset;
mIndicatorMinPos = mViewRect.left + mIndicatorOffset;
}
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"?>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
<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>
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,Now, instead of just hard-coding mIsVertical to true or false, you should grab the orientation from your attributes. But since the orientation attribute
R.styleable.CustomSlider);
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);Look at the help for getInt() and make sure that you understand what the parameters mean.
mIsVertical = (vertical != 0);
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"To make things a bit more interesting, you could add another slider, and make it vertical:
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"
/>
<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.
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:
- Part I - Graphics
- Part II - Interaction
- Part III - Xml Attributes
Thanks for reading. Please use the comments field if you have any questions.
/Robert