在Android开发中有时候需要自定义一些View或者Layout来满足特殊的需求,自定义View需要集成View类 并重载一些必要的方法,同时为了能够在XML文件中像使用系统原生控件一样简单,也需要自定义一些属性。
自定义View可以从继承View类开始,有两个构造方法最好都实现了,第一个构造方法比较方便在代码中实例化类的时候使用,第二个构造方法多了
一个 AttributeSet 参数,这个参数是用来在XML中配置类的属性的时候使用的,如果再XML布局中使用必须实现这个方法。
class PieChart extends View {
    public PieChart(Context context) {
        super(context);
    }
    
    public PieChart(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
}
为了能够在XML中配置自定义控件的属性,需要先在attrs.xml文件中把需要的配置属性声明出来,再在代码中解析这些属性,然后就可以在
XML布局中使用了。比如对于PieChart这个控件,希望可以在XML中配置显示的文字(showText)和文字位置(labelPosition),那么需要在
attrs.xml文件中添加一个<declare-styleable>元素,并做类似这样的配置:
<resources>
   <declare-styleable name="PieChart">
       <attr name="showText" format="boolean" />
       <attr name="labelPosition" format="enum">
           <enum name="left" value="0"/>
           <enum name="right" value="1"/>
       </attr>
   </declare-styleable>
</resources>
<declare-styleable> 元素的名字约定是和类的名字一样的都是 PieChart(并不是必须的,这样约定方便编辑器做代码补全),然后就可以在
XML布局中使用这些属性了。在XML文件中有命名空间的概念告诉XML文件某个属性的来源和说明,在我们写下android:layout_width="wrap_content"
的时候可能并没有注意到在XML文件的开始有一句话 xmlns:android="http://schemas.android.com/apk/res/android" 这是一个命名空间的声明,引号里面的是相关属性的命名空间,android
是这个命名空间的别名,关于命名空间的概念可以查看这里。在自定义的XML文件中所引用的属性也需要加上这样一个声明才可以使用,Android中约定成这样http://schemas.android.com/apk/res/[your package name]。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
   xmlns:custom="http://schemas.android.com/apk/res/com.example.customviews">
  <com.example.customviews.charting.PieChart
     custom:showText="true"
     custom:labelPosition="left" />
</LinearLayout>
但是这样在XML文件中配置完了,并不会对PieChart这个View产生任何影响,还需要在代码中解析这些属性,在通过XNL配置的View中,配置的属性
在代码中是通过 ` AttributeSet 来传递的,Android在解析这个XML布局的时候会把这些配置信息解析出来,并通过 AttributeSet`传入到View的
构造函数中。
public PieChart(Context context, AttributeSet attrs) {
   super(context, attrs);
   TypedArray a = context.getTheme().obtainStyledAttributes(
        attrs,
        R.styleable.PieChart,
        0, 0);
   try {
       mShowText = a.getBoolean(R.styleable.PieChart_showText, false);
       mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0);
   } finally {
       a.recycle();
   }
}
AttributeSet 中存储的属性信息是经过编码的,需要通过TypedArray 来把这些信息解析出来。为了在代码中也能更改一些必要信息,还需要
提供一些函数设置这些属性
public void setShowText(boolean showText) {
   mShowText = showText;
   // View内容发生变化的时候调用此方法法,重绘View
   invalidate();
   // View的大小发生变化的时候调用此方法,重新布局
   requestLayout();
}
View被绘制的时候都会调用onDraw(Canvas c)方法,这个方法只有一个参数Canvas ,Canvas就是一张画布啦,通过这个类提供的诸多方法
就可以图形画到屏幕上了。有了画布还需要画笔Paint,对于这两个类Android文档上有一段详细的解释
一般来说画布只有一个,但是画笔却有很多,绘制View的时候,需要提前定义好这个画笔,避免在绘制的时候再生成, 在PieChart示例中会定义 这些画笔。
private void init() {
   mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mTextPaint.setColor(mTextColor);
   if (mTextHeight == 0) {
       mTextHeight = mTextPaint.getTextSize();
   } else {
       mTextPaint.setTextSize(mTextHeight);
   }
   mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   mPiePaint.setStyle(Paint.Style.FILL);
   mPiePaint.setTextSize(mTextHeight);
   mShadowPaint = new Paint(0);
   mShadowPaint.setColor(0xff101010);
   mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL));
   ...
在继续View的绘制之前有必要看一下View在被绘制的时候的生命周期
| 分类 | 方法 | 描述 | 
|---|---|---|
| Creation | Constructors | 构造方法 | 
| Creation | onFinishInflate() | 一个View从XML中初始化完之后调用 | 
| Layout | onMeasure(int, int) | 用来决定这个View和它的子View的大小 | 
| Layout | onLayout(boolean, int, int, int, int) | 决定子View的位置和大小 | 
| Layout | onSizeChanged(int, int, int, int) | View 的大小变化的时候调用 | 
| Drawing | onDraw(Canvas) | View被绘制的时候调用 | 
一个View会先调用构造方法创建自己,如果是从XML文件中构造的还会调用 onFinishInflate方法,之后会调用 onMeasure
方法来决定自己的大小,如果是包含子View的Layout类型的View还需要在 onLayout 中计算子View的大小和位置。最后在View大小确定的情况
下会调用 onSizeChanged 方法,最后在绘制的时候调用 onDraw 方法。
onMeasure 是非常重要的方法,它会接收两个参数,这两个参数来自父View,是父View期望的大小,这个方法没有返回值,但是必须调用
setMeasuredDimension() 方法,来确定View的最终大小。它的两个参数是经过编码的int型,里面包含了模式和大小的信息,可以用View.MeasureSpec
来解析。
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
模式有如下三种
一般情况可以这么处理
      int desiredWidth = xxxxx; //计算期望的宽度
	    int desiredHeight = xxxxx; //计算期望的高度
	    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
	    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
	    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
	    int heightSize = MeasureSpec.getSize(heightMeasureSpec);
	    int width;
	    int height;
	    //Measure Width
	    if (widthMode == MeasureSpec.EXACTLY) {
	        //Must be this size
	        width = widthSize;
	    } else if (widthMode == MeasureSpec.AT_MOST) {
	        //Can't be bigger than...
	        width = Math.min(desiredWidth, widthSize);
	    } else {
	        //Be whatever you want
	        width = desiredWidth;
	    }
	    //Measure Height
	    if (heightMode == MeasureSpec.EXACTLY) {
	        //Must be this size
	        height = heightSize;
	    } else if (heightMode == MeasureSpec.AT_MOST) {
	        //Can't be bigger than...
	        height = Math.min(desiredHeight, heightSize);
	    } else {
	        //Be whatever you want
	        height = desiredHeight;
	    }
	    //MUST CALL THIS
	    setMeasuredDimension(width, height);
或者可以调用 resolveSizeAndState() 这个方法 内部实现和上面是一样的。对于PieChart类来说,可以使用下面这些代码来决定自身大小。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   // Try for a width based on our minimum
   int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth();
   int w = resolveSizeAndState(minw, widthMeasureSpec, 1);
   // Whatever the width ends up being, ask for a height that would let the pie
   // get as big as it can
   int minh = MeasureSpec.getSize(w) - (int)mTextWidth + getPaddingBottom() + getPaddingTop();
   int h = resolveSizeAndState(MeasureSpec.getSize(w) - (int)mTextWidth, heightMeasureSpec, 0);
   setMeasuredDimension(w, h);
}
真正的绘制需要在onDraw方法中进行,Canvas类提供了各种各样的方法来绘制图形
protected void onDraw(Canvas canvas) {
   super.onDraw(canvas);
   // Draw the shadow
   canvas.drawOval(
           mShadowBounds,
           mShadowPaint
   );
   // Draw the label text
   canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint);
   // Draw the pie slices
   for (int i = 0; i < mData.size(); ++i) {
       Item it = mData.get(i);
       mPiePaint.setShader(it.mShader);
       canvas.drawArc(mBounds,
               360 - it.mEndAngle,
               it.mEndAngle - it.mStartAngle,
               true, mPiePaint);
   }
   // Draw the pointer
   canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint);
   canvas.drawCircle(mPointerX, mPointerY, mPointerSize, mTextPaint);
}
Android中把View分成了两种一种是直接继承自View的比较原子的View类型,比如Button、TextView等等,一种是布局类型,比如LinearLayout、RelativeLayout等等。对于后者需要处理子View的事件分发和绘制问题。在绘制方面父View主要需要触发子View的布局(onLayout)和测量(onMeasure)事件,将这些事件从父View传递到子View。
实现自定义的布局,大部分情况下需要继承ViewGroup,对于这种情况,需要格外的处理子View的事件,在onMeasure方法中手动调用子View的measure方法,来触发子View的onMeasure方法
@Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    	super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    	  int mHeight = View.MeasureSpec.getSize(heightMeasureSpec);
        int mWidth = View.MeasureSpec.getSize(widthMeasureSpec);
        setMeasuredDimension(mWidth, mHeight);
        
        int count = getChildCount();
        
        for (int i = 0; i < count; i++) {
            getChildAt(i).measure(MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(mChildSize, MeasureSpec.EXACTLY));
        }
    }
同时需要重载 onLayout 来确定子View的布局,下面是一个横向的布局
@Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
    	  int count = getChildCount();
        
        int childWidth = xxxx;
        int childHeight = xxxx;
        
        for (int i = 0; i < count; i++) {
            getChildAt(i).layout(childWidth*i, 0, childWidth*(i+1),childHeight);
        }
    	}
    }
Android文档中提供了一个更全面的例子,见这里。
有的时候只需继承一个现有的View比如 Button就可以实现某些功能,有的时候需要组合其他的多种View,比如AutoCompleteTextView是一个EditText 和ListView的组合。
内容来源:
nTop 20 January 2015