美文网首页Android进阶程序员
深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动

深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动

作者: YY17 | 来源:发表于2017-03-26 11:59 被阅读116次

    onestep长按图标拖动功能代码分析

    文章结构

    • 长按图标触发拖动
    • 触摸事件派发,拖动图标处理
    • 触摸事件派发,手指松开后处理

    长按图标触发拖动

    DragMOVE_start.jpg

    普通模式下,长按侧边栏图标,开始拖动图标的处理

    public class SidebarListView extends ListView {
      public SidebarListView(Context context, AttributeSet attrs,
            int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        mDivider = LayoutInflater.from(context).inflate(R.layout.sidebar_view_divider, null);
        //设置长按事件监听器
        setOnItemLongClickListener(mOnLongClickListener);
      }
    
      private AdapterView.OnItemLongClickListener mOnLongClickListener = new AdapterView.OnItemLongClickListener() {
        @Override
        public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
          ... ...
            //记录下拖动的条目
            mDraggedItem = (SidebarItem) SidebarListView.this.getAdapter().getItem(position);
            mDragPosition = position;
            //Drawable icon = mDraggedItem.getAvatar();
            Drawable icon = new BitmapDrawable(mContext.getResources(), BitmapUtils.drawableToBitmap(mDraggedItem.getAvatar()));
            //开始拖动,由侧边栏根视图开始处理,传入icon图像信息过去,后面用它画出来,跟随你的拖动显示
            SidebarController.getInstance(mContext).getSidebarRootView().startDrag(icon, view, viewLoc);
            //设置当前拖动的列表视图实例为此列表,因为后面需要处理图标移动,占位,挪位置的问题
            mSideView.setDraggedList(SidebarListView.this);
            //原图标消失,并触发视图树重新layout,从而前面getSidebarRootView().startDrag()里增加的视图layout监听器被回调
            view.setVisibility(View.INVISIBLE);
            return false;
        }
    };
    

    SidebarRootView的处理:

    public class SidebarRootView extends FrameLayout {
    //具体图标控件长按触发拖拽事件,这里,根视图负责开始处理
      public void startDrag(Drawable icon, View view, int[] loc) {
        if(mDragging || mShowDragViewWhenRelayout != null){
            return ;
        }
        //增加视图树layout监听器,在上一步的图标OnLongClick()处理中,原图标setVisibility(INVISIBLE)后触发视图树重绘,
        //从而回调此监听器
        mShowDragViewWhenRelayout = new ShowDragViewWhenRelayout(icon, view, loc);
        getViewTreeObserver().addOnGlobalLayoutListener(mShowDragViewWhenRelayout);
        //set sidebar to full screen
        //把SideView窗口全屏,是因为要在底部显示个垃圾桶,你可以拖过去做图标删除的动作
        SidebarController.getInstance(mContext).updateDragWindow(true);
      }
    
      private class ShowDragViewWhenRelayout implements ViewTreeObserver.OnGlobalLayoutListener {
        ...
    
        @Override
        public void onGlobalLayout() {
            getViewTreeObserver().removeOnGlobalLayoutListener(this);
            //只有这个地方设置mDragging为true,也就是唯一触发的点就是长按图标,然后dispatchTouchEvent()中的拖动处理就起作用了。
            mDragging = true;
            //拖动的dragview的显示、处理,包括位置更新、最后放手时,找到合适位置或删除。
            //把拖拽图标显示出来,构造,调用显示函数,在构造时,又同样增加了layout变化监听器,见下
            mDragView = new DragView(mContext, iconOrig, mListViewItem, mLoc);
            mDragView.showView();//触发构造函数中的layout监听器被回调,见下
            //显示垃圾桶
            mTrash.trashAppearWithAnim();
            //回收监听器实例
            mShowDragViewWhenRelayout = null;
        }
      }
    }
    

    拖动时的拖动图标处理类DragView的处理:

    public class DragView {
        public View mListViewItem;
        private Drawable mIcon;
        public final View mView;
        //构造,把原来的图标、view传进来,后面从view中获得当前开始拖动的位置,把待显示的伪图标,准备好,show的时候,才加到父视图里
        //增加了layout监听器
        public DragView(Context context, Drawable icon, View view, int[] loc) {
            //这是拖动的那个假view的layout
            mView = LayoutInflater.from(context).inflate(R.layout.drag_view, null);
            mIcon = icon;//被拖动的图标icon
            mListViewItem = view;
            mDragViewIcon = (ImageView) mView.findViewById(R.id.drag_view_icon);
            mDragViewIcon.setBackground(mIcon);//被拖动的图标icon,被重新设为拖动时图标
            mBubbleText = (TextView) mView.findViewById(R.id.drag_view_bubble_text);//跟随拖动图标的图标名称
            mBubbleText.setText(getDisplayName());
    
            //当show()被调用,通过addView()添加拖动图标到父视图中,触发视图树变化,回调此监听器,才让拖动图标可见
            mView.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                @Override
                public void onGlobalLayout() {
                    mView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
                    //拖动图标的名称,给个动画,逐渐显示出来
                    Anim bubbleAlpha = new Anim(mBubbleText, Anim.TRANSPARENT, 100, Anim.CUBIC_OUT, new Vector3f(), new Vector3f(0, 0, 1));
                    bubbleAlpha.start();
                    int[] screenLoc = new int[2];
                    mListViewItem.getLocationOnScreen(screenLoc);
                    int realIconWidth = mListViewItem.getWidth();
                    int realIconHeight = mListViewItem.getHeight();
                    int deltaX = mView.getWidth() / 2 - realIconWidth / 2;
                    int deltaY = mDragViewIcon.getHeight() / 2 - realIconHeight / 2;
                    int x = screenLoc[0] - deltaX;
                    int y = screenLoc[1] - mBubbleText.getHeight() - deltaY;
                    mView.setTranslationX(x);
                    mView.setTranslationY(y);
                    //设置显示,这时,拖拽图标真正显示出来,真是千呼万唤始出来
                    mView.setVisibility(View.VISIBLE);
                }
            });
        }
    
        //用于拖动显示的伪图标dragview先添加到父视图中,真正显示可见却是在构造函数中的ViewTreeObserver.OnGlobalLayoutListener监听器里
        public void showView() {
            //用于拖动显示的伪图标,先隐藏
            mView.setVisibility(View.INVISIBLE);
            //用于拖动显示的伪图标dragview先添加到侧边栏视图中,就你正在拖动的那个伪图标
            //因为DragView是内部类,所以可以调用外围类的方法,这里addView()其实就是SidebarRootView.addView()
            //这一句触发视图树变化,因此,构造函数中的ViewTreeObserver.OnGlobalLayoutListener监听器将被回调
            addView(mView);
        }
    }
    

    触摸事件派发,拖动图标处理

    DragMOVE_touch_DOWN_MOVE.jpg

    Touch事件从ViewRootImpl派发下来,首先依次派发到三个根视图,即SidebarRootView,ContentView和TopView
    但可以看到,只有TopView和SidebarRootView重写了dispatchTouchEvent,且只有SidebarRootView满足条件并处理了,
    显然,此时长按拖动事件,本应该由侧边栏相关视图类来处理。

    public class SidebarRootView extends FrameLayout {
      @Override
      public boolean dispatchTouchEvent(MotionEvent ev) {//触摸事件由此开始,触摸1、顶层处理,拖放处理
        //处理触摸,拖动图标,因为在长按图标时,前面回调到startDrag时,就设置为true,即Dragging状态了
        if (mDragging) {
            precessTouch(ev);//拖动状态下,所有事件由它处理
            return true;//返回true,截断触摸事件,不再向下派发
        }
    
        return super.dispatchTouchEvent(ev);
    }
    
    
     //6-7、处理触摸,拖动图标
    private void precessTouch(MotionEvent event) {
        ...
        switch (action) {
            case MotionEvent.ACTION_DOWN : {
                if (ENABLE_TOUCH_LOG) log.error("ACTION_DOWN");
                break;
            }
            //触摸放开,处理拖动结果,两种情况,垃圾桶和新位置
            case MotionEvent.ACTION_CANCEL:
            case MotionEvent.ACTION_UP : {
                if (ENABLE_TOUCH_LOG) log.error("ACTION_UP");
                //情况一:垃圾桶
                if (mTrash.dragObjectUpOnUp(x, y, mDragView)) {
                    //handle uninstall
                } else {//情况二,侧边栏放下,新位置
                    dropDrag();
                    mTrash.trashDisappearWithAnim(null);//垃圾桶,动画退出
                }
                if (mDragView != null) {//让DragScrollView取消滚动处理
                    // we need action_update to stop scroll !
                    mSideView.dragObjectMove(event, eventTime);
                }
                break;
            }
            //DragView拖动,更新显示
            case MotionEvent.ACTION_MOVE : {
                if (ENABLE_TOUCH_LOG) log.error("ACTION_MOVE");
                if (mDragView != null) {
                    mDragView.move(x, y);//拖动图标移动
                    mSideView.dragObjectMove(event, eventTime);//侧栏边当前列表,要判断占位,挪动图标
                    mTrash.dragObjectMoveTo(x, y);//垃圾桶,要判断是否拖进来,根据触摸位置区域,垃圾桶要冒出来或者收回去
                }
                break;
            }
            case MotionEvent.ACTION_SCROLL : {//忽略,不做处理。ACTION_MOVE事件传递给了DragScrollView去处理了,但貌似结果也是没滚动
                break;
            }
        }
    }
    
    //DragView对拖动触摸事件ACTION_MOVE的处理,更新位置显示
    class DragView{
        public void move(float touchX, float touchY) {
            int x = (int) (touchX - mView.getWidth() / 2);
            int y = (int) (touchY - mDragViewIcon.getHeight() / 2 - mBubbleText.getHeight());
            //直接根据触摸点,移动DragView伪图标的位置
            mView.setTranslationX(x);
            mView.setTranslationY(y);
        }
    }
    

    }

    SideView对拖动触摸事件ACTION_MOVE的处理:

    public class SideView extends RelativeLayout {
        public void dragObjectMove(MotionEvent event, long eventTime) {
        //长按图标时,mDraggedListView就设置了的,也是唯一设置的地方
        //也就是当前拖动的列表,要处理图标占位、挪位的问题,见下
          mDraggedListView.dragObjectMove((int)(event.getRawX()), (int)(event.getRawY()));
        }
    }
    

    当前mDraggedListView,即SidebarListView对拖动触摸事件ACTION_MOVE的处理:

    public class SidebarListView extends ListView {
    
    public void dragObjectMove(int rawX, int rawY) {
        if (Utils.inArea(rawX, rawY, this)) {//图标在此列表区域内时,需要处理
            int count = getAdapter().getCount() - getHeaderViewsCount() - getFooterViewsCount();
            if (count > 0) {//列表里图标数不为0
        ...
                int[] localLoc = convertToLocalCoordinate(rawX, rawY, drawingRect);
                int subViewHeight = drawingRect.bottom / getChildCount();
                //获得当前触摸点在列表中的条目位置
                int position = localLoc[1] / subViewHeight;
                //在此函数处理
                pointToNewPositionWithAnim(position);
            }
        }
    }
    
     //根据拖动图标的位置,去做图标位置的调整
    private void pointToNewPositionWithAnim(int position) {
        int headViewCount = getHeaderViewsCount();
        int count = getCount() - getFooterViewsCount() - headViewCount;
        int begin = headViewCount;
        int end = begin + count;
        // check invisible count
        int invisibleViewCount = 0;
        for (int i = begin; i < end; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() != View.VISIBLE) {
                invisibleViewCount++;//不可见的子视图图标的数量
            }
        }
    
        mDragPosition = position;//记录这一次拖动图标位置
        position -= this.getHeaderViewsCount();//减去列表头部条目,获得在内容条目中的序号位置
        View[] viewArr = new View[count];
        int index = 0;
        for (int i = begin; i < end; i++) {
            View view = getChildAt(i);
            if (view.getVisibility() == View.INVISIBLE) {
                viewArr[position] = view;//不可见的子视图,就是当前拖动图标原来的条目视图,应该是唯一一个不可见
            } else {//可见的子视图
                if (index == position) {//跳过与当前在拖动的目标位置相同的位置,即让位,而此位置也即viewArr[position];在上一个语句已经记录了,就是拖动的原来条目视图,INVISIBLE
                    index++;
                }
                viewArr[index++] = view;
            }
        }
        AnimTimeLine moveAnimTimeLine = new AnimTimeLine();
        int toY = 0;
        for (int i = 0; i < headViewCount; ++i) {
            toY += getChildAt(i).getHeight();//计算第0个内容列表的图标,它的位置应该是去除列表头图标,比如那个切换应用的固定图标
        }
        //为需要挪动的图标加上动画
        for (int i = 0; i < viewArr.length; i++) {
            View view = viewArr[i];
            int fromY = (int) view.getY();
            //图标的原位置与目标位置不同,就加动画,去挪到目标位置,viewArr包括了不可见的拖动原图标视图,
            //这样就是已经导致列表图标位置发生改变了
            if (fromY != toY) {
                Anim anim = new Anim(view, Anim.TRANSLATE, 200, Anim.CUBIC_OUT, from, to);
                moveAnimTimeLine.addAnim(anim);
            }
            toY += view.getHeight();//从第0个开始,不断加上图标间隔
        }
        //动画开始,动画结束,图标的位置保持结束时的位置,即完成了图标调整
        moveAnimTimeLine.start();
      }
    }
    

    垃圾桶Trash对拖动触摸事件ACTION_MOVE的处理:

      class Trash{
       public void dragObjectMoveTo(float x, float y) {
          if (inTrashReactArea(x, y)) {
            //in trash area
            trashFloatUpWithAnim(null); //垃圾桶冒出来,动画效果
           } else {
            //out trash area
            trashFallDownWithAnim();//垃圾桶收回去,动画效果
        }
      }
    }
    

    触摸事件派发,手指松开后处理

    DragMOVE_touch_UP.jpg
        public class SidebarRootView extends FrameLayout {
            private void precessTouch(MotionEvent event) {
                ... ...
                switch (action) {
                    //触摸放开,处理拖动结果,两种情况
                    case MotionEvent.ACTION_CANCEL:
                    case MotionEvent.ACTION_UP : {
                        if (ENABLE_TOUCH_LOG) log.error("ACTION_UP");
                        //情况二:放进垃圾桶
                        if (mTrash.dragObjectUpOnUp(x, y, mDragView)) {
                            //handle uninstall
                        } else {//情况一,侧边栏放下,重新排序图标
                            dropDrag();
                            mTrash.trashDisappearWithAnim(null);//垃圾桶,动画退出
                        }
                        if (mDragView != null) {//让DragScrollView取消滚动处理
                            // we need action_update to stop scroll !
                            mSideView.dragObjectMove(event, eventTime);
                        }
                        break;
                    }
                }
            }
    
            //情况一,侧边栏放下拖放图标的后续处理,先来个动画
            public void dropDrag() {
                ... ...
                mDragDroping = true;//标记拖动状态为放下图标,后续会用到
                //calculate icon loc, bubble will hide, move icon to right loc
                ...
                mDragView.mView.setTranslationX(iconLoc[0]);
                mDragView.mView.setTranslationY(iconLoc[1]);
                mDragView.hideBubble();
    
                final ViewTreeObserver observer = mDragView.mView.getViewTreeObserver();
                observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                        observer.removeOnGlobalLayoutListener(this);
                        //来个动画,拖动图标从手指的位置移动进去目标位置,且图标从大到小变化,因为拖动图标在拖动时是被放大了显示的
                        DropAnim anim = new DropAnim(mDragView);
                        anim.setDuration(200);
                        mDragView.mView.startAnimation(anim);
                    }
                });
            }
    
            
            private class DropAnim extends Animation {
                ... ...
                //动画结束,删除拖动伪图标,更新排序后数据到数据库, 处理图标列表位置更新,最后变正常显示
                private void complete() {
                    mDragging = false;
                    mDragDroping = false;
                    //里面调用了内容列表视图处理位置更新,包括通知数据适配器,数据库更新
                    //这才是拖动排序的最终关键处理,真是历尽千辛万苦
                    mDragView.backToPostion();
                    mDragView.removeView(); //伪图标被删除
                    //侧边栏,取消全屏,恢复正常宽度
                    SidebarController.getInstance(mContext).updateDragWindow(false);
                }
            }
    
            //DragView对拖动触摸事件ACTION_UP/CANCEL的处理,转发给视图列表去更新位置显示,但其实是先更新数据库,再辗转回来更新视图
            class DragView{
                //内容列表图标更新显示,交由原来通过setDraggedList注入进来的SidebarListView处理
                public void backToPostion() {
                    mListViewItem.setVisibility(View.VISIBLE);//原来被拖动的原始图标变成可见
                    //原来开始图标长按时,已经有通过setDraggedList记下是那个SidebarListView实例了
                    mSideView.getDraggedListView().dropBackSidebarItem();
                }
            }
        }
    
        public class SidebarListView extends ListView {
            //目标条目图标更新位置
            public void dropBackSidebarItem() {
                if (mDraggedItem != null) {
                    if (mAdapter != null) {
                        //适配器数据更新,也就是条目数据索引更新,后面才真正反映到列表视图上
                        mAdapter.moveItemPostion(mDraggedItem, mDragPosition - this.getHeaderViewsCount());
                    }
                    //拖动结束,侧边栏正常显示,善后一些变量
                    dragEnd();
                }
            }
    
            public void onDragEnd() {
                if (mAdapter != null) {
                    mAdapter.onDragEnd();
                }
            }
    
        }
    
        public class AppListAdapter extends SidebarAdapter {
        //对列表条目数据进行重新排序,然后,通知视图列表更新,通知数据库管理类更新
            @Override
            public void moveItemPostion(Object object, int index) {
                index --;
                AppItem item = (AppItem)object;
               ...
                mAppItems.remove(item);
                mAppItems.add(index, item);
                onOrderChange();
            }
    
            
            private void onOrderChange() {
                for(int i = 0; i < mAppItems.size(); ++ i){
                    mAppItems.get(i).setIndex(mAppItems.size() - 1 - i);
                }
                //通知数据库更新
                mManager.updateOrder();
            }
        }
    
        public class AppManager extends DataManager {
            public void updateOrder() {
                synchronized (mAddedAppItems) {
                    Collections.sort(mAddedAppItems, new AppItem.IndexComparator());
                }
                notifyListener();//通知监听器,这是父类DataManager的方法,见下文
                mHandler.obtainMessage(MSG_SAVE_ORDER).sendToTarget();//异步去更新数据库
            }
    
            private AppManager(Context context) {
                //构造时就搞了个子线程在跑了,有消息来,就干活
                HandlerThread thread = new HandlerThread(ResolveInfoManager.class.getName());
                thread.start();
                mHandler = new AppManagerHandler(thread.getLooper());
                ... ...
            }
    
            //消息来了,干活
            private class AppManagerHandler extends Handler {
                ... ...
    
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    case MSG_SAVE_ORDER: //更新所有重新排序了的数据
                        mDatabase.saveOrderForList(getAddedAppItem());
                        ... ...
                    }
                }
            }
        }
    
            
        public abstract class DataManager {
            private List<RecentUpdateListener> mListeners = new ArrayList<RecentUpdateListener>();
            //通知所有的RecentUpdateListener
            protected void notifyListener(){
                for(RecentUpdateListener lis : mListeners){
                    lis.onUpdate();
                }
            }
        }
    
    
        public class AppListAdapter extends SidebarAdapter {
            //数据更新监听器
            private DataManager.RecentUpdateListener resolveInfoUpdateListener = new DataManager.RecentUpdateListener() {
                @Override
                public void onUpdate() {
                    // do anim first !
                    new Handler(Looper.getMainLooper()).post(new Runnable() {
                        @Override
                        public void run() {
                            //通知视图列表更新
                            mListView.animWhenDatasetChange();
                        }
                    });
                }
            };
        }
    
        public class SidebarListView extends ListView {
        //适配数据更新调用过来,视图列表更新图标列表
            public void animWhenDatasetChange() {
                ...
                int time = 200;
                mDatasetChangeTimeLine = new AnimTimeLine();
                ListAdapter adapter = getAdapter();
                if (adapter != null) {
                    for (int i = 0; i < adapter.getCount(); ++i) {
                        Object obj = adapter.getItem(i);
                        if (obj != null && obj instanceof SidebarItem) {
                            SidebarItem item = (SidebarItem) obj;
                            if (item.newAdded) {
                                ...
                                mDatasetChangeTimeLine.addAnim(alphaAnim);
                                mDatasetChangeTimeLine.addAnim(scaleBigAnim);
                                mDatasetChangeTimeLine.addAnim(scaleNormal);
                            } else if(item.newRemoved) {
                                ...
                                mDatasetChangeTimeLine.addAnim(alphaAnim);
                                mDatasetChangeTimeLine.addAnim(scaleBigAnim);
                            } else {
                                view.setAlpha(1.0f);
                                view.setScaleX(1.0f);
                                view.setScaleY(1.0f);
                            }
                        }
                    }
                }
                if (mListener != null) {
                    if (mDatasetChangeTimeLine.getAnimList() == null
                            || mDatasetChangeTimeLine.getAnimList().size() == 0) {
                        mListener.onComplete(0);
                    } else {
                        mDatasetChangeTimeLine.setAnimListener(mListener);
                    }
                }
                //动画开始,动画结束会回调AnimListener mListener
                mDatasetChangeTimeLine.start();
            }
    
                private AnimListener mListener = new AnimListener() {
    
                @Override
                public void onStart() {
                }
    
                @Override
                public void onComplete(int type) {//动画结束
                    if(mAdapter != null) {
                        mAdapter.updateData();//更新数据
                    }
                }
            };
        }
    
        public class AppListAdapter extends SidebarAdapter {
            //更新数据
            public void updateData() {
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        mAppItems = mManager.getAddedAppItem();//从数据库重新获取数据
                        notifyDataSetChanged();//通知视图列表更新
                        ... ...
                    }
                });
            }
        }
    

    至此,拖动的伪图标被删除,侧边栏图标列表按拖动后的位置更新数据、并显示,侧边栏宽度恢复到正常宽度,垃圾桶归位,一切恢复正常。

    情况二,放进垃圾桶的处理:
    垃圾桶删除图标比较简单,关键是显示个对话框,根据用户选择,决定是否删除图标

        class Trash{
            //如果拖动到垃圾桶有效区域,则作删除图标的处理
            public boolean dragObjectUpOnUp(float x, float y, DragView dragView) {
                if (!inTrashUninstallReactArea(x, y)) {
                    return false;
                }
                //move icon to trash
                moveIconToTrash(dragView);//来个动画,图标在垃圾桶上边悬浮摇动
                mUninstallAction = new UninstallAction(mContext, dragView);
                //显示删除图标的对话框,有意思的是,如果选择不删除,还会有个动画,把图标从垃圾桶丢回侧边栏
                mUninstallAction.showUninstallDialog();
                return true;
            }
        }
    

    相关文章

      网友评论

        本文标题:深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动

        本文链接:https://www.haomeiwen.com/subject/pupvnttx.html