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

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

文章结构

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

长按图标触发拖动

深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动_第1张图片
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);
    }
}

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

深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动_第2张图片
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();//垃圾桶收回去,动画效果
    }
  }
}

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

深入剖析锤子onestep代码实现 - 下篇 - 长按图标拖动_第3张图片
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 mListeners = new ArrayList();
        //通知所有的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代码实现 - 下篇 - 长按图标拖动)