Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android4.4-Launcher源碼分析系列之WorkSpace及屏幕滑動

Android4.4-Launcher源碼分析系列之WorkSpace及屏幕滑動

編輯:關於Android編程

一.WorkSpace是什麼

前面已經介紹了一個WorkSpace包含了多個CellLayout,再回憶下之前畫過的圖

\

WorkSpace是一個ViewGroup,它的布局如下

 

 
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);
    }

SpringLoadedShrinkFactor是在所有應用列表裡長按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,看下效果

 

\
 

歡迎留言

 
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved