CellLayout的設計主要為了存放大小不一的控制項。為了更好的控制item的添加和刪除,選擇直接繼承ViewGroup來實現該控制項。
我們長按案頭的時候,有兩種情況,一種是我們按的是一個item,還有一種是我們按的是一個空的位置。這裡,就有一個問題。
1、我怎麼知道當前按下的位置上是空白地區還是item呢?
2、就算我知道了當前的位置座標,我又如何知道當前的座標屬於哪個儲存格呢?
3、如果上面兩個問題都解決了,當我選擇了某個要添加的item,這個item怎麼樣才能添加到指定的儲存格呢,怎麼根據當前item的大小來分配大小合適的空間呢?
為了處理儲存格和item佔據的空間問題,CellLayout按照如示進行布局:
下面就來看看CellLayout中是如何表示上面的CellInfo的:
public static final class CellInfo implements ContextMenu.ContextMenuInfo{public View view; //當前這個item對應的Viewpublic int cellX; //該item水平方向上的起始儲存格public int cellY;//該item垂直方向上的起始儲存格public int cellHSpan; //該item水平方向上佔據的儲存格數目public int cellVSpan; //該item垂直方向上佔據的儲存格數目public boolean valid; //是否有效public int screen; //所在的螢幕Rect current = new Rect(); //用於遞迴尋找連續儲存格,當前連續地區的大小final ArrayList<VacantCell> vacantCells = new ArrayList<UorderCellLayout.CellInfo.VacantCell>();public void clear(){final ArrayList<VacantCell> list = vacantCells;final int count = list.size();for(int i=0; i<count; i++){list.get(i).release();}list.clear();}public String toString(){return "cellinfo:[cellX="+cellX+",cellY="+cellY+",cellHSpan="+cellHSpan+",cellVSpan="+cellVSpan+"]";}/** * * VacantCell:代表空的cells,由多個cell組成,將其實現為一個cell池,減少對象的建立 * */static final class VacantCell{private static final int POOL_SIZE = 100; //池最多緩衝100個VacantCellprivate static final Object mLock = new Object(); //用作同步鎖private static VacantCell mRoot;private static int count;private VacantCell mNext;//VacantCell的大小資訊private int cellX;private int cellY;private int cellHSpan;private int cellVSpan;public static VacantCell acquire(){synchronized (mLock) {if(mRoot == null){return new VacantCell(); //一開始沒有的時候,一直新建立再返回}//如果池存在,則從池中取VacantCell info = mRoot;mRoot = info.mNext;count--; //記得將統計更新return info;}}//release這個對象自身public void release(){synchronized(mLock){if(count < POOL_SIZE){count++;mNext = mRoot;mRoot = this;}}}}}}
其用一個CellInfo儲存當前位置上的View資訊和其位置資訊,但是注意到其還定義了一個VancantCell類,這個主代表某個空的“地區”,這個地區可能有多個儲存格。同時,其實現為一個鏈表結構的儲存格池,這樣主要不用每次都來建立新對象,最佳化效能。
對CellLayout的大概結構有所瞭解後,我們就可以接著去尋找開始提到的三個問題的答案了。
一、如何標識當前位置上的資訊
為了可以知道某個位置是空還是已經被佔用了,CellLayout用一個二維布爾數組boolean[水平儲存格數][豎直儲存格數]來儲存每個儲存格的佔用資訊,被佔用的為true,空的為false。
為了判斷當前長按事件的位置是否在item上,可以在onInterceptTouchEvent方法中如下判斷當前長按事件的位置是否在某個item的位置裡。如下:
final Rect frame = mRect;final int x = (int)ev.getX(); final int y = (int)ev.getY();Log.v(TAG, "MotionEvent.getX,getY:[x,y]=["+x+","+y+"]");final int count = getChildCount();boolean found = false;Log.v(TAG, "CellLayout Child count:"+count);for(int i=count-1; i>=0; i--){final View child = getChildAt(i);if(child.getVisibility() == VISIBLE || child.getAnimation() != null){child.getHitRect(frame); //擷取child的尺寸資訊,相對於CellLayoutLog.v(TAG, "View.getHitRect:"+frame.toString());if(frame.bottom<=frame.top || frame.right<= frame.left){Log.v(TAG, "The rectangle of the view is incorrect");continue;}if(frame.contains(x,y)){//如果當前事件正好落在該child上final LayoutParams lp = (LayoutParams)child.getLayoutParams();cellInfo.view = child;cellInfo.cellX = lp.cellX;cellInfo.cellY = lp.cellY;cellInfo.cellHSpan = lp.cellHSpan;cellInfo.cellVSpan = lp.cellVSpan;cellInfo.valid = true;found = true;Log.v(TAG, "YES,Found!");break;}}}
上面我們記錄了如果落在某個item上,我們記錄下當前的位置資訊和view資訊。那麼如果當前長按的是一塊空的地區呢?
if(!found){/** * 如果點擊的位置是空白地區,則也需要儲存當前的位置資訊 * 點擊空白地區的時候,是需要做更多的處理,在外層彈出對話方塊添加應用,檔案夾,捷徑等,然後在案頭該 * 位置處建立表徵圖 */int cellXY[] = mCellXY;pointToCellExact(x,y,cellXY); //得到當前事件所在的儲存格Log.v(TAG, "Not Found the cellXY is =["+cellXY[0]+","+cellXY[1]+"]");//然後儲存當前位置資訊cellInfo.view = null;cellInfo.cellX = cellXY[0];cellInfo.cellY = cellXY[1];cellInfo.cellHSpan = 1;cellInfo.cellVSpan = 1;//這裡需要計算哪些儲存格被佔用了final int xCount = mHCells; //TODO:沒有考慮橫豎屏的情況final int yCount = mVCells;final boolean[][] occupied = mOccupied;findOccupiedCells(xCount, yCount, occupied);//判斷當前位置是否有效,這裡不用再判斷cellXY是否越界,因為在pointToCellExact已經進行了處理cellInfo.valid = !occupied[cellXY[0]][cellXY[1]];//這裡其實我們需要以當前的cellInfo表示的儲存格為中心,向四周遞迴開闢連續的最大空間//但是,這裡還並不需要,只有當getTag()方法被調用的時候,才說明需要一塊地區去放一個View//所以,將這個開闢的方法放在getTag()中調用//這裡標記一下mTagFlag = true;}//將位置資訊儲存在CellLayout的tag中setTag(cellInfo);
長按某個地區,我們記錄下當前的位置資訊,注意,我們是記錄下當前事件所在的儲存格,然後儲存的是該儲存格的資訊,所以,上面調用了pointToCellExact這個方法來計算當前事件座標落在哪個儲存格內,並且調用了findOccupiedCells方法計算整個CellLayout上所有儲存格的被佔用情況。關於事件座標到儲存格的對應,計算並不困難,因為我們知道每個儲存格的寬度和高度,同時知道當前的事件座標,那麼簡單的除法就可以計算得到。
二、我們添加的item如何被添加到CellLayout上面
我們知道,要想繪製每個孩子自然在onLayout中調用每個孩子的layout方法,下面就看看這個方法的實現:
protected void onLayout(boolean changed, int l, int t, int r, int b) {int count = getChildCount();for(int i=0; i<count; i++){View child = getChildAt(i);if(child.getVisibility() != GONE){LayoutParams lp = (LayoutParams)child.getLayoutParams();child.layout(lp.x, lp.y, lp.x+lp.width, lp.y+lp.height);}}}
注意,在該方法中每個孩子的布局,是按照他們自身的LayoutParams對象中儲存的資訊來布局到具體的位置的。那麼接下來,我們就要分析下CellLayout中每個孩子的LayoutParams的結構。CellLayout中有個自訂的LayoutParams類,該類儲存了該孩子所在的儲存格資訊和其真實的座標位置,其含有一個set方法,在這個方法中計算了孩子的width,height,起始座標x和y。
public void set(int cellWidth, int cellHeight, int hStartPadding, int vStartPadding, int widthGap, int heightGap){//計算item的寬和高//這裡計算的時候,注意是width,height是需要排除掉margin的this.width = cellHSpan*cellWidth+(cellHSpan-1)*widthGap-leftMargin-rightMargin;this.height = cellVSpan*cellHeight + (cellVSpan-1)*heightGap - topMargin - bottomMargin;Log.v(TAG, "The width and height of the view are:"+this.width+","+this.height);//同時計算item的真實座標//除去item的margin和padding,view開始的位置this.x = cellX*(cellWidth+widthGap)+leftMargin+hStartPadding;this.y = cellY*(cellHeight+heightGap)+topMargin+vStartPadding;Log.v(TAG, "The x and y of the view are:"+this.x+","+this.y);}
這個時候,也就知道了,每個孩子的寬度和高度,以及如何在布局的時候根據其所在儲存格資訊,轉換為其真實座標。我們知道,控制項的布局需要經過兩個階段,一個是measure,接下來就是layout。measure主要完成控制項的測繪工作,計算每個控制項繪製需要的空間資訊,所以,在onMeasure中,自然可以看到給每個孩子測量大小的時候,就同時為其調用了set方法。如下:
int count = getChildCount();Log.v(TAG, "onMeasure 開始。。。");for(int i=0; i<count; i++){//對每個子控制項進行測量了View child = getChildAt(i);LayoutParams lp = (LayoutParams)child.getLayoutParams();//這裡需要將我們計算的結果封裝進LayoutParams中,供CellLayout在布局子控制項的時候使用//這裡橫豎屏需要不同對待//TODO:暫時不考慮lp.set(mCellWidth, mCellHeight, mHStartPadding, mVStartPadding, mHCellGap, mVCellGap);//下面將擷取子控制項的寬度和高度,並用MeasureSpec編碼int cWidth = lp.width;int cHeight = lp.height;int cWidthSpec = MeasureSpec.makeMeasureSpec(cWidth, MeasureSpec.EXACTLY);int cHeightSpec = MeasureSpec.makeMeasureSpec(cHeight, MeasureSpec.EXACTLY);child.measure(cWidthSpec, cHeightSpec);}
到這裡,關於CellLayout上面孩子的繪製工作就介紹完畢了。但是還沒有說到,我們長按案頭的時候,怎樣將我們選擇的item給添加到案頭上來。這個就得再回到Launcher的onLongClick方法中,我們看下:
if(!(v instanceof UorderCellLayout)){v = (View)v.getParent(); //如果當前點擊的是item,得到其父控制項,即UorderCellLayout}CellInfo cellInfo = (CellInfo)v.getTag(); //這裡擷取cellInfo資訊if(cellInfo == null){Log.v(TAG, "CellInfo is null");return true;}//Log.v(TAG, ""+cellInfo.toString());if(cellInfo.view == null){//說明是空白地區//ActivityUtils.alert(getApplication(), "空白地區");Log.v(TAG, "onLongClick,cellInfo.valid:"+cellInfo.valid);if(cellInfo.valid){//如果是有效地區//ActivityUtils.alert(getApplication(), "有效地區");addCellInfo = cellInfo;showPasswordDialog(REQUEST_CODE_SETUP, null);}}else{mWorkspace.startDrag(cellInfo);}return true;
我們看到在onLongClick中有CellInfo cellInfo = (CellInfo)v.getTag(); 這樣,我們就知道,在上面onInterceptTouchEvent方法中我們將cellInfo放入tag是為了在CellLayout的getTag方法中,返回cellInfo資訊。有了cellInfo資訊,我們就可以調用CellLayout.addView方法將我們所選擇的控制項添加到案頭了。在Workspace類中,調用addInScreen方法設定其儲存格資訊,然後直接調用CellLayout.addView方法添加到CellLayout。
public void addInScreen(View child, int screen, int cellX, int cellY, int spanX, int spanY, boolean insertFirst){if(screen<0 || screen >= getChildCount()){throw new IllegalArgumentException("The screen must be >= 0 and <"+getChildCount());}final UorderCellLayout group = getCellLayout(screen);UorderCellLayout.LayoutParams lp = (UorderCellLayout.LayoutParams)child.getLayoutParams();//初始化當前需要添加的View在CellLayout中的布局參數if(lp == null){lp = new UorderCellLayout.LayoutParams(cellX, cellY, spanX, spanY);}else{lp.cellX = cellX;lp.cellY = cellY;lp.cellHSpan = spanX;lp.cellVSpan = spanY;}Log.v(TAG, "Before add view on the screen");group.addView(child, insertFirst?0:-1, lp);child.setOnLongClickListener(mOnLongClickListener);//child的onClickListener在建立出View的時候設定的,在Uorderlauncher中}
上面,我們看到直接調用了group.addView方法,但是之前我們僅僅設定了child的LayoutParams中儲存格資訊,至於怎麼根據這些資訊得到其真實座標,怎麼布局,上面已經介紹了。
至此,CellLayout的大致就介紹完了。但是CellLayout還不至於如此簡單。當我們添加的控制項不止一個儲存格那麼大的時候,如何分配其空間,如果空間不夠怎麼處理等問題都是CellLayout需要考慮的問題。但是,我想有了上面的理解,這個也不是什麼難題了...
參考資料:
《說說Android案頭(Launcher應用)背後的故事(三)——CellLayout的秘密》