Android小知识-自定义View相关知识(1)

989 阅读6分钟

本篇文章已授权微信公众号 顾林海 独家发布

Android中的坐标系

在Android中,屏幕左上角是Android坐标系的原点,向右是x轴正方向,向下是y轴正方向,通过getRawX()和getRawY()方法可以获取屏幕的坐标系,通过getX()和getY()方法可以获取手指在某个View的坐标系。

通过如下方法可以获得View到其父控件的距离:

  • getTop():获取View自身顶边到其父布局顶边的距离。

  • getLeft():获取View自身左边到其父布局左边的距离。

  • getRight():获取View自身右边到其父布局左边的距离。

  • getBottom():获取View自身底边到其父布局顶边的距离。

总结如图:

image

Scroller

scrollTo(x,y)表示移动到一个具体的坐标点,而scrollBy(dx,dy)表示移动的增量为dx、dy,scrollBy最终还是调用scrollTo方法。

使用scrollTo/scrollBy方法进行滑动,整个滑动效果是瞬间完成的,可以使用Scroller来实现过渡效果的滑动。Scroller本身不能实现View的滑动,需要与View的computeScroll方法配合使用。

private Scroller mScroller;
private Context mContext;

private void init(){
    mScroller=new Scroller(mContext);
}

@Override
public void computeScroll() {
    super.computeScroll();
    if(mScroller.computeScrollOffset()){
        //内容在移动
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        //重绘
        invalidate();
    }
}

public void smoothScrollTo(int destX,int desY){
    int scrollX=getScrollX();
    int scrollY=getScrollY();
    int deltaX=destX-scrollX;
    int deltaY=desY-scrollY;
    mScroller.startScroll(scrollX,scrollY,deltaX,deltaY,2000);
    invalidate();
}

通过调用invalidate()方法不断地进行重绘,重绘就会调用computeScroll()方法,就这样通过不断的移动来实现滑动效果。

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
}

Scroller提供了三个构造方法,平时使用最多的就是第一个,第二个传入一个差值器Interpolator,默认使用ViscousFluidInterpolator。

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方法中并没有执行滑动代码,而是保存了各种参数,startX和startY表示滑动开始的起点,dx和dy表示滑动的距离,duration表示滑动持续的时间。这个startScroll方法为进行滑动做准备,在startScroll方法后,调用invalidate()方法进行重绘,重绘调用draw()方法,而draw()方法又会调用View的computeScroll()方法,重写computeScroll()方法。

@Override
public void computeScroll() {
super.computeScroll();
    if(mScroller.computeScrollOffset()){
        //内容在移动
        scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
        //重绘
        invalidate();
    }
}

在computeScroll()方法中通过Scroller获取当前的ScrollX和ScrollY,然后调用scrollTo()方法进行View的滑动,接着调用invalidate()方法进行重绘,重绘又会调用draw()方法,draw()方法调用computeScroll()方法,就这样不停的重绘不停的执行scrollTo方法,当调用Scroller对象的computeScrollOffset()方法,该方法返回false时滑动停止。

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

一开始计算动画的持续时间timePassed,如果动画持续时间小于我们设置的滑动时间mDuration,执行switch语句,在上述startScroll方法中mMode被设置为SCROLL_MODE,所以执行分支语句SCROLL_MODE,根据差值器来计算出在该时间段内移动的距离,赋值给mCurrX和mCurrY。

getCurrX和getCurrY方法:

public final int getCurrX() {
    return mCurrX;
}


public final int getCurrY() {
    return mCurrY;
}

这两个方法就拿到了computeScrollOffset方法中计算出来的某个时间段内应该移动的距离。

MeasureSpec

MeasureSpec是View的内部类,系统将View的LayoutParams根据父容器所施加的规则转换成对应的MeasureSpec,然后在onMeasure方法中根据这个MeasureSpec来确定View的宽和高。

MeasureSpec代表32位的int值,高2位代表SpecMode,低30位代表SpecSize。SpecMode是指测量模式,SpecSize是指测量大小。

SpecMode提供3中模式:

  1. UNSPECIFIED:表示未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。

  2. AT_MOST:表示最大模式,对应于wrap_content属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。

  3. EXACTLY:表示精确模式,对应于match_parent属性和具体的数值,父容器测量出View所需要的大小,也就是SpecSize值。

View测量过程中,通过makeMeasureSpec来保存宽高,通过getMode获取指定模式,通过getSize获取宽和高。MeasureSpec是受自身LayoutParams和父容器的MeasureSpec共同影响的。

View的工作流程

View的工作流程指的是measure、layout和draw。measure用来测量View的宽高,layout用来确定View的位置,draw用来绘制View。

Activity构建过程中,创建DecoreView后,它的内容还无法显示,因为它还没有被加载到Window中。

当调用Activity的startActivity方法时,最终调用ActivityThread的handleLaunchActivity方法来创建Activity。handleLaunchActivity方法中先通过performLaunchActivity方法来创建Activity,再执行handleResumeActivity方法。

Activity的startActivity局部过程如下:

image

WindowManager的addView方法传入DecorView,WindowManager的实现类是WindowManagerImpl。

WindowManagerImpl的addView相关过程如下:

image

ViewRootImpl是View的根View,控制View的测量和绘制,同时持有WindowSession通过Binder与WMS通信,最终将DecorView加载到Window中。

开始View的工作流程是在ViewRootImpl的performTraversals()方法中,performTraversals方法中重要的三个方法是:performMeasure、performLayout和performDraw,分别对应测量、布局和绘制。

View进行测量时,根据SpecMode来返回不同的值,在AT_MOST和EXACTLY模式下,都返回SpecSize这个值,也就是说它的wrap_content和match_parent属性的效果都一样,因此在自定义View时需要重写onMeasure方法,对wrap_content属性进行处理。对于ViewGroup来说,它会遍历子元素的measure方法,根据父容器的MeasureSpec模式再结合子元素的LayoutParams属性来得出子元素的MeasureSpec属性。

View进行布局时,通过layout方法确定自身的位置,在layout方法中调用setFrame方法确定mLeft、mTop、mRight、mBottom这4个值,通过这4个值就可以确定自身在父容器中的位置,在调用setFrame方法后,调用onLayout方法,这是一个空方法,由它们的子类来确定;对于ViewGroup来说,遍历layout方法用来确定子元素的位置,onLayout也是一个空方法,交由它的子类实现。

最后进行绘制时,会按照一定步骤来进行绘制:绘制背景、保存当前canvas层、绘制View的内容、绘制子View、绘制子View的边缘、绘制装饰。在第三步绘制View的内容时,调用onDraw方法,这是一个空方法,需要子类实现。


838794-506ddad529df4cd4.webp.jpg