1前言

由于项目需求,需要把项目的主界面采用GridView显示,并且需要根据模块优先级支持拖动图标(砍死产品狗)。为此,自定义了一个支持拖拽图标的GridView。效果如下:

具体效果如上图

2 可拖拽的GridView实现

要实现上面的效果有两个难点,第一就是如何创造一个可拖动的View在我们的Activity界面上。第二个就是如何实现两个View的交换

关于第一个:我们可以用WindowManager 来往我们的界面上添加View,这样我们再重写GridView的onTouchEvent()方法,根据移动的距离来更新的我们View的位置即可
关于第二个:可以这样实现,当我们拖动时,创建一个透明度低一点的镜像item
View。把要拖动的item对应的View先隐藏起来,此时Adapter的item先不交换,当我们把拖动的item移动到另一个item对应的范围内,我们再进行交换,先把这个item隐藏掉,然后在原来的位置显示出这个item。最后镜像item对应的view
再消失。

其实关于第二点,也有其他的交换策略,比如判断拖到镜像view到另一个item之上一段时间再进行交换等。

1 实现思路

好了,下面我们仔细总结了一下思路,有了思路我们就很好办了:

1.根据手指按下的X,Y坐标来获取我们在GridView上面点击的item,再获取对应的View

2.手指按下的时候使用Handler和Runnable来实现一个定时器,假如定时时间为1000毫秒,在1000毫秒内,如果手指抬起了就移除定时器,没有抬起并且手指点击在GridView的item所在的区域,则表示我们长按了GridView的item

3. 如果我们长按了item则隐藏item,然后使用WindowManager来添加一个item的镜像在屏幕用来代替刚刚隐藏的item

4.当我们手指在屏幕移动的时候,更新item镜像的位置,然后在根据我们移动的X,Y的坐标来获取移动到GridView的哪一个位置

5. 到GridView的item过多的时候,可能一屏幕显示不完,我们手指拖动item镜像到屏幕下方,要触发GridView想上滚动,同理,当我们手指拖动item镜像到屏幕上面,触发GridView向下滚动

6.GridView交换数据,刷新界面,移除item的镜像

2 实现代码

这里定义一个XGridView继承GridView来实现
代码如下,加了详细的注释,应该容易看懂

  1. package com.qiyei.javatest;
  2. import android.app.Activity;
  3. import android.content.Context;
  4. import android.graphics.Bitmap;
  5. import android.graphics.PixelFormat;
  6. import android.graphics.Rect;
  7. import android.os.Handler;
  8. import android.os.Vibrator;
  9. import android.util.AttributeSet;
  10. import android.view.Gravity;
  11. import android.view.MotionEvent;
  12. import android.view.View;
  13. import android.view.WindowManager;
  14. import android.widget.GridView;
  15. import android.widget.ImageView;
  16. /**
  17. * Created by qiyei2015 on 2017/1/5.
  18. */
  19. public class XGridView extends GridView {
  20. //拖拽响应的时间 默认为1s
  21. private long mDragResponseMs = 1000;
  22. //是否支持拖拽,默认不支持
  23. private boolean isDrag = false;
  24. //振动器,用于提示替换
  25. private Vibrator mVibrator;
  26. //拖拽的item的position
  27. private int mDragPosition;
  28. //拖拽的item对应的View
  29. private View mDragView;
  30. //窗口管理器,用于为Activity上添加拖拽的View
  31. private WindowManager mWindowManager;
  32. //item镜像的布局参数
  33. private WindowManager.LayoutParams mLayoutParams;
  34. //item镜像的 显示镜像,这里用ImageView显示
  35. private ImageView mDragMirrorView;
  36. //item镜像的bitmap
  37. private Bitmap mDragBitmap;
  38. //按下的点到所在item的左边缘距离
  39. private int mPoint2ItemLeft;
  40. private int mPoint2ItemTop;
  41. //DragView到上边缘的距离
  42. private int mOffset2Top;
  43. private int mOffset2Left;
  44. //按下时x,y
  45. private int mDownX;
  46. private int mDownY;
  47. //移动的时x.y
  48. private int mMoveX;
  49. private int mMoveY;
  50. //状态栏高度
  51. private int mStatusHeight;
  52. //XGridView向下滚动的边界值
  53. private int mDownScrollBorder;
  54. //XGridView向上滚动的边界值
  55. private int mUpScrollBorder;
  56. //滚动的速度
  57. private int mSpeed = 20;
  58. //item发生变化的回调接口
  59. private OnItemChangeListener changeListener;
  60. private Handler mHandler;
  61. /**
  62. * 长按的Runnable
  63. */
  64. private Runnable mLongClickRunable = new Runnable() {
  65. @Override
  66. public void run() {
  67. isDrag = true;
  68. mVibrator.vibrate(200);
  69. //隐藏该item
  70. mDragView.setVisibility(INVISIBLE);
  71. //在点击的地方创建并显示item镜像
  72. createDragView(mDragBitmap,mDownX,mDownY);
  73. }
  74. };
  75. /**
  76. * 当moveY的值大于向上滚动的边界值,触发GridView自动向上滚动
  77. * 当moveY的值小于向下滚动的边界值,触犯GridView自动向下滚动
  78. * 否则不进行滚动
  79. */
  80. private Runnable mScrollRunbale = new Runnable() {
  81. @Override
  82. public void run() {
  83. int scrollY = 0;
  84. if (mMoveY > mUpScrollBorder){
  85. scrollY = mSpeed;
  86. mHandler.postDelayed(mScrollRunbale,25);
  87. }else if (mMoveY < mDownScrollBorder){
  88. scrollY = -mSpeed;
  89. mHandler.postDelayed(mScrollRunbale,25);
  90. }else {
  91. scrollY = 0;
  92. mHandler.removeCallbacks(mScrollRunbale);
  93. }
  94. smoothScrollBy(scrollY,10);
  95. }
  96. };
  97. public XGridView(Context context) {
  98. this(context,null);
  99. }
  100. public XGridView(Context context, AttributeSet attrs) {
  101. this(context, attrs,0);
  102. }
  103. public XGridView(Context context, AttributeSet attrs, int defStyleAttr) {
  104. super(context, attrs, defStyleAttr);
  105. mVibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
  106. mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
  107. mHandler = new Handler();
  108. mStatusHeight = getStatusHeight(context);
  109. }
  110. @Override
  111. public boolean dispatchTouchEvent(MotionEvent ev) {
  112. switch (ev.getAction()){
  113. case MotionEvent.ACTION_DOWN:
  114. mDownX = (int)ev.getX();
  115. mDownY = (int)ev.getY();
  116. //获取按下的position
  117. mDragPosition = pointToPosition(mDownX,mDownY);
  118. if (mDragPosition == INVALID_POSITION){ //无效就返回
  119. return super.dispatchTouchEvent(ev);
  120. }
  121. //延时长按执行mLongClickRunable
  122. mHandler.postDelayed(mLongClickRunable,mDragResponseMs);
  123. //获取按下的item对应的View 由于存在复用机制,所以需要 处理FirstVisiblePosition
  124. mDragView = getChildAt(mDragPosition - getFirstVisiblePosition());
  125. if (mDragView == null){
  126. return super.dispatchTouchEvent(ev);
  127. }
  128. //计算按下的点到所在item的left top 距离
  129. mPoint2ItemLeft = mDownX - mDragView.getLeft();
  130. mPoint2ItemTop = mDownY - mDragView.getTop();
  131. //计算GridView的left top 偏移量:原始距离 - 相对距离就是偏移量
  132. mOffset2Left = (int)ev.getRawX() - mDownX;
  133. mOffset2Top = (int)ev.getRawY() - mDownY;
  134. //获取GridView自动向下滚动的偏移量,小于这个值,DragGridView向下滚动
  135. mDownScrollBorder = getHeight() /4;
  136. //获取GridView自动向上滚动的偏移量,大于这个值,DragGridView向上滚动
  137. mUpScrollBorder = getHeight() * 3/4;
  138. //开启视图缓存
  139. mDragView.setDrawingCacheEnabled(true);
  140. //获取缓存的中的bitmap镜像 包含了item中的ImageView和TextView
  141. mDragBitmap = Bitmap.createBitmap(mDragView.getDrawingCache());
  142. //释放视图缓存 避免出现重复的镜像
  143. mDragView.destroyDrawingCache();
  144. break;
  145. case MotionEvent.ACTION_MOVE:
  146. mMoveX = (int)ev.getX();
  147. mMoveY = (int)ev.getY();
  148. //如果只在按下的item上移动,未超过边界,就不移除mLongClickRunable
  149. if (!isTouchInItem(mDragView,mMoveX,mMoveY)){
  150. mHandler.removeCallbacks(mLongClickRunable);
  151. }
  152. break;
  153. case MotionEvent.ACTION_UP:
  154. mHandler.removeCallbacks(mLongClickRunable);
  155. mHandler.removeCallbacks(mScrollRunbale);
  156. break;
  157. default:
  158. break;
  159. }
  160. return super.dispatchTouchEvent(ev);
  161. }
  162. @Override
  163. public boolean onTouchEvent(MotionEvent ev) {
  164. if (isDrag && mDragMirrorView != null){
  165. switch (ev.getAction()){
  166. case MotionEvent.ACTION_DOWN:
  167. break;
  168. case MotionEvent.ACTION_MOVE:
  169. mMoveX = (int)ev.getX();
  170. mMoveY = (int)ev.getY();
  171. onDragItem(mMoveX,mMoveY);
  172. break;
  173. case MotionEvent.ACTION_UP:
  174. onStopDrag();
  175. isDrag = false;
  176. break;
  177. default:
  178. break;
  179. }
  180. return true;
  181. }
  182. return super.onTouchEvent(ev);
  183. }
  184. /************************对外提供的接口***************************************/
  185. public boolean isDrag() {
  186. return isDrag;
  187. }
  188. public void setDrag(boolean drag) {
  189. isDrag = drag;
  190. }
  191. public long getDragResponseMs() {
  192. return mDragResponseMs;
  193. }
  194. public void setDragResponseMs(long mDragResponseMs) {
  195. this.mDragResponseMs = mDragResponseMs;
  196. }
  197. public void setOnItemChangeListener(OnItemChangeListener changeListener) {
  198. this.changeListener = changeListener;
  199. }
  200. /******************************************************************************/
  201. /**
  202. * 点是否在该View上面
  203. * @param view
  204. * @param x
  205. * @param y
  206. * @return
  207. */
  208. private boolean isTouchInItem(View view, int x, int y) {
  209. if (view == null){
  210. return false;
  211. }
  212. if (view.getLeft() < x && x < view.getRight()
  213. && view.getTop() < y && y < view.getBottom()){
  214. return true;
  215. }else {
  216. return false;
  217. }
  218. }
  219. /**
  220. * 获取状态栏的高度
  221. * @param context
  222. * @return
  223. */
  224. private static int getStatusHeight(Context context){
  225. int statusHeight = 0;
  226. Rect localRect = new Rect();
  227. ((Activity) context).getWindow().getDecorView().getWindowVisibleDisplayFrame(localRect);
  228. statusHeight = localRect.top;
  229. if (0 == statusHeight){
  230. Class<?> localClass;
  231. try {
  232. localClass = Class.forName("com.android.internal.R$dimen");
  233. Object localObject = localClass.newInstance();
  234. int height = Integer.parseInt(localClass.getField("status_bar_height").get(localObject).toString());
  235. statusHeight = context.getResources().getDimensionPixelSize(height);
  236. } catch (Exception e) {
  237. e.printStackTrace();
  238. }
  239. }
  240. return statusHeight;
  241. }
  242. /**
  243. * 停止拖动
  244. */
  245. private void onStopDrag() {
  246. View view = getChildAt(mDragPosition - getFirstVisiblePosition());
  247. if (view != null){
  248. view.setVisibility(VISIBLE);
  249. }
  250. removeDragImage();
  251. }
  252. /**
  253. * WindowManager 移除镜像
  254. */
  255. private void removeDragImage() {
  256. if (mDragMirrorView != null){
  257. mWindowManager.removeView(mDragMirrorView);
  258. mDragMirrorView = null;
  259. }
  260. }
  261. /**
  262. * 拖动item到指定位置
  263. * @param x
  264. * @param y
  265. */
  266. private void onDragItem(int x, int y) {
  267. mLayoutParams.x = x - mPoint2ItemLeft + mOffset2Left;
  268. mLayoutParams.y = y - mPoint2ItemTop + mOffset2Top - mStatusHeight;
  269. //更新镜像位置
  270. mWindowManager.updateViewLayout(mDragMirrorView,mLayoutParams);
  271. onSwapItem(x,y);
  272. mHandler.post(mScrollRunbale);
  273. }
  274. /**
  275. * 交换 item 并且控制 item之间的显示与隐藏
  276. * @param x
  277. * @param y
  278. */
  279. private void onSwapItem(int x, int y) {
  280. //获取我们手指移动到那个item
  281. int tmpPosition = pointToPosition(x,y);
  282. if (tmpPosition != INVALID_POSITION && tmpPosition != mDragPosition){
  283. if (changeListener != null){
  284. changeListener.onChange(mDragPosition,tmpPosition);
  285. }
  286. //隐藏tmpPosition
  287. getChildAt(tmpPosition - getFirstVisiblePosition()).setVisibility(INVISIBLE);
  288. //显示之前的item
  289. getChildAt(mDragPosition - getFirstVisiblePosition()).setVisibility(VISIBLE);
  290. mDragPosition = tmpPosition;
  291. }
  292. }
  293. /**
  294. * 创建拖动的镜像
  295. * @param bitmap
  296. * @param downX
  297. * @param downY
  298. */
  299. private void createDragView(Bitmap bitmap, int downX, int downY) {
  300. mLayoutParams = new WindowManager.LayoutParams();
  301. mLayoutParams.format = PixelFormat.TRANSLUCENT; //图片之外其他地方透明
  302. mLayoutParams.gravity = Gravity.TOP | Gravity.LEFT; //左 上
  303. //指定位置 其实就是 该 item 对应的 rawX rawY 因为Window 添加View是需要知道 raw x ,y的
  304. mLayoutParams.x = mOffset2Left + (downX - mPoint2ItemLeft);
  305. mLayoutParams.y = mOffset2Top + (downY - mPoint2ItemTop) + mStatusHeight;
  306. //指定布局大小
  307. mLayoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
  308. mLayoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
  309. //透明度
  310. mLayoutParams.alpha = 0.4f;
  311. //指定标志 不能获取焦点和触摸
  312. mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
  313. | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
  314. mDragMirrorView = new ImageView(getContext());
  315. mDragMirrorView.setImageBitmap(bitmap);
  316. //添加View到窗口中
  317. mWindowManager.addView(mDragMirrorView,mLayoutParams);
  318. }
  319. /**
  320. * item 交换时的回调接口
  321. */
  322. public interface OnItemChangeListener{
  323. void onChange(int from,int to);
  324. }

android 自定义View开发实战(六) 可拖动的GridView的更多相关文章

  1. Android自定义View实战(SlideTab-可滑动的选择器)

    转载请标明出处: http://blog.csdn.net/xmxkf/article/details/52178553 本文出自:[openXu的博客] 目录: 初步分析重写onDraw绘制 重写o ...

  2. 【朝花夕拾】Android自定义View篇之(六)Android事件分发机制(中)从源码分析事件分发逻辑及经常遇到的一些“诡异”现象

    前言 转载请注明,转自[https://www.cnblogs.com/andy-songwei/p/11039252.html]谢谢! 在上一篇文章[[朝花夕拾]Android自定义View篇之(五 ...

  3. android 自定义view 前的基础知识

    本篇文章是自己自学自定义view前的准备,具体参考资料来自 Android LayoutInflater原理分析,带你一步步深入了解View(一) Android视图绘制流程完全解析,带你一步步深入了 ...

  4. 【朝花夕拾】Android自定义View篇之(八)多点触控(上)MotionEvent简介

    前言 在前面的文章中,介绍了不少触摸相关的知识,但都是基于单点触控的,即一次只用一根手指.但是在实际使用App中,常常是多根手指同时操作,这就需要用到多点触控相关的知识了.多点触控是在Android2 ...

  5. Android 自定义View合集

    自定义控件学习 https://github.com/GcsSloop/AndroidNote/tree/master/CustomView 小良自定义控件合集 https://github.com/ ...

  6. Android 自定义View (五)——实践

    前言: 前面已经介绍了<Android 自定义 view(四)-- onMeasure 方法理解>,那么这次我们就来小实践下吧 任务: 公司现有两个任务需要我完成 (1)监测液化天然气液压 ...

  7. Android 自定义 view(四)—— onMeasure 方法理解

    前言: 前面我们已经学过<Android 自定义 view(三)-- onDraw 方法理解>,那么接下我们还需要继续去理解自定义view里面的onMeasure 方法 推荐文章: htt ...

  8. Android 自定义View及其在布局文件中的使用示例(二)

    转载请注明出处 http://www.cnblogs.com/crashmaker/p/3530213.html From crash_coder linguowu linguowu0622@gami ...

  9. Android自定义View

    转载请标明出处:http://blog.csdn.net/lmj623565791/article/details/24252901 很多的Android入门程序猿来说对于Android自定义View ...

随机推荐

  1. Python 迭代器&生成器,装饰器,递归,算法基础:二分查找、二维数组转换,正则表达式,作业:计算器开发

    本节大纲 迭代器&生成器 装饰器  基本装饰器 多参数装饰器 递归 算法基础:二分查找.二维数组转换 正则表达式 常用模块学习 作业:计算器开发 实现加减乘除及拓号优先级解析 用户输入 1 - ...

  2. java web 项目常用框架

    java框架实在是太多了,网上一搜索一大箩筐,根本就了解不到什么. 我还是以我的经验来说一下j2ee的框架. 1.首先力推struts2框架,这是最经典的框架(可以说没有“之一”).可以帮你快速搭建出 ...

  3. linux命令1——基础

    Rm 删除命令 Rm [选项][文件] 删除一个文件或者目录 选项:r 递归的删除文件夹及其子文件,f 忽略不存在的文件(不提示) (2)rm删除目录下所有文件,但不删除目录 >>rm - ...

  4. hdu 2736 Average distance

    传送门 Average distance Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Oth ...

  5. Android广播Broadcast

    Android Broadcast简单认识 Broadcast是应用程序间传输信息的一种机制,BroadcastReceiver是对发送出来的广播(Broadcast)进行过滤并接收相应的一类组件. ...

  6. wordpress优化:Gravatar头像被墙及解决方案

    网站缓存现象: 打开网站是左下角出现0.gravatar.com.1.gravatar.com或2.gravatar.com字样,网站一直处于缓存状态,迟迟未能打开.很多人都会缺乏耐心地等待一个网页的 ...

  7. kafka调试中遇到Connection to node -1 could not be established. Broker may not be available.

    https://blog.csdn.net/Mr_Hou2016/article/details/79484032

  8. tyvj——P3524 最大半连通子图

    P3524 最大半连通子图 时间: 3000ms / 空间: 165536KiB / Java类名: Main 描述 输入格式 第一行包含两个整数N,M,X.N,M分别表示图G的点数与边数,X的意义如 ...

  9. Spring mvc之SimpleUrlHandlerMapping

    1.配置文件如下 <bean id="method" class="com.xx.controller.xxxController" scope=&quo ...

  10. 前端开发数据mock神器 -- xl_mock

    1.为什么要实现数据 mock 要理解为什么要实现数据 mock,我们可以提供几个场景来解释, 1.现在的开发很多都是前后端分离的模式,前后端的工作是不同的,当我们前端界面已经完成,但是后端的接口迟迟 ...