废话不多说先上图,只有一张静态图,实现的是可拖拽的GridView。有很什么方便的GIF制作软件,推荐一下,另外我从windows Live writer上传blog中的代码样式变得好丑了:
说到这里再推荐一篇同样是写可拖拽GridView的blogAndroid 可拖拽的GridView效果实现, 长按可拖拽和item实时交换
这里很好的讲解了拖拽的原理,也实现了item交换的效果。下面是他的步骤,和我要分析的这个开源项目有一些不同,我将比较来讲。
先说一下推荐blog的思路:
DynamicGridView的思路:
看起来两者的区别不大,但是上面只是一个思路,在多细节上的有很大的差异。这里交换数据是指将某一项数据从就得位置插入到新的位置,而不是前面blog中的将两个位置的数据交换。
作者在判断是否符合交换条件和找到item上面花费了比较多的代码。代码比较多,再看代码之前,有几个关键点。
1.GridView中的item视图从没有真正移动过,添加动画也不会改变item的真正位置
2.假设没有BitmapDrawable即镜像不存在,也不考虑动画效果,我们就可以跟清楚的知道真正发生改变的是什么。我们的手指移到哪里,哪里的item视图便隐藏,原先的隐藏的视图可见。在这个过程中改变数据的位置。
3.然后才是item交换的动画发生,此时data数据已经交换完成,并且调用了adapter.notifyDatasetchange()函数。
画一个草图来表示一下。
图片上显示的是adapter提供的数据,虚线表示为不可见。
第一张图,表示手指从5划入数据是9的item范围内,在数据层就是将5插入数据9之后,在视图上就是将镜像移动到数据为9的item范围内,并位置为4的视图设置为可见,位置为8的视图设置为不可见,等待下轮绘制。
第二张图,在下一轮draw环节,若不加人动画效果,GridView就会这样显示,位置为8的视图不可见,当然这个时候你会看到有着一个随着手指移动的显示数据为5的镜像。
第三张图,表示在显示第二张图的时候给Item视图添加的动画,比如会给显示数据7的位置为5的视图(绿色)设置这样的动画,从位置6(黄色)移过来。这样整体的效果就好像镜像让出一个位置,然后后面的视图一个接一个的补上。
我根据这个开源项目完成了一个demo,觉得作者实现的有些地方很巧妙,比如动画,有些地方有些冗余,比如根据Id找到数据位置在找到的视图位置。
package org.askerov.dynamicgid;
import android.animation.*;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.GridView;
import android.widget.ListAdapter;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
/**
* Author: alex askerov
* Date: 9/6/13
* Time: 12:31 PM
*/
public class DynamicGridView extends GridView {
private static final int INVALID_ID = AbstractDynamicGridAdapter.INVALID_ID;
/*
* 动画持续时间
*/
private static final int MOVE_DURATION = 300;
private static final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 8;
/*
* Item 镜像
*/
private BitmapDrawable mHoverCell;
/*
* BitmapDrawable当前边界
*/
private Rect mHoverCellCurrentBounds;
/*
* BitmapDrawable最初边界
*/
private Rect mHoverCellOriginalBounds;
/*
* 这个,不过多解释,当前边界与最初边界的差值
*/
private int mTotalOffsetY = 0;
private int mTotalOffsetX = 0;
/*
* mDownX,mDown记录手指按下的位置
* mLastEventY,mLastEventX最后手指移动的位置
* 每次更新完mHoverCellCurrentBounds位置后,会将mLastEventY,mLastEventX的值赋给mDownX,mDown
* 个人感觉作者在这一点上定义的变量有些重复
*/
private int mDownX = -1;
private int mDownY = -1;
private int mLastEventY = -1;
private int mLastEventX = -1;
/*
* 作者自定义的adapter中为每一个data设置了一个Long型Id,在我自己写的demo中没有用到这个。
* 作者的思路是将每一项数据和一个Id绑定,通过Id可以找到这个数据现在的位置position,这个位置就是该项数据在视图中的位置
*/
private List<Long> idList = new ArrayList<Long>();
/*
* 记录按下的item对应数据的Id,这个值在一次完整的拖拽中不会改变
*/
private long mMobileItemId = INVALID_ID;
/*
*
*/
private boolean mCellIsMobile = false;
private int mActivePointerId = INVALID_ID;
private boolean mIsMobileScrolling;
private int mSmoothScrollAmountAtEdge = 0;
private boolean mIsWaitingForScrollFinish = false;
private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
private boolean mIsEditMode = false;
private List<ObjectAnimator> mWobbleAnimators = new LinkedList<ObjectAnimator>();
private OnDropListener mDropListener;
private boolean mHoverAnimation;
/*
* 当数据交换时item的动画
*/
private boolean mReorderAnimation;
/*
* 当进入拖拽模式后,没有被选中的item会左右摇晃,mWobbleInEditMode表示是否要开启摇晃的动画模式
*/
private boolean mWobbleInEditMode = true;
private OnItemLongClickListener mUserLongClickListener;
/*
* 拖拽模式的入口
*/
private OnItemLongClickListener mLocalLongClickListener = new OnItemLongClickListener() {
public boolean onItemLongClick(AdapterView
arg0, View arg1, int pos, long id) {
if (!isEnabled() || isEditMode())
return false;
mTotalOffsetY = 0;
mTotalOffsetX = 0;
int position = pointToPosition(mDownX, mDownY);
int itemNum = position - getFirstVisiblePosition();
View selectedView = getChildAt(itemNum);
//获取与该项数据绑定的Id
mMobileItemId = getAdapter().getItemId(position);
//获取镜像 bitmapdrawable
mHoverCell = getAndAddHoverView(selectedView);
if (isPostHoneycomb() && selectedView != null)
selectedView.setVisibility(View.INVISIBLE);
mCellIsMobile = true;
//将当前Gridview可见的item所对应的数据的Id记为邻居
updateNeighborViewsForId(mMobileItemId);
if (isPostHoneycomb() && mWobbleInEditMode)
startWobbleAnimation();
if (mUserLongClickListener != null)
mUserLongClickListener.onItemLongClick(arg0, arg1, pos, id);
mIsEditMode = true;
return true;
}
};
private OnItemClickListener mUserItemClickListener;
private OnItemClickListener mLocalItemClickListener = new OnItemClickListener() {
@Override
public void onItemClick(AdapterView
parent, View view, int position, long id) {
if (!isEditMode() && isEnabled() && mUserItemClickListener != null) {
mUserItemClickListener.onItemClick(parent, view, position, id);
}
}
};
public DynamicGridView(Context context) {
super(context);
init(context);
}
public DynamicGridView(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public DynamicGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context);
}
public void setOnDropListener(OnDropListener dropListener) {
this.mDropListener = dropListener;
}
public void startEditMode() {
mIsEditMode = true;
if (isPostHoneycomb() && mWobbleInEditMode)
startWobbleAnimation();
}
public void stopEditMode() {
mIsEditMode = false;
if (isPostHoneycomb() && mWobbleInEditMode)
stopWobble(true);
}
public boolean isEditMode() {
return mIsEditMode;
}
public boolean isWobbleInEditMode() {
return mWobbleInEditMode;
}
public void setWobbleInEditMode(boolean wobbleInEditMode) {
this.mWobbleInEditMode = wobbleInEditMode;
}
@Override
public void setOnItemLongClickListener(final OnItemLongClickListener listener) {
mUserLongClickListener = listener;
super.setOnItemLongClickListener(mLocalLongClickListener);
}
@Override
public void setOnItemClickListener(OnItemClickListener listener) {
this.mUserItemClickListener = listener;
super.setOnItemClickListener(mLocalItemClickListener);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void startWobbleAnimation() {
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
if (v != null && Boolean.TRUE != v.getTag(R.id.dynamic_grid_wobble_tag)) {
if (i % 2 == 0)
animateWobble(v);
else
animateWobbleInverse(v);
v.setTag(R.id.dynamic_grid_wobble_tag, true);
}
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void stopWobble(boolean resetRotation) {
for (Animator wobbleAnimator : mWobbleAnimators) {
wobbleAnimator.cancel();
}
mWobbleAnimators.clear();
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
if (v != null) {
if (resetRotation) v.setRotation(0);
v.setTag(R.id.dynamic_grid_wobble_tag, false);
}
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void restartWobble() {
stopWobble(false);
startWobbleAnimation();
}
public void init(Context context) {
//设置ScrollListener,当滚动后继续判断switch Item
setOnScrollListener(mScrollListener);
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
mSmoothScrollAmountAtEdge = (int) (SMOOTH_SCROLL_AMOUNT_AT_EDGE * metrics.density + 0.5f);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void animateWobble(View v) {
ObjectAnimator animator = createBaseWobble(v);
animator.setFloatValues(-2, 2);
animator.start();
mWobbleAnimators.add(animator);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void animateWobbleInverse(View v) {
ObjectAnimator animator = createBaseWobble(v);
animator.setFloatValues(2, -2);
animator.start();
mWobbleAnimators.add(animator);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private ObjectAnimator createBaseWobble(View v) {
ObjectAnimator animator = new ObjectAnimator();
animator.setDuration(180);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.setRepeatCount(ValueAnimator.INFINITE);
animator.setPropertyName("rotation");
animator.setTarget(v);
return animator;
}
private void reorderElements(int originalPosition, int targetPosition) {
getAdapterInterface().reorderItems(originalPosition, targetPosition);
}
private int getColumnCount() {
return getAdapterInterface().getColumnCount();
}
private AbstractDynamicGridAdapter getAdapterInterface() {
return ((AbstractDynamicGridAdapter) getAdapter());
}
/**
* Creates the hover cell with the appropriate bitmap and of appropriate
* size. The hover cell's BitmapDrawable is drawn on top of the bitmap every
* single time an invalidate call is made.
*/
private BitmapDrawable getAndAddHoverView(View v) {
int w = v.getWidth();
int h = v.getHeight();
int top = v.getTop();
int left = v.getLeft();
Bitmap b = getBitmapFromView(v);
BitmapDrawable drawable = new BitmapDrawable(getResources(), b);
mHoverCellOriginalBounds = new Rect(left, top, left + w, top + h);
mHoverCellCurrentBounds = new Rect(mHoverCellOriginalBounds);
drawable.setBounds(mHoverCellCurrentBounds);
return drawable;
}
/**
* Returns a bitmap showing a screenshot of the view passed in.
*/
private Bitmap getBitmapFromView(View v) {
Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
v.draw(canvas);
return bitmap;
}
private void updateNeighborViewsForId(long itemId) {
int draggedPos = getPositionForID(itemId);
for (int pos = getFirstVisiblePosition(); pos <= getLastVisiblePosition(); pos++) {
if (draggedPos != pos) {
idList.add(getId(pos));
}
}
}
/**
* Retrieves the position in the grid corresponding to <code>itemId code>
*/
public int getPositionForID(long itemId) {
View v = getViewForId(itemId);
if (v == null) {
return -1;
} else {
return getPositionForView(v);
}
}
public View getViewForId(long itemId) {
int firstVisiblePosition = getFirstVisiblePosition();
AbstractDynamicGridAdapter adapter = ((AbstractDynamicGridAdapter) getAdapter());
for (int i = 0; i < getChildCount(); i++) {
View v = getChildAt(i);
int position = firstVisiblePosition + i;
long id = adapter.getItemId(position);
if (id == itemId) {
return v;
}
}
return null;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
/*
* 记下按下的位置和按下的触点Id,若mIsEditMode已开启,则更新选中的
* mMobileItemId和mCellIsMobile,以及邻居Id,这种情况只出现在长按开
* 启拖拽模式后,在结束的时候没有调用StopEditMode()
*/
case MotionEvent.ACTION_DOWN:
mDownX = (int) event.getX();
mDownY = (int) event.getY();
mActivePointerId = event.getPointerId(0);
if (mIsEditMode && isEnabled()) {
layoutChildren();
mTotalOffsetY = 0;
mTotalOffsetX = 0;
int position = pointToPosition(mDownX, mDownY);
int itemNum = position - getFirstVisiblePosition();
View selectedView = getChildAt(itemNum);
if (selectedView == null) {
return false;
} else {
mMobileItemId = getAdapter().getItemId(position);
mHoverCell = getAndAddHoverView(selectedView);
if (isPostHoneycomb())
selectedView.setVisibility(View.INVISIBLE);
mCellIsMobile = true;
updateNeighborViewsForId(mMobileItemId);
}
} else if (!isEnabled()) {
return false;
}
break;
/*
* 核心功能在这里了,
* event.findPointerIndex() 关于多点触摸的知识,将在下面做出讲解
* 更新镜像位置 mHoverCell.setBounds(mHoverCellCurrentBounds); invalidate();
* 判断是否满足交换数据的条件 handleCellSwitch();
* 判断是否Scroll的条件handleMobileCellScroll();
*/
case MotionEvent.ACTION_MOVE:
if (mActivePointerId == INVALID_ID) {
break;
}
int pointerIndex = event.findPointerIndex(mActivePointerId);
mLastEventY = (int) event.getY(pointerIndex);
mLastEventX = (int) event.getX(pointerIndex);
int deltaY = mLastEventY - mDownY;
int deltaX = mLastEventX - mDownX;
if (mCellIsMobile) {
mHoverCellCurrentBounds.offsetTo(mHoverCellOriginalBounds.left + deltaX + mTotalOffsetX,
mHoverCellOriginalBounds.top + deltaY + mTotalOffsetY);
mHoverCell.setBounds(mHoverCellCurrentBounds);
invalidate();
handleCellSwitch();
mIsMobileScrolling = false;
handleMobileCellScroll();
return false;
}
break;
/*
* 下面都是结束拖拽,应对不同的情况
*/
case MotionEvent.ACTION_UP:
touchEventsEnded();
if (mDropListener != null) {
mDropListener.onActionDrop();
}
break;
case MotionEvent.ACTION_CANCEL:
touchEventsCancelled();
if (mDropListener != null) {
mDropListener.onActionDrop();
}
break;
case MotionEvent.ACTION_POINTER_UP:
/* If a multitouch event took place and the original touch dictating
* the movement of the hover cell has ended, then the dragging event
* ends and the hover cell is animated to its corresponding position
* in the listview. */
pointerIndex = (event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
final int pointerId = event.getPointerId(pointerIndex);
if (pointerId == mActivePointerId) {
touchEventsEnded();
}
break;
default:
break;
}
return super.onTouchEvent(event);
}
private void handleMobileCellScroll() {
mIsMobileScrolling = handleMobileCellScroll(mHoverCellCurrentBounds);
}
/*
* computeVerticalScrollOffset计算滑动thumb已滑动的距离
* computeVerticalScrollExtent计算thumb的高度
* computeVerticalScrollRange计算thumb所表示的高度,一般为视图高度
*/
public boolean handleMobileCellScroll(Rect r) {
int offset = computeVerticalScrollOffset();
int height = getHeight();
int extent = computeVerticalScrollExtent();
int range = computeVerticalScrollRange();
int hoverViewTop = r.top;
int hoverHeight = r.height();
if (hoverViewTop <= 0 && offset > 0) {
smoothScrollBy(-mSmoothScrollAmountAtEdge, 0);
return true;
}
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) {
smoothScrollBy(mSmoothScrollAmountAtEdge, 0);
return true;
}
return false;
}
@Override
public void setAdapter(ListAdapter adapter) {
super.setAdapter(adapter);
}
private void touchEventsEnded() {
final View mobileView = getViewForId(mMobileItemId);
if (mCellIsMobile || mIsWaitingForScrollFinish) {
mCellIsMobile = false;
mIsWaitingForScrollFinish = false;
mIsMobileScrolling = false;
mActivePointerId = INVALID_ID;
// If the autoscroller has not completed scrolling, we need to wait for it to
// finish in order to determine the final location of where the hover cell
// should be animated to.
if (mScrollState != OnScrollListener.SCROLL_STATE_IDLE) {
mIsWaitingForScrollFinish = true;
return;
}
mHoverCellCurrentBounds.offsetTo(mobileView.getLeft(), mobileView.getTop());
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) {
animateBounds(mobileView);
} else {
mHoverCell.setBounds(mHoverCellCurrentBounds);
invalidate();
reset(mobileView);
}
} else {
touchEventsCancelled();
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void animateBounds(final View mobileView) {
TypeEvaluator<Rect> sBoundEvaluator = new TypeEvaluator<Rect>() {
public Rect evaluate(float fraction, Rect startValue, Rect endValue) {
return new Rect(interpolate(startValue.left, endValue.left, fraction),
interpolate(startValue.top, endValue.top, fraction),
interpolate(startValue.right, endValue.right, fraction),
interpolate(startValue.bottom, endValue.bottom, fraction));
}
public int interpolate(int start, int end, float fraction) {
return (int) (start + fraction * (end - start));
}
};
ObjectAnimator hoverViewAnimator = ObjectAnimator.ofObject(mHoverCell, "bounds",
sBoundEvaluator, mHoverCellCurrentBounds);
hoverViewAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator valueAnimator) {
invalidate();
}
});
hoverViewAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mHoverAnimation = true;
updateEnableState();
}
@Override
public void onAnimationEnd(Animator animation) {
mHoverAnimation = false;
updateEnableState();
reset(mobileView);
}
});
hoverViewAnimator.start();
}
private void reset(View mobileView) {
idList.clear();
mMobileItemId = INVALID_ID;
mobileView.setVisibility(View.VISIBLE);
mHoverCell = null;
if (!mIsEditMode && isPostHoneycomb() && mWobbleInEditMode)
stopWobble(true);
if (mIsEditMode && isPostHoneycomb() && mWobbleInEditMode)
restartWobble();
invalidate();
}
private void updateEnableState() {
setEnabled(!mHoverAnimation && !mReorderAnimation);
}
/**
* Seems that GridView before HONEYCOMB not support stable id in proper way.
* That cause bugs on view recycle if we will animate or change visibility state for items.
*
* @return
*/
private boolean isPostHoneycomb() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB;
}
private void touchEventsCancelled() {
View mobileView = getViewForId(mMobileItemId);
if (mCellIsMobile) {
reset(mobileView);
}
mCellIsMobile = false;
mIsMobileScrolling = false;
mActivePointerId = INVALID_ID;
}
/*
* 处理数据交换
*/
private void handleCellSwitch() {
final int deltaY = mLastEventY - mDownY;
final int deltaX = mLastEventX - mDownX;
//获取镜像的中心坐标
final int deltaYTotal = mHoverCellOriginalBounds.centerY() + mTotalOffsetY + deltaY;
final int deltaXTotal = mHoverCellOriginalBounds.centerX() + mTotalOffsetX + deltaX;
View mobileView = getViewForId(mMobileItemId);
View targetView = null;
float vX = 0;
float vY = 0;
//以下的工作便是判断是否满足改变数据位置的条件
Point mobileColumnRowPair = getColumnAndRowForView(mobileView);
for (Long id : idList) {
View view = getViewForId(id);
if (view != null) {
Point targetColumnRowPair = getColumnAndRowForView(view);
if ((aboveRight(targetColumnRowPair, mobileColumnRowPair)
&& deltaYTotal < view.getBottom() && deltaXTotal > view.getLeft()
|| aboveLeft(targetColumnRowPair, mobileColumnRowPair)
&& deltaYTotal < view.getBottom() && deltaXTotal < view.getRight()
|| belowRight(targetColumnRowPair, mobileColumnRowPair)
&& deltaYTotal > view.getTop() && deltaXTotal > view.getLeft()
|| belowLeft(targetColumnRowPair, mobileColumnRowPair)
&& deltaYTotal > view.getTop() && deltaXTotal < view.getRight()
|| above(targetColumnRowPair, mobileColumnRowPair) && deltaYTotal < view.getBottom())
|| below(targetColumnRowPair, mobileColumnRowPair) && deltaYTotal > view.getTop()
|| right(targetColumnRowPair, mobileColumnRowPair) && deltaXTotal > view.getLeft()
|| left(targetColumnRowPair, mobileColumnRowPair) && deltaXTotal < view.getRight()) {
float xDiff = Math.abs(DynamicGridUtils.getViewX(view) - DynamicGridUtils.getViewX(mobileView));
float yDiff = Math.abs(DynamicGridUtils.getViewY(view) - DynamicGridUtils.getViewY(mobileView));
if (xDiff >= vX && yDiff >= vY) {
vX = xDiff;
vY = yDiff;
targetView = view;
}
}
}
}
if (targetView != null) {
final int originalPosition = getPositionForView(mobileView);
int targetPosition = getPositionForView(targetView);
if (targetPosition == INVALID_POSITION) {
updateNeighborViewsForId(mMobileItemId);
return;
}
//改变数据位置
reorderElements(originalPosition, targetPosition);
mDownY = mLastEventY;
mDownX = mLastEventX;
mobileView.setVisibility(View.VISIBLE);
if (isPostHoneycomb()) {
targetView.setVisibility(View.INVISIBLE);
}
updateNeighborViewsForId(mMobileItemId);
final ViewTreeObserver observer = getViewTreeObserver();
final int finalTargetPosition = targetPosition;
if (isPostHoneycomb() && observer != null) {
observer.addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
observer.removeOnPreDrawListener(this);
mTotalOffsetY += deltaY;
mTotalOffsetX += deltaX;
animateReorder(originalPosition, finalTargetPosition);
return true;
}
});
} else {
mTotalOffsetY += deltaY;
mTotalOffsetX += deltaX;
}
}
}
private boolean belowLeft(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y > mobileColumnRowPair.y && targetColumnRowPair.x < mobileColumnRowPair.x;
}
private boolean belowRight(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y > mobileColumnRowPair.y && targetColumnRowPair.x > mobileColumnRowPair.x;
}
private boolean aboveLeft(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y < mobileColumnRowPair.y && targetColumnRowPair.x < mobileColumnRowPair.x;
}
private boolean aboveRight(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y < mobileColumnRowPair.y && targetColumnRowPair.x > mobileColumnRowPair.x;
}
private boolean above(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y < mobileColumnRowPair.y && targetColumnRowPair.x == mobileColumnRowPair.x;
}
private boolean below(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y > mobileColumnRowPair.y && targetColumnRowPair.x == mobileColumnRowPair.x;
}
private boolean right(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y == mobileColumnRowPair.y && targetColumnRowPair.x > mobileColumnRowPair.x;
}
private boolean left(Point targetColumnRowPair, Point mobileColumnRowPair) {
return targetColumnRowPair.y == mobileColumnRowPair.y && targetColumnRowPair.x < mobileColumnRowPair.x;
}
private Point getColumnAndRowForView(View view) {
int pos = getPositionForView(view);
int columns = getColumnCount();
int column = pos % columns;
int row = pos / columns;
return new Point(column, row);
}
private long getId(int position) {
return getAdapter().getItemId(position);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void animateReorder(final int oldPosition, final int newPosition) {
boolean isForward = newPosition > oldPosition;
List<Animator> resultList = new LinkedList<Animator>();
if (isForward) {
for (int pos = Math.min(oldPosition, newPosition); pos < Math.max(oldPosition, newPosition); pos++) {
View view = getViewForId(getId(pos));
if ((pos + 1) % getColumnCount() == 0) {
resultList.add(createTranslationAnimations(view, -view.getWidth() * (getColumnCount() - 1), 0, view.getHeight(), 0));
} else {
resultList.add(createTranslationAnimations(view, view.getWidth(), 0, 0, 0));
}
}
} else {
for (int pos = Math.max(oldPosition, newPosition); pos > Math.min(oldPosition, newPosition); pos--) {
View view = getViewForId(getId(pos));
if ((pos + getColumnCount()) % getColumnCount() == 0) {
resultList.add(createTranslationAnimations(view, view.getWidth() * (getColumnCount() - 1), 0, -view.getHeight(), 0));
} else {
resultList.add(createTranslationAnimations(view, -view.getWidth(), 0, 0, 0));
}
}
}
AnimatorSet resultSet = new AnimatorSet();
resultSet.playTogether(resultList);
resultSet.setDuration(MOVE_DURATION);
resultSet.setInterpolator(new AccelerateDecelerateInterpolator());
resultSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
mReorderAnimation = true;
updateEnableState();
}
@Override
public void onAnimationEnd(Animator animation) {
mReorderAnimation = false;
updateEnableState();
}
});
resultSet.start();
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
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;
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mHoverCell != null) {
mHoverCell.draw(canvas);
}
}
/**
* Interface provide callback for end of drag'n'drop event
*/
public interface OnDropListener {
/**
* called when view been dropped
*/
void onActionDrop();
}
/**
* This scroll listener is added to the gridview in order to handle cell swapping
* when the cell is either at the top or bottom edge of the gridview. If the hover
* cell is at either edge of the gridview, the gridview will begin scrolling. As
* scrolling takes place, the gridview continuously checks if new cells became visible
* and determines whether they are potential candidates for a cell swap.
*/
private OnScrollListener mScrollListener = new OnScrollListener() {
private int mPreviousFirstVisibleItem = -1;
private int mPreviousVisibleItemCount = -1;
private int mCurrentFirstVisibleItem;
private int mCurrentVisibleItemCount;
private int mCurrentScrollState;
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
int totalItemCount) {
mCurrentFirstVisibleItem = firstVisibleItem;
mCurrentVisibleItemCount = visibleItemCount;
mPreviousFirstVisibleItem = (mPreviousFirstVisibleItem == -1) ? mCurrentFirstVisibleItem
: mPreviousFirstVisibleItem;
mPreviousVisibleItemCount = (mPreviousVisibleItemCount == -1) ? mCurrentVisibleItemCount
: mPreviousVisibleItemCount;
checkAndHandleFirstVisibleCellChange();
checkAndHandleLastVisibleCellChange();
mPreviousFirstVisibleItem = mCurrentFirstVisibleItem;
mPreviousVisibleItemCount = mCurrentVisibleItemCount;
if (isPostHoneycomb() && mWobbleInEditMode) {
updateWobbleState(visibleItemCount);
}
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void updateWobbleState(int visibleItemCount) {
for (int i = 0; i < visibleItemCount; i++) {
View child = getChildAt(i);
if (child != null) {
if (mMobileItemId != INVALID_ID && Boolean.TRUE != child.getTag(R.id.dynamic_grid_wobble_tag)) {
if (i % 2 == 0)
animateWobble(child);
else
animateWobbleInverse(child);
child.setTag(R.id.dynamic_grid_wobble_tag, true);
} else if (mMobileItemId == INVALID_ID && child.getRotation() != 0) {
child.setRotation(0);
child.setTag(R.id.dynamic_grid_wobble_tag, false);
}
}
}
}
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
mCurrentScrollState = scrollState;
mScrollState = scrollState;
isScrollCompleted();
}
/**
* This method is in charge of invoking 1 of 2 actions. Firstly, if the gridview
* is in a state of scrolling invoked by the hover cell being outside the bounds
* of the gridview, then this scrolling event is continued. Secondly, if the hover
* cell has already been released, this invokes the animation for the hover cell
* to return to its correct position after the gridview has entered an idle scroll
* state.
*/
private void isScrollCompleted() {
if (mCurrentVisibleItemCount > 0 && mCurrentScrollState == SCROLL_STATE_IDLE) {
if (mCellIsMobile && mIsMobileScrolling) {
handleMobileCellScroll();
} else if (mIsWaitingForScrollFinish) {
touchEventsEnded();
}
}
}
/**
* Determines if the gridview scrolled up enough to reveal a new cell at the
* top of the list. If so, then the appropriate parameters are updated.
*/