美文网首页
Android 自定义View

Android 自定义View

作者: 潜心之力 | 来源:发表于2018-01-13 16:55 被阅读0次

    一、构造方法

        public MyView(Context context) {  //在代码中直接创建对象
            super(context);
        }
    
        public MyView(Context context, @Nullable AttributeSet attrs) { //默认调用
            super(context, attrs);
        }
    
        public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { //手动显式调用
            super(context, attrs, defStyleAttr);
        }
    
        public MyView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {  //手动显式调用
            super(context, attrs, defStyleAttr, defStyleRes);
        }
    
    TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.BottomView);
    array.recycle();
    
    参数解析:
    • context:上下文,View当前处于的环境。
    • attrs:View的属性参数,一般在布局文件中设置。
    • defStyleAttr:attrs.xml文件中的attribute,属于自定义属性。
    • defStyleRes:styles.xml文件中的style,只有当defStyleAttr没有起作用,才会使用到这个值。
        //自定义属性文件,attr.xml
        <declare-styleable name="SlideMenu">
            <attr name="rightPadding" format="dimension"/>
        </declare-styleable>
    
    一个属性最终的取值,有一个顺序,这个顺序优先级从高到低依次是:

    1.直接在XML文件中定义的 ==》布局文件。
    2.在XML文件中通过style这个属性定义的 ==》在布局中使用自定义属性样式。
    3.通过defStyleAttr定义的 ==》在View的构造方法中使用自定义属性样式。
    4.通过defStyleRes定义的 ==》在View的构造方法中使用自定义样式。
    5.直接在当然工程的theme主题下定义的 ==》AndroidManifest.xml中设置。

    二、测量流程

     @Override -> 使用默认的测量方法,由父类测量
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int measuredWidth =getMeasuredWidth();
            int measuredHeight =getMeasuredHeight();
            //测量已经完成,可以获取测量的参数值
        }
    
     @Override -> 覆写默认的测量方法,自定义测量规则
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            //测量模式
            int widthMode =MeasureSpec.getMode(widthMeasureSpec);
            int heightMode =MeasureSpec.getMode(heightMeasureSpec);
            //测量宽高
            int width =MeasureSpec.getSize(widthMeasureSpec);
            int height=MeasureSpec.getSize(heightMeasureSpec);
            setMeasuredDimension(width,height);
        }
    
    测量模式解析:
    • MeasureSpec.AT_MOST:父容器指定最大的空间,WRAP_CONTENT属性
    • MeasureSpec.EXACTLY:父容器指定精确的大小空间,MATCH_PARENT 或者 一个精确值
    • MeasureSpec.UNSPECIFIED:父容器未指定空间的大小,父容器是ScrollerView等可滑动控件
      宽高解析:
    • getWidth()和getHeight():View的最终大小,测量方法完成后可以得到具体的值
    • getMeasureWidth()和getMeasureHeight():View的测量大小,测量完成后可以得到具体的值
    • 宽的计算:控件的右边到屏幕的左边的距离 --- 控件的左边到屏幕左边的距离,即 right - left
    • 高的计算:控件的下边到屏幕的上边的距离 --- 控件的上边到屏幕上边的距离,即 bottom - top
    • 属性详解:top,控件顶部到屏幕顶部的距离。bottom,控件底部到屏幕顶部的距离。left,控件左边到屏幕左边的距离。right,控件右边到屏幕左边的距离。
    //当View的尺寸发生变化时会触发,如onMeasure完成后
     @Override
        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
            super.onSizeChanged(w, h, oldw, oldh);
            //初始化一些有关尺寸的成员变量
        }
    
    //自定义ViewGroup,还要测量子View的大小
    measureChild(child, widthMeasureSpec, heightMeasureSpec);
    
    • 设置控件自适应屏幕时的大小(在某些特殊情况,View的AT_MOST模式可以同时对应wrap_content和match_parent两种布局参数,所以通过测量模式来判断控件所设置的属性wrap_content和match_parent是不准确的)
            if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT && getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(mWidth, mHeight);
            } else if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(mWidth, heightSize);
            } else if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                setMeasuredDimension(widthSize, mHeight);
            }
    
    onMeasure、Measure、measureChild、measureChildren 的区别

    1、onMeasure 测量自身,自定义View时重写,定义控件的宽高,常在自定义的View中使用
    2、Measure 测量自身,方法不可重写,内部调用onMeasure方法,常在自定义的ViewGroup中使用
    3、measureChild 测量某个子View,内部调用Measure方法,常在自定义的ViewGroup中使用
    4、measureChildren 测量所有子View,内部调用measureChild方法,常在自定义的ViewGroup中使用

    创建新的测量参数

    在自定义View的开发中,我们重写测量方法,方法里的传参(widthMeasureSpec,heightMeasureSpec)都是由父类提供的,在自定义ViewGroup的开发中,我们可以根据当前布局的测量参数,为布局内的子控件创建新的测量参数,来控制子View在布局的显示大小

                int widthSize = MeasureSpec.getSize(widthMeasureSpec);
                int widthMode = MeasureSpec.getMode(widthMeasureSpec);
                int childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize,widthMode);
    

    三、布局流程

    ViewGroup中的方法,设置子View的布局参数和显示位置
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) { 
            if (changed) {
                layoutArcButton();
    
                int count = getChildCount();
                for (int i = 0; i < count - 1; i++) {
                    View child = getChildAt(i + 1);
                    child.setVisibility(View.GONE);
                    //为什么-2,因为主菜单也是其中一个子控件
                    int left = (int) (mRadius * Math.sin(Math.PI / 2 / (count - 2) * i));
                    int top = (int) (mRadius * Math.cos(Math.PI / 2 / (count - 2) * i));
    
                    int width = child.getMeasuredWidth();
                    int height = child.getMeasuredHeight();
    
                    //菜单在左下或者右下角
                    if (mPosition == Position.LEFT_BOTTOM || mPosition == Position.RIGHT_BOTTOM) {
                        top = getMeasuredHeight() - height - top;
                    }
    
                    if (mPosition == Position.RIGHT_BOTTOM || mPosition == Position.RIGHT_TOP) {
                        left = getMeasuredWidth() - width - left;
                    }
                    //布局子View
                    child.layout(left, top, left + width, top + height);
                }
            }
        }
    
    View的生命周期方法,在Activity.onCreate()中会加载布局,在布局文件完成加载后会触发该方法,常用于操纵子View,设置子View的布局参数,如果View的创建不通过布局文件加载,则该方法不会被触发,例如调用View的一个参数的构造方法
        @Override
        protected void onFinishInflate() { -> Activity onCreate 执行完触发
            super.onFinishInflate();
            //获取子控件个数
            int count = getChildCount();
            if (count == 0) return;
    
            for (int i = 0; i < count; i++) {
                View view = getChildAt(i);
                LinearLayout.LayoutParams Lp = (LayoutParams) view.getLayoutParams();
                Lp.weight = 0;
                Lp.width = getScreenWidth() / mTabVisibleCount;
                view.setLayoutParams(Lp);
            }
        }
    
    View的生命周期方法,在Activity.onResume()执行后,View被加载到窗口时触发该方法
        @Override
        protected void onAttachedToWindow() { -> Activity onResume 执行完触发
            super.onAttachedToWindow();
        }
    
    View的生命周期方法,当View从Window中被移除时触发该方法,常见于viewGroup.removeView(view)或者当前View所依赖的Activity被销毁了
        @Override
        protected void onDetachedFromWindow() { -> View 移出窗口时触发
            super.onDetachedFromWindow();
        }
    
    自定义View的布局类型(线性、相对、流式等)
        @Override
        public LayoutParams generateLayoutParams(AttributeSet attrs) {
            return new MarginLayoutParams(getContext(), attrs);
        }
    
    layout()、onLayout()、requestLayout()的区别

    1、layout:指定View新的显示位置,用法:view.layout(left,top,right,bottom);
    2、onLayout:设置View的显示位置,用法:重写该方法,定义View的显示规则
    3、requestLayout:强制View重新布局,用法:view.requestLayout();

    注意:View的生命周期方法均在Activity.onResume()方法后执行,所以在此之前,是获取不到View的属性的,比如在Activity.onCreate()中获取View的宽高,尽管View已经被创建,但得到View的宽高均为0,因为View的生命周期方法未得到执行,View还未经过测量,所有属性均是默认值
    View 生命周期方法执行的先后顺序

    onFinishInflate -> onAttachedToWindow -> onMeasure -> onSizeChanged -> onLayout -> onDraw -> onDetachedFromWindow

    四、绘制流程

    绘制方法
        @Override
        protected void onDraw(Canvas canvas) {
            canvas.drawARGB(255,255,255,255);
        }
    
    刷新界面,重新绘制
        private void invalidateView() {
            if (Looper.getMainLooper() == Looper.myLooper()) { //UI线程
                invalidate();
            } else { //非UI线程
                postInvalidate();
            }
        }
    
    ViewGroup中的方法,用于绘制子控件
        @Override
        protected void dispatchDraw(Canvas canvas) {   
            canvas.save();
            canvas.translate(mInitTranslationX + mTranslationX, getHeight() + 2);
            canvas.drawPath(mPath, mPaint);
            canvas.restore();
            super.dispatchDraw(canvas);
        }
    

    五、坐标系

    • View自身的坐标
    • getTop():获取View自身顶边到其父布局顶边的距离
    • getLeft():获取View自身左边到其父布局左边的距离
    • getRight():获取View自身右边到其父布局左边的距离
    • getBottom():获取View自身底边到其父布局顶边的距离
    • MotionEvent提供的方法
    • getX():获取点击事件距离控件左边的距离,即视图坐标
    • getY():获取点击事件距离控件顶边的距离,即视图坐标
    • getRawX():获取点击事件距离屏幕左边的距离,即绝对坐标
    • getRawY():获取点击事件距离屏幕左边的距离,即绝对坐标

    六、View的滑动

    • layout(left,top,right,bottom); View的坐标点
    • offsetLeftAndRight(offsetX) 和 offsetTopAndBottom(offsetY); View的偏移量
    • MarginLayoutParams.leftMargin ,设置View的边距改变View的位置
    • scrollTo(x,y) 和 scrollBy(x,y); 绝对位移和相对位移,瞬间完成
    • Scroller , 带有过渡动画的滑动,拥有良好的用户体验
    public class CustomView extends View {
        private Scroller mScroller;
       
        public CustomView(Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
            mScroller = new Scroller(context);
        }
        
        //系统会在绘制View的时候在draw中调用该方法
        @Override
        public void computeScroll() {
            super.computeScroll();
            if(mScroller.computeScrollOffset()){
                ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
                invalidate();
            }
        }
        
        public void smoothScrollTo(int destX,int destY){
            int scrollX =getScrollX();
            int delta = destX- scrollX;
            mScroller.startScroll(scrollX,0,delta,0,2000);
            invalidate();
        }
    }
    
    • getScrollX() 和 getScrollY() ,View的左上角的点到View的父视图左上角的点之间的X轴方向和Y轴方向的值

    七、VelocityTracker

        //速度跟踪器
        private VelocityTracker mVelocityTracker;
        //初始化滑动速度跟踪类
        mVelocityTracker = VelocityTracker.obtain();
        //计算一秒内的滑动速度
        mVelocityTracker.computeCurrentVelocity(1000);
        //获取X方向的滑动速度
        mVelocityTracker.getXVelocity();
        //获取Y方向的滑动速度
        mVelocityTracker.getYVelocity();
       
        @Override  //回收资源
        protected void onDetachedFromWindow() {
            mVelocityTracker.recycle();
            super.onDetachedFromWindow();
        }
    

    八、事件分发机制

    Android的事件分发可以理解为向下分发,向上回传,类似V字型,V字的左边是事件进行向下分发,如果途中没有进行事件的分发拦截,则事件传递到最底层的View,即是最接近屏幕的View。V字的右边是事件的回传,如果中途没有进行事件的消费,则事件传递到最顶层的View,直至消失。

        @Override //事件分发,默认false
        public boolean dispatchTouchEvent(MotionEvent ev) {
            return super.dispatchTouchEvent(ev);
        }
    
        @Override //事件拦截,默认false
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override //事件消费,默认false
        public boolean onTouchEvent(MotionEvent event) {
            return super.onTouchEvent(event);
        }
    
    • dispatchTouchEvent:Android所有的事件都经过此方法分发,返回true则表示对事件不再进行分发,事件没有被消费,返回false则继续往下分发,如果是ViewGroup,则分发给onInterceptTouchEvent决定事件继续分发还是被拦截
    • onInterceptTouchEvent:解决滑动冲突的外部拦截法,是ViewGroup中特有的方法,决定触摸事件是否拦截,返回true则表示拦截事件的分发,交给自身的onTouchEvent进行处理,返回false,则事件继续向下分发
    • onTouchEvent:Android的触摸事件消费,返回true,则表示当前View处理该事件,返回false,则事件继续进行分发,子View处理事件的优先级比父View处理事件的优先级要高
    • requestDisallowInterceptTouchEvent:解决滑动冲突的内部拦截法,作用于dispatchTouchEvent,请求不允许拦截触摸事件,有时候,子类并不希望父类拦截它的触摸事件,想将触摸事件交给自身处理,常用方式:view.getParent().requestDisallowInterceptTouchEvent(true);
    • 当前View接收到的触摸事件,可以通过调用dispatchTouchEvent方法,将触摸事件传递给其他View,从而达到不同的View处理相同的触摸事件,起到联动的效果,比如移动A的时候,想让B也同时移动,就可以把A的触摸事件传递给B,让B也处理这个触摸事件
    • 通过事件分发机制实现不能左右滑动的ViewPager
    import android.content.Context;
    import android.support.annotation.NonNull;
    import android.support.annotation.Nullable;
    import android.support.v4.view.ViewPager;
    import android.util.AttributeSet;
    import android.view.MotionEvent;
    
    public class NoScrollViewPager extends ViewPager {
    
        private boolean canScroll = false; //是否可以滑动
    
        public NoScrollViewPager(@NonNull Context context) {
            this(context,null);
        }
    
        public NoScrollViewPager(@NonNull Context context, @Nullable AttributeSet attrs) {
            super(context, attrs);
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            if (canScroll){
                return super.onInterceptTouchEvent(ev);
            }else{
                return false; -> 不拦截,让事件继续分发
            }
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent ev) {
            if (canScroll){
                return super.onTouchEvent(ev);
            }else {
                return true; -> 消费当前事件
            }
        }
    }
    

    九、View的点击事件

    View的点击事件设置只对单个View产生效果,比如设置ViewGroup的根布局不可点击,ViewGroup的子View依然能接收点击事件。而RelativeLayout等常用布局,设置根布局不可点击,所有子View都不会处理点击事件。在布局强转成ViewGroup时,部分功能会失效,比如设置该布局不可点击,需要遍历所有子View并为其设置不可点击事件
    view.setEnabled(true/false);
    

    十、获取View的宽高

    View的measure过程与Activity的生命周期不是同步执行的,在onCreat(),onStart(),onResume()中获取View的宽高为零,原因是测量还没完成
    View初始化完毕时触发
        @Override
        public void onWindowFocusChanged(boolean hasFocus) {
            super.onWindowFocusChanged(hasFocus);
            if (hasFocus){
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
            }
        }
    
    投递一个任务到消息队列的尾部
        view.post(new Runnable){
              @Override
              public void run(){
                int width = view.getMeasuredWidth();
                int height = view.getMeasuredHeight();
              }
        } 
    
    View树的状态发生改变或者View树的内部View的可见性发生改变时触发
            ViewTreeObserver observer = view.getViewTreeObserver();
            observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                   int width = view.getMeasuredWidth();
                   int height = view.getMeasuredHeight(); 
                }
            });
    

    十一、获取View在屏幕中的位置

            int position[] = new int[2]; -> 0存x , 1存y 
            view.getLocationInWindow(position);
            //view.getLocationOnScreen(position);
    
            TextView tv = new TextView(this); -> 生成与屏幕位置一样的View
            LayoutParams params = new LayoutParams(view.getLayoutParams());
            params.leftMargin = position[0];
            params.topMargin = position[1];
            view.setLayoutParams(params);
            viewGroup.addView(view);
    

    相关文章

      网友评论

          本文标题:Android 自定义View

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