Android 實現高仿iOS桌面效果之可拖動的GridView(上)
阿新 • • 發佈:2018-12-30
最近專案中遇到一個LIstview的拖動效果,github上一搜發現有叫DragListview的開源專案,然後自己再小手一搜拖動排序的GridView,卻沒發現什麼很全很好的開源專案,後來在部落格上發現有一遍比較好的拖動排序的文章,可是跟自己的期望的IOS Luncher效果一比,相差那甚遠,因此就在別人的程式碼基礎加以開發,好的利用,不好的摒棄。
特此我們可以將兩個fragment加入同一個activty中,當然也可以將兩個gridview放到一個線性佈局中,即可,先從上面的Gridview進行分析。
1 GridView長按支援拖動排序,並支援Item實時交換。
2 GridView長按Item出現有抖動效果。
3 Item條目有抖動效果,時不需要長按點選就可以進行拖動效果。
4 拖動的Item和被拖動的Item左標完全重合後可新建資料夾
5 長按Item 出現刪除按鈕,此時點選刪除按鈕可以任意刪除某一item
6 GridView橫豎屏排列列數改變,橫屏的行數是豎螢幕的列數
分析完主要功能之後我們就開始程式碼實現策略,方便我們理清思路。
2 重寫dispatchTouchEvent() 安卓事件是在dispatchTouchEvent()進行分發的,因此我們在這裡攔截按下和移動,以及按鍵彈起等事件,在按下事件裡獲取按下的座標,以及按了哪個item, 去獲取當前itemview,並開啟一個延時Runnable,用來控制抖動生效的閥值,當時間達到此閥值使拖動狀態可用,同時也擷取當前itemView儲存為映象圖片。用於手指Move時充當window檢視。手勢鬆開時候許登出此定時器。 mScrollRunnable是用來作為超出邊界執行的定時器, 觸發GridView向下滾動 或向上滾動。而我們這裡還需要isShowShake是用來判斷當前是否需要顯示抖動效果,如果目前已經在抖動了,並且刪除按鈕可用的狀態,長按的閥值將會設定為最小值,用來實現不用長按即可拖動Item的目的。
3 監聽返回鍵 如果當前為抖動狀態,並且刪除按鈕可見,就停止抖動動畫,並影藏item的刪除按鈕。如果不在抖動狀態,則直接退出。
由於程式碼比較多 因此不一一做說明
結束語: 通過上面的實現方式我們簡單的實現了需求中的以下幾點: 1 GridView長按支援拖動排序,並支援Item實時交換。 2 GridView長按Item出現有抖動效果。 3 Item條目有抖動效果,時不需要長按點選就可以進行拖動效果。 5 長按Item 出現刪除按鈕,此時點選刪除按鈕可以任意刪除某一item 6 GridView橫豎屏排列列數改變,橫屏的行數是豎螢幕的列數 對於建立資料夾,點選資料夾顯示子集合view的暫未實現,包括一屏放置兩個gridview並且互相拖動交換的功能也暫未實現,接下來的文章將會繼續完善一下功能 歡迎閱讀 如果你覺得此實現方式有欠缺的地方可以直接在gtihub上進行進一步完善,將自己的技術繼續分享出來。謝謝你的閱讀和支援 參考博文:http://blog.csdn.net/xiaanming/article/details/17718579
一 UI和功能分析
發現目前主流的安卓廠商的手機桌面應用已經實現了此效果,也有APP實現的,如有UC瀏覽器,但是他貌似無抖動效果。先就Ios的桌面效果作如下需求總結: 我們可以把ios的luncher拆分一下 如下圖:特此我們可以將兩個fragment加入同一個activty中,當然也可以將兩個gridview放到一個線性佈局中,即可,先從上面的Gridview進行分析。
- 根據手指按下的X,Y座標來獲取我們在GridView上面點選的item位置
- 根據當前螢幕狀態,動態設定gridview的列數。做到橫豎屏展現不同個列數的效果。
- 長按手指達不到規定的時間閥值,將無法拖動狀態。時間超過將鬆開手指後,將gridView的子控制元件一次開啟抖動動畫。
- 如果我們長按了item則隱藏item,然後使用WindowManager來新增一個item的映象在螢幕用來代替剛剛隱藏的item
- 當我們手指在螢幕移動的時候,更新item映象的位置,然後在根據手指移動的X,Y的座標來確定當前映象的位置。
- 到GridView的item過多的時候,可能一螢幕顯示不完,我們手指拖動item映象到螢幕下方,要觸發GridView想上滾動,同理,當我們手指拖動item映象到螢幕上面,觸發GridView向下滾動
- GridView交換資料,重新整理介面,移除item的映象,顯示被影藏的item.
- 當抖動效果出現,點選刪除按鈕時,為了贈加移動效果,將要刪除的item和末位item交換,然後刪除lastItem,通知介面卡更新資料。
- 抖動效果出現後,如果Onclick,就視為可拖動狀態。
二 新建動畫控制器
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