openedPositions = new SparseArray<>();
208 |
209 | /**
210 | * Data Observer that allow us to remove any opened positions when something is removed from the
211 | * adapter
212 | */
213 | private final RecyclerView.AdapterDataObserver adapterDataObserver =
214 | new RecyclerView.AdapterDataObserver() {
215 |
216 | @Override
217 | public void onChanged() {
218 | // if notifyDataSetChanged is used we cannot know if opened holders should stay open,
219 | // so close all of them
220 | openedPositions.clear();
221 | }
222 |
223 | @Override
224 | public void onItemRangeRemoved(int positionStart, int itemCount) {
225 | // if an item is removed, we need to remove the opened position
226 | for (int i = positionStart; i < positionStart + itemCount; i++) {
227 | openedPositions.remove(i);
228 | }
229 | }
230 | };
231 |
232 | private final RecyclerView.OnItemTouchListener mOnItemTouchListener =
233 | new RecyclerView.OnItemTouchListener() {
234 | @Override
235 | public boolean onInterceptTouchEvent(RecyclerView recyclerView, MotionEvent event) {
236 | if (DEBUG) {
237 | Log.d(TAG, "intercept: x:" + event.getX() + ",y:" + event.getY() + ", " + event);
238 | }
239 | final int action = event.getAction();
240 | if (action == MotionEvent.ACTION_DOWN) {
241 | activePointerId = event.getPointerId(0);
242 | initialTouchX = event.getX();
243 | initialTouchY = event.getY();
244 | obtainVelocityTracker();
245 | if (selected == null) {
246 | final RecoverAnimation animation = findAnimation(event);
247 | if (animation != null) {
248 | initialTouchX -= animation.x;
249 | initialTouchY -= animation.y;
250 | endRecoverAnimation(animation.viewHolder);
251 | select(animation.viewHolder, animation.actionState);
252 | updateDxDy(event, selectedFlags, 0);
253 | }
254 | }
255 | } else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
256 | activePointerId = ACTIVE_POINTER_ID_NONE;
257 | select(null, ACTION_STATE_IDLE);
258 | } else if (activePointerId != ACTIVE_POINTER_ID_NONE) {
259 | // in a non scroll orientation, if distance change is above threshold, we
260 | // can select the item
261 | final int index = event.findPointerIndex(activePointerId);
262 | if (DEBUG) {
263 | Log.d(TAG, "pointer index " + index);
264 | }
265 | if (index >= 0) {
266 | checkSelectForSwipe(action, event, index);
267 | }
268 | }
269 | if (velocityTracker != null) {
270 | velocityTracker.addMovement(event);
271 | }
272 | return selected != null;
273 | }
274 |
275 | @Override
276 | public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
277 | if (DEBUG) {
278 | Log.d(TAG, "on touch: x:" + initialTouchX + ",y:" + initialTouchY + ", :" + event);
279 | }
280 | if (velocityTracker != null) {
281 | velocityTracker.addMovement(event);
282 | }
283 | if (activePointerId == ACTIVE_POINTER_ID_NONE) {
284 | return;
285 | }
286 | final int action = event.getActionMasked();
287 | final int activePointerIndex = event.findPointerIndex(activePointerId);
288 | if (activePointerIndex >= 0) {
289 | checkSelectForSwipe(action, event, activePointerIndex);
290 | }
291 | if (selected == null) {
292 | return;
293 | }
294 | switch (action) {
295 | case MotionEvent.ACTION_MOVE: {
296 | // Find the index of the active pointer and fetch its position
297 | if (activePointerIndex >= 0) {
298 | updateDxDy(event, selectedFlags, activePointerIndex);
299 | SwipeOpenItemTouchHelper.this.recyclerView.invalidate();
300 | }
301 | break;
302 | }
303 | case MotionEvent.ACTION_CANCEL:
304 | if (velocityTracker != null) {
305 | velocityTracker.clear();
306 | }
307 | // fall through
308 | case MotionEvent.ACTION_UP:
309 | select(null, ACTION_STATE_IDLE);
310 | activePointerId = ACTIVE_POINTER_ID_NONE;
311 | break;
312 | case MotionEvent.ACTION_POINTER_UP: {
313 | final int pointerIndex = event.getActionIndex();
314 | final int pointerId = event.getPointerId(pointerIndex);
315 | if (pointerId == activePointerId) {
316 | // This was our active pointer going up. Choose a new
317 | // active pointer and adjust accordingly.
318 | final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
319 | activePointerId = event.getPointerId(newPointerIndex);
320 | updateDxDy(event, selectedFlags, pointerIndex);
321 | }
322 | break;
323 | }
324 | }
325 | }
326 |
327 | @Override
328 | public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
329 | if (!disallowIntercept) {
330 | return;
331 | }
332 | select(null, ACTION_STATE_IDLE);
333 | }
334 | };
335 |
336 | private final RecyclerView.OnScrollListener scrollListener = new RecyclerView.OnScrollListener() {
337 | @Override
338 | public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
339 | if (closeOnAction && (dx != 0 || dy != 0)) {
340 | if (prevSelected != null && (Math.abs(prevSelected.getSwipeView().getTranslationX()) > 0
341 | || Math.abs(prevSelected.getSwipeView().getTranslationY()) > 0)) {
342 | closeOpenHolder(prevSelected);
343 | prevSelected = null;
344 | }
345 | // if we've got any open positions saved from a rotation, close those
346 | if (openedPositions.size() > 0) {
347 | for (int i = 0; i < openedPositions.size(); i++) {
348 | RecyclerView.ViewHolder holder =
349 | recyclerView.findViewHolderForAdapterPosition(openedPositions.keyAt(i));
350 | if (holder instanceof SwipeOpenViewHolder) {
351 | closeOpenHolder((SwipeOpenViewHolder) holder);
352 | }
353 | openedPositions.removeAt(i);
354 | }
355 | }
356 | }
357 | }
358 | };
359 |
360 | /**
361 | * Creates an SwipeOpenItemTouchHelper that will work with the given Callback.
362 | *
363 | * You can attach SwipeOpenItemTouchHelper to a RecyclerView via
364 | * {@link #attachToRecyclerView(RecyclerView)}. Upon attaching, it will add an item decoration,
365 | * an onItemTouchListener and a Child attach / detach listener to the RecyclerView.
366 | *
367 | * @param callback The Callback which controls the behavior of this touch helper.
368 | */
369 | public SwipeOpenItemTouchHelper(Callback callback) {
370 | this.callback = callback;
371 | }
372 |
373 | private static boolean hitTest(View child, float x, float y, float left, float top) {
374 | return x >= left && x <= left + child.getWidth() && y >= top && y <= top + child.getHeight();
375 | }
376 |
377 | /**
378 | * Attaches the SwipeOpenItemTouchHelper to the provided RecyclerView. If the helper is already
379 | * attached to a RecyclerView, it will first detach from the previous one. You can call this
380 | * method with {@code null} to detach it from the current RecyclerView.
381 | * NOTE: RecyclerView must have an adapter set in order to allow adapter data observing to
382 | * correctly save opened positions state.
383 | *
384 | * @param recyclerView The RecyclerView instance to which you want to add this helper or
385 | * {@code null} if you want to remove SwipeOpenItemTouchHelper from the current
386 | * RecyclerView.
387 | */
388 | public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
389 | if (this.recyclerView == recyclerView) {
390 | return; // nothing to do
391 | }
392 | if (this.recyclerView != null) {
393 | destroyCallbacks();
394 | }
395 | this.recyclerView = recyclerView;
396 | if (this.recyclerView != null) {
397 | setupCallbacks();
398 | }
399 | }
400 |
401 | /**
402 | * Flag to determine if any open SwipeViewHolders are closed when the RecyclerView is scrolled,
403 | * or when a new view holder is swiped.
404 | * Default value is true.
405 | *
406 | * @param closeOnAction true to close on an action, false to keep them open
407 | */
408 | public void setCloseOnAction(boolean closeOnAction) {
409 | this.closeOnAction = closeOnAction;
410 | }
411 |
412 | /**
413 | * Flag to prevent SwipeOpenItemTouchHelper from swiping open zero-sized Start or End views.
414 | *
415 | * @param preventZeroSizeViewSwipes true to prevent swiping open zero sized views, false to allow
416 | */
417 | public void setPreventZeroSizeViewSwipes(boolean preventZeroSizeViewSwipes) {
418 | this.preventZeroSizeViewSwipes = preventZeroSizeViewSwipes;
419 | }
420 |
421 | private void setupCallbacks() {
422 | ViewConfiguration vc = ViewConfiguration.get(recyclerView.getContext());
423 | slop = vc.getScaledTouchSlop();
424 | recyclerView.addItemDecoration(this);
425 | recyclerView.addOnItemTouchListener(mOnItemTouchListener);
426 | recyclerView.addOnChildAttachStateChangeListener(this);
427 | recyclerView.addOnScrollListener(scrollListener);
428 | Resources resources = recyclerView.getContext().getResources();
429 | isRtl = false;
430 | if (recyclerView.getAdapter() == null) {
431 | throw new IllegalStateException(
432 | "SwipeOpenItemTouchHelper.attachToRecyclerView must be called after "
433 | + "the RecyclerView's adapter has been set.");
434 | } else {
435 | recyclerView.getAdapter().registerAdapterDataObserver(adapterDataObserver);
436 | }
437 | }
438 |
439 | private void destroyCallbacks() {
440 | recyclerView.removeItemDecoration(this);
441 | recyclerView.removeOnItemTouchListener(mOnItemTouchListener);
442 | recyclerView.removeOnChildAttachStateChangeListener(this);
443 | if (recyclerView.getAdapter() != null) {
444 | recyclerView.getAdapter().unregisterAdapterDataObserver(adapterDataObserver);
445 | }
446 |
447 | // clean all attached
448 | final int recoverAnimSize = recoverAnimations.size();
449 | for (int i = recoverAnimSize - 1; i >= 0; i--) {
450 | final RecoverAnimation recoverAnimation = recoverAnimations.get(0);
451 | callback.clearView(recyclerView, recoverAnimation.viewHolder);
452 | }
453 | recoverAnimations.clear();
454 | releaseVelocityTracker();
455 | isRtl = false;
456 | }
457 |
458 | private void getSelectedDxDy(float[] outPosition) {
459 | if ((selectedFlags & (LEFT | RIGHT)) != 0) {
460 | outPosition[0] = selectedStartX + dX - selected.getSwipeView().getLeft();
461 | } else {
462 | outPosition[0] = selected.getSwipeView().getTranslationX();
463 | }
464 | if ((selectedFlags & (UP | DOWN)) != 0) {
465 | outPosition[1] = selectedStartY + dY - selected.getSwipeView().getTop();
466 | } else {
467 | outPosition[1] = selected.getSwipeView().getTranslationY();
468 | }
469 | }
470 |
471 | @Override
472 | public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
473 | float dx = 0, dy = 0;
474 | if (selected != null) {
475 | getSelectedDxDy(tmpPosition);
476 | dx = tmpPosition[0];
477 | dy = tmpPosition[1];
478 | }
479 | callback.onDrawOver(c, parent, selected, recoverAnimations, actionState, dx, dy);
480 | }
481 |
482 | @Override
483 | public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
484 |
485 | float dx = 0, dy = 0;
486 | if (selected != null) {
487 | getSelectedDxDy(tmpPosition);
488 | dx = tmpPosition[0];
489 | dy = tmpPosition[1];
490 | }
491 |
492 | // checks if we need to prevent zero-size swipe-to-opens
493 | if (selected != null && preventZeroSizeViewSwipes) {
494 | if (preventHorizontalAction(selected, dx)) {
495 | dx = 0;
496 | } else if (preventVerticalAction(selected, dy)) {
497 | dy = 0;
498 | }
499 | }
500 | callback.onDraw(c, parent, selected, recoverAnimations, actionState, dx, dy, isRtl);
501 | }
502 |
503 | /**
504 | * Checks if we need to prevent a horizontal swipe action for a view holder -- this is used when
505 | * we have preventZeroSizeViewSwipes set to true and we need to check if we're preventing a
506 | * zero-size swipe
507 | *
508 | * @param holder the view holder
509 | * @param translationX the new translation x of the holder
510 | * @return true if we need to prevent the action, false if not
511 | */
512 | private boolean preventHorizontalAction(final SwipeOpenViewHolder holder,
513 | final float translationX) {
514 | if (translationX > 0f && ((!isRtl && holder.getStartHiddenViewSize() == 0f) ^ (isRtl
515 | && holder.getEndHiddenViewSize() == 0f))) {
516 | return true;
517 | } else if (translationX < 0f && ((!isRtl && holder.getEndHiddenViewSize() == 0f) ^ (isRtl
518 | && holder.getStartHiddenViewSize() == 0f))) {
519 | return true;
520 | }
521 | return false;
522 | }
523 |
524 | private boolean preventVerticalAction(final SwipeOpenViewHolder holder, final float dy) {
525 | if (dy > 0f && holder.getStartHiddenViewSize() == 0f) {
526 | return true;
527 | } else if (dy < 0f && holder.getEndHiddenViewSize() == 0f) {
528 | return true;
529 | }
530 | return false;
531 | }
532 |
533 | /**
534 | * Starts dragging or swiping the given View. Call with null if you want to clear it.
535 | *
536 | * @param selected The ViewHolder to swipe. Can be null if you want to cancel the
537 | * current action
538 | * @param actionState The type of action
539 | */
540 | private void select(SwipeOpenViewHolder selected, int actionState) {
541 | if (selected == this.selected && actionState == this.actionState) {
542 | return;
543 | }
544 | final int prevActionState = this.actionState;
545 | // prevent duplicate animations
546 | endRecoverAnimation(selected);
547 | this.actionState = actionState;
548 | int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState)) - 1;
549 | boolean preventLayout = false;
550 |
551 | // close the previously selected view holder if we're swiping a new one and the flag is true
552 | if (closeOnAction && selected != null && prevSelected != null && selected != prevSelected) {
553 | closeOpenHolder(prevSelected);
554 | prevSelected = null;
555 | preventLayout = true;
556 | }
557 |
558 | // if we've got any opened positions, and closeOnAction is true, close them
559 | // NOTE: only real way for this to happen is to have a view opened during configuration change
560 | // that then has its' state saved
561 | if (closeOnAction && openedPositions.size() > 0) {
562 | for (int i = 0; i < openedPositions.size(); i++) {
563 | RecyclerView.ViewHolder holder =
564 | recyclerView.findViewHolderForAdapterPosition(openedPositions.keyAt(i));
565 | // if our selected isn't the opened position, close it
566 | if (holder instanceof SwipeOpenViewHolder && (selected == null
567 | || holder.getAdapterPosition() != selected.getViewHolder().getAdapterPosition())) {
568 | closeOpenHolder((SwipeOpenViewHolder) holder);
569 | }
570 | openedPositions.removeAt(i);
571 | }
572 | }
573 |
574 | if (this.selected != null) {
575 | prevSelected = this.selected;
576 | // we've changed selection, we need to animate it back
577 | if (prevSelected.getViewHolder().itemView.getParent() != null) {
578 | final int swipeDir = checkPreviousSwipeDirection(prevSelected.getViewHolder());
579 | releaseVelocityTracker();
580 | // find where we should animate to
581 | final float targetTranslateX, targetTranslateY;
582 | getSelectedDxDy(tmpPosition);
583 |
584 | final float currentTranslateX = tmpPosition[0];
585 | final float currentTranslateY = tmpPosition[1];
586 | // only need to check if we need a recover animation for non-zero translation views
587 | if (prevSelected.getSwipeView().getTranslationX() != 0
588 | || prevSelected.getSwipeView().getTranslationY() != 0) {
589 | final float absTranslateX = Math.abs(currentTranslateX);
590 | final float absTranslateY = Math.abs(currentTranslateY);
591 | final SavedOpenState state;
592 | switch (swipeDir) {
593 | case LEFT:
594 | case START:
595 | targetTranslateY = 0;
596 | // check if we need to close or go to the open position
597 | if (absTranslateX > prevSelected.getEndHiddenViewSize() / 2) {
598 | targetTranslateX = prevSelected.getEndHiddenViewSize() * Math.signum(dX);
599 | state = SavedOpenState.END_OPEN;
600 | } else {
601 | targetTranslateX = 0;
602 | state = null;
603 | }
604 | break;
605 | case RIGHT:
606 | case END:
607 | targetTranslateY = 0;
608 | if (absTranslateX > prevSelected.getStartHiddenViewSize() / 2) {
609 | targetTranslateX = prevSelected.getStartHiddenViewSize() * Math.signum(dX);
610 | state = SavedOpenState.START_OPEN;
611 | } else {
612 | targetTranslateX = 0;
613 | state = null;
614 | }
615 | break;
616 | case UP:
617 | targetTranslateX = 0;
618 | if (absTranslateY > prevSelected.getEndHiddenViewSize() / 2) {
619 | targetTranslateY = prevSelected.getEndHiddenViewSize() * Math.signum(dY);
620 | state = SavedOpenState.END_OPEN;
621 | } else {
622 | targetTranslateY = 0;
623 | state = null;
624 | }
625 | break;
626 | case DOWN:
627 | targetTranslateX = 0;
628 | if (absTranslateY > prevSelected.getStartHiddenViewSize() / 2) {
629 | targetTranslateY = prevSelected.getStartHiddenViewSize() * Math.signum(dY);
630 | state = SavedOpenState.START_OPEN;
631 | } else {
632 | targetTranslateY = 0;
633 | state = null;
634 | }
635 | break;
636 | default:
637 | state = null;
638 | targetTranslateX = 0;
639 | targetTranslateY = 0;
640 | }
641 | // if state == null, we're closing it
642 | if (state == null) {
643 | openedPositions.remove(prevSelected.getViewHolder().getAdapterPosition());
644 | } else {
645 | openedPositions.put(prevSelected.getViewHolder().getAdapterPosition(), state);
646 | }
647 |
648 | final RecoverAnimation rv =
649 | new RecoverAnimation(prevSelected, prevActionState, currentTranslateX,
650 | currentTranslateY, targetTranslateX, targetTranslateY);
651 | final long duration = callback.getAnimationDuration(recyclerView, ANIMATION_TYPE_SWIPE,
652 | targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
653 | rv.setDuration(duration);
654 | recoverAnimations.add(rv);
655 | rv.start();
656 | preventLayout = true;
657 | } else {
658 | // if both translations are 0, it's closed
659 | openedPositions.remove(prevSelected.getViewHolder().getAdapterPosition());
660 | }
661 | } else {
662 | callback.clearView(recyclerView, prevSelected);
663 | }
664 | this.selected = null;
665 | }
666 | if (selected != null) {
667 | selectedFlags =
668 | (callback.getAbsMovementFlags(recyclerView, selected.getViewHolder()) & actionStateMask)
669 | >> (this.actionState * DIRECTION_FLAG_COUNT);
670 | selectedStartX =
671 | selected.getViewHolder().itemView.getLeft() + selected.getSwipeView().getTranslationX();
672 | selectedStartY =
673 | selected.getViewHolder().itemView.getTop() + selected.getSwipeView().getTranslationY();
674 | this.selected = selected;
675 | }
676 | final ViewParent rvParent = recyclerView.getParent();
677 | if (rvParent != null) {
678 | rvParent.requestDisallowInterceptTouchEvent(this.selected != null);
679 | }
680 | if (!preventLayout) {
681 | recyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
682 | }
683 | callback.onSelectedChanged(this.selected, this.actionState);
684 | recyclerView.invalidate();
685 | }
686 |
687 | @Override
688 | public void onChildViewAttachedToWindow(View view) {
689 | final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
690 | if (holder == null || !(holder instanceof SwipeOpenViewHolder)) {
691 | return;
692 | }
693 | // check if the view we are about to attach had previously saved open state,
694 | // and then open it based off that
695 | if (openedPositions.get(holder.getAdapterPosition(), null) != null) {
696 | final SwipeOpenViewHolder swipeHolder = (SwipeOpenViewHolder) holder;
697 | final SavedOpenState state = openedPositions.get(holder.getAdapterPosition());
698 |
699 | if (recyclerView.getLayoutManager().canScrollVertically()) {
700 | int rtlFlipStart = isRtl ? -1 : 1;
701 | int rtlFlipEnd = isRtl ? 1 : -1;
702 |
703 | // if we're in an opened state and both view sizes are 0, then we're attempting
704 | // to restore the opened position before the view has measured, so we need to measure it
705 | if (swipeHolder.getStartHiddenViewSize() == 0 && swipeHolder.getEndHiddenViewSize() == 0) {
706 | final int widthSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
707 | final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
708 | swipeHolder.getViewHolder().itemView.measure(widthSpec, heightSpec);
709 | }
710 |
711 | swipeHolder.getSwipeView().setTranslationX(
712 | state == SavedOpenState.START_OPEN ? swipeHolder.getStartHiddenViewSize() * rtlFlipStart
713 | : swipeHolder.getEndHiddenViewSize() * rtlFlipEnd);
714 | } else {
715 | swipeHolder.getSwipeView().setTranslationY(
716 | state == SavedOpenState.START_OPEN ? swipeHolder.getStartHiddenViewSize()
717 | : swipeHolder.getEndHiddenViewSize() * -1);
718 | }
719 | }
720 | }
721 |
722 | /**
723 | * When a View is detached from the RecyclerView it is either because the item has been deleted,
724 | * or the View is being detached/recycled because it is no longer visible (e.g. RecyclerView has
725 | * been scrolled)
726 | *
727 | * @param view the view being detached
728 | */
729 | @Override
730 | public void onChildViewDetachedFromWindow(View view) {
731 | final RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(view);
732 | if (holder == null || !(holder instanceof SwipeOpenViewHolder)) {
733 | return;
734 | }
735 | final SwipeOpenViewHolder swipeHolder = (SwipeOpenViewHolder) holder;
736 |
737 | if (prevSelected == swipeHolder) {
738 | prevSelected = null;
739 | }
740 | if (selected != null && swipeHolder == selected) {
741 | select(null, ACTION_STATE_IDLE);
742 | } else {
743 | callback.clearView(recyclerView, swipeHolder);
744 | endRecoverAnimation(swipeHolder);
745 | }
746 | }
747 |
748 | private void endRecoverAnimation(SwipeOpenViewHolder viewHolder) {
749 | final int recoverAnimSize = recoverAnimations.size();
750 | for (int i = recoverAnimSize - 1; i >= 0; i--) {
751 | final RecoverAnimation anim = recoverAnimations.get(i);
752 | if (anim.viewHolder == viewHolder) {
753 | if (!anim.ended) {
754 | anim.cancel();
755 | }
756 | recoverAnimations.remove(i);
757 | }
758 | }
759 | }
760 |
761 | /**
762 | * Opens the position of the START hidden view for a given position
763 | *
764 | * @param position the position
765 | */
766 | public void openPositionStart(final int position) {
767 | openPosition(position, SavedOpenState.START_OPEN);
768 | }
769 |
770 | /**
771 | * Opens the position of the END hidden view for a given position
772 | *
773 | * @param position the position
774 | */
775 | public void openPositionEnd(final int position) {
776 | openPosition(position, SavedOpenState.END_OPEN);
777 | }
778 |
779 | private void openPosition(final int position, final SavedOpenState direction) {
780 | if (recyclerView == null) {
781 | return;
782 | }
783 | // attempt to close any open positions
784 | if (closeOnAction) {
785 | closeAllOpenPositions();
786 | }
787 |
788 | RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(position);
789 | if (holder instanceof SwipeOpenViewHolder) {
790 | // check that the view holder is attached to a parent
791 | if (((SwipeOpenViewHolder) holder).getViewHolder().itemView.getParent() != null) {
792 | // end any current animations for the view holder
793 | endRecoverAnimation((SwipeOpenViewHolder) holder);
794 | openHolder((SwipeOpenViewHolder) holder, direction);
795 | recyclerView.invalidate();
796 | }
797 | }
798 | // add open position to our saved positions
799 | openedPositions.put(position, direction);
800 | }
801 |
802 | /**
803 | * Closes the given SwipeOpenViewHolder at the given position if there is one.
804 | * If the position is not currently attached to the RecyclerView (e.g. off-screen), then
805 | * the opened position will just be removed and the holder will appear in a closed position
806 | * when it is next created/bound.
807 | *
808 | * @param position the position to close
809 | */
810 | public void closeOpenPosition(final int position) {
811 | if (recyclerView == null) {
812 | return;
813 | }
814 | RecyclerView.ViewHolder holder = recyclerView.findViewHolderForAdapterPosition(position);
815 | if (holder instanceof SwipeOpenViewHolder) {
816 | // check that the view holder is attached to a parent
817 | if (((SwipeOpenViewHolder) holder).getViewHolder().itemView.getParent() != null) {
818 | // end any current animations for the view holder
819 | endRecoverAnimation((SwipeOpenViewHolder) holder);
820 | closeOpenHolder((SwipeOpenViewHolder) holder);
821 | recyclerView.invalidate();
822 | }
823 | }
824 | // remove the position if we have not already
825 | openedPositions.remove(position);
826 | }
827 |
828 | /**
829 | * Closes all currently opened SwipeOpenViewHolders for the currently attached RecyclerView
830 | */
831 | public void closeAllOpenPositions() {
832 | if (recyclerView == null) {
833 | return;
834 | }
835 | for (int i = openedPositions.size() - 1; i >= 0; i--) {
836 | closeOpenPosition(openedPositions.keyAt(i));
837 | }
838 | // remove all positions in case one was not removed
839 | openedPositions.clear();
840 | }
841 |
842 | /**
843 | * Closes a SwipeOpenHolder that has been previously opened
844 | *
845 | * @param holder the holder
846 | */
847 | private void closeOpenHolder(SwipeOpenViewHolder holder) {
848 | final View swipeView = holder.getSwipeView();
849 | final float translationX = swipeView.getTranslationX();
850 | final float translationY = swipeView.getTranslationY();
851 | final RecoverAnimation rv = new RecoverAnimation(holder, 0, translationX, translationY, 0, 0);
852 | final long duration =
853 | callback.getAnimationDuration(recyclerView, ANIMATION_TYPE_SWIPE, translationX,
854 | translationY);
855 | rv.setDuration(duration);
856 | recoverAnimations.add(rv);
857 | rv.start();
858 | // remove it from our open positions if we've got it
859 | openedPositions.remove(holder.getViewHolder().getAdapterPosition());
860 | }
861 |
862 | /**
863 | * Opens a SwipeOpenHolder in a given direction
864 | *
865 | * @param holder the holder
866 | * @param direction the direction
867 | * @return true if the view was opened, false if not
868 | */
869 | private void openHolder(SwipeOpenViewHolder holder, SavedOpenState direction) {
870 | final View swipeView = holder.getSwipeView();
871 | final float translationX = swipeView.getTranslationX();
872 | final float translationY = swipeView.getTranslationY();
873 | final float openSize = direction == SavedOpenState.START_OPEN ? holder.getStartHiddenViewSize()
874 | : holder.getEndHiddenViewSize();
875 |
876 | final RecoverAnimation rv;
877 | if (recyclerView.getLayoutManager().canScrollVertically()) {
878 | int rtlFlipStart = isRtl ? -1 : 1;
879 | int rtlFlipEnd = isRtl ? 1 : -1;
880 |
881 | float targetDx =
882 | direction == SavedOpenState.START_OPEN ? openSize * rtlFlipStart : openSize * rtlFlipEnd;
883 | rv = new RecoverAnimation(holder, 0, translationX, translationY, targetDx, 0);
884 | } else {
885 | float targetDx = direction == SavedOpenState.START_OPEN ? openSize * -1 : openSize;
886 | rv = new RecoverAnimation(holder, 0, translationX, translationY, 0, targetDx);
887 | }
888 | final long duration =
889 | callback.getAnimationDuration(recyclerView, ANIMATION_TYPE_SWIPE, translationX,
890 | translationY);
891 | rv.setDuration(duration);
892 | recoverAnimations.add(rv);
893 | rv.start();
894 | }
895 |
896 | @Override
897 | public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
898 | RecyclerView.State state) {
899 | outRect.setEmpty();
900 | }
901 |
902 | private void obtainVelocityTracker() {
903 | if (velocityTracker != null) {
904 | velocityTracker.recycle();
905 | }
906 | velocityTracker = VelocityTracker.obtain();
907 | }
908 |
909 | private void releaseVelocityTracker() {
910 | if (velocityTracker != null) {
911 | velocityTracker.recycle();
912 | velocityTracker = null;
913 | }
914 | }
915 |
916 | private RecyclerView.ViewHolder findSwipedView(MotionEvent motionEvent) {
917 | final RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
918 | if (activePointerId == ACTIVE_POINTER_ID_NONE) {
919 | return null;
920 | }
921 | final int pointerIndex = motionEvent.findPointerIndex(activePointerId);
922 | final float dx = motionEvent.getX(pointerIndex) - initialTouchX;
923 | final float dy = motionEvent.getY(pointerIndex) - initialTouchY;
924 | final float absDx = Math.abs(dx);
925 | final float absDy = Math.abs(dy);
926 |
927 | if (absDx < slop && absDy < slop) {
928 | return null;
929 | }
930 | if (absDx > absDy && lm.canScrollHorizontally()) {
931 | return null;
932 | } else if (absDy > absDx && lm.canScrollVertically()) {
933 | return null;
934 | }
935 | View child = findChildView(motionEvent);
936 | if (child == null) {
937 | return null;
938 | }
939 | RecyclerView.ViewHolder holder = recyclerView.getChildViewHolder(child);
940 | if (holder instanceof SwipeOpenViewHolder) {
941 | return holder;
942 | }
943 | return null;
944 | }
945 |
946 | /**
947 | * Checks whether we should select a View for swiping.
948 | */
949 | private boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
950 | if (selected != null || action != MotionEvent.ACTION_MOVE) {
951 | return false;
952 | }
953 | if (recyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
954 | return false;
955 | }
956 | final RecyclerView.ViewHolder vh = findSwipedView(motionEvent);
957 | if (vh == null) {
958 | return false;
959 | }
960 |
961 | final int movementFlags = callback.getAbsMovementFlags(recyclerView, vh);
962 |
963 | final int swipeFlags =
964 | (movementFlags & ACTION_MODE_SWIPE_MASK) >> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
965 |
966 | if (swipeFlags == 0) {
967 | return false;
968 | }
969 |
970 | // dX and dY are only set in allowed directions. We use custom x/y here instead of
971 | // updateDxDy to avoid swiping if user moves more in the other direction
972 | final float x = motionEvent.getX(pointerIndex);
973 | final float y = motionEvent.getY(pointerIndex);
974 |
975 | // Calculate the distance moved
976 | final float dx = x - initialTouchX;
977 | final float dy = y - initialTouchY;
978 | // swipe target is chose w/o applying flags so it does not really check if swiping in that
979 | // direction is allowed. This why here, we use dX dY to check slope value again.
980 | final float absDx = Math.abs(dx);
981 | final float absDy = Math.abs(dy);
982 |
983 | if (absDx < slop && absDy < slop) {
984 | return false;
985 | }
986 | if (absDx > absDy) {
987 | if (dx < 0 && (swipeFlags & LEFT) == 0) {
988 | return false;
989 | }
990 | if (dx > 0 && (swipeFlags & RIGHT) == 0) {
991 | return false;
992 | }
993 | } else {
994 | if (dy < 0 && (swipeFlags & UP) == 0) {
995 | return false;
996 | }
997 | if (dy > 0 && (swipeFlags & DOWN) == 0) {
998 | return false;
999 | }
1000 | }
1001 | dX = dY = 0f;
1002 | activePointerId = motionEvent.getPointerId(0);
1003 | select((SwipeOpenViewHolder) vh, ACTION_STATE_SWIPE);
1004 | return true;
1005 | }
1006 |
1007 | private View findChildView(MotionEvent event) {
1008 | // first check elevated views, if none, then call RV
1009 | final float x = event.getX();
1010 | final float y = event.getY();
1011 | if (selected != null) {
1012 | final View selectedView = selected.getViewHolder().itemView;
1013 | if (hitTest(selectedView, x, y, selectedStartX + dX, selectedStartY + dY)) {
1014 | return selectedView;
1015 | }
1016 | }
1017 | for (int i = recoverAnimations.size() - 1; i >= 0; i--) {
1018 | final RecoverAnimation anim = recoverAnimations.get(i);
1019 | final View view = anim.viewHolder.getViewHolder().itemView;
1020 | if (hitTest(view, x, y, anim.x, anim.y)) {
1021 | return view;
1022 | }
1023 | }
1024 | return recyclerView.findChildViewUnder(x, y);
1025 | }
1026 |
1027 | /**
1028 | * Starts swiping the provided ViewHolder.
1029 | * See {@link android.support.v7.widget.helper.ItemTouchHelper#startSwipe(RecyclerView.ViewHolder)}
1030 | *
1031 | * @param viewHolder The ViewHolder to start swiping. It must be a direct child of
1032 | * RecyclerView.
1033 | */
1034 | public void startSwipe(SwipeOpenViewHolder viewHolder) {
1035 | if (viewHolder.getViewHolder().itemView.getParent() != recyclerView) {
1036 | Log.e(TAG, "Start swipe has been called with a view holder which is not a child of "
1037 | + "the RecyclerView controlled by this SwipeOpenItemTouchHelper.");
1038 | return;
1039 | }
1040 | obtainVelocityTracker();
1041 | dX = dY = 0f;
1042 | select(viewHolder, ACTION_STATE_SWIPE);
1043 | }
1044 |
1045 | private RecoverAnimation findAnimation(MotionEvent event) {
1046 | if (recoverAnimations.isEmpty()) {
1047 | return null;
1048 | }
1049 | View target = findChildView(event);
1050 | for (int i = recoverAnimations.size() - 1; i >= 0; i--) {
1051 | final RecoverAnimation anim = recoverAnimations.get(i);
1052 | if (anim.viewHolder.getViewHolder().itemView == target) {
1053 | return anim;
1054 | }
1055 | }
1056 | return null;
1057 | }
1058 |
1059 | private void updateDxDy(MotionEvent ev, int directionFlags, int pointerIndex) {
1060 | final float x = ev.getX(pointerIndex);
1061 | final float y = ev.getY(pointerIndex);
1062 | // Calculate the distance moved
1063 | dX = x - initialTouchX;
1064 | dY = y - initialTouchY;
1065 | if ((directionFlags & LEFT) == 0) {
1066 | dX = Math.max(0, dX);
1067 | }
1068 | if ((directionFlags & RIGHT) == 0) {
1069 | dX = Math.min(0, dX);
1070 | }
1071 | if ((directionFlags & UP) == 0) {
1072 | dY = Math.max(0, dY);
1073 | }
1074 | if ((directionFlags & DOWN) == 0) {
1075 | dY = Math.min(0, dY);
1076 | }
1077 | }
1078 |
1079 | private int checkPreviousSwipeDirection(RecyclerView.ViewHolder viewHolder) {
1080 | final int originalMovementFlags = callback.getMovementFlags(recyclerView, viewHolder);
1081 | final int absoluteMovementFlags = callback.convertToAbsoluteDirection(originalMovementFlags,
1082 | ViewCompat.getLayoutDirection(recyclerView));
1083 | final int flags = (absoluteMovementFlags & ACTION_MODE_SWIPE_MASK) >> (ACTION_STATE_SWIPE
1084 | * DIRECTION_FLAG_COUNT);
1085 | if (flags == 0) {
1086 | return 0;
1087 | }
1088 | final int originalFlags = (originalMovementFlags & ACTION_MODE_SWIPE_MASK) >> (
1089 | ACTION_STATE_SWIPE
1090 | * DIRECTION_FLAG_COUNT);
1091 | int swipeDir;
1092 | if (Math.abs(dX) > Math.abs(dY)) {
1093 | if ((swipeDir = checkHorizontalSwipe(flags)) > 0) {
1094 | // if swipe dir is not in original flags, it should be the relative direction
1095 | if ((originalFlags & swipeDir) == 0) {
1096 | // convert to relative
1097 | return Callback.convertToRelativeDirection(swipeDir,
1098 | ViewCompat.getLayoutDirection(recyclerView));
1099 | }
1100 | return swipeDir;
1101 | }
1102 | if ((swipeDir = checkVerticalSwipe(flags)) > 0) {
1103 | return swipeDir;
1104 | }
1105 | } else {
1106 | if ((swipeDir = checkVerticalSwipe(flags)) > 0) {
1107 | return swipeDir;
1108 | }
1109 | if ((swipeDir = checkHorizontalSwipe(flags)) > 0) {
1110 | // if swipe dir is not in original flags, it should be the relative direction
1111 | if ((originalFlags & swipeDir) == 0) {
1112 | // convert to relative
1113 | return Callback.convertToRelativeDirection(swipeDir,
1114 | ViewCompat.getLayoutDirection(recyclerView));
1115 | }
1116 | return swipeDir;
1117 | }
1118 | }
1119 | return 0;
1120 | }
1121 |
1122 | private int checkHorizontalSwipe(int flags) {
1123 | if ((flags & (LEFT | RIGHT)) != 0) {
1124 | return dX > 0 ? RIGHT : LEFT;
1125 | }
1126 | return 0;
1127 | }
1128 |
1129 | private int checkVerticalSwipe(int flags) {
1130 | if ((flags & (UP | DOWN)) != 0) {
1131 | return dY > 0 ? DOWN : UP;
1132 | }
1133 | return 0;
1134 | }
1135 |
1136 | public void onSaveInstanceState(Bundle outState) {
1137 | outState.putSparseParcelableArray(OPENED_STATES, openedPositions);
1138 | }
1139 |
1140 | public void restoreInstanceState(Bundle savedInstanceState) {
1141 | openedPositions = savedInstanceState.getSparseParcelableArray(OPENED_STATES);
1142 | if (openedPositions == null) {
1143 | openedPositions = new SparseArray<>();
1144 | }
1145 | }
1146 |
1147 | /**
1148 | * Base Callback class that extends off of {@link ItemTouchHelper.Callback}
1149 | */
1150 | @SuppressWarnings("UnusedParameters")
1151 | public abstract static class Callback
1152 | extends ItemTouchHelper.Callback {
1153 |
1154 | @Override
1155 | public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
1156 | RecyclerView.ViewHolder target) {
1157 | // do not use
1158 | return false;
1159 | }
1160 |
1161 | @Override
1162 | public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
1163 | // do not use
1164 | }
1165 |
1166 | /**
1167 | * Convenience method to create movement flags.
1168 | *
1169 | * For instance, if you want to let your items be drag & dropped vertically and swiped
1170 | * left to be dismissed, you can call this method with:
1171 | * makeMovementFlags(UP | DOWN, LEFT);
1172 | *
1173 | * @param swipeFlags The directions in which the item can be swiped.
1174 | * @return Returns an integer composed of the given drag and swipe flags.
1175 | */
1176 | public static int makeMovementFlags(int swipeFlags) {
1177 | return makeFlag(ACTION_STATE_IDLE, swipeFlags) | makeFlag(ACTION_STATE_SWIPE, swipeFlags);
1178 | }
1179 |
1180 | final int getAbsMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
1181 | final int flags = getMovementFlags(recyclerView, viewHolder);
1182 | return convertToAbsoluteDirection(flags, ViewCompat.getLayoutDirection(recyclerView));
1183 | }
1184 |
1185 | /**
1186 | * Called when the ViewHolder is changed.
1187 | *
1188 | * If you override this method, you should call super.
1189 | *
1190 | * @param viewHolder The new ViewHolder that is being swiped. Might be null if
1191 | * it is cleared.
1192 | * @param actionState One of {@link SwipeOpenItemTouchHelper#ACTION_STATE_IDLE},
1193 | * {@link SwipeOpenItemTouchHelper#ACTION_STATE_SWIPE}
1194 | * @see #clearView(RecyclerView, SwipeOpenViewHolder)
1195 | */
1196 | public void onSelectedChanged(SwipeOpenViewHolder viewHolder, int actionState) {
1197 | if (viewHolder != null) {
1198 | getDefaultUIUtil().onSelected(viewHolder.getSwipeView());
1199 | }
1200 | }
1201 |
1202 | private void onDraw(Canvas c, RecyclerView parent, SwipeOpenViewHolder selected,
1203 | List recoverAnimationList, int actionState, float dX, float dY,
1204 | boolean isRtl) {
1205 | final int recoverAnimSize = recoverAnimationList.size();
1206 | for (int i = 0; i < recoverAnimSize; i++) {
1207 | final RecoverAnimation anim = recoverAnimationList.get(i);
1208 | anim.update();
1209 | final int count = c.save();
1210 | onChildDraw(c, parent, anim.viewHolder, anim.x, anim.y, false);
1211 | c.restoreToCount(count);
1212 | }
1213 | if (selected != null) {
1214 | final int count = c.save();
1215 | notifySwipeDirections(selected, isRtl, dX, dY);
1216 | onChildDraw(c, parent, selected, dX, dY, true);
1217 | c.restoreToCount(count);
1218 | }
1219 | }
1220 |
1221 | /**
1222 | * Notifies the SwipeOpenHolder when one of its hidden views has become visible.
1223 | *
1224 | * @param holder the holder
1225 | * @param isRtl if the layout is RTL or not
1226 | * @param dX the new dX of the swiped view
1227 | * @param dY the new dY of the swiped view
1228 | */
1229 | private void notifySwipeDirections(SwipeOpenViewHolder holder, boolean isRtl, float dX,
1230 | float dY) {
1231 | // check if we are about to start a swipe to open start or open end positions
1232 | View swipeView = holder.getSwipeView();
1233 | // 0 or negative translationX, heading to positive translationX
1234 | if (swipeView.getTranslationX() <= 0 && dX > 0) {
1235 | if (isRtl) {
1236 | holder.notifyEndOpen();
1237 | } else {
1238 | holder.notifyStartOpen();
1239 | }
1240 | // 0 or positive translationX, heading to negative translationX
1241 | } else if (swipeView.getTranslationX() >= 0 && dX < 0) {
1242 | if (isRtl) {
1243 | holder.notifyStartOpen();
1244 | } else {
1245 | holder.notifyEndOpen();
1246 | }
1247 | // 0 or positive translationY, heading to negative translationY
1248 | } else if (swipeView.getTranslationY() >= 0 && dY < 0) {
1249 | holder.notifyEndOpen();
1250 | } else if (swipeView.getTranslationY() <= 0 && dY > 0) {
1251 | holder.notifyStartOpen();
1252 | }
1253 | }
1254 |
1255 | private void onDrawOver(Canvas c, RecyclerView parent, SwipeOpenViewHolder selected,
1256 | List recoverAnimationList, int actionState, float dX, float dY) {
1257 | final int recoverAnimSize = recoverAnimationList.size();
1258 | boolean hasRunningAnimation = false;
1259 | for (int i = recoverAnimSize - 1; i >= 0; i--) {
1260 | final RecoverAnimation anim = recoverAnimationList.get(i);
1261 | if (anim.ended) {
1262 | recoverAnimationList.remove(i);
1263 | } else {
1264 | hasRunningAnimation = true;
1265 | }
1266 | }
1267 | if (hasRunningAnimation) {
1268 | parent.invalidate();
1269 | }
1270 | }
1271 |
1272 | public void clearView(RecyclerView recyclerView, SwipeOpenViewHolder viewHolder) {
1273 | getDefaultUIUtil().clearView(viewHolder.getSwipeView());
1274 | }
1275 |
1276 | public void onChildDraw(Canvas c, RecyclerView recyclerView, SwipeOpenViewHolder viewHolder,
1277 | float dX, float dY, boolean isCurrentlyActive) {
1278 | // handle the draw
1279 | getDefaultUIUtil().onDraw(c, recyclerView, viewHolder.getSwipeView(), dX, dY,
1280 | ACTION_STATE_SWIPE, isCurrentlyActive);
1281 | }
1282 |
1283 | public long getAnimationDuration(RecyclerView recyclerView, int animationType, float animateDx,
1284 | float animateDy) {
1285 | final RecyclerView.ItemAnimator itemAnimator = recyclerView.getItemAnimator();
1286 | if (itemAnimator == null) {
1287 | return DEFAULT_SWIPE_ANIMATION_DURATION;
1288 | } else {
1289 | return itemAnimator.getMoveDuration();
1290 | }
1291 | }
1292 | }
1293 |
1294 | /**
1295 | * Simple callback class that defines the swipe directions allowed and delegates everything else
1296 | * to the base class
1297 | */
1298 | @SuppressWarnings("UnusedParameters")
1299 | public static class SimpleCallback extends Callback {
1300 |
1301 | private int mDefaultSwipeDirs;
1302 |
1303 | public SimpleCallback(int swipeDirs) {
1304 | mDefaultSwipeDirs = swipeDirs;
1305 | }
1306 |
1307 | public void setDefaultSwipeDirs(int defaultSwipeDirs) {
1308 | mDefaultSwipeDirs = defaultSwipeDirs;
1309 | }
1310 |
1311 | public int getSwipeDirs(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
1312 | return mDefaultSwipeDirs;
1313 | }
1314 |
1315 | @Override
1316 | public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
1317 | return makeMovementFlags(getSwipeDirs(recyclerView, viewHolder));
1318 | }
1319 | }
1320 |
1321 | private static class RecoverAnimation implements Animator.AnimatorListener {
1322 |
1323 | final float startDx;
1324 |
1325 | final float startDy;
1326 |
1327 | final float targetX;
1328 |
1329 | final float targetY;
1330 |
1331 | final SwipeOpenViewHolder viewHolder;
1332 |
1333 | final int actionState;
1334 |
1335 | private final ValueAnimator valueAnimator;
1336 |
1337 | float x;
1338 |
1339 | float y;
1340 |
1341 | private boolean ended = false;
1342 |
1343 | private float fraction;
1344 |
1345 | public RecoverAnimation(SwipeOpenViewHolder viewHolder, int actionState, float startDx,
1346 | float startDy, float targetX, float targetY) {
1347 | this.actionState = actionState;
1348 | this.viewHolder = viewHolder;
1349 | this.startDx = startDx;
1350 | this.startDy = startDy;
1351 | this.targetX = targetX;
1352 | this.targetY = targetY;
1353 | valueAnimator = ValueAnimator.ofFloat(0, 1);
1354 | valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
1355 | @Override
1356 | public void onAnimationUpdate(ValueAnimator animation) {
1357 | setFraction(animation.getAnimatedFraction());
1358 | }
1359 | });
1360 | valueAnimator.setTarget(viewHolder.getViewHolder().itemView);
1361 | valueAnimator.addListener(this);
1362 | setFraction(0f);
1363 | }
1364 |
1365 | public void setDuration(long duration) {
1366 | valueAnimator.setDuration(duration);
1367 | }
1368 |
1369 | public void start() {
1370 | viewHolder.getViewHolder().setIsRecyclable(false);
1371 | valueAnimator.start();
1372 | }
1373 |
1374 | public void cancel() {
1375 | valueAnimator.cancel();
1376 | }
1377 |
1378 | public void setFraction(float fraction) {
1379 | this.fraction = fraction;
1380 | }
1381 |
1382 | /**
1383 | * We run updates on onDraw method but use the fraction from animator callback.
1384 | * This way, we can sync translate x/y values w/ the animators to avoid one-off frames.
1385 | */
1386 | public void update() {
1387 | if (startDx == targetX) {
1388 | x = viewHolder.getSwipeView().getTranslationX();
1389 | } else {
1390 | x = startDx + fraction * (targetX - startDx);
1391 | }
1392 | if (startDy == targetY) {
1393 | y = viewHolder.getSwipeView().getTranslationY();
1394 | } else {
1395 | y = startDy + fraction * (targetY - startDy);
1396 | }
1397 | }
1398 |
1399 | @Override
1400 | public void onAnimationStart(Animator animator) {
1401 |
1402 | }
1403 |
1404 | @Override
1405 | public void onAnimationEnd(Animator animator) {
1406 | if (!ended) {
1407 | viewHolder.getViewHolder().setIsRecyclable(true);
1408 | }
1409 | ended = true;
1410 | }
1411 |
1412 | @Override
1413 | public void onAnimationCancel(Animator animator) {
1414 | setFraction(1f); //make sure we recover the view's state.
1415 | }
1416 |
1417 | @Override
1418 | public void onAnimationRepeat(Animator animator) {
1419 |
1420 | }
1421 | }
1422 |
1423 | /**
1424 | * Enum for saving the opened state of the view holders
1425 | */
1426 | private enum SavedOpenState implements Parcelable {
1427 | START_OPEN, END_OPEN;
1428 |
1429 | @Override
1430 | public int describeContents() {
1431 | return 0;
1432 | }
1433 |
1434 | @Override
1435 | public void writeToParcel(Parcel dest, int flags) {
1436 | dest.writeInt(ordinal());
1437 | }
1438 |
1439 | public static final Creator CREATOR =
1440 | new Creator() {
1441 | @Override
1442 | public SavedOpenState createFromParcel(Parcel source) {
1443 | return SavedOpenState.values()[source.readInt()];
1444 | }
1445 |
1446 | @Override
1447 | public SavedOpenState[] newArray(int size) {
1448 | return new SavedOpenState[size];
1449 | }
1450 | };
1451 |
1452 | }
1453 | }
--------------------------------------------------------------------------------
/library/src/main/java/easyadapter/dc/com/library/SwipeOpenViewHolder.java:
--------------------------------------------------------------------------------
1 | package easyadapter.dc.com.library;
2 |
3 | import android.support.annotation.NonNull;
4 | import android.support.v7.widget.RecyclerView;
5 | import android.view.View;
6 |
7 | /**
8 | * Interface for interacting with a swipe open ViewHolder.
9 | * ViewHolders that are to be swiped must implement this interface
10 | */
11 | public interface SwipeOpenViewHolder {
12 |
13 | /**
14 | * Returns the {@link View} that will be swiped opened and closed.
15 | *
16 | * @return a non-null view to swipe
17 | */
18 | @NonNull
19 | View getSwipeView();
20 |
21 | /**
22 | * Returns the {@link RecyclerView.ViewHolder} that contains the Swipe View
23 | *
24 | * @return the view holder
25 | */
26 | @NonNull
27 | RecyclerView.ViewHolder getViewHolder();
28 |
29 | /**
30 | * Size of the hidden view at the END of the SwipeOpenViewHolder.
31 | * This will be the view at the RIGHT/END of the holder when horizontal swiping is supported,
32 | * and will be BOTTOM/DOWN when vertical swiping is supported.
33 | *
34 | * @return the width (if horizontal swiping) or height (if vertical swiping) of the view to reveal,
35 | * Return 0 if you want to return to a closed position after every swipe in that direciton
36 | */
37 | float getEndHiddenViewSize();
38 |
39 | /**
40 | * Size of the hidden view at the START of the SwipeOpenViewHolder.
41 | * This will be the view at the LEFT/START of the holder when horizontal swiping is supported,
42 | * and will be TOP/UP when vertical swiping is supported.
43 | *
44 | * @return the width (if horizontal swiping) or height (if vertical swiping) of the view to reveal.
45 | * Return 0 if you want to return to a closed position after every swipe in that direciton
46 | */
47 | float getStartHiddenViewSize();
48 |
49 | /**
50 | * Notify the SwipeOpenHolder that the START view has become visible from a swipe.
51 | * Ex: This could be used to set a background color to the underlying view so that it matches your
52 | * hidden view during an over-swipe
53 | */
54 | void notifyStartOpen();
55 |
56 | /**
57 | * Notify the SwipeOpenHolder that the END View has become visible from a swipe
58 | * Ex: This could be used to set a background color to the underlying view so that it matches your
59 | * hidden view during an over-swipe
60 | */
61 | void notifyEndOpen();
62 |
63 | }
--------------------------------------------------------------------------------
/library/src/main/res/anim/down_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/library/src/main/res/anim/down_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/library/src/main/res/anim/up_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/library/src/main/res/anim/up_out.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
14 |
--------------------------------------------------------------------------------
/library/src/main/res/layout/layout_load_more.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
--------------------------------------------------------------------------------
/library/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | library
3 |
4 |
--------------------------------------------------------------------------------
/library/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app', ':library'
2 |
3 |
--------------------------------------------------------------------------------