欢迎访问移动开发之家(rcyd.net),关注移动开发教程。移动开发之家  移动开发问答|  每日更新
页面位置 : > > > 内容正文

基于AndroidRecyclerView实现宫格拖拽效果,

来源: 开发者 投稿于  被查看 32375 次 评论:34

基于AndroidRecyclerView实现宫格拖拽效果,


目录
  • 前言
  • 效果
  • 拖拽效果原理
    • 事件处理
    • 图像平移
    • 数据更新
  • 本篇实现
    • 图片分片
    • 更新数据
  • 总结

    前言

    在Android发展的进程中,网格布局一直比较有热度,其中一个原因是对用户来说便捷操作,对app厂商而言也会带来很多的曝光量,对于很多头部app,展示网格菜单几乎是必选项。实现网格的方式有很多种,比如GridView、GridLayout,TableLayout等,实际上,由于RecyclerView的灵活性和可扩展性很高,这些View基本没必要去学了,为什么这样说呢?主要原因是基于RecyclerView可以实现很多布局效果,传统的很多Layout都可以通过RecyclerView去实现,比如ViewPager、SlingTabLayout、DrawerLayout、ListView等,甚至连九宫格解锁效果也可以实现。

    当然,在很早之前,实现网格的拖拽效果主要是通过GridView去实现的,如果列数为1的话,那么GridView基本上就实现了ListView一样的上下拖拽。

    话说回来,我们现在基本不用去学习这类实现了,因为RecyclerView足够强大,通过简单的数据组装,是完全可以替代GridView和ListView的。

    效果

    本篇我们会使用RecyclerView来实现网格拖拽,本篇将结合图片分片案例,实现拖拽效果。

    如果要实现网格菜单的拖拽,也是可以使用这种方式的,只要你的想象丰富,理论上,借助RecyclerView其实可以做出很多效果。

    拖拽效果原理

    拖动其实需要处理3个核心的问题,事件、图像平移、数据交换。

    事件处理

    实际上无论传统的拖拽效果还是最新的拖拽效果,都离不开事件处理,不过,好处就是,google为RecyclerView提供了ItemTouchHelper来处理这个问题,相比传统的GridView实现方式,省去了很多事情,如动画、目标查找等。

    不过,我们回顾下原理,其实他们很多方面都是相似的,不同之处就是ItemTouchHelper 设计的非常好用,而且接口暴露的非常彻底,甚至能控制那些可以拖动、那些不能拖动、以及什么方向可以拖动,如果我们上、下、左、右四个方向都选中的话,斜对角拖动完全没问题,

    事件处理这里,GridView使用的方式相对传统,而ItemTouchHelper借助RecyclerView的一个接口(看样子是开的后门),通过View自身去拦截事件.

    public interface OnItemTouchListener {
        //是否让RecyclerView拦截事件
        boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
        //拦截之后处理RecyclerView的事件
        void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
        //监听禁止拦截事件的请求结果
        void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
    }
    

    这种其实相对GridView来说简单的多

    图像平移

    无论是RecyclerView和传统GridView拖动,都需要图像平移。我们知道,RecyclerView和GridView本身是通过子View的边界(left\top\right\bottom)来移动的,那么,在平移图像的时候必然不能选择这种方式,只能选择Matrix 变化,也就是transitionX和transitionY的等。不同点是GridView的子View本身并不移动,而是将图像绘制到一个GridView之外的View上,当然,实现上是比较复杂的。

    但是,ItemTouchHelper设计比较巧妙的一点是,通过RecyclerView#ItemDecoration来实现,在捕获可以滑动的View之后,在绘制时对View进行偏移。

    class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
        static final ItemTouchUIUtil INSTANCE =  new ItemTouchUIUtilImpl();
    
        @Override
        public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
                int actionState, boolean isCurrentlyActive) {
            if (Build.VERSION.SDK_INT >= 21) {
                if (isCurrentlyActive) {
                    Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
                    if (originalElevation == null) {
                        originalElevation = ViewCompat.getElevation(view);
                        float newElevation = 1f + findMaxElevation(recyclerView, view);
                        ViewCompat.setElevation(view, newElevation);
                        view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
                    }
                }
            }
    
            view.setTranslationX(dX);
            view.setTranslationY(dY);
        }
         //省略一些有关或者无关的代码
    }
    

    不过,我们看到,Android 5.0的版本借助了setElevation 使得被拖拽View不被其他顺序的View遮住,那Android 5.0之前是怎么实现的呢?

    其实,做过TV app的都比较清楚,子View绘制顺序可以通过下面方式调整,借助下面的方法,在TV上某个View获取焦点之后,就不会被后面的View盖住。

    View#getChildDrawingOrder
    

    ItemTouchHelper 同样借助了此方法,为什么不统一一种呢,主要原因是getChildDrawingOrder是protected,总的来说,没有通过setElevation方便。

    private void addChildDrawingOrderCallback() {
        if (Build.VERSION.SDK_INT >= 21) {
            return; // we use elevation on Lollipop
        }
        if (mChildDrawingOrderCallback == null) {
            mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
                @Override
                public int onGetChildDrawingOrder(int childCount, int i) {
                    if (mOverdrawChild == null) {
                        return i;
                    }
                    int childPosition = mOverdrawChildPosition;
                    if (childPosition == -1) {
                        childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
                        mOverdrawChildPosition = childPosition;
                    }
                    if (i == childCount - 1) {
                        return childPosition;
                    }
                    return i < childPosition ? i : i + 1;
                }
            };
        }
        mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
    }
    

    数据更新

    数据更新这里其实ReyclerView的优势更加明显,我们知道RecyclerView可以做到无requestLayout的局部刷新,性能更好。

    @Override
    public boolean onItemMove(int fromPosition, int toPosition) {
        Collections.swap(mDataList, fromPosition, toPosition);
        notifyItemMoved(fromPosition, toPosition);
        return true;
    }
    

    不过,数据交换后还有一点需要处理,对Matrix相关属性清理,防止无法落到指定区域。

    @Override
    public void clearView(View view) {
        if (Build.VERSION.SDK_INT >= 21) {
            final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
            if (tag instanceof Float) {
                ViewCompat.setElevation(view, (Float) tag);
            }
            view.setTag(R.id.item_touch_helper_previous_elevation, null);
        }
    
        view.setTranslationX(0f);
        view.setTranslationY(0f);
    }
    

    本篇实现

    以上基本都是对ItemTouchHelper的原理梳理了,当然,如果你没时间看上面的话,就看实现部分吧。

    图片分片

    下面我们把多张图片分割成 [行数 x 列数]数量的图片。

    Bitmap srcInputBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.image_4);
    Bitmap source = Bitmap.createScaledBitmap(srcInputBitmap, width, height, true);
    srcInputBitmap.recycle();
    
    int colCount = spanCount;
    int rowCount = 6;
    
    int spanImageWidthSize = source.getWidth() / colCount;
    int spanImageHeightSize = (source.getHeight() - rowCount * padding/2) / rowCount;
    
    Bitmap[] bitmaps = new Bitmap[rowCount * colCount];
    for (int i = 0; i < rowCount; i++) {
        for (int j = 0; j < colCount; j++) {
            int y = i * spanImageHeightSize;
            int x = j * spanImageWidthSize;
            Bitmap bitmap = Bitmap.createBitmap(source, x, y, spanImageWidthSize, spanImageHeightSize);
            bitmaps[i * colCount + j] = bitmap;
        }
    }
    

    在这种过程我们一定要处理一个问题,如果我们对网格设置了边界线(ItemDecoration)且是纵向布局的话,那么,纵向总高度要减去rowCount * bottomPadding,这里bottomPadding == padding/2,如下面代码。

    为什么要这么做呢?因为RecyclerView计算高度的时候,需要考虑这个高度,如果不去处理,那么ReyclerView可能不是禁止不动,而是会滑动,虽然影响不大,但是如果实现全屏效果,还能上下滑的话体验比较差。

    public class SimpleItemDecoration extends RecyclerView.ItemDecoration {
    
        public int delta;
        public SimpleItemDecoration(int padding) {
            delta = padding;
        }
    
        @Override
        public void getItemOffsets(Rect outRect, View view,
                                   RecyclerView parent, RecyclerView.State state) {
            int position = parent.getChildAdapterPosition(view);
            RecyclerView.Adapter adapter = parent.getAdapter();
            int viewType = adapter.getItemViewType(position);
            if(viewType== Bean.TYPE_GROUP){
                return;
            }
            GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
             //列数量
            int cols = layoutManager.getSpanCount(); 
            //position转为在第几列
            int current =  layoutManager.getSpanSizeLookup().getSpanIndex(position,cols); 
            //可有可无
            int currentCol = current % cols;
    
    
            int bottomPadding = delta / 2;
    
            if (currentCol == 0) {  //第0列左侧贴边
                outRect.left = 0;
                outRect.right = delta / 4;
                outRect.bottom = bottomPadding;
            } else if (currentCol == cols - 1) {
                outRect.left = delta / 4;
                outRect.right = 0;
                outRect.bottom = bottomPadding;
                 //最后一列右侧贴边
            } else {
                outRect.left = delta / 4;
                outRect.right = delta / 4;
                outRect.bottom = bottomPadding;
            }
        }
    }
    

    更新数据

    这部分是常规操作,主要目的是设置LayoutManager、Decoration、Adapter以及ItemTouchHelper,当然,ItemTouchHelper比较特殊,因为其内部试下是ItemTouchHelper、OnItemTouchListener、Gesture的组合,因此封装为attachToRecyclerView 来调用。

    mLinearLayoutManager = new GridLayoutManager(this, spanCount, LinearLayoutManager.VERTICAL, false);
    mLinearLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
        @Override
        public int getSpanSize(int position) {
            if(mAdapter.getItemViewType(position) == Bean.TYPE_GROUP){
                return spanCount;
            }
            return 1;
        }
    });
    mAdapter = new RecyclerViewAdapter();
    mRecyclerView.setAdapter(mAdapter);
    mRecyclerView.setLayoutManager(mLinearLayoutManager);
    mRecyclerView.addItemDecoration(new SimpleItemDecoration(padding));
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GridItemTouchCallback(mAdapter));
    itemTouchHelper.attachToRecyclerView(mRecyclerView);
    

    这里,我们主要还是关注ItemTouchHelper,在初始化的时候,我们给了一个GridItemTouchCallback,用于监听相关处理逻辑,最终通知Adapter调用notifyXXX更新View。

    public class GridItemTouchCallback extends ItemTouchHelper.Callback {
        private final ItemTouchCallback mItemTouchCallback;
        public GridItemTouchCallback(ItemTouchCallback itemTouchCallback) {
            mItemTouchCallback = itemTouchCallback;
        }
        @Override
        public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
            // 上下左右拖动,但允许触发删除
            int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            return makeMovementFlags(dragFlags, 0);
        }
    
        @Override
        public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
            // 通知Adapter移动View
            return mItemTouchCallback.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        }
        @Override
        public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
            // 通知Adapter删除View
            mItemTouchCallback.onItemRemove(viewHolder.getAdapterPosition());
        }
    
        @Override
        public void onChildDraw(@NonNull Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }
        @Override
        public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
            Log.d("GridItemTouch","dx="+dX+", dy="+dY);
            super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
        }
    }
    

    这里,主要是对Flag的关注需要处理,第一参数是拖拽方向,第二个是删除方向,我们本篇不删除,因此,第二个参数为0即可。

    public static int makeMovementFlags(int dragFlags, int swipeFlags) {
        return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
                | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
                | makeFlag(ACTION_STATE_DRAG, dragFlags);
    }
    

    总结

    本篇到这里就结束了,我们利用RecyclerView实现了宫格图片的拖拽效果,主要是借助ItemTouchHelper实现,从ItemTouchHelper中我们能看到很多巧妙的的设计,里面有很多值得我们学习的技巧,特别是对事件的处理、绘制顺序调整的方式,如果做吸顶,未尝不是一种方案。

    以上就是基于Android RecyclerView实现宫格拖拽效果的详细内容,更多关于Android RecyclerView宫格拖拽的资料请关注3672js教程其它相关文章!

    您可能感兴趣的文章:
    • Android用RecyclerView实现图标拖拽排序以及增删管理
    • Android使用CardView作为RecyclerView的Item并实现拖拽和左滑删除
    • Android中RecyclerView拖拽、侧删功能的实现代码
    • Android利用RecyclerView实现全选、置顶和拖拽功能示例
    • android RecyclerView实现条目Item拖拽排序与滑动删除

    用户评论