說說Android案頭(Launcher應用)背後的故事(四)——揭秘Workspace

來源:互聯網
上載者:User

 部落格搬家啦——為了更好地經營部落格,本人已經將部落格遷移至www.ijavaboy.com。這裡已經不再更新,給您帶來的不便,深感抱歉!這篇文章的新地址:點擊我 

         前面說了Layout最主要的職責就是負責item的布局和空間的分配,這一節我們繼續來看看CellLayout的父親控制項Workspace。手機的案頭是由幾個螢幕的,你可以任意滑動的。這個布局就是一個Workspace。Launcher的Workspace主要的職責就是處理多個螢幕之間的滑動和壁紙的添加。

這裡先提下,我們知道DragLayer包含了Workspace,Workspace又包含了幾個CellLayout,那麼我們首先應該知道,它們是如何各司其職而互不影響的。這個就是Android中事件的傳遞機制。我們知道,一個應用中,整個的布局是一個樹狀,那麼當使用者的一個Touch操作,比如點擊事件,是如何從最外層的父親控制項傳遞到具體的子空間中去的。這個就要歸功於View的onInterceptTouchEvent和onTouchEvent兩個方法了。這兩個方法的傳回值決定了一個Touch事件的傳遞時序。onInterceptTouchEvent,顧名思義,就是起到一個攔截的作用。這兩個方法是如何決定事件的傳遞的呢?

1、如果使用者執行一個ACTION_DOWN事件,當前View的onInterceptTouchEvent返回true,則該事件和後續的ACTION_MOVE和,ACTION_UP將不再傳遞到View的子控制項,而是直接交由該View的onTouchEvent來處理。

2、如果上面onInterceptTouchEvent返回false,則該事件和後續的ACTION_UP,ACTION_MOVE將也會透過onInterceptTouchEvent,繼而傳遞到子控制項的onInterceptTouchEvent。

3、如果該View的onInterceptTouchEvent返回false,事件傳遞到目標View,然後目標View的onTouchEvent又返回了false,那麼事件將繼續傳遞到目標View的上一級的onTouchEvent。如果目標View的onTouchEvent方法返回了true,說明此事件已被處理了。

瞭解了Android中Touch事件的傳遞機制,也就很容易弄清楚DragLayer,Workspace和CellLayout是如何做到各司其職的了。下面,就讓我們一起打入Workspace的內部。

一、處理多個螢幕的滑動

我們知道Workspace是由幾個CellLayout橫向平鋪組成的,那麼簡單點,就是實際的布局超出了手機的螢幕,那麼就需要滑動,需要一個Scroller對象來計算每次滑動後的座標以及處理滑動的狀態。而且下了Launcher的源碼,你會發現,報了很多紅叉,其大部分是因為mScollX和mScollY錯誤,這是因為這兩個屬性是不公開的,子類無法直接使用,所以我們在實現的時候這部分注意,取mScollX和mScrollY的時候,用getScrollX和getScrollY,給mScrollX和mScrollY賦新值的時候,調用scrollBy()或者scrollTo函數來執行。

1、讓幾個CellLayout平鋪,由於每個CellLayout的大小是手機的一螢幕大小,所以,這裡讓其橫向平鋪就很簡單了,直接在onLayout中調用每個CellLayout的layout方法進行布局,按循序配置的時候,只需要控制好每個CellLayout的left和right就可以了。

protected void onLayout(boolean changed, int l, int t, int r, int b) {final int count = getChildCount();int childLeft = 0;//橫向平鋪CellLayoutfor(int i=0; i<count; i++){View child = getChildAt(i);final int width = child.getMeasuredWidth();final int height = child.getMeasuredHeight();if(child.getVisibility() != GONE){child.layout(childLeft, 0, childLeft+width, height);childLeft += width;}}}

2、如何處理螢幕的滑動,螢幕滑動,莫非就需要在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_MOVE:/** * 這裡是處理滑動的地方 * 注意,手指向右滑動的時候,螢幕是向左滑動的 *  */if(mTouchState == TOUCH_STATE_SCROLLING){final int xDiff = (int)(mLastMotionX - x);mLastMotionX = x; //注意更新mLastMotionX/** * 向右滑動的時候,scrollX的值=上一次scrollX+xDiff */Log.v(TAG, "當前scrollX的大小:"+getScrollX());Log.v(TAG, "當前差值大小:"+xDiff);//下面判斷是向左還是向右滑動if(xDiff < 0){//螢幕向左Log.v(TAG, "當前向左滑動");if(getScrollX()>0){//取差值小的一個/** * xDiff是負數,所以 * 和向右滑動類似,當在第一個螢幕的時候,再向左滑動的時候,就會出現xDiff的絕對值大庾scrollX的情況 * 這個時候scrollX的值接近於0,而xDiff的絕對值很可能大於0的。所以,這裡做了如下的限制 */int xDelta = Math.max(xDiff, -getScrollX());scrollBy(xDelta, 0);}}else if(xDiff > 0){//螢幕向右Log.v(TAG, "當前向右滑動");final int available = getChildAt(getChildCount()-1).getRight();Log.v(TAG, "當前可以滑動的最右邊:"+available);final int availableSroll = available-getScrollX()-getWidth();Log.v(TAG, "當前最大可以滑動的距離:"+availableSroll);if(availableSroll > 0){/** * 注意: *  * 當滑動倒數第二個螢幕的時候,就有可能出現xDiff>availableScoll的情況 * 因為scrollX最大為最後一個螢幕的最左邊 * available-getWidth就是scrollX的最大取值範圍M * 所以,availableSroll=M-當前已經滑動的距離(scrollX); * 這樣當在最後一個螢幕的時候,再向右就不能滑動了 */scrollBy(Math.min(availableSroll, xDiff), 0);}}}break;

在這裡,根據新的座標位置,就算是向左還是向右滑動。同時處理滑動操作。那麼,當我們停下的時候,它又是怎麼做的呢?看ACTION_UP事件中的處理邏輯:

if(mTouchState == TOUCH_STATE_SCROLLING){final VelocityTracker tracker = mVelocityTracker;tracker.computeCurrentVelocity(1000); //使用pix/s為單位int velX = (int)tracker.getXVelocity();Log.v(TAG, "當前滑動的速度:"+velX);if(velX > SNAP_VELOCITY && mCurrentScreen > 0){//向左snapToScreen(mCurrentScreen-1);}else if(velX < -SNAP_VELOCITY && mCurrentScreen < getChildCount()-1){//向右snapToScreen(mCurrentScreen+1);}else{//否則,看哪個螢幕顯示的部分更多,就滑動到哪個螢幕final int screenWidth = getWidth();//分析這裡為什麼可以這麼算final int whichScreen = (getScrollX()+screenWidth/2)/screenWidth;/** * 其實很簡單,就是以當前螢幕為基準,如果scrollX超出了一半,就滑倒下一個螢幕 * 如果沒有超過一半就停留在該螢幕 * 所以,getScrollX()+screenWidth/2/screenWidth的思想就是 * 如果scollX超過了螢幕的一半,再加上個半個螢幕的大小,在除以整個螢幕的大小就是下一屏了 * 否則,就還是scrollX所在的螢幕 */Log.w(TAG, "當前srollX的值:"+getScrollX());snapToScreen(whichScreen);}}

注意了,這裡用VelocityTracker 計算了滑動的速度,因為,我們在滑動案頭的時候,應該注意到一個細節,當我們不是拖著案頭滑動,而是很快的滑動的時候,螢幕之間滑動到下一個螢幕的。這個就是通過VelocityTracker 計算滑動速度,如果滑動速度大於某個值,就直接滑動到下一個螢幕。具體的滑動到哪一個螢幕,是由方法snapToScreen處理的。那麼我們就來看看這個好方法的邏輯:

    private void snapToScreen(int screen){        Log.w(TAG, "當前的螢幕:"+mCurrentScreen+"滑倒的螢幕是:"+screen);        enableChildrenCache();    screen = Math.max(0, Math.min(screen, getChildCount()-1));    boolean screenChange = screen != mCurrentScreen;        mNextScreen = screen;        View focusedChild = getFocusedChild();    if(focusedChild != null && screenChange && focusedChild == getChildAt(mCurrentScreen)){    focusedChild.clearFocus(); //當螢幕切換時需要將當前螢幕的focus去掉    }        final int newX = screen*getWidth();//當前需要滑到的螢幕的左邊x座標    final int scrollX = getScrollX();//當前滑輪所在的位置    final int delta = newX - scrollX; //偏差,〉0向右,<0向左    mScroller.startScroll(scrollX, 0, delta, 0, Math.abs(delta)*2);        Log.w(TAG, "startScroll yes");        invalidate();    }

在這個snapToScreen的方法中,邏輯很簡單,主要就是調用了Scroller的startScroll方法,以當前滑動的位置和目標位置作為參數,啟動滑動。但是,僅僅這樣,這個方法起不到任何的效果,因為startScroll方法只是開始滑動,並不會不斷的更新資料和處理滑動中的事情,這些事情是由computeScroll方法完成的。下面,我們再進入computeScroll方法來看看其邏輯:

/** * 這個是當mScroller在滑動到某個螢幕的時候調用的 * 我們調用ScrollToScreen這個方法,我們調用了startScroll()這個方法,但是,如果不重寫computeScroll * 你會發現,{@link #snapToScreen(int)}沒有效果的,原因就是 * 在其自己滑動的時候,我們調用startScroll的時候,只是設定了我們希望滑倒的位置,但是其滑動過程中 * 怎麼滑動,還是在這個方法裡。 * 當mScroller.computeScrollOffset返回真,說明還沒有滑倒目的地,就繼續計算 * 當返回假的時候,就說明滑動startScroll設定的終點了 *  * 奶奶的,想了半天才想明白,哎,杯具! * @see #snapToScreen(int) */public void computeScroll(){if(mScroller.computeScrollOffset()){//mScroller.computeScrollOffset計算當前新的位置//返回true,說明scroll還沒有停止int newX = mScroller.getCurrX();int newY = mScroller.getCurrY();/** * 其實這裡是不用scrollTo的,只需要設定mScrollX和mScrollY的值分別為 * mScroller.getCurrX()和mScroller.getCurrY()就行了 * 但是我們無法直接設定,所以用scrollTo完成 */scrollTo(newX, newY);/** * 這裡需要調用postInvalidate,否則滑動的時候,你會發現 * 介面會在兩個螢幕的中間位置卡住 */            postInvalidate();Log.v(TAG, "computeScroll was called:the scrollX and scrollY are"+getScrollX()+","+getScrollY());}else if(mNextScreen != INVALID_SCREEN){//scroll停止了,則滑動到合法且合適的螢幕mCurrentScreen = Math.min(Math.max(mNextScreen, 0), getChildCount()-1);mNextScreen = INVALID_SCREEN;  //標記mNextScreen為無效狀態//UorderLauncher.setScreen(mCurrentScreen);//清除子控制項繪製緩衝Log.v(TAG, "scroll is stop");clearChildrenCache();}}

到這裡,整個螢幕為什麼會滑動,這其中的邏輯處理,我想就基本清楚了。

下一篇,將繼續揭曉螢幕壁紙的添加,以及隨著螢幕的移動,壁紙是如何跟著移動的。     

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.