一、继承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动态变化的过程。