代码改变世界

说说Android桌面(Launcher应用)背后的故事(八)——让桌面的精灵穿越起来

2012-07-30 15:04  tang768168  阅读(434)  评论(1编辑  收藏  举报

 有了前面的工作,基本上这个桌面就已经像模像样了,但是,和系统自带的Launcher相比,还差得很远。其中,系统Launcher的桌面上的item是可以任意穿越(移动)的。同时,在其穿越的过程中,你也可以将其kill掉。在这篇文章中,就让我们来看看桌面上的精灵如何实现她们穿越的梦想….
         系统Launcher为了实现item的拖拽,可谓下了很大的功夫,面面俱到。为了实现拖拽的功能,其定义了一组比较抽象的概念:拖拽源(DragSource);拖拽目的地(DragTarget);拖拽控制器(DragController);和拖拽界面(DragLayer)。DragSource主要用来表示桌面上的item可以从哪些位置被拖动,这里的“哪些位置”就是用DragSource来标识。比如,系统Launcher中的Workspace,AllAppsGridView,Folder都是DragSource,也就是在这些位置里的item可以被拖拽。DragTarget则对应的,表示可以容纳一个item的位置。比如,系统Launcher中的Workspace,Folder,DeleteZone就是DragTarget,也就是我们可以拖着某个item在这些位置上放下,item就被移到这个位置了。同时,为了处理多个桌面之间的拖动,其定义了一个DragScroller来负责处理多个桌面之间拖动的时候的逻辑。一个item在桌面上的生命周期有来,自然也得有去的时候,拖拽的时候,桌面下方的某块区域固定为删除区域(DeleteZone),这样,当被拖拽的item在这个区域内被释放的时候,就意味着其在桌面上生命的结束。
有了这个基本的了解,再进入代码中寻求更细的答案就不再费事了。下面,就让我们继续实现我们未完成的功能:
我们知道,前面的部分,所有的动作都是始于用户长按桌面某个空白的区域,那么当用户长按的是桌面上的某个item,我们要处理什么呢?对,就是拖拽了。所以,在Launcher中的onLongClick事件中,我们要加入长按某个item的处理。完整的onLongClick方法如下:
[java] view plaincopy
<span style="font-size:13px;">  /**
     * 在这个方法中需要判断当前长按的事件是在空白的区域还是某个item
     */  
    @Override  
    public boolean onLongClick(View v) {  
        //ActivityUtils.alert(getApplication(), "长按");  
        if(!(v instanceof UorderCellLayout)){  
            v = (View)v.getParent(); //如果当前点击的是item,得到其父控件,即UorderCellLayout  
        }  
          
        CellInfo cellInfo = (CellInfo)v.getTag(); //这里获取cellInfo信息  
 
        if(cellInfo == null){  
            Log.v(TAG, "CellInfo is null");  
            return true;  
        }  
          
        //Log.v(TAG, ""+cellInfo.toString());  
        /**
         * 注意,我们在CellLayout中获取当前位置信息的时候,就一并判断了当前位置上是否是item,如果是,则将
         * item保存在cellinfo.view中
         */  
        if(cellInfo.view == null){  
            //说明是空白区域  
            Log.v(TAG, "onLongClick,cellInfo.valid:"+cellInfo.valid);  
            if(cellInfo.valid){  
                //如果是有效的区域  
                addCellInfo = cellInfo;  
                showPasswordDialog(REQUEST_CODE_SETUP, null);     
            }  
        }else{  
            //处理拖拽  
            mWorkspace.startDrag(cellInfo);  
        }  
        return true;  
    }</span>  

 
在这个方法中当判断当前用户长按的是一个item的时候,执行mWorkspace.startDrag(cellInfo);所以,我们进入这个方法中看看其处理逻辑:
[java] view plaincopy
<span style="font-size:13px;">  /**
     * 开始拖拽,这个方法并不真正处理拖拽行为,而是交付给DragController来完成
     * @param cellInfo
     */  
    public void startDrag(UorderCellLayout.CellInfo cellInfo){  
        View child = cellInfo.view;  
          
        UorderCellLayout layout = (UorderCellLayout)getChildAt(mCurrentScreen);  
        layout.onDragChild(child);  
          
        cellInfo.screen = mCurrentScreen;  
        //启动拖拽  
        mDragController.startDrag(this, child, cellInfo);  
        invalidate();  
    }</span>  

 
在这个方法中,只是做了一些辅助的处理,真正实现拖拽的是DragController接口的实现者。那么,到这里,就自然的追溯到Workspace布局控件的父控件DragLayer控件了。接下来,就让我们进入DragLayer中一探item移动的秘密:
由于DragLayer实现了DragController接口,所以,我们先从刚刚中断的地方开始,先来看看startDrag中的逻辑,在这个方法里,屏蔽了输入法,创建了移动的Bitmap,同时,放大了一定的倍数,并设置了相关的动画参数。完整的代码如下:
[java] view plaincopy
<span style="font-size:13px;">  public void startDrag(UorderWorkspace workspace, View v, CellInfo cellInfo) {  
        Log.v(TAG, "开始拖拽...");  
        //隐藏输入法  
        if(mInputManager == null){  
            mInputManager = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE);  
        }  
        mInputManager.hideSoftInputFromWindow(getWindowToken(), 0);  
          
        if(mDragListener != null){  
            mDragListener.onDragStart();  
        }  
        Log.w(TAG, "当前view的scrollX,scrollY:"+v.getScrollX()+","+v.getScrollY());  
          
        mDragRect.set(v.getScrollX(), v.getScrollY(), 0, 0);  
        offsetDescendantRectToMyCoords(v, mDragRect); //将孩子的坐标系移到父坐标系中  
          
        //这里的mLastMotionX是在onInterceptTouchEvent中获取的  
        mTouchOffsetX = (int)(mLastMotionX - mDragRect.left);  
        mTouchOffsetY = (int)(mLastMotionY - mDragRect.top);  
          
        v.clearFocus();  
        v.setPressed(false);  
          
        /**
         * 下面需要创建View对应的Bitmap,移动过程你看到的被你拖拽的对象是一个Bitmap对象
         */  
        //设置绘制缓存  
        boolean willNotCache = v.willNotCacheDrawing();  
        v.setWillNotCacheDrawing(false);  
        v.buildDrawingCache();  
          
        Bitmap bitmap = v.getDrawingCache();  
        int width = bitmap.getWidth();  
        int height = bitmap.getHeight();  
          
        /**
         * 我们知道,在Launcher中,我们长按某个item的时候,首先,一个动画效果将其弹起,并放大一定的倍数
         * 这里,就是计算放大的倍数,并按照该放大倍数绘制Bitmap
         */  
        Matrix matrix = new Matrix();  
        float scaleFactor = v.getWidth();  
        //计算缩放比例  
        scaleFactor = (scaleFactor + SCALE_SIZE) /scaleFactor;  
        matrix.setScale(scaleFactor, scaleFactor);  
        //根据指定的缩放比例创建一个Bitmap  
        mDragBitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, true);  
        //清除绘制缓存  
        v.destroyDrawingCache();  
        v.setWillNotCacheDrawing(willNotCache);  
          
        //计算缩放后的Bitmap与View的offset  
        mBitmapOffsetX = (mDragBitmap.getWidth()-width) / 2;  
        mBitmapOffsetY = (mDragBitmap.getHeight()-height) /2;  
 
        //将原来的item保存,并隐藏掉  
        mOriginator = v;  
        v.setVisibility(View.GONE);  
          
        //根据scaleFactor指定动画起始大小  
        /**
         * 这个主要是为了让桌面上的item在被弹起的时候,有一个过渡的动画效果,以免突兀
         * 这里设置初始值,在dispatchDraw中处理动画逻辑
         */  
        mAnimationTo = 1.0f;  
        mAnimationFrom = 1.0f / scaleFactor;  
        mAnimationState = ANIMATION_STATE_STARTING;  
        mAnimationType = ANIMATION_TYPE_SCALE;  
        mAnimationDuration = ANIMATION_DURATION;  
          
        mDragging = true;  
        mDrawBitmaptPaint = mPaint;  
        mWorkspace = workspace;  
        mCellInfo = cellInfo;  
          
        invalidate();  
    }</span>  

 
到了这里,长按桌面某个item,item放大的原理想必就一目了然了。那么其,从原始大小向放大后的Bitmap是如何过渡的呢?这个,就让我们再到dispatchDraw中一探究竟。在这个方法中,处理了这个过渡的动画效果,其实,就是设定一个动画时间,然后在这么长时间内才从原始大小渐变到放大后的大小。完整代码如下:
[java] view plaincopy
<span style="font-size:13px;">  /**
     * 在这个方法中,我们处理item从原始大小到放大后的大小的一个过渡效果
     */  
    protected void dispatchDraw(Canvas canvas){  
        super.dispatchDraw(canvas);  
          
        if(mDragging && mDragBitmap != null){  
            if(mAnimationState == ANIMATION_STATE_STARTING){  
                /**
                 * 动画开始,记下当前时间
                 */  
                mAnimationState = ANIMATION_STATE_RUNNING;  
                mAnimationStartTime = SystemClock.uptimeMillis();  
            }  
              
            int left = (int)(mScrollX + mLastMotionX - mTouchOffsetX - mBitmapOffsetX);  
            int top = (int)(mScrollY + mLastMotionY - mTouchOffsetY - mBitmapOffsetY);  
              
            if(mAnimationState == ANIMATION_STATE_RUNNING){  
                //计算当前已经正常化了多少  
                float normalized = (float)(SystemClock.uptimeMillis()-mAnimationStartTime)/mAnimationDuration;  
                if(normalized >= 1.0f){  
                    mAnimationState = ANIMATION_STATE_DONE;  
                }  
                  
                normalized = Math.min(normalized, 1.0f);  
                float value = mAnimationFrom + (mAnimationTo - mAnimationFrom)*mAnimationDuration;  
                  
                if(mAnimationType == ANIMATION_TYPE_SCALE){  
                    //缩放当前的mDragBitmap  
                    int width = mDragBitmap.getWidth();  
                    int height = mDragBitmap.getHeight();  
                      
                    canvas.save();  
                    canvas.translate(left, top);  
                    canvas.translate(width*(1.0f-value), height*(1.0f-value));  
                    canvas.scale(value, value);  
                    canvas.drawBitmap(mDragBitmap, 0.0f, 0.0f, mDrawBitmaptPaint);//因为做了translate,所以这里是0.0  
                    canvas.restore();  
                }  
            }else{  
                //否则开始或者停止了,则按照当前的画  
                  
                canvas.drawBitmap(mDragBitmap, left, top, mDrawBitmaptPaint);  
            }  
        }  
    }</span>  

 
前面都只是拖拽的准备工作,接下来就要看看拖拽是如何实现的了,那么经过前面系列文章的分析,到这里,你应该有一个自然而然的直觉:到onTouchEvent中去寻找答案。呵呵,因为要处理拖拽,无非就是处理MOTION_MOVE等事件。在这里,处理了移动区域的重新绘制,以及判断当前item所在的位置是否为屏幕的边缘20dip以内,如果是的话,则需要将其移动到下一个屏幕。同时,也需要判断当前item所在的位置是否是删除区域,如果是删除区域的话则将绘制Bitmap的画笔加一个变色蒙版,也就是我们看到当我们拖拽item经过垃圾箱区域的时候,被捉拽的item就变成红色了。下面,看看该方法的完整逻辑:
[java] view plaincopy
<span style="font-size:13px;">  public boolean onTouchEvent(MotionEvent event){  
        if(!mDragging){  
            Log.v(TAG, "拖拽已经停止,传递到孩子");  
            return false;  
        }  
          
        final int action = event.getAction();  
        final float x = event.getX();  
        final float y = event.getY();  
          
        switch (action) {  
        case MotionEvent.ACTION_DOWN:  
            mLastMotionX = x;  
            mLastMotionY = y;  
              
            /**
             * 这里我们需要判断
             * 我们规定屏幕左边和右边20dip以内为非拖拽区,这样当item停留在边缘的时候
             * 就标示需要移到下一个屏幕,当然如果这个方向有下一个屏幕的话
             */  
 
            if(x<NON_SCROLL_ZONE || x > getWidth()-NON_SCROLL_ZONE){  
                  
                Log.v(TAG, "滑动到边缘");  
                  
                mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
                if(x<NON_SCROLL_ZONE){  
                    scrollAction.setDirection(DIRECTION_LEFT);  
                }else{  
                    scrollAction.setDirection(DIRECTION_RIGHT);  
                }  
                postDelayed(scrollAction, SCROLL_DELAY);  
                  
            }else{  
                mScrollState = SCROLL_STATE_IN_ZONE;  
            }  
              
            break;  
              
        case MotionEvent.ACTION_MOVE:  
            final int scrollX = getScrollX();  
            final int scrollY = getScrollY();  
              
            final int touchOffsetX = mTouchOffsetX;  
            final int touchOffsetY = mTouchOffsetY;  
              
            final int bitmapOffsetX = mBitmapOffsetX;  
            final int bitmapOffsetY = mBitmapOffsetY;  
              
            //计算上一个位置  
            int left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);  
            int top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);  
              
            final int width = mDragBitmap.getWidth();  
            final int height = mDragBitmap.getHeight();  
              
            //上一个位置信息  
            mDragRect.set(left-1,top-1,width+left+1,height+top+1);  
              
            mLastMotionX = x;  
            mLastMotionY = y;  
              
            left = (int)(scrollX + mLastMotionX - touchOffsetX - bitmapOffsetX);  
            top = (int)(scrollY + mLastMotionY - touchOffsetY - bitmapOffsetY);  
            //设置新的位置,将所有拖拽经过的区域叠加在一起  
            mDragRect.union(left-1, top-1, width+left+1, height+top+1);  
              
            //这里判断当前被拖拽的item是否在垃圾箱区域  
            if(mDragListener != null){  
                Rect rect = new Rect();  
                int rawX = (int)(mLastMotionX - touchOffsetX - bitmapOffsetX);  
                int rawY = (int)(mLastMotionY - touchOffsetY - bitmapOffsetY);  
                  
                rect.set(rawX, rawY, rawX+width, rawY+height);  
                  
                boolean inDeleteZone = mDragListener.onDragMove(rect);  
                  
                /**
                 * 如果是在删除区域,则拖拽的item的绘制颜色需要加个变色蒙版,系统Launcher中是变红
                 */  
                if(inDeleteZone){  
                    mDrawBitmaptPaint = mTrashPaint;  
                }else{  
                    mDrawBitmaptPaint = mPaint;  
                }  
            }  
              
            //请求重新绘制拖拽区  
            invalidate(mDragRect);  
              
            /**
             * 这里判断是否在边缘,如果是,则滑动到下一个屏幕
             */  
              
            if(x<NON_SCROLL_ZONE){  
                //当当前状态从滑动区域滑到边缘的时候进行处理  
                if(mScrollState == SCROLL_STATE_IN_ZONE){  
                    mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
                    scrollAction.setDirection(DIRECTION_LEFT);  
                     //延时一定的时间,就是我们看到在边缘时,其停顿了一段时间再滑向了下一个屏幕  
                    postDelayed(scrollAction, SCROLL_DELAY);   
                }  
                  
            }else if (x > getWidth() - NON_SCROLL_ZONE){  
                  
                if(mScrollState == SCROLL_STATE_IN_ZONE){  
                    Log.w(TAG, "滑动到下一个屏幕");  
                    mScrollState = SCROLL_STATE_WAITING_OUTSIZE;  
                    scrollAction.setDirection(DIRECTION_RIGHT);  
                    postDelayed(scrollAction, SCROLL_DELAY);  
                }  
                  
            }else{  
                mScrollState = SCROLL_STATE_IN_ZONE;  
            }  
              
            break;  
              
        case MotionEvent.ACTION_UP:  
            removeCallbacks(scrollAction);  
            //stopDrag();  
            int rawX = (int)(mLastMotionX - mTouchOffsetX - mBitmapOffsetX);  
            int rawY = (int)(mLastMotionY - mTouchOffsetY - mBitmapOffsetY);  
            Rect rect = new Rect();  
            rect.set(rawX, rawY, rawX+mDragBitmap.getWidth(), rawY+mDragBitmap.getHeight());  
              
            stopDrag((int)x,(int)y, rect);  
              
            break;  
 
        default:  
            break;  
        }  
          
        return true;  
    }</span>  

 
上面的代码中在MOVE事件中进行了移动过程的处理。代码中,应该看到了,当item被拖到边缘20dip以内的位置,其执行的逻辑是:
[java] view plaincopy
<span style="font-size:13px;">scrollAction.setDirection(DIRECTION_RIGHT);  
postDelayed(scrollAction, SCROLL_DELAY);</span>  
其中,scollAction就是一个线程,由它来完成从一个屏幕滑动到另一个屏幕。这里需要注意的是:你所看到的滑动到下一个屏幕,其实,不是你在拖拽着item滑动到下一个屏幕的,而是,下一个屏幕滑动到当前手机窗口下的。所以,这个逻辑,不是应该由DragLayer来实现,而是由Workspace来实现。所以,Workspace实现了DragScroller接口,负责处理item滑动到下一个屏幕的逻辑。其中scrollAction对应的线程类代码如下:
[java] view plaincopy
<span style="font-size:13px;">  class ScrollRunnable implements Runnable{  
        private int mDirection;  
        @Override  
        public void run() {  
            if(mDragScroller != null){  
                if(mDirection == DIRECTION_LEFT){  
                    mDragScroller.scrollLeft();  
                }else{  
                    mDragScroller.scrollRight();  
                }  
            }  
        }  
          
        public void setDirection(int dir){  
            this.mDirection = dir;  
        }  
          
    }</span>  
 
其根据在ACTION_MOVE中计算的位置和方向来判断其滑动到下一个屏幕的方向。上面说了,眼前的滑动行为是下一个屏幕显示到了当前手机的窗口下,而不是当前的item真的被你拖到下一个屏幕的。所以,我们需要找到DragScroller的实现者:Workspace。在Workspace中,其执行了scrollLeft和scrollRight的逻辑:
[java] view plaincopy
<span style="font-size:13px;">  /**
     * {@inheritDoc}
     */  
    @Override  
    public void scrollLeft() {  
        if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen > 0 && mScroller.isFinished()){  
            snapToScreen(mCurrentScreen-1);  
        }  
    }  
 
    /**
     * {@inheritDoc}
     */  
    @Override  
    public void scrollRight() {  
        if(mCurrentScreen != INVALID_SCREEN && mCurrentScreen < getChildCount()-1 && mScroller.isFinished()){  
            snapToScreen(mCurrentScreen+1);  
        }  
    }</span>  
 
想必您还记得前面在Workspace分析的时候,其中的snapToScreen方法,不记得的话,可以到那里去寻找答案。好了,现在这个拖动的过程就这么实现了。那么,当我们停止拖拽释放了item,其又是如何被添加到新的位置或者被删除的呢?这个看上面ACTION_UP中的部分,调用了stopDrag方法。那么我们看看这个方法的逻辑。其实,这里很简单了,就是判断当前位置是否是删除区域,如果是执行删除逻辑,如果不是,则执行移动位置逻辑。代码如下:
[java] view plaincopy
<span style="font-size:13px;">  public void stopDrag(int x, int y, Rect dragRect){  
        if(mDragging){  
              
            mDragging = false;  
              
            if(mDragBitmap != null){  
                mDragBitmap.recycle();  
            }  
              
            UorderCellLayout screen = mWorkspace.getCurrentCellLayout();  
            Log.e(TAG, "当前屏幕是:"+mWorkspace.getCurrentScreen());  
            Log.e(TAG, "被移动项所在的屏幕是"+mCellInfo.screen);  
              
            if(mDragListener != null){  
                boolean inDeleteZone = mDragListener.onDragStop(dragRect);  
                if(inDeleteZone){  
                    //删除这个item  
                    screen.removeView(mOriginator);  
                    invalidate();  
                    ItemInfo info = (ItemInfo)mOriginator.getTag();  
                    UorderDBUtils.deleteItemFromDB(getContext(), info);  
                    return;  
                }  
            }  
              
            //将当前x,y所在的坐标转换为当前屏幕中的单元格  
              
            final int[] cellXY = new int[2];  
            screen.pointToCellExact(x, y, cellXY);  
            final CellInfo cellInfo = mCellInfo;  
            cellInfo.cellX = cellXY[0];  
            cellInfo.cellY = cellXY[1];  
            cellInfo.cellHSpan = 1;  
            cellInfo.cellVSpan = 1;  
              
            Log.e(TAG, "移动后的单元格:"+cellXY[0]+","+cellXY[1]);  
              
            //调用Workspace的方法,来移动当前的项  
            mWorkspace.moveShortcutOnDesk(mOriginator, cellInfo);  
              
              
            if(mOriginator != null){  
                mOriginator.setVisibility(View.VISIBLE);  
            }  
              
        }  
          
        invalidate();         
    }</span>  
 
上面的代码很简单,就是判断了其是否在删除区域内,如果不是调用mWorkspace.moveShortcutOnDesk(mOriginator, cellInfo);方法来完成位置的移动。那么,我们来看看这个方法,这个方法中的逻辑就是分两种情况,如果是在同一个屏幕中移动,则直接根据新的位置重新绘制就可以了,但是如果移动前和移动后不在同一个屏幕,则需要将原来的屏幕上的给删掉,在新的屏幕上添加。代码如下:
[java] view plaincopy
<span style="font-size:13px;">  /**
     * 用户长拖拽一个item移动,当其停止的时候,就根据其最新的位置,将其显示在当前最新的位置
     * 同时需要更新数据库
     * @param v
     * @param cellInfo
     */  
    public void moveShortcutOnDesk(View v, CellInfo cellInfo){  
          
        int screen = cellInfo.screen;  
          
          
        /**
         * 这里需要分为两种情况来进行处理,
         * 1、如果在同一个CellLayout中移动,则很简单,直接设置新的位置,然后让该CellLayout重新绘制就行了
         * 2、如果移动到了另一个CellLayout中,则首先需要将View从原来的CellLayout中删除掉,然后再将其添加到
         * 当前屏幕的CellLayout中
         */  
        if(screen != mCurrentScreen){  
            UorderCellLayout lastScreen = getCellLayout(screen);  
            lastScreen.removeView(v);  
            this.addInScreen(v, mCurrentScreen, cellInfo.cellX, cellInfo.cellY, cellInfo.cellHSpan, cellInfo.cellVSpan, false);  
          
        }else{  
              
            UorderCellLayout group = getCurrentCellLayout();  
            UorderCellLayout.LayoutParams lp = (UorderCellLayout.LayoutParams)v.getLayoutParams();  
            lp.cellX = cellInfo.cellX;  
            lp.cellY = cellInfo.cellY;  
            lp.cellHSpan = cellInfo.cellHSpan;  
            lp.cellVSpan = cellInfo.cellVSpan;  
              
            group.invalidate();           
        }  
          
 
 
        /**
         * 同时需要将移动后的新位置同步到数据库
         */  
        ItemInfo item = (ItemInfo)v.getTag();  
        item.cellX = cellInfo.cellX;  
        item.cellY = cellInfo.cellY;  
        item.spanX = cellInfo.cellHSpan;  
        item.spanY = cellInfo.cellVSpan;  
        item.screen = mCurrentScreen;     
          
        UorderDBUtils.moveItemInDB(getContext(), item);  
    }</span>  
 
如果你对其中的一些关于拖拽的细节还不是很清楚,那么你可以看看前面两篇关于拖拽的文章,它们可以帮你很好的理解拖拽的细节性问题。
到这里,完整的item拖拽过程就介绍完毕了。我们的桌面也越来越接近系统Launcher了。但是,我们现在只支持向桌面添加Application和Shortcut,是只占据一个单元格的最简单的item了。还无法向桌面添加各式各样的Widget以及文件夹等功能。但是不要心急,什么事情都有个先后顺序,都是从简单到复杂的。
下一篇我们就要介绍如何让我们的桌面也支持各种Widget(小部件)。