设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间13 现在时间13 缓存数据 风骨神仙籍里人,诗狂酒圣且平生。开元一遇成何事,留得千秋万古名。

风骨神仙籍里人,诗狂酒圣且平生。开元一遇成何事,留得千秋万古名。 -- 杨花落尽子规啼

查看: 1177|回复: 2

基于Android RecyclerView实现宫格拖拽效果

[复制链接]

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
35
主题
27
精华
0
金钱
99
积分
62
注册时间
2023-10-4
最后登录
2025-3-13

发表于 2024-9-11 15:00:21 | 显示全部楼层 |阅读模式
目录


  • 前言
  • 效果
  • 拖拽效果原理

    • 事件处理
    • 图像平移
    • 数据更新

  • 本篇实现

    • 图片分片
    • 更新数据

  • 总结

前言

在Android发展的进程中,网格布局一直比较有热度,其中一个原因是对用户来说便捷操作,对app厂商而言也会带来很多的曝光量,对于很多头部app,展示网格菜单几乎是必选项。实现网格的方式有很多种,比如GridView、GridLayout,TableLayout等,实际上,由于RecyclerView的灵活性和可扩展性很高,这些View基本没必要去学了,为什么这样说呢?主要原因是基于RecyclerView可以实现很多布局效果,传统的很多Layout都可以通过RecyclerView去实现,比如ViewPager、SlingTabLayout、DrawerLayout、ListView等,甚至连九宫格解锁效果也可以实现。
当然,在很早之前,实现网格的拖拽效果主要是通过GridView去实现的,如果列数为1的话,那么GridView基本上就实现了ListView一样的上下拖拽。
话说回来,我们现在基本不用去学习这类实现了,因为RecyclerView足够强大,通过简单的数据组装,是完全可以替代GridView和ListView的。

效果

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

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


拖拽效果原理

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

事件处理

实际上无论传统的拖拽效果还是最新的拖拽效果,都离不开事件处理,不过,好处就是,google为RecyclerView提供了ItemTouchHelper来处理这个问题,相比传统的GridView实现方式,省去了很多事情,如动画、目标查找等。
不过,我们回顾下原理,其实他们很多方面都是相似的,不同之处就是ItemTouchHelper 设计的非常好用,而且接口暴露的非常彻底,甚至能控制那些可以拖动、那些不能拖动、以及什么方向可以拖动,如果我们上、下、左、右四个方向都选中的话,斜对角拖动完全没问题,
事件处理这里,GridView使用的方式相对传统,而ItemTouchHelper借助RecyclerView的一个接口(看样子是开的后门),通过View自身去拦截事件.
  1. public interface OnItemTouchListener {
  2.     //是否让RecyclerView拦截事件
  3.     boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
  4.     //拦截之后处理RecyclerView的事件
  5.     void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e);
  6.     //监听禁止拦截事件的请求结果
  7.     void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept);
  8. }
复制代码
这种其实相对GridView来说简单的多

图像平移

无论是RecyclerView和传统GridView拖动,都需要图像平移。我们知道,RecyclerView和GridView本身是通过子View的边界(left\top\right\bottom)来移动的,那么,在平移图像的时候必然不能选择这种方式,只能选择Matrix 变化,也就是transitionX和transitionY的等。不同点是GridView的子View本身并不移动,而是将图像绘制到一个GridView之外的View上,当然,实现上是比较复杂的。
但是,ItemTouchHelper设计比较巧妙的一点是,通过RecyclerView#ItemDecoration来实现,在捕获可以滑动的View之后,在绘制时对View进行偏移。
  1. class ItemTouchUIUtilImpl implements ItemTouchUIUtil {
  2.     static final ItemTouchUIUtil INSTANCE =  new ItemTouchUIUtilImpl();

  3.     @Override
  4.     public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
  5.             int actionState, boolean isCurrentlyActive) {
  6.         if (Build.VERSION.SDK_INT >= 21) {
  7.             if (isCurrentlyActive) {
  8.                 Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
  9.                 if (originalElevation == null) {
  10.                     originalElevation = ViewCompat.getElevation(view);
  11.                     float newElevation = 1f + findMaxElevation(recyclerView, view);
  12.                     ViewCompat.setElevation(view, newElevation);
  13.                     view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
  14.                 }
  15.             }
  16.         }

  17.         view.setTranslationX(dX);
  18.         view.setTranslationY(dY);
  19.     }
  20.      //省略一些有关或者无关的代码
  21. }
复制代码
不过,我们看到,Android 5.0的版本借助了setElevation 使得被拖拽View不被其他顺序的View遮住,那Android 5.0之前是怎么实现的呢?
其实,做过TV app的都比较清楚,子View绘制顺序可以通过下面方式调整,借助下面的方法,在TV上某个View获取焦点之后,就不会被后面的View盖住。
  1. View#getChildDrawingOrder
复制代码
ItemTouchHelper 同样借助了此方法,为什么不统一一种呢,主要原因是getChildDrawingOrder是protected,总的来说,没有通过setElevation方便。
  1. private void addChildDrawingOrderCallback() {
  2.     if (Build.VERSION.SDK_INT >= 21) {
  3.         return; // we use elevation on Lollipop
  4.     }
  5.     if (mChildDrawingOrderCallback == null) {
  6.         mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
  7.             @Override
  8.             public int onGetChildDrawingOrder(int childCount, int i) {
  9.                 if (mOverdrawChild == null) {
  10.                     return i;
  11.                 }
  12.                 int childPosition = mOverdrawChildPosition;
  13.                 if (childPosition == -1) {
  14.                     childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
  15.                     mOverdrawChildPosition = childPosition;
  16.                 }
  17.                 if (i == childCount - 1) {
  18.                     return childPosition;
  19.                 }
  20.                 return i < childPosition ? i : i + 1;
  21.             }
  22.         };
  23.     }
  24.     mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
  25. }
复制代码
数据更新

数据更新这里其实ReyclerView的优势更加明显,我们知道RecyclerView可以做到无requestLayout的局部刷新,性能更好。
  1. @Override
  2. public boolean onItemMove(int fromPosition, int toPosition) {
  3.     Collections.swap(mDataList, fromPosition, toPosition);
  4.     notifyItemMoved(fromPosition, toPosition);
  5.     return true;
  6. }
复制代码
不过,数据交换后还有一点需要处理,对Matrix相关属性清理,防止无法落到指定区域。
  1. @Override
  2. public void clearView(View view) {
  3.     if (Build.VERSION.SDK_INT >= 21) {
  4.         final Object tag = view.getTag(R.id.item_touch_helper_previous_elevation);
  5.         if (tag instanceof Float) {
  6.             ViewCompat.setElevation(view, (Float) tag);
  7.         }
  8.         view.setTag(R.id.item_touch_helper_previous_elevation, null);
  9.     }

  10.     view.setTranslationX(0f);
  11.     view.setTranslationY(0f);
  12. }
复制代码
本篇实现

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

图片分片

下面我们把多张图片分割成 [行数 x 列数]数量的图片。
  1. Bitmap srcInputBitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.image_4);
  2. Bitmap source = Bitmap.createScaledBitmap(srcInputBitmap, width, height, true);
  3. srcInputBitmap.recycle();

  4. int colCount = spanCount;
  5. int rowCount = 6;

  6. int spanImageWidthSize = source.getWidth() / colCount;
  7. int spanImageHeightSize = (source.getHeight() - rowCount * padding/2) / rowCount;

  8. Bitmap[] bitmaps = new Bitmap[rowCount * colCount];
  9. for (int i = 0; i < rowCount; i++) {
  10.     for (int j = 0; j < colCount; j++) {
  11.         int y = i * spanImageHeightSize;
  12.         int x = j * spanImageWidthSize;
  13.         Bitmap bitmap = Bitmap.createBitmap(source, x, y, spanImageWidthSize, spanImageHeightSize);
  14.         bitmaps[i * colCount + j] = bitmap;
  15.     }
  16. }
复制代码
在这种过程我们一定要处理一个问题,如果我们对网格设置了边界线(ItemDecoration)且是纵向布局的话,那么,纵向总高度要减去rowCount * bottomPadding,这里bottomPadding == padding/2,如下面代码。
为什么要这么做呢?因为RecyclerView计算高度的时候,需要考虑这个高度,如果不去处理,那么ReyclerView可能不是禁止不动,而是会滑动,虽然影响不大,但是如果实现全屏效果,还能上下滑的话体验比较差。
  1. public class SimpleItemDecoration extends RecyclerView.ItemDecoration {

  2.     public int delta;
  3.     public SimpleItemDecoration(int padding) {
  4.         delta = padding;
  5.     }

  6.     @Override
  7.     public void getItemOffsets(Rect outRect, View view,
  8.                                RecyclerView parent, RecyclerView.State state) {
  9.         int position = parent.getChildAdapterPosition(view);
  10.         RecyclerView.Adapter adapter = parent.getAdapter();
  11.         int viewType = adapter.getItemViewType(position);
  12.         if(viewType== Bean.TYPE_GROUP){
  13.             return;
  14.         }
  15.         GridLayoutManager layoutManager = (GridLayoutManager) parent.getLayoutManager();
  16.          //列数量
  17.         int cols = layoutManager.getSpanCount();
  18.         //position转为在第几列
  19.         int current =  layoutManager.getSpanSizeLookup().getSpanIndex(position,cols);
  20.         //可有可无
  21.         int currentCol = current % cols;


  22.         int bottomPadding = delta / 2;

  23.         if (currentCol == 0) {  //第0列左侧贴边
  24.             outRect.left = 0;
  25.             outRect.right = delta / 4;
  26.             outRect.bottom = bottomPadding;
  27.         } else if (currentCol == cols - 1) {
  28.             outRect.left = delta / 4;
  29.             outRect.right = 0;
  30.             outRect.bottom = bottomPadding;
  31.              //最后一列右侧贴边
  32.         } else {
  33.             outRect.left = delta / 4;
  34.             outRect.right = delta / 4;
  35.             outRect.bottom = bottomPadding;
  36.         }
  37.     }
  38. }
复制代码
更新数据

这部分是常规操作,主要目的是设置LayoutManager、Decoration、Adapter以及ItemTouchHelper,当然,ItemTouchHelper比较特殊,因为其内部试下是ItemTouchHelper、OnItemTouchListener、Gesture的组合,因此封装为attachToRecyclerView 来调用。
  1. mLinearLayoutManager = new GridLayoutManager(this, spanCount, LinearLayoutManager.VERTICAL, false);
  2. mLinearLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup(){
  3.     @Override
  4.     public int getSpanSize(int position) {
  5.         if(mAdapter.getItemViewType(position) == Bean.TYPE_GROUP){
  6.             return spanCount;
  7.         }
  8.         return 1;
  9.     }
  10. });
  11. mAdapter = new RecyclerViewAdapter();
  12. mRecyclerView.setAdapter(mAdapter);
  13. mRecyclerView.setLayoutManager(mLinearLayoutManager);
  14. mRecyclerView.addItemDecoration(new SimpleItemDecoration(padding));
  15. ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new GridItemTouchCallback(mAdapter));
  16. itemTouchHelper.attachToRecyclerView(mRecyclerView);
复制代码
这里,我们主要还是关注ItemTouchHelper,在初始化的时候,我们给了一个GridItemTouchCallback,用于监听相关处理逻辑,最终通知Adapter调用notifyXXX更新View。
  1. public class GridItemTouchCallback extends ItemTouchHelper.Callback {
  2.     private final ItemTouchCallback mItemTouchCallback;
  3.     public GridItemTouchCallback(ItemTouchCallback itemTouchCallback) {
  4.         mItemTouchCallback = itemTouchCallback;
  5.     }
  6.     @Override
  7.     public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
  8.         // 上下左右拖动,但允许触发删除
  9.         int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
  10.         return makeMovementFlags(dragFlags, 0);
  11.     }

  12.     @Override
  13.     public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
  14.         // 通知Adapter移动View
  15.         return mItemTouchCallback.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
  16.     }
  17.     @Override
  18.     public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
  19.         // 通知Adapter删除View
  20.         mItemTouchCallback.onItemRemove(viewHolder.getAdapterPosition());
  21.     }

  22.     @Override
  23.     public void onChildDraw(@NonNull Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
  24.         super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
  25.     }
  26.     @Override
  27.     public void onChildDrawOver(Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) {
  28.         Log.d("GridItemTouch","dx="+dX+", dy="+dY);
  29.         super.onChildDrawOver(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
  30.     }
  31. }
复制代码
这里,主要是对Flag的关注需要处理,第一参数是拖拽方向,第二个是删除方向,我们本篇不删除,因此,第二个参数为0即可。
  1. public static int makeMovementFlags(int dragFlags, int swipeFlags) {
  2.     return makeFlag(ACTION_STATE_IDLE, swipeFlags | dragFlags)
  3.             | makeFlag(ACTION_STATE_SWIPE, swipeFlags)
  4.             | makeFlag(ACTION_STATE_DRAG, dragFlags);
  5. }
复制代码
总结

本篇到这里就结束了,我们利用RecyclerView实现了宫格图片的拖拽效果,主要是借助ItemTouchHelper实现,从ItemTouchHelper中我们能看到很多巧妙的的设计,里面有很多值得我们学习的技巧,特别是对事件的处理、绘制顺序调整的方式,如果做吸顶,未尝不是一种方案。
以上就是基于Android RecyclerView实现宫格拖拽效果的详细内容,更多关于Android RecyclerView宫格拖拽的资料请关注晓枫资讯其它相关文章!

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
19
积分
18
注册时间
2022-12-25
最后登录
2022-12-25

发表于 2024-11-19 08:20:23 | 显示全部楼层
顶顶更健康!!!
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
16
积分
12
注册时间
2022-12-28
最后登录
2022-12-28

发表于 2025-3-2 08:59:22 | 显示全部楼层
感谢楼主分享。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼
3楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表