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; |
||||
|
||||
import android.support.v7.widget.RecyclerView; |
||||
import android.view.LayoutInflater; |
||||
import android.view.View; |
||||
import android.view.ViewGroup; |
||||
import android.widget.TextView; |
||||
|
||||
public class PinAdapter extends RecyclerView.Adapter<PinAdapter.PinViewHolder> { |
||||
@Override |
||||
public PinViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { |
||||
import org.floens.chan.ChanApplication; |
||||
import org.floens.chan.R; |
||||
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; |
||||
|
||||
import static org.floens.chan.utils.AndroidUtils.dp; |
||||
|
||||
public class PinAdapter extends RecyclerView.Adapter<PinAdapter.PinViewHolder> implements SwipeListener.Callback { |
||||
private final Callback callback; |
||||
private List<Pin> pins = new ArrayList<>(); |
||||
|
||||
TextView test = new TextView(parent.getContext()); |
||||
public PinAdapter(Callback callback) { |
||||
this.callback = callback; |
||||
} |
||||
|
||||
return new PinViewHolder(test); |
||||
@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 |
||||
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 |
||||
public int getItemCount() { |
||||
return 1000; |
||||
return pins.size(); |
||||
} |
||||
|
||||
public static class PinViewHolder extends RecyclerView.ViewHolder { |
||||
private View itemView; |
||||
public void onPinsChanged(List<Pin> pins) { |
||||
this.pins.clear(); |
||||
this.pins.addAll(pins); |
||||
notifyDataSetChanged(); |
||||
} |
||||
|
||||
public PinViewHolder(View itemView) { |
||||
super(itemView); |
||||
this.itemView = itemView; |
||||
public void onPinAdded(Pin pin) { |
||||
pins.add(pin); |
||||
notifyItemInserted(pins.size() - 1); |
||||
} |
||||
|
||||
public void onPinRemoved(Pin pin) { |
||||
// TODO: this is a workaround for recyclerview crashing when the last item is removed, remove this when it is fixed
|
||||
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; |
||||
|
||||
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.widget.ImageView; |
||||
import android.view.View; |
||||
|
||||
import com.android.volley.VolleyError; |
||||
import com.android.volley.toolbox.ImageLoader; |
||||
|
||||
import org.floens.chan.ChanApplication; |
||||
|
||||
public class ThumbnailView extends ImageView implements ImageLoader.ImageListener { |
||||
private String url; |
||||
public class ThumbnailView extends View implements ImageLoader.ImageListener { |
||||
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) { |
||||
super(context); |
||||
init(); |
||||
} |
||||
|
||||
public ThumbnailView(Context context, AttributeSet attrs) { |
||||
super(context, attrs); |
||||
init(); |
||||
} |
||||
|
||||
public ThumbnailView(Context context, AttributeSet attrs, int defStyleAttr) { |
||||
super(context, attrs, defStyleAttr); |
||||
init(); |
||||
} |
||||
|
||||
public void setUrl(String url) { |
||||
this.url = url; |
||||
private void init() { |
||||
} |
||||
|
||||
public void setUrl(String url, int width, int height) { |
||||
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 |
||||
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 |
||||
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