一、继承View复写onDraw方法
新建Paint对象用于绘制自定义图像
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
复写onDraw方法(注意手动实现padding属性,部分代码)
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int paddingLeft = getPaddingLeft(); //使padding属性生效
//在计算宽高时,考虑padding
int width = getWidth()-paddingLeft-paddingRight;
//绘制自定义图形
canvas.drawCircle(paddingLeft+width/2,+paddingTop+height/2,radius,mPaint);
}
复写onMeasure方法,以实现wrap_content
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//获得Spec模式
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
//获得spec宽
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
//复写实现wrap_content,赋予默认值
setMeasuredDimension(200,200);
}
//多情况判断
}
以上已粗略完成一个简单的自定义View,为了使用更为方便,为自定义View添加自定义属性
1,在values目录下新建attrs.xml文件,用于定义自定义属性
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<declare-styleable name="CircleView" >
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
2,在自定义View的构造方法中,加载自定义属性(部分代码)
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.GREEN);
//获得并使用资源,并设置默认值
a.recycle();
//实现资源
}
注:1、为了自定义属性保证生效,在两参数构造方法中调用三参数构造方法。
2、在布局使用自定义属性时,应使用新的命名空间。
二、继承ViewGroup派生特殊的Layout
用于实现自定义的布局,需要合适地处理ViewGroup的测量、布局这两个过程,并同时处理子元素的测量和布局过程。在此编写一父布局左右滑动,子元素上下滑动的自定义布局。以下分段分析代码。
1、复写onInteceptTouchEvent方法,事件分发方法,用于解决滑动冲突的问题。
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
//获得滑动坐标
int x = (int)ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
intercepted = false;
//优化滑动体验
if (!mScroller.isFinished()) {
//此处为滑动结束后,若滑动动画未结束下一次事件仍由父容 //器拦截,但实际效果不佳,快速切换时误操作频繁
//取消拦截可减少,但会有快速切换仍会有些许迟滞感
mScroller.abortAnimation();
//intercepted = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
//记录滑动距离
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
//水平滑动距离大于垂直时,认为是水平滑动事件,父容器拦截
if (Math.abs(deltaX) > Math.abs(deltaY)*2) {
intercepted = true;
} else intercepted = false;
break;
}
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
//记录坐标
mLastX = x;
mLastY = y;
mLastYIntercept = y;
mLastXIntercept = x;
return intercepted;
}
2、复写onTouchEvent方法,负责处理父容器的点击事件,在移动(ACTION_MOVE)时,通过scrollBy方法动态移动布局。在操作结束时(ACTION_UP),判断当前的位置已确定是否移动画面,以避免子元素滑动至一半的情形。
smoothScroll方法为自定义弹性滑动方法,用Scroller实现。
public boolean onTouchEvent(MotionEvent event) {
//跟踪滑动速度
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
//滑动效果
int deltaX = x - mLastX;
int deltaY = y - mLastY;
scrollBy(-deltaX,0);
break;
case MotionEvent.ACTION_UP:{
//记录滑动距离
int scrollX = getScrollX();
//设置速度事件间隔
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();
//大于五十时,认为又滑过一个子项
if (Math.abs(xVelocity) >= 50){
mChildIndex = xVelocity > 0 ? mChildIndex-1:mChildIndex+1;
}else{
//否则计算得到划过子项个数
mChildIndex = (scrollX + mChildWidth / 2)/mChildWidth;
}
//最终值大于0小于子项总数
mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize-1));
//计算自动滑动距离,缓慢滑动
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx,0);
mVelocityTracker.clear();
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
3、复写onMeasure方法,实现父布局以及子元素的measure过程,主要逻辑为判断SpecMode类型,分情况计算父布局的宽,高。子元素通过measureChildren方法完成measure过程。
此处有待改进的地方:没有考虑padding以及margin属性的作用,而无子元素时也不应将高宽直接赋值为0,应进一步判断进行赋值。
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
final int childCount = getChildCount();
//执行子元素的Measure方法
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
//无子项,高宽为0
if (childCount == 0){
setMeasuredDimension(0,0);
}//以下多次判断,由父布局的SpecMode,计算得父布局的高宽并设置
else if(widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getWidth()*childCount;
measureHeight = childView.getHeight();
setMeasuredDimension(measureWidth,measureHeight);
}else if (heightSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize,measureHeight);
}else if (widthSpecMode == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getWidth()*childCount;
setMeasuredDimension(measureWidth,heightSpecSize);
}
}
4、复写onLayout方法,用于完成父布局的layout过程,即是遍历所有子元素,完成子元素的layout过程。
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 < mChildrenSize ; i++){
final View childView = getChildAt(i);
//判断是否可见
if (childView.getVisibility() != View.GONE){
//执行子元素Layout
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
以上基本完成了一个简单的自定义View,还有一些细节问题,如Scroller、VelocityTracker相关方法的使用等。
总结一下,实现继承Viewgroup的自定义layout,只需分别手动实现Measure、Layout、onTouchEvent方法,完成父容器以及子元素的构建过程即可。为了解决滑动冲突的问题,也只需复写onInterceptTouchEvent方法,实现自定义的事件分发逻辑。
其中,Measure过程调用measureChildren方法完成子元素的测量过程,父布局则根据SpecMode具体计算宽高。
Layout过程遍历子元素,调用子元素的layout方法即可。
onTouchEvent方法,则是书写移动以及操作结束时的View动态变化的过程。