Android系统启动后,加载的第一个程序就是Launcher应用。
Launcher的构
成:HomeScreen(workspace(AppWidget、WallPaper、LiveFolder、ShortCut))、HotSeats、AllApps/AllApplist:GridView
这是launcher的主界面,里面有一些应用的图标,可以点击图标来启动该应用。默认的情况下,主界面由五个屏组成,每一个屏都可以放置多个程序的图标。可以用手指按住屏幕在五个屏之间来回拖动,也可以快速地滑动来快速切换不同的屏。主界面主要关注UI是如何生成的,五个屏来回滑动是如何实现的,小图标的拖放是如何实现的。
点击顶部中间的格子图标后进入程序管理器,如下图所示。
程序管理器列出了系统已经安装的所有应用和小部件,可以点击图标来启动应用。长按住小图标一会,可以拖动小图标回到主界面并放置在主界面作为快捷方式。程序管理器主要关注UI是如何生成的,小图标的拖放是如何实现的,拖动画面时的动画是如何实现的。
系统框图如下:
Android原生为sw600dp和sw720dp提供了两种不同的home界面布局。
sw600dp的hotseat放置在右侧,sw720dp的hotseat放置在底部。
Hotseat水平放置和垂直放置是可以调整的,在
packages/apps/Launcher2/res/layout-land/hotseat.xml中有如下属性:
launcher:cellCountX="1"
launcher:cellCountY="@integer/hotseat_cell_count">
packages/apps/Launcher2/res/layout-port/hotseat.xml里是:
launcher:cellCountX="@integer/hotseat_cell_count"
launcher:cellCountY="1">
即在横屏时hotseat是垂直放置的,竖屏时是水平放置的。
因为两个分辨率下的布局不同,所以对应的value也有很大差别,如果想统一界面显示,目前的方法是把某一分辨率的布局文件和value文件都删除掉,用另一个的文件代替,这样修改工作会少很多。
因为Launcher不同分辨率的布局文件中的数值都是用dimen或者config设置,所以在做sw600dp和sw480dp适配的时候,只需要修改values-sw480dp、values-sw480dp-land以及values-sw480dp-port目录下的文件就可以了。
dimen.xml里面的参数不一一解释了,因为从名字上面就能够看出是什么布局的数值,例如<dimen name="hotseat_cell_width">60dp</dimen>就是hotseat中每个图标的width,<dimen name="apps_customize_cell_width">76dp</dimen>应用列表界面中每个应用的width。
Launcher应用的启动activity是Launcher.java中的Launcher,其中的onCreate( )函数作为应用的起始点。根据其中的setContentView(R.layout.launcher); 可以看出应用的UI布局从launcher.xml开始。launcher.xml中包括有:
workspace ―― 主界面。
apps_customize_pane ―― 程序管理器。
qsb_bar ―― 上文中的图1顶部的搜索条。
workspace_cling ―― 是提示可以将应用拖动到主界面的提示画面。
folder_cling ―― 是提示可以将应用程序放到一个文件夹里面的提示画面。
Workspace是Launcher的主界面,布局由workspace.xml设置:
<com.android.launcher2.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingStart="@dimen/workspace_left_padding"
android:paddingEnd="@dimen/workspace_right_padding"
android:paddingTop="@dimen/workspace_top_padding"
android:paddingBottom="@dimen/workspace_bottom_padding"
launcher:defaultScreen="2"
launcher:cellCountX="@integer/cell_count_x"
launcher:cellCountY="@integer/cell_count_y"
launcher:pageSpacing="@dimen/workspace_page_spacing"
launcher:scrollIndicatorPaddingLeft="@dimen/qsb_bar_height"
launcher:scrollIndicatorPaddingRight="@dimen/button_bar_height">
<include android:id="@+id/cell1" layout="@layout/workspace_screen" />
<include android:id="@+id/cell2" layout="@layout/workspace_screen" />
<include android:id="@+id/cell3" layout="@layout/workspace_screen" />
<include android:id="@+id/cell4" layout="@layout/workspace_screen" />
<include android:id="@+id/cell5" layout="@layout/workspace_screen" />
</com.android.launcher2.Workspace>
由上可以看出,workspace(继承PagedView、ViewGroup)由五个workspace_screen(实际是CellLayout, ViewGroup)组成,实现五个可以来回拖动的页面,缺省显示的是第二页。背景图是系统的WallPaper,与launcher无关。
在A20-android4.4中,home默认有5个屏,编号从左到右依次为0-4。这样五个页面就按顺序在水平方向并排地放置。
PagedView通过重写onMeasure()方法来设置每个页面的宽高,重写onLayout( )方法来设置五个页面的坐标。
PagedView.onMeasure() ->CellLayout.onMeasure():
newWidth = mPaddingLeft + mPaddingRight + (mCountX * mCellWidth)
+ ((mCountX - 1) * mWidthGap);
newHeight = mPaddingTop + mPaddingBottom + (mCountY * mCellHeight)
+ ((mCountY - 1) * mHeightGap);
setMeasuredDimension(newWidth, newHeight); //设置每一页的宽高
PagedView.onLayout():
for (int i = 0; i < childCount; i++) //将五个页面指定到相应的坐标。
{
final View child = getPageAt(i);
if (child.getVisibility() != View.GONE)
{
final int childWidth = getScaledMeasuredWidth(child);
final int childHeight = child.getMeasuredHeight();
int childTop = mPaddingTop;
child.layout(childLeft, childTop,
childLeft + child.getMeasuredWidth(), childTop
+ childHeight); //指定每一页的坐标
childLeft += childWidth + mPageSpacing;
}
}
home显示的默认应用在xml-swXXXdp/default_workspace.xml中设置。
<appwidget/>表示小部件,<favorite/>快捷图标,<search/>搜索栏,<folder/>表示文件夹。
1.<appwidget/>的属性如下:
<appwidget
launcher:packageName="com.android.settings"
launcher:className="com.android.settings.widget.SettingsAppWidgetProvider"
launcher:screen="1"
launcher:x="2"
launcher:y="1"
launcher:spanX="4"
launcher:spanY="1" />
launcher:packageName、launcher:className指定启动对应的包名和类名;如果不知道没有源码的第三方应用的包名和类名,可以启动应用的时候打印logcat就能看到了。
launcher:screen指定显示的第几个屏幕。
launcher:x、launcher:y显示的坐标,即在X/Y轴上的位置,values-swXXXdp目录下的config.xml定义了每个屏幕横竖显示多少个网格,参数为 <integer name="cell_count_x">6</integer>和<integer name="cell_count_y">6</integer>,launcher:x、launcher:y指的就是在这些网格中的位置。在X轴上是向右从0开始依次增加,在Y轴上是向下从0开始依次增加。
launcher:spanX和launcher:spanY是小部件特有的属性,指定小部件在X/Y轴上的占用网格的个数。
2.<favorite/>
<favorite/>也有特定的属性:launcher:container="-101",如果定义了这个属性,那么该<favorite/>就会放置在hotseat中,而 launcher:screen、launcher:x和launcher:y 三个属性的意义就会改变。
首先需要知道在config.xml中定义了HotSeat的图标个数<integer name="hotseat_cell_count">X</integer>,编号从左到右或者从下到上依次为0到X-1。上面所说的launcher:screen和launcher:x 需要等于相同的值x,表示按钮的位置,0表示第一个,launcher:y需要等于0,那么该<favorite/>就会放置在hotseat的第x-1的位置上。Hotseat的中间位置是放置appcutoms的按钮,<favorite/>放置在那里不会有效果。
3.<folder/>
在<folder/>内部可以添加不同<favorite/>,这些<favorite/>不需要定义坐标等,只要定义包名和类名可以。根据GMS要求,就需要在桌面的添加这样一个<folder/>,在这个<folder/>里面有所有google的<favorite/>。
图标可放置图标的位置并不是随意的,而是在每一页中一个固定的表格之内,在拖动小图标时才会显示该表格。
在workspace的构造函数中,计算该表格的行数和列数:
final float actionBarHeight = actionBarSizeTypedArray.getDimension(0, 0f);
Point minDims = new Point();
Point maxDims = new Point();
mLauncher.getWindowManager().getDefaultDisplay().getCurrentSizeRange(minDims, maxDims);
cellCountX = 1;
//cellCountX初始化为1,然后通过一个while循环去计算X、Y方向各能放多少个应用
while (CellLayout.widthInPortrait(res, cellCountX + 1) <= minDims.x) {
cellCountX++;
}
cellCountY = 1;
while (actionBarHeight + CellLayout.heightInLandscape(res, cellCountY + 1)
<= minDims.y) {
cellCountY++;
}
}
我们看看widthInPortrait方法:
static int widthInPortrait(Resources r, int numCells) {
//We use this method from Workspace to figure out how many rows/columns Launcher should
// have. We ignore the left/right padding on CellLayout because it turns out in our design
// the padding extends outside the visible screen size, but it looked fine anyway.
int cellWidth = r.getDimensionPixelSize(R.dimen.workspace_cell_width);
int minGap = Math.min(r.getDimensionPixelSize(R.dimen.workspace_width_gap),
r.getDimensionPixelSize(R.dimen.workspace_height_gap));
return minGap * (numCells - 1) + cellWidth * numCells;
}
getDimensionPixelSize方法实际上是把dimens.xml中设置的cellWidth大小由dp转换成px!
widthInPortrait计算后的返回值会与smallestScreenDim进行比较,如果比smallestScreenDim 小,那么说明能够再放一个APP,cellCount进行加一。循环比较之后就会得出在该屏幕上能放多少行,多少列个应用。
大致如下图的虚线表格:
配置AllAPP应用列表界面的配置文件是\res\Layout\apps_customize_pane.xml文件。AllAPP列表使用了一个TabHost组织了两个页面(全部应用和Widget),通过界面上面的TabHost进行切换。下面对部分属性进行分析:
android:id="@+id/tabs_container" //TabHost栏,可以配置TabHost栏的高度和宽度
android:id="@android:id/tabs" //TabHost上面widget的按钮
android:id="@+id/market_button" //TabHost右边的Android市场的图标,不需要可以去掉
<!--下面这里就是我们所有应用列表的选项和所有应用列表的显示View,需要注意的是AppsCustomizePagedView同时支持显示所有应用列表和Widget列表 -->
<FrameLayout
android:id="@android:id/tabcontent"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 所有应用列表是通过自定义VIewAppsCustomizePagedView显示,后面会详细分析这个View下面只对部分重要属性加入注释 -->
<com.android.launcher2.AppsCustomizePagedView
android:id="@+id/apps_customize_pane_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
//MaxAppCellCountX 和MaxAppCellCounY指的是所有App图标排列的最大行列数。
//一般设置为-1,表示无限制
launcher:maxAppCellCountX="@integer/apps_customize_maxCellCountX" launcher:maxAppCellCountY="@integer/apps_customize_maxCellCountY"
//pageLayoutWidthGap和pageLayoutHeightGap分别表示菜单界面与屏幕边缘的距离,
//一般小屏幕这里设置为-1。避免边框太窄误触屏幕才需要设置。 launcher:pageLayoutWidthGap="@dimen/apps_customize_pageLayoutWidthGap" launcher:pageLayoutHeightGap="@dimen/apps_customize_pageLayoutHeightGap"
//pageLayoutPaddingXXX指的是内填充,这个和系统的padding一样 launcher:pageLayoutPaddingTop="@dimen/apps_customize_pageLayoutPaddingTop" launcher:pageLayoutPaddingBottom="@dimen/apps_customize_pageLayoutPaddingBottom" launcher:pageLayoutPaddingLeft="@dimen/apps_customize_pageLayoutPaddingLeft" launcher:pageLayoutPaddingRight="@dimen/apps_customize_pageLayoutPaddingRight"
//widgetCellWithGap和widgetCellHeightGap指的是widget列表界面各个widget之间的间隔,
//和系统的margin属性类似
launcher:widgetCellWidthGap="@dimen/apps_customize_widget_cell_width_gap"
launcher:widgetCellHeightGap="@dimen/apps_customize_widget_cell_height_gap"
//widgetCountX和WidgetCountY都是表示Widget界面每行每列显示多少Widget
launcher:widgetCountX="@integer/apps_customize_widget_cell_count_x" launcher:widgetCountY="@integer/apps_customize_widget_cell_count_y"
//提示界面的焦点
launcher:clingFocusedX="@integer/apps_customize_cling_focused_x"
launcher:clingFocusedY="@integer/apps_customize_cling_focused_y"
launcher:maxGap="@dimen/workspace_max_gap" />
<!-- 加载全部应用时的旋转动画 -->
<FrameLayout
android:id="@+id/animation_buffer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#FF000000"
android:visibility="gone" />
<!-- 分页符,代表多少页和当前页面-->
<include
android:id="@+id/paged_view_indicator"
layout="@layout/scroll_indicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom" />
</FrameLayout>
</LinearLayout>
<!--第一次进入所有应用列表的提示界面,和workspace提示界面一样-->
<include layout="@layout/all_apps_cling"
android:id="@+id/all_apps_cling"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
</com.android.launcher2.AppsCustomizeTabHost>
图标下面的名称行数也可以通过修改res/values/styles.xml来调整:
<item name="android:singleLine">false</item>
<item name="android:maxLines">2</item>
这两行代码可以进行修改。
res/values/config.xml文件定义了workspace到appsCustomize两界面切换时的动画参数,如下:
config_appsCustomizeSpringLoadedBgAlpha 进入appcustomize界面背景的透明度
config_workspaceUnshrinkTime home键返回时,workspace的扩张动画时间
config_appsCustomizeWorkspaceShrinkTime 返回workspace的收缩动画时间
config_workspaceSpringLoadShrinkPercentage 把应用拖放到主界面的时候,显示主界面边框的大小
config_appsCustomizeZoomInTime/ config_appsCustomizeZoomOutTime 进入/退出appsCustomize的扩张收缩动画时间
config_appsCustomizeFadeInTime/ config_appsCustomizeFadeOutTime 进入/退出appsCustomize的透明度改变时间
config_appsCustomizeWorkspaceAnimationStagger/ config_workspaceAppsCustomizeAnimationStagger 进入/退出workspace时桌面上图标的移动时间
PagedView.java里面定义翻页速度MAX_PAGE_SNAP_DURATION。