android launcher梳理

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那层中自己添加的一个类,所以使用了反射技术。

 

复制代码
代码
    
    
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)。

 

复制代码
代码
    
    
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其实就是各种属性的集合。

复制代码
代码
    
    
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中调用完成,废话不多说,上代码:

复制代码
代码
    
    
@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的位置进行设定,以满足他们的布局。

 

复制代码
代码
    
    
@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消息,并在下列代码中作出相应处理:

 

复制代码
代码
    
    
@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的主要工作已基本算是完成,还有一些它担任委托的工作就没有介绍了,应该不会很复杂,看看就好。


Android launcher(四) CellLayout类

workspace是有celllayout组成。Celllayout被划分为了4行4列的表格,用Boolean类型的mOccupied二维数组来标记每个cell是否被占用。在attrs.xml中定义了shortAxisCells和longAxisCells分别存储x轴和y轴方向的cell个数。在Celllayout构造函数中初始化。 
2) 内部类CellInfo为静态类,实现了ContextMenu.ContextMenuInfo接口。其对象用于存储cell的基本信息。 
 VacantCell类用于存储空闲的cell,用到了同步机制用于管理对空闲位置的操作。所有的空cell都存储在vacantCells中。 
 cellX和cellY用于记录cell的位置,起始位0。如:(0,0) (0,1),每一页从新开始编号。 
 clearVacantCells作用是将Vacant清空:具体是释放每个cell,将list清空。 
 findVacantCellsFromOccupied从存放cell的数值中找到空闲的cell。在Launcher.Java中的restoreState方法中调用。 
3) mPortrait用于标记是横屏还是竖屏,FALSE表示竖屏,默认为FALSE。 
4) 修改CellLayout页面上cell的布局: 
 CellLayout页面上默认的cell为4X4=16个,可以通过修改配置文件来达到修改目的。 
 在CellLayout.Java类的CellLayout(Context context, AttributeSet attrs, int defStyle)构造方法中用变量mShortAxisCells和mLongAxisCells存储行和列。 
 其值是在自定义配置文件attrs.xml中定义的,并在workspace_screen.xml中赋初值的,初值都为4,即4行、4列。可以在workspace_screen.xml修改对应的值。 
 注意:CellLayout构造方法中从attrs.xml中获取定义是这样的:mShortAxisCells = a.getInt(R.styleable.CellLayout_shortAxisCells, 4);当workspace_screen.xml中没有给定义的变量赋值时,上面的4就起作用。 
5)下面的分析转载自:http://blog.csdn.net/netpirate/archive/2009/06/05/4245445.aspx 
Launcher(主屏/待机) App的BUG: 没有初始化定义CellLayout中屏幕方向的布尔值参数 
Launcher App:\cupcake\packages\apps\Launcher 
待机画面分为多层,桌面Desktop Items在\res\layout-*\workspace_screen.xml中置: 
<com.android.launcher.CellLayout 
... ... 
    launcher:shortAxisCells="4" 
    launcher:longAxisCells="4" 
... ... 
/> 
表示4行4列 
再看看 com.android.launcher.CellLayout ,其中有定义屏幕方向的参数, 
private boolean mPortrait; 
但是一直没有初始化,也就是mPortrait=false,桌面的单元格设置一直是以非竖屏(横屏)的设置定义进行初始化。 
再来看看横屏和竖屏情况下的初始化不同之处,就可以看出BUG了。 
boolean[][] mOccupied;//二元单元格布尔值数组 
            if (mPortrait) { 
                mOccupied = new boolean[mShortAxisCells][mLongAxisCells]; 
            } else { 
                mOccupied = new boolean[mLongAxisCells][mShortAxisCells]; 
            } 
如果我们满屏显示桌面(横向和纵向的单元格数不一致),而不是默认的只显示4行4列,则mShortAxisCells = 4, mLongAxisCells = 5,数组应该初始化是:new boolean[4][5],但是实际是按照非竖屏处理,初始化成了new boolean[5][4],会产生数组越界异常。 
可以在构造函数中,添加通过屏幕方向初始化mPortrait,代码如下: 
public CellLayout(Context context, AttributeSet attrs, int defStyle) 

        super(context, attrs, defStyle); 
        mPortrait = this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;// 新增代码


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值