自定义View知识梳理

前言

自定义View的基础是了解绘制的流程及相关方法(onMeasure()、onLayout()、onDraw()),了解事件分发机制及相关方法,还有Canvas、Paint等与绘制有关的类,详细的学习可看大神的文章
AndroidNote。此篇文章做个梳理,以及如何自定义一个展开收起控件。

下面这张图可以直观看出绘制的流程,非原创。


这是一张从其他文章拷贝过来的图.png

一、自定义View分类

1、自定义组合控件。例如继承LinearLayout,初始化时通过LayoutInflater添加xml布局,只需要得到布局的View做相应处理,不需要考虑测量、定位、绘制等方法。
2、继承系统控件,在基础功能上做拓展,比如继承EditText,在它右侧添加删除按钮。
3、继承View、ViewGroup,这种要复杂得多,需要了解View的绘制流程和关键方法,实现onMeasure()、onLayout()、onDraw(),实现触摸事件onTouchEvent()做相应处理,需要思考整个详细的流程。

二、绘制的流程及相关方法

1、onMeasure()
@Overrideprotected 
void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 
     
       //1、获取系统根据mode测量出来的宽高值,它不一定是最终的宽高值,因为重写onMeasure(),
       //一般都是想自己设置宽高,如果要拿最终的测量值,要从onSizeChanged()里面取。
       int size = MeasureSpec.getSize(widthMeasureSpec);
      
       //2、获取mode,三种返回值解释如下
       int mode = MeasureSpec.getMode(widthMeasureSpec);
       switch (mode) {    
           case MeasureSpec.UNSPECIFIED: 
           //未指定,在这个模式下父控件不会干涉子 View 想要多大的尺寸,比如可在RecyclerView源码看到它的使用。
           //自定义View时可以根据需求定制,比如mode是这个时,给宽高设置一个默认值。       
           break;    
           case MeasureSpec.AT_MOST:   
           //对应 wrap_content
           break;        
           case MeasureSpec.EXACTLY:  
           //对应确切的值和 match_parent    
          break;
         }
     
     //3、最后别忘了调这个方法设置宽高
     setMeasuredDimension(width, height);
}

自定义ViewGroup,除了上述方法,还要注意以下几个方法调用。
1)measureChildren(widthMeasureSpec,heightMeasureSpec)
触发每个子View的onMeasure(),这是必须调用的,写在onMeausre()最前面,不然后面无法得到子View宽高。
2)getChildCount()
获取直接子View的数量,也就是说ViewGroup里有两个子View,两个子View又有自己的子View,那么该ViewGroup 调用这个方法会得到 2。
3)getChildAt(int)
获取子View。

2、onLayout()

定位,确定子View在父View中的位置。这个方法在View的源码里是空实现,在ViewGroup源码是抽象方法,所以自定义View不需要这个方法,自定义ViewGroup时一定要重写这个方法。这是因为子View的定位是由父View决定,在父View的 onLayout() 方法里调用子View的 layout() 来定位子View。
大致流程如下:

/** * 
* 遍历循环子View,调用子View的layout(int l, int t, int r, int b)定位
*
* @param changed 
* @param l  MyViewGroup 的 左坐标 
* @param t  MyViewGroup 的 顶坐标 
* @param r  MyViewGroup 的 右坐标 
* @param b  MyViewGroup 的 底坐标 
*/
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {    
    int count = getChildCount();   
    int curHeight = 0;
    for(int k = 0;k
3、onDraw()

绘制,涉及到Paint,Canvas,Path等知识,此处不详细展开,注意不要在onDraw() 里 new 对象,例如Paint,应该在View初始化时设置。

4、onSizeChanged()

当View的size有变化时会调用,可以用来取最终宽高。

5、总结

自定义view
重写onMeasure()、onDraw()。
1)onMeasure():MeasureSpec.size()获取Size,MeasureSpec.mode()获取模式,最后记得调用setMeasuredDimension(width,size);设置宽高。
2)onSizeChanged():会得到最终的宽高,当view的size有变化时会调用。
3)onDraw():注意不要在此方法创建新对象,例如Paint不要放在里面new出来,Invalidate()和postInvalidate(),都会调用onDraw()重绘。如果需要重新测量定位,调用requestLayout()。

  1. TypeArray:获取attrs.xml定义的属性。

自定义ViewGroup
除了onMeasure() 和 onDraw(),还要重写onLayout()。
1)onMeasure():
除了上述相关内容,还要注意以下几点,measureChildren(),会触发每个子View的onMeasure(),注意和measureChild()区分;调用getChildCount()获取子View数量;调用getChildAt(i)获取子View。
2)onLayout():
遍历循环子View,调用子View的layout(int l, int t, int r, int b)定位。

三、事件分发机制及相关方法

1、在ViewGroup 事件分发
image.png
image.png
2、在View 消费事件
image.png

image.png

image.png
总结

1)事件分发流程dispatchTouchEvent()、onInterceptTouchEvent()、onTouchEvent(),如果做拦截事件,在ViewGroup 的 onInterceptTouchEvent()返回true即可,View 没有onInterceptTouchEvent()。
2)注意onTouchEvent() 和 onTouch() 的关系,自定义View时,常常需要重写 onTouchEvent()。
3)ACTION_DOWN、ACTION_MOVE、ACTION_UP 传递流程的梳理,自定义View的时候常见。

四、其他知识点以及注意事项(待更新)

1、LayoutInflater
三种方法的理解,详情请看 Android LayoutInflate深度解析 给你带来全新的认识

image.png

五、自定义控件学习例子

了解View的绘制和事件分发基本知识后,再去自定义控件还是有难度的。自定义控件难点在于怎么去把效果拆分,协调父View、子View之间的关系,然后一点一点去实现,而不是看到一个完整的效果懵逼。这个可以通过拆分别人的自定义控件去学习,考虑怎么达到这样的效果,下面推荐两个例子学习。

1、SlideView
Android自定义滑动确认控件SlideView
这是一个日常工作中很可能用到的控件。
自定义ViewGroup 和 View,获取自定义属性TypedArray,绘制流程onMeasure()、onLayout()、onDraw(),触摸事件处理onTouchEvent(),还有接口回调设置监听,整体逻辑不复杂,实用性强,适合入门学习。基本上不是太复杂的自定义控件就是这些内容了。

2、StepView
StepView
步骤指示器,可用于快递收件流程、任务完成流程等。

3、SlideShowView
一个下滑展开,上滑收起的View,具体效果如下图

效果展示.gif

需求分析:
两个View,可拖动的View 叫 sView, 上层View 叫 topView。
1、需要定义一个父View 来装 sView 和 topView,且 sView 是在 topView 的底层。
方案:RelativeLayout、FrameLayout、自定义ViewGroup 选一。
2、一开始只显示topView,sView完全不显示。
方案:重写父View onMeasure(),一开始设置高度为 topView 的宽高。
3、下滑上滑。
方案:重写onTouchEvent(),对三种状态做处理。
4、sView 展开和收起。
方案:动态改变sView高度、父View 的高度,重写onLayout()重新定位 sView。

public class SlideShowView extends ViewGroup {

    private String TAG = getClass().getSimpleName();

    /**
     * 可拖动View的宽高
     * */
    private int msHeight;
    private int msWidth;

    /**
     * 上层View的宽高
     * */
    private int mTopHeight;
    private int mTopWidth;

    /**
     * 布局最大宽高
     * */
    private int maxHeight;
    private int maxWidth;



    /**
     * 按下时的点
     * */
    private int downY = 0;

    /**
     * 当前高度
     * */
    private int curHeight;

    /**
     * 按下时,父View的高度
     * */
    private int downHeight;

    /**
     * 抬起时,父View的目标高度
     * */
    private int targetHeight;

    /**
     * 滑动距离
     * */
    private int slide = 0;

    /**
     * 属性:滑动有效距离
     * */
    private int mSlideEffectSize;

    /**
     * 属性:是否能滑动
     * */
    private boolean mEnableSlideShow;


    public SlideShowView(Context context) {
        this(context,null);
    }

    public SlideShowView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
    }

    public SlideShowView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //获取自定义属性
        TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.SlideShowView, 0, 0);
        mSlideEffectSize = a.getDimensionPixelSize(R.styleable.SlideShowView_slide_effect_size,50);
        mEnableSlideShow = a.getBoolean(R.styleable.SlideShowView_enable_slide_show,true);
        a.recycle();
    }


    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //第一测量,需要得到子View宽高
        if(curHeight == 0){
            //对所有的子View进行测量
            measureChildren(widthMeasureSpec,heightMeasureSpec);
            //得到直接子View的数量
            int childCount = getChildCount();
            //子View不是2个的,此控件失效
            if(childCount != 2){
                setMeasuredDimension(0,0);
            }else{
                //第一个View的宽高
                View child1 = getChildAt(0);
                msWidth = child1.getMeasuredWidth();
                msHeight = child1.getMeasuredHeight();

                //第二个子View的宽高
                View child2 = getChildAt(1);
                mTopWidth = child2.getMeasuredWidth();
                mTopHeight = child2.getMeasuredHeight();

                //整个viewGroup最大宽高
                maxWidth = Math.max(msWidth,mTopWidth);
                maxHeight = msHeight + mTopHeight;
                //初始设置高度为 上层View  的高度
                setMeasuredDimension(maxWidth,mTopHeight);
            }
        }else{
            //经由上下滑动改变高度测量
            setMeasuredDimension(maxWidth,curHeight);
        }
    }


    /**
     * 测量后确定的值
     * @param w
     * @param h
     * @param oldw
     * @param oldh
     */
    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        Log.e(TAG,"onSizeChanged:新宽--" + w + ",新高--" + h);
        curHeight = h;
    }


    /**
     * 定位,其实是定子View 相对于父View 的位置信息。
     * 此处两个子View。
     * topView:顶部和 父View 保持一致,不收滑动影响。
     * sView: 底部和 父View 保持一致,收滑动影响。
     *
     * @param changed
     * @param l
     * @param t
     * @param r
     * @param b
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        //第一个子View是可拖动的
        View child1 = getChildAt(0);
        //layout()里的参数,是指子View 在 父View 里的坐标,因为要和顶部保持一致,所以l和t都是0。
        child1.layout(0,curHeight - msHeight,msWidth,curHeight);


        //第二个子View是不变的
        View child2 = getChildAt(1);
        child2.layout(0,0,mTopWidth,mTopHeight);

    }


    /**
     * 触摸事件
     * @param event
     * @return
     */
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        if(!mEnableSlideShow){
            return false;
        }
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                downY = (int) event.getY();
                Log.e(TAG,"downY:" + downY);
                //记录按下时,整个父view的高
                downHeight = curHeight;
                break;
            case MotionEvent.ACTION_MOVE:
                /**
                 * slide < 0,往下滑动。 slide>0,往上滑动
                 * */
                slide = downY - (int)event.getY();
                if(slide < 0 && curHeight < maxHeight) {
                    //下滑操作,且当前高度没达到最大高度
                    curHeight = downHeight + Math.abs(slide);
                    requestLayout();
                }else if(slide > 0 && curHeight > mTopHeight){
                    //上滑操作,当前高度没有达到最小高度
                    curHeight = downHeight - Math.abs(slide);
                    requestLayout();
                }
                Log.e(TAG,"slide:" + slide);
                break;
            case MotionEvent.ACTION_UP:
                //滑动决策,滑动距离达到某个值,就进行展开 or 收起
                if(Math.abs(slide) > mSlideEffectSize){
                    if(slide<0){
                        targetHeight = maxHeight;
                    }else{
                        targetHeight = mTopHeight;
                    }
                }else{
                    //恢复原样
                    targetHeight = downHeight;
                }
                showAnim();
                Log.e(TAG,"最终高度:" + targetHeight);
                //requestLayout();
                break;
        }
        return true;
    }


    /**
     * 属性动画,过渡最终展开收起效果
     */
    private void showAnim(){
        ValueAnimator animator = ValueAnimator.ofInt(curHeight,targetHeight);
        animator.setDuration(300);
        animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                curHeight = (int) animation.getAnimatedValue();
                requestLayout();
            }
        });
        animator.setInterpolator(new LinearInterpolator());
        animator.start();
    }
}



    

        
        

        
        

    





    

        

            

        

        

            
        

    

你可能感兴趣的:(自定义View知识梳理)