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(); } } }
到這裡,整個螢幕為什麼會滑動,這其中的邏輯處理,我想就基本清楚了。
參考資料:
《說說Android案頭(Launcher應用)背後的故事(四)——揭秘Workspace》