Recyclerview实现网格拖拽排序

门禁列表拖拽排序


需求:

前两行是常用门禁,黄色显示,之后是普通门,白色显示。长按住一个门时,除了被按住的门,其他门抖动,被按住门加阴影。常用门和普通门交换位置时,开启渐变动画。拖拽完成后记录门禁排序。

演示:
门禁拖拽.gif
下面直接上代码,关键地方有注释

总布局关键代码(片段)

open_door_list_activity.xml(需求里需要头部提示白板随门禁列表滑动,由于Recyclerview的拖拽排序功能无法和添加头部的方法兼容[奔溃],万不得已使用了scrollview嵌套Recyclerview的方法实现了该效果,代价就是拖拽门禁的时候无法分页排序[拖拽门禁向下滑动]):

……
<RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_above="@id/btn_lay"
        android:gravity="center">

        <ScrollView
            android:id="@+id/scroll"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:fillViewport="true">

            <!--门禁列表头部提示,随门禁列表一起滑动-->
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <RelativeLayout
                    android:layout_width="match_parent"
                    android:layout_height="@dimen/x296"
                    android:background="@color/white"
                    android:visibility="visible">
                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="@string/doorlist_guide"
                        android:layout_alignParentBottom="true"
                        android:layout_centerHorizontal="true"
                        android:layout_marginBottom="@dimen/x100"
                        style="@style/Txt_3_R_26"/>
                </RelativeLayout>

                <!--门禁列表,CustomRecyclerview是自定义的可添加头部底部的Recyclerview-->
                <com.segi.view.recyclerview.CustomRecyclerView
                    android:id="@+id/door_list"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_marginLeft="@dimen/x10"
                    android:layout_marginRight="@dimen/x10"/>

            </LinearLayout>

        </ScrollView>

        <include
            android:id="@+id/empty_view"
            layout="@layout/common_list_empty_view"/>

    </RelativeLayout>
……
该文章最主要的部分,拖拽事件类

DragItemTouchHelper.java

package com.uhome.hardware.module.access.helper;

import android.content.Context;
import android.os.Build;
import android.support.annotation.RequiresApi;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;

import com.uhome.base.common.adapter.RecyclerViewHolder;
import com.uhome.hardware.R;
import com.uhome.hardware.module.access.adapter.OpenDoorListAdapter;
import com.uhome.hardware.module.access.model.AccessInfo;

import java.util.ArrayList;
import java.util.Collections;

/**
 * Created by Luzj on 2018/9/26.
 *
 * 拖拽事件类,与ItemDecoration是同级子类
 */
public class DragItemTouchHelper extends ItemTouchHelper.Callback {

    private Context context;
    private OpenDoorListAdapter adapter;
    private ArrayList<AccessInfo> data;//门禁实体列表
    private RecyclerView.LayoutManager manager;
    private Animation shake;//抖动动画
    /**
     * 是否正在拖拽。
     * 由于门禁列表的加载方式是先加载本地,再访问网络数据,覆盖上来。
     * 为了避免用户进入门禁列表之后,长按拖拽排序的过程中,网络数据刚好下来刷新页面
     * 造成的奔溃(adapter.notifyDataSetChange的时候列表不可滑动,item也不可被拖拽)。
     * 故增设该参数,在adapter刷新数据的时候判断如果在拖拽状态,则先不刷新(拖拽完之后       
     *  也有刷新操作)。
     */
    private boolean isStrage = false;

    public DragItemTouchHelper(Context context, OpenDoorListAdapter adapter, ArrayList<AccessInfo> data, RecyclerView.LayoutManager manager) {
        this.context = context;
        this.adapter = adapter;
        this.data = data;
        this.manager = manager;
        shake = AnimationUtils.loadAnimation(context, com.uhome.base.R.anim.door_shake);
    }

    /**
     * 拖拽方向注册
     */
    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        final int dragFlags;
        final int swipeFlags;
        if (recyclerView.getLayoutManager() instanceof GridLayoutManager) {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
            swipeFlags = 0;
        } else {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
            swipeFlags = 0;
        }
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    /**
     * 拖拽结果回调
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        int fromPosition = viewHolder.getAdapterPosition();
        int toPosition = target.getAdapterPosition();
        //模拟数据插入
        if (fromPosition < toPosition) {
            for (int i = fromPosition; i < toPosition; i++) {
                Collections.swap(data, i, i + 1);
            }
        } else {
            for (int i = fromPosition; i > toPosition; i--) {
                Collections.swap(data, i, i - 1);
            }
        }
        adapter.notifyItemMoved(fromPosition, toPosition);
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
    }

    /**
     * 开始拖拽回调
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        isStrage = true;
        RecyclerViewHolder rvh = (RecyclerViewHolder) viewHolder;
        FrameLayout imgContainer;
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            ViewGroup vg ;
            View v;
            AccessInfo accessInfo;
            //屏幕内可见门抖动(不是全部门抖动,因为复用的机制)
            for (int k = 0; k < data.size(); k++) {
                accessInfo = data.get(k);
                accessInfo.isNeedShake = true;
                vg = (ViewGroup) manager.getChildAt(k);
                if(null != vg) {
                    v = vg.getChildAt(0);
                    if(null != v){
                        v.startAnimation(shake);
                    }
                }
            }
            imgContainer = rvh.getView(R.id.image_container);
            //被拖拽门不抖动
            imgContainer.clearAnimation();
            //高亮(加阴影)
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                imgContainer.setElevation(20.0f);
            }
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    /**
     * 完成拖拽回调
     */
    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        //去除动画
//      ViewGroup viewG;
//      AccessInfo accessInfo;
//      for (int k = 0; k < data.size(); k++) {
//          accessInfo = data.get(k);
//          accessInfo.isShake = false;
//          viewG = (ViewGroup) manager.getChildAt(k);
//          if(null != viewG) {
//              viewG.clearAnimation();
//          }
//      }
        adapter.notifyDataSetChanged();
        isStrage = false;
    }

    /**
     * 返回false,然后再Activity中通过item的viewholder调用。
     * itemTouchHelper.startDrag(vh);开启自定义拖拽。
     */
    @Override
    public boolean isLongPressDragEnabled() {
        return false;
    }

    public boolean getStrageState() {
        return isStrage;
    }
}

再之是门禁钥匙的自定义ImageView

DoorItemImg.java

package com.uhome.hardware.module.access.view;

import android.content.Context;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;

import com.uhome.hardware.module.access.model.AccessInfo;

/**
 * Created by Luzj on 2018/10/12.
 *
 * 门图标的自定义类,注册可见监听,用于拖拽时抖动屏幕内可见门item
 * 如前面的DragItemTouchHelper:onSelectedChanged方法体里所说,在长按住一个门时,使用
 * 循环将所有门实体的isNeedShake状态标记为需要抖动。
 * 
 * 这里特别声明:在所有列表或者网格控件里面,通过下标来拿item的做法,均会引起复用影响到
 * 其他的item。比如上面DragItemTouchHelper.onSelectedChanged方法中的循环,看似通过
 * vg.getChildAt方法拿到所有item,实际上仍有部分没被引用到。
 * 
 * 特此重写了该ImageView,作用是添加可视监听,当该ImageView在屏幕内可见时,调用监听,
 * 在可视接口里面判断是否需要抖动,需要则抖动。目的是为了弥补
 * DragItemTouchHelper.onSelectedChanged方法中遍历抖动的不足。
 * 
 */
public class DoorItemImg extends android.support.v7.widget.AppCompatImageView {

    private AppearedListener appearedListener;

    public DoorItemImg(Context context) {
        super(context);
    }

    public DoorItemImg(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
    }

    public DoorItemImg(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    @Override
    protected void onWindowVisibilityChanged(int visibility) {
        super.onWindowVisibilityChanged(visibility);
        if(visibility == View.VISIBLE) {
            if(this.getTag() instanceof AccessInfo) {
                AccessInfo accessInfo = (AccessInfo) this.getTag();
                if(accessInfo.isNeedShake) {
                    appearedListener.onAppearedListener(this);
                }
            }
        } else if(visibility == View.INVISIBLE || visibility == View.GONE) {
            appearedListener.onDisappearedListener();
        }
    }

    /**
     * 可见状态监听
     */
    public interface AppearedListener {
        /**
         * 可见且需要抖动效果时条用
         */
        void onAppearedListener(DoorItemImg img);
        /**
         * 不可见时
         */
        void onDisappearedListener();
    }

    public void setonAppearedListener(AppearedListener appearedListener) {
        this.appearedListener = appearedListener;
    }
}

item布局

要实现常用门和普通门之间的渐变效果,最简单的方法就是把常用门的贴图和普通门的贴图重叠在一起,当需要显示常用门时,则先将常用门的贴图置顶(RelativeLayout或者FrameLayout的bringToFront),再执行常用门贴图的透明度渐变动画;反之普通门亦然。
open_door_list_item.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
              xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="@dimen/x253"
              android:gravity="center_horizontal"
              android:orientation="vertical"
              android:layout_marginLeft="@dimen/x20"
              android:layout_marginRight="@dimen/x20">

    <!--门禁贴图,常用门和普通门贴图重叠-->
    <FrameLayout
        android:id="@+id/image_container"
        android:layout_width="@dimen/x106"
        android:layout_height="@dimen/x106"
        android:background="@drawable/door_circle_background"
        android:layout_marginTop="@dimen/x4"
        android:outlineProvider="background">
        <com.uhome.hardware.module.access.view.DoorItemImg
            android:id="@+id/icon_bot"
            android:layout_width="@dimen/x106"
            android:layout_height="@dimen/x106"
            android:layout_gravity="center"
            android:src="@drawable/btn_opendoor_img_major"/>
        <com.uhome.hardware.module.access.view.DoorItemImg
            android:id="@+id/icon_top"
            android:layout_width="@dimen/x106"
            android:layout_height="@dimen/x106"
            android:layout_gravity="center"
            android:src="@drawable/btn_opendoor_img"/>
    </FrameLayout>

    <!--门禁名称-->
    <TextView
        android:id="@+id/name"
        style="@style/Txt_1_R_24"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:ellipsize="end"
        android:maxLines="3"
        android:layout_marginTop="@dimen/x15"
        android:gravity="top|center_horizontal"/>

</LinearLayout>
渐变动画

doorimg_alpha_anime.xml

<?xml version="1.0" encoding="utf-8"?>
<!--android:fillAfter="true"设置true则将对象view停留在动画结束的一帧上-->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="500"
    android:fillAfter="true"
    android:fillBefore="false"
    android:repeatCount="0"
    android:fromAlpha="0.0"
    android:toAlpha="1.0"/>
抖动动画
<?xml version="1.0" encoding="utf-8"?>
<!--android:repeatMode="reverse" 执行完一次正向效果之后,继续执行反向效果-->
<!--android:repeatCount="infinite" 无限循环动画效果-->
<!--android:pivotX & Y="55%" 抖动圆心-->
<!--Degrees 幅度-->
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
    android:toDegrees="16"
    android:repeatMode="reverse"
    android:repeatCount="infinite"
    android:pivotY="55%"
    android:pivotX="55%"
    android:fromDegrees="-16"
    android:duration="100"
    />
门禁适配器
package com.uhome.hardware.module.access.adapter;

import android.content.Context;
import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.FrameLayout;
import android.widget.TextView;

import com.uhome.base.common.adapter.CommonRecyclerAdapter;
import com.uhome.base.common.adapter.RecyclerViewHolder;
import com.uhome.hardware.R;
import com.uhome.hardware.module.access.model.AccessInfo;
import com.uhome.hardware.module.access.view.DoorItemImg;

import java.util.List;

/**
 * 开门弹窗(所有门)适配器
 * Author: chen
 * Date: [2018/7/5]
 */

public class OpenDoorListAdapter extends CommonRecyclerAdapter<AccessInfo> implements DoorItemImg.AppearedListener{

    private View.OnClickListener mOnClickListener;

    private Animation shake;

    private AlphaAnimation alphaAnimation;

    private DoorItemImg iconTop, iconBot;

    public OpenDoorListAdapter(Context context, List<AccessInfo> mDatas, int itemLayoutId, View.OnClickListener onClickListener) {
        super(context, mDatas, itemLayoutId);
        this.mContext = context;
        this.mOnClickListener = onClickListener;
        //抖动
        shake = AnimationUtils.loadAnimation(context, com.uhome.base.R.anim.door_shake);
        //渐变
        alphaAnimation = (AlphaAnimation) AnimationUtils.loadAnimation(context, com.uhome.base.R.anim.doorimg_alpha_anime);
    }

    @Override
    protected void convert(RecyclerViewHolder holder, int position, AccessInfo accessInfo) {
        ((TextView)holder.getView(R.id.name)).setText(accessInfo.name);
        FrameLayout imgContainer = holder.getView(R.id.image_container);
        iconTop = holder.getView(R.id.icon_top);//普通门
        iconBot = holder.getView(R.id.icon_bot);//常用门
        if(position >= 0 && position < 8) {
            //常用门列,常用门图标置顶,并执行渐变
            if(0 == accessInfo.isMajorFront) {
                iconBot.startAnimation(alphaAnimation);
                accessInfo.isMajorFront = 1;
            }
            iconBot.bringToFront();
        } else {
            //普通门列,普通门图标置顶,并执行渐变
            if(1 == accessInfo.isMajorFront) {
                iconTop.startAnimation(alphaAnimation);
                accessInfo.isMajorFront = 0;
            }
            iconTop.bringToFront();
        }
        //将门禁数据实体绑定在ImageView的container上,这样在拖拽排序之后点击的门才是其本体
        imgContainer.setTag(accessInfo);
        imgContainer.setOnClickListener(mOnClickListener);
        //可见状态监听
        iconBot.setonAppearedListener(this);
        iconTop.setonAppearedListener(this);
    }

    @Override
    public void onAppearedListener(DoorItemImg img) {
        img.startAnimation(shake);
    }

    @Override
    public void onDisappearedListener() {
//      icon.clearAnimation();
    }

}
 
Activity中调用Recyclerview并注册拖拽效果

OpenDoorListActivity.java(片段)

private void initView() {
        ……
        mGridView = (CustomRecyclerView) findViewById(R.id.door_list);
          //ItemDecoration间隔类与DragItemTouchHelper拖拽类为同级子类,不能同时添加
//        int padding_45 = this.getResources().getDimensionPixelSize(R.dimen.x45);
//        int padding_30 = this.getResources().getDimensionPixelSize(R.dimen.x30);
//        ItemDecoration decoration = new ItemDecoration(padding_45, 0, this);
//        mGridView.addItemDecoration(decoration);
        mGridLayoutManager = new GridLayoutManager(this, 4) {
            //由于前面所说的问题,布局是采用scrollview嵌套Recyclerview实现的,
            //这里更改了manager的滑动返回,防止滑动卡顿
            @Override
            public boolean canScrollVertically() {
                return false;
            }
        };
        mGridView.setLayoutManager(mGridLayoutManager);
        //这里的mOnClickListener是前面adapter中的门禁ImageView的点击监听,暴露出来
        //让Activity实现
        mAdapter = new OpenDoorListAdapter(this, mDoorList, R.layout.open_door_list_item, mOnClickListener);
        mGridView.setAdapter(mAdapter);
        mAdapter.notifyDataSetChanged();
        dragItemTouchHelper = new DragItemTouchHelper(OpenDoorListActivity.this, mAdapter, mDoorList, mGridLayoutManager);
        itemTouchHelper = new ItemTouchHelper(dragItemTouchHelper);
        itemTouchHelper.attachToRecyclerView(mGridView);
        mGridView.addOnItemTouchListener(new OnRecyclerItemClickListener(mGridView) {
            @Override
            public void onItemLongClick(RecyclerView.ViewHolder vh) {
                itemTouchHelper.startDrag(vh);
            }
        });
        openDoorUtil = new OpenDoorUtil(this, false);
    }

关于排序之后的门顺序存储,这点跟本文的主题关系不大。因为门禁实体info里面有门id,在用户退出门禁列表页面的时候,将当前的门id顺序存进xml里,下次打开列表页面时,将加载数据根据门id顺序表进行排序展示,这里不做过多赘述。

参考文章:

Recyclerview长按事件:https://blog.csdn.net/liaoinstan/article/details/51200600
Recyclerview网格拖拽:https://blog.csdn.net/u014651216/article/details/51456658
阴影:https://blog.csdn.net/xiaoweiguoyuan/article/details/73469877
滑动冲突:https://www.jianshu.com/p/98f2fcfb0e22

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272