现在的位置: 首页 > 综合 > 正文

android Launcher源码解析07:Workspace 01——概述

2012年10月02日 ⁄ 综合 ⁄ 共 9635字 ⁄ 字号 评论关闭

       launcher最重要部分是几个屏幕,其中涉及到一个Workspace布局。Workspace的主要功能是完成多个屏幕及壁纸的显示,同时完成屏幕之间的切换及壁纸添加。

1、初始化

/**
     * Used to inflate the Workspace from XML.
     *
     * @param context The application's context.
     * @param attrs The attribtues set containing the Workspace's customization values.
     * @param defStyle Unused.
     */
    public Workspace(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);

        mWallpaperManager = WallpaperManager.getInstance(context);
        
        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Workspace, defStyle, 0);
        mDefaultScreen = a.getInt(R.styleable.Workspace_defaultScreen, 1);
        a.recycle();

        setHapticFeedbackEnabled(false);
        initWorkspace();
    }

    /**
     * Initializes various states for this workspace.
     */
    private void initWorkspace() {
        Context context = getContext();
        mScrollInterpolator = new WorkspaceOvershootInterpolator();
        mScroller = new Scroller(context, mScrollInterpolator);
        mCurrentScreen = mDefaultScreen;
        Launcher.setScreen(mCurrentScreen);
        LauncherApplication app = (LauncherApplication)context.getApplicationContext();
        mIconCache = app.getIconCache();

        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
        mTouchSlop = configuration.getScaledTouchSlop();
        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
    }

        这里需要注意的是,默认屏幕是在配置文件中配置的,另外WorkspaceOvershootInterpolator是一个变化速率,其具体知识参看《android基础知识35:Interpolator》。

2、子view大小设置及排列

 @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        final int width = MeasureSpec.getSize(widthMeasureSpec);
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        if (widthMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
        }

        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        if (heightMode != MeasureSpec.EXACTLY) {
            throw new IllegalStateException("Workspace can only be used in EXACTLY mode.");
        }

        // The children are given the same width and height as the workspace
        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            getChildAt(i).measure(widthMeasureSpec, heightMeasureSpec);
        }


        if (mFirstLayout) {
            setHorizontalScrollBarEnabled(false);
            scrollTo(mCurrentScreen * width, 0);
            setHorizontalScrollBarEnabled(true);
            updateWallpaperOffset(width * (getChildCount() - 1));
            mFirstLayout = false;
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int childLeft = 0;

        final int count = getChildCount();
        for (int i = 0; i < count; i++) {
            final View child = getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                final int childWidth = child.getMeasuredWidth();
                child.layout(childLeft, 0, childLeft + childWidth, child.getMeasuredHeight());
                childLeft += childWidth;
            }
        }
    }

        这里需要注意的是,在第一次调用onMeasure时,需要切换到当前屏幕(实际上是默认屏幕);

        让几个CellLayout平铺,由于每个CellLayout的大小是手机的一屏幕大小,所以,这里让其横向平铺就很简单了,直接在onLayout中调用每个CellLayout的layout方法进行布局,按顺序布局的时候,只需要控制好每个CellLayout的left和right就可以了。

3、处理多屏幕滑动

      处理屏幕滑动时,涉及到android的事件处理过程。这部分可以看《 android基础知识03——事件处理02:事件流顺序》。
     如何处理屏幕的滑动,屏幕滑动,莫非就需要在ACTION_MOVE事件中处理。我们在文章的开头介绍了Android的事件拦截机制,那么我们想,要让滑动事件让Workspace处理,而不会干扰到CellLayout,自然要在onInterceptTouchEvent中做一些处理了。那我们先从onInterceptTouchEvent方法入手,在onInterceptTouchEvent方法中显眼的位置,我们就可以一眼发现如下代码:

		if(action == MotionEvent.ACTION_MOVE && mTouchState != TOUCH_STATE_STOPED){
			return true;
		}

同时在ruturn的时候,其返回的是:mTouchState != TOUCH_STATE_STOPED;这个就是说,如果当前正在滑动,则返回true,交给onTouchEvent事件来处理滑动逻辑。

那么,我们就再来看看onTouchEvent中对ACTION_MOVE事件的处理:

        case MotionEvent.ACTION_DOWN:
            /*
             * If being flinged and user touches, stop the fling. isFinished
             * will be false if being flinged.
             */
            if (!mScroller.isFinished()) {
                mScroller.abortAnimation();
            }

            // Remember where the motion event started
            mLastMotionX = ev.getX();
            mActivePointerId = ev.getPointerId(0);
            if (mTouchState == TOUCH_STATE_SCROLLING) {
                enableChildrenCache(mCurrentScreen - 1, mCurrentScreen + 1);
            }
            break;
        case MotionEvent.ACTION_MOVE:
    /** 
     * 这里是处理滑动的地方 
     * 注意,手指向右滑动的时候,屏幕是向左滑动的 
     *  
     */  
            if (mTouchState == TOUCH_STATE_SCROLLING) {
                // Scroll to follow the motion event
                final int pointerIndex = ev.findPointerIndex(mActivePointerId);
                final float x = ev.getX(pointerIndex);
                final float deltaX = mLastMotionX - x;
                mLastMotionX = x;//注意更新mLastMotionX
       /** 
         * 向右滑动的时候,scrollX的值=上一次scrollX+xDiff 
         */  
                //下面判断是向左还是向右滑动  
                if (deltaX < 0) {
                      //屏幕向左  
                    if (mTouchX > 0) {
                 //取差值小的一个  
                /** 
                 * xDiff是负数,所以 
                 * 和向右滑动类似,当在第一个屏幕的时候,再向左滑动的时候,就会出现xDiff的绝对值大余scrollX的情况 
                 * 这个时候scrollX的值接近于0,而xDiff的绝对值很可能大于0的。所以,这里做了如下的限制 
                 */  
                        mTouchX += Math.max(-mTouchX, deltaX);
                        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
                        invalidate();
                    }
                } else if (deltaX > 0) {
                   //屏幕向右 
                   //当前可以滑动的最右边
                    final float availableToScroll = getChildAt(getChildCount() - 1).getRight() -
                            mTouchX - getWidth();
                    if (availableToScroll > 0) {
                /** 
                 * 注意: 
                 *  
                 * 当滑动倒数第二个屏幕的时候,就有可能出现xDiff>availableScoll的情况 
                 * 因为scrollX最大为最后一个屏幕的最左边 
                 * available-getWidth就是scrollX的最大取值范围M 
                 * 所以,availableSroll=M-当前已经滑动的距离(scrollX); 
                 * 这样当在最后一个屏幕的时候,再向右就不能滑动了 
                 */  
                        mTouchX += Math.min(availableToScroll, deltaX);
                        mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
                        invalidate();
                    }
                } else {
                    awakenScrollBars();
                }
            }
            break;

在这里,根据新的坐标位置,就算是向左还是向右滑动。同时处理滑动操作。那么,当我们停下的时候,它又是怎么做的呢?看ACTION_UP事件中的处理逻辑:

        case MotionEvent.ACTION_UP:
            if (mTouchState == TOUCH_STATE_SCROLLING) {
                final VelocityTracker velocityTracker = mVelocityTracker;
                velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
                final int velocityX = (int) velocityTracker.getXVelocity(mActivePointerId);
                
                final int screenWidth = getWidth();
                final int whichScreen = (getScrollX() + (screenWidth / 2)) / screenWidth;
                final float scrolledPos = (float) getScrollX() / screenWidth;
                
                if (velocityX > SNAP_VELOCITY && mCurrentScreen > 0) {
                    //向左
                    // Fling hard enough to move left.
                    // Don't fling across more than one screen at a time.
                    final int bound = scrolledPos < whichScreen ?
                            mCurrentScreen - 1 : mCurrentScreen;
                    snapToScreen(Math.min(whichScreen, bound), velocityX, true);
                } else if (velocityX < -SNAP_VELOCITY && mCurrentScreen < getChildCount() - 1) {
                    // Fling hard enough to move right
                    // Don't fling across more than one screen at a time.
                    final int bound = scrolledPos > whichScreen ?
                            mCurrentScreen + 1 : mCurrentScreen;
                    snapToScreen(Math.max(whichScreen, bound), velocityX, true);
                } else {
                  //否则,看哪个屏幕显示的部分更多,就滑动到哪个屏幕
                            /** 
         * 其实很简单,就是以当前屏幕为基准,如果scrollX超出了一半,就滑倒下一个屏幕 
         * 如果没有超过一半就停留在该屏幕 
         * 所以,getScrollX()+screenWidth/2/screenWidth的思想就是 
         * 如果scollX超过了屏幕的一半,再加上个半个屏幕的大小,在除以整个屏幕的大小就是下一屏了 
         * 否则,就还是scrollX所在的屏幕 
         */  
                    snapToScreen(whichScreen, 0, true);
                }
            }
            mTouchState = TOUCH_STATE_REST;
            mActivePointerId = INVALID_POINTER;
            releaseVelocityTracker();
            break;

注意了,这里用VelocityTracker 计算了滑动的速度,因为,我们在滑动桌面的时候,应该注意到一个细节,当我们不是拖着桌面滑动,而是很快的滑动的时候,屏幕之间滑动到下一个屏幕的。这个就是通过VelocityTracker 计算滑动速度,如果滑动速度大于某个值,就直接滑动到下一个屏幕。具体的滑动到哪一个屏幕,是由方法snapToScreen处理的。那么我们就来看看这个好方法的逻辑:

 private void snapToScreen(int whichScreen, int velocity, boolean settle) {
        //if (!mScroller.isFinished()) return;

        whichScreen = Math.max(0, Math.min(whichScreen, getChildCount() - 1));
        
        clearVacantCache();
        enableChildrenCache(mCurrentScreen, whichScreen);

        mNextScreen = whichScreen;

        mPreviousIndicator.setLevel(mNextScreen);
        mNextIndicator.setLevel(mNextScreen);

        View focusedChild = getFocusedChild();
        if (focusedChild != null && whichScreen != mCurrentScreen &&
                focusedChild == getChildAt(mCurrentScreen)) {
            focusedChild.clearFocus();//当屏幕切换时需要将当前屏幕的focus去掉
        }
        
        final int screenDelta = Math.max(1, Math.abs(whichScreen - mCurrentScreen));
        final int newX = whichScreen * getWidth();//当前需要滑到的屏幕的左边x坐标 
        final int delta = newX - getScrollX();//偏差,〉0向右,<0向左 
        int duration = (screenDelta + 1) * 100;

        if (!mScroller.isFinished()) {
            mScroller.abortAnimation();
        }
        
        if (settle) {
            mScrollInterpolator.setDistance(screenDelta);
        } else {
            mScrollInterpolator.disableSettle();
        }
        
        velocity = Math.abs(velocity);
        if (velocity > 0) {
            duration += (duration / (velocity / BASELINE_FLING_VELOCITY))
                    * FLING_VELOCITY_INFLUENCE;
        } else {
            duration += 100;
        }

        awakenScrollBars(duration);
        mScroller.startScroll(getScrollX(), 0, delta, 0, duration);
        invalidate();
    }

在这个snapToScreen的方法中,逻辑很简单,主要就是调用了Scroller的startScroll方法,以当前滑动的位置和目标位置作为参数,启动滑动。但是,仅仅这样,这个方法起不到任何的效果,因为startScroll方法只是开始滑动,并不会不断的更新数据和处理滑动中的事情,这些事情是由computeScroll方法完成的。下面,我们再进入computeScroll方法来看看其逻辑:

/** 
 * 这个是当mScroller在滑动到某個屏幕的時候調用的 
 * 我们调用ScrollToScreen这个方法,我们调用了startScroll()这个方法,但是,如果不重写computeScroll 
 * 你会发现,{@link #snapToScreen(int)}没有效果的,原因就是 
 * 在其自己滑动的时候,我们调用startScroll的时候,只是设定了我们希望滑倒的位置,但是其滑动过程中 
 * 怎么滑动,还是在这个方法里。 
 * 当mScroller.computeScrollOffset返回真,说明还没有滑倒目的地,就继续计算 
 * 当返回假的时候,就说明滑动startScroll设定的终点了 
 *  
 * 奶奶的,想了半天才想明白,哎,杯具! 
 * @see #snapToScreen(int) 
 */  
@Override
    public void computeScroll() {
        //mScroller.computeScrollOffset计算当前新的位置  
        //返回true,说明scroll还没有停止 
        if (mScroller.computeScrollOffset()) {
        	/**
        	 * mScrollX 是保护属性,不能跨包访问,使用scrollTo(int,int) modify by author
        	 */
            //mTouchX = mScrollX = mScroller.getCurrX();
        	mTouchX = mScroller.getCurrX();
        	
            mSmoothingTime = System.nanoTime() / NANOTIME_DIV;
        	/**
        	 * mScrollY 是保护属性,不能跨包访问,使用scrollTo(int,int) modify by author
        	 */
            //mScrollY = mScroller.getCurrY();
                    /** 
         * 其实这里是不用scrollTo的,只需要设置mScrollX和mScrollY的值分别为 
         * mScroller.getCurrX()和mScroller.getCurrY()就行了 
         * 但是我们无法直接设置,所以用scrollTo完成 
         */  
            scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            
            updateWallpaperOffset();
        /** 
         * 这里需要调用postInvalidate,否则滑动的时候,你会发现 
         * 界面会在两个屏幕的中间位置卡住 
         */   
            postInvalidate();
        } else if (mNextScreen != INVALID_SCREEN) {
        //scroll停止了,则滑动到合法且合适的屏幕
            mCurrentScreen = Math.max(0, Math.min(mNextScreen, getChildCount() - 1));
            mPreviousIndicator.setLevel(mCurrentScreen);
            mNextIndicator.setLevel(mCurrentScreen);
            Launcher.setScreen(mCurrentScreen);
            mNextScreen = INVALID_SCREEN;
            //清除子控件绘制缓存  
            clearChildrenCache();
        } else if (mTouchState == TOUCH_STATE_SCROLLING) {
            final float now = System.nanoTime() / NANOTIME_DIV;
            final float e = (float) Math.exp((now - mSmoothingTime) / SMOOTHING_CONSTANT);
            final float dx = mTouchX - getScrollX();
        	/**
        	 * mScrollX 是保护属性,不能跨包访问,使用scrollBy(int,int) modify by author
        	 */
            //mScrollX += dx * e;
            scrollBy((int)(dx * e), 0);
            mSmoothingTime = now;

            // Keep generating points as long as we're more than 1px away from the target
            if (dx > 1.f || dx < -1.f) {
                updateWallpaperOffset();
                postInvalidate();
            }
        }
    }

到这里,整个屏幕为什么会滑动,这其中的逻辑处理,我想就基本清楚了。 

参考资料:

说说Android桌面(Launcher应用)背后的故事(四)——揭秘Workspace

抱歉!评论已关闭.