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