View的体系和自定义View的流程(一)

前言: 最近学习了View的体系与自定义View,并且看了android进阶之光这部书,记录一下学习心得

一、View与ViewGroup


其实,平时我们开发用的各种控件(TextView,Button)和布局(LinearLayout,RelativeLayout)都是基于View写成的,都是View的子类,所以View是所以控件的父类。

ViewGroup也是继承View,并且ViewGroup可以理解为多个View的组合,而ViewGroup又可以包含View和ViewGroup,所以它们的关系图如下:

View和ViewGroup关系图

下面是View的继承关系图:
继承关系

二、坐标系


2.1 Android坐标系

android手机的坐标系是不同于我们平时学习的空间直角坐标系,所以还没学习到这里之前,我是非常懵的,怎么每次计算布局和滑动的代码我都看不懂它们的计算过程,所以如果你连view的位置都不知道,那根本是很难操作的。

android手机的坐标系是以左上角的顶点为坐标系的原点,原点向右是X轴正方向,向下是Y轴正方向
android坐标系

2.2 View坐标系

View获取自身的宽和高
系统提供了:getHeight()getWidth()
View源码中getHeight()和getWidth()方法:

public final int getHeight(){
  return mButtom - mTop;
  }

public final int getWidth(){
  return mRight - mLeft;
  }

View自身的坐标

下列方法可以获取View到ViewGroup的距离:

  • getTop(): 获取View自身顶部到父布局顶部的距离
  • getLeft(): 获取View自身左边到父布局左边的距离
  • getRight(): 获取View自身右边到其父布局右边的距离
  • getBottom(): 获取View自身底部到其父布局顶部的距离

如图:
View

MotionEvent提供的方法:

  • getX(): 获取点击事件距离控件左边的距离(就是点击处到View左边边缘距离)
  • getY(): 获取点击事件距离控件顶部的距离(就是点击处到View顶部边缘距离)
  • getRawX(): 获取点击事件距离屏幕左边边缘的距离(绝对坐标)
  • getRawY(): 获取点击事件距离屏幕顶部边缘的距离(绝对坐标)

三、View的滑动


3.1 layout()方法

View进行绘制的时候会调用onLayout的方法来设置显示的位置,所以我们也可以通过改变View的left、top、right、bottom这4种属性来控制View的坐标,所以调用layout方法来控制View的位置

layout的源码我们来了解一下:
(这里只放关键的部分)

 public void layout(int l, int t, int r, int b) {
          if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
          //利用onMeasure进行测量View的长宽
            onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }

   int oldL = mLeft;
        int oldT = mTop;
        int oldB = mBottom;
        int oldR = mRight;
        //这里判断View的位置是否发生改变,改变了就调用setFrame()方法,具体的我们后面再说
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            }
            ......
}

onLayout的方法是一个空方法,需要我们自己在子类去实现逻辑:

  protected void onLayout(boolean changed, int left, int top, int right, int bottom) {}


在onTouchEvent()方法中调用可以实现拖动View:

public boolean onTouchEvent(MotionEvent event){
//获取获取点击事件距离控件左边的距离x
int x = (int) event.getX();
//获取点击事件距离控件顶部的距离y
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方法来重新放置它的位置
layout(getLeft()+offsetX , getTop()+offsetY , getRight()+offsetX , getBottom()+offsetY);
break;
}
return true;
}

3.2 offsetLeftAndRight()与offsetTopAndBottom()

效果和layout()方法差不多,代码可以写成:

case MotionEvent.ACTION_MOVE:
//计算移动的距离
int offsetX = x - lastX;
int offsetY = y - lastY;
//对left和right进行偏移
offsetLeftAndRight(offsetX);
//对top和bottom进行偏移
offsetTopAndBottom(offsetY);
break;

3.3 LayoutParams(改变布局参数)

LayoutParams保存了一个View的布局参数,因此我们可以通过LayoutParams来改变View的布局参数从而达到改变View位置的效果,代码可以写成:

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

如果父控件是RelativeLayout,则要使用RelativeLayout.LayoutParams
除了使用布局的LayoutParams外,还可以使用ViewGroup.MarginLayoutParams , 具体代码差不多,就不写出来了。

3.4 动画

补间动画: alpha(渐变)、translate(位移)、scale(缩放)、rotate(旋转)
XML实现(在res/anim/文件夹下定义动画实现方式)

set标签—定义动作合集(属性:从Animation类继承)
set标签可以将几个不同的动作定义成一个组
例如:

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="2000"
    android:fillAfter="true">
    <alpha
        android:fromAlpha="0.0"
        android:toAlpha="1.0">
    </alpha>
    <rotate
        android:fromDegrees="300"
        android:toDegrees="-360"
        android:pivotX="10%"
        android:pivotY="100%">
    </rotate>
    <scale
        android:fromXScale="0.0"
        android:fromYScale="1.0"
        android:toXScale="1.0"
        android:toYScale="1.5">
    </scale>
    <translate
        android:fromXDelta="320"
        android:fromYDelta="480"
        android:toXDelta="0"
        android:toYDelta="0">
    </translate>
</set>

在java代码调用:

mView.setAnimation(AnimationUtils.loadAnimation(this,R.anim.set_anim));

或者:

Animation animation = AnimationUtils.loadAnimation(this,R.anim.set_anim);
mView.startAnimation(animation);

View补间动画并不能改变View的位置参数,例如我们如果对一个Button进行上述的动画操作,当动画结束停留在完成后的位置时,我们点击这个Button并不会触发点击事件,但是我们点击这个Button的原始位置时却触发了点击事件。所以,我们可以知道系统并没有改变Button原来的位置,所以我们点击其他的地方当然不会触发事件。

针对这个问题android随后提供了属性动画,解决了这个问题。

3.5 ScrollTo 与 ScrollBy

scrollTo(x,y)表示移动到一个具体的坐标点,scrollBy(dx,dy)表示移动的增量为dx,dy,其中,scrollBy最终也是要调用scrollTo的。
View中的源码如下:

public void scrollTo(int x, int y) {
        if (mScrollX != x || mScrollY != y) {
            int oldX = mScrollX;
            int oldY = mScrollY;
            mScrollX = x;
            mScrollY = y;
            invalidateParentCaches();
            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
            if (!awakenScrollBars()) {
                postInvalidateOnAnimation();
            }
        }
    }
 public void scrollBy(int x, int y) {
        scrollTo(mScrollX + x, mScrollY + y);
    }

之前ACTION_MOVE:中的代码替换成如下代码:

((View)getParent()).scrollBy(-offsetX,-offsetY);

3.6 Scroller

scroller可以实现有过渡效果的滑动,不过scroller本身是不能实现View的滑动的,它需要与View的computeScroll()方法配合才能实现弹性滑动的效果。

public XXXView(Context context,AttributeSet attrs){
  super(context,attrs);
  mScroller = new Scroller(context);
}

接下来重写computeScroll()方法,每移动一小段就调用invalidate()方法不断进行重绘,重绘调用computeScroll()方法,这样我们通过不断移动一个小的距离并连贯起来就实现了平滑移动的效果。

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

我们在XXXView中写一个smoothScrollTo方法,调用Scroller的startScroll()方法。

 public void smoothScrollTo(int destX, int destY) {
        mScroller.startScroll(getScrollX(), 0, destX - getScrollX(),0,1000);
        invalidate();
    }


四、属性动画


4.1 ObjectAnimator

public static ObjectAnimator ofFloat(Object target,String propertyName,float...values){
ObjectAnimator anim = new ObjectAnimator(target,propertyName);
anim.setFloatValues(values);
return anim;
}

从源码可以看出参数

  • Object target-要操作的Object类
  • String propertyName-要操作的属性
  • float…values-可变的float类数组

在使用ObjectAnimator的时候,要操作的属性必须要有get和set方法,不然ObjectAnimator就会无法生效。

4.2 动画的监听

animator.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {

            }

            @Override
            public void onAnimationEnd(Animator animation) {

            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        });

Android也提供了AnimatorListenerAdapter来让我们选择必要的事件进行监听

 animator.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
            }
        });

4.3 组合动画——AnimatorSet

AnimatorSet animatorSet = new AnimatorSet();
animatorSet.play(animator1).with(animator2).after(animator3);
animatorSet.start();

在play()方法中,创建了一个AnimatorSet.Builder类,Builder类采用了建造者模式(虽然现在不是很懂什么意思),每次调用方法都返回Builder自身用于继续构建。

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


上面基本是书上的描述,所以可能会有点懵,其实说的就是可以利用上面的with,after,before方法来控制动画的顺序,比如我想让View视图先平移在缩放最后旋转,那么我们边可以play(缩放).before(平移).after(旋转)。

我们只需利用ObjectAnimator来传人with,after,before,play的Animator anim参数。

五、解析Scroller


先按书上的步骤看看Scroller的构造方法:

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

public Scroller(Context context, Interpolator interpolator) {
        this(context, interpolator,
           context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
    }

public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
        mFinished = true;
        if (interpolator == null) {
            mInterpolator = new ViscousFluidInterpolator();
        } else {
            mInterpolator = interpolator;
        }
        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
        mFlywheel = flywheel;

        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
    }

一般我们都用第一个;第二个要传人进去一个插值器Interpolator。如果不传也有默认的插值器ViscousFluidInterpolator。

接着看看Scroller的startScroll()方法:

public void startScroll(int startX, int startY, int dx, int dy, int duration) {
        mMode = SCROLL_MODE;
        mFinished = false;
        mDuration = duration;
        mStartTime = AnimationUtils.currentAnimationTimeMillis();
        mStartX = startX;
        mStartY = startY;
        mFinalX = startX + dx;
        mFinalY = startY + dy;
        mDeltaX = dx;
        mDeltaY = dy;
        mDurationReciprocal = 1.0f / (float) mDuration;
    }

在startScroll()方法中并没有调用类似开启滑动的方法,而是保存了传进来的各种参数。调用invalidate()方法,这个方法会导致View的重绘,而View的重绘会调用View的draw()方法,draw()方法又会调用View的computeScroll()方法。

重写computeScroll()方法:

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

这里判断调用computeScrollOffset()方法,看源代码:

public boolean computeScrollOffset() {
        if (mFinished) {
            return false;
        }

        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);

        if (timePassed < mDuration) {
            switch (mMode) {
            case SCROLL_MODE:
                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
                mCurrX = mStartX + Math.round(x * mDeltaX);
                mCurrY = mStartY + Math.round(x * mDeltaY);
                break;
            case FLING_MODE:
                final float t = (float) timePassed / mDuration;
                final int index = (int) (NB_SAMPLES * t);
                float distanceCoef = 1.f;
                float velocityCoef = 0.f;
                if (index < NB_SAMPLES) {
                    final float t_inf = (float) index / NB_SAMPLES;
                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
                    final float d_inf = SPLINE_POSITION[index];
                    final float d_sup = SPLINE_POSITION[index + 1];
                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
                }

                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;

                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
                // Pin to mMinX <= mCurrX <= mMaxX
                mCurrX = Math.min(mCurrX, mMaxX);
                mCurrX = Math.max(mCurrX, mMinX);

                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
                // Pin to mMinY <= mCurrY <= mMaxY
                mCurrY = Math.min(mCurrY, mMaxY);
                mCurrY = Math.max(mCurrY, mMinY);

                if (mCurrX == mFinalX && mCurrY == mFinalY) {
                    mFinished = true;
                }

                break;
            }
        }
        else {
            mCurrX = mFinalX;
            mCurrY = mFinalY;
            mFinished = true;
        }
        return true;
    }

computeScrollOffset()返回true则表示滑动未结束,为false则表示滑动结束。所以如果滑动未结束则持续调用scrollTo()方法和invalidate()方法来进行View的滑动。

所以原理总结出来就是:Scroller并不能直接实现View的滑动,它需要配合View的computeScroll()方法。在computeScroll()方法中不断让View进行重绘,每次都计算滑动持续时间,根据时间算出这次View滑动的位置,不断重复实现弹性滑动。

文章太长,接下来的6.View的事件分发机制7.View的工作流程 留在下篇再写

猜你喜欢

转载自blog.csdn.net/weixin_38364803/article/details/80042660