launcher最重要部分是几个屏幕,其中涉及到一个Workspace布局。Workspace的主要功能是完成多个屏幕及壁纸的显示,同时完成屏幕之间的切换及壁纸添加。
1、初始化
/**
* Used to inflate the Workspace from XML.
*
* @param context The application's context.
* @param attrs The attribtues set containing the Workspace's customization values.
* @param defStyle Unused.
*/
public Workspace(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mWallpaperManager = WallpaperManager.getInstance(context);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0);
mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 1);
a.recycle();
setHapticFeedbackEnabled(false);
initWorkspace();
}
/**
* Initializes various states for this workspace.
*/
private void initWorkspace() {
Context context = getContext();
mScrollInterpolator = new WorkspaceOvershootInterpolator();
mScroller = new Scroller(context, mScrollInterpolator);
mCurrentScreen = mDefaultScreen;
Launcher.setScreen(mCurrentScreen);
LauncherApplication app = (LauncherApplication)context.getApplicationContext();
mIconCache = app.getIconCache();
final ViewConfiguration configuration = ViewConfiguration.get(getContext());
mTouchSlop = configuration.getScaledTouchSlop();
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
}
这里需要注意的是,默认屏幕是在配置文件中配置的,另外WorkspaceOvershootInterpolator是一个变化速率,其具体知识参看《
android基础知识35:Interpolator》。
2、子view大小设置及排列
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int width = MeasureSpec.getSize(widthMeasureSpec);
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
}
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
}
// The children are given the same width and height as the workspace
final int count = getChildCount();
for (int i = 0; i < count; i++) {
getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
}
if (mFirstLayout) {
setHorizontalScrollBarEnabled(false);
scrollTo(mCurrentScreen * width, 0);
setHorizontalScrollBarEnabled(true);
updateWallpaperOffset(width * (getChildCount() - 1));
mFirstLayout = false;
}
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
int childLeft = 0;
final int count = getChildCount();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != View.GONE) {
final int childWidth = child.getMeasuredWidth();
child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
childLeft += childWidth;
}
}
}
这里需要注意的是,在第一次调用onMeasure时,需要切换到当前屏幕(实际上是默认屏幕);
让几个CellLayout平铺,由于每个CellLayout的大小是手机的一屏幕大小,所以,这里让其横向平铺就很简单了,直接在onLayout中调用每个CellLayout的layout方法进行布局,按顺序布局的时候,只需要控制好每个CellLayout的left和right就可以了。
3、处理多屏幕滑动
处理屏幕滑动时,涉及到android的事件处理过程。这部分可以看《 android基础知识03——事件处理02:事件流顺序》。
如何处理屏幕的滑动,屏幕滑动,莫非就需要在ACTION_MOVE事件中处理。我们在文章的开头介绍了Android的事件拦截机制,那么我们想,要让滑动事件让Workspace处理,而不会干扰到CellLayout,自然要在onInterceptTouchEvent中做一些处理了。那我们先从onInterceptTouchEvent方法入手,在onInterceptTouchEvent方法中显眼的位置,我们就可以一眼发现如下代码:
if(action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_STOPED){
return true;
}
同时在ruturn的时候,其返回的是:mTouchState != TOUCH_STATE_STOPED;这个就是说,如果当前正在滑动,则返回true,交给onTouchEvent事件来处理滑动逻辑。
那么,我们就再来看看onTouchEvent中对ACTION_MOVE事件的处理:
case MotionEvent.ACTION_DOWN:
/*
* If being flinged and user touches, stop the fling. isFinished
* will be false if being flinged.
*/
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
// Remember where the motion event started
mLastMotionX = ev.getX();
mActivePointerId = ev.getPointerId(0);
if (mTouchState == TOUCH_STATE_SCROLLING) {
enableChildrenCache(mCurrentScreen - 1, mCurrentScreen + 1);
}
break;
case MotionEvent.ACTION_MOVE:
/**
* 这里是处理滑动的地方
* 注意,手指向右滑动的时候,屏幕是向左滑动的
*
*/
if (mTouchState == TOUCH_STATE_SCROLLING) {
// Scroll to follow the motion event
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
final float x = ev.getX(pointerIndex);
final float deltaX = mLastMotionX - x;
mLastMotionX = x;//注意更新mLastMotionX
/**
* 向右滑动的时候,scrollX的值=上一次scrollX+xDiff
*/
//下面判断是向左还是向右滑动
if (deltaX < 0) {
//屏幕向左
if (mTouchX > 0) {
//取差值小的一个
/**
* xDiff是负数,所以
* 和向右滑动类似,当在第一个屏幕的时候,再向左滑动的时候,就会出现xDiff的绝对值大余scrollX的情况
* 这个时候scrollX的值接近于0,而xDiff的绝对值很可能大于0的。所以,这里做了如下的限制
*/
mTouchX += Math.max(-mTouchX, deltaX);
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
invalidate();
}
} else if (deltaX > 0) {
//屏幕向右
//当前可以滑动的最右边
final float availableToScroll = getChildAt(getChildCount() - 1).getRight() -
mTouchX - getWidth();
if (availableToScroll > 0) {
/**
* 注意:
*
* 当滑动倒数第二个屏幕的时候,就有可能出现xDiff>availableScoll的情况
* 因为scrollX最大为最后一个屏幕的最左边
* available-getWidth就是scrollX的最大取值范围M
* 所以,availableSroll=M-当前已经滑动的距离(scrollX);
* 这样当在最后一个屏幕的时候,再向右就不能滑动了
*/
mTouchX += Math.min(availableToScroll, deltaX);
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
invalidate();
}
} else {
awakenScrollBars();
}
}
break;
在这里,根据新的坐标位置,就算是向左还是向右滑动。同时处理滑动操作。那么,当我们停下的时候,它又是怎么做的呢?看ACTION_UP事件中的处理逻辑:
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
final int velocityX = (int) velocityTracker.getXVelocity(mActivePointerId);
final int screenWidth = getWidth();
final int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;
final float scrolledPos = (float) getScrollX() / screenWidth;
if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
//向左
// Fling hard enough to move left.
// Don't fling across more than one screen at a time.
final int bound = scrolledPos < whichScreen ?
mCurrentScreen - 1 : mCurrentScreen;
snapToScreen(Math.min(whichScreen, bound), velocityX, true);
} else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1) {
// Fling hard enough to move right
// Don't fling across more than one screen at a time.
final int bound = scrolledPos > whichScreen ?
mCurrentScreen + 1 : mCurrentScreen;
snapToScreen(Math.max(whichScreen, bound), velocityX, true);
} else {
//否则,看哪个屏幕显示的部分更多,就滑动到哪个屏幕
/**
* 其实很简单,就是以当前屏幕为基准,如果scrollX超出了一半,就滑倒下一个屏幕
* 如果没有超过一半就停留在该屏幕
* 所以,getScrollX()+screenWidth/2/screenWidth的思想就是
* 如果scollX超过了屏幕的一半,再加上个半个屏幕的大小,在除以整个屏幕的大小就是下一屏了
* 否则,就还是scrollX所在的屏幕
*/
snapToScreen(whichScreen, 0, true);
}
}
mTouchState = TOUCH_STATE_REST;
mActivePointerId = INVALID_POINTER;
releaseVelocityTracker();
break;
注意了,这里用VelocityTracker 计算了滑动的速度,因为,我们在滑动桌面的时候,应该注意到一个细节,当我们不是拖着桌面滑动,而是很快的滑动的时候,屏幕之间滑动到下一个屏幕的。这个就是通过VelocityTracker 计算滑动速度,如果滑动速度大于某个值,就直接滑动到下一个屏幕。具体的滑动到哪一个屏幕,是由方法snapToScreen处理的。那么我们就来看看这个好方法的逻辑: private void snapToScreen(int whichScreen, int velocity, boolean settle) {
//if (!mScroller.isFinished()) return;
whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
clearVacantCache();
enableChildrenCache(mCurrentScreen, whichScreen);
mNextScreen = whichScreen;
mPreviousIndicator.setLevel(mNextScreen);
mNextIndicator.setLevel(mNextScreen);
View focusedChild = getFocusedChild();
if (focusedChild != null && whichScreen != mCurrentScreen &&
focusedChild == getChildAt(mCurrentScreen)) {
focusedChild.clearFocus();//当屏幕切换时需要将当前屏幕的focus去掉
}
final int screenDelta = Math.max(1, Math.abs(whichScreen - mCurrentScreen));
final int newX = whichScreen * getWidth();//当前需要滑到的屏幕的左边x坐标
final int delta = newX - getScrollX();//偏差,〉0向右,<0向左
int duration = (screenDelta + 1) * 100;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
if (settle) {
mScrollInterpolator.setDistance(screenDelta);
} else {
mScrollInterpolator.disableSettle();
}
velocity = Math.abs(velocity);
if (velocity > 0) {
duration += (duration / (velocity / BASELINE_FLING_VELOCITY))
* FLING_VELOCITY_INFLUENCE;
} else {
duration += 100;
}
awakenScrollBars(duration);
mScroller.startScroll(getScrollX(), 0, delta, 0, duration);
invalidate();
}
在这个snapToScreen的方法中,逻辑很简单,主要就是调用了Scroller的startScroll方法,以当前滑动的位置和目标位置作为参数,启动滑动。但是,仅仅这样,这个方法起不到任何的效果,因为startScroll方法只是开始滑动,并不会不断的更新数据和处理滑动中的事情,这些事情是由computeScroll方法完成的。下面,我们再进入computeScroll方法来看看其逻辑:
/**
* 这个是当mScroller在滑动到某個屏幕的時候調用的
* 我们调用ScrollToScreen这个方法,我们调用了startScroll()这个方法,但是,如果不重写computeScroll
* 你会发现,{@link #snapToScreen(int)}没有效果的,原因就是
* 在其自己滑动的时候,我们调用startScroll的时候,只是设定了我们希望滑倒的位置,但是其滑动过程中
* 怎么滑动,还是在这个方法里。
* 当mScroller.computeScrollOffset返回真,说明还没有滑倒目的地,就继续计算
* 当返回假的时候,就说明滑动startScroll设定的终点了
*
* 奶奶的,想了半天才想明白,哎,杯具!
* @see #snapToScreen(int)
*/
@Override
public void computeScroll() {
//mScroller.computeScrollOffset计算当前新的位置
//返回true,说明scroll还没有停止
if (mScroller.computeScrollOffset()) {
/**
* mScrollX 是保护属性,不能跨包访问,使用scrollTo(int,int) modify by author
*/
//mTouchX = mScrollX = mScroller.getCurrX();
mTouchX = mScroller.getCurrX();
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
/**
* mScrollY 是保护属性,不能跨包访问,使用scrollTo(int,int) modify by author
*/
//mScrollY = mScroller.getCurrY();
/**
* 其实这里是不用scrollTo的,只需要设置mScrollX和mScrollY的值分别为
* mScroller.getCurrX()和mScroller.getCurrY()就行了
* 但是我们无法直接设置,所以用scrollTo完成
*/
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
updateWallpaperOffset();
/**
* 这里需要调用postInvalidate,否则滑动的时候,你会发现
* 界面会在两个屏幕的中间位置卡住
*/
postInvalidate();
} else if (mNextScreen != INVALID_SCREEN) {
//scroll停止了,则滑动到合法且合适的屏幕
mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
mPreviousIndicator.setLevel(mCurrentScreen);
mNextIndicator.setLevel(mCurrentScreen);
Launcher.setScreen(mCurrentScreen);
mNextScreen = INVALID_SCREEN;
//清除子控件绘制缓存
clearChildrenCache();
} else if (mTouchState == TOUCH_STATE_SCROLLING) {
final float now = System.nanoTime() / NANOTIME_DIV;
final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT);
final float dx = mTouchX - getScrollX();
/**
* mScrollX 是保护属性,不能跨包访问,使用scrollBy(int,int) modify by author
*/
//mScrollX += dx * e;
scrollBy((int)(dx * e), 0);
mSmoothingTime = now;
// Keep generating points as long as we're more than 1px away from the target
if (dx > 1.f || dx < -1.f) {
updateWallpaperOffset();
postInvalidate();
}
}
}
到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。
参考资料: