分类:
- 继承View重写onDraw方法:这种情况下,需要自己支持wrap_content,并且padding也需要自己处理。
- 继承ViewGroup派生特殊的layout:需要处理ViewGroup的测量和布局这两个过程,并同时处理子元素的测量和布局过程。
- 继承特定的View(比如TextView):这种方法一般不需要自己处理wrap_content和padding。
- 继承特定的ViewGroup(比如LinearLayout)
自定义View须知:
- 对于继承View和ViewGroup的情况下,需要处理wrap_content和padding
- 尽量不要在View中使用handler,因为View内部本身就提供了post系列。
- View中如果有线程或动画,需要及时停止。当View不可见或View的Activity推出时,要及时停止线程和动画,否则有可能造成内存泄漏。我们一般是在onDetacheddFromWindow中去停止,因为当View不可见或Activity退出时,该方法会被调用。
- View带有滑动嵌套情形时,需要处理好滑动冲突。
示例一:继承View重写onDraw方法
这里我们来实现自定义圆,首先看到onMeasure方法:
解析传入的宽高 --- 考虑wrap_contet情况 --- 若存在,则设置一个默认的值
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取到width,height对应的mode和size
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//考虑wrap_content的情况,需要为其设置默认的值,这里设置为200dp
//对于宽高都设定为wrap_content时
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, 200);
}
//对于高设定为wrap_content的情况
else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSpecSize);
}
//对于宽设定为wrap_content的情况
else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, 200);
}
}
接下来是,实现onDraw方法:
要注意的一点是,这里要处理padding的情况
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取到padding值
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//获取到真实的宽高
int height = getHeight() - paddingTop - paddingBottom;
int width = getWidth() - paddingLeft - paddingRight;
//以最小的值/2作为半径
int radius = Math.min(width, height) / 2;
//进行画圆
canvas.drawCircle(paddingLeft + width/2 , paddingTop + height/2, radius, mPaint);
}
我们还可以为其设置自定义属性:
a. 在res/values中创建一个xml文件:
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color"/>
</declare-styleable>
</resources>
其中,name表示为属性名,format表示为属性类型,这里是color类型。
b. 在布局中引入:xmlns:app = "http://schemas.android.com/apk/res-auto"
并设置自定义控件:
<com.example.viewtest.CircleView
android:id="@+id/circleView"
android:background="#000"
app:circle_color = "@color/gray_color"
android:padding="10dp"
android:layout_margin="10dp"
android:layout_width="200dp"
android:layout_height="wrap_content" />
我们可以看到其中的app:cirle_color表示为自定义的属性。
c. 在自定义View中获取到属性:
//获取到自定义属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//获取到自定义属性的值,其中第二个参数为默认参数,表示若不存在,则设置为默认值
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
完整代码如下:
public class CircleView extends View {
//定义颜色和画笔
private int mColor = Color.RED;
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
public CircleView(Context context) {
super(context);
init();
}
public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
//获取到自定义属性
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//获取到自定义属性的值,其中第二个参数为默认参数,表示若不存在,则设置为默认值
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
init();
}
public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mColor = a.getColor(R.styleable.CircleView_circle_color, Color.RED);
init();
}
//给画笔设置颜色
private void init(){
mPaint.setColor(mColor);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获取到width,height对应的mode和size
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
//考虑wrap_content的情况,需要为其设置默认的值,这里设置为200dp
//对于宽高都设定为wrap_content时
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, 200);
}
//对于高设定为wrap_content的情况
else if (widthSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(200, heightSpecSize);
}
//对于宽设定为wrap_content的情况
else if (heightSpecMode == MeasureSpec.AT_MOST){
setMeasuredDimension(widthSpecSize, 200);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//获取到padding值
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();
//获取到真实的宽高
int height = getHeight() - paddingTop - paddingBottom;
int width = getWidth() - paddingLeft - paddingRight;
//以最小的值/2作为半径
int radius = Math.min(width, height) / 2;
//进行画圆
canvas.drawCircle(paddingLeft + width/2 , paddingTop + height/2, radius, mPaint);
}
}
实现效果:
实例二:自定义的ViewGroup
实现一个类似ViewPager的自定义ViewGroup:
a. 实现onMeasure方法:
遍历孩子元素进行测量 --- 考虑wrap_content情况:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//定义两个变量,作为测量的宽和高
int measureWidth = 0;
int measureHeight = 0;
//获取到孩子个数
final int childCount = getChildCount();
//遍历孩子元素,对孩子元素分别进行测量
measureChildren(widthMeasureSpec, heightMeasureSpec);
//解析宽高,获取到模式和大小
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//考虑wrap_content情况
//孩子为0的情况下
if (childCount == 0){
setMeasuredDimension(0 ,0);
}
//宽高都是为wrap_content
else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
//获取到对应的宽度
measureWidth = childView.getMeasuredWidth() * childCount;
//设置孩子的高度作为当前高度
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if(widthSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measureHeight);
}
}
b. 实现布局方式:
遍历所有的孩子元素,若孩子元素不为GONE模式,则进行放置其位置。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
//遍历所有的孩子元素
for (int i = 0; i < childCount; i ++){
final View childView = getChildAt(i);
//若当前的孩子元素不为GONE,则对该孩子进行放置其位置
if (childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}