android Launcher——拖放功能深入研究


Luancher有一個相對比較復雜的功能就是拖放功能,要深入了解launcher,深入理解拖放功能是有必要的,這篇blog,我將對launcher的拖放功能做深入的了解
1.首先直觀感受什么時候開始拖放?我們長按桌面一個應用圖標或者控件的時候拖放就開始了,包括在all app view中長按應用圖標,下面就是我截取的拖放開始的代碼調用堆棧
 at com.android.launcher2.DragController.startDrag(DragController.java:170)
 at com.android.launcher2.Workspace.startDrag(Workspace.java:1068)
 at com.android.launcher2.Launcher.onLongClick(Launcher.java:1683)
 at android.view.View.performLongClick(View.java:2427)
 at android.widget.TextView.performLongClick(TextView.java:7286)
 at android.view.View$CheckForLongPress.run(View.java:8792)
 at android.os.Handler.handleCallback(Handler.java:587)
 at android.os.Handler.dispatchMessage(Handler.java:92)
 at android.os.Looper.loop(Looper.java:123)
 桌面應用圖標由Launcher.onLongClick負責監聽處理,插入斷點debug進入onLongclick函數
         if (!(v instanceof CellLayout)) {
            v = (View) v.getParent();
        }
                                 //獲取桌面CellLayout上一個被拖動的對象
         CellLayout.CellInfo cellInfo = (CellLayout.CellInfo) v.getTag();
               ...
        if (mWorkspace.allowLongPress()) {
            if (cellInfo.cell == null) {
                ...
            } else {
                if (!(cellInfo.cell instanceof Folder)) {
                    ...
                    //調用Workspace.startDrag處理拖動
                    mWorkspace.startDrag(cellInfo);
                }
            }
        }
我上面只寫出關鍵代碼,首先是獲取被拖動的對象v.getTag(),Tag什么時候被設置進去的了
   public boolean onInterceptTouchEvent(MotionEvent ev) {
        ...
        if (action == MotionEvent.ACTION_DOWN) {
                        ...
            boolean found = false;
            for (int i = count - 1; i >= 0; i--) {
                final View child = getChildAt(i);

                if ((child.getVisibility()) == VISIBLE || child.getAnimation() != null) {
                    child.getHitRect(frame);
                    //判斷區域是否在這個子控件的區間,如果有把child信息賦給mCellInfo
                    if (frame.contains(x, y)) {
                        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
                        cellInfo.cell = child;
                        cellInfo.cellX = lp.cellX;
                        cellInfo.cellY = lp.cellY;
                        cellInfo.spanX = lp.cellHSpan;
                        cellInfo.spanY = lp.cellVSpan;
                        cellInfo.valid = true;
                        found = true;
                        mDirtyTag = false;
                        break;
                    }
                }
            }
           
            mLastDownOnOccupiedCell = found;

            if (!found) {
                            ...
                            //沒有child view 說明沒有點擊桌面圖標項
                cellInfo.cell = null;               
            }
            setTag(cellInfo);
        }
看了上面代碼知道,當開始點擊桌面時,celllayout就會根據點擊區域去查找在該區域是否有child存在,若有把它設置為tag.cell,沒有,tag.cell設置為null,后面在開始拖放時launcher.onlongclick中對tag進行處理,
這個理順了,再深入到workspace.startDrag函數,workspace.startDrag調用DragController.startDrag去處理拖放
mDragController.startDrag(child, this, child.getTag(), DragController.DRAG_ACTION_MOVE);
再分析一下上面調用的幾個參數
child = tag.cell
this = workspace
child.getTag()是什么呢?在什么時候被設置?再仔細回顧原來launcher加載過程代碼,在launcher.createShortcut中它被設置了:注意下面我代碼中的注釋
    View createShortcut(int layoutResId, ViewGroup parent, ShortcutInfo info) {
        TextView favorite = (TextView) mInflater.inflate(layoutResId, parent, false);

        favorite.setCompoundDrawablesWithIntrinsicBounds(null,
                new FastBitmapDrawable(info.getIcon(mIconCache)),
                null, null);
        favorite.setText(info.title);
        //設置favorite(一個桌面Shortcut類型的圖標)的tag
        favorite.setTag(info);
        favorite.setOnClickListener(this);

        return favorite;
    }
繼續深入解讀DragController.startDrag函數
    public void startDrag(View v, DragSource source, Object dragInfo, int dragAction) {
            //設置拖放源view
            mOriginator = v;
        //獲取view的bitmap
        Bitmap b = getViewBitmap(v);

        if (b == null) {
            // out of memory?
            return;
        }
        //獲取源view在整個屏幕的坐標
        int[] loc = mCoordinatesTemp;
        v.getLocationOnScreen(loc);
        int screenX = loc[0];
        int screenY = loc[1];
                                //該函數功能解讀請繼續往下看
        startDrag(b, screenX, screenY, 0, 0, b.getWidth(), b.getHeight(),
                source, dragInfo, dragAction);

        b.recycle();
        //設置原來view不可見
        if (dragAction == DRAG_ACTION_MOVE) {
            v.setVisibility(View.GONE);
        }
    }

////////////////////////////////////////////////////////////
    public void startDrag(Bitmap b, int screenX, int screenY,
            int textureLeft, int textureTop, int textureWidth, int textureHeight,
            DragSource source, Object dragInfo, int dragAction) {
        //隱藏軟鍵盤
        if (mInputMethodManager == null) {
            mInputMethodManager = (InputMethodManager)
                    mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
        }
        mInputMethodManager.hideSoftInputFromWindow(mWindowToken, 0);
                                //mListener = deletezone,在blog laucher ui框架中有說明該函數,主要就是現實deletezone
        if (mListener != null) {
            mListener.onDragStart(source, dragInfo, dragAction);
        }
                                //記住手指點擊位置與屏幕左上角位置偏差
        int registrationX = ((int)mMotionDownX) - screenX;
        int registrationY = ((int)mMotionDownY) - screenY;

        mTouchOffsetX = mMotionDownX - screenX;
        mTouchOffsetY = mMotionDownY - screenY;

        mDragging = true;
        mDragSource = source;
        mDragInfo = dragInfo;

        mVibrator.vibrate(VIBRATE_DURATION);
                                //創建DragView對象
        DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY,
                textureLeft, textureTop, textureWidth, textureHeight);
        //顯示Dragview對象
        dragView.show(mWindowToken, (int)mMotionDownX, (int)mMotionDownY);
    }
到這里,拖放開始處理的框框基本清楚,但是DragView的創建和顯示還有必要進一步深究
        DragView dragView = mDragView = new DragView(mContext, b, registrationX, registrationY,
                textureLeft, textureTop, textureWidth, textureHeight);
//函數參數說明:
mContext = launcher
b = 根據拖放源view創建的大小一致的bitmap對象
registrationX = 手指點擊位置與拖放源view 坐標x方向的偏移       
registrationY = 手指點擊位置與拖放源view 坐標y方向的偏移       
textureLeft = 0
textureTop = 0
textureWidth = b.getWidth()
textureHeight =  b.getHeight()
//函數體
        super(context);
                                //獲取window管理器
        mWindowManager = WindowManagerImpl.getDefault();
        //一個動畫,開始拖放時顯示
        mTween = new SymmetricalLinearTween(false, 110 /*ms duration*/, this);
                                //對源b 做一個縮放產生一個新的bitmap對象
        Matrix scale = new Matrix();
        float scaleFactor = width;
        scaleFactor = mScale = (scaleFactor + DRAG_SCALE) / scaleFactor;
        scale.setScale(scaleFactor, scaleFactor);

        mBitmap = Bitmap.createBitmap(bitmap, left, top, width, height, scale, true);

        // The point in our scaled bitmap that the touch events are located
        mRegistrationX = registrationX + (DRAG_SCALE / 2);
        mRegistrationY = registrationY + (DRAG_SCALE / 2);
其實函數很簡單,就是記錄一些參數,然后對view圖片做一個縮放處理,並且准備一個tween動畫,在長按桌面圖標后圖標跳躍到手指上顯示該動畫,了解這些,有助於理解函數dragView.show
//windowToken來自與workspace.onattchtowindow時候獲取的view 所有attch的window標識,有這個參數,可以把dragview添加到
workspace所屬的同一個window對象
//touchX,手指點擊在屏幕的位置x
//touchy,手指點擊在屏幕的位置y
    public void show(IBinder windowToken, int touchX, int touchY) {
        WindowManager.LayoutParams lp;
        int pixelFormat;

        pixelFormat = PixelFormat.TRANSLUCENT;
        //布局參數值的注意的是view位置參數,
        //x=touchX-mRegistrationX=touchX-(registrationX + (DRAG_SCALE / 2))=手指點擊位置-view坐標與手指點擊位置偏差加上縮放值
        lp = new WindowManager.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                touchX-mRegistrationX, touchY-mRegistrationY,
                WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL,
                WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
                    | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS
                    /*| WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM*/,
                pixelFormat);
//        lp.token = mStatusBarView.getWindowToken();
        lp.gravity = Gravity.LEFT | Gravity.TOP;
        lp.token = windowToken;
        lp.setTitle("DragView");
        mLayoutParams = lp;
                                //dragview的父類是Window,也就是說dragview可以拖放到屏幕的任意位置
        mWindowManager.addView(this, lp);

        mAnimationScale = 1.0f/mScale;
        //播放開始拖動動畫(直觀感覺是圖標變大了)
        mTween.start(true);
    }

2,拖放過程
拖放過程的處理需要深入了解DragController.onTouchEvent(MotionEvent ev)函數的實現,我下面列出關鍵的MotionEvent.ACTION_MOVE部分代碼並作出注釋說明
                        case MotionEvent.ACTION_MOVE:
                                // 根據手指坐標移動dragview
                                mDragView.move((int) ev.getRawX(), (int) ev.getRawY());

                                // 根據手指所在屏幕坐標獲取目前所在的拖放目的view
                                final int[] coordinates = mCoordinatesTemp;
                                DropTarget dropTarget = findDropTarget(screenX, screenY, coordinates);
                                // 根據不同狀態調用DropTarget的生命周期處理函數
                                if (dropTarget != null) {
                                        if (mLastDropTarget == dropTarget) {
                                                dropTarget.onDragOver(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX,
                                                                (int) mTouchOffsetY, mDragView, mDragInfo);
                                        } else {
                                                if (mLastDropTarget != null) {
                                                        mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
                                                                        (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
                                                }
                                                dropTarget.onDragEnter(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX,
                                                                (int) mTouchOffsetY, mDragView, mDragInfo);
                                        }
                                } else {
                                        if (mLastDropTarget != null) {
                                                mLastDropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1], (int) mTouchOffsetX,
                                                                (int) mTouchOffsetY, mDragView, mDragInfo);
                                        }
                                }
                                mLastDropTarget = dropTarget;

                                //判斷是否在delete區域
                                boolean inDeleteRegion = false;
                                if (mDeleteRegion != null) {
                                        inDeleteRegion = mDeleteRegion.contains(screenX, screenY);
                                }
                                 //不在delete區域,在左邊切換區
                                if (!inDeleteRegion && screenX < SCROLL_ZONE) {
                                        if (mScrollState == SCROLL_OUTSIDE_ZONE) {
                                                mScrollState = SCROLL_WAITING_IN_ZONE;
                                                mScrollRunnable.setDirection(SCROLL_LEFT);
                                                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
                                        }
                                }
                                //不在delete區,在右邊切換區
                                else if (!inDeleteRegion && screenX > scrollView.getWidth() - SCROLL_ZONE) {
                                        if (mScrollState == SCROLL_OUTSIDE_ZONE) {
                                                mScrollState = SCROLL_WAITING_IN_ZONE;
                                                mScrollRunnable.setDirection(SCROLL_RIGHT);
                                                mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
                                        }
                                }
                                //在delete區域
                                else {
                                        if (mScrollState == SCROLL_WAITING_IN_ZONE) {
                                                mScrollState = SCROLL_OUTSIDE_ZONE;
                                                mScrollRunnable.setDirection(SCROLL_RIGHT);
                                                mHandler.removeCallbacks(mScrollRunnable);
                                        }
                                }

                                break;
拖 放過程總的處理思路就是根據當前坐標位置獲取dropTarget的目標位置,然后又根據相關狀態和坐標位置調用dropTarget的對應生命周期函 數,這里面有兩個點需要進一步深入了解,一是查找dropTarget:findDropTarget(screenX, screenY, coordinates),二是mHandler.postDelayed(mScrollRunnable, SCROLL_DELAY);
--1.findDropTarget
    private DropTarget findDropTarget(int x, int y, int[] dropCoordinates) {
        final Rect r = mRectTemp;
                                //mDropTargets是一個拖放目標view別表,在laucher初始化等被添加
        final ArrayList<DropTarget> dropTargets = mDropTargets;
        final int count = dropTargets.size();
        //遍歷dropTargets列表,查看{x,y}是否落在dropTarget坐標區域,若是,返回dropTarget。
        for (int i=count-1; i>=0; i--) {
            final DropTarget target = dropTargets.get(i);
            target.getHitRect(r);
            //獲取target左上角屏幕坐標
            target.getLocationOnScreen(dropCoordinates);
            r.offset(dropCoordinates[0] - target.getLeft(), dropCoordinates[1] - target.getTop());
            if (r.contains(x, y)) {
                dropCoordinates[0] = x - dropCoordinates[0];
                dropCoordinates[1] = y - dropCoordinates[1];
                return target;
            }
        }
        return null;
    }
--2.mScrollRunnable
// 看mScrollRunnable對象的構造類,通過setDirection設置滾動方向,然后通過一步調用 DragScroller.scrollLeft/scrollRight來對桌面進行向左向右滾動,想深入了解如何實現的,敬請閱讀我相關 blog:Launcher——桌面移動詳解
    private class ScrollRunnable implements Runnable {
        private int mDirection;

        ScrollRunnable() {
        }

        public void run() {
            if (mDragScroller != null) {
                if (mDirection == SCROLL_LEFT) {
                    mDragScroller.scrollLeft();
                } else {
                    mDragScroller.scrollRight();
                }
                mScrollState = SCROLL_OUTSIDE_ZONE;
            }
        }

        void setDirection(int direction) {
            mDirection = direction;
        }
    }
3.拖放結束,入口還是在DragController.onTouchEvent(MotionEvent ev)
        先看調用堆棧:
at com.android.launcher2.DragController.endDrag(DragController.java:315)
at com.android.launcher2.DragController.onTouchEvent(DragController.java:471)
at com.android.launcher2.DragLayer.onTouchEvent(DragLayer.java:64)
at android.view.View.dispatchTouchEvent(View.java:3766)
        onTouchEvent關鍵代碼:
                        case MotionEvent.ACTION_UP:
                                mHandler.removeCallbacks(mScrollRunnable);
                                if (mDragging) {
                                        // 拖動過程手指離開屏幕
                                        drop(screenX, screenY);
                                }
                                endDrag();
                                break;
--1.drop(screenX, screenY);
        final int[] coordinates = mCoordinatesTemp;
        //獲取dropTarget對象
        DropTarget dropTarget = findDropTarget((int) x, (int) y, coordinates);
        //coordinates=點觸點在dropTarget 中的xy坐標

        if (dropTarget != null) {
            dropTarget.onDragExit(mDragSource, coordinates[0], coordinates[1],
                    (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
                    //根據相關參數判斷是否可dropTarget是否接受該drag view
            if (dropTarget.acceptDrop(mDragSource, coordinates[0], coordinates[1],
                    (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo)) {
                dropTarget.onDrop(mDragSource, coordinates[0], coordinates[1],
                        (int) mTouchOffsetX, (int) mTouchOffsetY, mDragView, mDragInfo);
                mDragSource.onDropCompleted((View) dropTarget, true);
                return true;
            } else {
                mDragSource.onDropCompleted((View) dropTarget, false);
                return true;
            }
        }
        return false;


原文:http://blog.csdn.net/stonecao/article/details/6561631


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM