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