您的位置:首页 > 移动开发 > Android开发

android launcher3拖放功能深入剖析

2015-12-04 16:22 543 查看
Launcherr有一个相对比较复杂的功能就是拖放功能,要深入了解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;
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: