Customizing a View: Essential Code Snippets
satya - Thu Oct 25 2012 10:07:27 GMT-0400 (Eastern Daylight Time)
Start with examining the over-ridable methods of the view
public abstract class AbstractBaseView
extends View
{
public static String tag="AbstractBaseView";
//Required if you are directly instantiating.
//You could have more constructors with more arguments if you want
//to consistently set the internal variables.
public AbstractBaseView(Context context) {
super(context);
}
//Called by the layout inflater.
//Use TypedArray approach to read the custom attributes
public AbstractBaseView(Context context, AttributeSet attrs) {
super(context, attrs);
}
//This is not called by the layout inflater but a derived class.
//A derived view when invoked by the layout inflater may choose to
//pass another attribute set reference from its theme whose
//resource id is defStyle.
public AbstractBaseView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
//Called by measure() of the base view class.
//This method is called only if a layout is requested on this child.
//Return after setting the required size through setMeasuredDimension
//The default onMeasure from View may be sufficient for you.
//But if you allow using match_parent for this view you may want to
//override the match_parent which by default takes the entire space
//supplied. The following logic implments this properly by overriding
//onMeasure. This should be suitable for a number of cases, otherwise
//you have all the logic here.
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
{
logSpec(MeasureSpec.getMode(widthMeasureSpec));
Log.d(tag, "size:" + MeasureSpec.getSize(widthMeasureSpec));
setMeasuredDimension(getImprovedDefaultWidth(widthMeasureSpec),
getImprovedDefaultHeight(heightMeasureSpec));
}
//Just a utility method to see what the spec is
private void logSpec(int specMode)
{
if (specMode == MeasureSpec.UNSPECIFIED)
{
Log.d(tag,"mdoe: unspecified");
return;
}
if (specMode == MeasureSpec.AT_MOST)
{
Log.d(tag,"mdoe: at msot");
return;
}
if (specMode == MeasureSpec.EXACTLY)
{
Log.d(tag,"mdoe: exact");
return;
}
}
/*
* This is called as part of layout() method from the base View class.
* onLayout() will be called subsequently.
* onLayout() is useful for view groups
* oldw and oldh are zero if you are newly added.
* View has already recorded the width and height
* invalidate() is already active and called by the super class
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh)
{
super.onSizeChanged(w,h,oldw,oldh);
}
//Called by layout() of the View class
//By the time onLayout() is called the onSizeChanged
//is already called by layout().
//You typically override this when you are writing your own layouts.
//Then you will call the layout() on your children.
//
//If you are customizing just a view you don't have to override this method.
//parent onLayout() is empty.
//
//changed: this is a new size or position for this view
//rest: postions with respect to the parent
//
@Override
protected void onLayout (boolean changed, int left, int top, int right, int bottom)
{
Log.d(tag,"onLayout");
super.onLayout(changed, left, top, right, bottom);
}
//onDraw may not be called if you don't invalidate.
//You can use the size of the view and don't have to
//rely on onSizeChanged. Because by the time this method is called
//the layout method has already set the sizes properly.
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(tag,"onDraw called");
}
//There are number of ways to save and restore state.
//The recommended approach is for each dervied view to
//do this. So it is hard to encapsulate that in a base class.
//See CircleView for a pattern that uses BaseSavedData from
//the base View class.
@Override
protected void onRestoreInstanceState(Parcelable p)
{
Log.d(tag,"onRestoreInstanceState");
super.onRestoreInstanceState(p);
}
@Override
protected Parcelable onSaveInstanceState()
{
Log.d(tag,"onSaveInstanceState");
Parcelable p = super.onSaveInstanceState();
return p;
}
/*
* Unspecified:
* used for scrolling
* means you can be as big as you would like to be
* return the maximum comfortable size
* it is ok if you are bigger because scrolling may be on
*
* Exact:
* You have indicated your explicist size in the layout
* or you said match_parent with the parents exact size
* return back the passed in size
*
* atmost:
* I have this much space to spare
* sent when wrap_content
* Take as much as you think your natural size is
* this is a bit misleading
* you are advised not to take all the size
* you should be smaller and return a preferred size
* if you don't and take all the space, the other siblings
* will get lost.
* In this implementation I used the minimum size to satisfy
* atmost.
*
*/
private int getImprovedDefaultHeight(int measureSpec) {
//int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
return hGetMaximumHeight();
case MeasureSpec.EXACTLY:
return specSize;
case MeasureSpec.AT_MOST:
return hGetMinimumHeight();
}
//you shouldn't come here
Log.e(tag,"unknown specmode");
return specSize;
}
private int getImprovedDefaultWidth(int measureSpec) {
//int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
return hGetMaximumWidth();
case MeasureSpec.EXACTLY:
return specSize;
case MeasureSpec.AT_MOST:
return hGetMinimumWidth();
}
//you shouldn't come here
Log.e(tag,"unknown specmode");
return specSize;
}
//Override these methods to provide a maximum size
//"h" stands for hook pattern
abstract protected int hGetMaximumHeight();
abstract protected int hGetMaximumWidth();
protected int hGetMinimumHeight()
{
return this.getSuggestedMinimumHeight();
}
protected int hGetMinimumWidth()
{
return this.getSuggestedMinimumWidth();
}
}
satya - Thu Oct 25 2012 10:08:13 GMT-0400 (Eastern Daylight Time)
Here is the CircleView (most code is for managing the view state)
public class CircleView
extends AbstractBaseView
implements OnClickListener
{
public static String tag="CircleView";
private int defRadius = 20;
private int strokeColor = 0xFFFF8C00;
private int strokeWidth = 10;
public CircleView(Context context) {
super(context);
initCircleView();
}
public CircleView(Context context, int inStrokeWidth, int inStrokeColor) {
super(context);
strokeColor = inStrokeColor;
strokeWidth = inStrokeWidth;
initCircleView();
}
public CircleView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
//Meant for derived classes to call
public CircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
TypedArray t = context.obtainStyledAttributes(attrs,R.styleable.CircleView, defStyle,0);
strokeColor = t.getColor(R.styleable.CircleView_strokeColor, strokeColor);
strokeWidth = t.getInt(R.styleable.CircleView_strokeWidth, strokeWidth);
t.recycle();
initCircleView();
}
public void initCircleView()
{
this.setMinimumHeight(defRadius * 2);
this.setMinimumWidth(defRadius * 2);
this.setOnClickListener(this);
this.setClickable(true);
this.setSaveEnabled(true);
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(tag,"onDraw called");
int w = this.getWidth();
int h = this.getHeight();
int t = this.getTop();
int l = this.getLeft();
int ox = w/2;
int oy = h/2;
int rad = Math.min(ox,oy)/2;
canvas.drawCircle(ox, oy, rad, getBrush());
}
private Paint getBrush()
{
Paint p = new Paint();
p.setAntiAlias(true);
p.setStrokeWidth(strokeWidth);
p.setColor(strokeColor);
p.setStyle(Paint.Style.STROKE);
return p;
}
@Override
protected int hGetMaximumHeight()
{
return defRadius * 2;
}
@Override
protected int hGetMaximumWidth()
{
return defRadius * 2;
}
public void onClick(View v)
{
//increase the radius
defRadius *= 1.2;
adjustMinimumHeight();
requestLayout();
invalidate();
}
private void adjustMinimumHeight()
{
this.setMinimumHeight(defRadius * 2);
this.setMinimumWidth(defRadius * 2);
}
/*
* ***************************************************************
* Save and restore work
* ***************************************************************
*/
@Override
protected void onRestoreInstanceState(Parcelable p)
{
this.onRestoreInstanceStateStandard(p);
this.initCircleView();
}
@Override
protected Parcelable onSaveInstanceState()
{
return this.onSaveInstanceStateStandard();
}
/*
* ***************************************************************
* Use a simpler approach
* ***************************************************************
*/
private Parcelable onSaveInstanceStateSimple()
{
//call the base class as it will return a marker object
//You will need to pass it back to the parent as is!
Parcelable p = super.onSaveInstanceState();
Bundle b = new Bundle();
b.putInt("defRadius",defRadius);
b.putParcelable("super",p);
return b;
}
private void onRestoreInstanceStateSimple(Parcelable p)
{
if (!(p instanceof Bundle))
{
throw new RuntimeException("unexpected bundle");
}
Bundle b = (Bundle)p;
defRadius = b.getInt("defRadius");
Parcelable sp = b.getParcelable("super");
//You have to do this. Parent view is expecting it
super.onRestoreInstanceState(sp);
}
/*
* ***************************************************************
* Use a standard approach
* ***************************************************************
*/
private void onRestoreInstanceStateStandard(Parcelable state)
{
//If it is not yours doesn't mean it is BaseSavedState
//You may have a parent in your hierarchy that has their own
//state derived from BaseSavedState
//It is like peeling an onion or a Russian doll
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
//it is our state
SavedState ss = (SavedState)state;
//Peel it and give the child to the super class
super.onRestoreInstanceState(ss.getSuperState());
defRadius = ss.defRadius;
}
private Parcelable onSaveInstanceStateStandard()
{
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.defRadius = this.defRadius;
return ss;
}
/*
* ***************************************************************
* Saved State inner static class
* ***************************************************************
*/
public static class SavedState extends BaseSavedState {
int defRadius;
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(defRadius);
}
//Read back the values
private SavedState(Parcel in) {
super(in);
defRadius = in.readInt();
}
@Override
public String toString() {
return "CircleView defRadius:" + defRadius;
}
@SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}//eof-state-class
}//eof-main-view class
satya - Thu Oct 25 2012 10:08:44 GMT-0400 (Eastern Daylight Time)
Here is attrs.xml
<resources>
<declare-styleable name="CircleView">
<attr name="strokeWidth" format="integer"/>
<attr name="strokeColor" format="color|reference" />
</declare-styleable>
</resources>
satya - Thu Oct 25 2012 10:09:35 GMT-0400 (Eastern Daylight Time)
Here is a sample layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:apptemplate="http://schemas.android.com/apk/res/com.androidbook.custom"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="match_parent"
>
<com.androidbook.custom.MyTextView
android:id="@+id/custom_text_id"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Debut Text Appears here"
apptemplate:custom_text="Custom Hello"
/>
<com.androidbook.custom.CircleView
android:id="@+id/circle_view_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
apptemplate:strokeWidth="5"
apptemplate:strokeColor="@android:color/holo_red_dark"
/>
<TextView
android:id="@+id/text1"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="Debut Text Appears here"
/>
</LinearLayout>
satya - Thu Oct 25 2012 10:32:50 GMT-0400 (Eastern Daylight Time)
Here is what it will look like first
That is 3 views. The circle view is in the middle. To start out it has a smaller radius. On each mouse click it will get bigger. here is that view.
satya - Thu Oct 25 2012 10:33:16 GMT-0400 (Eastern Daylight Time)
Responding to events
satya - Thu Oct 25 2012 10:33:48 GMT-0400 (Eastern Daylight Time)
Now flip it through view state management