前言
在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宫格拖拽的资料请关注代码网其它相关文章!
发表评论