Android 实现高仿iOS桌面效果之可拖动的GridView(上)

版权声明:本文为博主原创文章,请尊重原创,转载请标明地址。有问题请关注 [开发者技术前线] 咨询 https://blog.csdn.net/sk719887916/article/details/40074663
   
      转载请标明出处:http://blog.csdn.net/sk719887916/article/details/40074663,作者:skay
     最近项目中遇到一个LIstview的拖动效果,github上一搜发现有叫DragListview的开源项目,然后自己再小手一搜拖动排序的GridView,却没发现什么很全很好的开源项目,后来在博客上发现有一遍比较好的拖动排序的文章,可是跟自己的期望的IOS Luncher效果一比,相差那甚远,因此就在别人的代码基础加以开发,好的利用,不好的摒弃。

一 UI和功能分析

    发现目前主流的安卓厂商的手机桌面应用已经实现了此效果,也有APP实现的,如有UC浏览器,但是他貌似无抖动效果。先就Ios的桌面效果作如下需求总结:
 我们可以把ios的luncher拆分一下 如下图:

 特此我们可以将两个fragment加入同一个activty中,当然也可以将两个gridview放到一个线性布局中,即可,先从上面的Gridview进行分析。
    1  GridView长按支持拖动排序,并支持Item实时交换。
     2  GridView长按Item出现有抖动效果。
     3 Item条目有抖动效果,时不需要长按点击就可以进行拖动效果。
      4 拖动的Item和被拖动的Item左标完全重合后可新建文件夹
      5 长按Item 出现删除按钮,此时点击删除按钮可以任意删除某一item
      6 GridView横竖屏排列列数改变,横屏的行数是竖屏幕的列数

分析完主要功能之后我们就开始代码实现策略,方便我们理清思路。

  1. 根据手指按下的X,Y坐标来获取我们在GridView上面点击的item位置
  2. 根据当前屏幕状态,动态设置gridview的列数。做到横竖屏展现不同个列数的效果。
  3. 长按手指达不到规定的时间阀值,将无法拖动状态。时间超过将松开手指后,将gridView的子控件一次开启抖动动画。
  4. 如果我们长按了item则隐藏item,然后使用WindowManager来添加一个item的镜像在屏幕用来代替刚刚隐藏的item
  5. 当我们手指在屏幕移动的时候,更新item镜像的位置,然后在根据手指移动的X,Y的坐标来确定当前镜像的位置。
  6. 到GridView的item过多的时候,可能一屏幕显示不完,我们手指拖动item镜像到屏幕下方,要触发GridView想上滚动,同理,当我们手指拖动item镜像到屏幕上面,触发GridView向下滚动
  7. GridView交换数据,刷新界面,移除item的镜像,显示被影藏的item.
  8. 当抖动效果出现,点击删除按钮时,为了赠加移动效果,将要删除的item和末位item交换,然后删除lastItem,通知适配器更新数据。
  9. 抖动效果出现后,如果Onclick,就视为可拖动状态。
   接下来就开始实现需求所列的效果了。为了实现上面需求,需要写一个Gridview,Adapter,和抖动动画。包括移动是的动画。

二 新建动画控制器 

   1  item实现抖动效果
    新建一个抖动的动画效果,用于每个item进行抖动。
  
	/**
	 * NeedShake
	 * @return
	 */
	public boolean isNeedShake() {
		return mNeedShake;
	}

	/**
	 * @param mNeedShake
	 */
	public void setNeedShake(boolean mNeedShake) {
		this.mNeedShake = mNeedShake;
	}
	
	
	/**
	 *  ShakeAnimation isRunning
	 * @return
	 */
	private boolean isShowShake() {
		
		return mNeedShake && mStartShake;
		
	}

	/**
	 * start shakeAnimation
	 * @param v
	 */
	private void shakeAnimation(final View v) {
		float rotate = 0;
		int c = mCount++ % 15;
		if (c == 0) {
			rotate = DEGREE_0;
		} else if (c == 1) {
			rotate = DEGREE_1;
		} else if (c == 2) {
			rotate = DEGREE_2;
		} else if (c == 3) {
			rotate = DEGREE_3;
		} else {
			rotate = DEGREE_4;
		}
		final RotateAnimation mra = new RotateAnimation(rotate, -rotate,
				ICON_WIDTH * mDensity / 4, ICON_HEIGHT * mDensity / 4);
		final RotateAnimation mrb = new RotateAnimation(-rotate, rotate,
				ICON_WIDTH * mDensity / 4, ICON_HEIGHT * mDensity / 4);

		mra.setDuration(ANIMATION_DURATION);
		mrb.setDuration(ANIMATION_DURATION);

		mra.setAnimationListener(new AnimationListener() {
			@Override
			public void onAnimationEnd(Animation animation) {
				if (mNeedShake && mStartShake) {
					mra.reset();
					v.startAnimation(mrb);
				}
			}

			@Override
			public void onAnimationRepeat(Animation animation) {

			}

			@Override
			public void onAnimationStart(Animation animation) {

			}

		});

		mrb.setAnimationListener(new AnimationListener() {
			@Override
			public void onAnimationEnd(Animation animation) {
				if (mNeedShake && mStartShake) {
					mrb.reset();
					v.startAnimation(mra);
				}
			}

			@Override
			public void onAnimationRepeat(Animation animation) {

			}

			@Override
			public void onAnimationStart(Animation animation) {

			}

		});
		v.startAnimation(mra);
	}
   2 创建item交换是的动画
     
private AnimatorSet createTranslationAnimations(View view, float startX,
			float endX, float startY, float endY) {
		ObjectAnimator animX = ObjectAnimator.ofFloat(view, "translationX",
				startX, endX);
		ObjectAnimator animY = ObjectAnimator.ofFloat(view, "translationY",
				startY, endY);
		AnimatorSet animSetXY = new AnimatorSet();
		animSetXY.playTogether(animX, animY);
		return animSetXY;
	}

	/**
	 * item的交换动画效果
	 * 
	 * @param oldPosition
	 * @param newPosition
	 */
	private void animateReorder(final int oldPosition, final int newPosition) {
		boolean isForward = newPosition > oldPosition;
		List<Animator> resultList = new LinkedList<Animator>();
		if (isForward) {
			for (int pos = oldPosition; pos < newPosition; pos++) {
				View view = getChildAt(pos - getFirstVisiblePosition());
				System.out.println(pos);

				if ((pos + 1) % mNumColumns == 0) {
					resultList.add(createTranslationAnimations(view,
							-view.getWidth() * (mNumColumns - 1), 0,
							view.getHeight(), 0));
				} else {
					resultList.add(createTranslationAnimations(view,
							view.getWidth(), 0, 0, 0));
				}
			}
		} else {
			for (int pos = oldPosition; pos > newPosition; pos--) {
				View view = getChildAt(pos - getFirstVisiblePosition());
				if ((pos + mNumColumns) % mNumColumns == 0) {
					resultList.add(createTranslationAnimations(view,
							view.getWidth() * (mNumColumns - 1), 0,
							-view.getHeight(), 0));
				} else {
					resultList.add(createTranslationAnimations(view,
							-view.getWidth(), 0, 0, 0));
				}
			}
		}

		AnimatorSet resultSet = new AnimatorSet();
		resultSet.playTogether(resultList);
		resultSet.setDuration(300);
		resultSet.setInterpolator(new AccelerateDecelerateInterpolator());
		resultSet.addListener(new AnimatorListenerAdapter() {
			@Override
			public void onAnimationStart(Animator animation) {
				mAnimationEnd = false;
			}

			@Override
			public void onAnimationEnd(Animator animation) {
				mAnimationEnd = true;
			}
		});
		resultSet.start();
	}

三 建立Adapter自定义监听器

      新建的Adapter用于Item的删除,隐藏,排序等,除了以上方法,也承担adapter适配数据源到grifView上的功能。
ublic interface DragGridListener {
	/**
	 * 重新排列数据
	 * @param oldPosition
	 * @param newPosition
	 */
	public void reorderItems(int oldPosition, int newPosition);
	
	
	/**
	 * 设置某个item隐藏
	 * @param hidePosition
	 */
	public void setHideItem(int hidePosition);
	
	
	/**
	 * 删除某个item
	 * @param hidePosition
	 */
	public void removeItem(int hidePosition);
	

}
    当然本次还未实现两个item建立文件夹,因此此接口后面还会陆续加入其扩展方法。
四  adpter
      用来控制Item的添加和删除,已经隐藏交换等。
public class DragAdapter extends BaseAdapter implements DragGridListener{
	private List<HashMap<String, Object>> list;
	private LayoutInflater mInflater;
	private int mHidePosition = -1;
	
	public DragAdapter(Context context, List<HashMap<String, Object>> list){
		this.list = list;
		mInflater = LayoutInflater.from(context);
	}

	@Override
	public int getCount() {
		return list.size();
	}

	@Override
	public Object getItem(int position) {
		return list.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	/**
	 * 由于复用convertView导致某些item消失了,所以这里不复用item,
	 */
	@Override
	public View getView(final int position, View convertView, ViewGroup parent) {
		convertView = mInflater.inflate(R.layout.grid_item, null);
		ImageView mImageView = (ImageView) convertView.findViewById(R.id.item_image);
		TextView mTextView = (TextView) convertView.findViewById(R.id.item_text);
		
		mImageView.setImageResource((Integer) list.get(position).get("item_image"));
		mTextView.setText((CharSequence) list.get(position).get("item_text"));
		
		if(position == mHidePosition){
			convertView.setVisibility(View.INVISIBLE);
		}
		
		return convertView;
	}
	

	@Override
	public void reorderItems(int oldPosition, int newPosition) {
		HashMap<String, Object> temp = list.get(oldPosition);
		if(oldPosition < newPosition){
			for(int i=oldPosition; i<newPosition; i++){
				Collections.swap(list, i, i+1);
			}
		}else if(oldPosition > newPosition){
			for(int i=oldPosition; i>newPosition; i--){
				Collections.swap(list, i, i-1);
			}
		}
		
		list.set(newPosition, temp);
	}

	@Override
	public void setHideItem(int hidePosition) {
		this.mHidePosition = hidePosition; 
		notifyDataSetChanged();
	}

	@Override
	public void removeItem(int deletePosition) {
		
		list.remove(deletePosition);
		notifyDataSetChanged();
	}

 四 GridVIew

   1 自定义DragridView继承GridView,重写onMueause()用来重新测量和根据横竖屏设置不同的列数
  
@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		
		if (mNumColumns == AUTO_FIT){
			if (isLandscape(getContext())) {
				mPaddingTopInit  = (int) getResources().getDimension(R.dimen.HriontalPaddingTop);
				setNumColumns(mColumnNum_Hriztal);

			} else {
				setNumColumns(mColumnNum);
			}
		}

		setPadding(mPaddingLeftInit, mPaddingTopInit, mPaddingRightInit, mPaddingBottomInit);
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
	}

       2  重写dispatchTouchEvent()
        安卓事件是在dispatchTouchEvent()进行分发的,因此我们在这里拦截按下和移动,以及按键弹起等事件,在按下事件里获取按下的坐标,以及按了哪个item, 去获取当前itemview,并开启一个延时Runnable,用来控制抖动生效的阀值,当时间达到此阀值使拖动状态可用,同时也截取当前itemView保存为镜像图片。用于手指Move时充当window视图。手势松开时候许注销此定时器。
 mScrollRunnable是用来作为超出边界执行的定时器,   触发GridView向下滚动 或向上滚动。而我们这里还需要isShowShake是用来判断当前是否需要显示抖动效果,如果目前已经在抖动了,并且删除按钮可用的状态,长按的阀值将会设置为最小值,用来实现不用长按即可拖动Item的目的。
@Override
	public boolean dispatchTouchEvent(MotionEvent ev) {
		switch (ev.getAction()) {
		case MotionEvent.ACTION_DOWN:
			mDownX = (int) ev.getX();
			mDownY = (int) ev.getY();

			// 根据按下的X,Y坐标获取所点击item的position
			mDragPosition = pointToPosition(mDownX, mDownY);
			

			if (mDragPosition == AdapterView.INVALID_POSITION) {
				return super.dispatchTouchEvent(ev);
			}
			
			mStartDragItemView = getChildAt(mDragPosition
					- getFirstVisiblePosition());
		
			//
			//performLongClick();
			if (isShowShake() && isShowDelele()) {
				dragResponseMS = dragResponseCT;
			}
			else {
				dragResponseMS = 1000 ;
			}
			

			// 使用Handler延迟dragResponseMS执行mLongClickRunnable
			mHandler.postDelayed(mLongClickRunnable, dragResponseMS);
			
			mPoint2ItemTop = mDownY - mStartDragItemView.getTop();
			mPoint2ItemLeft = mDownX - mStartDragItemView.getLeft();

			mOffset2Top = (int) (ev.getRawY() - mDownY);
			mOffset2Left = (int) (ev.getRawX() - mDownX);

			// 获取DragGridView自动向上滚动的偏移量,小于这个值,DragGridView向下滚动
			mDownScrollBorder = getHeight() / 5;
			// 获取DragGridView自动向下滚动的偏移量,大于这个值,DragGridView向上滚动
			mUpScrollBorder = getHeight() * 4 / 5;

			// 开启mDragItemView绘图缓存
			mStartDragItemView.setDrawingCacheEnabled(true);
			// 获取mDragItemView在缓存中的Bitmap对象
			mDragBitmap = Bitmap.createBitmap(mStartDragItemView
					.getDrawingCache());
			// 这一步很关键,释放绘图缓存,避免出现重复的镜像
			mStartDragItemView.destroyDrawingCache();
			
			

			break;
		case MotionEvent.ACTION_MOVE:
			int moveX = (int) ev.getX();
			int moveY = (int) ev.getY();
			if (!isTouchInItem(mStartDragItemView, moveX, moveY)) {
				
				mHandler.removeCallbacks(mLongClickRunnable);
			}
			break;
		case MotionEvent.ACTION_UP:
			mHandler.removeCallbacks(mLongClickRunnable);
			mHandler.removeCallbacks(mScrollRunnable);
			
			break;
		}
		return super.dispatchTouchEvent(ev);
	}
    2 onTuch()
      主要用来处理当前手势,按下移动时就开始执行拖动,并执行抖动效果,弹起停止拖动效果,继续开启抖动动画,重新排列gridview
@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (isDrag && mDragImageView != null) {
			switch (ev.getAction()) {
			case MotionEvent.ACTION_MOVE:
				moveX = (int) ev.getX();
				moveY = (int) ev.getY();
				onDragItem(moveX, moveY);
				onStartAnimation();
				break;
			case MotionEvent.ACTION_UP:
				onStopDrag();
				isDrag = false;
				onStartAnimation();
				
				break;
			}
			return true;
		}
		return super.onTouchEvent(ev);
	}
   拖动某一个Item时 根据移动的x,y来实时更新当前截取的item镜像窗体位置位置。
/**
	 * 拖动item,在里面实现了item镜像的位置更新,item的相互交换以及GridView的自行滚动
	 * 
	 * @param x
	 * @param y
	 */
	private void onDragItem(int moveX, int moveY) {
		
		
		mDragAdapter.setHideItem(mDragPosition);
		//setHideSartItemView();
		mWindowLayoutParams.x = moveX - mPoint2ItemLeft + mOffset2Left;
		mWindowLayoutParams.y = moveY - mPoint2ItemTop + mOffset2Top
				- mStatusHeight;
		if (mDragImageView != null) {
			mWindowManager.updateViewLayout(mDragImageView, mWindowLayoutParams); // 更新镜像的位置
		}
		onSwapItem(moveX, moveY);

		// GridView自动滚动
		mHandler.post(mScrollRunnable);
	}
   手指弹起时,将镜像移除,将移动本身item设置为可见状态,
	/**
	 * 停止拖拽我们将之前隐藏的item显示出来,并将镜像移除
	 */
	private void onStopDrag() {
		View view = getChildAt(mDragPosition - getFirstVisiblePosition());
		if (view != null) {
			view.setVisibility(View.VISIBLE);
		}
		mDragAdapter.setHideItem(-1);
		removeDragImage();
	}

 3 监听返回键 
   如果当前为抖动状态,并且删除按钮可见,就停止抖动动画,并影藏item的删除按钮。如果不在抖动状态,则直接退出。
 
@Override    
    public boolean onKeyDown(int keyCode, KeyEvent event) {    
      if (keyCode == KeyEvent.KEYCODE_BACK) {    
              pressAgainExit();    
              return true;    
         }    
    
        return super.onKeyDown(keyCode, event);    
    } 
	
	
	 /**
	 * pressAgainExit
	 */
	private void pressAgainExit() {    
		  
	          if (mEMrg.isExit() || mDeleteButton ==null   ) {    
	                System.exit(0);
	          } else { 
	        	  setHideDeleltButton();
	        	  
	        	  if (mStartShake && mNeedShake) {
	        		  
	        		  onStopAnimation();
	        	  }
	        	 
	             
	              mEMrg.doExitInOneSecond();    
	          }
	          
	          
	      
	} 
	

由于代码比较多 因此不一一做说明 
具体代码如下详见开源地址:https://github.com/NeglectedByBoss/IOS_DragGridView

 五 Activity

     用于初始化数据等,这里不做详细说明。
/**
 * 
 * 
 * @author lyk
 *
 */
public class DemoMainActivity extends Activity implements OnItemClickListener{
	private List<HashMap<String, Object>> dataSourceList = new ArrayList<HashMap<String, Object>>();
	
	/**
	 * 一页可见提条目数
	 */
	private static final int VISIBIY_NUMS = 24;
	private DragAdapter mDragAdapter;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		
		DragGridView mDragGridView = (DragGridView)findViewById(R.id.dragGridView);
		mDragGridView.setOnItemClickListener(this);
	
		for (int i = 0; i < VISIBIY_NUMS; i++) {
			HashMap<String, Object> itemHashMap = new HashMap<String, Object>();
			Random random =new Random();
			
			
			if (random.nextInt(3) == 1) {
				itemHashMap.put("item_image",R.drawable.ic_icon);
			}
			
			if (random.nextInt(3) == 0) {
				itemHashMap.put("item_image",R.drawable.icon);
			}
			
			else {
				itemHashMap.put("item_image",R.drawable.icon4);
			}
			itemHashMap.put("item_text", "icon" + Integer.toString(i));
			dataSourceList.add(itemHashMap);
		}
		mDragAdapter = new DragAdapter(this, dataSourceList);
		
		mDragGridView.setAdapter(mDragAdapter);
		//设置需要抖动
		mDragGridView.setNeedShake(true);
	}
	@Override
	public void onItemClick(AdapterView<?> parent, View view, int position,
			long id) {
		
		Toast.makeText(this, "onClick:" + position,    
                Toast.LENGTH_SHORT).show();   
	}
	

}
效果:
动态效果参考iphone桌面luncher效果。


      结束语:
     通过上面的实现方式我们简单的实现了需求中的以下几点:
    1  GridView长按支持拖动排序,并支持Item实时交换。
     2  GridView长按Item出现有抖动效果。
     3 Item条目有抖动效果,时不需要长按点击就可以进行拖动效果。
      5 长按Item 出现删除按钮,此时点击删除按钮可以任意删除某一item
      6 GridView横竖屏排列列数改变,横屏的行数是竖屏幕的列数
 对于建立文件夹,点击文件夹显示子集合view的暂未实现,包括一屏放置两个gridview并且互相拖动交换的功能也暂未实现,接下来的文章将会继续完善一下功能 欢迎阅读
如果你觉得此实现方式有欠缺的地方可以直接在gtihub上进行进一步完善,将自己的技术继续分享出来。谢谢你的阅读和支持
 参考博文:http://blog.csdn.net/xiaanming/article/details/17718579
  


展开阅读全文

没有更多推荐了,返回首页