android Launcher源碼解析07:Workspace 01——概述

來源:互聯網
上載者:User

       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》

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.