本篇文章已授权微信公众号 顾林海 独家发布
Android中的坐标系
在Android中,屏幕左上角是Android坐标系的原点,向右是x轴正方向,向下是y轴正方向,通过getRawX()和getRawY()方法可以获取屏幕的坐标系,通过getX()和getY()方法可以获取手指在某个View的坐标系。
通过如下方法可以获得View到其父控件的距离:
-
getTop():获取View自身顶边到其父布局顶边的距离。
-
getLeft():获取View自身左边到其父布局左边的距离。
-
getRight():获取View自身右边到其父布局左边的距离。
-
getBottom():获取View自身底边到其父布局顶边的距离。
总结如图:
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中模式:
-
UNSPECIFIED:表示未指定模式,View想多大就多大,父容器不做限制,一般用于系统内部的测量。
-
AT_MOST:表示最大模式,对应于wrap_content属性,子View的最终大小是父View指定的SpecSize值,并且子View的大小不能大于这个值。
-
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局部过程如下:
WindowManager的addView方法传入DecorView,WindowManager的实现类是WindowManagerImpl。
WindowManagerImpl的addView相关过程如下:
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方法,这是一个空方法,需要子类实现。