门禁列表拖拽排序
需求:
前两行是常用门禁,黄色显示,之后是普通门,白色显示。长按住一个门时,除了被按住的门,其他门抖动,被按住门加阴影。常用门和普通门交换位置时,开启渐变动画。拖拽完成后记录门禁排序。
演示:
下面直接上代码,关键地方有注释
总布局关键代码(片段)
open_door_list_activity.xml(需求里需要头部提示白板随门禁列表滑动,由于Recyclerview的拖拽排序功能无法和添加头部的方法兼容[奔溃],万不得已使用了scrollview嵌套Recyclerview的方法实现了该效果,代价就是拖拽门禁的时候无法分页排序[拖拽门禁向下滑动]):
……
……
该文章最主要的部分,拖拽事件类
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 data;//门禁实体列表
private RecyclerView.LayoutManager manager;
private Animation shake;//抖动动画
/**
* 是否正在拖拽。
* 由于门禁列表的加载方式是先加载本地,再访问网络数据,覆盖上来。
* 为了避免用户进入门禁列表之后,长按拖拽排序的过程中,网络数据刚好下来刷新页面
* 造成的奔溃(adapter.notifyDataSetChange的时候列表不可滑动,item也不可被拖拽)。
* 故增设该参数,在adapter刷新数据的时候判断如果在拖拽状态,则先不刷新(拖拽完之后
* 也有刷新操作)。
*/
private boolean isStrage = false;
public DragItemTouchHelper(Context context, OpenDoorListAdapter adapter, ArrayList 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
渐变动画
doorimg_alpha_anime.xml
抖动动画
门禁适配器
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 implements DoorItemImg.AppearedListener{
private View.OnClickListener mOnClickListener;
private Animation shake;
private AlphaAnimation alphaAnimation;
private DoorItemImg iconTop, iconBot;
public OpenDoorListAdapter(Context context, List 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