一.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,看下效果
歡迎留言