View和动画

View是所有控件的基类,布局控件继承自ViewGroup,ViewGroup是所有View和ViewGroup的集合。ViewGroup也是继承自View。

Android坐标系:以屏幕左上方为原点,触控事件getRawX(),getRawY()得到的就是Android坐标。

View坐标系 :和Android坐标系共同存在,如图:
坐标

根据图,获取自身高度可以通过getBottom()-getTop()。也可以通过getHeight()直接得到View的高度。

MotionEvent提供的方法:
getX():获取到控件左边的距离,是视图坐标。
getY():获取到控件上方的距离,是视图坐标。
getRawX():获取到屏幕左边的距,是绝对坐标。
getRawY():获取到屏幕上方的距离,是绝对坐标。

View的滑动

layout()方法

view会调用onLayout()方法来显示位置。

    public boolean onTouchEvent(MotionEvent event) {
        int x = (int) event.getX();
        int y = (int) event.getY();
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                lastX = x;
                lastY = y;
                break;
            case MotionEvent.ACTION_MOVE:
                int offsetX = x - lastX;
                int offsetY = y - lastY;
                layout(getLeft()+offsetX,getTop()+offsetY,
                        getRight()+offsetX,getBottom()+offsetY);
                break;
        }
        return true;
    }

首先自定义View继承View,然后重写onTouchEvent方法,根据MotionEvent计算出偏移量,然后使用layout重新绘制,这样就能实现View的滑动了,注意,layout方法中的参数位置必须是左上右下。

offsetLeftAndRight和offsetTopAndBottom

方法和layout差不多,修改如下:

    case MotionEvent.ACTION_MOVE:
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        offsetLeftAndRight(offsetX);
        offsetTopAndBottom(offsetY);
        break;
改变layoutParam

通过修改布局参数,修改如下:

    case MotionEvent.ACTION_MOVE:
        int offsetX = x - lastX;
        int offsetY = y - lastY;
        LinearLayout.LayoutParams layoutParams = (LinearLayout.LayoutParams)
                getLayoutParams();
        layoutParams.leftMargin = getLeft()+offsetX;
        layoutParams.topMargin = getTop() + offsetY;
        setLayoutParams(layoutParams);
        break;
使用ScrollBy和ScrollTo

ScrollBy和ScrollTo是View的方法,ScrollBy是根据ScrollTo实现的,注意这里移动的是view的位置,因此偏移量应该是负值。

使用Scroller
    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }
    public void smoothScrollerTo (int destX,int destY){
        int scrollX = getScrollX();
        int delta = destX - scrollX;
        mScroller.startScroll(scrollX,0,delta,0,2000);
        invalidate();
    }

使用computeScroll计算位置,用invalidate启动View的重新绘制。用startScroll方法控制平滑移动。

动画

ObjectAnimator

可以通过静态工厂来返还一个ObjectAnimator对象,参数必须包括一个对象和对象的属性名字,这个属性必须有set和get方法,内部通过反射机制修改对象的属性名。

    ObjectAnimator mObjectAnimator = ObjectAnimator.ofFloat(mCustomView,
            "translationX",200);
    mObjectAnimator.setDuration(300);
    mObjectAnimator.start();

因此我们可以通过包装类的方法来给View的属性加上get和set方法。

    private static class MyView{
       private View mTarget;
       private MyView(View mTarget){
           this.mTarget = mTarget;
       }
       public int getWidth(){
           return mTarget.getLayoutParams().width;
       }
       public int setWidth(int width){
           return mTarget.getLayoutParams().width;
       }
    }

ValueAnimator不提供任何动画效果,它像一个数值发生器,用来产生一定规律的数字,从而让调用者控制动画的实现过程。完整的动画具有start,repeat,end,cancel4个过程。大部分时候我们只关心onAnimationEnd的事件,Android也提供了AnimatorListenerAdapter来让我们选择的必要事件进行监听。AnimatorSet提供了一个play方法,如果在这个方法里面传入一个Animator对象,将会返回一个Animator.Builder的实例,Builder类采用建造者模式,每次调用方法都会返回自身用于继续构建,包括以下4个方法:

  • after(Animator anim):将现有的动画插入到传入的动画后进行。
  • after(long delay):将现有动画延迟指定毫秒后执行。
  • before(Animator anim):将现有的动画插入到传入的动画前进行。
  • with(Animator animator):将现有动画和传入的动画同时执行。

如下,创建3个动画,让动画3先执行,然后执行动画1和动画2.

    ObjectAnimator animator1 = ObjectAnimator.ofFloat(mCustomView,"translationX",0.0f,200.0f,0f);
    ObjectAnimator animator2 = ObjectAnimator.ofFloat(mCustomView,"scaleX",1.0f,2.0f);
    ObjectAnimator animator3 = ObjectAnimator.ofFloat(mCustomView,"rotationX",0.0f,90.0f,0.0f);
    AnimatorSet set = new AnimatorSet();
    set.setDuration(1000);
    set.play(animator1).with(animator2).after(animator3);
    set.start();

或者使用PropertyValuesHolder也可以实现组合动画,但是动画只能一起执行,如下:

        PropertyValuesHolder valuesHolder1 = PropertyValuesHolder.ofFloat("scaleX",
                1.0f,1.5f);  
        PropertyValuesHolder valuesHolder2 = PropertyValuesHolder.ofFloat("scaleX",
                1.0f,1.5f);
        PropertyValuesHolder valuesHolder3 = PropertyValuesHolder.ofFloat("scaleX",
                1.0f,1.5f);
        ObjectAnimator objectAnimator = ObjectAnimator.ofPropertyValuesHolder(mCustomView,
                valuesHolder1,valuesHolder2,valuesHolder3);
        objectAnimator.setDuration(2000).start();

也可以使用xml来定义动画,然后用Animator的loadAnimator方法引用动画。

Scroller

Scroller有三个构造方法,通常情况下我们使用第一个构造方法,第二个构造方法传入一个插值器Interpolator。
在startScroll()方法中,并没有调用开始滑动的方法,而是保存了传入的各种参数,startX和startY表示滑动开始的起点,dx和dy表示滑动的距离。在startScroll()之后我们调用了invalidate方法进行了view的重绘,重绘调用了View的draw方法,而draw方法又会调用View的computeScroll方法,如下:

    public void computeScroll() {
        super.computeScroll();
        if (mScroller.computeScrollOffset()){
            ((View)getParent()).scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
            invalidate();
        }
    }

这样就实现了移动,在scrollTo之前,调用了方法computeScrollOffset,首先会计算动画的持续时间timePassed,如果动画的持续时间小于mDuration,那么执行Switch语句,因为之前在startScroll方法中mMode的值为SCROLL_MODE,所以执行该分支语句,然后根据插值器来计算该段时间移动的距离,赋值给mCurrX和mCurrY,这样就能获得当前的ScrollX和ScrollY了。另外computeScrollOffet返回值为true那么表示滑动没有结束,那么会一直移动,返回false表示滑动结束。原理就是根据computeScrollOffset计算当前位置,然后用scrollTo进行重绘,不断计算,不断重绘就形成了动画。

View中的事件分发机制。

Activity的构成
点击事件用MotionEvent表示,当一个点击事件产生后,事件最先传递给Activity,当我们写Activity的时候会调用setContentView方法来加载布局,调用了getWindow方法,getWindow指的是PhoneWindow,然后PhoneWindow的setContentView方法中调用了installDecor,installDecor调用了generateDecor,generateDecor创建了一个DecorView,这个DecorView就是Activity中的根View。然后看PhoneView中的generateLayout方法,主要就是根据不同的情况给不同的布局loyoutResource。其中有布局R.layout.screen_title,布局中ViewStub标签是用来显示ActionBar的,下面两个FrameLauput,一个是title,用来显示标题,另一个是content,用来显示内容。如图:

当我们点击屏幕时,就产生了点击事件,这个事件被封装成了一个类:MotionEvent传递给View层级,在View层级中的传递过程就是事件分发。。然后系统就会将这个MotionEvent传递给View层级,在View层级中的传递过程就是事件分发。控制事件分发的三个重要方法:

  • dispatchTouchEvent(MotionEvent ev) - 用来进行事件的分发。
  • onInterceptTouchEvent(MotionEvent ev) - 用来进行事件的拦截,在dispatchTouchEvent中调用。
  • onTouchEvent(MotionEvent ev) - 用来处理点击事件,在dispacherTouchEvent方法中调用。

view事件的分发机制:
事件交给Activity,具体的工作交给PhoneWindow,PhoneWindow把工作交给DecorView,DecorView再把工作交给ViewGroup,从ViewGroup的dispacherTouchEvent开始分析。首先判断事件是否是DOWN事件,如果是,那么进行初始化,resetTouchState会把mFirstTouchTarget的值置为null。因为每个事件都是DOWN开始,以UP结束的,所以如果是DOWN事件表示一个新事件开始了,需要进行重置。然后进行判断,如果当前拦截了事件,那么mFirtTouchTarget != null则为false,那么这时候如果是down事件,那么会触发onInterceptTouchEvent事件,然后出现了一个标志位,主要是为了防止拦截down以外的时间。那么就是ViewGroup需要拦截事件的时候,后序的事件都会交给他来处理,而不再调用onInterceptTouchEvent了。

onIntercepter

首先遍历ViewGroup中的子元素,判断子元素能否接受到点击事件,这个循环是从外层向里层遍历的,判断触摸点位置是否在子View的范围内或者View是否在播放动画,然后执行dispacherTransformedTouchEvent,如果有子View,那么调用子View的dispacherTouchEvent方法,如果OnTouchListener不为null并且onTouch方法返回true,表示事件被消费,就不会执行onTouchEvent,在onTouchEvent中,只要View的CLICKABLE和LONG_CLICKABLE中有一个为true,那么就会返回true消耗这个事件。

分发事件总结

当发生点击事件的时候,点击事件自上向下传递,如果没有被拦截,那么会传递到底层容器。同样的底层容器没有消耗事件并处理,那么就会传递给父view的onTouchEvenet处理,如果onTouchEvenet发生错误,那么继续向上传递。

View的工作流程

主要是指measure,layout和draw,其中measure用来测量view的宽高,layout用来确定View的位置,draw用来绘制View。

View的工作流程

当我们调用Activity的startAcitivty方法的时候,最终调用的是Activity的handlerLaunchActivity方法来创建Activity的,handlerLaunchActivity里调用了performLaunchAcitivty,这里会调用到Acitivity的onCreate方法,从而完成DecorView的创建,接着调用handleResumeActivity方法,然后在handleResumeActivity中的performResumeActivity方法中调用了Acitivity的onResume方法,然后相继得到了DecorView,WindowManager,WindowManager是一个接口并继承了接口ViewManager,之后调用了WindowManager的addView方法,实际上最终调用了WindowManagerGlobal的addView方法。然后创建了ViewRootImpl实例,然后调用了ViewRootImpl的setView方法并将DecorView作为参数传进去,这样就就把DecorView加载到了Window中。

ViewRootImpl的PerformTraveals方法

PerformTraversal使得ViewTree开始View的工作流程。主要执行了三个方法,分别是performMeasure,performLayout,
performDraw,内部调用了View的对应方法。其中performMeasure需要传入两个参数,分别是childWidthMeasureSpec和childWidthMeasureSpec。

MeasureSpec

MeasureSpec是View的内部类,主要是根据父容器的限制来确定MeasureSpec,然后在onMeasure中根据MeasureSpec来确定View的宽和高。那么顶层的DecorView的MeasureSpec是如何得到的呢?在PerformTraveals中调用了getRootMeasureSpec方法,根据自身的LayoutParams和窗口尺寸来确定MeasureSpec

View的measure流程

View的measure用来测量View的宽和高,ViewGroup的measure除了要完成自己的测量,还要遍历调用子元素的measure方法。SpecMode是View的测量模式,而SpecSize是View的测量大熊啊,View在AT_MOST和EXACTLY的模式下,都返回SpecSize的值,在UNSPECIFIED模式下,返回的是getDefaultSize方法的第一个参数Size的值,size是根据getSuggestedMinimumWidth或者getSuggestedMinimumWidth方法得到的,getSuggestedMinimumWidth会判断背景和设置的mMinWidth的大小,如果没有背景就返回mMinWidth,否则返回两者真的较大值。

ViewGroup的measure流程

不仅要测量自身,还要递归调用子元素的measure方法,在getChildMeasurSpec中根据父容器的MeasureSpec模式结合子元素的LayoutParams属性来得到子元素的MeasureSpec属性。比如在measureVertical定义了mTotalLength用来存储LinearLayout在垂直方向的高度,然后遍历子元素,根据子元素的MeasureSpec模式分别计算每个子元素的高度,最后加上padding值。

View的layout流程

layout方法用来确定子元素的位置,layout方法的4个参数l,t,r,b分别是View从左,上,右,下相对于其父容器的距离。setFrame方法用传进来的l,t,r,b4个参数fen’bie初始化mLeft,mTop,mRight,mBottom这4个值,这样就确定了View在父容器的位置。View和View Group中都没有实现onLayout方法,LinearLayout的onLayout方法,根据方向来调用不同的方法。比如layoutVertical方法,会遍历子元素,并调用setChildFrame方法,其中childTop的值是不断累加的,这样能够保证子元素不会重叠。

View的draw流程

(1) 如果需要,绘制背景。
(2) 保存当前的canvas层。
(3) 绘制View的内容。
(4) 绘制子View。
(5) 如果需要,绘制View的褪色边缘。
(6) 绘制装饰,比如滚动条。

绘制背景是调用了View的drawBackgroud方法,其中考虑了偏移参数scrollX和scrollY。绘制View的内容时,调用了View的onDraw方法,在自定义View中重写该方法来实现。绘制子View调用了dispatchDraw方法,也是一个空实现,在dispatch方法中对子类View进行遍历,并调用drawChild方法,主要调用了View的draw方法,判断是否有缓存,如果没有缓存那么正常绘制,如果有,那么利用缓存显示。绘制装饰的方法为View的onDrawForegroud方法,用于绘制ScrollBar以及其他装饰,并将它们绘制在视图内容的上层。

自定义View

继承系统控件的自定义View

一般是为了添加新的功能或者修改显示的效果,一般情况下载onDraw方法中处理。

继承View的自定义View

不只要实现onDraw方法,还要考虑到wrap_content和padding属性的设置,onTouchEvent方法等。

自定义属性

首先在values目录下创建attrs.xml,然后定义属性如下:

<resources>
    <declare-styleable name="RectView">
        <attr name="rect_color" format="color"/>
    </declare-styleable>
</resources>

接下来需要在RectView中的构造方法解析自定义属性的值

    public RectView(Context context,  AttributeSet attrs) {
        super(context, attrs);
        TypedArray mTypedArray = context.obtainStyledAttributes(attrs,R.styleable.RectView);
        mColor = mTypedArray.getColor(R.styleable.RectView_rect_color,Color.RED);
        mTypedArray.recycle();
        initDraw();
    }

然后就可用app:rect_color的方式来获取自定义的属性值了。

自定义组合控件

就是多个控件组合起来成为一个新的控件。首先需要定义组合控件的布局。然后组合控件继承布局,在布局中做一些初始化的工作。使用自定义属性,在布局的构造方法中解析自定义属性的值。然后使用组合控件的布局。然后在活动中调用自定义的组合控件,设置点击事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值