Launcher3界面中比较复杂的UI就是folder了。3个与之相关的类就是FolderIcon.java, Folder.java, FolderInfo.java,FolderPageView.java。大致思路就是mv模式:典型的数据驱动界面。
一、数据加载
1.1 加载逻辑时序图
1.1.1 主要流程
这边的逻辑和我之前Android Launcher7.0首次数据加载逻辑写的逻辑类似,主要方法对象组装方法还是在4方法loadWorkSpace
(1)解析ItemType为ITEM_TYPE_APPLICATION,ITEM_TYPE_SHORTCUT,ITEM_TYPE_DEEP_SHORTCUT,container不为DESKTOP | HOTSEAT的数据。
(2)根据container有无在集合sBgFolders找到FolderInfo或者创建一个FolderInfo加入到sBgFolders。
(3)将相同container的shortcutInfo加入到folderInfo。
(4)更新folderInfo位置信息,检查信息正确性,加入到全局变量中。
private void loadWorkspace() {
switch (itemType) {
case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION:
case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT:
case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT:
switch (container) {
default:
// Item is in a user folder
FolderInfo folderInfo =
findOrMakeFolder(sBgFolders, container);
folderInfo.add(info, false);
break;
}
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
id = c.getLong(idIndex);
FolderInfo folderInfo = findOrMakeFolder(sBgFolders, id);
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(cursorIconInfo.titleIndex);
folderInfo.id = id;
folderInfo.container = container;
folderInfo.screenId = c.getInt(screenIndex);
folderInfo.cellX = c.getInt(cellXIndex);
folderInfo.cellY = c.getInt(cellYIndex);
folderInfo.spanX = 1;
folderInfo.spanY = 1;
folderInfo.options = c.getInt(optionsIndex);
// check & update map of what's occupied
if (!checkItemPlacement(occupied, folderInfo, sBgWorkspaceScreens)) {
itemsToRemove.add(id);
break;
}
switch (container) {
case LauncherSettings.Favorites.CONTAINER_DESKTOP:
case LauncherSettings.Favorites.CONTAINER_HOTSEAT:
sBgWorkspaceItems.add(folderInfo);
break;
}
if (restored) {
// no special handling required for restored folders
restoredRows.add(id);
}
sBgItemsIdMap.put(folderInfo.id, folderInfo);
sBgFolders.put(folderInfo.id, folderInfo);
break;
}
二、界面绑定
2.1 UI示例图
2.2 文件夹创建时序图
加载逻辑时序图7方法bindItems中如果当前info属性itemType为LauncherSettings.Favorites.ITEM_TYPE_FOLDER,就会准备执行此时序图。
2.2.1 流程介绍
2.2.1.1(1~3 ) FolderIcon布局生成,文件夹名,位置确认。 Folder布局生成。
方法1 流程介绍
(1)创建folderIcon对象
(2)绘制文件夹名称
(3)创建folder对象
public static FolderIcon fromXml(int resId, Launcher launcher, ViewGroup group,
FolderInfo folderInfo, IconCache iconCache) {
DeviceProfile grid = launcher.getDeviceProfile();
//create FolderIcon
FolderIcon icon = (FolderIcon) LayoutInflater.from(launcher).inflate(resId, group, false);
icon.setLayerType(LAYER_TYPE_SOFTWARE, new Paint(Paint.FILTER_BITMAP_FLAG));
icon.setClipToPadding(false);
//draw bubbleTextView
icon.mFolderName = (BubbleTextView) icon.findViewById(R.id.folder_icon_name);
icon.mFolderName.setText(folderInfo.title);
icon.mFolderName.setCompoundDrawablePadding(0);
FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) icon.mFolderName.getLayoutParams();
lp.topMargin = grid.iconSizePx + grid.iconDrawablePaddingPx;
//init icon
icon.setTag(folderInfo);
icon.setOnClickListener(launcher);
icon.mInfo = folderInfo;
icon.mLauncher = launcher;
icon.setContentDescription(launcher.getString(R.string.folder_name_format, folderInfo.title));
//create Folder
Folder folder = Folder.fromXml(launcher);
folder.setDragController(launcher.getDragController());
folder.setFolderIcon(icon);
folder.bind(folderInfo);
//invalidate FolderIcon
icon.setFolder(folder);
icon.setAccessibilityDelegate(launcher.getAccessibilityDelegate());
folderInfo.addListener(icon);
icon.setOnFocusChangeListener(launcher.mFocusHandler);
return icon;
}
2.2.1.2(4~8 ) FolderPageView界面绑定数据。
方法4流程介绍
(1)获取folderInfo里面的shortcutInfo集合contents,比较器由小到大排序。执行方法5。
(2)这边的逻辑也很有意思,如果你的文件夹设置无法滚动,那么超过文件夹最大页面个数我就不加载了,并且通知数据库干掉他。
(3)检查逻辑:界面绑定结束如果发现里面的item个数不超过1个,就干掉文件夹,创建BubbleTextView对象
void bind(FolderInfo info) {
(1)
mInfo = info;
ArrayList<ShortcutInfo> children = info.contents;
Collections.sort(children, ITEM_POS_COMPARATOR);
(2)
ArrayList<ShortcutInfo> overflow = mContent.bindItems(children);
// If our folder has too many items we prune them from the list. This is an issue
// when upgrading from the old Folders implementation which could contain an unlimited
// number of items.
// TODO: Remove this, as with multi-page folders, there will never be any overflow
for (ShortcutInfo item: overflow) {
mInfo.remove(item, false);
LauncherModel.deleteItemFromDatabase(mLauncher, item);
}
(3)
// In case any children didn't come across during loading, clean up the folder accordingly
mFolderIcon.post(new Runnable() {
public void run() {
if (getItemCount() <= 1) {
replaceFolderWithFinalItem();
}
}
});
}
方法5流程介绍
(1)调用方法6创建BubbleTextView对象,存入集合icons,调用方法7
public ArrayList<ShortcutInfo> bindItems(ArrayList<ShortcutInfo> items) {
ArrayList<View> icons = new ArrayList<View>();
ArrayList<ShortcutInfo> extra = new ArrayList<ShortcutInfo>();
for (ShortcutInfo item : items) {
if (!ALLOW_FOLDER_SCROLL && icons.size() >= mMaxItemsPerPage) {
extra.add(item);
} else {
icons.add(createNewView(item));
}
}
arrangeChildren(icons, icons.size(), false);
return extra;
}
方法7流程介绍
(1)遍历已有页CellLayout,移除里面所有子View,等待重新绑定子View。
(2)根据item个数生成对应网格布局。
(3)遍历item对象,如果currentPage为空或者当前的position大于页面最大item个数,就要去获取cellLayout,如果集合没有就去生成。(google真的考虑性能极致化,我们写完全可以直接干掉FolderPageView里面所有的子View,然后每次生成新的cellLayout。然后在接着绑定数据,然后根本就不需要做4的逻辑)。
(4)根据position生成横纵坐标点,绑定布局。
(5)万一以有的比新生成的页数多,这里就会删除多余的空页。
(6)更新页面指示器位置,文件夹名称位置。
private void arrangeChildren(ArrayList<View> list, int itemCount, boolean saveChanges) {
(1)
ArrayList<CellLayout> pages = new ArrayList<CellLayout>();
for (int i = 0; i < getChildCount(); i++) {
CellLayout page = (CellLayout) getChildAt(i);
page.removeAllViews();
pages.add(page);
}
(2)
setupContentDimensions(itemCount);
(3)
Iterator<CellLayout> pageItr = pages.iterator();
CellLayout currentPage = null;
int position = 0;
int newX, newY, rank;
rank = 0;
for (int i = 0; i < itemCount; i++) {
View v = list.size() > i ? list.get(i) : null;
if (currentPage == null || position >= mMaxItemsPerPage) {
// Next page
if (pageItr.hasNext()) {
currentPage = pageItr.next();
} else {
currentPage = createAndAddNewPage();
}
position = 0;
}
(4)
if (v != null) {
CellLayout.LayoutParams lp = (CellLayout.LayoutParams) v.getLayoutParams();
newX = position % mGridCountX;
newY = position / mGridCountX;
ItemInfo info = (ItemInfo) v.getTag();
if (info.cellX != newX || info.cellY != newY || info.rank != rank) {
info.cellX = newX;
info.cellY = newY;
info.rank = rank;
if (saveChanges) {
LauncherModel.addOrMoveItemInDatabase(getContext(), info,
mFolder.mInfo.id, 0, info.cellX, info.cellY);
}
}
lp.cellX = info.cellX;
lp.cellY = info.cellY;
currentPage.addViewToCellLayout(
v, -1, mFolder.mLauncher.getViewIdForItem(info), lp, true);
if (rank < FolderIcon.NUM_ITEMS_IN_PREVIEW && v instanceof BubbleTextView) {
((BubbleTextView) v).verifyHighRes();
}
}
rank ++;
position++;
}
(5)
// Remove extra views.
boolean removed = false;
while (pageItr.hasNext()) {
removeView(pageItr.next());
removed = true;
}
if (removed) {
setCurrentPage(0);
}
setEnableOverscroll(getPageCount() > 1);
(6)
// Update footer
mPageIndicator.setVisibility(getPageCount() > 1 ? View.VISIBLE : View.GONE);
// Set the gravity as LEFT or RIGHT instead of START, as START depends on the actual text.
mFolder.mFolderName.setGravity(getPageCount() > 1 ?
(mIsRtl ? Gravity.RIGHT : Gravity.LEFT) : Gravity.CENTER_HORIZONTAL);
}
2.2.1.3(9~11) FolderIcon图标绘制数据准备。
方法10流程介绍
方法10和方法13作用一样(但是我不了解google这样写的意图是什么,完全可以直接在13方法完成逻辑,但是他拆成2块)。就是要确认folderIcon上面所有小icon的左顶点坐标。但是方法10缺少15,16两个方法,所以无法计算真实大小,确认icon位置。
10方法仅只有如下作用:
(1)确认绘制icon个数。
(2)创建PreviewItemDrawingParams对象添加到全局变量集合mDrawingParams中。
(3)给mReferenceDrawable。
private void updateItemDrawingParams(boolean animate) {
(1)
ArrayList<View> items = mFolder.getItemsInReadingOrder();
int nItemsInPreview = Math.min(items.size(), mPreviewLayoutRule.numItems());
int prevNumItems = mDrawingParams.size();
// We adjust the size of the list to match the number of items in the preview
while (nItemsInPreview < mDrawingParams.size()) {
mDrawingParams.remove(mDrawingParams.size() - 1);
}
(2)
while (nItemsInPreview > mDrawingParams.size()) {
mDrawingParams.add(new PreviewItemDrawingParams(0, 0, 0, 0));
}
(3)
for (int i = 0; i < mDrawingParams.size(); i++) {
PreviewItemDrawingParams p = mDrawingParams.get(i);
p.drawable = getTopDrawable((TextView) items.get(i));
if (!animate || FeatureFlags.LAUNCHER3_LEGACY_FOLDER_ICON) {
computePreviewItemDrawingParams(i, nItemsInPreview, p);
if (mReferenceDrawable == null) {
mReferenceDrawable = p.drawable;
}
}
}
}
2.2.1.3(12~24) FolderIcon图标绘制。
(12 )绘制阶段入口。
(13~20)计算所有小icon的位置。
方法20流程介绍
这个是这段方法核心逻辑。没法删他的一点代码,我可以根据他的代码得到数值,但是让我用自己的数学能力,我觉得我写不出来这么漂亮的代码。可能用策略模式,2个图标一个逻辑,3个图标一个逻辑,4个图标一个逻辑。来自学霸对学渣的碾压。
private void getPosition(int index, int curNumItems, float[] result) {
// The case of two items is homomorphic to the case of one.
curNumItems = Math.max(curNumItems, 2);
// We model the preview as a circle of items starting in the appropriate piece of the
// upper left quadrant (to achieve horizontal and vertical symmetry).
double theta0 = mIsRtl ? 0 : Math.PI;
// In RTL we go counterclockwise
int direction = mIsRtl ? 1 : -1;
double thetaShift = 0;
if (curNumItems == 3) {
thetaShift = Math.PI / 6;
} else if (curNumItems == 4) {
thetaShift = Math.PI / 4;
}
theta0 += direction * thetaShift;
// We want the items to appear in reading order. For the case of 1, 2 and 3 items, this
// is natural for the circular model. With 4 items, however, we need to swap the 3rd and
// 4th indices to achieve reading order.
if (curNumItems == 4 && index == 3) {
index = 2;
} else if (curNumItems == 4 && index == 2) {
index = 3;
}
// We bump the radius up between 0 and MAX_RADIUS_DILATION % as the number of items increasex
float radius = mRadius * (1 + MAX_RADIUS_DILATION * (curNumItems -
MIN_NUM_ITEMS_IN_PREVIEW) / (MAX_NUM_ITEMS_IN_PREVIEW - MIN_NUM_ITEMS_IN_PREVIEW));
double theta = theta0 + index * (2 * Math.PI / curNumItems) * direction;
float halfIconSize = (mIconSize * scaleForNumItems(curNumItems)) / 2;
// Map the location along the circle, and offset the coordinates to represent the center
// of the icon, and to be based from the top / left of the preview area. The y component
// is inverted to match the coordinate system.
result[0] = mAvailableSpace / 2 + (float) (radius * Math.cos(theta) / 2) - halfIconSize;
result[1] = mAvailableSpace / 2 + (float) (- radius * Math.sin(theta) / 2) - halfIconSize;
}
以2个小icon为例:在方法12dispatchDraw中有这一句
canvas.translate(mBackground.basePreviewOffsetX, mBackground.basePreviewOffsetY);
这个是平移到文件夹背景的左顶点,小图标的绘制是文件夹背景的左顶点为坐标原点,向右为X轴正半轴,向下为Y轴正半轴建立坐标系。如下图黑色线坐标系。
接下来计算绘制每个图标的左顶点坐标。图标索引是从左往右,从上往下排序。这里看一下方法20流程介绍getPosition最后一个Google注释。他是以icon中心点为坐标系原点,向右为X轴正半轴,向上为Y轴正半轴建立坐标系,如下图红色线部分。
根据如上图通过极坐标方式得到图标中心点坐标,接着换算成图片左顶点坐标。存入对应对象中以供绘制对象调用。
(21 )绘制文件夹背景
(22 )根据文件背景剪裁画布,确立文件夹背景为可是区域
(23 )绘制item
(24 )绘制白色边框
这边最复杂的就是位置的确认,后面绘制没什么难度,也就不介绍了。