目录
- 前言
- 初始化
- 布局
- 页面初始化
- 桌面图标
- 图标生成
- 图标拖动
- 图标点击效果
- 页面滑动
前言
Workspace是桌面的主要一个部分,一般设备(如手机)启动起来所看到的桌面的主要界面就是Workspace,在Launcher里其继承关系如下:
Workspace->SmoothPagedView->PagedView->ViewGroup
所以可以说Workspace是一个视图容器类,容器里面主要放插件和应用快捷方式的图标。它负责桌面视图的布局工作,如桌面图标是多少行多少列;用户事件的分发与处理;桌面图标的拖放;子视图的更新等操作。本文简单解析一下Workspace的源码。
初始化
1.布局
1.1.xml布局
其布局在launcher.xml里如下:
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
launcher:defaultScreen="@integer/config_workspaceDefaultScreen" />
然后在Launcher.java的onCreate里调用setupViews初始化变量mWorkspace,以便调用Workspace里的一些方法和变量,初始化如下:
mDragLayer = (DragLayer) findViewById(R.id.drag_layer);
mWorkspace = (Workspace) mDragLayer.findViewById(R.id.workspace);
1.2.参数初始化
Workspace的真正Layout是在DeviceProfile.java里。首先在Launcher.java的onCreate里初始化DeviceProfile,然后调用其layout方法实现布局,如下:
.....
DeviceProfile grid = app.initDynamicGrid(this);
.....
grid.layout(this);
先看看DeviceProfile的初始化过程,调用的是LauncherAppState的initDynamicGrid方法,具体如下:
DeviceProfile initDynamicGrid(Context context) {
mDynamicGrid = createDynamicGrid(context, mDynamicGrid);
mDynamicGrid.getDeviceProfile().addCallback(this);
return mDynamicGrid.getDeviceProfile();
}
先调用createDynamicGrid得到mDynamicGrid对象,再通过mDynamicGrid来获取DeviceProfile对象。
LauncherAppState.java里的createDynamicGrid:
static DynamicGrid createDynamicGrid(Context context, DynamicGrid dynamicGrid) {
.....
if (dynamicGrid == null) {
Point smallestSize = new Point();
Point largestSize = new Point();
display.getCurrentSizeRange(smallestSize, largestSize);
dynamicGrid = new DynamicGrid(context,
context.getResources(),
Math.min(smallestSize.x, smallestSize.y),
Math.min(largestSize.x, largestSize.y),
realSize.x, realSize.y,
dm.widthPixels, dm.heightPixels);
}
.....
return dynamicGrid;
}
DynamicGrid.java的构造函数:
public DynamicGrid(Context context, Resources resources,
int minWidthPx, int minHeightPx,
int widthPx, int heightPx,
int awPx, int ahPx) {
DisplayMetrics dm = resources.getDisplayMetrics();
ArrayList<DeviceProfile> deviceProfiles =
new ArrayList<DeviceProfile>();
boolean hasAA = !LauncherAppState.isDisableAllApps();
DEFAULT_ICON_SIZE_PX = pxFromDp(DEFAULT_ICON_SIZE_DP, dm);
// Our phone profiles include the bar sizes in each orientation
deviceProfiles.add(new DeviceProfile("Super Short Stubby",
255, 300, 2, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4));
deviceProfiles.add(new DeviceProfile("Shorter Stubby",
255, 400, 3, 3, 48, 13, (hasAA ? 3 : 5), 48, R.xml.default_workspace_4x4));
......
mMinWidth = dpiFromPx(minWidthPx, dm);
mMinHeight = dpiFromPx(minHeightPx, dm);
mProfile = new DeviceProfile(context, deviceProfiles,
mMinWidth, mMinHeight,
widthPx, heightPx,
awPx, ahPx,
resources);
}
可以看到根据不同分辨率加载不同的默认布局文件,最后new DeviceProfile对象。
再看看DeviceProfile的构造函数,上面调用了DeviceProfile的两个构造函数,先看第一个:
DeviceProfile(String n, float w, float h, float r, float c,
float is, float its, float hs, float his, int dlId) {
// Ensure that we have an odd number of hotseat items (since we need to place all apps)
if (!LauncherAppState.isDisableAllApps() && hs % 2 == 0) {
throw new RuntimeException("All Device Profiles must have an odd number of hotseat spaces");
}
name = n;
minWidthDps = w;
minHeightDps = h;
numRows = r;
numColumns = c;
iconSize = is;
iconTextSize = its;
numHotseatIcons = hs;
hotseatIconSize = his;
defaultLayoutId = dlId;
}
这就是初始化一个页面的一些参数,其中numRows和numColumns对应的是桌面图标的排列分别是几行几列,iconSize就是图标的大小了,看这些变量名基本都能猜到是用来干嘛的了。
第二个构造函数则是根据当前设备找出最接近最合适的一个布局,最终确定一个页面的具体参数。
DeviceProfile(Context context,
ArrayList<DeviceProfile> profiles,
float minWidth, float minHeight,
int wPx, int hPx,
int awPx, int ahPx,
Resources res) {
.....
DeviceProfile closestProfile = findClosestDeviceProfile(minWidth, minHeight, points);
// Snap to the closest row count
numRows = closestProfile.numRows;
// Snap to the closest column count
numColumns = closestProfile.numColumns;
.....
}
1.3.layout实现
接着看看DeviceProfile.java里的layout实现。继续看代码:
public void layout(Launcher launcher) {
FrameLayout.LayoutParams lp;
......
// Layout the workspace
PagedView workspace = (PagedView) launcher.findViewById(R.id.workspace);
lp = (FrameLayout.LayoutParams) workspace.getLayoutParams();
lp.gravity = Gravity.CENTER;
int orientation = isLandscape ? CellLayout.LANDSCAPE : CellLayout.PORTRAIT;
Rect padding = getWorkspacePadding(orientation);
workspace.setLayoutParams(lp);
workspace.setPadding(padding.left, padding.top, padding.right, padding.bottom);
workspace.setPageSpacing(getWorkspacePageSpacing(orientation));
.....
}
可以看到初始化了workspace,然后设置其布局和间距Padding等。
2.页面初始化
WorkSpace里面可以包含多个页面,一个页面就是一个CellLayout。首先,Launcher在启动的时候,会在LauncherModel里加载桌面图标等资源,加载完成之后,最终会在bindWorkspaceScreens方法里通过回调调用bindScreens方法,而Launcher.java实现了LauncherModel.Callbacks的接口,所以最终调用的是Launcher.java的bindScreens方法。
bindScreens里调用bindAddScreens方法,bindAddScreens里根据实际加载的页面数,循环调用Workspace的insertNewWorkspaceScreenBeforeEmptyScreen方法来生成页面。
for (int i = 0; i < count; i++) {
mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(
orderedScreenIds.get(i));
}
insertNewWorkspaceScreenBeforeEmptyScreen里又调用insertNewWorkspaceScreen,insertNewWorkspaceScreen方法如下:
public long insertNewWorkspaceScreen(long screenId, int insertIndex) {
// Log to disk
Launcher.addDumpLog(TAG, "11683562 - insertNewWorkspaceScreen(): " + screenId +
" at index: " + insertIndex, true);
if (mWorkspaceScreens.containsKey(screenId)) {
throw new RuntimeException("Screen id " + screenId + " already exists!");
}
CellLayout newScreen = (CellLayout)
mLauncher.getLayoutInflater().inflate(
R.layout.workspace_screen, null);
newScreen.setOnLongClickListener(mLongClickListener);
newScreen.setOnClickListener(mLauncher);
newScreen.setSoundEffectsEnabled(false);
mWorkspaceScreens.put(screenId, newScreen);
mScreenOrder.add(insertIndex, screenId);
addView(newScreen, insertIndex);
return screenId;
}
可以看到通过workspace_screen.xml布局实例化了一个CellLayout对象newScreen,最后将该newScreen通过addView方法添加到了Workspace的这个容器里。
桌面图标
1.图标生成
桌面图标其实是一个继承TextView的BubbleTextView对象,其实就是一个小icon加文字的TextView。这个桌面图标的生成可以有三种途径:
①.默认配置生成;
②.从抽屉(或编辑模式里的小部件)里拖动到桌面生成;
③.第三方应用主动生成。
具体途径的各个流程就不分析了,不管是从哪个途径生成的,最终都是调用applyFromShortcutInfo方法来生成最终的图标,所以要改桌面图标风格/样式的都可以在这里修改。具体看下代码:
public void applyFromShortcutInfo(ShortcutInfo info, IconCache iconCache,boolean setDefaultPadding, boolean promiseStateChanged) {
Bitmap b = info.getIcon(iconCache);
LauncherAppState app = LauncherAppState.getInstance();
FastBitmapDrawable iconDrawable = Utilities.createIconDrawable(b);
iconDrawable.setGhostModeEnabled(info.isDisabled != 0);
setCompoundDrawables(null, iconDrawable, null, null);
if (setDefaultPadding) {
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
setCompoundDrawablePadding(grid.iconDrawablePaddingPx);
}
if (info.contentDescription != null) {
setContentDescription(info.contentDescription);
}
setText(info.title);
setTag(info);
if (promiseStateChanged || info.isPromise()) {
applyState(promiseStateChanged);
}
}
可以看到桌面icon是用工具类Utilities生成的FastBitmapDrawable,调用setCompoundDrawables把icon放在Text的上面,setText设置图标文字内容。
2.图标拖动
Launcher里的图标拖动都是由DragController进行控制的,先来看看DragController是怎么控制的。从Launcher.java的onCreate开始,初始化了变量mDragController,然后调用setupViews()进行一些处理:
private void setupViews() {
final DragController dragController = mDragController;
.....
// Setup the drag layer
mDragLayer.setup(this, dragController);
.....
// Setup the workspace
mWorkspace.setHapticFeedbackEnabled(false);
mWorkspace.setOnLongClickListener(this);
mWorkspace.setup(dragController);
dragController.addDragListener(mWorkspace);
// Get the search/delete bar
mSearchDropTargetBar = (SearchDropTargetBar)
mDragLayer.findViewById(R.id.search_drop_target_bar);
// Setup AppsCustomize
mAppsCustomizeTabHost = (AppsCustomizeTabHost) findViewById(R.id.apps_customize_pane);
mAppsCustomizeContent = (AppsCustomizePagedView)
mAppsCustomizeTabHost.findViewById(
R.id.apps_customize_pane_content);
mAppsCustomizeContent.setup(this, dragController);
// Setup the drag controller (drop targets have to be added in reverse order in priority)
dragController.setDragScoller(mWorkspace);
dragController.setScrollView(mDragLayer);
dragController.setMoveTarget(mWorkspace);
dragController.addDropTarget(mWorkspace);
if (mSearchDropTargetBar != null) {
mSearchDropTargetBar.setup(this, dragController);
mSearchDropTargetBar.setQsbSearchBar(getQsbBar());
}
.....
先是对DragLayer对象进行设置,然后把有拖动处理的对象添加到DragController的拖动列表mListeners里。DragLayer实现了对View树改变的监听接口,主要就是拦截触屏事件,然后将事件转到DragController里处理。
图标的拖动有三种情况:
①.在Workspace上进行拖动;
②.从桌面文件夹里拖动到桌面;
③.从抽屉(或编辑模式)里拖动到桌面;
2.1.开始拖动
我们知道拖动事件都是我们长按图标开始的,所以都是从onLongClick方法开始,上面三种情况对应处理:第一种是在Launcher.java的onLongClick里,第二种是在
Folder.java的onLongClick里,第三种则是在AppsCustomizePagedView的onLongClick里。
这三种情况最终都会转到Workspace.beginDragShared方法来处理。那就看看beginDragShared的实现:
public void beginDragShared(View child, DragSource source) {
child.clearFocus();
child.setPressed(false);
// The outline is used to visualize where the item will land if dropped
mDragOutline = createDragOutline(child, DRAG_BITMAP_PADDING);
.....
final Bitmap b = createDragBitmap(child, padding);
.....
int dragLayerX = Math.round(mTempXY[0] - (bmpWidth - scale * child.getWidth()) / 2);
int dragLayerY = Math.round(mTempXY[1] - (bmpHeight - scale * bmpHeight) / 2 - padding.get() / 2);
......
DragView dv = mDragController.startDrag(b, dragLayerX, dragLayerY, source, child.getTag(),DragController.DRAG_ACTION_MOVE, dragVisualizeOffset, dragRect, scale);
......
}
先是调用createDragOutline画出拖动时在桌面显示的原图标轮廓Bitmap,接着调用createDragBitmap创建拖动时的图标,然后dragLayerX和dragLayerY是图标拖动时对应的偏移量,最后调用了DragController的startDrag方法开始拖动。
接着看看DragController里的startDrag方法:
public DragView startDrag(Bitmap b, int dragLayerX, int dragLayerY,
DragSource source, Object dragInfo, int dragAction, Point dragOffset, Rect dragRegion,float initialDragViewScale) {
if (PROFILE_DRAWING_DURING_DRAG) {
android.os.Debug.startMethodTracing("Launcher");
}
// Hide soft keyboard, if visible
if (mInputMethodManager == null) {
mInputMethodManager = (InputMethodManager)
mLauncher.getSystemService(Context.INPUT_METHOD_SERVICE);
}
mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
for (DragListener listener : mListeners) {
listener.onDragStart(source, dragInfo, dragAction);
}
final int registrationX = mMotionDownX - dragLayerX;
final int registrationY = mMotionDownY - dragLayerY;
final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
mDragging = true;
mDragObject = new DropTarget.DragObject();
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDownX - (dragLayerX + dragRegionLeft);
mDragObject.yOffset = mMotionDownY - (dragLayerY + dragRegionTop);
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
final DragView dragView = mDragObject.dragView = new DragView(mLauncher, b, registrationX,
registrationY, 0, 0, b.getWidth(), b.getHeight(), initialDragViewScale);
if (dragOffset != null) {
dragView.setDragVisualizeOffset(new Point(dragOffset));
}
if (dragRegion != null) {
dragView.setDragRegion(new Rect(dragRegion));
}
mLauncher.getDragLayer().performHapticFeedback(
HapticFeedbackConstants.LONG_PRESS);
dragView.show(mMotionDownX, mMotionDownY);
handleMoveEvent(mMotionDownX, mMotionDownY);
return dragView;
}
可以看到循环遍历mListeners了,通知所有之前加进来的拖动监听对象DragListener拖动开始;然后创建拖动对象mDragObject,并设置响应属性;接着创建DragView,并调用其show显示拖动,开始一个属性动画;最后调用handleMoveEvent来移动DragView。
2.2.结束拖动
当用户将被拖拽物移动到相应位置后,将手指从屏幕上移开,此时要处理的就是MotionEvent.ACTION_UP事件,最终在DragController里调用drop方法把拖动对象放到相应位置,调用endDrag()做一些改变变量/释放拖动对象等结束拖动的操作。
private void drop(float x, float y) {
final int[] coordinates = mCoordinatesTemp;
final DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
mDragObject.x = coordinates[0];
mDragObject.y = coordinates[1];
boolean accepted = false;
if (dropTarget != null) {
mDragObject.dragComplete = true;
dropTarget.onDragExit(mDragObject);
if (dropTarget.acceptDrop(mDragObject)) {
dropTarget.onDrop(mDragObject);
accepted = true;
}
}
mDragObject.dragSource.onDropCompleted((View) dropTarget, mDragObject, false, accepted);
}
可以看到drop里先调用findDropTarget查找到当前拖动的对象,如果找到了则把该对象放到最终的位置上。
3.图标点击效果
点击桌面图标,会在BubbleTextView的updateIconState方法里处理点击的效果。
private void updateIconState() {
Drawable top = getCompoundDrawables()[1];
if (top instanceof FastBitmapDrawable) {
((FastBitmapDrawable) top).setPressed(isPressed() || mStayPressed);
}
}
先获取点击的图标,这应该是一个FastBitmapDrawable对象,然后调用FastBitmapDrawable的setPressed方法处理。接着看看FastBitmapDrawable的setPressed方法:
public void setPressed(boolean pressed) {
if (mPressed != pressed) {
mPressed = pressed;
if (mPressed) {
mPressedAnimator = ObjectAnimator
.ofInt(this, "brightness", PRESSED_BRIGHTNESS)
.setDuration(CLICK_FEEDBACK_DURATION);
mPressedAnimator.setInterpolator(
CLICK_FEEDBACK_INTERPOLATOR);
mPressedAnimator.start();
} else if (mPressedAnimator != null) {
mPressedAnimator.cancel();
setBrightness(0);
}
}
invalidateSelf();
}
可以看到这里使用了属性动画ObjectAnimator,设置的动画的属性是”brightness”,变化范围是0-100,ObjectAnimator在动画的过程中会自动更新属性值,即会调用setBrightness方法。
public void setBrightness(int brightness) {
if (mBrightness != brightness) {
mBrightness = brightness;
updateFilter();
invalidateSelf();
}
}
setBrightness里可以看到最终是调用了updateFilter()方法处理点击效果。updateFilter()里最终通过Paint.setColorFilter方法实现了点击时亮度改变的这个效果。
页面滑动
滑动首先是从触摸事件开始,onInterceptTouchEvent拦截触摸事件,onInterceptTouchEvent返回false则事件传递给子view处理,返回true则在PagedView里的onTouchEvent处理。页面滑动就是在PagedView里实现的。所以我们看看PagedView的onTouchEvent处理,主要就是处理down/move/up这三个事件,这个代码比较多,一个个事件来看吧。down这个事件其实没太多处理,就是初始化按下的位置等变量值。move事件就是要获取滑动到的位置然后重新绘制界面了,先看看move事件的处理:
case MotionEvent.ACTION_MOVE:
if (mTouchState == TOUCH_STATE_SCROLLING) {
// Scroll to follow the motion event
final int pointerIndex = ev.findPointerIndex(mActivePointerId);
if (pointerIndex == -1) return true;
final float x = ev.getX(pointerIndex);
final float deltaX = mLastMotionX + mLastMotionXRemainder - x;
mTotalMotionX += Math.abs(deltaX);
// Only scroll and update mLastMotionX if we have moved some discrete amount. We
// keep the remainder because we are actually testing if we‘ve moved from the last
// scrolled position (which is discrete).
if (Math.abs(deltaX) >= 1.0f) {
mTouchX += deltaX;
mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
if (!mDeferScrollUpdate) {
scrollBy((int) deltaX, 0);
if (DEBUG) Log.d(TAG, "onTouchEvent().Scrolling: " + deltaX);
} else {
invalidate();
}
mLastMotionX = x;
mLastMotionXRemainder = deltaX - (int) deltaX;
} else {
awakenScrollBars();
}
}
.....
从代码可以看到当移动距离deltaX大于等于1时才做滑动处理,调用invalidate()来重新绘制界面。这里PagedView继承ViewGroup,而ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法,而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法。所以接下来会在dispatchDraw里重新绘制界面,如果想实现自己的滑动效果,修改dispatchDraw的实现就可以了。
再看看up事件的处理,up事件即手指离开了屏幕,最后决定是否需要切换页面。
case MotionEvent.ACTION_UP:
if (mTouchState == TOUCH_STATE_SCROLLING) {
final int activePointerId = mActivePointerId;
final int pointerIndex = ev.findPointerIndex(activePointerId);
final float x = ev.getX(pointerIndex);
final VelocityTracker velocityTracker = mVelocityTracker;
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
int velocityX = (int) velocityTracker.getXVelocity(activePointerId);
final int deltaX = (int) (x - mDownMotionX);
final int pageWidth = getPageAt(mCurrentPage).getMeasuredWidth();
boolean isSignificantMove = Math.abs(deltaX) > pageWidth *
SIGNIFICANT_MOVE_THRESHOLD;
mTotalMotionX += Math.abs(mLastMotionX + mLastMotionXRemainder - x);
boolean isFling = mTotalMotionX > MIN_LENGTH_FOR_FLING &&
Math.abs(velocityX) > mFlingThresholdVelocity;
......
if (((isSignificantMove && !isDeltaXLeft && !isFling) ||
(isFling && !isVelocityXLeft)) && mCurrentPage > 0) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage - 1;
snapToPageWithVelocity(finalPage, velocityX);
} else if (((isSignificantMove && isDeltaXLeft && !isFling) ||(isFling && isVelocityXLeft)) &&mCurrentPage < getChildCount() - 1) {
finalPage = returnToOriginalPage ? mCurrentPage : mCurrentPage + 1;
snapToPageWithVelocity(finalPage, velocityX);
} else {
snapToDestination();
}
......
} else if (mTouchState == TOUCH_STATE_PREV_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.max(0, mCurrentPage - 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
} else if (mTouchState == TOUCH_STATE_NEXT_PAGE) {
// at this point we have not moved beyond the touch slop
// (otherwise mTouchState would be TOUCH_STATE_SCROLLING), so
// we can just page
int nextPage = Math.min(getChildCount() - 1, mCurrentPage + 1);
if (nextPage != mCurrentPage) {
snapToPage(nextPage);
} else {
snapToDestination();
}
}
......
首先是页面在滑动(TOUCH_STATE_SCROLLING)的处理,根据isSignificantMove/isDeltaXLeft/isFling/isVelocityXLeft/mCurrentPage这几个变量,确定页面是向左移动还是向右移动,还是留在当前页面。isSignificantMove是判断移动距离是否超过页面宽40%的;isDeltaXLeft是根据移动距离来判断是滑动从左到右还是从右到左;isFling是根据滑动距离和速率,判断是否是滑动;isVelocityXLeft是横向滑动的速率是否大于0;mCurrentPage则是当前页是否超出了页面个数的限制。
页面切换都调用了snapToPage方法,snapToPageWithVelocity方法是根据传进来的whichPage来切换到该页面,snapToDestination方法则是向离屏幕中心最近的页面移动。
接着是不在滑动状态的处理,根据状态是直接切换到上一页(TOUCH_STATE_PREV_PAGE),还是是直接切换到下一页(TOUCH_STATE_NEXT_PAGE)。
接着看看页面切换的方法snapToPage:
protected void snapToPage(int whichPage, int delta, int duration, boolean immediate,TimeInterpolator interpolator) {
whichPage = validateNewPage(whichPage);
mNextPage = whichPage;
.....
mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration);
updatePageIndicator();
// Trigger a compute() to finish switching pages if necessary
if (immediate) {
computeScroll();
}
// Defer loading associated pages until the scroll settles
mDeferLoadAssociatedPagesUntilScrollCompletes = true;
mForceScreenScrolled = true;
invalidate();
}
snapToPage里调用了mScroller.startScroll开始切换操作,完成切换在computeScroll()方法里面,然后会调用scrollTo()方法进行最终的切换。这里面还会调用invalidate()方法进行界面的绘制刷新,形成动画效果和页面切换效果。