1. 程式人生 > >Android 實現高仿iOS桌面效果之可拖動的GridView(上)

Android 實現高仿iOS桌面效果之可拖動的GridView(上)

     最近專案中遇到一個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();    
	          }
	          
	          
	      
	} 
	

由於程式碼比較多 因此不一一做說明 

 五 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