当前位置: 代码网 > it编程>App开发>Android > 基于Android RecyclerView实现宫格拖拽效果

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

2024年05月15日 Android 我要评论
前言在android发展的进程中,网格布局一直比较有热度,其中一个原因是对用户来说便捷操作,对app厂商而言也会带来很多的曝光量,对于很多头部app,展示网格菜单几乎是必选项。实现网格的方式有很多种,

前言

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

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com