mirror of https://github.com/kurisufriend/Clover
Added ThumbnailView for round icons Added a recyclerview listener that allows the items to be reordered and swiped away.filtering
parent
328a13669f
commit
ce989cb4ed
@ -1,36 +1,125 @@ |
|||||||
package org.floens.chan.ui.adapter; |
package org.floens.chan.ui.adapter; |
||||||
|
|
||||||
import android.support.v7.widget.RecyclerView; |
import android.support.v7.widget.RecyclerView; |
||||||
|
import android.view.LayoutInflater; |
||||||
import android.view.View; |
import android.view.View; |
||||||
import android.view.ViewGroup; |
import android.view.ViewGroup; |
||||||
import android.widget.TextView; |
import android.widget.TextView; |
||||||
|
|
||||||
public class PinAdapter extends RecyclerView.Adapter<PinAdapter.PinViewHolder> { |
import org.floens.chan.ChanApplication; |
||||||
@Override |
import org.floens.chan.R; |
||||||
public PinViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
import org.floens.chan.core.model.Pin; |
||||||
|
import org.floens.chan.ui.cell.PinCell; |
||||||
|
import org.floens.chan.ui.helper.SwipeListener; |
||||||
|
import org.floens.chan.ui.view.ThumbnailView; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
TextView test = new TextView(parent.getContext()); |
import static org.floens.chan.utils.AndroidUtils.dp; |
||||||
|
|
||||||
return new PinViewHolder(test); |
public class PinAdapter extends RecyclerView.Adapter<PinAdapter.PinViewHolder> implements SwipeListener.Callback { |
||||||
|
private final Callback callback; |
||||||
|
private List<Pin> pins = new ArrayList<>(); |
||||||
|
|
||||||
|
public PinAdapter(Callback callback) { |
||||||
|
this.callback = callback; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public PinViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
||||||
|
PinCell pinCell = (PinCell) LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_pin, parent, false); |
||||||
|
return new PinViewHolder(pinCell); |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public void onBindViewHolder(PinViewHolder holder, int position) { |
public void onBindViewHolder(PinViewHolder holder, int position) { |
||||||
((TextView)holder.itemView).setText("Position = " + position); |
final Pin pin = pins.get(position); |
||||||
|
|
||||||
|
holder.textView.setText(pin.loadable.title); |
||||||
|
holder.image.setUrl(pin.thumbnailUrl, dp(40), dp(40)); |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public int getItemCount() { |
public int getItemCount() { |
||||||
return 1000; |
return pins.size(); |
||||||
} |
} |
||||||
|
|
||||||
public static class PinViewHolder extends RecyclerView.ViewHolder { |
public void onPinsChanged(List<Pin> pins) { |
||||||
private View itemView; |
this.pins.clear(); |
||||||
|
this.pins.addAll(pins); |
||||||
|
notifyDataSetChanged(); |
||||||
|
} |
||||||
|
|
||||||
|
public void onPinAdded(Pin pin) { |
||||||
|
pins.add(pin); |
||||||
|
notifyItemInserted(pins.size() - 1); |
||||||
|
} |
||||||
|
|
||||||
public PinViewHolder(View itemView) { |
public void onPinRemoved(Pin pin) { |
||||||
super(itemView); |
// TODO: this is a workaround for recyclerview crashing when the last item is removed, remove this when it is fixed
|
||||||
this.itemView = itemView; |
if (pins.size() == 1) { |
||||||
|
pins.remove(pin); |
||||||
|
notifyDataSetChanged(); |
||||||
|
} else { |
||||||
|
int location = pins.indexOf(pin); |
||||||
|
pins.remove(pin); |
||||||
|
notifyItemRemoved(location); |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
|
public void onPinChanged(Pin pin) { |
||||||
|
notifyItemChanged(pins.indexOf(pin)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public SwipeListener.Swipeable getSwipeable(int position) { |
||||||
|
return SwipeListener.Swipeable.RIGHT; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void removeItem(int position) { |
||||||
|
ChanApplication.getWatchManager().removePin(pins.get(position)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public boolean isMoveable(int position) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void moveItem(int from, int to) { |
||||||
|
Pin item = pins.remove(from); |
||||||
|
pins.add(to, item); |
||||||
|
notifyItemMoved(from, to); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void movingDone() { |
||||||
|
} |
||||||
|
|
||||||
|
public class PinViewHolder extends RecyclerView.ViewHolder { |
||||||
|
private PinCell pinCell; |
||||||
|
private ThumbnailView image; |
||||||
|
private TextView textView; |
||||||
|
|
||||||
|
public PinViewHolder(PinCell pinCell) { |
||||||
|
super(pinCell); |
||||||
|
this.pinCell = pinCell; |
||||||
|
image = (ThumbnailView) pinCell.findViewById(R.id.thumb); |
||||||
|
image.setCircular(true); |
||||||
|
textView = (TextView) pinCell.findViewById(R.id.text); |
||||||
|
|
||||||
|
pinCell.setOnClickListener(new View.OnClickListener() { |
||||||
|
@Override |
||||||
|
public void onClick(View v) { |
||||||
|
callback.onPinClicked(pins.get(getAdapterPosition())); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public interface Callback { |
||||||
|
void onPinClicked(Pin pin); |
||||||
|
} |
||||||
} |
} |
||||||
|
@ -0,0 +1,67 @@ |
|||||||
|
package org.floens.chan.ui.drawable; |
||||||
|
|
||||||
|
import android.graphics.Canvas; |
||||||
|
import android.graphics.ColorFilter; |
||||||
|
import android.graphics.Paint; |
||||||
|
import android.graphics.Path; |
||||||
|
import android.graphics.PixelFormat; |
||||||
|
import android.graphics.drawable.Drawable; |
||||||
|
|
||||||
|
import static org.floens.chan.utils.AndroidUtils.dp; |
||||||
|
|
||||||
|
|
||||||
|
public class ThumbDrawable extends Drawable { |
||||||
|
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); |
||||||
|
private Path path = new Path(); |
||||||
|
private int width; |
||||||
|
private int height; |
||||||
|
|
||||||
|
public ThumbDrawable() { |
||||||
|
width = dp(40); |
||||||
|
height = dp(40); |
||||||
|
|
||||||
|
paint.setStrokeWidth(dp(2)); |
||||||
|
paint.setStyle(Paint.Style.STROKE); |
||||||
|
paint.setStrokeCap(Paint.Cap.ROUND); |
||||||
|
paint.setColor(0xff757575); |
||||||
|
|
||||||
|
path.reset(); |
||||||
|
for (int i = 0; i < 3; i++) { |
||||||
|
int top = (int) (getMinimumHeight() / 2f + (i - 1) * dp(6)); |
||||||
|
path.moveTo(dp(8), top); |
||||||
|
path.lineTo(getMinimumWidth() - dp(8), top); |
||||||
|
} |
||||||
|
path.moveTo(0f, 0f); |
||||||
|
path.close(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void draw(Canvas canvas) { |
||||||
|
canvas.drawPath(path, paint); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getIntrinsicWidth() { |
||||||
|
return width; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getIntrinsicHeight() { |
||||||
|
return height; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setAlpha(int alpha) { |
||||||
|
paint.setAlpha(alpha); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void setColorFilter(ColorFilter cf) { |
||||||
|
paint.setColorFilter(cf); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getOpacity() { |
||||||
|
return PixelFormat.TRANSLUCENT; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,656 @@ |
|||||||
|
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) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
; |
||||||
|
} |
@ -0,0 +1,387 @@ |
|||||||
|
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 final Context context; |
||||||
|
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) { |
||||||
|
this.context = context; |
||||||
|
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 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)) { |
||||||
|
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(); |
||||||
|
} |
||||||
|
} |
@ -1,41 +1,171 @@ |
|||||||
package org.floens.chan.ui.view; |
package org.floens.chan.ui.view; |
||||||
|
|
||||||
import android.content.Context; |
import android.content.Context; |
||||||
|
import android.graphics.Bitmap; |
||||||
|
import android.graphics.BitmapShader; |
||||||
|
import android.graphics.Canvas; |
||||||
|
import android.graphics.Matrix; |
||||||
|
import android.graphics.Paint; |
||||||
|
import android.graphics.RectF; |
||||||
|
import android.graphics.Shader; |
||||||
|
import android.text.TextUtils; |
||||||
import android.util.AttributeSet; |
import android.util.AttributeSet; |
||||||
import android.widget.ImageView; |
import android.view.View; |
||||||
|
|
||||||
import com.android.volley.VolleyError; |
import com.android.volley.VolleyError; |
||||||
import com.android.volley.toolbox.ImageLoader; |
import com.android.volley.toolbox.ImageLoader; |
||||||
|
|
||||||
import org.floens.chan.ChanApplication; |
import org.floens.chan.ChanApplication; |
||||||
|
|
||||||
public class ThumbnailView extends ImageView implements ImageLoader.ImageListener { |
public class ThumbnailView extends View implements ImageLoader.ImageListener { |
||||||
private String url; |
private ImageLoader.ImageContainer container; |
||||||
|
private int fadeTime = 200; |
||||||
|
|
||||||
|
private boolean circular = false; |
||||||
|
|
||||||
|
private boolean calculate; |
||||||
|
private Bitmap bitmap; |
||||||
|
private RectF bitmapRect = new RectF(); |
||||||
|
private Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); |
||||||
|
private RectF drawRect = new RectF(); |
||||||
|
private RectF outputRect = new RectF(); |
||||||
|
|
||||||
|
private Matrix matrix = new Matrix(); |
||||||
|
BitmapShader bitmapShader; |
||||||
|
private Paint roundPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); |
||||||
|
|
||||||
public ThumbnailView(Context context) { |
public ThumbnailView(Context context) { |
||||||
super(context); |
super(context); |
||||||
|
init(); |
||||||
} |
} |
||||||
|
|
||||||
public ThumbnailView(Context context, AttributeSet attrs) { |
public ThumbnailView(Context context, AttributeSet attrs) { |
||||||
super(context, attrs); |
super(context, attrs); |
||||||
|
init(); |
||||||
} |
} |
||||||
|
|
||||||
public ThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { |
public ThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { |
||||||
super(context, attrs, defStyleAttr); |
super(context, attrs, defStyleAttr); |
||||||
|
init(); |
||||||
|
} |
||||||
|
|
||||||
|
private void init() { |
||||||
} |
} |
||||||
|
|
||||||
public void setUrl(String url) { |
public void setUrl(String url, int width, int height) { |
||||||
this.url = url; |
if (container != null && container.getRequestUrl().equals(url)) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
if (container != null) { |
||||||
|
container.cancelRequest(); |
||||||
|
container = null; |
||||||
|
setImageBitmap(null); |
||||||
|
} |
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(url)) { |
||||||
|
container = ChanApplication.getVolleyImageLoader().get(url, this, width, height); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
ImageLoader.ImageContainer container = ChanApplication.getVolleyImageLoader().get(url, this); |
public void setCircular(boolean circular) { |
||||||
|
this.circular = circular; |
||||||
|
} |
||||||
|
|
||||||
|
public void setFadeTime(int fadeTime) { |
||||||
|
this.fadeTime = fadeTime; |
||||||
|
} |
||||||
|
|
||||||
|
public Bitmap getBitmap() { |
||||||
|
return bitmap; |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { |
public void onResponse(ImageLoader.ImageContainer response, boolean isImmediate) { |
||||||
|
if (response.getBitmap() != null) { |
||||||
|
setImageBitmap(response.getBitmap()); |
||||||
|
|
||||||
|
clearAnimation(); |
||||||
|
if (fadeTime > 0 && !isImmediate) { |
||||||
|
setAlpha(0f); |
||||||
|
animate().alpha(1f).setDuration(fadeTime); |
||||||
|
} else { |
||||||
|
setAlpha(1f); |
||||||
|
} |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
@Override |
@Override |
||||||
public void onErrorResponse(VolleyError error) { |
public void onErrorResponse(VolleyError error) { |
||||||
|
error.printStackTrace(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected boolean onSetAlpha(int alpha) { |
||||||
|
if (circular) { |
||||||
|
roundPaint.setAlpha(alpha); |
||||||
|
} else { |
||||||
|
paint.setAlpha(alpha); |
||||||
|
} |
||||||
|
invalidate(); |
||||||
|
|
||||||
|
return true; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected void onDraw(Canvas canvas) { |
||||||
|
if (bitmap == null || getAlpha() == 0f) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
int width = getWidth() - getPaddingLeft() - getPaddingRight(); |
||||||
|
int height = getHeight() - getPaddingTop() - getPaddingBottom(); |
||||||
|
|
||||||
|
if (calculate) { |
||||||
|
calculate = false; |
||||||
|
bitmapRect.set(0, 0, bitmap.getWidth(), bitmap.getHeight()); |
||||||
|
float scale = Math.max( |
||||||
|
(float) width / (float) bitmap.getWidth(), |
||||||
|
(float) height / (float) bitmap.getHeight()); |
||||||
|
float scaledX = bitmap.getWidth() * scale; |
||||||
|
float scaledY = bitmap.getHeight() * scale; |
||||||
|
float offsetX = (scaledX - width) * 0.5f; |
||||||
|
float offsetY = (scaledY - height) * 0.5f; |
||||||
|
|
||||||
|
drawRect.set(-offsetX, -offsetY, scaledX - offsetX, scaledY - offsetY); |
||||||
|
drawRect.offset(getPaddingLeft(), getPaddingTop()); |
||||||
|
|
||||||
|
outputRect.set(getPaddingLeft(), getPaddingTop(), getWidth() - getPaddingRight(), getHeight() - getPaddingBottom()); |
||||||
|
|
||||||
|
matrix.setRectToRect(bitmapRect, drawRect, Matrix.ScaleToFit.FILL); |
||||||
|
|
||||||
|
if (circular) { |
||||||
|
bitmapShader.setLocalMatrix(matrix); |
||||||
|
roundPaint.setShader(bitmapShader); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
canvas.save(); |
||||||
|
canvas.clipRect(outputRect); |
||||||
|
if (circular) { |
||||||
|
canvas.drawRoundRect(outputRect, width / 2, height / 2, roundPaint); |
||||||
|
} else { |
||||||
|
canvas.drawBitmap(bitmap, matrix, paint); |
||||||
|
} |
||||||
|
canvas.restore(); |
||||||
|
} |
||||||
|
|
||||||
|
private void setImageBitmap(Bitmap bitmap) { |
||||||
|
bitmapShader = null; |
||||||
|
roundPaint.setShader(null); |
||||||
|
|
||||||
|
this.bitmap = bitmap; |
||||||
|
if (bitmap != null) { |
||||||
|
calculate = true; |
||||||
|
if (circular) { |
||||||
|
bitmapShader = new BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); |
||||||
|
} |
||||||
|
} |
||||||
|
invalidate(); |
||||||
} |
} |
||||||
} |
} |
||||||
|
Loading…
Reference in new issue