Android4.4-Launcher源码分析系列之WorkSpace及屏幕滑动
Posted Dennis-Android
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android4.4-Launcher源码分析系列之WorkSpace及屏幕滑动相关的知识,希望对你有一定的参考价值。
一.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,看下效果
欢迎留言
以上是关于Android4.4-Launcher源码分析系列之WorkSpace及屏幕滑动的主要内容,如果未能解决你的问题,请参考以下文章