仿今日头条频道管理实现排序移动

写在前面:参考YoKey,感谢。
附上他的链接:http://www.jianshu.com/p/d30fd8da4eac

主页

界面我们分成4个viewType,分别是我的频道头,我的频道内容,其他频道头,其他频道内容。

    // 我的频道 标题部分
    public static final int TYPE_MY_CHANNEL_HEADER = 0;
    // 我的频道
    public static final int TYPE_MY = 1;
    // 其他频道 标题部分
    public static final int TYPE_OTHER_CHANNEL_HEADER = 2;
    // 其他频道
    public static final int TYPE_OTHER = 3;
  • 填充假数据
       //我的频道的数据
        final List myItems = new ArrayList<>();
        for (int i = 0 ; i < 20 ; i++){
            ChannelData channelData = new ChannelData();
            channelData.setName("频道"+i);
            myItems.add(channelData);
        }

        //其他频道的数据
        final List otherItems = new ArrayList<>();
        for (int i = 0 ; i < 20 ; i++){
            ChannelData channelData = new ChannelData();
            channelData.setName("其他"+i);
            otherItems.add(channelData);
        }
  • 首页代码
    开启网格模式
        //网格模式 4列
        GridLayoutManager gridLayoutManager = new GridLayoutManager(this,4);
        mRecy.setLayoutManager(gridLayoutManager);

使用ItemTouchHelper的大概模型,这里itemTouchHelper就是实现item拖拽和滑动的关键类,new对象的时候里面要放itemTouchHelper.CallBak回调。最后通过attachToRecyclerView方法绑定RecyclerView。

        ItemDragHelperCallback callback = new ItemDragHelperCallback();
        final ItemTouchHelper helper = new ItemTouchHelper(callback);//实现item的拖拽和滑动
        helper.attachToRecyclerView(mRecy);//通过attachToRecyclerView方法绑定RecyclerView

new adapter对象

        //adapter对象
        final ChannelAdapter adapter = new ChannelAdapter(this,helper,myItems,otherItems);

我们看看里面的四个参数,先看看ChannelAdapter的构造方法。

public ChannelAdapter(Context context, ItemTouchHelper mItemTouchHelper, List mMyChannelItems, List mOtherChannelItems) {
        this.mInflater = LayoutInflater.from(context);
        this.mItemTouchHelper = mItemTouchHelper;
        this.mMyChannelItems = mMyChannelItems;
        this.mOtherChannelItems = mOtherChannelItems;
    }

第一个参数上下文环境,第二个参数就是ItemTouchHelper对象,第三个参数是我的频道item列表,第四个参数是其他频道item列表。
因为4种类型都写在一个界面里了,但是头要单独占一行,非头的item要几个(具体数自己定)才占一行,这时通过网格布局管理器中setSpanSizeLookup方法进行操作了,这个方法会根据position来设置span size ,这个我们的span count是4 如果是头我们就给span size 为4 ,内容就给1。具体分析看代码

 //根据position来设置span size 这个我们的span count是4 如果是头我们就给span size 为4 内容就给1
        gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
            @Override
            public int getSpanSize(int position) {
                int viewType = adapter.getItemViewType(position);
                //三元运算 如果是内容就给1 是头我们就给span size 为4
                return viewType == ChannelAdapter.TYPE_MY || viewType == ChannelAdapter.TYPE_OTHER ? 1 : 4;
            }
        });
        mRecy.setAdapter(adapter);

我们看到里面有一个getItemViewType方法,点进去看下

    @Override
    public int getItemViewType(int position) {
        //不同position对应的viewType
        if (position == 0) {
            return TYPE_MY_CHANNEL_HEADER;
        } else if (position == mMyChannelItems.size() + 1) {
            return TYPE_OTHER_CHANNEL_HEADER;
        } else if (position > 0 && position < mMyChannelItems.size() + 1) {
            return TYPE_MY;
        } else {
            return TYPE_OTHER;
        }
    }

哦,原来是一个复写的方法,看看RecyclerView里对这个方法的描述

        /**
         * Return the view type of the item at position for the purposes
         * of view recycling.
         *
         * 

The default implementation of this method returns 0, making the assumption of * a single view type for the adapter. Unlike ListView adapters, types need not * be contiguous. Consider using id resources to uniquely identify item view types. * * @param position position to query * @return integer value identifying the type of the view needed to represent the item at * position. Type codes need not be contiguous. */ public int getItemViewType(int position) { return 0; }

原来是根据position的位置获取这个item的viewtype,那继续看我们复写的那个方法,position==0是那就是我的频道头,我的频道的item数量+1就是其他频道头,在这之间就是我的频道,剩下的就是其他频道了,这样类型就是出来,在往上看,三元运算符,意思很明显,就是当viewType是我的频道或者其他频道的时候返回值就为1,否则就为4,返回值就是span size。

ItemTouchHelper.Callback类

这里我们写了一个ItemTouchHelper.Callback的继承类。看看这类里面一些必须的方法。
设置滑动类型

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags;
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof GridLayoutManager || manager instanceof StaggeredGridLayoutManager){
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT ;
        }else {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        }

        // 如果想支持滑动(删除)操作, swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END
        int swipeFlags = 0 ;

        //返回一个整数类型的标识,用于判断Item那种移动行为是允许的
        return makeMovementFlags(dragFlags,swipeFlags);
    }

如上代码,如果属于网格类型,就可以上下左右的拖拽,不然就只能上下拖拽,滑动的话是禁止的。返回一个整数类型的标识,用于判断Item那种移动行为是允许的。
拖拽切换Item的回调

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        if (viewHolder.getItemViewType() != target.getItemViewType()){
            return false;
        }
        //instanceof 这个对象是否是这个特定类或者是它的子类的一个实例。
        if (recyclerView.getAdapter() instanceof OnItemMoveListener){
            OnItemMoveListener listener = (OnItemMoveListener) recyclerView.getAdapter();
            listener.onItemMove(viewHolder.getAdapterPosition(),target.getAdapterPosition());
        }
        return true;
    }

如果Item切换了位置,返回true;反之,返回false。onMove()是在拖动到新位置时候的回调方法,我们在这里做数组集合的交换操作,在这里我们把它暴露出去,交给Adapter自己处理。注意这里一个判断recyclerView.getAdapter() instanceof OnItemMoveListener,在写adapter的时候会实现OnItemMoveListener这个接口。

public interface OnItemMoveListener {
    void  onItemMove(int fromPosition,int toPosition);
}

这个类里还复写了item被选中和调用完毕后item的状态方法

    /**
     * item被选中时改变item的背景
     */
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        //不在闲置的状态
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE){
            if (viewHolder instanceof OnDragVHListener){
                OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
                itemViewHolder.onItemSelected();
            }
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    /**
     * 用户操作完毕或者动画完毕后调用,恢复item的背景和透明度
     */
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        if (viewHolder instanceof  OnDragVHListener){
            OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
            itemViewHolder.onItemFinished();
        }
        super.clearView(recyclerView, viewHolder);
    }

这里注意下OnDragVHListener

public interface OnDragVHListener {
    /**
     * Item被选中时触发
     */
    void onItemSelected();

    /**
     * Item在拖拽结束/滑动结束后触发
     */
    void onItemFinished();
}

后续中,因为拖拽操作只被设定在我的频道里操作,所以目前只有我的频道ViewHolder实现了OnDragVHListener,这里就改变了一下背景色。

    /**
     * 我的频道
     */
    class MyViewHolder extends RecyclerView.ViewHolder implements OnDragVHListener {
        private TextView textView;
        private ImageView imgEdit;

        public MyViewHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv);
            imgEdit = (ImageView) itemView.findViewById(R.id.img_edit);
        }

        /**
         * item 被选中时
         */
        @Override
        public void onItemSelected() {
            textView.setBackgroundResource(R.drawable.bg_channel_p);
        }

        /**
         * item 取消选中时
         */
        @Override
        public void onItemFinished() {

            textView.setBackgroundResource(R.drawable.bg_channel);

        }
    }

长按拖拽,这里其他频道不需要拖拽,所以返回false,对于我的频道则手动调用ItemTouchHelper的startDrag方法启动拖拽。

    /**
     * isLongPressDragEnabled()如果返回true,则支持长按拖拽,
     * 这里“其他频道”等不需要拖拽,所以返回false,手动调用ItemTouchHelper的startDrag方法启动拖拽。
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return false;
    }

    /**
     *  不支持滑动
     * @return false
     */
    @Override
    public boolean isItemViewSwipeEnabled() {
        return false;
    }

ChannelAdapter

对于这个类,吧代码贴上看吧,注释已经写的很详细了。

public RecyclerView.ViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
switch (viewType) {
        case TYPE_MY_CHANNEL_HEADER:
        case TYPE_MY:
        case TYPE_OTHER_CHANNEL_HEADER:
        case TYPE_OTHER:
        }
    return null;
    }

拆开看吧
我的频道头,就是进入和取消编辑状态

                case TYPE_MY_CHANNEL_HEADER:
                view = mInflater.inflate(R.layout.item_my_channel_header, parent, false);
                final MyChannelHeaderViewHolder holder = new MyChannelHeaderViewHolder(view);
                holder.tvBtnEdit.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        if (!isEditMode) {
                            startEditMode((RecyclerView) parent);
                            holder.tvBtnEdit.setText(R.string.finish);
                        } else {
                            cancelEditMode((RecyclerView) parent);
                            holder.tvBtnEdit.setText(R.string.edit);
                        }
                    }
                });
                return holder;

我的频道,这里有三种监听,一种是点击,一种是长按,还一种是触摸

                case TYPE_MY:
                view = mInflater.inflate(R.layout.item_my, parent, false);
                final MyViewHolder myViewHolder = new MyViewHolder(view);

                //点击
                myViewHolder.textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        int position = myViewHolder.getAdapterPosition();
                        if (isEditMode) {
                            RecyclerView recyclerView = (RecyclerView) parent;
                            //目标view也就是即将要去其他频道的那个item
                            // position是 mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER 也既是我的频道头+其他频道头+我的频道内容全部item
                            View targetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER);
                            View currentView = recyclerView.getLayoutManager().findViewByPosition(position);
                            // 如果targetView不在屏幕内,则indexOfChild为-1  此时不需要添加动画,因为此时notifyItemMoved自带一个向目标移动的动画
                            // 如果在屏幕内,则添加一个位移动画
                            if (recyclerView.indexOfChild(targetView) >= 0) {
                                int targetX, targetY;
                                //获取到网格布局的列数
                                RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
                                int spanCount = ((GridLayoutManager) manager).getSpanCount();

                                if ((mMyChannelItems.size() - 1) % spanCount == 0) {
                                    //这种情况 移动后高度发生变化 (少了一行)
                                    View preTargetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER - 1);
                                    targetX = preTargetView.getLeft();
                                    targetY = preTargetView.getTop();
                                } else {
                                    targetX = targetView.getLeft();
                                    targetY = targetView.getTop();
                                }

                                moveMyToOther(myViewHolder);
                                startAnimation(recyclerView, currentView, targetX, targetY);

                            } else {
                                //targetView不在屏幕内,则indexOfChild为-1  此时不需要添加动画
                                moveMyToOther(myViewHolder);
                            }

                        } else {
                            //不是编辑状态下点击我的频道item
                            mChannelItemClickListener.OnItemClick(v, position - 1);
                        }
                    }
                });

                //长按
                myViewHolder.textView.setOnLongClickListener(new View.OnLongClickListener() {
                    @Override
                    public boolean onLongClick(View v) {
                        if (!isEditMode) {
                            RecyclerView recyclerView = ((RecyclerView) parent);
                            startEditMode(recyclerView);

                            //header 按钮文字 改成"完成"
                            View view = recyclerView.getChildAt(0);
                            if (view == recyclerView.getLayoutManager().findViewByPosition(0)) {
                                TextView tvBtnEdit = (TextView) view.findViewById(R.id.tv_btn_edit);
                                tvBtnEdit.setText(R.string.finish);
                            }
                        }
                        mItemTouchHelper.startDrag(myViewHolder);
                        return true;
                    }
                });

                //touch
                myViewHolder.textView.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (isEditMode) {
                            switch (MotionEventCompat.getActionMasked(event)) {
                                case MotionEvent.ACTION_DOWN:
                                    startTime = System.currentTimeMillis();
                                    break;
                                case MotionEvent.ACTION_MOVE:
                                    if (System.currentTimeMillis() - startTime > SPACE_TIME) {
                                        mItemTouchHelper.startDrag(myViewHolder);
                                    }
                                    break;
                                case MotionEvent.ACTION_CANCEL:
                                case MotionEvent.ACTION_UP:
                                    startTime = 0;
                                    break;
                            }
                        }
                        return false;
                    }
                });
                return myViewHolder;

这里我想说下判断targetView在不在屏幕里的那个方法indexOfChild,以及item移动后高度发生变化的情况。
先看indexOfChild

    /**
     * Returns the position in the group of the specified child view.
     *
     * @param child the view for which to get the position
     * @return a positive integer representing the position of the view in the
     *         group, or -1 if the view does not exist in the group
     */
    public int indexOfChild(View child) {
        final int count = mChildrenCount;
        final View[] children = mChildren;
        for (int i = 0; i < count; i++) {
            if (children[i] == child) {
                return i;
            }
        }
        return -1;
    }

看他里面实现是把所有的指view都轮询一遍,看看我们传入的view在不在轮询的里面,不在就return -1。在看高度变化,想想在什么时候高度会发生变化,当我的频道多出一个item的时候,移动后我们这行少了。告诉发生变化,其他频道的位置也跟着变化,所有添加一个(mMyChannelItems.size() - 1) % spanCount == 0的判断。
其他频道

                case TYPE_OTHER:
                view = mInflater.inflate(R.layout.item_other, parent, false);
                final OtherViewHolder otherHolder = new OtherViewHolder(view);
                otherHolder.textView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        RecyclerView recyclerView = ((RecyclerView) parent);
                        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
                        int currentPiosition = otherHolder.getAdapterPosition();
                        // 如果RecyclerView滑动到底部,移动的目标位置的y轴 - height
                        View currentView = manager.findViewByPosition(currentPiosition);
                        // 目标位置的前一个item  即当前MyChannel的最后一个
                        View preTargetView = manager.findViewByPosition(mMyChannelItems.size() - 1 + COUNT_PRE_MY_HEADER);

                        // 如果targetView不在屏幕内,则为-1  此时不需要添加动画,因为此时notifyItemMoved自带一个向目标移动的动画
                        // 如果在屏幕内,则添加一个位移动画
                        if (recyclerView.indexOfChild(preTargetView) >= 0) {
                            int targetX = preTargetView.getLeft();
                            int targetY = preTargetView.getTop();

                            int targetPosition = mMyChannelItems.size() - 1 + COUNT_PRE_OTHER_HEADER;

                            GridLayoutManager gridLayoutManager = ((GridLayoutManager) manager);
                            int spanCount = gridLayoutManager.getSpanCount();
                            // target 在最后一行第一个
                            if ((targetPosition - COUNT_PRE_MY_HEADER) % spanCount == 0) {
                                View targetView = manager.findViewByPosition(targetPosition);
                                targetX = targetView.getLeft();
                                targetY = targetView.getTop();
                            } else {
                                targetX += preTargetView.getWidth();

                                // 最后一个item可见
                                if (gridLayoutManager.findLastVisibleItemPosition() == getItemCount() - 1) {
                                    // 最后的item在最后一行第一个位置
                                    if ((getItemCount() - 1 - mMyChannelItems.size() - COUNT_PRE_OTHER_HEADER) % spanCount == 0) {
                                        // RecyclerView实际高度 > 屏幕高度 && RecyclerView实际高度 < 屏幕高度 + item.height
                                        int firstVisiblePostion = gridLayoutManager.findFirstVisibleItemPosition();
                                        if (firstVisiblePostion == 0) {
                                            // FirstCompletelyVisibleItemPosition == 0 即 内容不满一屏幕 , targetY值不需要变化
                                            // // FirstCompletelyVisibleItemPosition != 0 即 内容满一屏幕 并且 可滑动 , targetY值 + firstItem.getTop
                                            if (gridLayoutManager.findFirstCompletelyVisibleItemPosition() != 0) {
                                                int offset = (-recyclerView.getChildAt(0).getTop()) - recyclerView.getPaddingTop();
                                                targetY += offset;
                                            }
                                        } else { // 在这种情况下 并且 RecyclerView高度变化时(即可见第一个item的 position != 0),
                                            // 移动后, targetY值  + 一个item的高度
                                            targetY += preTargetView.getHeight();
                                        }
                                    }
                                } else {
                                    System.out.println("呵呵哒");
                                }
                            }

                            // 如果当前位置是otherChannel可见的最后一个
                            // 并且 当前位置不在grid的第一个位置
                            // 并且 目标位置不在grid的第一个位置

                            // 则 需要延迟250秒 notifyItemMove , 这是因为这种情况 , 并不触发ItemAnimator , 会直接刷新界面
                            // 导致我们的位移动画刚开始,就已经notify完毕,引起不同步问题
                            if (currentPiosition == gridLayoutManager.findLastVisibleItemPosition()
                                    && (currentPiosition - mMyChannelItems.size() - COUNT_PRE_OTHER_HEADER) % spanCount != 0
                                    && (targetPosition - COUNT_PRE_MY_HEADER) % spanCount != 0) {
                                moveOtherToMyWithDelay(otherHolder);
                            } else {
                                moveOtherToMy(otherHolder);
                            }
                            startAnimation(recyclerView, currentView, targetX, targetY);

                        } else {
                            moveOtherToMy(otherHolder);
                        }
                    }
                });
                return otherHolder;

动画

rivate void startAnimation(RecyclerView recyclerView, final View currentView, int targetX, int targetY) {
        final ViewGroup viewGroup = (ViewGroup) recyclerView.getParent();
        final ImageView mirrorView = addMirrorView(viewGroup, recyclerView, currentView);

        Animation animation = getTranslateAnimator(
                targetX - currentView.getLeft(), targetY - currentView.getTop());
        currentView.setVisibility(View.INVISIBLE);
        mirrorView.startAnimation(animation);

        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                viewGroup.removeView(mirrorView);
                if (currentView.getVisibility() == View.INVISIBLE) {
                    currentView.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

添加镜像,镜像ImageView启动位移动画的同时,调用notifyItemMove。

/**
     * 添加需要移动的 镜像View
     */
    private ImageView addMirrorView(ViewGroup parent, RecyclerView recyclerView, View view) {
        view.destroyDrawingCache();
        view.setDrawingCacheEnabled(true);
        final ImageView mirrorView = new ImageView(recyclerView.getContext());
        Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
        mirrorView.setImageBitmap(bitmap);
        view.setDrawingCacheEnabled(false);
        int[] locations = new int[2];
        view.getLocationOnScreen(locations);
        int[] parenLocations = new int[2];
        recyclerView.getLocationOnScreen(parenLocations);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight());
        params.setMargins(locations[0], locations[1] - parenLocations[1], 0, 0);
        parent.addView(mirrorView, params);

        return mirrorView;
    }

代码下载

Demo代码下载(AS导到Module里)

你可能感兴趣的:(仿今日头条频道管理实现排序移动)