Created
December 21, 2018 06:51
-
-
Save iamutkarshtiwari/4b130d0d26a77896b8dfc8cadcee3fc2 to your computer and use it in GitHub Desktop.
DynamicGridView.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.project.ui.dynamicgridview; | |
import android.animation.Animator; | |
import android.animation.AnimatorListenerAdapter; | |
import android.animation.AnimatorSet; | |
import android.animation.ObjectAnimator; | |
import android.animation.TypeEvaluator; | |
import android.annotation.TargetApi; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.graphics.BlurMaskFilter; | |
import android.graphics.Canvas; | |
import android.graphics.Paint; | |
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.util.Pair; | |
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 com.project.R; | |
import com.project.util.DeviceUtils; | |
import java.util.ArrayList; | |
import java.util.Collections; | |
import java.util.LinkedList; | |
import java.util.List; | |
import java.util.Stack; | |
/** | |
* Author: alex askerov | |
* Date: 9/6/13 | |
* Time: 12:31 PM | |
*/ | |
public class DynamicGridView extends GridView { | |
private static final int INVALID_ID = -1; | |
private static final int MOVE_DURATION = 300; | |
private static final int SMOOTH_SCROLL_AMOUNT_AT_EDGE = 8; | |
private BitmapDrawable hoverCell; | |
private Rect hoverCellCurrentBounds; | |
private Rect hoverCellOriginalBounds; | |
private int totalOffsetY = 0; | |
private int totalOffsetX = 0; | |
private int downX = -1; | |
private int downY = -1; | |
private int lastEventY = -1; | |
private int lastEventX = -1; | |
//used to distinguish straight line and diagonal switching | |
private int overlapIfSwitchStraightLine; | |
private List<Long> idList = new ArrayList<>(); | |
private long mobileItemId = INVALID_ID; | |
private boolean cellIsMobile = false; | |
private int activePointerId = INVALID_ID; | |
private boolean isMobileScrolling; | |
private int smoothScrollAmountAtEdge = 0; | |
private boolean isWaitingForScrollFinish = false; | |
private int scrollState = OnScrollListener.SCROLL_STATE_IDLE; | |
private boolean isEditMode = false; | |
private boolean hoverAnimation; | |
private boolean reorderAnimation; | |
private boolean isEditModeEnabled = true; | |
private OnScrollListener userScrollListener; | |
private OnDropListener dropListener; | |
private OnDragListener dragListener; | |
private OnEditModeChangeListener editModeChangeListener; | |
private OnSelectedItemDraggableListener selectedItemDraggableListener; | |
private OnItemClickListener userItemClickListener; | |
private OnItemClickListener localItemClickListener = new OnItemClickListener() { | |
@Override | |
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | |
if (!isEditMode() && isEnabled() && userItemClickListener != null) { | |
userItemClickListener.onItemClick(parent, view, position, id); | |
} | |
} | |
}; | |
private boolean undoSupportEnabled; | |
private Stack<DynamicGridModification> modificationStack; | |
private DynamicGridModification currentModification; | |
private OnSelectedItemBitmapCreationListener selectedItemBitmapCreationListener; | |
private View mobileView; | |
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); | |
} | |
@Override | |
public void setOnScrollListener(OnScrollListener scrollListener) { | |
this.userScrollListener = scrollListener; | |
} | |
public void setOnDropListener(OnDropListener dropListener) { | |
this.dropListener = dropListener; | |
} | |
public void setOnDragListener(OnDragListener dragListener) { | |
this.dragListener = dragListener; | |
} | |
public void setOnSelectedItemDraggableListener(OnSelectedItemDraggableListener listener){ | |
this.selectedItemDraggableListener = listener; | |
} | |
/** | |
* Start edit mode without starting drag; | |
*/ | |
public void startEditMode() { | |
startEditMode(-1); | |
} | |
/** | |
* Start edit mode with position. Useful for start edit mode in | |
* {@link android.widget.AdapterView.OnItemClickListener} | |
* or {@link android.widget.AdapterView.OnItemLongClickListener} | |
*/ | |
public void startEditMode(int position) { | |
if (!isEditModeEnabled) | |
return; | |
requestDisallowInterceptTouchEvent(true); | |
if (position != -1) { | |
startDragAtPosition(position); | |
} | |
isEditMode = true; | |
if (editModeChangeListener != null) | |
editModeChangeListener.onEditModeChanged(true); | |
} | |
public void stopEditMode() { | |
isEditMode = false; | |
requestDisallowInterceptTouchEvent(false); | |
if (editModeChangeListener != null) | |
editModeChangeListener.onEditModeChanged(false); | |
} | |
public boolean isEditModeEnabled() { | |
return isEditModeEnabled; | |
} | |
public void setEditModeEnabled(boolean enabled) { | |
this.isEditModeEnabled = enabled; | |
} | |
public void setOnEditModeChangeListener(OnEditModeChangeListener editModeChangeListener) { | |
this.editModeChangeListener = editModeChangeListener; | |
} | |
public boolean isEditMode() { | |
return isEditMode; | |
} | |
@Override | |
public void setOnItemClickListener(OnItemClickListener listener) { | |
this.userItemClickListener = listener; | |
super.setOnItemClickListener(localItemClickListener); | |
} | |
public boolean isUndoSupportEnabled() { | |
return undoSupportEnabled; | |
} | |
public void setUndoSupportEnabled(boolean undoSupportEnabled) { | |
if (this.undoSupportEnabled != undoSupportEnabled) { | |
if (undoSupportEnabled) { | |
this.modificationStack = new Stack<>(); | |
} else { | |
this.modificationStack = null; | |
} | |
} | |
this.undoSupportEnabled = undoSupportEnabled; | |
} | |
public void undoLastModification() { | |
if (undoSupportEnabled) { | |
if (modificationStack != null && !modificationStack.isEmpty()) { | |
DynamicGridModification modification = modificationStack.pop(); | |
undoModification(modification); | |
} | |
} | |
} | |
public void undoAllModifications() { | |
if (undoSupportEnabled) { | |
if (modificationStack != null && !modificationStack.isEmpty()) { | |
while (!modificationStack.isEmpty()) { | |
DynamicGridModification modification = modificationStack.pop(); | |
undoModification(modification); | |
} | |
} | |
} | |
} | |
public boolean hasModificationHistory() { | |
if (undoSupportEnabled) { | |
if (modificationStack != null && !modificationStack.isEmpty()) { | |
return true; | |
} | |
} | |
return false; | |
} | |
public void clearModificationHistory() { | |
modificationStack.clear(); | |
} | |
public void setOnSelectedItemBitmapCreationListener(OnSelectedItemBitmapCreationListener selectedItemBitmapCreationListener) { | |
this.selectedItemBitmapCreationListener = selectedItemBitmapCreationListener; | |
} | |
private void undoModification(DynamicGridModification modification) { | |
for (Pair<Integer, Integer> transition : modification.getTransitions()) { | |
reorderElements(transition.second, transition.first); | |
} | |
} | |
public void init(Context context) { | |
super.setOnScrollListener(scrollListener); | |
DisplayMetrics metrics = context.getResources().getDisplayMetrics(); | |
smoothScrollAmountAtEdge = (int) (SMOOTH_SCROLL_AMOUNT_AT_EDGE * metrics.density + 0.5f); | |
overlapIfSwitchStraightLine = getResources().getDimensionPixelSize(R.dimen.overlap_if_switch_straight_line); | |
} | |
private void reorderElements(int originalPosition, int targetPosition) { | |
if (dragListener != null) | |
dragListener.onDragPositionsChanged(originalPosition, targetPosition); | |
getAdapterInterface().reorderItems(originalPosition, targetPosition); | |
} | |
private int getColumnCount() { | |
return getAdapterInterface().getColumnCount(); | |
} | |
private DynamicGridAdapterInterface getAdapterInterface() { | |
return ((DynamicGridAdapterInterface) 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); | |
Bitmap b = createShadowBitmap(v); | |
BitmapDrawable drawable = new BitmapDrawable(getResources(), b); | |
hoverCellOriginalBounds = new Rect(left, top, left + w, top + h); | |
hoverCellCurrentBounds = new Rect(hoverCellOriginalBounds); | |
drawable.setBounds(hoverCellCurrentBounds); | |
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; | |
} | |
/** | |
* Returns a bitmap showing a screenshot with shadow of the view passed in. | |
*/ | |
private Bitmap createShadowBitmap(View v) { | |
Bitmap bitmap = Bitmap.createBitmap(v.getWidth(), v.getHeight(), Bitmap.Config.ARGB_8888); | |
Canvas canvas = new Canvas(bitmap); | |
v.draw(canvas); | |
BlurMaskFilter blurFilter = new BlurMaskFilter(7, BlurMaskFilter.Blur.NORMAL); | |
Paint shadowPaint = new Paint(); | |
shadowPaint.setMaskFilter(blurFilter); | |
int[] offsetXY = new int[2]; | |
Bitmap shadowImage = bitmap.extractAlpha(shadowPaint, offsetXY); | |
/* Need to convert shadowImage from 8-bit to ARGB here. */ | |
Bitmap shadowImage32 = shadowImage.copy(Bitmap.Config.ARGB_8888, true); | |
// http://stackoverflow.com/questions/21837671/new-warning-in-android-4-4 | |
// Fix the non pre-multiplied exception for API 19+. | |
// ギャラリーの画像はアルファチャンネル(プリマルチプライド)を設定されていない場合がある。 | |
// そのような画像をv19以降でキャンバスに設定するとエラーが発生する | |
// そのため v19以降では明示的にsetPremultipliedを呼んであげる必要がある | |
if ( android.os.Build.VERSION.SDK_INT >= 19 && !shadowImage32.isPremultiplied() ) | |
{ | |
shadowImage32.setPremultiplied( true ); | |
} | |
Canvas c = new Canvas(shadowImage32); | |
c.drawBitmap(bitmap, -offsetXY[0]-3, -offsetXY[1]-3, null); | |
return shadowImage32; | |
} | |
private void updateNeighborViewsForId(long itemId) { | |
idList.clear(); | |
int draggedPos = getPositionForID(itemId); | |
for (int pos = getFirstVisiblePosition(); pos <= getLastVisiblePosition(); pos++) { | |
if (draggedPos != pos && getAdapterInterface().canReorder(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(); | |
ListAdapter adapter = 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) { | |
case MotionEvent.ACTION_DOWN: | |
downX = (int) event.getX(); | |
downY = (int) event.getY(); | |
activePointerId = event.getPointerId(0); | |
if (isEditMode && isEnabled()) { | |
layoutChildren(); | |
int position = pointToPosition(downX, downY); | |
startDragAtPosition(position); | |
} else if (!isEnabled()) { | |
return false; | |
} | |
break; | |
case MotionEvent.ACTION_MOVE: | |
if (activePointerId == INVALID_ID) { | |
break; | |
} | |
int pointerIndex = event.findPointerIndex(activePointerId); | |
lastEventY = (int) event.getY(pointerIndex); | |
lastEventX = (int) event.getX(pointerIndex); | |
int deltaY = lastEventY - downY; | |
int deltaX = lastEventX - downX; | |
if (cellIsMobile) { | |
hoverCellCurrentBounds.offsetTo(hoverCellOriginalBounds.left + deltaX + totalOffsetX, | |
hoverCellOriginalBounds.top + deltaY + totalOffsetY); | |
hoverCell.setBounds(hoverCellCurrentBounds); | |
invalidate(); | |
handleCellSwitch(); | |
isMobileScrolling = false; | |
handleMobileCellScroll(); | |
return false; | |
} | |
break; | |
case MotionEvent.ACTION_UP: | |
touchEventsEnded(); | |
if (undoSupportEnabled) { | |
if (currentModification != null && !currentModification.getTransitions().isEmpty()) { | |
modificationStack.push(currentModification); | |
currentModification = new DynamicGridModification(); | |
} | |
} | |
if (hoverCell != null) { | |
if (dropListener != null) { | |
dropListener.onActionDrop(); | |
} | |
} | |
break; | |
case MotionEvent.ACTION_CANCEL: | |
touchEventsCancelled(); | |
if (hoverCell != null) { | |
if (dropListener != null) { | |
dropListener.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 == activePointerId) { | |
touchEventsEnded(); | |
} | |
break; | |
default: | |
break; | |
} | |
return super.onTouchEvent(event); | |
} | |
private void startDragAtPosition(int position) { | |
totalOffsetY = 0; | |
totalOffsetX = 0; | |
int itemNum = position - getFirstVisiblePosition(); | |
View selectedView = getChildAt(itemNum); | |
if (selectedView != null) { | |
mobileItemId = getAdapter().getItemId(position); | |
if (selectedItemBitmapCreationListener != null) | |
selectedItemBitmapCreationListener.onPreSelectedItemBitmapCreation(selectedView, position, mobileItemId); | |
hoverCell = getAndAddHoverView(selectedView); | |
if (selectedItemBitmapCreationListener != null) | |
selectedItemBitmapCreationListener.onPostSelectedItemBitmapCreation(selectedView, position, mobileItemId); | |
selectedView.setVisibility(View.INVISIBLE); | |
cellIsMobile = true; | |
updateNeighborViewsForId(mobileItemId); | |
if (dragListener != null) { | |
dragListener.onDragStarted(position); | |
} | |
} | |
} | |
private void handleMobileCellScroll() { | |
isMobileScrolling = handleMobileCellVerticalScroll(hoverCellCurrentBounds); | |
isMobileScrolling = handleMobileCellHorizontalScroll(hoverCellCurrentBounds); | |
} | |
public boolean handleMobileCellVerticalScroll(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(-smoothScrollAmountAtEdge, 0); | |
return true; | |
} | |
if (hoverViewTop + hoverHeight >= height && (offset + extent) < range) { | |
smoothScrollBy(smoothScrollAmountAtEdge, 0); | |
return true; | |
} | |
return false; | |
} | |
public boolean handleMobileCellHorizontalScroll(Rect r) { | |
int offset = computeHorizontalScrollOffset(); | |
int width = getWidth(); | |
int extent = computeHorizontalScrollExtent(); | |
int range = computeHorizontalScrollRange(); | |
int hoverViewLeft = r.left; | |
int hoverWidth = r.width(); | |
if (hoverViewLeft <= 0 && offset > 0) { | |
smoothScrollBy(-smoothScrollAmountAtEdge, 0); | |
return true; | |
} | |
if (hoverViewLeft + hoverWidth >= width && (offset + extent) < range) { | |
smoothScrollBy(smoothScrollAmountAtEdge, 0); | |
return true; | |
} | |
return false; | |
} | |
@Override | |
public void setAdapter(ListAdapter adapter) { | |
super.setAdapter(adapter); | |
} | |
private void touchEventsEnded() { | |
final View mobileView = getViewForId(mobileItemId); | |
if (mobileView != null && (cellIsMobile || isWaitingForScrollFinish)) { | |
cellIsMobile = false; | |
isWaitingForScrollFinish = false; | |
isMobileScrolling = false; | |
activePointerId = 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 (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { | |
isWaitingForScrollFinish = true; | |
return; | |
} | |
hoverCellCurrentBounds.offsetTo(mobileView.getLeft(), mobileView.getTop()); | |
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB) { | |
animateBounds(mobileView); | |
} else { | |
hoverCell.setBounds(hoverCellCurrentBounds); | |
invalidate(); | |
reset(mobileView); | |
} | |
} else { | |
touchEventsCancelled(); | |
} | |
} | |
@TargetApi(Build.VERSION_CODES.HONEYCOMB) | |
private void animateBounds(final View mobileView) { | |
TypeEvaluator<Rect> boundEvaluator = 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(hoverCell, "bounds", | |
boundEvaluator, hoverCellCurrentBounds); | |
hoverViewAnimator.addUpdateListener(valueAnimator -> invalidate()); | |
hoverViewAnimator.addListener(new AnimatorListenerAdapter() { | |
@Override | |
public void onAnimationStart(Animator animation) { | |
hoverAnimation = true; | |
updateEnableState(); | |
} | |
@Override | |
public void onAnimationEnd(Animator animation) { | |
hoverAnimation = false; | |
updateEnableState(); | |
reset(mobileView); | |
} | |
}); | |
hoverViewAnimator.start(); | |
} | |
private void reset(View mobileView) { | |
idList.clear(); | |
mobileItemId = INVALID_ID; | |
mobileView.setVisibility(View.VISIBLE); | |
hoverCell = null; | |
//ugly fix for unclear disappearing items after reorder | |
for (int i = 0; i < getLastVisiblePosition() - getFirstVisiblePosition(); i++) { | |
View child = getChildAt(i); | |
if (child != null) { | |
child.setVisibility(View.VISIBLE); | |
} | |
} | |
invalidate(); | |
} | |
private void updateEnableState() { | |
setEnabled(!hoverAnimation && !reorderAnimation); | |
} | |
private void touchEventsCancelled() { | |
View mobileView = getViewForId(mobileItemId); | |
if (cellIsMobile) { | |
reset(mobileView); | |
} | |
cellIsMobile = false; | |
isMobileScrolling = false; | |
activePointerId = INVALID_ID; | |
} | |
private void handleCellSwitch() { | |
final int deltaY = lastEventY - downY; | |
final int deltaX = lastEventX - downX; | |
final int deltaYTotal = hoverCellOriginalBounds.centerY() + totalOffsetY + deltaY; | |
final int deltaXTotal = hoverCellOriginalBounds.centerX() + totalOffsetX + deltaX; | |
mobileView = getViewForId(mobileItemId); | |
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() - overlapIfSwitchStraightLine | |
|| below(targetColumnRowPair, mobileColumnRowPair) | |
&& deltaYTotal > view.getTop() + overlapIfSwitchStraightLine | |
|| right(targetColumnRowPair, mobileColumnRowPair) | |
&& deltaXTotal > view.getLeft() + overlapIfSwitchStraightLine | |
|| left(targetColumnRowPair, mobileColumnRowPair) | |
&& deltaXTotal < view.getRight() - overlapIfSwitchStraightLine)) { | |
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 is not draggable then do nothing | |
if(!selectedItemDraggableListener.isDraggable(targetPosition)) return; | |
final DynamicGridAdapterInterface adapter = getAdapterInterface(); | |
if (targetPosition == INVALID_POSITION || !adapter.canReorder(originalPosition) || !adapter.canReorder(targetPosition)) { | |
updateNeighborViewsForId(mobileItemId); | |
return; | |
} | |
reorderElements(originalPosition, targetPosition); | |
if (undoSupportEnabled) { | |
currentModification.addTransition(originalPosition, targetPosition); | |
} | |
downY = lastEventY; | |
downX = lastEventX; | |
SwitchCellAnimator switchCellAnimator; | |
if (!DeviceUtils.isAtLeastLollipop()) //below Android L | |
switchCellAnimator = new KitKatSwitchCellAnimator(deltaX, deltaY); | |
else //Android L | |
switchCellAnimator = new LSwitchCellAnimator(deltaX, deltaY); | |
updateNeighborViewsForId(mobileItemId); | |
switchCellAnimator.animateSwitchCell(originalPosition, targetPosition); | |
} | |
} | |
private interface SwitchCellAnimator { | |
void animateSwitchCell(final int originalPosition, final int targetPosition); | |
} | |
/** | |
* for versions KitKat and below. | |
*/ | |
private class KitKatSwitchCellAnimator implements SwitchCellAnimator { | |
private int deltaY; | |
private int deltaX; | |
public KitKatSwitchCellAnimator(int deltaX, int deltaY) { | |
this.deltaX = deltaX; | |
this.deltaY = deltaY; | |
} | |
@Override | |
public void animateSwitchCell(final int originalPosition, final int targetPosition) { | |
assert mobileView != null; | |
getViewTreeObserver().addOnPreDrawListener(new AnimateSwitchViewOnPreDrawListener(mobileView, originalPosition, targetPosition)); | |
mobileView = getViewForId(mobileItemId); | |
} | |
private class AnimateSwitchViewOnPreDrawListener implements ViewTreeObserver.OnPreDrawListener { | |
private final View previousMobileView; | |
private final int originalPosition; | |
private final int targetPosition; | |
AnimateSwitchViewOnPreDrawListener(final View previousMobileView, final int originalPosition, final int targetPosition) { | |
this.previousMobileView = previousMobileView; | |
this.originalPosition = originalPosition; | |
this.targetPosition = targetPosition; | |
} | |
@Override | |
public boolean onPreDraw() { | |
getViewTreeObserver().removeOnPreDrawListener(this); | |
totalOffsetY += deltaY; | |
totalOffsetX += deltaX; | |
animateReorder(originalPosition, targetPosition); | |
previousMobileView.setVisibility(View.VISIBLE); | |
if (mobileView != null) { | |
mobileView.setVisibility(View.INVISIBLE); | |
} | |
return true; | |
} | |
} | |
} | |
/** | |
* for versions L and above. | |
*/ | |
private class LSwitchCellAnimator implements SwitchCellAnimator { | |
private int deltaY; | |
private int deltaX; | |
public LSwitchCellAnimator(int deltaX, int deltaY) { | |
this.deltaX = deltaX; | |
this.deltaY = deltaY; | |
} | |
@Override | |
public void animateSwitchCell(final int originalPosition, final int targetPosition) { | |
getViewTreeObserver().addOnPreDrawListener(new AnimateSwitchViewOnPreDrawListener(originalPosition, targetPosition)); | |
} | |
private class AnimateSwitchViewOnPreDrawListener implements ViewTreeObserver.OnPreDrawListener { | |
private final int originalPosition; | |
private final int targetPosition; | |
AnimateSwitchViewOnPreDrawListener(final int originalPosition, final int targetPosition) { | |
this.originalPosition = originalPosition; | |
this.targetPosition = targetPosition; | |
} | |
@Override | |
public boolean onPreDraw() { | |
getViewTreeObserver().removeOnPreDrawListener(this); | |
totalOffsetY += deltaY; | |
totalOffsetX += deltaX; | |
animateReorder(originalPosition, targetPosition); | |
assert mobileView != null; | |
mobileView.setVisibility(View.VISIBLE); | |
mobileView = getViewForId(mobileItemId); | |
assert mobileView != null; | |
mobileView.setVisibility(View.INVISIBLE); | |
return true; | |
} | |
} | |
} | |
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<>(); | |
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) { | |
reorderAnimation = true; | |
updateEnableState(); | |
} | |
@Override | |
public void onAnimationEnd(Animator animation) { | |
reorderAnimation = 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 (hoverCell != null) { | |
hoverCell.draw(canvas); | |
} | |
} | |
public interface OnDropListener { | |
void onActionDrop(); | |
} | |
public interface OnDragListener { | |
public void onDragStarted(int position); | |
public void onDragPositionsChanged(int oldPosition, int newPosition); | |
} | |
public interface OnEditModeChangeListener { | |
public void onEditModeChanged(boolean inEditMode); | |
} | |
public interface OnSelectedItemBitmapCreationListener { | |
public void onPreSelectedItemBitmapCreation(View selectedView, int position, long itemId); | |
public void onPostSelectedItemBitmapCreation(View selectedView, int position, long itemId); | |
} | |
public interface OnSelectedItemDraggableListener { | |
boolean isDraggable(int position); | |
} | |
/** | |
* 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 scrollListener = new OnScrollListener() { | |
private int previousFirstVisibleItem = -1; | |
private int previousVisibleItemCount = -1; | |
private int currentFirstVisibleItem; | |
private int currentVisibleItemCount; | |
private int currentScrollState; | |
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, | |
int totalItemCount) { | |
currentFirstVisibleItem = firstVisibleItem; | |
currentVisibleItemCount = visibleItemCount; | |
previousFirstVisibleItem = (previousFirstVisibleItem == -1) ? currentFirstVisibleItem | |
: previousFirstVisibleItem; | |
previousVisibleItemCount = (previousVisibleItemCount == -1) ? currentVisibleItemCount | |
: previousVisibleItemCount; | |
checkAndHandleFirstVisibleCellChange(); | |
checkAndHandleLastVisibleCellChange(); | |
previousFirstVisibleItem = currentFirstVisibleItem; | |
previousVisibleItemCount = currentVisibleItemCount; | |
if (userScrollListener != null) { | |
userScrollListener.onScroll(view, firstVisibleItem, visibleItemCount, totalItemCount); | |
} | |
} | |
@Override | |
public void onScrollStateChanged(AbsListView view, int scrollState) { | |
currentScrollState = scrollState; | |
DynamicGridView.this.scrollState = scrollState; | |
isScrollCompleted(); | |
if (userScrollListener != null) { | |
userScrollListener.onScrollStateChanged(view, scrollState); | |
} | |
} | |
/** | |
* 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 (currentVisibleItemCount > 0 && currentScrollState == SCROLL_STATE_IDLE) { | |
if (cellIsMobile && isMobileScrolling) { | |
handleMobileCellScroll(); | |
} else if (isWaitingForScrollFinish) { | |
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. | |
*/ | |
public void checkAndHandleFirstVisibleCellChange() { | |
if (currentFirstVisibleItem != previousFirstVisibleItem) { | |
if (cellIsMobile && mobileItemId != INVALID_ID) { | |
updateNeighborViewsForId(mobileItemId); | |
handleCellSwitch(); | |
} | |
} | |
} | |
/** | |
* Determines if the gridview scrolled down enough to reveal a new cell at the | |
* bottom of the list. If so, then the appropriate parameters are updated. | |
*/ | |
public void checkAndHandleLastVisibleCellChange() { | |
int currentLastVisibleItem = currentFirstVisibleItem + currentVisibleItemCount; | |
int previousLastVisibleItem = previousFirstVisibleItem + previousVisibleItemCount; | |
if (currentLastVisibleItem != previousLastVisibleItem) { | |
if (cellIsMobile && mobileItemId != INVALID_ID) { | |
updateNeighborViewsForId(mobileItemId); | |
handleCellSwitch(); | |
} | |
} | |
} | |
}; | |
private static class DynamicGridModification { | |
private List<Pair<Integer, Integer>> transitions; | |
DynamicGridModification() { | |
super(); | |
this.transitions = new Stack<>(); | |
} | |
public boolean hasTransitions() { | |
return !transitions.isEmpty(); | |
} | |
public void addTransition(int oldPosition, int newPosition) { | |
transitions.add(new Pair<>(oldPosition, newPosition)); | |
} | |
public List<Pair<Integer, Integer>> getTransitions() { | |
Collections.reverse(transitions); | |
return transitions; | |
} | |
} | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment