http://www.cnblogs.com/Sunde/archive/2010/11/29/1890797.html
http://www.cnblogs.com/Sunde/archive/2010/11/29/1891023.html
http://www.cnblogs.com/Sunde/archive/2010/11/29/1891488.html
Android laucher总结
在android上做了一个月左右的launcher方面的项目,伴随着R3的发出,基本进入维护阶段,由于大部分时间我都是在android提供的源码Launcher中做修改,所以对它的整体基本有了概念,现在开始做一些细节上的总结,按照国际惯例和从整体到局部的原则,我还是先对Launcher做一个整体上的描述:
从manifestl开始看起,先是一些权限方面的设置(补充加上:<uses-sdk android:minSdkVersion="7" />),而后即是一个application(补充加上:android:debuggable="true"),接着是两个activity,一个就是我们的主程序Launcher了,另一个从名字上看也很清晰WallpaperChooser(用于查找列出所有带有特定标记名称的图片,并将他们的返回值作为背景图 ---> 我对这里进行了修改,通过startActivityForResult函数来让他们返回resId,以此作为All Progaram Page的背景)。
再往下是两个Receiver用于安装和卸载shortcut,这里从名字上看我们不太清楚这里的shortcut到底指什么,经过我的尝试,这里的shortcut主要并不是指那些application的icon,而是如:打开浏览器,创建一个网页的快捷方式到桌面。这里的快捷方式的创建和卸载便是由这两个receiver来控制,从他们对应的Java文件可以看出功能。要注意的是,当manifest中做了receiver的配置时,那么程序启动时,receiver就已经伴随运行了。
最后一部分是一个provider,熟悉android的朋友都知道这是用于管理数据库,而在Launcher中,有专门的一个类LauncherProvider用于提供对数据库的具体操作,而数据库的创建和获取还是要由SQLiteOpenHelper来完成,这一部分我会在后面慢慢加入。好,现在对manifest的部分已经基本总结完毕,接着来看main.xml(launcher.xml):
它的层次关系非常易读:DragLayer ---> WorkSpace ---> CellLayout
---> DeleteZone
---> MenuBar
---> ProgramPage
DragLayer对所有位于其中的DragTarget的移动、绘制进行拦截处理(主要是针对屏幕上的Widget、Shortcut、Folder,因为我这里是不存在Shortcut和Folder的,所以只针对Widget)。
WorkSpace则是一个很关键的类,主要是处理屏幕的切换中相关操作(如壁纸的移动,计算移动距离等)、为当前屏幕添加Widget的处理、长按当前屏幕的处理(这里的setLongclick关系到Launcher等类)和在当前屏幕上查找空白格子用于添加Widget,它包含了5个CellLayout分别代表5个屏幕,CellLayout中定义了横竖轴各有多少个空格,并用数组保存了所有的空白格子,当占用一个后就将之对应的Occupied数组位置为true。当然CellLayout中还有许多其他的操作,这里我们先大概说明一下就好。
MenuBar这个就很简单了,由于项目需要,我在屏幕下方添加了MenuBar主要用于添加Shortcut,那么它的操作当然主要就是处理图标的添加和移动,保存和删除。
ProgramPage用于显示所有程序并可以进行分类保存,同样移动、添加、切换、保存、删除。这个ViewGroup其实是包括两部分,一个是ProgramCatagoryList,一个是AllProgramPage。
一个Catagory对应一个AllProagramPage。
这里我已经对manifest和main.xml(launcher.xml)分析完毕,后面的内容则开始对细节上的问题做详细的阐述。
Android launcher - Launcher(一)
Launcher这个类乍一看,好多函数好多变量好像很复杂,不急,这需要慢慢的梳理。
从最上面的onCreate函数开始看起:
在这里为了调用framework那层中自己添加的一个类,所以使用了反射技术。
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
Method method = null; Class myclass = null; myclass = Class.forName("android.appwidget.SPAppWidgetHost"); //查找指定类名的类 Class[] paramtersList = {Context.class, Integer.TYPE}; //构造函数参数列表 java.lang.reflect.Constructor constructor = myclass.getConstructor(paramtersList);//根据参数列表获得构造函数 Object[] arg= new Object[] { this, Integer.valueOf(APPWIDGET_HOST_ID)};//为构造函数写好参数 mObject = constructor.newInstance(arg); //调用构造函数获得类实例 mAppWidgetHost = (AppWidgetHost) mObject; //基类转换 if (myclass != null) { DBLog.d(TAG, "android.appwidget.SPAppWidgetHost found"); method = myclass.getDeclaredMethod("startListening", null); //根据函数名获得函数实例 method.invoke(mObject, null); //调用函数
上述技术主要是为了监听Widget状态变化,用于更新Widget状态。
技术点 如何设置屏幕虚拟尺寸?
1、通过getSystemService获得wallpaperManager的实例;
2、调用suggestDesiredDimensions,设定横竖轴虚拟宽度;Launcher中,设横为2倍屏幕宽,竖屏幕尺寸。
附:getWindowManager().getDefaultDisplay()获得屏幕的尺寸。
技术点 如何检测安装、卸载应用程序并进行处理?
Receiver是最好的解决办法,它可以对自己感兴趣的Intent作出响应反应(在OnReceive函数中,可通过判断Intent的Action进行选择 --- 如果可以检测多种Intent)。
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); filter.addAction(Intent.ACTION_PACKAGE_REMOVED); filter.addAction(Intent.ACTION_PACKAGE_CHANGED); filter.addDataScheme("package"); registerReceiver(mApplicationsReceiver, filter); // 看上面就是在注册Receiver,并加入它所感兴趣的Action(能作出响应的Action) filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS); registerReceiver(mCloseSystemDialogsReceiver, filter);
技术点 如何检测数据库的变化?
ContentObserver正可用于这项工作,它对你所感兴趣的URI(数据库地址)进行检测,在重写的onChange函数中进行处理(最多用于更新UI)。
当对数据库进行增删查等操作后,可以根据我们自己设定的flag来判断是否需要通知Observer来对我们做出的修改发生响应(这里的响应不是指数据库的具体操作响应,而很可能是UI上的表现),如果需要则调用getContext().getContentResolver().notifyChange(uri, null);
这里的ContentResolver为应用程序提供了访问数据库模型的实例。
注册Observer的操作看代码:
ContentResolver resolver = getContentResolver(); //获取“当前”数据库实例,Provider中定义了它的操作如insert, delete, update...resolver.registerContentObserver(LauncherSettings.Favorites.CONTENT_URI,true, mObserver);
做完一系列操作后,进入到程序的加载阶段,下次开始分析startLoader。
Android laucher - Launcher(二)
从startLoader开始说起,从函数命名可以看出,用于在启动过程中加载程序所需品,那么它具体做了哪些操作,还是展开阐述一下。
主要加载步骤分为两个:loadUserItems loadApplications
对于这里loadApplications中的具体操作,我尚有许多不解之处。
Android launcher(三)- Workspace
废话就不多说,Workspace的大体功能前面已有简介,现在具体说其中的技术点。
技术点 如何通过xml文件来构造自己定义的View组件?
在通过xml文件构造view组件的时候,往往都要使用到AttributeSet和defStyle这个两个参数。defStyleAttr是一个reference, 它指向当前Theme中的一个style, style其实就是各种属性的集合。
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace,defStyle, 0); // style就是各种属性的数组集合 mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 2); // 通过styleable name + _attr name得到属性值,如果没有定义则默认为2 a.recycle();
技术点 如何保证自定义的ViewGroup如Workspace按照我们想的那样横向显示3个(5个)屏幕?
要知道这一点我们必须先知道一个View是如何显示出来的,在显示View之前,我们应该要为其指定尺寸和分布,这些是在onMeasure和onLayout中调用完成,废话不多说,上代码:
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); final int width = MeasureSpec.getSize(widthMeasureSpec); final int widthMode = MeasureSpec.getMode(widthMeasureSpec); // 仅当ViewGroup为fill_parent才处于EXACTLY模式if (widthMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Workspace can only be EXACTLY mode.");} final int heightMode = MeasureSpec.getMode(heightMeasureSpec); if (heightMode != MeasureSpec.EXACTLY) { throw new IllegalStateException("Workspace can only be EXACTLY mode.");} final int count = getChildCount(); for (int i = 0; i < count; i++) { getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);// 为所有子View设定标准尺寸 } if (mFirstLayout) { scrollTo(mCurrentScreen * width, 0);updateWallpaperOffset(width * (getChildCount() - 1)); mFirstLayout = false; }//滚动到第三屏设为默认主屏。 }
在尺寸设定完毕后,我们需要对ViewGroup中所有子View的位置进行设定,以满足他们的布局。
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
@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());//很明显是吧? 0 - 320 | 320 - 640 | 640 - 960 ...(假设屏幕宽320)childLeft += childWidth; } } }
布局设定OK。这样我们就了解到是如何为Workspace设定了屏幕数,并为他们做好布局、定好尺寸。(实际上在每次滑动结束后都会调用他们)下面我们需要知道的是具体的切屏操作是如何完成,在切屏时,又做了什么?
技术点 各屏幕被遮挡后如何重新获取焦点?
我们知道,View一般通过requestfocus获取焦点,当然前提是他们“能够”获得。通过复写addFocusables函数来为当前ViewGroup中的所有子View添加焦点获取能力 - -。然后当屏幕被遮盖又恢复后,ViewGroup会首先收到requestfocus消息,并在下列代码中作出相应处理:
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
@Override protected boolean onRequestFocusInDescendants(int direction,Rect previouslyFocusedRect) {int focusableScreen; if (mNextScreen != INVALID_SCREEN) { focusableScreen = mNextScreen; } else { focusableScreen = mCurrentScreen; } getChildAt(focusableScreen).requestFocus(direction, previouslyFocusedRect); return false; }应该是很清晰了,函数名称起的很好,即当前ViewGroup树形结构中的某个View获取焦点。条件判断主要是用于检测是否发生过切屏。
技术点 在发生切屏时,是如何进行绘制和切换屏幕的工作?
有细心的朋友肯定已经注意到,在onMeasue函数调用中有一个scrollTo函数,用于将屏幕移动到指定坐标位置。程序初次启动时,显示默认第三屏便是在这里做了滚动操作,它会首先获得焦点。由ViewGroup调用dispatchDraw()操作,进行绘制和draw动作的传递。这里补充一个知识,除非ViewGroup本身有背景,否则调用onDraw是无效的。
在dispatchDraw操作中,判断是否处于滑动状态,通过drawChild()来完成对当前主屏和滑动状态的下一主屏进行绘制。否则,当切屏完成后,屏幕上不会有Widget或Shortcut,但是点击他们的位置却能启动相应程序。
滑动屏幕时有两个个非常重要的函数即scrollTo、scrollBy,我看了很多地方都没有对他们解释的很清楚。scrollTo, scrollBy其实都是在对View的内容进行移动。在Touch Move中发生scrollBy后并不会去调用computeScroll(),反而在move结束,up事件中调用startSrcoll,开始不断调整mScrollX, mSrcollY,将当前的基准点重新置回标准的:0,0 320,0 640,0...
简单的说下整体的绘制操作:接受onTouchEvent事件,当在移动中计算移动的距离,调整基准点并不停的绘制屏幕和移动壁纸,当touch结束后,通过startScroll将滑动完成,并将基准点置为标准点(在computeScroll中),更新重绘屏幕。
这样,Workspace的主要工作已基本算是完成,还有一些它担任委托的工作就没有介绍了,应该不会很复杂,看看就好。