美文网首页
自定义View知识梳理

自定义View知识梳理

作者: Dengszzzzz | 来源:发表于2019-11-01 21:49 被阅读0次

    前言

    自定义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<count;k++){        
             View child = getChildAt(k);        
             int height = child.getMeasuredHeight();        
             int width = child.getMeasuredWidth();        
             //子View定位方法,它的参数是相对于父View来说的,也就是说如果要定位在父View的左上角
             //那么,l 和 t 应该传0。而不是传onLayout() 这个方法得到的l 和 t。
             child.layout(0,curHeight,width,curHeight + height);        
             curHeight += height;    
        }
    }
    
    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();
        }
    }
    
    <!--自定义属性-->
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
        <declare-styleable name="SlideShowView">
    
            <!--滑动多大距离,才判定是展开 or 收起-->
            <attr name="slide_effect_size" format="dimension"/>
    
            <!--是否可以滑动显示-->
            <attr name="enable_slide_show" format="boolean"/>
    
        </declare-styleable>
    </resources>
    
    <!--在布局中使用-->
    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
        <com.sz.dzh.dandroidsummary.widget.custom.SlideShowView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:layout_marginTop="20dp"
            android:orientation="vertical"
            app:slide_effect_size = "20dp">
    
            <LinearLayout
                android:layout_width="200dp"
                android:layout_height="200dp"
                android:gravity="center"
                android:background="@color/color_53">
    
                <TextView
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="详情" />
    
            </LinearLayout>
    
            <LinearLayout
                android:id="@+id/ll_drag"
                android:layout_width="200dp"
                android:layout_height="100dp"
                android:gravity="center"
                android:background="@color/colorPrimary">
    
                <TextView
                    android:id="@+id/tv_show"
                    android:layout_width="wrap_content"
                    android:layout_height="wrap_content"
                    android:text="显示详情"
                    android:padding="10dp"
                    android:textSize="@dimen/text_size20"/>
            </LinearLayout>
    
        </com.sz.dzh.dandroidsummary.widget.custom.SlideShowView>
    </LinearLayout>
    

    相关文章

      网友评论

          本文标题:自定义View知识梳理

          本文链接:https://www.haomeiwen.com/subject/vzjxbctx.html