天天看點

Android4.4-Launcher源碼分析系列之關鍵的類和接口之DragLayer

一、DragLayer布局

上一篇文章分析過Launcher的布局,它是最外層的布局

<!-- Full screen view projects under the status bar and contains the background -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3"
    android:id="@+id/launcher"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@drawable/workspace_bg" >

    <com.android.launcher3.DragLayer
        android:id="@+id/drag_layer"
        android:layout_width="match_parent"
        android:layout_height="match_parent" >

        <!-- The workspace contains 5 screens of cells -->
           

它繼承自FrameLayout,它對應的類是DragLayer.

二、DragLayer代碼分析

官方解釋為:A ViewGroup that coordinates dragging across its descendants.意思是說它是一個協調處理它的子view拖動的容器.那麼很明顯,我們需要關注它對螢幕觸摸事件的處理.它有三個方法onTouchEvent,onInterceptTouchEvent和onInterceptHoverEvent.在分析這三個方法之前,先看下DragLayer的構造函數,它重寫了FrameLayout的一些屬性.

public DragLayer(Context context, AttributeSet attrs) {
        super(context, attrs);

        // 禁止多點觸控
        setMotionEventSplittingEnabled(false);
        // 按指定順序渲染子控件
        setChildrenDrawingOrderEnabled(true);
        // 偵聽子view的add和remove事件
        setOnHierarchyChangeListener(this);
        //拖動圖示到螢幕左邊緣的圖檔
        mLeftHoverDrawable = getResources().getDrawable(R.drawable.page_hover_left_holo);
        //拖動圖示到螢幕右邊緣的圖檔
        mRightHoverDrawable = getResources().getDrawable(R.drawable.page_hover_right_holo);
    }
           

我已經在代碼寫上了注釋,mLeftHoverDrawable是拖動桌面上的圖示到螢幕左側邊緣時顯示的圖檔,預設的圖檔實作的是高亮的效果,這裡我把它替換為手指的圖檔,便于觀看效果,如下圖

Android4.4-Launcher源碼分析系列之關鍵的類和接口之DragLayer

不了解android螢幕事件傳遞機制的可以先去了解下.接下來講上面提到的三個方法.

當點選螢幕時,會先進入onInterceptTouchEvent方法

@Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        int action = ev.getAction();

          if (action == MotionEvent.ACTION_DOWN) {
            if (handleTouchDown(ev, true)) {
            	 
            	return true;
            }
        } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
            if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
        }
        clearAllResizeFrames();
        return mDragController.onInterceptTouchEvent(ev);
    }
           

先判斷action是不是down,點選事件必然會執行down,那麼繼續判斷handleTouchDown(ev, true)是否為true,為true則攔截,這個方法是當正在縮放插件、打開檔案夾、編輯檔案夾名稱的時候傳回true.

private boolean handleTouchDown(MotionEvent ev, boolean intercept) {
        Rect hitRect = new Rect();
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        for (AppWidgetResizeFrame child: mResizeFrames) {
        	// 獲得widget縮放框占據的矩形區域的矩形坐标
        	child.getHitRect(hitRect);
        	// 動作如果碰到了子控件。檢查如果坐标處于縮放框可以對widget進行縮放操作的區域中,則禁止父控件再響應觸控事件
        	if (hitRect.contains(x, y)) {
                if (child.beginResizeIfPointInRegion(x - child.getLeft(), y - child.getTop())) {
                    mCurrentResizeFrame = child;
                    mXDown = x;
                    mYDown = y;
                    requestDisallowInterceptTouchEvent(true);
                    return true;
                }
            }
        }
        // 擷取目前打開的檔案夾。
         Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
         if (currentFolder != null && !mLauncher.isFolderClingVisible() && intercept) {
        	 // 如果點選的是檔案夾内區域并且正處于重命名狀态,則取消重命名
        	if (currentFolder.isEditingName()) {
                if (!isEventOverFolderTextRegion(currentFolder, ev)) {
                    currentFolder.dismissEditingName();
                    return true;
                }
            }
        	getDescendantRectRelativeToSelf(currentFolder, hitRect);
            // 如果檔案夾正在打開狀态。那麼如果點選的是檔案夾以外區域則關閉檔案夾。
            if (!isEventOverFolder(currentFolder, ev)) {
                mLauncher.closeFolder();
                return true;
            }
        }
        return false;
    }
           

判斷觸控事件是否經過了檔案夾圖示的文字區域的方法

private boolean isEventOverFolderTextRegion(Folder folder, MotionEvent ev) {
        getDescendantRectRelativeToSelf(folder.getEditTextRegion(), mHitRect);
        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
        	
        	return true;
        }
        return false;
    }
           

判斷觸控事件是否經過了檔案夾圖示區域的方法

private boolean isEventOverFolder(Folder folder, MotionEvent ev) {
        getDescendantRectRelativeToSelf(folder, mHitRect);
        if (mHitRect.contains((int) ev.getX(), (int) ev.getY())) {
            
        	return true;
        }
        return false;
    }
           

你們可以在裡面加上列印或者Toast驗證下,想出現檔案夾,就把一個圖示移動到另一個圖示上面,自動出現檔案夾.

在down下之後,如果handleTouchDown(ev, true)傳回為false,則進入到clearAllResizeFrames方法,這個方法是清除widget的所有縮放框,

widget不是可以調整大小嗎,當點選下去的時候就重置widget大小,該方法如下

/**
	 * 清除widget的所有縮放框
	 */
    public void clearAllResizeFrames() {
        if (mResizeFrames.size() > 0) {
            for (AppWidgetResizeFrame frame: mResizeFrames) {
                frame.commitResize();
                removeView(frame);
            }
            mResizeFrames.clear();
        }
    }
           

執行完這個方法後會傳回DragController的onInterceptTouchEvent值

return mDragController.onInterceptTouchEvent(ev);
           

如果你點選檢視會發現,如果正在拖動圖示則傳回true,否則傳回false.

當你點選下圖示就松開,那麼隻執行onInterceptTouchEvent方法.當你點選圖示之後拖動,那麼接下來會執行onTouchEvent方法

@Override
    public boolean onTouchEvent(MotionEvent ev) {
        boolean handled = false;
        int action = ev.getAction();
        int x = (int) ev.getX();
        int y = (int) ev.getY();

        if (action == MotionEvent.ACTION_DOWN) {
            if (handleTouchDown(ev, false)) {
            	return true;
            }
          } else if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
        	System.out.println("307");
        	if (mTouchCompleteListener != null) {
                mTouchCompleteListener.onTouchComplete();
            }
            mTouchCompleteListener = null;
            
        }

        if (mCurrentResizeFrame != null) {
            handled = true;
            switch (action) {
                case MotionEvent.ACTION_MOVE:
                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                     break;
                case MotionEvent.ACTION_CANCEL:
                case MotionEvent.ACTION_UP:
                    mCurrentResizeFrame.visualizeResizeForDelta(x - mXDown, y - mYDown);
                    mCurrentResizeFrame.onTouchUp();
                    mCurrentResizeFrame = null;
                   
            }
        }
        // 如果沒有正在調節widget大小也沒有打開檔案夾,則進入正題,開始拖動
        if (handled) return true;
        return mDragController.onTouchEvent(ev);
    }
           

它會調用DragController的onTouchEvent方法.交給子view處理.

最後是這個onInterceptHoverEvent方法,意思是如果沒有正在縮放插件、沒有打開檔案夾、沒有編輯檔案夾名稱。 DragLayer将處理觸控動作,

并取消一切widget的Resize動作(如果有)

@Override
    public boolean onInterceptHoverEvent(MotionEvent ev) {
        if (mLauncher == null || mLauncher.getWorkspace() == null) {
            return false;
        }
        Folder currentFolder = mLauncher.getWorkspace().getOpenFolder();
        if (currentFolder == null) {
            return false;
        } else {
                AccessibilityManager accessibilityManager = (AccessibilityManager)
                        getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
            if (accessibilityManager.isTouchExplorationEnabled()) {
                final int action = ev.getAction();
                boolean isOverFolder;
                switch (action) {
                    case MotionEvent.ACTION_HOVER_ENTER:
                        isOverFolder = isEventOverFolder(currentFolder, ev);
                        if (!isOverFolder) {
                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
                            mHoverPointClosesFolder = true;
                            return true;
                        } else if (isOverFolder) {
                            mHoverPointClosesFolder = false;
                        } else {
                            return true;
                        }
                    case MotionEvent.ACTION_HOVER_MOVE:
                        isOverFolder = isEventOverFolder(currentFolder, ev);
                        if (!isOverFolder && !mHoverPointClosesFolder) {
                            sendTapOutsideFolderAccessibilityEvent(currentFolder.isEditingName());
                            mHoverPointClosesFolder = true;
                            return true;
                        } else if (isOverFolder) {
                            mHoverPointClosesFolder = false;
                        } else {
                            return true;
                        }
                }
            }
        }
        return false;
    }
           

好了,關于DragLayer的介紹就這麼多了,關于其它的一些細節,我會在後面上傳注釋後的Launcher源碼的.