说说Android桌面(Launcher应用)???最近由于项目需要自己定制一套管理系统,遂想到了Android的启动器,下来了源码,一编译 到处是错,查了查原因,原来是引用了自家的成员,他们家开发的就是方便,想直接用就直接用。于是下载了个包,终于错误少了一大半。但是还有 一些错误,主要是引用了一些不存在的资源。于是改了改,基本可以运行了。但是,问题来了,一眼望去,红色叉叉是不见了,但是革命依然不容乐 观,一个人我认识,但是一下子成千上百个人出现在我的面前,我就不是一点点晕了,而是晕的不行了。然而,军令如山,纵然晕,也得硬着头皮将 革命进行下去,因为有一点是肯定的,革命必须胜利,不容失败!接下来的一系列文章,就记录下我的革命之旅!本来打算在原有launche r的基础上修改的,但是后来发现,功能不需要它那么复杂,改动的地方也挺多,所以,就一边分析源码一边自己重新写了,说是重新写,其实就是 抄,但是抄有抄的好处,老师布置作业的时候,作业没有作对的时候,老师总会说,这个字母抄三百遍,那个单词抄一百遍...可见抄的多了,也 就记住了。但是前提是用心抄,不能小和尚念经,有口无心。这里,当然不能抄个百遍,抄个一遍就消费了上亿个脑细胞。自己写的话,我们能更好 地思考,我要实现哪些功能,每个功能如何实现,系统Launcher是如何实现的,这样带着问题去分析,就很容易分门别类,理清思路,不至 于在浩瀚的代码中迷失方向。言归正传,要想自己写个一模一样的Launcher来,我们就得先对着模拟器或者Android系统的机子, 好好对每次第一个启动起来的这个看似简单却深藏不露的启动器。通过一番折腾,大致可以将Launcher实现的功能分为如下几个:1、在 空的位置长按,具备添加应用、快捷方式、文件夹和小部件的功能2、左右可以滑动的屏幕3、可以更换壁纸4、长按桌面上面某个item (上面说的四个可以添加到桌面的东东),就可以任意拖动了,同时下面的SldingDrawer位置会变成一个垃圾箱,你将拖拽的item 放到垃圾箱的位置其颜色变红的时候,就意味着你一放手,该item就被删除了5、桌面下方是个SlidingDrawer,你一点就弹上 来,在里面就是所有的应用,长按可以将其拖到桌面。?有了这样几个功能分类,我们的任务也就非常明了了。我们就来分析并思考怎么样才能实现 每个功能,最后融合为你现在所看到的效果。我们首先看第一个问题。简单的思考了下,觉得应该很简单,就是监听下界面的长按事件,在该事件中 ,弹出一个应用列表就OK了,如果这个弹出的应用列表还可以调用系统自带的,那就更省事了。第二个问题:滑动的屏幕,一开始还不知道怎么实 现,晚上下班的时候,一个人闷闷不乐地上了公交车,站到了窗口的位置,一双黯然的眼神充满了犹豫,可是脑子里始终是这个问题。当公交车开动 的时候,当窗外的一切在我的眼前缓缓向后退去的时候,我如梦初醒,失声惊叫,原来如此!幸好声音不大,否则又要出丑一次。原来手机屏幕就相 当于公交车的窗口,背后是一个大风景,只不过当我们拿着手机的时候,这个窗口是静止的,但是当我们滑动的时候,风景是随着移动的。至于,这 个是怎么移动的等细节问题,后面再去慢慢分析。第三个问题:关于更换壁纸,由于之前做过一个,所以,不用想太多,直接调用调用系统壁纸选择 应用,再实现一个BroadcastReciever,监听下壁纸的改变,获得一个Bitmap类型就行了。但是这里关键是获得Bitma p后如何将其显示出来,并随着屏幕的滑动,壁纸也要相应地移动,这里壁纸就相当于上面讲的风景,目前想到的是直接将其设置为背景,但是觉得 在多个屏幕之间,不可行。第四个问题:拖动本身并不难,但是要在多个屏幕之间拖动就比较难办了。这个没有具体的思路,只能在实现第二个问题 后,再融合多屏幕和拖拽的功能了。第五个问题:这个很简单,SlidingDrawer的content为一个GridView,Grid View的每个item就是一个应用,关键是长按某个item就将其拖动到桌面,这个暂时也么有思路,待后面走到这一步再思考吧。?到此, 要实现的功能已经各个功能大致的思路就有了。但是,当要下手时,又发现光有这些还是毫无头绪,没办法,还是要先来打第一仗,做到了解“敌人 ”的目的。所以,对Launcher的源码大致看了下,主要看其每个类的用途,找准其设计思想。这里不用纠结于某个类的某个具体的方法的具 体功能,只需要知道每个类是干什么的就可以了,然后分个主次关系,看看哪些是关键,哪些是次要的,哪些是附属品...经过第一战役,获得如 下成果:一、桌面的每种类型的item进行了类的封装,主要层次关系如下图:——ItemInfo——ApplicationInfo ——FolderInfo——LiveFolderInfo——UserFolderInfo——Widget?二、整个布局层次如 下图:——DragLayer——Workspace——CellLayout——CellInfo(Application,Sh ortcut,Folder,Widget)?——CellLayout?——CellLayout------------------ -----------------------三、关键的几个重要的类:CellLayout,Workspace,Launcher, DragLayer,把握这几个基本就清楚了桌面的布局。其中最外层是DragLayer,控制桌面上item的拖动,DragLayer 中是一个Workspace,Workspace就是整个桌面,由几个CellLayout横铺组成,主要控制多屏幕的滑动,壁纸的绘制; CellLayout是一个自定义布局控件,其功能类似一个GridView,由mn个单元格(Cell)组成,其主要负责桌面长按事件 的处理,获得长按的区域信息,主要需要判断当前区域是空的还是已经被占用了,同时,控制item的拖动。CellLayout中放的就是上 面几种类型的item了。item的拖动功能,是由DragLayer,Workspace,CellLayout协同完成的。?好了,至 此,基本应用可以动手了。先不关注其他类的具体用途,先搞定所有的布局!下一篇将具体解决第一个功能,同时深入CellLayout的背后 ,一探CellLayout的究竟。。。?上篇中,讲到了第一个功能中需要获取应用程序的信息,然后添加到桌面。这里,先记录下如何获取A ndroid中的应用程序信息。一、调用系统快捷方式列表[java]?viewplain?copy?Intent?pickInte nt?=?new?Intent(Intent.ACTION_PICK_ACTIVITY);??pickIntent.putExtr a(Intent.EXTRA_INTENT,???new?Intent(Intent.ACTION_CREATE_SHORTCUT ));??pickIntent.putExtra(Intent.EXTRA_TITLE,???res.getString(R.st ring.title_select_app));???pickIntent.putExtras(bundle);???startA ctivityForResult(pickIntent,?REQUEST_PICK_SHORTCUT);?这个获取到的就是所有可以 创建的快捷方式,但是我们需要在这个列表中加入其他项怎么办呢?因为我们知道,我需要在这个列表中加入“应用程序”,这一项,这样当我们点 击应用程序的时候,就可以显示应用程序列表。这个时候,我们只需要传入额外的信息就可以了,如下:?[java]?viewplain? copyResources?res?=?getResources();???/???用一个Bundle对象,传递两个list 信息???一个list中存放需要附加到快捷列表中的项的文字???一个list中存放每一个对应的图标信息??/???Bundl e?bundle?=?new?Bundle();????ArrayList?shortcutNames?=?new ?ArrayList();???shortcutNames.add(res.getString(R.string. group_application));???shortcutNames.add("其他");???//显示在List第一个的应用 快捷方式的名字??bundle.putStringArrayList(Intent.EXTRA_SHORTCUT_NAME,?sh ortcutNames);????//显示图标??ArrayList?shortcut Icons?=???new?ArrayList();???shortcutIcons. add(ShortcutIconResource.fromContext(UorderLauncher.this,???R.dra wable.icon));???shortcutIcons.add(ShortcutIconResource.fromContex t(UorderLauncher.this,???R.drawable.icon));???bundle.putParcelabl eArrayList(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,?shortcutIcons);?? ??Intent?pickIntent?=?new?Intent(Intent.ACTION_PICK_ACTIVITY);??? pickIntent.putExtra(Intent.EXTRA_INTENT,???new?Intent(Intent.ACTI ON_CREATE_SHORTCUT));???pickIntent.putExtra(Intent.EXTRA_TITLE,?? ?res.getString(R.string.title_select_app));???//将附加信息加入到pickInten t???pickIntent.putExtras(bundle);????startActivityForResult(pickI ntent,?REQUEST_PICK_SHORTCUT);二、调用应用程序列表当我们点击我们附加的项“应用程序”时,我们希望弹出 应用程序列表,这个也可以条用系统自带列表,也可以使用自己定义的列表。1、调用系统应用信息列表在上面我们加上了一个附加项“应用程序” ,那么我们怎么知道,当前点击的是“应用程序”这一项呢?注意上面我们启动的时候是startActivityForResult(pic kIntent,REQUEST_PICK_SHORTCUT);在onActivityResult中处理该请求对应的逻辑:如果用户 选择的是“应用程序”,则调用系统应用程序列表,否则,直接请求创建快捷方式[java]?viewplain?copyprivate ?void?addShortcut(Intent?data){???Resources?res?=?getResources(); ????final?String?appName?=?res.getString(R.string.group_applicati on);???final?String?shortcutName?=?data.getStringExtra(Intent.EXT RA_SHORTCUT_NAME);???if(appName.equals(shortcutName)){??//说明用户选择的 是应用程序,进入应用程序列表??Intent?mainIntent?=?new?Intent(Intent.ACTION_MAIN ,null);???mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);????In tent?pickIntent?=?new?Intent(Intent.ACTION_PICK_ACTIVITY);???pick Intent.putExtra(Intent.EXTRA_INTENT,?mainIntent);???startActivity ForResult(pickIntent,?REQUEST_PICK_APPLICATION);???}else{???//否则, 直接创建快捷方式??startActivityForResult(data,?REQUEST_CREATE_SHORTCUT);? ??}??}??这样,就实现点击“应用程序”则显示应用程序列表,否则,创建快捷方式2、获取所有应用程序信息,实现自定义的应用程序列 表其实,对于系统中所有应用程序的信息,程序中都是可以获取到的。在桌面点击的时候用的是系统自带的应用程序列表。但是,当点击桌面下方的 应用程序图标的时候,就需要显示一个GridView显示所有的应用程序信息,这里,就需要获取应用程序信息,然后放入每一个item中。 这里就顺带实现一下。布局和效果略过,只记录如果获取应用程序信息。[java]?viewplain?copy/???同样,以 ACTION_MAIN和CATEGORY_LAUNCHER作为过滤条件???查询所有符合的应用信息(ResolveInfo)?? ?ResolveInfo中就包含了我们需要的主要信息。???当然还有其他的方式获取应用信息,如PackageInfo,关于这些 信息,可以参看???http://android.tgbus.com/Android/tutorial/201108/36421 0.shtml??/???final?Intent?mainIntent?=?new?Intent(Intent.ACTION_ MAIN,?null);??mainIntent.addCategory(Intent.CATEGORY_LAUNCHER);?? final?PackageManager?pManager?=?this.getPackageManager();??/?? ?queryIntentActivities通过解析所有应用程序中含有如下Intent-filter的App???-filter>???? ??? ??/??final?List?infoList?=?pManager .queryIntentActivities(mainIntent,?0);??if(infoList?!=?null){???A rrayList?appList?=?new?ArrayList();???for(Resol veInfo?info?:?infoList){???AppInfo?app?=?makeAppInfo(pManager,?in fo);???appList.add(app);???}???return?appList;??}???return?null;? ?程序还包含了一个makeAppInfo(pManager,info);这个方法源码是:[java]?viewplain?co pyprivate?AppInfo?makeAppInfo(PackageManager?pManager,?ResolveInf o?info)?{???//有了ComponentName,就可以保存每个应用程序的启动信息??//下次我们可以直接启动它??Co mponentName?component?=?new?ComponentName(info.activityInfo.appli cationInfo.packageName,???info.activityInfo.name);???//这个是自己定义的一个 用来封装我们需要的应用程序的信息??AppInfo?app?=?new?AppInfo();????//获取应用的名称??Char Sequence?appName?=?info.loadLabel(pManager);???if(appName?==?null ){???//如果没有成功获取到,则用其主Activity的名字作为应用名称??appName?=?info.activityIn fo.name;???}???app.name?=?appName.toString();????//获取应用程序的图标??Dra wable?draw?=?info.activityInfo.loadIcon(pManager);????//应用程序图片可以大 小不一,这里需要将他们的size限定在一定的大小??app.icon?=?BitmapUtils.createIconThumbn ail(draw,?getApplicationContext());????//将启动信息保存在AppInfo中,在我们的应用中 启动另一个应用程序,使用FLAG_ACTIVITY_NEW_TASK???app.setActivity(component,?I ntent.FLAG_ACTIVITY_NEW_TASK?|?Intent.FLAG_ACTIVITY_RESET_TASK_IF _NEEDED);????return?app;??}???这样就得到了所有的应用程序信息。系统Launcher在其GridVie w中列出的所有应用就是这么获取的。?三、获取应用程序信息的其他方式:下面这个获取到的应用信息,你会发现比上面的方法多了很多,因为其 获得的是系统中所有的安装包信息[java]?viewplain?copyList?packageLis t?=?this.getPackageManager().getInstalledPackages(0);??appList?=? new?ArrayList();??for(PackageInfo?info?:?packageList){?? ?CharSequence?appName?=?info.applicationInfo.loadLabel(getPackage Manager());???if(TextUtils.isEmpty(appName)){??appName?=?info.pac kageName;???}???//加上下面这个if条件就把系统自带的应用给过滤掉了,获取到的仅仅是我们自己安装的??if((in fo.applicationInfo.flags?&?ApplicationInfo.FLAG_SYSTEM)<=0){???Co mponentName?comp?=?getComponentName(info.packageName);????AppInfo ?appInfo?=?new?AppInfo();???appInfo.icon?=?info.applicationInfo.l oadIcon(getPackageManager());???appInfo.name?=?appName.toString() ;???appInfo.setActivity(comp,?Intent.FLAG_ACTIVITY_NEW_TASK|???In tent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);????appList.add(appInfo) ;?}??}??程序中调用了getComponentName,其源码是:[java]?viewplain?copyprivate ?ComponentName?getComponentName(String?packageName)?{???Intent?in tent?=?new?Intent(Intent.ACTION_MAIN,?null);???intent.addCategory (Intent.CATEGORY_LAUNCHER);???intent.setPackage(packageName);???? List?infoList?=?getPackageManager().queryIntentActiv ities(intent,?0);???if(infoList?!=?null?&&?infoList.size()>0){??? ResolveInfo?info?=?infoList.get(0);???if(info?!=?null){???String? pkgName?=?info.activityInfo.packageName;???String?clsName?=?info. activityInfo.name;???return?new?ComponentName(pkgName,?clsName);? ??}???}???return?null;??}???到处,也就很清楚Launcher中长按桌面,应用程序是如何选择和在Grid View中的应用程序信息是如何获取到的了。下一篇,将继续介绍CellLayout自定义控件的实现原理!三?CellLayout的设 计主要为了存放大小不一的控件。为了更好的控制item的添加和删除,选择直接继承ViewGroup来实现该控件。我们长按桌面的时候, 有两种情况,一种是我们按的是一个item,还有一种是我们按的是一个空的位置。这里,就有一个问题。1、我怎么知道当前按下的位置上是空 白区域还是item呢?2、就算我知道了当前的位置坐标,我又如何知道当前的坐标属于哪个单元格呢?3、如果上面两个问题都解决了,当我选 择了某个要添加的item,这个item怎么样才能添加到指定的单元格呢,怎么根据当前item的大小来分配大小合适的空间呢?为了处理单 元格和item占据的空间问题,CellLayout按照如下图示进行布局:下面就来看看CellLayout中是如何表示上面的Cell Info的:[java]?viewplain?copypublic?static?final?class?CellInfo?im plements?ContextMenu.ContextMenuInfo{???public?View?view;?//当前这个i tem对应的View????public?int?cellX;?//该item水平方向上的起始单元格??public?int?ce llY;???//该item垂直方向上的起始单元格??public?int?cellHSpan;?//该item水平方向上占据的单 元格数目??public?int?cellVSpan;?//该item垂直方向上占据的单元格数目???public?boolean ?valid;?//是否有效???public?int?screen;?//所在的屏幕???Rect?current?=?new? Rect();?//用于递归寻找连续单元格,当前连续区域的大小???final?ArrayList?vac antCells?=?new?ArrayList(); ?????public?void?clear(){???final?ArrayList?list?=?va cantCells;???final?int?count?=?list.size();???for(int?i=0;?it;?i++){???list.get(i).release();???}????list.clear();???}????pub lic?String?toString(){????return?"cellinfo:[cellX="+cellX+",cellY ="+cellY+",cellHSpan="+cellHSpan+",cellVSpan="+cellVSpan+"]";???} ???/??????VacantCell:代表空的cells,由多个cell组成,将其实现为一个cell池,减少对象的创建 ????/???static?final?class?VacantCell{???private?static?final?i nt?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;???pr ivate?int?cellHSpan;???private?int?cellVSpan;????public?static?Va cantCell?acquire(){???synchronized?(mLock)?{???if(mRoot?==?null){ ???return?new?VacantCell();?//一开始没有的时候,一直新创建再返回??}???//如果池存在,则从池中 取??VacantCell?info?=?mRoot;???mRoot?=?info.mNext;???count--;?//记得 将统计更新???return?info;???}???}????//release这个对象自身??public?void?rele ase(){???synchronized(mLock){??if(count???mNext?=?mRoot;???mRoot?=?this;???}???}???}???}??}??其用一个CellInfo 保存当前位置上的View信息和其位置信息,但是注意到其还定义了一个VancantCell类,这个主代表某个空的“区域”,这个区域可 能有多个单元格。同时,其实现为一个链表结构的单元格池,这样主要不用每次都来创建新对象,优化性能。对CellLayout的大概结构有 所了解后,我们就可以接着去寻找开始提到的三个问题的答案了。一、如何标识当前位置上的信息为了可以知道某个位置是空还是已经被占用了,C ellLayout用一个二维布尔数组boolean[水平单元格数][竖直单元格数]来保存每个单元格的占用信息,被占用的为true, 空的为false。为了判断当前长按事件的位置是否在item上,可以在onInterceptTouchEvent方法中如下判断当前长 按事件的位置是否在某个item的位置里。如下:[java]?viewplain?copyfinal?Rect?frame?=?m Rect;??final?int?x?=?(int)ev.getX();final?int?y?=?(int)ev.getY(); ???Log.v(TAG,?"MotionEvent.getX,getY:[x,y]=["+x+","+y+"]");???fin al?int?count?=?getChildCount();??boolean?found?=?false;??Log.v(TA G,?"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.getHitRe ct(frame);?//获取child的尺寸信息,相对于CellLayout????Log.v(TAG,?"View.getHi tRect:"+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.getLay outParams();???cellInfo.view?=?child;???cellInfo.cellX?=?lp.cellX ;???cellInfo.cellY?=?lp.cellY;???cellInfo.cellHSpan?=?lp.cellHSpa n;???cellInfo.cellVSpan?=?lp.cellVSpan;???cellInfo.valid?=?true;? ??found?=?true;???Log.v(TAG,?"YES,Found!");???break;???}???}??}?? 上面我们记录了如果落在某个item上,我们记录下当前的位置信息和view信息。那么如果当前长按的是一块空的区域呢?[java]?v iewplain?copyif(!found){???/???如果点击的位置是空白区域,则也需要保存当前的位置信息??? 点击空白区域的时候,是需要做更多的处理,在外层弹出对话框添加应用,文件夹,快捷方式等,然后在桌面该???位置处创建图标??/? ??int?cellXY[]?=?mCellXY;???pointToCellExact(x,y,cellXY);?//得到当前事 件所在的单元格??Log.v(TAG,?"Not?Found?the?cellXY?is?=["+cellXY[0]+","+ce llXY[1]+"]");???//然后保存当前位置信息??cellInfo.view?=?null;???cellInfo.ce llX?=?cellXY[0];???cellInfo.cellY?=?cellXY[1];???cellInfo.cellHSp an?=?1;???cellInfo.cellVSpan?=?1;????//这里需要计算哪些单元格被占用了??final?int ?xCount?=?mHCells;?//TODO:没有考虑横竖屏的情况??final?int?yCount?=?mVCells; ???final?boolean[][]?occupied?=?mOccupied;???findOccupiedCells(xC ount,?yCount,?occupied);????//判断当前位置是否有效,这里不用再判断cellXY是否越界,因为在poi ntToCellExact已经进行了处理??cellInfo.valid?=?!occupied[cellXY[0]][cellX Y[1]];????//这里其实我们需要以当前的cellInfo表示的单元格为中心,向四周递归开辟连续的最大空间??//但是,这里 还并不需要,只有当getTag()方法被调用的时候,才说明需要一块区域去放一个View???//所以,将这个开辟的方法放在getT ag()中调用??//这里标记一下??mTagFlag?=?true;??}???//将位置信息保存在CellLayout的tag 中?setTag(cellInfo);??长按某个区域,我们记录下当前的位置信息,注意,我们是记录下当前事件所在的单元格,然后保存 的是该单元格的信息,所以,上面调用了pointToCellExact这个方法来计算当前事件坐标落在哪个单元格内,并且调用了find OccupiedCells方法计算整个CellLayout上所有单元格的被占用情况。关于事件坐标到单元格的对应,计算并不困难,因为 我们知道每个单元格的宽度和高度,同时知道当前的事件坐标,那么简单的除法就可以计算得到。?二、我们添加的item如何被添加到Cell Layout上面我们知道,要想绘制每个孩子自然在onLayout中调用每个孩子的layout方法,下面就看看这个方法的实现:[ja va]?viewplain?copyprotected?void?onLayout(boolean?changed,?int?l ,?int?t,?int?r,?int?b)?{???int?count?=?getChildCount();???for(int ?i=0;?iVisibility()?!=?GONE){???LayoutParams?lp?=?(LayoutParams)child.ge tLayoutParams();???child.layout(lp.x,?lp.y,?lp.x+lp.width,?lp.y+l p.height);???}???}??}??注意,在该方法中每个孩子的布局,是按照他们自身的LayoutParams对象中保存的 信息来布局到具体的位置的。那么接下来,我们就要分析下CellLayout中每个孩子的LayoutParams的结构。CellLay out中有个自定义的LayoutParams类,该类保存了该孩子所在的单元格信息和其真实的坐标位置,其含有一个set方法,在这个方 法中计算了孩子的width,height,起始坐标x和y。[java]?viewplain?copypublic?void?se t(int?cellWidth,?int?cellHeight,?int?hStartPadding,?int?vStartPad ding,?int?widthGap,?int?heightGap){???//计算item的宽和高??//这里计算的时候,注意是 width,height是需要排除掉margin的??this.width?=?cellHSpancellWidth+(cell HSpan-1)widthGap-leftMargin-rightMargin;???this.height?=?cellVSp ancellHeight?+?(cellVSpan-1)heightGap?-?topMargin?-?bottomMargi n;????Log.v(TAG,?"The?width?and?height?of?the?view?are:"+this.wid th+","+this.height);???//同时计算item的真实坐标??//除去item的margin和padding,v iew开始的位置??this.x?=?cellX(cellWidth+widthGap)+leftMargin+hStartPa dding;???this.y?=?cellY(cellHeight+heightGap)+topMargin+vStartPa dding;????Log.v(TAG,?"The?x?and?y?of?the?view?are:"+this.x+","+th is.y);??}??这个时候,也就知道了,每个孩子的宽度和高度,以及如何在布局的时候根据其所在单元格信息,转换为其真实坐标。我们 知道,控件的布局需要经过两个阶段,一个是measure,接下来就是layout。measure主要完成控件的测绘工作,计算每个控件 绘制需要的空间信息,所以,在onMeasure中,自然可以看到给每个孩子测量大小的时候,就同时为其调用了set方法。如下:[jav a]?viewplain?copyint?count?=?getChildCount();??Log.v(TAG,?"onMea sure?开始。。。");??for(int?i=0;?ichild?=?getChildAt(i);???LayoutParams?lp?=?(LayoutParams)child.ge tLayoutParams();???//这里需要将我们计算的结果封装进LayoutParams中,供CellLayout在布局子 控件的时候使用??//这里横竖屏需要不同对待??//TODO:暂时不考虑??lp.set(mCellWidth,?mCellHei ght,?mHStartPadding,?mVStartPadding,?mHCellGap,?mVCellGap);????// 下面将获取子控件的宽度和高度,并用MeasureSpec编码??int?cWidth?=?lp.width;???int?cHei ght?=?lp.height;???int?cWidthSpec?=?MeasureSpec.makeMeasureSpec(c Width,?MeasureSpec.EXACTLY);???int?cHeightSpec?=?MeasureSpec.make MeasureSpec(cHeight,?MeasureSpec.EXACTLY);???child.measure(cWidth Spec,?cHeightSpec);??}??到这里,关于CellLayout上面孩子的绘制工作就介绍完毕了。但是还没有说到,我 们长按桌面的时候,怎样将我们选择的item给添加到桌面上来。这个就得再回到Launcher的onLongClick方法中,我们看下 :[java]?viewplain?copyif(!(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?tr ue;??}???//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(getA pplication(),?"有效区域");???addCellInfo?=?cellInfo;???showPasswordDi alog(REQUEST_CODE_SETUP,?null);?}??}else{???mWorkspace.startDrag( cellInfo);??}??return?true;??我们看到在onLongClick中有CellInfocellInfo =(CellInfo)v.getTag();这样,我们就知道,在上面onInterceptTouchEvent方法中我们将ce llInfo放入tag是为了在CellLayout的getTag方法中,返回cellInfo信息。有了cellInfo信息,我们就 可以调用CellLayout.addView方法将我们所选择的控件添加到桌面了。在Workspace类中,调用addInScree n方法设置其单元格信息,然后直接调用CellLayout.addView方法添加到CellLayout。[java]?viewp lain?copypublic?void?addInScreen(View?child,?int?screen,?int?cell X,?int?cellY,?int?spanX,?int?spanY,?boolean?insertFirst){???if(sc reen<0?||?screen?>=?getChildCount()){???throw?new?IllegalArgument Exception("The?screen?must?be?>=?0?and?<"+getChildCount());???}?? ?final?UorderCellLayout?group?=?getCellLayout(screen);???UorderCe llLayout.LayoutParams?lp?=?(UorderCellLayout.LayoutParams)child.g etLayoutParams();????//初始化当前需要添加的View在CellLayout中的布局参数??if(lp?==? null){???lp?=?new?UorderCellLayout.LayoutParams(cellX,?cellY,?spa nX,?spanY);???}else{???lp.cellX?=?cellX;???lp.cellY?=?cellY;???lp .cellHSpan?=?spanX;???lp.cellVSpan?=?spanY;???}????Log.v(TAG,?"Be fore?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需要考虑 的问题。但是,我想有了上面的理解,这个也不是什么难题了...四?前面说了Layout最主要的职责就是负责item的布局和空间的分配 ,这一节我们继续来看看CellLayout的父亲控件Workspace。手机的桌面是由几个屏幕的,你可以任意滑动的。这个布局就是一 个Workspace。Launcher的Workspace主要的职责就是处理多个屏幕之间的滑动和壁纸的添加。这里先提下,我们知道D ragLayer包含了Workspace,Workspace又包含了几个CellLayout,那么我们首先应该知道,它们是如何各司 其职而互不影响的。这个就是Android中事件的传递机制。我们知道,一个应用中,整个的布局是一个树状,那么当用户的一个Touch操 作,比如点击事件,是如何从最外层的父亲控件传递到具体的子空间中去的。这个就要归功于View的onInterceptTouchEve nt和onTouchEvent两个方法了。这两个方法的返回值决定了一个Touch事件的传递时序。onInterceptTouchE vent,顾名思义,就是起到一个拦截的作用。这两个方法是如何决定事件的传递的呢?1、如果用户执行一个ACTION_DOWN事件,当 前View的onInterceptTouchEvent返回true,则该事件和后续的ACTION_MOVE和,ACTION_UP将 不再传递到View的子控件,而是直接交由该View的onTouchEvent来处理。2、如果上面onInterceptTouchE vent返回false,则该事件和后续的ACTION_UP,ACTION_MOVE将也会透过onInterceptTouchEve nt,继而传递到子控件的onInterceptTouchEvent。3、如果该View的onInterceptTouchEvent 返回false,事件传递到目标View,然后目标View的onTouchEvent又返回了false,那么事件将继续传递到目标Vi ew的上一级的onTouchEvent。如果目标View的onTouchEvent方法返回了true,说明此事件已被处理了。了解了 Android中Touch事件的传递机制,也就很容易弄清楚DragLayer,Workspace和CellLayout是如何做到各 司其职的了。下面,就让我们一起打入Workspace的内部。一、处理多个屏幕的滑动我们知道Workspace是由几个CellLay out横向平铺组成的,那么简单点,就是实际的布局超出了手机的屏幕,那么就需要滑动,需要一个Scroller对象来计算每次滑动后的坐 标以及处理滑动的状态。而且下了Launcher的源码,你会发现,报了很多红叉,其大部分是因为mScollX和mScollY错误,这 是因为这两个属性是不公开的,子类无法直接使用,所以我们在实现的时候这部分注意,取mScollX和mScrollY的时候,用getS crollX和getScrollY,给mScrollX和mScrollY赋新值的时候,调用scrollBy()或者scrollTo 函数来执行。1、让几个CellLayout平铺,由于每个CellLayout的大小是手机的一屏幕大小,所以,这里让其横向平铺就很简 单了,直接在onLayout中调用每个CellLayout的layout方法进行布局,按顺序布局的时候,只需要控制好每个CellL ayout的left和right就可以了。[java]?viewplain?copyprotected?void?onLayou t(boolean?changed,?int?l,?int?t,?int?r,?int?b)?{???final?int?coun t?=?getChildCount();???int?childLeft?=?0;???//横向平铺CellLayout???fo r(int?i=0;?int?width?=?child.getMeasuredWidth();???final?int?height?=?child.g etMeasuredHeight();???if(child.getVisibility()?!=?GONE){???child. layout(childLeft,?0,?childLeft+width,?height);???childLeft?+=?wid th;???}???}??}??2、如何处理屏幕的滑动,屏幕滑动,莫非就需要在ACTION_MOVE事件中处理。我们在文章的开头介 绍了Android的事件拦截机制,那么我们想,要让滑动事件让Workspace处理,而不会干扰到CellLayout,自然要在on InterceptTouchEvent中做一些处理了。那我们先从onInterceptTouchEvent方法入手,在onInte rceptTouchEvent方法中显眼的位置,我们就可以一眼发现如下代码:[java]?viewplain?copyif(ac tion?==?MotionEvent.ACTION_MOVE?&&?mTouchState?!=?TOUCH_STATE_STO PED){???return?true;??}??同时在ruturn的时候,其返回的是:mTouchState!=TOUCH_ STATE_STOPED;这个就是说,如果当前正在滑动,则返回true,交给onTouchEvent事件来处理滑动逻辑。那么,我们 就再来看看onTouchEvent中对ACTION_MOVE事件的处理:[java]?viewplain?copycase?Mo tionEvent.ACTION_MOVE:???/???这里是处理滑动的地方???注意,手指向右滑动的时候,屏幕是向左滑 动的?????/???if(mTouchState?==?TOUCH_STATE_SCROLLING){???final?in t?xDiff?=?(int)(mLastMotionX?-?x);???mLastMotionX?=?x;?//注意更新mLas tMotionX????/???向右滑动的时候,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?=?get ChildAt(getChildCount()-1).getRight();???Log.v(TAG,?"当前可以滑动的最右边:" +available);????final?int?availableSroll?=?available-getScrollX() -getWidth();???Log.v(TAG,?"当前最大可以滑动的距离:"+availableSroll);???if(av ailableSroll?>?0){???/???注意:??????当滑动倒数第二个屏幕的时候,就有可能出现xDiff> availableScoll的情况???因为scrollX最大为最后一个屏幕的最左边???available-getWidth 就是scrollX的最大取值范围M???所以,availableSroll=M-当前已经滑动的距离(scrollX);???这 样当在最后一个屏幕的时候,再向右就不能滑动了??/???scrollBy(Math.min(availableSroll,?xD iff),?0);???}???}???}???break;??在这里,根据新的坐标位置,就算是向左还是向右滑动。同时处理滑动操作 。那么,当我们停下的时候,它又是怎么做的呢?看ACTION_UP事件中的处理逻辑:[java]?viewplain?copyif (mTouchState?==?TOUCH_STATE_SCROLLING){???final?VelocityTracker?t racker?=?mVelocityTracker;???tracker.computeCurrentVelocity(1000) ;?//使用pix/s为单位??int?velX?=?(int)tracker.getXVelocity();????Log.v( TAG,?"当前滑动的速度:"+velX);????if(velX?>?SNAP_VELOCITY?&&?mCurrentScre en?>?0){???//向左??snapToScreen(mCurrentScreen-1);???}else?if(velX? -SNAP_VELOCITY?&&?mCurrentScreen?snapToScreen(mCurrentScreen+1);???}else{???//否则,看哪个屏幕显示的部分更多,就滑动到 哪个屏幕??final?int?screenWidth?=?getWidth();????//分析这里为什么可以这么算??fina l?int?whichScreen?=?(getScrollX()+screenWidth/2)/screenWidth;???/ ???其实很简单,就是以当前屏幕为基准,如果scrollX超出了一半,就滑倒下一个屏幕???如果没有超过一半就停留在该屏幕 ???所以,getScrollX()+screenWidth/2/screenWidth的思想就是???如果scollX超过了 屏幕的一半,再加上个半个屏幕的大小,在除以整个屏幕的大小就是下一屏了???否则,就还是scrollX所在的屏幕??/???Lo g.w(TAG,?"当前srollX的值:"+getScrollX());????snapToScreen(whichScreen );???}??}??注意了,这里用VelocityTracker计算了滑动的速度,因为,我们在滑动桌面的时候,应该注意到一个细 节,当我们不是拖着桌面滑动,而是很快的滑动的时候,屏幕之间滑动到下一个屏幕的。这个就是通过VelocityTracker计算滑动 速度,如果滑动速度大于某个值,就直接滑动到下一个屏幕。具体的滑动到哪一个屏幕,是由方法snapToScreen处理的。那么我们就来 看看这个好方法的逻辑:[java]?viewplain?copyprivate?void?snapToScreen(int?sc reen){????Log.w(TAG,?"当前的屏幕:"+mCurrentScreen+"滑倒的屏幕是:"+screen);?? ??enableChildrenCache();???screen?=?Math.max(0,?Math.min(screen,? getChildCount()-1));???boolean?screenChange?=?screen?!=?mCurrentS creen;????mNextScreen?=?screen;?????View?focusedChild?=?getFocuse dChild();???if(focusedChild?!=?null?&&?screenChange?&&?focusedChi ld?==?getChildAt(mCurrentScreen)){???focusedChild.clearFocus();?/ /当屏幕切换时需要将当前屏幕的focus去掉??}????final?int?newX?=?screengetWidth();/ /当前需要滑到的屏幕的左边x坐标??final?int?scrollX?=?getScrollX();//当前滑轮所在的位置??f inal?int?delta?=?newX?-?scrollX;?//偏差,〉0向右,<0向左??mScroller.startS croll(scrollX,?0,?delta,?0,?Math.abs(delta)2);????Log.w(TAG,?"st artScroll?yes");????invalidate();??}??在这个snapToScreen的方法中,逻辑很简单,主 要就是调用了Scroller的startScroll方法,以当前滑动的位置和目标位置作为参数,启动滑动。但是,仅仅这样,这个方法起 不到任何的效果,因为startScroll方法只是开始滑动,并不会不断的更新数据和处理滑动中的事情,这些事情是由computeSc roll方法完成的。下面,我们再进入computeScroll方法来看看其逻辑:[java]?viewplain?copy/ ???这个是当mScroller在滑动到某個屏幕的時候調用的???我们调用ScrollToScreen这个方法,我们调用了st artScroll()这个方法,但是,如果不重写computeScroll???你会发现,{@link?#snapToScree n(int)}没有效果的,原因就是???在其自己滑动的时候,我们调用startScroll的时候,只是设定了我们希望滑倒的位置, 但是其滑动过程中???怎么滑动,还是在这个方法里。???当mScroller.computeScrollOffset返回真,说 明还没有滑倒目的地,就继续计算???当返回假的时候,就说明滑动startScroll设定的终点了??????奶奶的,想了半天 才想明白,哎,杯具!???@see?#snapToScreen(int)??/??public?void?computeScr oll(){???if(mScroller.computeScrollOffset()){???//mScroller.compu teScrollOffset计算当前新的位置??//返回true,说明scroll还没有停止??int?newX?=?mScrol ler.getCurrX();???int?newY?=?mScroller.getCurrY();????/???其实这里 是不用scrollTo的,只需要设置mScrollX和mScrollY的值分别为???mScroller.getCurrX()和 mScroller.getCurrY()就行了???但是我们无法直接设置,所以用scrollTo完成??/???scrollT o(newX,?newY);????/???这里需要调用postInvalidate,否则滑动的时候,你会发现???界面会 在两个屏幕的中间位置卡住??/???postInvalidate();???Log.v(TAG,?"computeScroll? was?called:the?scrollX?and?scrollY?are"+getScrollX()+","+getScrol lY());????}else?if(mNextScreen?!=?INVALID_SCREEN){???//scroll停止了, 则滑动到合法且合适的屏幕???mCurrentScreen?=?Math.min(Math.max(mNextScreen,?0) ,?getChildCount()-1);???mNextScreen?=?INVALID_SCREEN;??//标记mNextS creen为无效状态??//UorderLauncher.setScreen(mCurrentScreen);???//清除子控件 绘制缓存??Log.v(TAG,?"scroll?is?stop");???clearChildrenCache();???}?? }??到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。下一篇,将继续揭晓屏幕壁纸的添加,以及随着屏幕的移动,壁纸 是如何跟着移动的。?五上一篇中,我们了解了Workspace是如何处理多个CellLayout之间的滑动的。这篇,将记录如何将壁纸 添加到桌面,以及Workspace如何处理滑动的时候,壁纸的滑动。壁纸的添加,也是调用系统自带的,用如下方式调用:[java]?v iewplain?copy//调用系统自带壁纸选择功能,ACTION_SET_WALLPAPER为选择的时候使用的过滤条件?In tent?chooseIntent?=?new?Intent(Intent.ACTION_SET_WALLPAPER);???// 启动系统选择应用?Intent?intent?=?new?Intent(Intent.ACTION_CHOOSER);???int ent.putExtra(Intent.EXTRA_INTENT,?chooseIntent);???intent.putExtr a(Intent.EXTRA_TITLE,?"选择壁纸");???startActivity(intent);??这样就会列出所有 Action含有android.intent.action.SET_WALLPAPER的应用。当然,上面的代码也可以直接简写为:[ java]?viewplain?copyIntent?chooseIntent?=?new?Intent(Intent.ACTI ON_SET_WALLPAPER);??startActivity(Intent.createChooser(chooseInte nt,?"选择壁纸"));???但是,按照常见的,应该是startActivityForResult来启动,然后在onActivi tyResult中来获取返回后的壁纸。但是,选择壁纸后,并不是通过这种方式获取选择的壁纸的,那么我们如何获取选择的壁纸呢?其实,当 我们选择了一张壁纸后,系统会发出一个Broadcast,同时将选择的壁纸,缓存在整个应用程序的上下文中,这样,其实就是由于壁纸,不 仅可以在桌面上更换,也可以在图片显示应用中更换,所以,其使用Broadcast机制来通知壁纸已经更换。我们需要实现一个Broadc astReciever来监听壁纸的选择:[java]?viewplain?copy?/??????当别的应用程序改变了壁 纸后,这里定义一个BroadcastReceiver来接受通知????/???class?WallpaperIntentRec eiver?extends?BroadcastReceiver{???private?Application?applicatio n;???//WeakReference使得WallpaperIntentReceiver不会因为Launcher的引用而被推迟注 销掉??private?WeakReference?rLauncher;????public?Wa llpaperIntentReceiver(Application?application,?UorderLauncher?lau ncher)?{???this.application?=?application;???this.rLauncher?=?new ?WeakReference(launcher);??}????public?void?setLa uncher(UorderLauncher?l){???this.rLauncher?=?new?WeakReferencerderLauncher>(l);???}???@Override??public?void?onReceive(Context? context,?Intent?intent)?{???Log.v(TAG,?"更换了壁纸");???/???从Applic ationContext获取壁纸??/???final?Drawable?lDrawable?=?application.get Wallpaper();???if(lDrawable?instanceof?BitmapDrawable){???Log.v(T AG,?"壁纸是BitmapDrawable类型的");???mWallpaper?=?((BitmapDrawable)lDra wable).getBitmap();????}else{???throw?new?IllegalStateException(" The?wallpaper?must?be?a?BitmapDrawable?object");???}????/???如果 此时Launcher是活动的,未被锁定,则加载新的Wallpaper??/???if(rLauncher?!=?null){?? ?final?UorderLauncher?launcher?=?rLauncher.get();???if(launcher?! =?null){???launcher.loadWallpaper();???}???}??}????}??在这个Broadcas tReciever中,直接通过Application.getWallpaper();获取最新的壁纸,那么有了BroadcastRe ciever,我们怎么知道这个Reciever是接收壁纸更换的Broadcast的呢?所以,我们需要注册它:[java]?view plain?copyprivate?void?registerIntentReceivers(){???if(mWallpape rReceiver?==?null){???mWallpaperReceiver?=?new?WallpaperIntentRec eiver(getApplication(),?this);???/???注册的时候,指定IntentFilter,这样改B roadcastReciver就是接收壁纸更换的Broadcast的了??/???IntentFilter?filter?=?n ew?IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);???getApplicatio n().registerReceiver(mWallpaperReceiver,?filter);???}else{???mWal lpaperReceiver.setLauncher(this);???}??}??现在,知道了壁纸的获取,那么,接下来我们自然而 然想到的是:壁纸我已经获取到了,但是怎么样将它绘制在Workspace上面呢?注意,上面BroadcastReceiver获取到壁 纸的时候调用了launcher.loadWallpaper()来完成壁纸加载的,在这个方法中,可以看到其调用了mWorkspace .loadWallpaper(mWallpaper);那么,就需要回到Workspace中了:[java]?viewplain? copy/???这里加载壁纸,需要重新绘制界面???@param?wallpaper??/??public?void?l oadWallpaper(Bitmap?wallpaper)?{???wallpaperLoaded?=?true;???mWal lpaper?=?wallpaper;???//要求重新绘制界面??requestLayout();???invalidate() ;??}??这个方法仅仅要求重新绘制布局,那么,我们就知道,在绘制的方法中,应该会对壁纸进行相关的绘制。在dispatchDraw 中,有对壁纸的处理,[java]?viewplain?copy/???壁纸本身可能并没有整个workspace那么宽?? ?所以,屏幕滑动的时候,壁纸向右边滑动的距离需要根据mWallpaperOffset做相应的调整??/??float?x?=?g etScrollX()mWallpaperOffset;??//这里啥个意思,TODO:??Log.v(TAG,?"getRig ht-getLeft="+getRight()+"-"+getLeft()+"="+(getRight()-getLeft())) ;???/???getRight()-getLeft()=手机屏幕的宽度??/??if(xeft()-mWallpaperWidth){??//当壁纸宽度小于屏幕宽度的时候,才会出现小于的情况??//这种情况就固定??/ /但是在获得用户选择的壁纸的时候,我们对其作了大小调整,所以,这里基本不会出现这种情况??x?=?getRight()-getLe ft()-mWallpaperWidth;??}???float?y?=?(getBottom()-getTop()-mWallp aperHeight)/2;???Log.v(TAG,?"开始画壁纸:x,y分别为:"+x+","+y);??//canvas.d rawColor(Color.BLACK);??if(mWallpaper!=null){???Log.v(TAG,?"开始画壁纸 ");???canvas.drawBitmap(mWallpaper,?x,?y,?mPaint);???//invalidate ();??}在这里,我们看到了,其通过canvas.drawBitmap(mWallpaper,x,y,mPaint);绘制 了壁纸,这里关键的是x,y的计算,桌面可以横向滑动的,那么每次滑动后重新绘制的时候,这个x的值是在变化的,通过代码我们可以发现,其 中有一个变量mWallpaperOffset,查找这个变量,在onMeasure中,对该变量进行了赋值:[java]?viewp lain?copy//加载壁纸?if(wallpaperLoaded){??wallpaperLoaded?=?false;??? mWallpaper?=?BitmapUtils.centerToFit(mWallpaper,?width,?height,?g etContext());???mWallpaperWidth?=?mWallpaper.getWidth();???mWallp aperHeight?=?mWallpaper.getHeight();???Log.v(TAG,?"测量壁纸大小:"+mWall paperWidth+","+mWallpaperHeight);??}???final?int?wallpaperWidth?= ?mWallpaperWidth;??//计算Wallpaper每次随着屏幕滑动移动的距离?if(wallpaperWidth?> ?width){???/???计算壁纸滑动的速率???壁纸可以滑动的距离是countwidth-wallpaperWid th???屏幕可以滑动的距离是(count-1)width???这样,一除就是壁纸相对于屏幕滑动的速率了??/???//m WallpaperOffset?=?wallpaperWidth/(count(float)width);???mWallpap erOffset?=?(countwidth-wallpaperWidth)/((count-1)(float)width); ??}else?{???mWallpaperOffset?=?1.0f;??}???Log.v(TAG,?"wallpaper的o ffset:"+mWallpaperOffset);??这样,当我们每次滑动的时候,Workspace在重绘的时候就会计算这个值, 然后在dispatchDraw中首先绘制壁纸,然后绘制每个CellLayout。好了,至此,我们应该清楚壁纸的添加机制了。下一篇将 揭晓item的拖拽之谜...六本来这一篇将写Android中Launcher是如何实现桌面上item的拖拽的,当研究了其机理之后 ,突然大脑发热,想实现一个可以拖拽的ListView,在理解了Launcher中item的拖拽,再来实现可以拖拽的ListView 简直就是小菜一碟了。于是将此篇位于Launcher中拖拽之前,可以起到一个过渡理解的作用。只是,这里还有些不一样的地方就是Laun cher上item拖拽后可以放到一个空的位置,而ListView中某个item被拖拽之后是需要交换新位置和原来位置上的item。下 面,就不再废话了,直接上代码,具体需要注意的地方请看注释:[java]?viewplain?copypublic?class?D ragListView?extends?ListView{??private?static?final?String?TAG?=? "DragListView";???private?static?final?int?INVALID_POSITION?=?-1; ???private?Bitmap?mDragBitmap;????private?View?mDragView;????priv ate?UorderDeleteZone?zone;????private?View?footer;????private?Obj ect?srcContent;????private?int?startPosition;????private?Paint?mP aint?=?new?Paint();????private?float?mLastMotionX;???private?floa t?mLastMotionY;????private?float?mTouchOffsetX;???private?float?m TouchOffsetY;????private?float?mBitmapOffsetX;???private?float?mB itmapOffsetY;????private?boolean?mDragging?=?false;????private?Re ct?mDragRect?=?new?Rect();?//当拖动一个item的时候,记录拖动经过的区域???//是一个距离,表示滑 动的时候,手的移动要大于这个距离才开始移动控件。??private?int?mScaledTouchSlop;????privat e?float?mTopScrollBound;//向上滑动超过这个边界的时候,上面的item向下滚动???private?flo at?mBottomScrollBound;//向下滑动超过这个边界的时候,下面的item开始向上滚动???public?Drag ListView(Context?context){???this(context,?null);???}????public?D ragListView(Context?context,?AttributeSet?attrs)?{???this(context ,?attrs,?0);???}??public?DragListView(Context?context,?AttributeS et?attrs,?int?defStyle)?{???super(context,?attrs,?defStyle);????/ ???当某个item被拖拽时,将其颜色改变下,??/???final?int?srcColor?=?context.get Resources().getColor(R.color.drag_filter);???mPaint.setColorFilte r(new?PorterDuffColorFilter(srcColor,?PorterDuff.Mode.SRC_ATOP)); ????//是一个距离,表示滑动的时候,手的移动要大于这个距离才开始移动控件。??mScaledTouchSlop?=?ViewC onfiguration.get(context).getScaledTouchSlop();?????}????protecte d?void?dispatchDraw(Canvas?canvas)?{???super.dispatchDraw(canvas) ;???if(mDragging?&&?mDragBitmap?!=?null){???final?int?scrollX?=?g etScrollX();???final?int?scrollY?=?getScrollY();????float?left?=? scrollX?+?mLastMotionX-mTouchOffsetX-mBitmapOffsetX;???float?top? =?scrollY?+?mLastMotionY-mTouchOffsetY-mBitmapOffsetY;???canvas.d rawBitmap(mDragBitmap,?left,?top,?mPaint);???}?????}????/???在这 个方法中判断当前用户按下事件所在的位置???如果该位置在右边30dip之内,就进行拖拽??/???public?boolean ?onInterceptTouchEvent(MotionEvent?event){?????int?action?=?event .getAction();???final?float?x?=?event.getX();???final?float?y?=?e vent.getY();????if(getWidth()-x?>?30){???//不是位于拖拽区域,直接返回就OK了??ret urn?super.onInterceptTouchEvent(event);???}??switch?(action)?{??? case?MotionEvent.ACTION_DOWN:???mLastMotionX?=?x;???mLastMotionY? =?y;???break;???default:???break;???}???Log.e(TAG,?"touchSlop:"+m ScaledTouchSlop);???//计算滚动边界??/???这里当向上拖拽到1/3屏幕高度的时候就滚动,但是这里如果 开始拖拽的item就是位于这1/3屏幕内位置的???则,取当前小的一个.当然你也就可以设置成1/3的屏幕??/???mTopS crollBound?=?Math.min(y-mScaledTouchSlop,?getHeight()/3);???mBott omScrollBound?=?Math.min(y?+?mScaledTouchSlop,?getHeight()2/3);? ??//??????mTopScrollBound?=?getHeight()/3;??//??????mBottomScroll Bound?=?getHeight()2/3;????Log.e(TAG,?"bound?scroll:"+mTopScroll Bound+","+mBottomScrollBound);????//获取当前事件坐标所在的item项,ListView中拖动是 上下拖动的,所以,和x坐标关系不大??int?position?=?this.pointToPosition((int)x,?(i nt)y);????if(position?==?INVALID_POSITION){???return?super.onInte rceptTouchEvent(event);???}????startPosition?=?position;????/?? ?这里获取当前被拖拽的item,注意,因为ListView中并不是每个item都是一个View,这个View是重用的??/?? ?mDragView?=?getChildAt(position-getFirstVisiblePosition());???sr cContent?=?getItemAtPosition(position);????if(mDragView?!=?null){ ????mDragBitmap?=?createViewBitmap(mDragView);???//当item被拖拽后,删除原来 位置上的item,其实就是将Adapter中该位置的内容给删掉??/???注意,该ListView用的Adapter是Arr ayAdapter,其有remove(Object?item)方法???但是,经常处理ListView显示数据的应该知道,Arr ayList的remove(object)方法实现了,???但是我们使用ArrayAdatper的时候,如果我们的数据传递到Ad apter用的是数组,那么ArrayAdapter内???默认使用Arrays.asList(Object[])方法将数组转换为 List对象,这样,当我们调用ArrayList的???removeItem方法的时候,就会出现java.lang.Unsupp ortedOperationException异常。这是因为???Arrays.asList(Object[])转换后的List 是Arrays.ArrayList对象,而不是java.utils.ArrayList???Arrays.ArrayList并没 有实现remove方法,所以,执行父类AbstractList默认的remove方法,而???AbstractList的remo ve方法就是抛出一个UnsupportedOperationException异常??/???removeItem(positi on);????mDragging?=?true;?}????return?true;???}????@SuppressWarni ngs("unchecked")???private?void?removeItem(int?position){???Array Adapter |
|