可拖拽排序的GridView(高仿今日头条编辑频道效果)
来源:互联网 发布:tower软件 编辑:程序博客网 时间:2024/06/06 11:37
最近在一直在用今日头条,发现在我的频道编辑时的拖拽排序体验非常有意思,这种拖拽功能其实在支付宝等app上也频繁使用,于是打算自己研究一下,网上虽然有很多类似于此类功能的博客,但实现的都不是特别完美,效果总有瑕疵,今天我分享一个完美体验版,大家用了就知道!
老规矩,先上效果:
准备工作
要想实现这个效果,首先你要了解这几个方面的知识,有欠缺的同学赶紧先补一下:
* 使用WindowManager添加悬浮窗口
* onTouchEvent触摸事件的处理
* 简单的TranslateAnimation平移动画
* GridView api的熟练使用
原理分析
首先它是一个gridview,里面放了很多item,至于item你可以自己随意布局,例子中我只用了一个textview。
1、触发gridview长按事件,用windowmanager添加一个悬浮view,并占位隐藏原来的item,这个悬浮窗就是我们长按的item的一个图片副本,并且将悬浮view定位到自己的手指触摸点,将这个悬浮view设置放大倍数及透明度;
2、监听手指的移动,实时改变悬浮view的位置;
3、当移动距离超过自己的position时,用TranslateAnimation动画平移从起始位置到目标位置之间的item;
4、当手机抬起时,改变adapter中数据的位置,刷新gridview,并释放悬浮view;
下面我们一步一步的来撸代码
//首先展示一下需要的成员变量 private WindowManager.LayoutParams mWindowParams; private WindowManager mWindowManager; //被拖拽的item图片副本 private ImageView mDragImageView; //按下时手指的坐标 private float downX, downY; //是否正在拖拽 private boolean isDraging = false; //是否正在进行移动item动画,防止高频率触发动画而发生抖动 private boolean isMoving = false; //被拖拽和未被拖拽的标记 private static final int NOT_DRAG_ITEM = 0x0; private static final int SHOW_DRAG_ITEM = 0x1; //拖动时副本放大倍数 private static final float DRAG_SCALE = 1.2F; //记录拖拽的item位置 private int mDragItemPosition; //记录最后一个item动画toString格式 private String lastAnim;
在构造方法中初始化必要的一些变量:
public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { //设置item长按事件 setOnItemLongClickListener(this); mWindowParams = new WindowManager.LayoutParams(); mDragImageView = new ImageView(getContext()); //标记未被拖拽 mDragImageView.setTag(NOT_DRAG_ITEM); //获取窗口管理对象,用于后面向窗口中添加dragImageView mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); }
在长按事件中创建副本,隐藏拖拽item
@Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { mDragItemPosition = position; //清空item之前的图片缓存 view.destroyDrawingCache(); //开启图片缓存 view.setDrawingCacheEnabled(true); //创建一个item的图片副本 Bitmap dragBitmap = Bitmap.createBitmap(view.getDrawingCache()); mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; //定义副本的长和宽 mWindowParams.width = (int) (DRAG_SCALE * dragBitmap.getWidth()); mWindowParams.height = (int) (DRAG_SCALE * dragBitmap.getHeight()); //定义副本的位置 mWindowParams.x = (int) (downX - dragBitmap.getWidth() / 2); mWindowParams.y = (int) (downY - dragBitmap.getHeight() / 2); //定义副本的附加参数:不能点击、不能获取焦点、悬浮窗的形式 mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; //定义副本支持透明格式 mWindowParams.format = PixelFormat.TRANSLUCENT; mWindowParams.windowAnimations = 0; //如果之前有这个副本先移除 if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) { mWindowManager.removeView(mDragImageView); mDragImageView.setTag(NOT_DRAG_ITEM); } //将图片副本放入imageview mDragImageView.setImageBitmap(dragBitmap); //设置已有副本标记 mDragImageView.setTag(SHOW_DRAG_ITEM); //设置imageView透明度 mDragImageView.setAlpha(0.7f); //添加这个imageview到悬浮窗 mWindowManager.addView(mDragImageView, mWindowParams); //此时状态变为可拖拽 isDraging = true; //通知adapter隐藏拖拽的item ((DragAdapter) getAdapter()).hideItem(position); //将拖拽item的图片缓存功能关闭,释放内存 view.setDrawingCacheEnabled(false); return true; }
监听手指的移动来移动item副本,并执行动画:
@Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //记录按下x、y轴位置 downX = ev.getRawX(); downY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: if (isDraging) { //移动时实时刷新副本的坐标位置 mWindowParams.x = (int) (ev.getRawX() - mDragImageView.getWidth() / 2); mWindowParams.y = (int) (ev.getRawY() - mDragImageView.getHeight() / 2); //改变副本的位置 mWindowManager.updateViewLayout(mDragImageView, mWindowParams); //移动中间的item moveItem((int) ev.getX(), (int) ev.getY()); } break; }
使用平移动画移动中间item的位置
/** * 移动item的位置 */ private void moveItem(int x, int y) { //得到移动到的点x、y在gridview中的所在位置 final int currentPosition = pointToPosition(x, y); //如果移动位置移除自己所在的位置,则支持位置移动 if (currentPosition != mDragItemPosition && currentPosition > -1) { //如果已经在执行移动动画,则不再执行,防止高频率触发动画而发生抖动 if (isMoving) return; //向后拖拽,则中间的item向前移动 if (currentPosition > mDragItemPosition) { for (int i = mDragItemPosition+1; i <= currentPosition; i++) { View before = getChildAt(i); int toPosition = i; int toX = -before.getWidth(); int toY = 0; if(toPosition % getNumColumns() == 0){ toX = before.getWidth() * (getNumColumns()-1); toY = -before.getHeight(); } startAnim(before, toX , toY , i ,currentPosition); } } else { //向前拖拽,则中间的item向后移动 for (int i = mDragItemPosition - 1 ; i >= currentPosition ; i--) { View before = getChildAt(i); int toPosition = i+1; int toX = before.getWidth(); int toY = 0; if(toPosition % getNumColumns() == 0){ toX = - before.getWidth() * (getNumColumns()-1); toY = before.getHeight(); } startAnim(before, toX , toY , i ,currentPosition); } } } } /** * 启动平移动画 * @param view 将要移动的view * @param toX x轴上移动值 * @param toY y轴上的移动值 * @param positon 当前item的位置 * @param currentPosition 拖拽中的view的最终位置 */ private void startAnim(View view, float toX , float toY , int positon , final int currentPosition) { Animation anim = getMoveAnim( toX , toY ); if (positon == currentPosition) { lastAnim = anim.toString(); } view.startAnimation(anim); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { //动画开始将移动状态设为true isMoving = true; } @Override public void onAnimationEnd(Animation animation) { //最后一个item动画结束,将移动状态设为false if (!StringUtils.isEmpty(lastAnim) && lastAnim.equals(animation.toString())) { //通知adapter刷新数据的位置,刷新界面 ((DragAdapter) getAdapter()).changeItemPosition(currentPosition, mDragItemPosition); mDragItemPosition = currentPosition; isMoving = false; } } @Override public void onAnimationRepeat(Animation animation) { } }); } /** * 生成item移动动画 */ private TranslateAnimation getMoveAnim(float toX, float toY) { TranslateAnimation anim = new TranslateAnimation(0.0F, toX , 0.0F , toY); anim.setDuration(300); anim.setFillAfter(true); return anim; }
手指抬起时,将所有状态还原,释放窗口悬浮view:
case MotionEvent.ACTION_UP: if (isDraging) { ((DragAdapter) getAdapter()).cancelDrag(); if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) { mWindowManager.removeView(mDragImageView); mDragImageView.setTag(NOT_DRAG_ITEM); } isDraging = false; isMoving = false; } break;
在来看看拖拽GridView专用的adapter基类,主要是提供了一些工具方法:
/** * 拖拽adapter基类,如要使用拖拽gridview必须继承 * 继承时不能复用convertView,否则会出现一些奇怪现象 */ public static abstract class DragAdapter<T> extends BaseAdapter { protected List<T> datas = new ArrayList<T>(); private int hidePosition = AdapterView.INVALID_POSITION; /** * 是否隐藏标记 */ public static final int ITEM_TYPE_NORMAL = 0x0; public static final int ITEM_TYPE_HIDE = 0x1; public void setDatas(List<T> list) { this.datas.clear(); this.datas.addAll(list); notifyDataSetChanged(); } @Override public int getCount() { return datas.size(); } @Override public T getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { return getItemView(position, convertView, parent); } protected abstract View getItemView(int position, View convertView, ViewGroup parent); /** * 返回要隐藏的item位置,在继承这个adapter基类时只要通过这个方法判断type = ITEM_TYPE_NORMAL时将该item隐藏即可 */ @Override public int getItemViewType(int position) { if (hidePosition == position) { return ITEM_TYPE_HIDE; } return ITEM_TYPE_NORMAL; } /** * 取消拖拽 */ public void cancelDrag() { hidePosition = AdapterView.INVALID_POSITION; notifyDataSetChanged(); } /** * 改变拖拽item位置 */ public void changeItemPosition(int currentPosition, int dragPosition) { //从后往前移 if (currentPosition < dragPosition) { datas.add(currentPosition, getItem(dragPosition)); datas.remove(dragPosition + 1); } else { //从前往后移 datas.add(currentPosition + 1, getItem(dragPosition)); datas.remove(dragPosition); } hidePosition = currentPosition; notifyDataSetChanged(); } /** * 隐藏item */ public void hideItem(int positon) { hidePosition = positon; notifyDataSetChanged(); } }
好了,最后贴上GragGridView源码:
/** * Created by caoyujie on 17/1/13. * 可拖拽的gridView */public class DragGridView<T extends DragGridView.DragAdapter> extends GridView implements AdapterView.OnItemLongClickListener { private WindowManager.LayoutParams mWindowParams; private WindowManager mWindowManager; private float downX, downY; /** * 被拖拽的item图片副本 */ private ImageView mDragImageView; /** * 是否正在拖拽 */ private boolean isDraging = false; /** * 是否正在进行移动item动画,防止高频率触发动画而发生抖动 */ private boolean isMoving = false; /** * 被拖拽和未被拖拽的标记 */ private static final int NOT_DRAG_ITEM = 0x0; private static final int SHOW_DRAG_ITEM = 0x1; /** * 拖动时放大倍数 */ private static final float DRAG_SCALE = 1.2F; /** * 记录拖拽的item位置 */ private int mDragItemPosition; /** * 记录最后一个item动画toString格式 */ private String lastAnim; /** * 行间距 */ private int verticalSpacing; public DragGridView(Context context) { this(context, null); } public DragGridView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public DragGridView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); initView(); } private void initView() { setOnItemLongClickListener(this); mWindowParams = new WindowManager.LayoutParams(); mDragImageView = new ImageView(getContext()); //标记未被拖拽 mDragImageView.setTag(NOT_DRAG_ITEM); //获取窗口管理对象,用于后面向窗口中添加dragImageView mWindowManager = (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE); verticalSpacing = getVerticalSpacing(); } @Override public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) { mDragItemPosition = position; //清空item之前的图片缓存 view.destroyDrawingCache(); //开启图片缓存 view.setDrawingCacheEnabled(true); //创建一个item的图片副本 Bitmap dragBitmap = Bitmap.createBitmap(view.getDrawingCache()); mWindowParams.gravity = Gravity.TOP | Gravity.LEFT; //定义副本的长和宽 mWindowParams.width = (int) (DRAG_SCALE * dragBitmap.getWidth()); mWindowParams.height = (int) (DRAG_SCALE * dragBitmap.getHeight()); //定义副本的位置 mWindowParams.x = (int) (downX - dragBitmap.getWidth() / 2); mWindowParams.y = (int) (downY - dragBitmap.getHeight() / 2); //定义副本的附加参数:不能点击、不能获取焦点、悬浮窗的形式 mWindowParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE | WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; //定义副本支持透明格式 mWindowParams.format = PixelFormat.TRANSLUCENT; mWindowParams.windowAnimations = 0; //如果之前有这个副本先移除 if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) { mWindowManager.removeView(mDragImageView); mDragImageView.setTag(NOT_DRAG_ITEM); } //将图片副本放入imageview mDragImageView.setImageBitmap(dragBitmap); //设置已有副本标记 mDragImageView.setTag(SHOW_DRAG_ITEM); //设置imageView透明度 mDragImageView.setAlpha(0.7f); //添加这个imageview到悬浮窗 mWindowManager.addView(mDragImageView, mWindowParams); //此时状态变为可拖拽 isDraging = true; //通知adapter隐藏拖拽的item ((DragAdapter) getAdapter()).hideItem(position); //将拖拽item的图片缓存功能关闭,释放内存 view.setDrawingCacheEnabled(false); return true; } @Override public boolean onTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: //记录按下x、y轴位置 downX = ev.getRawX(); downY = ev.getRawY(); break; case MotionEvent.ACTION_MOVE: if (isDraging) { //移动时实时刷新副本的坐标位置 mWindowParams.x = (int) (ev.getRawX() - mDragImageView.getWidth() / 2); mWindowParams.y = (int) (ev.getRawY() - mDragImageView.getHeight() / 2); //改变副本的位置 mWindowManager.updateViewLayout(mDragImageView, mWindowParams); //移动中间的item moveItem((int) ev.getX(), (int) ev.getY()); } break; case MotionEvent.ACTION_UP: if (isDraging) { ((DragAdapter) getAdapter()).cancelDrag(); if ((int) mDragImageView.getTag() == SHOW_DRAG_ITEM) { mWindowManager.removeView(mDragImageView); mDragImageView.setTag(NOT_DRAG_ITEM); } isDraging = false; isMoving = false; } break; } return super.onTouchEvent(ev); } /** * 移动item的位置 */ private void moveItem(int x, int y) { //得到移动到的点x、y在gridview中的所在位置 final int currentPosition = pointToPosition(x, y); //如果移动位置移除自己所在的位置,则支持位置移动 if (currentPosition != mDragItemPosition && currentPosition > -1) { //如果已经在执行移动动画,则不再执行,防止高频率触发动画而发生抖动 if (isMoving) return; //向后拖拽,则中间的item向前移动 if (currentPosition > mDragItemPosition) { for (int i = mDragItemPosition+1; i <= currentPosition; i++) { View before = getChildAt(i); int toPosition = i; int toX = -before.getWidth(); int toY = 0; if(toPosition % getNumColumns() == 0){ toX = before.getWidth() * (getNumColumns()-1); toY = -before.getHeight() - verticalSpacing; } startAnim(before, toX , toY , i ,currentPosition); } } else { //向前拖拽,则中间的item向后移动 for (int i = mDragItemPosition - 1 ; i >= currentPosition ; i--) { View before = getChildAt(i); int toPosition = i+1; int toX = before.getWidth(); int toY = 0; if(toPosition % getNumColumns() == 0){ toX = - before.getWidth() * (getNumColumns()-1); toY = before.getHeight() + verticalSpacing; } startAnim(before, toX , toY , i ,currentPosition); } } } } /** * 启动平移动画 * @param view 将要移动的view * @param toX x轴上移动值 * @param toY y轴上的移动值 * @param positon 当前item的位置 * @param currentPosition 拖拽中的view的最终位置 */ private void startAnim(View view, float toX , float toY , int positon , final int currentPosition) { Animation anim = getMoveAnim( toX , toY ); if (positon == currentPosition) { lastAnim = anim.toString(); } view.startAnimation(anim); anim.setAnimationListener(new Animation.AnimationListener() { @Override public void onAnimationStart(Animation animation) { //动画开始将移动状态设为true isMoving = true; } @Override public void onAnimationEnd(Animation animation) { //最后一个item动画结束,将移动状态设为false if (!StringUtils.isEmpty(lastAnim) && lastAnim.equals(animation.toString())) { //通知adapter刷新数据的位置,刷新界面 ((DragAdapter) getAdapter()).changeItemPosition(currentPosition, mDragItemPosition); mDragItemPosition = currentPosition; isMoving = false; } } @Override public void onAnimationRepeat(Animation animation) { } }); } /** * 生成item移动动画 */ private TranslateAnimation getMoveAnim(float toX, float toY) { TranslateAnimation anim = new TranslateAnimation(0.0F, toX , 0.0F , toY); anim.setDuration(300); anim.setFillAfter(true); return anim; } /** * 拖拽adapter基类,如要使用拖拽gridview必须继承 * 继承时不能复用convertView,否则会出现一些奇怪现象 */ public static abstract class DragAdapter<T> extends BaseAdapter { protected List<T> datas = new ArrayList<T>(); private int hidePosition = AdapterView.INVALID_POSITION; /** * 是否隐藏标记 */ public static final int ITEM_TYPE_NORMAL = 0x0; public static final int ITEM_TYPE_HIDE = 0x1; public void setDatas(List<T> list) { this.datas.clear(); this.datas.addAll(list); notifyDataSetChanged(); } @Override public int getCount() { return datas.size(); } @Override public T getItem(int position) { return datas.get(position); } @Override public long getItemId(int position) { return 0; } @Override public View getView(int position, View convertView, ViewGroup parent) { return getItemView(position, convertView, parent); } protected abstract View getItemView(int position, View convertView, ViewGroup parent); /** * 返回要隐藏的item位置,在继承这个adapter基类时只要通过这个方法判断type = ITEM_TYPE_NORMAL时将该item隐藏即可 */ @Override public int getItemViewType(int position) { if (hidePosition == position) { return ITEM_TYPE_HIDE; } return ITEM_TYPE_NORMAL; } /** * 取消拖拽 */ public void cancelDrag() { hidePosition = AdapterView.INVALID_POSITION; notifyDataSetChanged(); } /** * 改变拖拽item位置 */ public void changeItemPosition(int currentPosition, int dragPosition) { //从后往前移 if (currentPosition < dragPosition) { datas.add(currentPosition, getItem(dragPosition)); datas.remove(dragPosition + 1); } else { //从前往后移 datas.add(currentPosition + 1, getItem(dragPosition)); datas.remove(dragPosition); } hidePosition = currentPosition; notifyDataSetChanged(); } /** * 隐藏item */ public void hideItem(int positon) { hidePosition = positon; notifyDataSetChanged(); } }}/** * Created by caoyujie on 17/1/13. * 拖拽adapter的实现类 * 我们实际使用的adapter */public class MenuDragAdapter extends DragGridView.DragAdapter { private LayoutInflater mLayoutInflater; public MenuDragAdapter(Context context) { mLayoutInflater = LayoutInflater.from(context); } @Override protected View getItemView(int position, View convertView, ViewGroup parent) { convertView = mLayoutInflater.inflate(R.layout.list_menu_drag, parent, false); TextView label = (TextView) convertView.findViewById(R.id.tv_label); label.setText((String) datas.get(position)); return convertView; }}
通过这个view我们可以学到很多的知识,值得我们去探索,想看源码的可以参考我的开源项目:https://github.com/18973809797/BaseStorehouse。
全局搜索 DragGridView即可,效果在主页点击标题进入:
里面还有很多实用的app框架实现及实用view的学习哦~
- 可拖拽排序的GridView(高仿今日头条编辑频道效果)
- 高仿今日头条频道管理
- 仿今日头条的频道管理
- 仿今日头条的频道管理
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- Android 高仿 频道管理----网易、今日头条、腾讯视频 (可以拖动的GridView)附源码DEMO
- 程序员面试金典: 9.14 Java 14.1 从继承的角度来看,将构造函数声明为私有会有何作用?
- Tomcat连接池
- 洛谷 P1540 机器翻译
- ValidateCode验证码
- 算法提高 P1003
- 可拖拽排序的GridView(高仿今日头条编辑频道效果)
- 蓝桥杯练习系统基础练习——十六进制转十进制
- vector类笔记
- 抽象和接口的理解
- Java四大名著下载大全(中文+英文)
- 几个命令轻松搞定linux的服务状态
- javascript基础:数据类型、运算符
- js学习笔记(一)
- STM32F103学习记录-----构建库函数雏形