mirror of https://github.com/kurisufriend/Clover
Replace my homemade swipe-to-dismiss and drag implementation SwipeListener with the RecyclerView native ItemTouchHelper. Also add an undo snackbar when removing boards.multisite
parent
48d8f94e94
commit
3ac7a1a348
@ -1,673 +0,0 @@ |
||||
/* |
||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||
* Copyright (C) 2014 Floens |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package org.floens.chan.ui.helper; |
||||
|
||||
import android.support.v4.view.ViewCompat; |
||||
import android.support.v4.view.ViewPropertyAnimatorCompat; |
||||
import android.support.v4.view.ViewPropertyAnimatorListener; |
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.view.View; |
||||
import android.view.animation.AccelerateDecelerateInterpolator; |
||||
import android.view.animation.LinearInterpolator; |
||||
|
||||
import java.util.ArrayList; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
|
||||
/** |
||||
* This is an adaption of the {@link android.support.v7.widget.DefaultItemAnimator} but to animate |
||||
* swipes to the left or right. |
||||
*/ |
||||
public class SwipeItemAnimator extends RecyclerView.ItemAnimator { |
||||
private static final boolean DEBUG = true; |
||||
|
||||
private ArrayList<RecyclerView.ViewHolder> mPendingRemovals = new ArrayList<RecyclerView.ViewHolder>(); |
||||
private ArrayList<RecyclerView.ViewHolder> mPendingAdditions = new ArrayList<RecyclerView.ViewHolder>(); |
||||
private ArrayList<MoveInfo> mPendingMoves = new ArrayList<MoveInfo>(); |
||||
private ArrayList<ChangeInfo> mPendingChanges = new ArrayList<ChangeInfo>(); |
||||
|
||||
private ArrayList<ArrayList<RecyclerView.ViewHolder>> mAdditionsList = |
||||
new ArrayList<ArrayList<RecyclerView.ViewHolder>>(); |
||||
private ArrayList<ArrayList<MoveInfo>> mMovesList = new ArrayList<ArrayList<MoveInfo>>(); |
||||
private ArrayList<ArrayList<ChangeInfo>> mChangesList = new ArrayList<ArrayList<ChangeInfo>>(); |
||||
|
||||
private ArrayList<RecyclerView.ViewHolder> mAddAnimations = new ArrayList<RecyclerView.ViewHolder>(); |
||||
private ArrayList<RecyclerView.ViewHolder> mMoveAnimations = new ArrayList<RecyclerView.ViewHolder>(); |
||||
private ArrayList<RecyclerView.ViewHolder> mRemoveAnimations = new ArrayList<RecyclerView.ViewHolder>(); |
||||
private ArrayList<RecyclerView.ViewHolder> mChangeAnimations = new ArrayList<RecyclerView.ViewHolder>(); |
||||
|
||||
private static class MoveInfo { |
||||
public RecyclerView.ViewHolder holder; |
||||
public int fromX, fromY, toX, toY; |
||||
|
||||
private MoveInfo(RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { |
||||
this.holder = holder; |
||||
this.fromX = fromX; |
||||
this.fromY = fromY; |
||||
this.toX = toX; |
||||
this.toY = toY; |
||||
} |
||||
} |
||||
|
||||
private static class ChangeInfo { |
||||
public RecyclerView.ViewHolder oldHolder, newHolder; |
||||
public int fromX, fromY, toX, toY; |
||||
|
||||
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder) { |
||||
this.oldHolder = oldHolder; |
||||
this.newHolder = newHolder; |
||||
} |
||||
|
||||
private ChangeInfo(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, |
||||
int fromX, int fromY, int toX, int toY) { |
||||
this(oldHolder, newHolder); |
||||
this.fromX = fromX; |
||||
this.fromY = fromY; |
||||
this.toX = toX; |
||||
this.toY = toY; |
||||
} |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return "ChangeInfo{" + |
||||
"oldHolder=" + oldHolder + |
||||
", newHolder=" + newHolder + |
||||
", fromX=" + fromX + |
||||
", fromY=" + fromY + |
||||
", toX=" + toX + |
||||
", toY=" + toY + |
||||
'}'; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void runPendingAnimations() { |
||||
boolean removalsPending = !mPendingRemovals.isEmpty(); |
||||
boolean movesPending = !mPendingMoves.isEmpty(); |
||||
boolean changesPending = !mPendingChanges.isEmpty(); |
||||
boolean additionsPending = !mPendingAdditions.isEmpty(); |
||||
if (!removalsPending && !movesPending && !additionsPending && !changesPending) { |
||||
// nothing to animate
|
||||
return; |
||||
} |
||||
// First, remove stuff
|
||||
for (RecyclerView.ViewHolder holder : mPendingRemovals) { |
||||
animateRemoveImpl(holder); |
||||
} |
||||
mPendingRemovals.clear(); |
||||
// Next, move stuff
|
||||
if (movesPending) { |
||||
final ArrayList<MoveInfo> moves = new ArrayList<MoveInfo>(); |
||||
moves.addAll(mPendingMoves); |
||||
mMovesList.add(moves); |
||||
mPendingMoves.clear(); |
||||
Runnable mover = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
for (MoveInfo moveInfo : moves) { |
||||
animateMoveImpl(moveInfo.holder, moveInfo.fromX, moveInfo.fromY, |
||||
moveInfo.toX, moveInfo.toY); |
||||
} |
||||
moves.clear(); |
||||
mMovesList.remove(moves); |
||||
} |
||||
}; |
||||
if (removalsPending) { |
||||
View view = moves.get(0).holder.itemView; |
||||
ViewCompat.postOnAnimationDelayed(view, mover, getRemoveDuration()); |
||||
} else { |
||||
mover.run(); |
||||
} |
||||
} |
||||
// Next, change stuff, to run in parallel with move animations
|
||||
if (changesPending) { |
||||
final ArrayList<ChangeInfo> changes = new ArrayList<ChangeInfo>(); |
||||
changes.addAll(mPendingChanges); |
||||
mChangesList.add(changes); |
||||
mPendingChanges.clear(); |
||||
Runnable changer = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
for (ChangeInfo change : changes) { |
||||
animateChangeImpl(change); |
||||
} |
||||
changes.clear(); |
||||
mChangesList.remove(changes); |
||||
} |
||||
}; |
||||
if (removalsPending) { |
||||
RecyclerView.ViewHolder holder = changes.get(0).oldHolder; |
||||
ViewCompat.postOnAnimationDelayed(holder.itemView, changer, getRemoveDuration()); |
||||
} else { |
||||
changer.run(); |
||||
} |
||||
} |
||||
// Next, add stuff
|
||||
if (additionsPending) { |
||||
final ArrayList<RecyclerView.ViewHolder> additions = new ArrayList<RecyclerView.ViewHolder>(); |
||||
additions.addAll(mPendingAdditions); |
||||
mAdditionsList.add(additions); |
||||
mPendingAdditions.clear(); |
||||
Runnable adder = new Runnable() { |
||||
public void run() { |
||||
for (RecyclerView.ViewHolder holder : additions) { |
||||
animateAddImpl(holder); |
||||
} |
||||
additions.clear(); |
||||
mAdditionsList.remove(additions); |
||||
} |
||||
}; |
||||
if (removalsPending || movesPending || changesPending) { |
||||
long removeDuration = removalsPending ? getRemoveDuration() : 0; |
||||
long moveDuration = movesPending ? getMoveDuration() : 0; |
||||
long changeDuration = changesPending ? getChangeDuration() : 0; |
||||
long totalDelay = removeDuration + Math.max(moveDuration, changeDuration); |
||||
View view = additions.get(0).itemView; |
||||
ViewCompat.postOnAnimationDelayed(view, adder, totalDelay); |
||||
} else { |
||||
adder.run(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public boolean animateRemove(final RecyclerView.ViewHolder holder) { |
||||
endAnimation(holder); |
||||
mPendingRemovals.add(holder); |
||||
return true; |
||||
} |
||||
|
||||
public static class SwipeAnimationData { |
||||
public long time; |
||||
public boolean right; |
||||
} |
||||
|
||||
private Map<View, SwipeAnimationData> removeData = new HashMap<>(); |
||||
|
||||
public void addRemoveData(View view, SwipeAnimationData data) { |
||||
removeData.put(view, data); |
||||
} |
||||
|
||||
private void animateRemoveImpl(final RecyclerView.ViewHolder holder) { |
||||
final View view = holder.itemView; |
||||
|
||||
SwipeAnimationData data = removeData.remove(view); |
||||
if (data == null) { |
||||
data = new SwipeAnimationData(); |
||||
data.time = getRemoveDuration(); |
||||
data.right = true; |
||||
} |
||||
|
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); |
||||
animation.setDuration(data.time) |
||||
.translationX(data.right ? view.getWidth() : -view.getWidth()) |
||||
.alpha(0) |
||||
.setInterpolator(new LinearInterpolator()) |
||||
.setListener(new VpaListenerAdapter() { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
dispatchRemoveStarting(holder); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
animation.setListener(null); |
||||
|
||||
// Reset view properties
|
||||
ViewCompat.setAlpha(view, 1); |
||||
ViewCompat.setTranslationX(view, 0f); |
||||
|
||||
dispatchRemoveFinished(holder); |
||||
mRemoveAnimations.remove(holder); |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
}).start(); |
||||
mRemoveAnimations.add(holder); |
||||
} |
||||
|
||||
@Override |
||||
public boolean animateAdd(final RecyclerView.ViewHolder holder) { |
||||
endAnimation(holder); |
||||
ViewCompat.setAlpha(holder.itemView, 0); |
||||
mPendingAdditions.add(holder); |
||||
return true; |
||||
} |
||||
|
||||
private void animateAddImpl(final RecyclerView.ViewHolder holder) { |
||||
final View view = holder.itemView; |
||||
mAddAnimations.add(holder); |
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); |
||||
animation.alpha(1).setInterpolator(new AccelerateDecelerateInterpolator()).setDuration(getAddDuration()). |
||||
setListener(new VpaListenerAdapter() { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
dispatchAddStarting(holder); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationCancel(View view) { |
||||
ViewCompat.setAlpha(view, 1); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
animation.setListener(null); |
||||
dispatchAddFinished(holder); |
||||
mAddAnimations.remove(holder); |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
}).start(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean animateMove(final RecyclerView.ViewHolder holder, int fromX, int fromY, |
||||
int toX, int toY) { |
||||
final View view = holder.itemView; |
||||
fromX += ViewCompat.getTranslationX(holder.itemView); |
||||
fromY += ViewCompat.getTranslationY(holder.itemView); |
||||
endAnimation(holder); |
||||
int deltaX = toX - fromX; |
||||
int deltaY = toY - fromY; |
||||
if (deltaX == 0 && deltaY == 0) { |
||||
dispatchMoveFinished(holder); |
||||
return false; |
||||
} |
||||
if (deltaX != 0) { |
||||
ViewCompat.setTranslationX(view, -deltaX); |
||||
} |
||||
if (deltaY != 0) { |
||||
ViewCompat.setTranslationY(view, -deltaY); |
||||
} |
||||
mPendingMoves.add(new MoveInfo(holder, fromX, fromY, toX, toY)); |
||||
return true; |
||||
} |
||||
|
||||
private void animateMoveImpl(final RecyclerView.ViewHolder holder, int fromX, int fromY, int toX, int toY) { |
||||
final View view = holder.itemView; |
||||
final int deltaX = toX - fromX; |
||||
final int deltaY = toY - fromY; |
||||
if (deltaX != 0) { |
||||
ViewCompat.animate(view).translationX(0); |
||||
} |
||||
if (deltaY != 0) { |
||||
ViewCompat.animate(view).translationY(0); |
||||
} |
||||
ViewCompat.animate(view).alpha(1f); |
||||
// TDO: make EndActions end listeners instead, since end actions aren't called when
|
||||
// vpas are canceled (and can't end them. why?)
|
||||
// need listener functionality in VPACompat for this. Ick.
|
||||
mMoveAnimations.add(holder); |
||||
final ViewPropertyAnimatorCompat animation = ViewCompat.animate(view); |
||||
animation.setDuration(getMoveDuration()).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new VpaListenerAdapter() { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
dispatchMoveStarting(holder); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationCancel(View view) { |
||||
if (deltaX != 0) { |
||||
ViewCompat.setTranslationX(view, 0); |
||||
} |
||||
if (deltaY != 0) { |
||||
ViewCompat.setTranslationY(view, 0); |
||||
} |
||||
ViewCompat.setAlpha(view, 1f); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
animation.setListener(null); |
||||
dispatchMoveFinished(holder); |
||||
mMoveAnimations.remove(holder); |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
}).start(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, |
||||
int fromX, int fromY, int toX, int toY) { |
||||
final float prevTranslationX = ViewCompat.getTranslationX(oldHolder.itemView); |
||||
final float prevTranslationY = ViewCompat.getTranslationY(oldHolder.itemView); |
||||
final float prevAlpha = ViewCompat.getAlpha(oldHolder.itemView); |
||||
endAnimation(oldHolder); |
||||
int deltaX = (int) (toX - fromX - prevTranslationX); |
||||
int deltaY = (int) (toY - fromY - prevTranslationY); |
||||
// recover prev translation state after ending animation
|
||||
ViewCompat.setTranslationX(oldHolder.itemView, prevTranslationX); |
||||
ViewCompat.setTranslationY(oldHolder.itemView, prevTranslationY); |
||||
ViewCompat.setAlpha(oldHolder.itemView, prevAlpha); |
||||
if (newHolder != null && newHolder.itemView != null) { |
||||
// carry over translation values
|
||||
endAnimation(newHolder); |
||||
ViewCompat.setTranslationX(newHolder.itemView, -deltaX); |
||||
ViewCompat.setTranslationY(newHolder.itemView, -deltaY); |
||||
ViewCompat.setAlpha(newHolder.itemView, 0); |
||||
} |
||||
mPendingChanges.add(new ChangeInfo(oldHolder, newHolder, fromX, fromY, toX, toY)); |
||||
return true; |
||||
} |
||||
|
||||
private void animateChangeImpl(final ChangeInfo changeInfo) { |
||||
final RecyclerView.ViewHolder holder = changeInfo.oldHolder; |
||||
final View view = holder.itemView; |
||||
final RecyclerView.ViewHolder newHolder = changeInfo.newHolder; |
||||
final View newView = newHolder != null ? newHolder.itemView : null; |
||||
mChangeAnimations.add(changeInfo.oldHolder); |
||||
|
||||
final ViewPropertyAnimatorCompat oldViewAnim = ViewCompat.animate(view).setDuration( |
||||
getChangeDuration()).setInterpolator(new AccelerateDecelerateInterpolator()); |
||||
oldViewAnim.translationX(changeInfo.toX - changeInfo.fromX); |
||||
oldViewAnim.translationY(changeInfo.toY - changeInfo.fromY); |
||||
oldViewAnim.alpha(0).setListener(new VpaListenerAdapter() { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
dispatchChangeStarting(changeInfo.oldHolder, true); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
oldViewAnim.setListener(null); |
||||
ViewCompat.setAlpha(view, 1); |
||||
ViewCompat.setTranslationX(view, 0); |
||||
ViewCompat.setTranslationY(view, 0); |
||||
dispatchChangeFinished(changeInfo.oldHolder, true); |
||||
mChangeAnimations.remove(changeInfo.oldHolder); |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
}).start(); |
||||
if (newView != null) { |
||||
mChangeAnimations.add(changeInfo.newHolder); |
||||
final ViewPropertyAnimatorCompat newViewAnimation = ViewCompat.animate(newView); |
||||
newViewAnimation.translationX(0).translationY(0).setDuration(getChangeDuration()). |
||||
alpha(1).setInterpolator(new AccelerateDecelerateInterpolator()).setListener(new VpaListenerAdapter() { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
dispatchChangeStarting(changeInfo.newHolder, false); |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
newViewAnimation.setListener(null); |
||||
ViewCompat.setAlpha(newView, 1); |
||||
ViewCompat.setTranslationX(newView, 0); |
||||
ViewCompat.setTranslationY(newView, 0); |
||||
dispatchChangeFinished(changeInfo.newHolder, false); |
||||
mChangeAnimations.remove(changeInfo.newHolder); |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
}).start(); |
||||
} |
||||
} |
||||
|
||||
private void endChangeAnimation(List<ChangeInfo> infoList, RecyclerView.ViewHolder item) { |
||||
for (int i = infoList.size() - 1; i >= 0; i--) { |
||||
ChangeInfo changeInfo = infoList.get(i); |
||||
if (endChangeAnimationIfNecessary(changeInfo, item)) { |
||||
if (changeInfo.oldHolder == null && changeInfo.newHolder == null) { |
||||
infoList.remove(changeInfo); |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void endChangeAnimationIfNecessary(ChangeInfo changeInfo) { |
||||
if (changeInfo.oldHolder != null) { |
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.oldHolder); |
||||
} |
||||
if (changeInfo.newHolder != null) { |
||||
endChangeAnimationIfNecessary(changeInfo, changeInfo.newHolder); |
||||
} |
||||
} |
||||
|
||||
private boolean endChangeAnimationIfNecessary(ChangeInfo changeInfo, RecyclerView.ViewHolder item) { |
||||
boolean oldItem = false; |
||||
if (changeInfo.newHolder == item) { |
||||
changeInfo.newHolder = null; |
||||
} else if (changeInfo.oldHolder == item) { |
||||
changeInfo.oldHolder = null; |
||||
oldItem = true; |
||||
} else { |
||||
return false; |
||||
} |
||||
ViewCompat.setAlpha(item.itemView, 1); |
||||
ViewCompat.setTranslationX(item.itemView, 0); |
||||
ViewCompat.setTranslationY(item.itemView, 0); |
||||
dispatchChangeFinished(item, oldItem); |
||||
return true; |
||||
} |
||||
|
||||
@Override |
||||
public void endAnimation(RecyclerView.ViewHolder item) { |
||||
final View view = item.itemView; |
||||
// this will trigger end callback which should set properties to their target values.
|
||||
ViewCompat.animate(view).cancel(); |
||||
// TDO if some other animations are chained to end, how do we cancel them as well?
|
||||
for (int i = mPendingMoves.size() - 1; i >= 0; i--) { |
||||
MoveInfo moveInfo = mPendingMoves.get(i); |
||||
if (moveInfo.holder == item) { |
||||
ViewCompat.setTranslationY(view, 0); |
||||
ViewCompat.setTranslationX(view, 0); |
||||
dispatchMoveFinished(item); |
||||
mPendingMoves.remove(item); |
||||
} |
||||
} |
||||
endChangeAnimation(mPendingChanges, item); |
||||
if (mPendingRemovals.remove(item)) { |
||||
ViewCompat.setAlpha(view, 1); |
||||
dispatchRemoveFinished(item); |
||||
} |
||||
if (mPendingAdditions.remove(item)) { |
||||
ViewCompat.setAlpha(view, 1); |
||||
dispatchAddFinished(item); |
||||
} |
||||
|
||||
for (int i = mChangesList.size() - 1; i >= 0; i--) { |
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i); |
||||
endChangeAnimation(changes, item); |
||||
if (changes.isEmpty()) { |
||||
mChangesList.remove(changes); |
||||
} |
||||
} |
||||
for (int i = mMovesList.size() - 1; i >= 0; i--) { |
||||
ArrayList<MoveInfo> moves = mMovesList.get(i); |
||||
for (int j = moves.size() - 1; j >= 0; j--) { |
||||
MoveInfo moveInfo = moves.get(j); |
||||
if (moveInfo.holder == item) { |
||||
ViewCompat.setTranslationY(view, 0); |
||||
ViewCompat.setTranslationX(view, 0); |
||||
dispatchMoveFinished(item); |
||||
moves.remove(j); |
||||
if (moves.isEmpty()) { |
||||
mMovesList.remove(moves); |
||||
} |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
for (int i = mAdditionsList.size() - 1; i >= 0; i--) { |
||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i); |
||||
if (additions.remove(item)) { |
||||
ViewCompat.setAlpha(view, 1); |
||||
dispatchAddFinished(item); |
||||
if (additions.isEmpty()) { |
||||
mAdditionsList.remove(additions); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// animations should be ended by the cancel above.
|
||||
if (mRemoveAnimations.remove(item) && DEBUG) { |
||||
throw new IllegalStateException("after animation is cancelled, item should not be in " |
||||
+ "mRemoveAnimations list"); |
||||
} |
||||
|
||||
if (mAddAnimations.remove(item) && DEBUG) { |
||||
throw new IllegalStateException("after animation is cancelled, item should not be in " |
||||
+ "mAddAnimations list"); |
||||
} |
||||
|
||||
if (mChangeAnimations.remove(item) && DEBUG) { |
||||
throw new IllegalStateException("after animation is cancelled, item should not be in " |
||||
+ "mChangeAnimations list"); |
||||
} |
||||
|
||||
if (mMoveAnimations.remove(item) && DEBUG) { |
||||
throw new IllegalStateException("after animation is cancelled, item should not be in " |
||||
+ "mMoveAnimations list"); |
||||
} |
||||
dispatchFinishedWhenDone(); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isRunning() { |
||||
return (!mPendingAdditions.isEmpty() || |
||||
!mPendingChanges.isEmpty() || |
||||
!mPendingMoves.isEmpty() || |
||||
!mPendingRemovals.isEmpty() || |
||||
!mMoveAnimations.isEmpty() || |
||||
!mRemoveAnimations.isEmpty() || |
||||
!mAddAnimations.isEmpty() || |
||||
!mChangeAnimations.isEmpty() || |
||||
!mMovesList.isEmpty() || |
||||
!mAdditionsList.isEmpty() || |
||||
!mChangesList.isEmpty()); |
||||
} |
||||
|
||||
/** |
||||
* Check the state of currently pending and running animations. If there are none |
||||
* pending/running, call {@link #dispatchAnimationsFinished()} to notify any |
||||
* listeners. |
||||
*/ |
||||
private void dispatchFinishedWhenDone() { |
||||
if (!isRunning()) { |
||||
dispatchAnimationsFinished(); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void endAnimations() { |
||||
int count = mPendingMoves.size(); |
||||
for (int i = count - 1; i >= 0; i--) { |
||||
MoveInfo item = mPendingMoves.get(i); |
||||
View view = item.holder.itemView; |
||||
ViewCompat.setTranslationY(view, 0); |
||||
ViewCompat.setTranslationX(view, 0); |
||||
dispatchMoveFinished(item.holder); |
||||
mPendingMoves.remove(i); |
||||
} |
||||
count = mPendingRemovals.size(); |
||||
for (int i = count - 1; i >= 0; i--) { |
||||
RecyclerView.ViewHolder item = mPendingRemovals.get(i); |
||||
dispatchRemoveFinished(item); |
||||
mPendingRemovals.remove(i); |
||||
} |
||||
count = mPendingAdditions.size(); |
||||
for (int i = count - 1; i >= 0; i--) { |
||||
RecyclerView.ViewHolder item = mPendingAdditions.get(i); |
||||
View view = item.itemView; |
||||
ViewCompat.setAlpha(view, 1); |
||||
dispatchAddFinished(item); |
||||
mPendingAdditions.remove(i); |
||||
} |
||||
count = mPendingChanges.size(); |
||||
for (int i = count - 1; i >= 0; i--) { |
||||
endChangeAnimationIfNecessary(mPendingChanges.get(i)); |
||||
} |
||||
mPendingChanges.clear(); |
||||
if (!isRunning()) { |
||||
return; |
||||
} |
||||
|
||||
int listCount = mMovesList.size(); |
||||
for (int i = listCount - 1; i >= 0; i--) { |
||||
ArrayList<MoveInfo> moves = mMovesList.get(i); |
||||
count = moves.size(); |
||||
for (int j = count - 1; j >= 0; j--) { |
||||
MoveInfo moveInfo = moves.get(j); |
||||
RecyclerView.ViewHolder item = moveInfo.holder; |
||||
View view = item.itemView; |
||||
ViewCompat.setTranslationY(view, 0); |
||||
ViewCompat.setTranslationX(view, 0); |
||||
dispatchMoveFinished(moveInfo.holder); |
||||
moves.remove(j); |
||||
if (moves.isEmpty()) { |
||||
mMovesList.remove(moves); |
||||
} |
||||
} |
||||
} |
||||
listCount = mAdditionsList.size(); |
||||
for (int i = listCount - 1; i >= 0; i--) { |
||||
ArrayList<RecyclerView.ViewHolder> additions = mAdditionsList.get(i); |
||||
count = additions.size(); |
||||
for (int j = count - 1; j >= 0; j--) { |
||||
RecyclerView.ViewHolder item = additions.get(j); |
||||
View view = item.itemView; |
||||
ViewCompat.setAlpha(view, 1); |
||||
dispatchAddFinished(item); |
||||
additions.remove(j); |
||||
if (additions.isEmpty()) { |
||||
mAdditionsList.remove(additions); |
||||
} |
||||
} |
||||
} |
||||
listCount = mChangesList.size(); |
||||
for (int i = listCount - 1; i >= 0; i--) { |
||||
ArrayList<ChangeInfo> changes = mChangesList.get(i); |
||||
count = changes.size(); |
||||
for (int j = count - 1; j >= 0; j--) { |
||||
endChangeAnimationIfNecessary(changes.get(j)); |
||||
if (changes.isEmpty()) { |
||||
mChangesList.remove(changes); |
||||
} |
||||
} |
||||
} |
||||
|
||||
cancelAll(mRemoveAnimations); |
||||
cancelAll(mMoveAnimations); |
||||
cancelAll(mAddAnimations); |
||||
cancelAll(mChangeAnimations); |
||||
|
||||
dispatchAnimationsFinished(); |
||||
} |
||||
|
||||
void cancelAll(List<RecyclerView.ViewHolder> viewHolders) { |
||||
for (int i = viewHolders.size() - 1; i >= 0; i--) { |
||||
ViewCompat.animate(viewHolders.get(i).itemView).cancel(); |
||||
} |
||||
} |
||||
|
||||
private static class VpaListenerAdapter implements ViewPropertyAnimatorListener { |
||||
@Override |
||||
public void onAnimationStart(View view) { |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationEnd(View view) { |
||||
} |
||||
|
||||
@Override |
||||
public void onAnimationCancel(View view) { |
||||
} |
||||
} |
||||
|
||||
; |
||||
} |
@ -1,408 +0,0 @@ |
||||
/* |
||||
* Clover - 4chan browser https://github.com/Floens/Clover/
|
||||
* Copyright (C) 2014 Floens |
||||
* |
||||
* This program is free software: you can redistribute it and/or modify |
||||
* it under the terms of the GNU General Public License as published by |
||||
* the Free Software Foundation, either version 3 of the License, or |
||||
* (at your option) any later version. |
||||
* |
||||
* This program is distributed in the hope that it will be useful, |
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
* GNU General Public License for more details. |
||||
* |
||||
* You should have received a copy of the GNU General Public License |
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/ |
||||
package org.floens.chan.ui.helper; |
||||
|
||||
import android.content.Context; |
||||
import android.graphics.Canvas; |
||||
import android.graphics.Rect; |
||||
import android.support.v7.widget.LinearLayoutManager; |
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.util.Log; |
||||
import android.view.MotionEvent; |
||||
import android.view.VelocityTracker; |
||||
import android.view.View; |
||||
import android.view.ViewConfiguration; |
||||
|
||||
import org.floens.chan.R; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
|
||||
/** |
||||
* An ItemDecorator and Touch listener that enabled the list to be reordered and items to be swiped away. |
||||
* It isn't perfect, but works good for what is should do. |
||||
*/ |
||||
public class SwipeListener extends RecyclerView.ItemDecoration implements RecyclerView.OnItemTouchListener { |
||||
public enum Swipeable { |
||||
NO, |
||||
LEFT, |
||||
RIGHT, |
||||
BOTH; |
||||
} |
||||
|
||||
private static final String TAG = "SwipeListener"; |
||||
private static final int MAX_SCROLL_SPEED = 14; // dp/s
|
||||
|
||||
private final int slopPixels; |
||||
private final int flingPixels; |
||||
private final int maxFlingPixels; |
||||
|
||||
private Callback callback; |
||||
private final RecyclerView recyclerView; |
||||
private final LinearLayoutManager layoutManager; |
||||
private final SwipeItemAnimator swipeItemAnimator; |
||||
|
||||
private VelocityTracker tracker; |
||||
private boolean swiping; |
||||
private float touchDownX; |
||||
private float touchDownY; |
||||
private float totalScrolled; |
||||
private float touchDownOffsetX; |
||||
private float offsetX; |
||||
private View downView; |
||||
|
||||
private boolean dragging; |
||||
private boolean somePositionChanged = false; |
||||
private int dragPosition = -1; |
||||
private float offsetY; |
||||
private float touchDownOffsetY; |
||||
|
||||
private final Runnable scrollRunnable = new Runnable() { |
||||
@Override |
||||
public void run() { |
||||
if (dragging) { |
||||
float scroll; |
||||
boolean up = offsetY < recyclerView.getHeight() / 2f; |
||||
if (up) { |
||||
scroll = Math.max(-dp(MAX_SCROLL_SPEED), (offsetY - recyclerView.getHeight() / 6f) * 0.1f); |
||||
} else { |
||||
scroll = Math.min(dp(MAX_SCROLL_SPEED), (offsetY - recyclerView.getHeight() * 5f / 6f) * 0.1f); |
||||
} |
||||
|
||||
if (up && scroll < 0f && layoutManager.findFirstCompletelyVisibleItemPosition() != 0) { |
||||
recyclerView.scrollBy(0, (int) scroll); |
||||
} else if (!up && scroll > 0f && layoutManager.findLastCompletelyVisibleItemPosition() != recyclerView.getAdapter().getItemCount() - 1) { |
||||
recyclerView.scrollBy(0, (int) scroll); |
||||
} |
||||
|
||||
if (scroll != 0) { |
||||
processDrag(); |
||||
recyclerView.post(scrollRunnable); |
||||
} |
||||
} |
||||
} |
||||
}; |
||||
|
||||
public SwipeListener(Context context, RecyclerView rv, Callback callback) { |
||||
recyclerView = rv; |
||||
this.callback = callback; |
||||
|
||||
layoutManager = new LinearLayoutManager(context); |
||||
rv.setLayoutManager(layoutManager); |
||||
swipeItemAnimator = new SwipeItemAnimator(); |
||||
swipeItemAnimator.setMoveDuration(250); |
||||
rv.setItemAnimator(swipeItemAnimator); |
||||
rv.addOnItemTouchListener(this); |
||||
rv.addItemDecoration(this); |
||||
|
||||
ViewConfiguration viewConfiguration = ViewConfiguration.get(context); |
||||
slopPixels = viewConfiguration.getScaledTouchSlop(); |
||||
flingPixels = viewConfiguration.getScaledMinimumFlingVelocity(); |
||||
maxFlingPixels = viewConfiguration.getScaledMaximumFlingVelocity(); |
||||
} |
||||
|
||||
@Override |
||||
public void onRequestDisallowInterceptTouchEvent(boolean b) { |
||||
} |
||||
|
||||
@Override |
||||
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) { |
||||
switch (e.getActionMasked()) { |
||||
case MotionEvent.ACTION_DOWN: |
||||
totalScrolled = 0f; |
||||
touchDownX = e.getRawX(); |
||||
touchDownY = e.getRawY(); |
||||
downView = rv.findChildViewUnder(e.getX(), e.getY()); |
||||
if (downView == null) { |
||||
// There can be gaps when a move animation is running
|
||||
break; |
||||
} |
||||
|
||||
// Dragging gets initiated immediately if the touch went down on the thumb area
|
||||
// Do not allow dragging when animations are running
|
||||
View thumbView = downView.findViewById(R.id.thumb); |
||||
if (thumbView != null && e.getX() < rv.getPaddingLeft() + thumbView.getRight() && !swipeItemAnimator.isRunning()) { |
||||
int touchAdapterPos = rv.getChildAdapterPosition(downView); |
||||
if (touchAdapterPos < 0 || !callback.isMoveable(touchAdapterPos)) { |
||||
break; |
||||
} |
||||
|
||||
dragging = true; |
||||
dragPosition = touchAdapterPos; |
||||
|
||||
rv.post(scrollRunnable); |
||||
|
||||
offsetY = e.getY(); |
||||
touchDownOffsetY = offsetY - downView.getTop(); |
||||
|
||||
downView.setVisibility(View.INVISIBLE); |
||||
rv.invalidate(); |
||||
|
||||
return true; |
||||
} |
||||
|
||||
// Didn't went down on the thumb area, start up the tracker
|
||||
if (tracker != null) { |
||||
Log.w(TAG, "Tracker was not null, recycling extra"); |
||||
tracker.recycle(); |
||||
} |
||||
tracker = VelocityTracker.obtain(); |
||||
tracker.addMovement(e); |
||||
break; |
||||
case MotionEvent.ACTION_MOVE: |
||||
if (dragging) { |
||||
return true; |
||||
} |
||||
|
||||
float deltaX = e.getRawX() - touchDownX; |
||||
float deltaY = e.getRawY() - touchDownY; |
||||
totalScrolled += Math.abs(deltaY); |
||||
int adapterPosition = rv.getChildAdapterPosition(downView); |
||||
if (adapterPosition < 0) { |
||||
break; |
||||
} |
||||
|
||||
if (swiping) { |
||||
return true; |
||||
} else { |
||||
// Logic to find out if a swipe should be initiated
|
||||
Swipeable swipeable = callback.getSwipeable(adapterPosition); |
||||
if (swipeable != Swipeable.NO && Math.abs(deltaX) >= slopPixels && totalScrolled < slopPixels) { |
||||
boolean wasSwiped = false; |
||||
if (swipeable == Swipeable.BOTH) { |
||||
wasSwiped = true; |
||||
} else if (swipeable == Swipeable.LEFT && deltaX < -slopPixels) { |
||||
wasSwiped = true; |
||||
} else if (swipeable == Swipeable.RIGHT && deltaX > slopPixels) { |
||||
wasSwiped = true; |
||||
} |
||||
|
||||
if (wasSwiped) { |
||||
swiping = true; |
||||
touchDownOffsetX = deltaX; |
||||
return true; |
||||
} |
||||
} |
||||
} |
||||
break; |
||||
case MotionEvent.ACTION_UP: |
||||
case MotionEvent.ACTION_CANCEL: |
||||
reset(); |
||||
break; |
||||
} |
||||
|
||||
return false; |
||||
} |
||||
|
||||
@Override |
||||
public void onTouchEvent(RecyclerView rv, MotionEvent e) { |
||||
if (swiping) { |
||||
float deltaX = e.getRawX() - touchDownX; |
||||
switch (e.getActionMasked()) { |
||||
case MotionEvent.ACTION_MOVE: { |
||||
tracker.addMovement(e); |
||||
offsetX = deltaX - touchDownOffsetX; |
||||
downView.setTranslationX(offsetX); |
||||
downView.setAlpha(Math.min(1f, Math.max(0f, 1f - (Math.abs(offsetX) / (float) downView.getWidth())))); |
||||
break; |
||||
} |
||||
case MotionEvent.ACTION_UP: |
||||
case MotionEvent.ACTION_CANCEL: { |
||||
boolean reset = false; |
||||
|
||||
int adapterPosition = rv.getChildAdapterPosition(downView); |
||||
if (adapterPosition < 0) { |
||||
reset = true; |
||||
} else if (e.getActionMasked() == MotionEvent.ACTION_UP) { |
||||
tracker.addMovement(e); |
||||
tracker.computeCurrentVelocity(1000); |
||||
float xVelocity = tracker.getXVelocity(); |
||||
|
||||
if (Math.abs(xVelocity) > flingPixels && Math.abs(xVelocity) < maxFlingPixels && |
||||
(xVelocity < 0) == (deltaX < 0) // Swiping in the same direction
|
||||
) { |
||||
SwipeItemAnimator.SwipeAnimationData data = new SwipeItemAnimator.SwipeAnimationData(); |
||||
data.right = xVelocity > 0; |
||||
|
||||
// Remove animations are linear, calculate the time here to mimic the fling speed
|
||||
float timeLeft = (rv.getWidth() - Math.abs(offsetX)) / Math.abs(xVelocity); |
||||
timeLeft = Math.min(0.5f, timeLeft); |
||||
data.time = (long) (timeLeft * 1000f); |
||||
swipeItemAnimator.addRemoveData(downView, data); |
||||
callback.removeItem(rv.getChildAdapterPosition(downView)); |
||||
} else { |
||||
reset = true; |
||||
} |
||||
} else { |
||||
reset = true; |
||||
} |
||||
|
||||
// The item should be reset to its original alpha and position.
|
||||
// Otherwise our SwipeItemAnimator will handle the swipe remove animation
|
||||
if (reset) { |
||||
swipeItemAnimator.animateMove(rv.getChildViewHolder(downView), 0, 0, 0, 0); |
||||
swipeItemAnimator.runPendingAnimations(); |
||||
} |
||||
|
||||
reset(); |
||||
break; |
||||
} |
||||
} |
||||
} else if (dragging) { |
||||
// Invalidate hover view
|
||||
recyclerView.invalidate(); |
||||
|
||||
switch (e.getActionMasked()) { |
||||
case MotionEvent.ACTION_MOVE: { |
||||
offsetY = e.getY(); |
||||
|
||||
processDrag(); |
||||
|
||||
break; |
||||
} |
||||
case MotionEvent.ACTION_UP: |
||||
case MotionEvent.ACTION_CANCEL: { |
||||
if (somePositionChanged) { |
||||
callback.movingDone(); |
||||
} |
||||
|
||||
RecyclerView.ViewHolder vh = recyclerView.getChildViewHolder(downView); |
||||
swipeItemAnimator.endAnimation(vh); |
||||
float floatingViewPos = offsetY - touchDownOffsetY - downView.getTop(); |
||||
swipeItemAnimator.animateMove(vh, 0, (int) floatingViewPos, 0, 0); |
||||
swipeItemAnimator.runPendingAnimations(); |
||||
|
||||
reset(); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void processDrag() { |
||||
float floatingViewPos = offsetY - touchDownOffsetY + downView.getHeight() / 2f; |
||||
|
||||
View viewAtPosition = null; |
||||
|
||||
// like findChildUnder, but without looking at the x axis
|
||||
for (int c = layoutManager.getChildCount(), i = c - 1; i >= 0; i--) { |
||||
final View child = layoutManager.getChildAt(i); |
||||
if (floatingViewPos >= child.getTop() && floatingViewPos <= child.getBottom()) { |
||||
viewAtPosition = child; |
||||
break; |
||||
} |
||||
} |
||||
|
||||
if (viewAtPosition == null) { |
||||
return; |
||||
} |
||||
|
||||
int touchAdapterPos = recyclerView.getChildAdapterPosition(viewAtPosition); |
||||
if (touchAdapterPos < 0) { |
||||
return; |
||||
} |
||||
|
||||
int firstCompletelyVisible = layoutManager.findFirstCompletelyVisibleItemPosition(); |
||||
int lastCompletelyVisible = layoutManager.findLastCompletelyVisibleItemPosition(); |
||||
|
||||
if (touchAdapterPos < firstCompletelyVisible || touchAdapterPos > lastCompletelyVisible) { |
||||
return; |
||||
} |
||||
|
||||
if ((touchAdapterPos > dragPosition && floatingViewPos > viewAtPosition.getTop() + viewAtPosition.getHeight() / 5f) || |
||||
(touchAdapterPos < dragPosition && floatingViewPos < viewAtPosition.getTop() + viewAtPosition.getHeight() * 4f / 5f)) { |
||||
if (callback.isMoveable(touchAdapterPos)) { |
||||
callback.moveItem(dragPosition, touchAdapterPos); |
||||
dragPosition = touchAdapterPos; |
||||
somePositionChanged = true; |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { |
||||
if (swiping && this.downView == view) { |
||||
outRect.set((int) offsetX, 0, (int) -offsetX, 0); |
||||
} else { |
||||
outRect.set(0, 0, 0, 0); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onDraw(Canvas canvas, RecyclerView parent, RecyclerView.State state) { |
||||
if (dragging) { |
||||
for (int i = 0, c = layoutManager.getChildCount(); i < c; i++) { |
||||
View child = layoutManager.getChildAt(i); |
||||
if (child.getVisibility() != View.VISIBLE) { |
||||
child.setVisibility(View.VISIBLE); |
||||
} |
||||
} |
||||
|
||||
RecyclerView.ViewHolder vh = parent.findViewHolderForAdapterPosition(dragPosition); |
||||
if (vh != null) { |
||||
vh.itemView.setVisibility(View.INVISIBLE); |
||||
} |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
public void onDrawOver(Canvas canvas, RecyclerView parent, RecyclerView.State state) { |
||||
if (dragging) { |
||||
RecyclerView.ViewHolder vh = parent.findViewHolderForAdapterPosition(dragPosition); |
||||
if (vh != null) { |
||||
int left = parent.getPaddingLeft(); |
||||
int top = (int) offsetY - (int) touchDownOffsetY; |
||||
canvas.save(); |
||||
canvas.translate(left, top); |
||||
vh.itemView.draw(canvas); |
||||
canvas.restore(); |
||||
} |
||||
} |
||||
} |
||||
|
||||
private void reset() { |
||||
if (tracker != null) { |
||||
tracker.recycle(); |
||||
tracker = null; |
||||
} |
||||
downView = null; |
||||
for (int i = 0, c = layoutManager.getChildCount(); i < c; i++) { |
||||
View child = layoutManager.getChildAt(i); |
||||
if (child.getVisibility() != View.VISIBLE) { |
||||
child.setVisibility(View.VISIBLE); |
||||
} |
||||
} |
||||
swiping = false; |
||||
offsetX = 0f; |
||||
dragging = false; |
||||
dragPosition = -1; |
||||
somePositionChanged = false; |
||||
} |
||||
|
||||
public interface Callback { |
||||
Swipeable getSwipeable(int position); |
||||
|
||||
void removeItem(int position); |
||||
|
||||
boolean isMoveable(int position); |
||||
|
||||
void moveItem(int from, int to); |
||||
|
||||
void movingDone(); |
||||
} |
||||
} |
Loading…
Reference in new issue