一.WorkSpace是什么
前面已经介绍了一个WorkSpace包含了多个CellLayout,再回忆下之前画过的图
WorkSpace是一个ViewGroup,它的布局如下
<com.android.launcher3.Workspace android:id="@+id/workspace" android:layout_width="match_parent" android:layout_height="match_parent" launcher:defaultScreen="@integer/config_workspaceDefaultScreen" launcher:pageIndicator="@id/page_indicator" launcher:pageSpacing="@dimen/workspace_page_spacing" >
defaultScreen是默认的屏幕序号
pageIndicator是滑动指示器
pageSpacing是页面之间的距离
二.WorkSpace代码分析
WorkSpace的继承关系如下
实现了DropTarget、DragSource等多个接口
public class Workspace extends SmoothPagedView implements DropTarget, DragSource, DragScroller, View.OnTouchListener, DragController.DragListener, LauncherTransitionable, ViewGroup.OnHierarchyChangeListener, Insettable {
看下它的构造函数
<pre name="code" class="java"> public Workspace(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContentIsRefreshable = false; //获取绘制轮廓的辅助类对象 mOutlineHelper = HolographicOutlineHelper.obtain(context); //获取拖动的监听对象 mDragEnforcer = new DropTarget.DragEnforcer(context); // With workspace, data is available straight from the get-go setDataIsReady(); mLauncher = (Launcher) context; final Resources res = getResources(); mWorkspaceFadeInAdjacentScreens = res.getBoolean(R.bool.config_workspaceFadeAdjacentScreens); mFadeInAdjacentScreens = false; //获取壁纸管理者 mWallpaperManager = WallpaperManager.getInstance(context); //获取自定义属性 TypedArray a = context.obtainStyledAttributes(attrs,R.styleable.Workspace, defStyle, 0); //在all app列表里拖动app时workspace的缩放比例 mSpringLoadedShrinkFactor =res.getInteger(R.integer.config_workspaceSpringLoadShrinkPercentage) / 100.0f; //可以滑动的区域 mOverviewModeShrinkFactor =res.getInteger(R.integer.config_workspaceOverviewShrinkPercentage) / 100.0f; mOverviewModePageOffset = res.getDimensionPixelSize(R.dimen.overview_mode_page_offset); //滑动屏幕到边缘不能再滑动时拖动的Z轴距离 mCameraDistance = res.getInteger(R.integer.config_cameraDistance); //开机时的屏幕 mOriginalDefaultPage = mDefaultPage = a.getInt(R.styleable.Workspace_defaultScreen, 1); a.recycle(); //监听view层次的变化 setOnHierarchyChangeListener(this); //打开触摸反馈 setHapticFeedbackEnabled(false); //初始化WorkSpace initWorkspace(); // Disable multitouch across the workspace/all apps/customize tray setMotionEventSplittingEnabled(true); setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); }
mSpringLoadedShrinkFactor是在所有应用列表里长按item时workspace的缩略图比例,默认的是0.8,我把它改为0.01,看下效果,workspace缩小到只有一点点了
mOverviewModeShrinkFactor是可以滑动的区域缩放比例, 如果你把item拖出这个区域,那么删除框就会出现, 我把它改为4,默认的是0.58,看下效果
mCameraDistance是滑动屏幕到边缘不能再滑动时拖动的Z轴距离,就是那种3D效果,默认的是8000,我把它改为1000,3D效果更明显了
mOriginalDefaultPage是开机时默认的屏幕序号.
往下看initWorkspace()方法
protected void initWorkspace() { Context context = getContext(); mCurrentPage = mDefaultPage; //当前页设置为默认页 Launcher.setScreen(mCurrentPage); LauncherAppState app = LauncherAppState.getInstance(); DeviceProfile grid = app.getDynamicGrid().getDeviceProfile(); //保存应用图片的缓存 mIconCache = app.getIconCache(); setWillNotDraw(false); setClipChildren(false); setClipToPadding(false); //设置子view绘图缓存开启 setChildrenDrawnWithCacheEnabled(true); // This is a bit of a hack to account for the fact that we translate the workspace // up a bit, and still need to draw the background covering the whole screen. setMinScale(mOverviewModeShrinkFactor - 0.2f); setupLayoutTransition(); final Resources res = getResources(); //设置桌面缩略图背景 try { mBackground = res.getDrawable(R.drawable.apps_customize_bg); } catch (Resources.NotFoundException e) { // In this case, we will skip drawing background protection } //wallPaper 偏移 mWallpaperOffset = new WallpaperOffsetInterpolator(); //获取屏幕大小,此方法在android 4.0之前不支持 Display display = mLauncher.getWindowManager().getDefaultDisplay(); display.getSize(mDisplaySize); mMaxDistanceForFolderCreation = (0.55f * grid.iconSizePx); mFlingThresholdVelocity = (int) (FLING_THRESHOLD_VELOCITY * mDensity); }
在这个方法里设置当前页为默认页,并设置workspace缩略图背景,我把它换成手指的图片,看下
WorkSpace实现了DragSource和DropTarget,说明它既是一个拖动的容器也是一个拖动的源,那就看下它的startDrag方法
void startDrag(CellLayout.CellInfo cellInfo) { View child = cellInfo.cell; // Make sure the drag was started by a long press as opposed to a long click. if (!child.isInTouchMode()) { return; } mDragInfo = cellInfo; //原位置的item设置为不可见 child.setVisibility(INVISIBLE); CellLayout layout = (CellLayout) child.getParent().getParent(); layout.prepareChildForDrag(child); child.clearFocus(); child.setPressed(false); final Canvas canvas = new Canvas(); // 当item拖动时跟随着的的背景图 mDragOutline = createDragOutline(child, canvas, DRAG_BITMAP_PADDING); beginDragShared(child, this); }
在开始拖动时,就隐藏了原来位置的item,我把它改为不隐藏,mDragOutline是item拖动时跟着移动的背景图,我把它替换为手指的图片,看下效果
接下来分析它的触摸事件onInterceptTouchEvent和onTouch
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction() & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_DOWN: mXDown = ev.getX(); mYDown = ev.getY(); //纪录按下的时间 mTouchDownTime = System.currentTimeMillis(); break; case MotionEvent.ACTION_POINTER_UP: case MotionEvent.ACTION_UP: if (mTouchState == TOUCH_STATE_REST) { final CellLayout currentPage = (CellLayout) getChildAt(mCurrentPage); if (!currentPage.lastDownOnOccupiedCell()) { onWallpaperTap(ev); } } } //调用父类的onInterceptTouchEvent,这里是调用了PagedView return super.onInterceptTouchEvent(ev); }
把拦截事件交给父类PageView处理了.
OnTouch事件当workspace进入缩略图的场景或者没有完成状态切换时返回true
@Override public boolean onTouch(View v, MotionEvent event) { return (isSmall() || !isFinishedSwitchingState()) || (!isSmall() && indexOfChild(v) != mCurrentPage); }
WorkSpace作为一个ViewGroup的子类,看下它重写的view方法.它只重写onLayout和ondraw方法.
@Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { if (mFirstLayout && mCurrentPage >= 0 && mCurrentPage < getChildCount()) { mWallpaperOffset.syncWithScroll(); mWallpaperOffset.jumpToFinal(); } super.onLayout(changed, left, top, right, bottom); }
如果位于当前布局并且不是最后一页,那么执行 mWallpaperOffset.syncWithScroll()和mWallpaperOffset.jumpToFinal()方法.mWallpaperOffset是WallpaperOffsetInterpolator的实例,
class WallpaperOffsetInterpolator implements Choreographer.FrameCallback {
这个类是处理UI绘制的.syncWithScroll方法是处理壁纸偏移的
public void syncWithScroll() { //获取壁纸偏移量 float offset = wallpaperOffsetForCurrentScroll(); //设置壁纸偏移量 mWallpaperOffset.setFinalX(offset); //更新壁纸偏移量 updateOffset(true); }
jumpToFinal方法是把壁纸最终偏移量设为当前偏移量
public void jumpToFinal() { mCurrentOffset = mFinalOffset; }
三、屏幕滑动分析
桌面滑动是在WorkSpace的父类PagedView里处理的.前面已经分析了,WorkSpace的onInterceptTouchEvent方法调用了父类的onInterceptTouchEvent.这里就是分析入口.看下
PagedView的onInterceptTouchEvent方法
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { if (DISABLE_TOUCH_INTERACTION) { return false; } // 获取速度跟踪器,记录各个时刻的速度。并且添加当前的MotionEvent以记录更行速度值。 acquireVelocityTrackerAndAddMovement(ev); // 没有页面,直接跳过给父类处理。 if (getChildCount() <= 0) return super.onInterceptTouchEvent(ev); //最常见的需要拦截的情况:用户已经进入滑动状态,而且正在移动手指滑动,对这种情况直接进行拦截,调用PagedView的onTouchEvent() final int action = ev.getAction(); if ((action == MotionEvent.ACTION_MOVE) && (mTouchState == TOUCH_STATE_SCROLLING)) { return true; } switch (action & MotionEvent.ACTION_MASK) { case MotionEvent.ACTION_MOVE: { // 如果已经发生触摸 if (mActivePointerId != INVALID_POINTER) { // 检查用户滑动距离是否足够远 determineScrollingStart(ev); } break; } case MotionEvent.ACTION_DOWN: { final float x = ev.getX(); final float y = ev.getY(); // 记下触摸位置 mDownMotionX = x; mDownMotionY = y; mDownScrollX = getScrollX(); mLastMotionX = x; mLastMotionY = y; // 做一个该坐标在view上对parent的映射, float[] p = mapPointFromViewToParent(this, x, y); mParentDownMotionX = p[0]; mParentDownMotionY = p[1]; mLastMotionXRemainder = 0; mTotalMotionX = 0; // 第一个触摸点,返回0 mActivePointerId = ev.getPointerId(0); final int xDist = Math.abs(mScroller.getFinalX() - mScroller.getCurrX()); final boolean finishedScrolling = (mScroller.isFinished() || xDist < mTouchSlop); // 如果完成了滑动 if (finishedScrolling) { // 设置当前桌面状态为静止 mTouchState = TOUCH_STATE_REST; // 停止滑动动画 mScroller.abortAnimation(); } else { if (isTouchPointInViewportWithBuffer((int) mDownMotionX, (int) mDownMotionY)) { // 设置当前桌面状态为滑动中 mTouchState = TOUCH_STATE_SCROLLING; } else { // 设置当前桌面状态为静止 mTouchState = TOUCH_STATE_REST; } } // 如果页面可以触摸 if (!DISABLE_TOUCH_SIDE_PAGES) { // 识别触摸状态是否是直接翻页状态,如果是直接翻页,在onTouchEvent里面会直接调用 if (mTouchState != TOUCH_STATE_PREV_PAGE && mTouchState != TOUCH_STATE_NEXT_PAGE) { if (getChildCount() > 0) { if (hitsPreviousPage(x, y)) { // 设置桌面状态为上一页 mTouchState = TOUCH_STATE_PREV_PAGE; } else if (hitsNextPage(x, y)) { // 设置桌面状态为下一页 mTouchState = TOUCH_STATE_NEXT_PAGE; } } } } break; } // 不做处理 case MotionEvent.ACTION_UP: case MotionEvent.ACTION_CANCEL: // 重置桌面状态 resetTouchState(); break; case MotionEvent.ACTION_POINTER_UP: onSecondaryPointerUp(ev); releaseVelocityTracker(); break; } // 只要是mTouchState的状态不为TOUCH_STATE_REST,那么就进行事件拦截,调用onTouchEvent return mTouchState != TOUCH_STATE_REST; }
重点看最后一行代码的返回,mTouchState是纪录桌面状态的一个int值,默认是TOUCH_STATE_REST,总共有5种状态
/** * 滑动结束状态 */ protected final static int TOUCH_STATE_REST = 0; /** * 正在滑动 */ protected final static int TOUCH_STATE_SCROLLING = 1; /** * 滑动到上一页 */ protected final static int TOUCH_STATE_PREV_PAGE = 2; /** * 滑动到下一页 */ protected final static int TOUCH_STATE_NEXT_PAGE = 3; /** * 滑动状态重新排序 */ protected final static int TOUCH_STATE_REORDERING = 4;
如果mTouchState的值不为TOUCH_STATE_REST,即桌面静止,那么就拦截事件,交给onTouchEvent处理.在onInterceptTouchEvent得down move up事件里进行mTouchState的改变.滑动肯定是在move事件里,它里面调用了determineScrollingStart方法,这个方法是判断滑动距离是否足够大到滑动页面
protected void determineScrollingStart(MotionEvent ev, float touchSlopScale) { // 禁止滚动,如果我们没有一个有效的指针指数 final int pointerIndex = ev.findPointerIndex(mActivePointerId); if (pointerIndex == -1) return; // 如果我们从滚动视图外开始的手势那么禁止 final float x = ev.getX(pointerIndex); final float y = ev.getY(pointerIndex); if (!isTouchPointInViewportWithBuffer((int) x, (int) y)) return; final int xDiff = (int) Math.abs(x - mLastMotionX); final int yDiff = (int) Math.abs(y - mLastMotionY); final int touchSlop = Math.round(touchSlopScale * mTouchSlop); boolean xPaged = xDiff > mPagingTouchSlop; boolean xMoved = xDiff > touchSlop; boolean yMoved = yDiff > touchSlop; if (xMoved || xPaged || yMoved) { if (mUsePagingTouchSlop ? xPaged : xMoved) { // 如果用户滑动距离足够,那么开始滑动 mTouchState = TOUCH_STATE_SCROLLING; mTotalMotionX += Math.abs(mLastMotionX - x); mLastMotionX = x; mLastMotionXRemainder = 0; mTouchX = getViewportOffsetX() + getScrollX(); mSmoothingTime = System.nanoTime() / NANOTIME_DIV; pageBeginMoving(); } } }
这个方法里判断如果滑动距离足够,就把mTouchState的值设为TOUCH_STATE_SCROLLING,即滑动中.然后调用pageBeginMoving
protected void pageBeginMoving() { // 如果没正在移动,那么移动 if (!mIsPageMoving) { mIsPageMoving = true; onPageBeginMoving(); } }
而onPageBeginMoving是个空方法,是让子类去重写的.
在move时间里返回了true,那么拦截事件,由onTouchEvent来处理,看下onTouchEvent的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(); } } else if (mTouchState == TOUCH_STATE_REORDERING) { // 更新最后一次的触摸坐标 mLastMotionX = ev.getX(); mLastMotionY = ev.getY(); // Update the parent down so that our zoom animations take this // new movement into // account float[] pt = mapPointFromViewToParent(this, mLastMotionX, mLastMotionY); mParentDownMotionX = pt[0]; mParentDownMotionY = pt[1]; updateDragViewTranslationDuringDrag(); // 寻找离触摸点最近的页面 final int dragViewIndex = indexOfChild(mDragView); // Change the drag view if we are hovering over the drop target boolean isHoveringOverDelete = isHoveringOverDeleteDropTarget((int) mParentDownMotionX, (int) mParentDownMotionY); setPageHoveringOverDeleteDropTarget(dragViewIndex, isHoveringOverDelete); if (DEBUG) Log.d(TAG, "mLastMotionX: " + mLastMotionX); if (DEBUG) Log.d(TAG, "mLastMotionY: " + mLastMotionY); if (DEBUG) Log.d(TAG, "mParentDownMotionX: " + mParentDownMotionX); if (DEBUG) Log.d(TAG, "mParentDownMotionY: " + mParentDownMotionY); final int pageUnderPointIndex = getNearestHoverOverPageIndex(); if (pageUnderPointIndex > -1 && pageUnderPointIndex != indexOfChild(mDragView) && !isHoveringOverDelete) { mTempVisiblePagesRange[0] = 0; mTempVisiblePagesRange[1] = getPageCount() - 1; getOverviewModePages(mTempVisiblePagesRange); if (mTempVisiblePagesRange[0] <= pageUnderPointIndex && pageUnderPointIndex <= mTempVisiblePagesRange[1] && pageUnderPointIndex != mSidePageHoverIndex && mScroller.isFinished()) { mSidePageHoverIndex = pageUnderPointIndex; mSidePageHoverRunnable = new Runnable() { @Override public void run() { // Setup the scroll to the correct page before // we swap the views snapToPage(pageUnderPointIndex); // For each of the pages between the paged view // and the drag view, // animate them from the previous position to // the new position in // the layout (as a result of the drag view // moving in the layout) int shiftDelta = (dragViewIndex < pageUnderPointIndex) ? -1 : 1; int lowerIndex = (dragViewIndex < pageUnderPointIndex) ? dragViewIndex + 1 : pageUnderPointIndex; int upperIndex = (dragViewIndex > pageUnderPointIndex) ? dragViewIndex - 1 : pageUnderPointIndex; for (int i = lowerIndex; i <= upperIndex; ++i) { View v = getChildAt(i); // dragViewIndex < pageUnderPointIndex, so // after we remove the // drag view all subsequent views to // pageUnderPointIndex will // shift down. int oldX = getViewportOffsetX() + getChildOffset(i); int newX = getViewportOffsetX() + getChildOffset(i + shiftDelta); // Animate the view translation from its old // position to its new // position AnimatorSet anim = (AnimatorSet) v.getTag(ANIM_TAG_KEY); if (anim != null) { anim.cancel(); } v.setTranslationX(oldX - newX); anim = new AnimatorSet(); anim.setDuration(REORDERING_REORDER_REPOSITION_DURATION); anim.playTogether(ObjectAnimator.ofFloat(v, "translationX", 0f)); anim.start(); v.setTag(anim); } removeView(mDragView); onRemoveView(mDragView, false); addView(mDragView, pageUnderPointIndex); onAddView(mDragView, pageUnderPointIndex); mSidePageHoverIndex = -1; mPageIndicator.setActiveMarker(getNextPage()); } }; postDelayed(mSidePageHoverRunnable, REORDERING_SIDE_PAGE_HOVER_TIMEOUT); } } else { removeCallbacks(mSidePageHoverRunnable); mSidePageHoverIndex = -1; } } else { determineScrollingStart(ev); } break;
如果滑动距离大于1.0f,那么调用scrollBy滑动.在滑动的时候会调用snapToPage方法,这个方法有很多重载,但最终会进入到
protected void snapToPage(int whichPage, int delta, int duration, boolean immediate) { mNextPage = whichPage; View focusedChild = getFocusedChild(); if (focusedChild != null && whichPage != mCurrentPage && focusedChild == getPageAt(mCurrentPage)) { focusedChild.clearFocus(); } sendScrollAccessibilityEvent(); pageBeginMoving(); awakenScrollBars(duration); if (immediate) { duration = 0; } else if (duration == 0) { duration = Math.abs(delta); } if (!mScroller.isFinished()) { mScroller.abortAnimation(); } // 滑动的持续时间 mScroller.startScroll(mUnboundedScrollX, 0, delta, 0, duration); notifyPageSwitchListener(); // 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(); }
这个方法里定义了一些滑动的操作,比如距离,滑动持续时间,滑到哪一页等.比如我把这个持续时间duration改为9000,看下效果
欢迎留言