您的位置:首页 > 移动开发 > Android开发

android Launcher应用之CellLayout的设计

2015-12-12 17:33 676 查看
CellLayout的设计主要为了存放大小不一的控件。为了更好的控制item的添加和删除,选择直接继承ViewGroup来实现该控件。

我们长按桌面的时候,有两种情况,一种是我们按的是一个item,还有一种是我们按的是一个空的位置。这里,就有一个问题。

1、我怎么知道当前按下的位置上是空白区域还是item呢?

2、就算我知道了当前的位置坐标,我又如何知道当前的坐标属于哪个单元格呢?

3、如果上面两个问题都解决了,当我选择了某个要添加的item,这个item怎么样才能添加到指定的单元格呢,怎么根据当前item的大小来分配大小合适的空间呢?

为了处理单元格和item占据的空间问题,CellLayout按照如下图示进行布局:



下面就来看看CellLayout中是如何表示上面的CellInfo的:

[java] view
plaincopy

public static final class CellInfo implements ContextMenu.ContextMenuInfo{  

    public View view; //当前这个item对应的View  

      

    public 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个VacantCell  

        private 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的位置里。如下:

[java] view
plaincopy

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的尺寸信息,相对于CellLayout  

          

        Log.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信息。那么如果当前长按的是一块空的区域呢?

[java] view
plaincopy

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方法,下面就看看这个方法的实现:

[java] view
plaincopy

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。

[java] view
plaincopy

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方法。如下:

[java] view
plaincopy

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方法中,我们看下:

[java] view
plaincopy

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。

[java] view
plaincopy

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需要考虑的问题。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: