Launcher3源码浅析(5.1)--Workspace

来源:互联网 发布:雷霆队数据 编辑:程序博客网 时间:2024/06/04 19:42

目录

  • 前言
  • 初始化
    • 布局
    • 页面初始化
  • 桌面图标
    • 图标生成
    • 图标拖动
    • 图标点击效果
  • 页面滑动

前言

  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()方法进行界面的绘制刷新,形成动画效果和页面切换效果。

博客搬家:http://www.mosiliang.top/?p=168
以后更新都在自个博客上了,有兴趣的可以关注看看,谢谢!

1 0
原创粉丝点击