• 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

  • 相关阅读:
    B00009 C语言分割字符串库函数strtok
    B00009 C语言分割字符串库函数strtok
    I00026 计算数根
    I00026 计算数根
    I00025 寻找循环数
    Magic Stones CodeForces
    Continued Fractions CodeForces
    AtCoder Beginner Contest 116 D
    Applese 的毒气炸弹 G 牛客寒假算法基础集训营4(图论+最小生成树)
    Choosing The Commander CodeForces
  • 原文地址:https://www.cnblogs.com/xiaoxiaoboke/p/2342913.html
Copyright © 2020-2023  润新知