Android 自定义View

2018-01-13  本文已影响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();
参数解析:
    //自定义属性文件,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);
    }
测量模式解析:
//当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);
        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的滑动

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();
    }
}

七、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);
    }
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);
上一篇下一篇

猜你喜欢

热点阅读