当前位置:网站首页>RecyclerView进阶使用-实现仿支付宝菜单编辑页面拖拽功能

RecyclerView进阶使用-实现仿支付宝菜单编辑页面拖拽功能

2022-04-23 14:06:00 森之千手

先上一张效果图
最终效果
之前看见了支付宝的菜单编辑页面,有个类似GridView的拖拽排序效果,于是想自己实现一下。经过网上的大量资料搜索,最终得出了如下的解决方案。

1.实现拖拽的控件

整个拖拽的控件,可以使用网上的可拖拽GridView或者自定义的RecyclerView.我自己是两种都尝试过。发现GridView的实现方式,并不是真正的拖拽,而是将你要拖拽的item制造一个镜像,接着将原来的item的Visiblity设置成看不见,你拖拽的时候,实际移动的是镜像。效果虽然能实现,但是和RecyclerView比起来,感觉就low了一点。RecyclerView首先是自带item插入移动变动的动画的。通过ItemTouchHelper的一个官方辅助类可以比较轻松地实现拖拽效果,流畅性比用GridView实现地假拖拽要好得多。最终在Github上找到了一个第三方地RecyclerView的强大的封装类AdvancedRecyclerView,Github的地址是:https://github.com/h6ah4i/android-advancedrecyclerview,这个类封装得还是比较强大的,支持侧滑,拖拽,header,footer等等。有兴趣的童鞋可以自己去下载原生demo看看。

2.建立数据模型

这里写图片描述
首先主页面的item和编辑页面的子条目是一致的,那么属性都应该有

	private String name;//名字
    private String icon;//图标
    private String desc;//item的描述
    private String group;//item所属组

其中,group属性用于标识这个item的分组是什么,

	/*分组的标签*/
    public static final String GROUP_FAVORITE="favorite";
    public static final String GROUP_COLD_WEAPON="cold_weapon";
    public static final String GROUP_MODERN_WEAPON="modern_weapon";
    public static final String GROUP_MISC="misc";
    public static final String GROUP_PERSON="person";
    public static final String GROUP_EQUIPMENT="equipment";

根据你项目的需求,来决定你自己的组的类别。请注意的favorite这组。这个组是其他组选了过后才放进这个组的,也就是说里面的item的group属性并不是GROUP_FAVORITE。实际上分组标签的另一个作用是用于获取分组数据的key。比如说,你要向服务器请求favorite这组数据,那么传GROUP_FAVORITE给服务器就可以得到数据了。
然后再看看列表条目。标记页列表的条目是一个复合条目,每个条目包含一个组的标题和其包含的所有子元素,那么,我们就可以设计为:

    private String mGroup;
    private String mGroupTitle;
    private List<MenuItem> mMenuItemList;

mGroup就是上面的Group的标签,mGroupTitle是改标签所对应的要显示的标题(当然这个字段不是必须,你也可以在外部根据取到的标签进行判断),mMenuItemList就是其所包含的子列表数据了。

3.页面结构设计

首先主页的结构很简单,就只需要一个grid类型的recyclerView就行了。关键是编辑页。
首先,编辑页从整体上应该分为两个部分。首部和列表部分。所以设计模式可以考虑在正常的list上面添加一个header。又或者传入两种的itemViewType,分别进行渲染视图。两种方案都可行。只是第一种的话,原生recyclerView是没有header,footer说法的,需要自己封装。在这里,我选择第一种方法,自行封装header。

4.数据持久化

1)服务端数据结构可以参考我的样式(json格式):
这里写图片描述
在这里为了演示,我将json原始数据放到asset目录下,用于模拟从服务器获取的数据。
2)本地持久化
最正规的做法是采用数据库方式。例如GreenDao或者Realm都是很牛掰的数据库框架。这里,由于菜单数据相对较少,我就直接以sharedPreference来进行数据存储读取。另外,考虑到json对象与实体对象的相互转换,我这里推荐大家使用阿里巴巴的fastjson,来进行数据的序列化与反序列化操作,转换效率杠杠滴。
下面的就是我写的一个数据操作类。

package csii.cjs.demo.com.superboy;

import android.content.Context;
import android.content.SharedPreferences;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;

import java.util.ArrayList;
import java.util.List;

import csii.cjs.demo.com.superboy.base.ContextUtil;
import csii.cjs.demo.com.superboy.entity.MenuItem;
import csii.cjs.demo.com.superboy.tools.IOKit;

/**
 * 描述:菜单数据控制助手,模拟本地数据库
 * <p>
 * 作者:cjs
 * 创建时间:2017年11月03日 15:21
 * 邮箱:[email protected]
 *
 * @version 1.0
 */
public class MenuHelper {

    /*分组的标签*/
    public static final String GROUP_FAVORITE="favorite";
    public static final String GROUP_COLD_WEAPON="cold_weapon";
    public static final String GROUP_MODERN_WEAPON="modern_weapon";
    public static final String GROUP_MISC="misc";
    public static final String GROUP_PERSON="person";
    public static final String GROUP_EQUIPMENT="equipment";

    private int itemCounter=0;//用于统计共有多少个子item,依次给每个item设置独立的id

    /*分组数据的缓存列表,初始化分组的时候用*/
    private List<MenuItem> favoriteList;
    private List<MenuItem> coldList;
    private List<MenuItem> modernList;
    private List<MenuItem> miscList;
    private List<MenuItem> eqtList;
    private List<MenuItem> personList;

    /**
     * 解析原始数据,用于模拟从服务器上获取到的JSON报文
     */
    private void parseJSONData(){
        String jsonStr= IOKit.getStringFromAssets(ContextUtil.getContext(),"dummy.json");//获取到assets目录下的报文
        JSONObject dataJson= JSON.parseObject(jsonStr);//将报文string转换为JSON
        favoriteList=parseJSONList(dataJson,GROUP_FAVORITE);
        coldList=parseJSONList(dataJson,GROUP_COLD_WEAPON);
        modernList=parseJSONList(dataJson,GROUP_MODERN_WEAPON);
        miscList=parseJSONList(dataJson,GROUP_MISC);
        eqtList=parseJSONList(dataJson,GROUP_EQUIPMENT);
        personList=parseJSONList(dataJson,GROUP_PERSON);

        savePreferFavoriteList(favoriteList);
        savePreferColdWeaponList(coldList);
        savePreferEqtList(eqtList);
        savePreferMiscList(miscList);
        savePreferModernWeaponList(modernList);
        savePreferPersonList(personList);
    }

    private List<MenuItem> parseJSONList(JSONObject dataJSON,String group){
        List<MenuItem> list=new ArrayList<>();
        JSONArray array=dataJSON.getJSONArray(group);
        int size=array.size();
        for(int i=0;i<size;i++,itemCounter++){
            JSONObject object=array.getJSONObject(i);
            //之所以没有在array层就进行JSON到java对象的转换,是为了进入内部遍历,产生id,并将id赋值给menuItem
            MenuItem item=JSON.toJavaObject(object,MenuItem.class);
            item.setItemId(itemCounter);
            list.add(item);
        }
        return list;
    }

    /**
     * 初始化数据
     */
    public static void init(){
        MenuHelper helper=new MenuHelper();
        helper.parseJSONData();
        setInit(true);
    }

    /**
     * 用于保存本地数据的文件名字
     */
    private static final String PREFERENCE_MENU_DATA_NAME="menu_data";
    /**
     * 是否已经进行过初始化的字段名
     */
    private static final String PREFERENCE_HAS_EVER_INIT="has_ever_init";

    /**
     * 获取本地数据的文件
     * @return
     */
    public static SharedPreferences getMenuDataConfig(){
        return ContextUtil.getContext().getSharedPreferences(PREFERENCE_MENU_DATA_NAME, Context.MODE_PRIVATE);
    }

    /**
     * 清空本地数据文件里面的内容
     */
    public static void clearMenuDataConfig(){
        getMenuDataConfig().edit().clear().commit();
    }

    public static boolean hasEverInit(){
        return getMenuDataConfig().getBoolean(PREFERENCE_HAS_EVER_INIT,false);
    }

    public static void setInit(boolean isInit){
        getMenuDataConfig().edit().putBoolean(PREFERENCE_HAS_EVER_INIT,isInit);
    }

    /*----------------------------原始方法-----------------------------------*/
    /**
     * 将List转换为JsonString保存进SharedPreference
     * @param group
     * @param list
     */
    private static void savePreferMenuListData(String group,List<MenuItem> list){
        SharedPreferences.Editor editor=getMenuDataConfig().edit();
        editor.putString(group,JSON.toJSONString(list));
        editor.commit();
    }

    /**
     * 从SharedPreference里面取出JsonString,再转换为List
     * @param group
     * @return
     */
    private static List<MenuItem> getPreferMenuListData(String group){
        String jsonStr=getMenuDataConfig().getString(group,"");
        JSONArray array=JSONArray.parseArray(jsonStr);
        return array.toJavaList(MenuItem.class);
    }

    /**
     * 从本地数据缓存列表里面删除一个item
     * @param group
     * @param item
     */
    public static void deleteItem(String group,MenuItem item){
        List<MenuItem> list=getPreferMenuListData(group);
        for(MenuItem i:list){
            if(i.getItemId()==item.getItemId()){
                list.remove(i);
                break;
            }
        }
        savePreferMenuListData(group,list);
    }

    /**
     * 从本地数据元素里面添加一个item
     * @param group
     * @param item
     */
    public static void addItem(String group,MenuItem item){
        List<MenuItem> list=getPreferMenuListData(group);
        if(!contains(list,item)){
            list.add(item);
            savePreferMenuListData(group,list);
        }
    }

    private static boolean contains(List<MenuItem> list,MenuItem item){
        if(list!=null && list.size()>0){
            for(MenuItem i:list){
                if(i.getItemId()==item.getItemId()){
                    return true;
                }
            }
        }
        return false;
    }
    /*----------------------------原始方法-----------------------------------*/

    /*----------------------------衍生方法-----------------------------------*/
    public static void savePreferFavoriteList(List<MenuItem> list){
        savePreferMenuListData(GROUP_FAVORITE,list);
    }

    public static void savePreferColdWeaponList(List<MenuItem> list){
        savePreferMenuListData(GROUP_COLD_WEAPON,list);
    }

    public static void savePreferModernWeaponList(List<MenuItem> list){
        savePreferMenuListData(GROUP_MODERN_WEAPON,list);
    }

    public static void savePreferMiscList(List<MenuItem> list){
        savePreferMenuListData(GROUP_MISC,list);
    }

    public static void savePreferEqtList(List<MenuItem> list){
        savePreferMenuListData(GROUP_EQUIPMENT,list);
    }

    public static void savePreferPersonList(List<MenuItem> list){
        savePreferMenuListData(GROUP_PERSON,list);
    }

    public static List<MenuItem> getPreferFavoriteList(){
        return getPreferMenuListData(GROUP_FAVORITE);
    }

    public static List<MenuItem> getPreferColdWeaponList(){
        return getPreferMenuListData(GROUP_COLD_WEAPON);
    }

    public static List<MenuItem> getPreferModernWeaponList(){
        return getPreferMenuListData(GROUP_MODERN_WEAPON);
    }

    public static List<MenuItem> getPreferMiscList(){
        return getPreferMenuListData(GROUP_MISC);
    }

    public static List<MenuItem> getPreferEquipmentList(){
        return getPreferMenuListData(GROUP_EQUIPMENT);
    }

    public static List<MenuItem> getPreferPersonList(){
        return getPreferMenuListData(GROUP_PERSON);
    }

    public static void addPreferFavoriteItem(MenuItem item){
        addItem(GROUP_FAVORITE,item);
    }

    public static void addPreferColdItem(MenuItem item){
        addItem(GROUP_COLD_WEAPON,item);
    }

    public static void addPreferEqtItem(MenuItem item){
        addItem(GROUP_EQUIPMENT,item);
    }

    public static void addPreferModernItem(MenuItem item){
        addItem(GROUP_MODERN_WEAPON,item);
    }

    public static void addPreferMiscItem(MenuItem item){
        addItem(GROUP_MISC,item);
    }

    public static void addPreferPersonItem(MenuItem item){
        addItem(GROUP_PERSON,item);
    }

    public static void deletePreferFavoriteItem(MenuItem item){
        deleteItem(GROUP_FAVORITE,item);
    }

    public static void deletePreferColdItem(MenuItem item){
        deleteItem(GROUP_COLD_WEAPON,item);
    }

    public static void deletePreferModernItem(MenuItem item){
        deleteItem(GROUP_MODERN_WEAPON,item);
    }

    public static void deletePreferMiscItem(MenuItem item){
        deleteItem(GROUP_MISC,item);
    }

    public static void deletePreferEqtItem(MenuItem item){
        deleteItem(GROUP_EQUIPMENT,item);
    }

    public static void deletePreferPersonItem(MenuItem item){
        deleteItem(GROUP_PERSON,item);
    }
    /*----------------------------衍生方法-----------------------------------*/
}

5.源码相关说明

这里写图片描述
其中recyclerview包是我自己对AdvancedRecyclerView进行的二次封装。封装后,实现起来更简单。tools包是个工具包,里面包含了一些比较有用的方法,这些大家都可以直接应用到自己的项目里面。然后就是adapter包,这里面是对recyclerview包里面的具体实现。另外MenuHelper的初始化操作,在MyApplication里面。

Warining:

关于AdvancedRecyclerView,目前不支持安卓26的编译,请用25+。还有,如果你采用UniversalImageLoader加载图片出现闪烁的问题,请移步这篇文章:
http://blog.csdn.net/cjs1534717040/article/details/78285741

代码已经上传至Github免费下载。新代码修复了一些bug,增加了类似支付宝最多添加5个的控制。
在这里插入图片描述
Github地址:https://github.com/ChenJunsen/DragRecyclerView
同时欢迎同行加我一起共同交流qq:1534717040
原来代码http://download.csdn.net/download/cjs1534717040/10106300,需要1个积分,这个链接只做为打赏积分的,谢谢大家的支持

更新(2020-07-20)

  1. 原Git地址源码就因为含有公司名字而被公司告知下架(无语),现被我设置私有,所有大家现在无法访问。
  2. AS更新到3.4后编译源代码,可能会报appCompat冲突的错误。原因是主代码用的26+,AdvRecyclerView用的是25+,导致合并冲突,我目前的解决是将主代码也换成25+。
    为什么AdvRecyclerView要用25+编译?因为AnimatorCompatHelper这个类在26+上被移除了。这个代码你们也可以看看时间,是我工作两年左右时写的。当时api也没这么高,技术也有些欠缺。后期会考虑改为AndroidX。
  3. 下载地址:https://download.csdn.net/download/cjs1534717040/12641844(免费)
  4. CSDN设置不了免费,度盘吧(去掉&)
    链接:https&&&&&/&&&&pan.baidu.com/s/1WqaDmCtPbEcGzYhGK8az5Q
    提取码:esq8

版权声明
本文为[森之千手]所创,转载请带上原文链接,感谢
https://blog.csdn.net/cjs1534717040/article/details/78459821