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(); } } }
到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。
参考资料: