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,看下效果



欢迎留言


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章